mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
chore: merge main — keep both theme and isFavorite params in NappletBrowserActivity
Resolved conflicts from main adding isFavorite to NappletBrowserActivity.intent() and FavoriteAppLauncher.launchUrl() while our branch added the theme parameter. Both params are now present together. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198rKcuv32DEoUPpLqsBYbx
This commit is contained in:
@@ -89,8 +89,9 @@ object FavoriteAppLauncher {
|
||||
if (nightMask == Configuration.UI_MODE_NIGHT_YES) "DARK" else "LIGHT"
|
||||
}
|
||||
}
|
||||
val isFavorite = FavoriteAppsRegistry.isFavorite("url:$url")
|
||||
val intent =
|
||||
NappletBrowserActivity.intent(context, url, proxyPort, useTor, theme = theme).apply {
|
||||
NappletBrowserActivity.intent(context, url, proxyPort, useTor, theme = theme, isFavorite = isFavorite).apply {
|
||||
if (context !is Activity) addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.os.RemoteException
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletBroker
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity
|
||||
@@ -41,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse
|
||||
import com.vitorpamplona.amethyst.favorites.BrowserHistoryRegistry
|
||||
import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.napplet.gateways.AccountNappletGateways
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletIpc
|
||||
@@ -175,6 +177,21 @@ class NappletBrokerService : Service() {
|
||||
return true
|
||||
}
|
||||
|
||||
// The direct-WebView browser requests a favorite toggle for the current URL (main process only).
|
||||
if (msg.what == NappletIpc.MSG_TOGGLE_WEB_FAVORITE) {
|
||||
val data = msg.data ?: return true
|
||||
val url = data.getString(NappletIpc.KEY_FAVORITE_URL)?.takeIf { it.isNotBlank() } ?: return true
|
||||
val label = data.getString(NappletIpc.KEY_FAVORITE_LABEL).orEmpty().ifBlank { url }
|
||||
FavoriteAppsRegistry.init(applicationContext)
|
||||
val id = "url:$url"
|
||||
if (FavoriteAppsRegistry.isFavorite(id)) {
|
||||
FavoriteAppsRegistry.remove(id)
|
||||
} else {
|
||||
FavoriteAppsRegistry.add(FavoriteApp.WebUrl(url, label, System.currentTimeMillis()))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// The direct-WebView browser relays its per-host Tor choice; persist it (main process only).
|
||||
if (msg.what == NappletIpc.MSG_SET_WEB_TOR) {
|
||||
val data = msg.data ?: return true
|
||||
|
||||
+1
-1
@@ -342,7 +342,7 @@ class NotificationRelayService : Service() {
|
||||
.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.always_on_notif_title))
|
||||
.setContentText(contentText)
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
.setSmallIcon(R.drawable.amethyst_service)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
|
||||
+3
@@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource.SoftwareAppsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource.UrlFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.datasource.OnchainZapsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssembler
|
||||
@@ -116,6 +117,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val profile = UserProfileFilterAssembler(client)
|
||||
val hashtags = HashtagFilterAssembler(client)
|
||||
val geohashes = GeoHashFilterAssembler(client)
|
||||
val urls = UrlFilterAssembler(client)
|
||||
val relayFeed = RelayFeedFilterAssembler(client)
|
||||
val relayInfoNip66 = RelayInfoNip66FilterAssembler(client)
|
||||
val followPacks = FollowPackFeedFilterAssembler(client)
|
||||
@@ -209,6 +211,7 @@ class RelaySubscriptionsCoordinator(
|
||||
profile,
|
||||
hashtags,
|
||||
geohashes,
|
||||
urls,
|
||||
relayFeed,
|
||||
relayInfoNip66,
|
||||
chess,
|
||||
|
||||
@@ -51,8 +51,10 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.UriParser
|
||||
import java.net.URLDecoder
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -147,6 +149,18 @@ fun isNotificationRoute(uri: String) = uri.startsWith("notifications", true) ||
|
||||
|
||||
fun isHashtagRoute(uri: String) = uri.startsWith("hashtag?id=") || uri.startsWith("nostr:hashtag?id=")
|
||||
|
||||
fun isUrlRoute(uri: String) = uri.startsWith("url?id=") || uri.startsWith("nostr:url?id=")
|
||||
|
||||
fun urlRoute(uri: String): Route.Url? {
|
||||
val url =
|
||||
runCatching {
|
||||
val rawUrl = java.net.URI(uri.removePrefix("nostr:")).findParameterValue("id") ?: return null
|
||||
URLDecoder.decode(rawUrl, Charsets.UTF_8.name())
|
||||
}.getOrNull() ?: return null
|
||||
|
||||
return UrlId.toScopeOrNull(url)?.let { Route.Url(it) }
|
||||
}
|
||||
|
||||
fun isWalletConnectRoute(uri: String) = uri.startsWith("dlnwc?value=") || uri.startsWith("amethyst+walletconnect:dlnwc?value=") || uri.startsWith("amethyst+walletconnect://dlnwc?value=")
|
||||
|
||||
fun isMarmotGroupRoute(uri: String) = uri.startsWith("marmot:")
|
||||
@@ -164,6 +178,9 @@ fun uriToRoute(
|
||||
if (isHashtagRoute(uri)) {
|
||||
return Route.Hashtag(uri.removePrefix("nostr:").removePrefix("hashtag?id=").lowercase())
|
||||
}
|
||||
if (isUrlRoute(uri)) {
|
||||
return urlRoute(uri)
|
||||
}
|
||||
|
||||
val nip19 = Nip19Parser.uriToRoute(uri)?.entity
|
||||
if (nip19 != null) {
|
||||
|
||||
@@ -24,9 +24,7 @@ import android.graphics.drawable.Animatable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -77,9 +75,13 @@ fun GifVideoView(
|
||||
val borderModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier
|
||||
val context = LocalContext.current
|
||||
|
||||
// Share the static-image sizing policy so animated media obeys it too: in Crop
|
||||
// contexts (e.g. the multi-image gallery grid) the cell has a fixed width AND height,
|
||||
// so the content must fill and crop rather than impose its own aspect ratio, which
|
||||
// would overflow the cell and spill over neighbouring content like the reaction row.
|
||||
val containerModifier =
|
||||
(if (ratio != null) borderModifier.aspectRatio(ratio) else borderModifier)
|
||||
.fillMaxWidth()
|
||||
borderModifier
|
||||
.then(mediaSizingModifier(ratio, contentScale))
|
||||
.let { if (onDialog != null) it.clickable { onDialog() } else it }
|
||||
|
||||
Box(
|
||||
|
||||
@@ -30,6 +30,8 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
|
||||
@@ -39,11 +41,12 @@ fun LoadUrlPreview(
|
||||
urlText: String,
|
||||
callbackUri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav? = null,
|
||||
) {
|
||||
if (!accountViewModel.settings.showUrlPreview()) {
|
||||
ClickableUrl(urlText, url)
|
||||
} else {
|
||||
LoadUrlPreviewDirect(url, urlText, callbackUri, accountViewModel)
|
||||
LoadUrlPreviewDirect(url, urlText, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +56,7 @@ fun LoadUrlPreviewDirect(
|
||||
urlText: String,
|
||||
callbackUri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav? = null,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val urlPreviewState by
|
||||
@@ -72,7 +76,7 @@ fun LoadUrlPreviewDirect(
|
||||
) { state ->
|
||||
when (state) {
|
||||
is UrlPreviewState.Loaded -> {
|
||||
RenderLoaded(state, url, callbackUri, accountViewModel)
|
||||
RenderLoaded(state, url, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is UrlPreviewState.Loading -> {
|
||||
@@ -94,6 +98,7 @@ fun RenderLoaded(
|
||||
url: String,
|
||||
callbackUri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav? = null,
|
||||
) {
|
||||
when {
|
||||
state.previewInfo.mimeType.startsWith("image") -> {
|
||||
@@ -130,7 +135,14 @@ fun RenderLoaded(
|
||||
}
|
||||
|
||||
else -> {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
UrlPreviewCard(
|
||||
url,
|
||||
state.previewInfo,
|
||||
onUrlComments =
|
||||
nav?.let {
|
||||
{ it.nav(Route.Url(url)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,7 +549,7 @@ private fun RenderWordWithPreview(
|
||||
is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is VideoSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is PdfSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel)
|
||||
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel, nav)
|
||||
is NowhereLinkSegment -> NowhereLinkCard(word)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText, accountViewModel)
|
||||
|
||||
@@ -52,6 +52,7 @@ import kotlinx.coroutines.launch
|
||||
fun UrlPreviewCard(
|
||||
url: String,
|
||||
previewInfo: UrlInfoItem,
|
||||
onUrlComments: (() -> Unit)? = null,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val popupExpanded =
|
||||
@@ -76,6 +77,15 @@ fun UrlPreviewCard(
|
||||
popupExpanded.value = false
|
||||
}
|
||||
}
|
||||
onUrlComments?.let {
|
||||
M3ActionRow(
|
||||
icon = MaterialSymbols.Link,
|
||||
text = stringRes(R.string.kind_comments),
|
||||
) {
|
||||
popupExpanded.value = false
|
||||
it()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ class MarkdownMediaRenderer(
|
||||
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
|
||||
} else {
|
||||
renderInlineFullWidth(richTextStringBuilder) {
|
||||
LoadUrlPreview(uri, title ?: uri, callbackUri, accountViewModel)
|
||||
LoadUrlPreview(uri, title ?: uri, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.ShortsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.SoftwareAppsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.NewHlsVideoScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.AddCashuWalletScreen
|
||||
@@ -460,6 +462,7 @@ fun BuildNavigation(
|
||||
composableFromEndArgs<Route.ContactListUsers> { ContactListUsersScreen(it.noteId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Hashtag> { HashtagScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Url> { UrlScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RelayFeed> { RelayFeedScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.ChessGame> { ChessGameScreen(it.gameId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||
@@ -570,6 +573,19 @@ fun BuildNavigation(
|
||||
)
|
||||
}
|
||||
|
||||
composableFromBottomArgs<Route.UrlPost> {
|
||||
UrlPostScreen(
|
||||
url = it.url,
|
||||
message = it.message,
|
||||
attachment = it.attachment,
|
||||
replyId = it.replyTo,
|
||||
quoteId = it.quote,
|
||||
draftId = it.draft,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
composableFromBottomArgs<Route.GenericCommentPost> {
|
||||
ReplyCommentPostScreen(
|
||||
replyId = it.replyTo,
|
||||
|
||||
+5
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import java.net.URLEncoder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -144,6 +145,10 @@ class BouncingIntentNav(
|
||||
NOSTR_URI_PREFIX + "hashtag?id=" + route.hashtag
|
||||
}
|
||||
|
||||
is Route.Url -> {
|
||||
NOSTR_URI_PREFIX + "url?id=" + URLEncoder.encode(route.url, Charsets.UTF_8.name())
|
||||
}
|
||||
|
||||
is Route.LiveActivityChannel -> {
|
||||
NOSTR_URI_PREFIX + NAddress.create(route.kind, route.pubKeyHex, route.dTag, null)
|
||||
}
|
||||
|
||||
@@ -477,6 +477,10 @@ sealed class Route {
|
||||
val geohash: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class Url(
|
||||
val url: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class ChessGame(
|
||||
val gameId: String,
|
||||
) : Route()
|
||||
@@ -691,6 +695,16 @@ sealed class Route {
|
||||
val draft: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class UrlPost(
|
||||
val url: String? = null,
|
||||
val message: String? = null,
|
||||
val attachment: String? = null,
|
||||
val replyTo: String? = null,
|
||||
val quote: String? = null,
|
||||
val draft: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class GenericCommentPost(
|
||||
val message: String? = null,
|
||||
|
||||
+11
-5
@@ -84,7 +84,7 @@ fun PreviewUrl(
|
||||
}
|
||||
|
||||
else -> {
|
||||
MyLoadUrlPreviewDirect(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirect(myUrlPreview, myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ fun PreviewUrl(
|
||||
}
|
||||
|
||||
RichTextParser.isUrlWithoutScheme(myUrlPreview) -> {
|
||||
MyLoadUrlPreviewDirect("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirect("https://$myUrlPreview", myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ fun PreviewUrlFillWidth(
|
||||
}
|
||||
|
||||
else -> {
|
||||
MyLoadUrlPreviewDirectFillWidth(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirectFillWidth(myUrlPreview, myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
|
||||
@@ -154,7 +154,7 @@ fun PreviewUrlFillWidth(
|
||||
nav = nav,
|
||||
)
|
||||
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
|
||||
MyLoadUrlPreviewDirectFillWidth("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirectFillWidth("https://$myUrlPreview", myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ private fun MyLoadUrlPreviewDirect(
|
||||
url: String,
|
||||
urlText: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val urlPreviewState by
|
||||
@@ -268,6 +269,7 @@ private fun MyLoadUrlPreviewDirectFillWidth(
|
||||
url: String,
|
||||
urlText: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val urlPreviewState by
|
||||
@@ -304,7 +306,11 @@ private fun MyLoadUrlPreviewDirectFillWidth(
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
UrlPreviewCard(url, previewInfo = state.previewInfo)
|
||||
UrlPreviewCard(
|
||||
url,
|
||||
previewInfo = state.previewInfo,
|
||||
onUrlComments = { nav.nav(com.vitorpamplona.amethyst.ui.navigation.routes.Route.Url(url)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -30,7 +30,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -68,7 +68,7 @@ fun DisplayExternalId(
|
||||
}
|
||||
|
||||
is UrlId -> {
|
||||
DisplayUrlExternalId(externalId)
|
||||
DisplayUrlExternalId(externalId, nav)
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -78,13 +78,15 @@ fun DisplayExternalId(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayUrlExternalId(externalId: UrlId) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
fun DisplayUrlExternalId(
|
||||
externalId: UrlId,
|
||||
nav: INav,
|
||||
) {
|
||||
DisplayExternalIdChip(
|
||||
symbol = MaterialSymbols.Link,
|
||||
contentDescription = stringRes(id = R.string.external_url_scope),
|
||||
label = externalId.url,
|
||||
linkInteractionListener = { runCatching { uriHandler.openUri(externalId.url) } },
|
||||
linkInteractionListener = { nav.nav(Route.Url(externalId.url)) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
-10
@@ -195,12 +195,6 @@ private fun BrowserLauncher(
|
||||
onValueChange = ::onValueChange,
|
||||
onClear = { field = TextFieldValue("") },
|
||||
onOpen = { open(field.text) },
|
||||
onFavorite = {
|
||||
val url = OmniboxInput.resolve(field.text)?.url ?: return@OmniBar
|
||||
FavoriteAppsRegistry.add(
|
||||
FavoriteApp.WebUrl(url = url, label = hostOf(url), addedAt = System.currentTimeMillis()),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -264,7 +258,6 @@ private fun OmniBar(
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
onOpen: () -> Unit,
|
||||
onFavorite: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
@@ -309,9 +302,6 @@ private fun OmniBar(
|
||||
),
|
||||
)
|
||||
if (field.text.isNotBlank()) {
|
||||
IconButton(onClick = onFavorite) {
|
||||
Icon(MaterialSymbols.StarBorder, contentDescription = stringResource(R.string.favorite_app_add))
|
||||
}
|
||||
IconButton(onClick = onOpen) {
|
||||
Icon(MaterialSymbols.AutoMirrored.ArrowForward, contentDescription = stringResource(R.string.browser_go))
|
||||
}
|
||||
|
||||
+16
-1
@@ -44,9 +44,12 @@ import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.napplet.WebUrlNetworkRegistry
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.favoriteIds
|
||||
@@ -104,6 +107,9 @@ private fun EmbeddedFavoriteTab(
|
||||
// can opt one out and it must stick). Only meaningful when Tor is actually available.
|
||||
var torOn by remember { mutableStateOf(proxyAvailable && WebUrlNetworkRegistry.useTor(url)) }
|
||||
|
||||
val apps by FavoriteAppsRegistry.favorites.collectAsStateWithLifecycle()
|
||||
val isFavorite = remember(apps, currentUrl) { apps.any { it is FavoriteApp.WebUrl && it.url == currentUrl } }
|
||||
|
||||
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
|
||||
|
||||
val controller =
|
||||
@@ -121,7 +127,7 @@ private fun EmbeddedFavoriteTab(
|
||||
|
||||
// Rebuilt only when a displayed value changes, so the tab layer isn't recomposed every frame.
|
||||
val chrome =
|
||||
remember(currentUrl, torOn, proxyAvailable) {
|
||||
remember(currentUrl, torOn, proxyAvailable, isFavorite) {
|
||||
EmbeddedTabChrome(
|
||||
title = hostLabel(currentUrl),
|
||||
isSandbox = false,
|
||||
@@ -133,6 +139,15 @@ private fun EmbeddedFavoriteTab(
|
||||
controller.setTor(torOn)
|
||||
WebUrlNetworkRegistry.set(url, torOn)
|
||||
},
|
||||
isFavorite = isFavorite,
|
||||
onFavorite = {
|
||||
val favId = "url:$currentUrl"
|
||||
if (FavoriteAppsRegistry.isFavorite(favId)) {
|
||||
FavoriteAppsRegistry.remove(favId)
|
||||
} else {
|
||||
FavoriteAppsRegistry.add(FavoriteApp.WebUrl(currentUrl, hostLabel(currentUrl), System.currentTimeMillis()))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
// Publish the top-sheet controls to the tab layer (which draws them over the z-below surface). In a
|
||||
|
||||
+41
-36
@@ -128,43 +128,8 @@ fun BottomConsoleSheet(
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
val dimColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
logs.forEach { entry ->
|
||||
val levelColor = consoleLevelColor(entry.level)
|
||||
val srcShort =
|
||||
entry.source
|
||||
.substringAfterLast("/")
|
||||
.substringAfterLast("\\")
|
||||
.let { if (it.isBlank()) entry.source.takeLast(20) else it }
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(vertical = 1.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
consoleLevelChar(entry.level),
|
||||
color = levelColor,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.width(14.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = levelColor)) {
|
||||
append(entry.message)
|
||||
}
|
||||
if (srcShort.isNotBlank()) {
|
||||
withStyle(SpanStyle(color = dimColor)) {
|
||||
append(" $srcShort:${entry.lineNumber}")
|
||||
}
|
||||
}
|
||||
},
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
ConsoleLogRow(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,6 +168,46 @@ fun BottomConsoleSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConsoleLogRow(entry: ConsoleLogEntry) {
|
||||
val levelColor = consoleLevelColor(entry.level)
|
||||
val dimColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val srcShort =
|
||||
entry.source
|
||||
.substringAfterLast("/")
|
||||
.substringAfterLast("\\")
|
||||
.let { if (it.isBlank()) entry.source.takeLast(20) else it }
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(vertical = 1.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
consoleLevelChar(entry.level),
|
||||
color = levelColor,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.width(14.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = levelColor)) {
|
||||
append(entry.message)
|
||||
}
|
||||
if (srcShort.isNotBlank()) {
|
||||
withStyle(SpanStyle(color = dimColor)) {
|
||||
append(" $srcShort:${entry.lineNumber}")
|
||||
}
|
||||
}
|
||||
},
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun consoleLevelColor(level: String): Color {
|
||||
val warningAmber = if (isSystemInDarkTheme()) Color(0xFFFFB74D) else Color(0xFFE65100)
|
||||
|
||||
+4
@@ -37,4 +37,8 @@ data class EmbeddedTabChrome(
|
||||
val onToggleTor: () -> Unit = {},
|
||||
/** The "what it can access" sheet, for sandboxed napplets/nsites; null for a plain web client. */
|
||||
val onInfo: (() -> Unit)? = null,
|
||||
/** Whether the current URL/app is already saved as a favorite. */
|
||||
val isFavorite: Boolean = false,
|
||||
/** Toggles the current site/app in the favorites registry; null when not applicable. */
|
||||
val onFavorite: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
+9
@@ -131,6 +131,15 @@ fun TopControlSheet(
|
||||
onExpandedChange(false)
|
||||
chrome.onOpenFull()
|
||||
}
|
||||
chrome.onFavorite?.let { toggleFavorite ->
|
||||
SheetItem(
|
||||
if (chrome.isFavorite) MaterialSymbols.Star else MaterialSymbols.StarBorder,
|
||||
stringResource(if (chrome.isFavorite) R.string.favorite_app_remove else R.string.favorite_app_add),
|
||||
) {
|
||||
onExpandedChange(false)
|
||||
toggleFavorite()
|
||||
}
|
||||
}
|
||||
onConsole?.let { showConsole ->
|
||||
SheetItem(
|
||||
MaterialSymbols.Code,
|
||||
|
||||
+16
-2
@@ -53,9 +53,11 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletEmbedContract
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletHostContract
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
@@ -125,6 +127,9 @@ private fun EmbeddedNappletTab(
|
||||
var canGoBack by remember { mutableStateOf(false) }
|
||||
var showAccess by remember { mutableStateOf(false) }
|
||||
|
||||
val apps by FavoriteAppsRegistry.favorites.collectAsStateWithLifecycle()
|
||||
val isFavorite = remember(apps, coordinate) { apps.any { it.id == "nostr:$coordinate" } }
|
||||
|
||||
val controller =
|
||||
remember(id) {
|
||||
EmbeddedTabFactory.acquireNapplet(context, coordinate, params, backgroundColor)
|
||||
@@ -138,15 +143,24 @@ private fun EmbeddedNappletTab(
|
||||
}
|
||||
}
|
||||
|
||||
// Stable per app (title/coordinate don't change), so the tab layer isn't recomposed every frame.
|
||||
// Stable per app (title/coordinate/isFavorite don't change often), so the tab layer isn't recomposed every frame.
|
||||
val chrome =
|
||||
remember(title, coordinate) {
|
||||
remember(title, coordinate, isFavorite) {
|
||||
EmbeddedTabChrome(
|
||||
title = title.ifBlank { coordinate },
|
||||
isSandbox = true,
|
||||
onReload = { controller.reload() },
|
||||
onOpenFull = { FavoriteAppLauncher.launch(context, FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis())) },
|
||||
onInfo = { showAccess = true },
|
||||
isFavorite = isFavorite,
|
||||
onFavorite = {
|
||||
val favId = "nostr:$coordinate"
|
||||
if (FavoriteAppsRegistry.isFavorite(favId)) {
|
||||
FavoriteAppsRegistry.remove(favId)
|
||||
} else {
|
||||
FavoriteAppsRegistry.add(FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis()))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
// Publish the top-sheet controls to the tab layer (drawn over the z-below surface). In a SideEffect
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url
|
||||
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
|
||||
@Composable
|
||||
fun NewUrlPostButton(
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
nav.nav(Route.UrlPost(url))
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_compose, 1),
|
||||
contentDescription = stringRes(id = R.string.new_community_note),
|
||||
modifier = Size26Modifier,
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun UrlPostScreen(
|
||||
url: String? = null,
|
||||
message: String? = null,
|
||||
attachment: String? = null,
|
||||
replyId: HexKey? = null,
|
||||
quoteId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
val postViewModel: CommentPostViewModel = viewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
url?.let {
|
||||
UrlId.toScopeOrNull(it)?.let { normalizedUrl ->
|
||||
postViewModel.newPostFor(UrlId(normalizedUrl))
|
||||
}
|
||||
}
|
||||
replyId?.let { accountViewModel.getNoteIfExists(it) }?.let {
|
||||
postViewModel.reply(it)
|
||||
}
|
||||
draftId?.let { accountViewModel.getNoteIfExists(it) }?.let {
|
||||
postViewModel.editFromDraft(it)
|
||||
}
|
||||
quoteId?.let { accountViewModel.getNoteIfExists(it) }?.let {
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
val mediaType = context.contentResolver.getType(it)
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GenericCommentPostScreen(postViewModel, accountViewModel, nav)
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal.UrlFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource.UrlFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
@Composable
|
||||
fun UrlScreen(
|
||||
route: Route.Url,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val url = UrlId.toScopeOrNull(route.url) ?: return
|
||||
|
||||
PrepareViewModelsUrlScreen(url, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
fun PrepareViewModelsUrlScreen(
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val urlFeedViewModel: UrlFeedViewModel =
|
||||
viewModel(
|
||||
key = url + "UrlFeedViewModel",
|
||||
factory =
|
||||
UrlFeedViewModel.Factory(
|
||||
url,
|
||||
accountViewModel.account.followOutboxesOrProxy.flow.value,
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
UrlScreen(url, urlFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UrlScreen(
|
||||
url: String,
|
||||
feedViewModel: UrlFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(feedViewModel)
|
||||
UrlFilterAssemblerSubscription(url, accountViewModel)
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
TopBarExtensibleWithBackButton(
|
||||
title = {
|
||||
DisplayUrlHeader(url, Modifier.weight(1f))
|
||||
},
|
||||
popBack = nav::popBack,
|
||||
)
|
||||
},
|
||||
floatingButton = {
|
||||
FabBottomBarPadded(nav) {
|
||||
NewUrlPostButton(url, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayUrlHeader(
|
||||
url: String,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
url,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = modifier,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
class UrlFeedFilter(
|
||||
url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
val cache: LocalCache,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
val normalizedUrl = UrlId.toScopeOrNull(url).orEmpty()
|
||||
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + normalizedUrl
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val notes =
|
||||
cache.notes.filterIntoSet { _, it ->
|
||||
acceptableEvent(it, normalizedUrl)
|
||||
}
|
||||
|
||||
return sort(notes)
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = newItems.filterTo(HashSet()) { acceptableEvent(it, normalizedUrl) }
|
||||
|
||||
fun acceptableEvent(
|
||||
it: Note,
|
||||
url: String,
|
||||
): Boolean =
|
||||
acceptableViaScope(it.event, url) &&
|
||||
!it.isHiddenFor(account.hiddenUsers.flow.value) &&
|
||||
account.isAcceptable(it)
|
||||
|
||||
fun acceptableViaScope(
|
||||
event: com.vitorpamplona.quartz.nip01Core.core.Event?,
|
||||
url: String,
|
||||
): Boolean = UrlId.toScopeOrNull(url)?.let { event is CommentEvent && event.isTaggedScope(it, UrlId::match) } ?: false
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedByDefaultFeedOrder()
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@Stable
|
||||
class UrlFeedViewModel(
|
||||
val url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) : AndroidFeedViewModel(
|
||||
UrlFeedFilter(url, relays, account, LocalCache),
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class Factory(
|
||||
val url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = UrlFeedViewModel(url, relays, account) as T
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
fun filterPostsByUrl(
|
||||
url: String,
|
||||
relays: Set<NormalizedRelayUrl>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val normalizedUrl = UrlId.toScopeOrNull(url) ?: return emptyList()
|
||||
val urlScopeMap = mapOf("I" to listOf(normalizedUrl))
|
||||
|
||||
return relays.map { relay ->
|
||||
val since = since?.get(relay)?.time
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
tags = urlScopeMap,
|
||||
kinds = CommentKinds,
|
||||
limit = 100,
|
||||
since = since,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
class UrlFeedFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<UrlQueryState>,
|
||||
) : PerUniqueIdEoseManager<UrlQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: UrlQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> = filterPostsByUrl(key.normalizedUrl, key.relays, since)
|
||||
|
||||
override fun id(key: UrlQueryState) = key.normalizedUrl
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
class UrlQueryState(
|
||||
val url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
val normalizedUrl = UrlId.toScopeOrNull(url).orEmpty()
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UrlFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<UrlQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
UrlFeedFilterSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||
|
||||
override fun destroy() = group.forEach { it.destroy() }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
fun UrlFilterAssemblerSubscription(
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val state =
|
||||
remember(url) {
|
||||
UrlQueryState(url, accountViewModel.account.followOutboxesOrProxy.flow.value)
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().urls)
|
||||
}
|
||||
+15
-7
@@ -1234,17 +1234,25 @@ private fun MintPicker(
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
mints.forEach { m ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(m) },
|
||||
onClick = {
|
||||
onPick(m)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
MintPickerItem(m) {
|
||||
onPick(m)
|
||||
expanded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MintPickerItem(
|
||||
mint: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(mint) },
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Send LN (melt)
|
||||
// ============================================================================
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
~ Status-bar icon for the always-on relay-connection service.
|
||||
~
|
||||
~ Notification small icons are rendered by the system as flat, alpha-only
|
||||
~ silhouettes, so the regular `amethyst` gem becomes an identical solid gem
|
||||
~ for both real notifications and this persistent service. To stop the
|
||||
~ service from reading as a new notification, this variant draws the gem as
|
||||
~ a hollow outline — same brand shape, clearly different at a glance.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="22"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M370.18,326.86C369.91,326.04 307.3,176.97 290.09,135.63c-20.96,0 -65.23,0 -96.46,0 -9.17,22.67 -32.82,80.44 -51.8,126.98 10.4,24.81 19.74,47.1 26.95,64.29l39.28,0a2.68,2.68 135,0 0,2.51 -3.15c-0.34,-7.93 -21.93,-36.76 -15.49,-67.94 1.72,-6.71 3.03,-13.53 4.91,-20.21a235.88,235.88 135,0 1,18.04 -46.7c1.19,-2.27 1.74,-3.29 4.5,-1.57 5.86,3.65 12.41,3.44 18.94,2.97 5.62,-1.22 8.36,-3.92 18.67,-1.18 2.04,0 4.03,0 6.11,0.19 4.95,0.39 9.6,1.57 12.57,6.05 3.25,4.9 3.77,10.44 3.25,16.1 -0.79,8.47 -0.23,16.36 6.94,22.33a77.19,77.19 0,0 0,8.05 5.37c3.56,2.28 7.85,3.27 10.9,6.44 1.8,1.87 2.86,3.85 2.08,6.51 -0.78,2.66 -2.86,3.91 -5.6,4.2 -6.92,0.75 -13.69,-0.66 -20.49,-1.48 -0.89,-0.11 -1.76,-0.17 -2.68,-0.23a15.39,15.39 0,0 1,-5.11 0.17l-1.6,0a35.83,35.83 0,0 0,-7.92 1.89c-6.41,2.34 -12.25,4.91 -19.82,4.78a5.8,5.8 0,0 0,-2.75 1.19c-5.93,5.27 -9.5,11.93 -6.54,21.66 8.13,22.72 29,77.53 37.96,101.18 -0.25,-16.02 -0.32,-44.71 -0.38,-58.64 35.8,0.05 90.7,0.03 95.08,0.03z" />
|
||||
</vector>
|
||||
@@ -276,6 +276,7 @@
|
||||
<string name="unblock">Odblokuj</string>
|
||||
<string name="copy_user_id">Kopiuj ID użytkownika</string>
|
||||
<string name="unblock_user">Odblokuj użytkownika</string>
|
||||
<string name="user_is_blocked_hidden">Ten użytkownik jest zablokowany/uciszony. Ich posty są ukryte.</string>
|
||||
<string name="npub_hex_username">"npub, nazwa użytkownika, tekst"</string>
|
||||
<string name="clear">Wyczyść</string>
|
||||
<string name="app_logo">Logo aplikacji</string>
|
||||
@@ -588,6 +589,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="profile_app_recommendations_title">Polecane aplikacje</string>
|
||||
<string name="profile_app_recommendations_description">Wybierz aplikacje Nostr, które chcesz publicznie polecić. Twoje rekomendacje pojawią się na Twoim profilu i pomogą innym użytkownikom znaleźć aplikacje z treściami, których ten klient nie może otworzyć.</string>
|
||||
<string name="profile_app_recommendations_empty">Nie znaleziono jeszcze żadnych aplikacji. Aplikacje będą się tu pojawiać w miarę ich wykrywania na Twoich transmiterach.</string>
|
||||
<string name="profile_app_recommendations_search">Szukaj aplikacji po nazwie</string>
|
||||
<string name="profile_app_recommendations_search_empty">Brak aplikacji spełniających kryteria wyszukiwania.</string>
|
||||
<string name="profile_app_recommendations_filter_empty">Żadna rekomendowana aplikacja nie pasuje do tego filtra.</string>
|
||||
<string name="app_definition_untitled">Aplikacja bez nazwy</string>
|
||||
<string name="app_definition_no_supported_kinds">Nie podaje, jakie treści obsługuje</string>
|
||||
<string name="app_definition_recommend">Poleć</string>
|
||||
@@ -616,6 +620,8 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="workout_sets">Zestawy</string>
|
||||
<string name="workout_reps">Powtórzenia</string>
|
||||
<string name="workout_weight">Waga</string>
|
||||
<string name="workout_exercises">Ćwiczenia</string>
|
||||
<string name="workout_volume">Objętość</string>
|
||||
<string name="workout_notes">Uwagi</string>
|
||||
<string name="workout_hours">Godziny</string>
|
||||
<string name="workout_minutes">Minuty</string>
|
||||
@@ -636,10 +642,103 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="exercise_meditation">Medytacja</string>
|
||||
<string name="exercise_diet">Dieta</string>
|
||||
<string name="exercise_fasting">Post</string>
|
||||
<string name="exercise_circuit">Układ</string>
|
||||
<string name="exercise_emom">EMOM</string>
|
||||
<string name="exercise_amrap">AMRAP</string>
|
||||
<string name="software_apps">Aplikacje</string>
|
||||
<string name="route_software_apps">Aplikacje</string>
|
||||
<string name="napplets">nApplets</string>
|
||||
<string name="nsites">nSites</string>
|
||||
<string name="nsite_none_found">Nie znaleziono żadnych witryn nSite.</string>
|
||||
<string name="browser">Przeglądarka</string>
|
||||
<string name="browser_address_hint">Szukaj lub wprowadź adres</string>
|
||||
<string name="browser_reload">Odśwież</string>
|
||||
<string name="browser_console_title_short">Konsola</string>
|
||||
<string name="browser_console_title">Konsola (%1$d)</string>
|
||||
<string name="browser_console_clear">Wyczyść</string>
|
||||
<string name="browser_tor_on">Wczytywanie przez Tor. Kliknij, aby użyć otwartej sieci.</string>
|
||||
<string name="browser_tor_off">Pobieranie przez otwartą sieć. Kliknij, aby skorzystać z sieci Tor.</string>
|
||||
<string name="browser_unsupported">Przeglądarka w aplikacji wymaga Androida 11 lub nowszego.</string>
|
||||
<string name="embedded_tab_load_failed">Nie można załadować tej aplikacji.</string>
|
||||
<string name="browser_go">Otwórz</string>
|
||||
<string name="browser_clear">Wyczyść</string>
|
||||
<string name="browser_favorites">Ulubione</string>
|
||||
<string name="browser_recent_options">Opcje</string>
|
||||
<string name="browser_recent_remove">Usuń z historii</string>
|
||||
<string name="favorite_apps">Ulubione aplikacje</string>
|
||||
<string name="favorite_apps_empty">Brak ulubionych aplikacji. Otwórz klienta lub witrynę internetową i kliknij gwiazdkę, aby przypiąć ją tutaj.</string>
|
||||
<string name="favorite_app_add">Dodaj do ulubionych</string>
|
||||
<string name="favorite_app_remove">Usuń z ulubionych</string>
|
||||
<string name="favorite_app_open_window">Otwórz w osobnym oknie</string>
|
||||
<string name="favorite_app_access_title">Do czego ta aplikacja ma dostęp</string>
|
||||
<string name="favorite_app_access_static">Działa w środowisku izolowanym i nie ma żadnego specjalnego dostępu do Twojego konta.</string>
|
||||
<string name="favorite_app_access_show">Do czego ma dostęp</string>
|
||||
<string name="favorite_app_network_tor">Przesyłanie danych przez Tor.</string>
|
||||
<string name="favorite_app_network_open">Wczytuje się z otwartej sieci.</string>
|
||||
<string name="favorite_app_unavailable">Ta aplikacja nie została jeszcze załadowana. Otwórz ją z karty lub spróbuj ponownie za chwilę.</string>
|
||||
<string name="favorite_notice_published">Opublikowane do transmiterów</string>
|
||||
<string name="favorite_notice_uploaded">Wgrano plik</string>
|
||||
<string name="favorite_notice_paid">Dokonano płatności</string>
|
||||
<string name="favorite_app_still_loading">Ta aplikacja nie została jeszcze załadowana. Spróbuj ponownie za chwilę.</string>
|
||||
<string name="favorite_app_recent">Najnowsze</string>
|
||||
<string name="napplet_permissions">uprawnienia nApplet</string>
|
||||
<string name="napplet_manage_permissions">Zarządzaj uprawnieniami</string>
|
||||
<string name="napplet_permissions_empty">Brak uprawnień nApplet</string>
|
||||
<string name="napplet_permissions_empty_subtitle">Uprawnienia, które przyznasz dla nApplets pojawią się tutaj.</string>
|
||||
<string name="napplet_permissions_forget">Zapomnij o tym nApplet</string>
|
||||
<string name="napplet_permissions_blocked">Zablokowane</string>
|
||||
<string name="napplet_permissions_revoke">Odwołaj</string>
|
||||
<string name="napplet_untitled">nApplet bez tytułu</string>
|
||||
<string name="napplet_none_found">Nie znaleziono jeszcze żadnych nApplets.</string>
|
||||
<string name="napplet_fallback_title">nApplet %1$s…</string>
|
||||
<!-- Napplet sandbox chrome (host top bar + live action notices) -->
|
||||
<!-- Napplet capability names -->
|
||||
<string name="napplet_cap_shell">Powłoka</string>
|
||||
<string name="napplet_cap_identity">Tożsamość</string>
|
||||
<string name="napplet_cap_keys">Operacje klawiatury</string>
|
||||
<string name="napplet_cap_relay">Transmitery</string>
|
||||
<string name="napplet_cap_storage">Schowek</string>
|
||||
<string name="napplet_cap_value">Płatności</string>
|
||||
<string name="napplet_cap_resource">Sieć</string>
|
||||
<string name="napplet_cap_upload">Wgrane</string>
|
||||
<string name="napplet_cap_shell_desc">Zapytaj jakie opcje są dostępne</string>
|
||||
<string name="napplet_cap_identity_desc">Odczytaj swój klucz publiczny</string>
|
||||
<string name="napplet_cap_keys_desc">Przypisz skróty klawiszowe</string>
|
||||
<string name="napplet_cap_relay_desc">Przeczytaj, podpisz i opublikuj swoje wydarzenia</string>
|
||||
<string name="napplet_cap_storage_desc">Własny prywatny schowek</string>
|
||||
<string name="napplet_cap_value_desc">Opłać faktury Lightning</string>
|
||||
<string name="napplet_cap_resource_desc">Pobierz zasoby sieci web i blossom</string>
|
||||
<string name="napplet_cap_upload_desc">Prześlij pliki na serwer multimediów</string>
|
||||
<string name="napplet_cap_theme">Motyw</string>
|
||||
<string name="napplet_cap_notify">Powiadomienia</string>
|
||||
<string name="napplet_cap_inc">Wiadomości</string>
|
||||
<string name="napplet_cap_theme_desc">Dopasuj kolory aplikacji</string>
|
||||
<string name="napplet_cap_notify_desc">Wyświetl powiadomienia</string>
|
||||
<string name="napplet_cap_inc_desc">Wymieniaj wiadomości z innymi nappletami</string>
|
||||
<!-- Napplet consent dialog -->
|
||||
<string name="napplet_consent_capability">Możliwość: %1$s</string>
|
||||
<string name="napplet_consent_allow_always">Zawsze zezwalaj</string>
|
||||
<string name="napplet_consent_allow_once">Zezwól jednorazowo</string>
|
||||
<string name="napplet_consent_deny_always">Nigdy nie zezwalaj</string>
|
||||
<string name="napplet_consent_not_now">Nie teraz</string>
|
||||
<string name="napplet_consent_get_pubkey">Ten nApplet chce odczytać Twój klucz publiczny.</string>
|
||||
<string name="napplet_consent_identity_read">Ten nApplet chce odczytać dane Twojego profilu i konta.</string>
|
||||
<string name="napplet_consent_publish">Ta aplikacja nApplet chce podpisać i opublikować zdarzenie typu %1$d w Twoim imieniu.</string>
|
||||
<string name="napplet_consent_publish_preview">Ta aplikacja nApplet chce podpisać i opublikować zdarzenie typu %1$d w Twoim imieniu:</string>
|
||||
<string name="napplet_consent_publish_encrypted">Ten nApplet chce wysłać zaszyfrowane wydarzenie jako Ty.</string>
|
||||
<string name="napplet_consent_query">Ten nApplet chce odczytywać wydarzenia z twoich transmiterów.</string>
|
||||
<string name="napplet_consent_storage">Ta aplikacja nApplet chce korzystać ze swojego prywatnego schowka.</string>
|
||||
<string name="napplet_consent_pay">Ta aplikacja nApplet chce zapłacić fakturę w systemie Lightning.</string>
|
||||
<string name="napplet_consent_resource">Ten nApplet chce pobrać zasób internetowy.</string>
|
||||
<string name="napplet_consent_upload">Ten nApplet chce przesłać plik na Twój serwer multimedialny.</string>
|
||||
<string name="napplet_consent_notify">Ten nApplet chce pokazywać Ci powiadomienia.</string>
|
||||
<string name="napplet_consent_sign">Ta witryna chce podpisać wydarzenie %1$d za pomocą Twojego klucza Nostr.</string>
|
||||
<plurals name="napplet_consent_pay_amount">
|
||||
<item quantity="one">Ta aplikacja nApplet chce opłacić fakturę Lightning w wysokości %1$d sat.</item>
|
||||
<item quantity="few">Ta aplikacja nApplet chce opłacić fakturę Lightning w wysokości %1$d satoszów.</item>
|
||||
<item quantity="many">Ta aplikacja nApplet chce opłacić fakturę Lightning w wysokości %1$d satoszy.</item>
|
||||
<item quantity="other">Ta aplikacja nApplet chce opłacić fakturę Lightning w wysokości %1$d satoszów.</item>
|
||||
</plurals>
|
||||
<string name="nip82_repository_label">Źródło: %1$s</string>
|
||||
<string name="nip82_version_label">v%1$s</string>
|
||||
<string name="nip82_download">Pobierz</string>
|
||||
@@ -1203,6 +1302,8 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="always_on_notif_setting_description">Utrzymuje stałe połączenie z transmiterami odbiorczymi, aby zapewnić natychmiastowe dostarczanie powiadomień. Wyświetla bieżące powiadomienia. Zużywa więcej baterii, ale gwarantuje, że nigdy nie przegapisz żadnej wiadomości.</string>
|
||||
<string name="split_notifications_setting_title">Podział powiadomień według obserwowanych</string>
|
||||
<string name="split_notifications_setting_description">Wyświetl dwie zakładki powiadomień — „Obserwowani” (osoby, które obserwujesz) oraz „Wszyscy”. Wskaźnik nieprzeczytanych powiadomień świeci się tylko w przypadku aktywności osób, które obserwujesz.</string>
|
||||
<string name="show_messages_in_notifications_setting_title">Pokaż wiadomości</string>
|
||||
<string name="show_messages_in_notifications_setting_description">W zakładce „Powiadomienia” znajdują się zarówno wiadomości bezpośrednie, jak i grupowe. Wyłącz tę opcję, aby wiadomości były wyświetlane wyłącznie w zakładce „Wiadomości”.</string>
|
||||
<string name="notification_tab_following">Obserwowani</string>
|
||||
<string name="notification_tab_everyone">Wszyscy</string>
|
||||
<string name="battery_optimization_title">Optymalizacja baterii aktywna</string>
|
||||
@@ -2223,9 +2324,13 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="git_repo_section_topics">Tematy</string>
|
||||
<string name="git_repo_personal_fork">Osobisty fork</string>
|
||||
<string name="nsite_title">Statyczna Witryna: %1$s</string>
|
||||
<string name="napplet_card_title">nApplet: %1$s</string>
|
||||
<string name="napplet_card_permissions">Uprawnienia:</string>
|
||||
<string name="profile_tab_apps">Aplikacje & Witryny</string>
|
||||
<string name="nsite_root_site">Strona główna</string>
|
||||
<string name="nsite_source">Źródło:</string>
|
||||
<string name="nsite_servers">Serwery:</string>
|
||||
<string name="nsite_open">Otwórz</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Potwierdzenie znacznika czasu</string>
|
||||
<string name="ots_info_description">Istnieje dowód na to, że ten post został podpisany przed %1$s. Dowód został opatrzony pieczęcią w łańcuchu bloków Bitcoin w tym dniu i czasie.</string>
|
||||
@@ -3015,6 +3120,83 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="nowhere_link_card_art">Sztuka na „Nowhere”</string>
|
||||
<string name="nowhere_link_card_forum">Forum „Nowhere”</string>
|
||||
<!-- Marmot (MLS) group chats -->
|
||||
<string name="marmot_groups_title">Grupy Marmot</string>
|
||||
<string name="marmot_create_group">Utwórz Grupę</string>
|
||||
<string name="marmot_create_group_title">Utwórz Grupę Marmot</string>
|
||||
<string name="marmot_create_group_footer">Zostanie utworzona nowa grupa MLS. Następnie możesz dodać członków.</string>
|
||||
<string name="marmot_group_default_name">Grupy Marmot</string>
|
||||
<string name="marmot_group_fallback_name">Grupa %1$s…</string>
|
||||
<string name="marmot_user_fallback_name">%1$s…</string>
|
||||
<string name="marmot_tab_known">Znane</string>
|
||||
<string name="marmot_tab_known_count">Znane (%1$d)</string>
|
||||
<string name="marmot_no_groups">Brak grup</string>
|
||||
<string name="marmot_no_groups_desc">Utwórz grupę lub zaakceptuj zaproszenie.</string>
|
||||
<string name="marmot_no_invitations">Brak zaproszeń</string>
|
||||
<string name="marmot_no_invitations_desc">Gdy ktoś doda cię do grupy, będzie to widoczne tutaj, dopóki nie odpowiesz.</string>
|
||||
<string name="marmot_no_messages_yet">Brak wiadomości</string>
|
||||
<plurals name="marmot_message_count">
|
||||
<item quantity="one">%1$d wiadomość</item>
|
||||
<item quantity="few">%1$d wiadomości</item>
|
||||
<item quantity="many">%1$d wiadomości</item>
|
||||
<item quantity="other">%1$d wiadomości</item>
|
||||
</plurals>
|
||||
<plurals name="marmot_member_count">
|
||||
<item quantity="one">%1$d członek</item>
|
||||
<item quantity="few">%1$d członków</item>
|
||||
<item quantity="many">%1$d członków</item>
|
||||
<item quantity="other">%1$d członków</item>
|
||||
</plurals>
|
||||
<string name="marmot_group_name">Nazwa grupy</string>
|
||||
<string name="marmot_group_name_placeholder">Wpisz nazwę grupy</string>
|
||||
<string name="marmot_group_description_placeholder">Dodaj opis grupy (opcjonalnie)</string>
|
||||
<string name="marmot_edit_info_footer">Zmiany zostaną zatwierdzone w grupie za pośrednictwem MLS i przekazane wszystkim członkom.</string>
|
||||
<string name="marmot_group_info_updated">Informacje o grupie zaktualizowane</string>
|
||||
<string name="marmot_failed_to_update">Nie udało się zaktualizować: %1$s</string>
|
||||
<string name="marmot_failed_to_create_group">Nie udało się utworzyć grupy: %1$s</string>
|
||||
<string name="marmot_keypackage_relays_not_set_title">Nie ustawiono transmiterów KeyPackage</string>
|
||||
<string name="marmot_keypackage_relays_not_set_message">Nie masz jeszcze listy transmiterów KeyPackage (MIP-00). Lista ta informuje innych użytkowników, gdzie opublikowano Twój KeyPackage, dzięki czemu mogą zapraszać Cię do czatów grupowych.\n\nUżyj do tego swoich obecnych transmiterów ze skrzynki wysyłkowej?</string>
|
||||
<string name="marmot_use_outbox_relays">Użyj transmiterów wychodzących</string>
|
||||
<string name="marmot_skip_for_now">Na razie pomiń</string>
|
||||
<string name="marmot_group_info_title">Informacje o grupie</string>
|
||||
<string name="marmot_edit_group_info">Edytuj informacje o grupie</string>
|
||||
<string name="marmot_leave_group">Opuść grupę</string>
|
||||
<string name="marmot_this_group">ta grupa</string>
|
||||
<string name="marmot_leave_group_confirm">Czy na pewno chcesz opuścić grupę „%1$s”? Nie będziesz już otrzymywać wiadomości od tej grupy.</string>
|
||||
<string name="marmot_failed_to_leave_group">Nie udało się opuścić grupy: %1$s</string>
|
||||
<string name="marmot_adding_user">Dodawanie %1$s…</string>
|
||||
<string name="marmot_failed_to_add_user">Nie udało się dodać %1$s: %2$s</string>
|
||||
<string name="marmot_unknown_error">nieznany błąd</string>
|
||||
<string name="marmot_keypackage_required">Aby można było dodać dany pakiet, musi on zostać opublikowany jako KeyPackage (kind:30443).</string>
|
||||
<string name="marmot_add_to_group">Dodaj do grupy</string>
|
||||
<string name="marmot_add_member">Dodaj członka</string>
|
||||
<string name="marmot_add_member_action">Dodaj Członka</string>
|
||||
<string name="marmot_add_member_placeholder">Nazwa, npub lub NIP-05</string>
|
||||
<string name="marmot_member_suffix_you"> (Ty)</string>
|
||||
<string name="marmot_member_suffix_admin"> - administrator</string>
|
||||
<string name="marmot_member_removed">Członek usunięty</string>
|
||||
<string name="marmot_failed_to_remove_member">Nie udało się usunąć członka: %1$s</string>
|
||||
<string name="marmot_remove_member">Usuń Członka</string>
|
||||
<string name="marmot_remove_member_confirm">Czy na pewno chcesz usunąć \"%1$s\" z tej grupy?</string>
|
||||
<string name="marmot_grant">Przyznaj</string>
|
||||
<string name="marmot_grant_admin_privileges">Przyznaj uprawnienia admina</string>
|
||||
<string name="marmot_grant_admin_title">Przyznaj uprawnienia administratora</string>
|
||||
<string name="marmot_grant_admin_confirm">Zrób \"%1$s\" administratorem tej grupy? Administratorzy mogą dodawać lub usuwać członków, zmieniać informacje o grupie i przyznawać uprawnienia administratora innym członkom.</string>
|
||||
<string name="marmot_admin_granted">Przyznane uprawnienia administracyjne</string>
|
||||
<string name="marmot_failed_to_grant_admin">Nie udało się przyznać uprawnień admina: %1$s</string>
|
||||
<string name="marmot_revoke">Wycofaj</string>
|
||||
<string name="marmot_revoke_admin_privileges">Cofnij uprawnienia administratora</string>
|
||||
<string name="marmot_revoke_admin_title">Cofnij uprawnienia administratora</string>
|
||||
<string name="marmot_revoke_admin_confirm">Odbierz uprawnienia administratora od „%1$s”? Użytkownik ten pozostanie członkiem grupy.</string>
|
||||
<string name="marmot_admin_revoked">Uprawnienia administratora cofnięte</string>
|
||||
<string name="marmot_failed_to_revoke_admin">Nie udało się cofnąć upragnień admina: %1$s</string>
|
||||
<string name="marmot_relays_header">Transmitery</string>
|
||||
<string name="marmot_relay_no_events">brak wydarzeń</string>
|
||||
<string name="marmot_relay_last_event">ostatnie wydarzenie%1$s</string>
|
||||
<string name="marmot_not_a_member">Nie jest członkiem tej grupy</string>
|
||||
<string name="copy">Kopiuj</string>
|
||||
<string name="ots_reset_to_auto_select">Zresetuj do automatycznego wyboru</string>
|
||||
<string name="dvm_pay_invoice_from_dvm">Zapłać fakturę z DVM</string>
|
||||
<string name="dvm_pay_amount_to_dvm">Zapłać %1$s satoszy na DVM</string>
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Usługa lightning odbiorcy w %1$s jest niedostępna. Została obliczona na podstawie adresu lightning \"%2$s\". Błąd: %3$s. Sprawdź, czy serwer jest gotowy i czy adres lightning jest poprawny</string>
|
||||
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Nie można rozwiązać %1$s. Sprawdź, czy jesteś połączony, czy serwer jest gotowy i czy lightning adres %2$s jest poprawny.\n\nWyjątkiem było: %3$s</string>
|
||||
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user">Błąd przetwarzania pliku JSON z pobierania faktury %1$s. Sprawdź konfigurację lightning użytkownika</string>
|
||||
|
||||
@@ -69,7 +69,7 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string>
|
||||
<string name="chat_zap_amount_suffix">zapnil %1$s satov</string>
|
||||
<string name="chat_zap_anonymous">Anonimno</string>
|
||||
<string name="chat_raid_is_raiding">izvaja vdor</string>
|
||||
<string name="chat_clip_created_a_clip">ustvarjen izsek</string>
|
||||
<string name="chat_clip_created_a_clip">izsek ustvarjen</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Uporabljaš javni ključ in javni ključi omogočajo le branje.
|
||||
Prijavite se s privatnim ključem, da boste lahko pošiljali Zape</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">Uporabljaš javni ključ in javni ključi omogočajo le branje.
|
||||
@@ -658,9 +658,96 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="exercise_emom">EMOM</string>
|
||||
<string name="exercise_amrap">AMRAP</string>
|
||||
<string name="software_apps">Aplikacije</string>
|
||||
<string name="route_software_apps">Applikacije</string>
|
||||
<string name="napplets">nApplets</string>
|
||||
<string name="nsites">nSites</string>
|
||||
<string name="nsite_none_found">nSites še niso najdena.</string>
|
||||
<string name="browser">Brskalnik</string>
|
||||
<string name="browser_address_hint">Išči ali vpiši naslov</string>
|
||||
<string name="browser_reload">Ponovno naloži</string>
|
||||
<string name="browser_tor_on">Nalaganje prek Tor. Tapnite za uporabo odprtega spleta.</string>
|
||||
<string name="browser_tor_off">Nalaganje prek odprtega spleta. Tapnite za uporabo Tor.</string>
|
||||
<string name="browser_unsupported">Vgrajeni brskalnik zahteva Android 11 ali novejši.</string>
|
||||
<string name="embedded_tab_load_failed">Te aplikacije ni bilo mogoče naložiti.</string>
|
||||
<string name="browser_go">Odpri</string>
|
||||
<string name="browser_clear">Počisti</string>
|
||||
<string name="browser_favorites">Priljubljene</string>
|
||||
<string name="browser_recent_options">Možnosti</string>
|
||||
<string name="browser_recent_remove">Odstrani iz zgodovine</string>
|
||||
<string name="favorite_apps">Priljubljene aplikacije</string>
|
||||
<string name="favorite_apps_empty">Še ni priljubljenih aplikacij. Odprite spletni odjemalec ali nsite in tapnite zvezdico za pripenjanje.</string>
|
||||
<string name="favorite_app_add">dodaj med priljubljene</string>
|
||||
<string name="favorite_app_remove">Odstrani iz priljubljenih</string>
|
||||
<string name="favorite_app_open_window">Odpri v novem oknu</string>
|
||||
<string name="favorite_app_access_title">Do česa lahko ta aplikacija dostopa</string>
|
||||
<string name="favorite_app_access_static">Deluje v peskovniku brez posebnega dostopa do vašega računa.</string>
|
||||
<string name="favorite_app_access_show">Dostop do</string>
|
||||
<string name="favorite_app_network_tor">Nalaganje prek Tor</string>
|
||||
<string name="favorite_app_network_open">Nalaganje prek odprtega spleta</string>
|
||||
<string name="favorite_app_unavailable">Ta aplikacija še ni naložena. Odprite jo s kartice ali poskusite znova čez trenutek.</string>
|
||||
<string name="favorite_notice_published">Objavljeno na relejih</string>
|
||||
<string name="favorite_notice_uploaded">Datoteka naložena</string>
|
||||
<string name="favorite_notice_paid">Plačilo izvedeno</string>
|
||||
<string name="favorite_app_still_loading">Ta aplikacija še ni naložena. Poskusite znova kasneje.</string>
|
||||
<string name="favorite_app_recent">Nedavno</string>
|
||||
<string name="napplet_permissions">nApplet dovoljenja</string>
|
||||
<string name="napplet_manage_permissions">Upravljanje dovoljenj</string>
|
||||
<string name="napplet_permissions_empty">Še ni dovoljenj za nApplet</string>
|
||||
<string name="napplet_permissions_empty_subtitle">Tukaj bodo prikazana dovoljenja, ki jih dodelite nAppletom.</string>
|
||||
<string name="napplet_permissions_forget">Pozabi ta nApplet</string>
|
||||
<string name="napplet_permissions_blocked">Blokirano</string>
|
||||
<string name="napplet_permissions_revoke">Prekliči</string>
|
||||
<string name="napplet_untitled">Neimenovan nApplet</string>
|
||||
<string name="napplet_none_found">Še ni najdenih nobenih nAppletov.</string>
|
||||
<string name="napplet_fallback_title">nApplet%1$s…</string>
|
||||
<!-- Napplet sandbox chrome (host top bar + live action notices) -->
|
||||
<!-- Napplet capability names -->
|
||||
<string name="napplet_cap_shell">Shell</string>
|
||||
<string name="napplet_cap_identity">Identiteta</string>
|
||||
<string name="napplet_cap_keys">Tipkovne bližnjice</string>
|
||||
<string name="napplet_cap_relay">Releji</string>
|
||||
<string name="napplet_cap_storage">Shramba</string>
|
||||
<string name="napplet_cap_value">Plačila</string>
|
||||
<string name="napplet_cap_resource">Mreža</string>
|
||||
<string name="napplet_cap_upload">Prenosi</string>
|
||||
<string name="napplet_cap_shell_desc">Ta nApplet želi preveriti, katere zmogljivosti so na voljo</string>
|
||||
<string name="napplet_cap_identity_desc">Prebere vaš javni ključ</string>
|
||||
<string name="napplet_cap_keys_desc">Poveži tipkovne bližnjice</string>
|
||||
<string name="napplet_cap_relay_desc">Branje, podpisovanje in objavljanje dogodkov</string>
|
||||
<string name="napplet_cap_storage_desc">Lastna zasebna shramba</string>
|
||||
<string name="napplet_cap_value_desc">Plačaj Lightning račune</string>
|
||||
<string name="napplet_cap_resource_desc">Pridobi spletne in Blossom vire</string>
|
||||
<string name="napplet_cap_upload_desc">Nalaganje datotek na vaš medijski strežnik</string>
|
||||
<string name="napplet_cap_theme">Tema</string>
|
||||
<string name="napplet_cap_notify">Obvestila</string>
|
||||
<string name="napplet_cap_inc">Sporočanje</string>
|
||||
<string name="napplet_cap_theme_desc">Prilagoditev barv aplikaciji</string>
|
||||
<string name="napplet_cap_notify_desc">Prikaz obvestil</string>
|
||||
<string name="napplet_cap_inc_desc">Izmenjava sporočil z drugimi nAppleti</string>
|
||||
<!-- Napplet consent dialog -->
|
||||
<string name="napplet_consent_capability">Zmogljivost: %1$s</string>
|
||||
<string name="napplet_consent_allow_always">Vedno dovoli</string>
|
||||
<string name="napplet_consent_allow_once">Dovoli enkrat</string>
|
||||
<string name="napplet_consent_deny_always">Nikoli ne dovoli</string>
|
||||
<string name="napplet_consent_not_now">Ne zdaj</string>
|
||||
<string name="napplet_consent_get_pubkey">Ta nApplet želi prebrati vaš javni ključ.</string>
|
||||
<string name="napplet_consent_identity_read">Ta nApplet želi prebrati vaše podatke o profilu in računu.</string>
|
||||
<string name="napplet_consent_publish">Ta nApplet želi v vašem imenu podpisati in objaviti dogodek vrste %1$d.</string>
|
||||
<string name="napplet_consent_publish_preview">Ta nApplet želi v vašem imenu podpisati in objaviti dogodek vrste %1$d:</string>
|
||||
<string name="napplet_consent_publish_encrypted">Ta nApplet želi v vašem imenu poslati šifriran dogodek.</string>
|
||||
<string name="napplet_consent_query">Ta nApplet želi prebrati dogodke z vaših relejev.</string>
|
||||
<string name="napplet_consent_storage">Ta nApplet želi uporabiti svojo zasebno shrambo.</string>
|
||||
<string name="napplet_consent_pay">Ta nApplet želi plačati Lightning račun.</string>
|
||||
<string name="napplet_consent_resource">Ta nApplet želi pridobiti spletni vir.</string>
|
||||
<string name="napplet_consent_upload">Ta nApplet želi naložiti datoteko na vaš medijski strežnik.</string>
|
||||
<string name="napplet_consent_notify">Ta nApplet vam želi prikazati obvestila.</string>
|
||||
<string name="napplet_consent_sign">Ta stran želi podpisati dogodek vrste (kind) %1$d, z vašim Nostr ključem.</string>
|
||||
<plurals name="napplet_consent_pay_amount">
|
||||
<item quantity="one">Ta nApplet želi plačati Lightning račun za %1$d sat.</item>
|
||||
<item quantity="two">Ta nApplet želi plačati Lightning račun za %1$d sata.</item>
|
||||
<item quantity="few">Ta nApplet želi plačati Lightning račun za %1$d sate.</item>
|
||||
<item quantity="other">Ta nApplet želi plačati Lightning račun za %1$d satov.</item>
|
||||
</plurals>
|
||||
<string name="nip82_repository_label">Vir: %1$s</string>
|
||||
<string name="nip82_version_label">v%1$s</string>
|
||||
<string name="nip82_download">Prenos</string>
|
||||
@@ -2249,9 +2336,13 @@ Za ohranitev zasebnosti to denarnico polni in prazni prek ne-zasebnih računov,
|
||||
<string name="git_repo_section_topics">Teme</string>
|
||||
<string name="git_repo_personal_fork">Osebni fork</string>
|
||||
<string name="nsite_title">Statična spletna stran: %1$s</string>
|
||||
<string name="napplet_card_title">nApplet: %1$s</string>
|
||||
<string name="napplet_card_permissions">Dovoljenja:</string>
|
||||
<string name="profile_tab_apps">Aplikacije in spletna mesta</string>
|
||||
<string name="nsite_root_site">Izhodiščno spletišče</string>
|
||||
<string name="nsite_source">Vir:</string>
|
||||
<string name="nsite_servers">Strežniki:</string>
|
||||
<string name="nsite_open">Odpri</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Dokaz časovnega žiga</string>
|
||||
<string name="ots_info_description">Obstaja dokaz, da je bil ta zapisek podpisan pred %1$s. Dokaz je bil ožigosan v Bitcoin verigi blokov na ta datum in čas.</string>
|
||||
|
||||
@@ -628,6 +628,37 @@
|
||||
<string name="napplets">nApplets</string>
|
||||
<string name="nsites">nSites</string>
|
||||
<string name="nsite_none_found">尚未找到nSite。</string>
|
||||
<string name="browser">浏览器</string>
|
||||
<string name="browser_address_hint">搜索或输入地址</string>
|
||||
<string name="browser_reload">重新加载</string>
|
||||
<string name="browser_console_title_short">控制台</string>
|
||||
<string name="browser_console_title">控制台(%1$d)</string>
|
||||
<string name="browser_console_clear">清除</string>
|
||||
<string name="browser_tor_on">正通过 Tor 加载。点击使用 open web。</string>
|
||||
<string name="browser_tor_off">正通过 open web 加载 Tor。点击使用 Tor。</string>
|
||||
<string name="browser_unsupported">应用内浏览器需要 Android 11 或更高版本。</string>
|
||||
<string name="embedded_tab_load_failed">无法加载此应用。</string>
|
||||
<string name="browser_go">打开</string>
|
||||
<string name="browser_clear">清除</string>
|
||||
<string name="browser_favorites">收藏夹</string>
|
||||
<string name="browser_recent_options">选项</string>
|
||||
<string name="browser_recent_remove">从历史记录中删除</string>
|
||||
<string name="favorite_apps">喜欢的应用</string>
|
||||
<string name="favorite_apps_empty">还没有喜欢的应用。打开 web 客户端或 nsite 并点击⭐来固定它。</string>
|
||||
<string name="favorite_app_add">添加至收藏夹</string>
|
||||
<string name="favorite_app_remove">从收藏中移除</string>
|
||||
<string name="favorite_app_open_window">在自己的窗口打开</string>
|
||||
<string name="favorite_app_access_title">此应用可以访问什么</string>
|
||||
<string name="favorite_app_access_static">在沙盒中运行,没有对账户的特殊访问权。</string>
|
||||
<string name="favorite_app_access_show">此应用可以访问什么</string>
|
||||
<string name="favorite_app_network_tor">通过 Tor 加载。</string>
|
||||
<string name="favorite_app_network_open">通过 open web 加载。</string>
|
||||
<string name="favorite_app_unavailable">此应用尚未加载。请从其卡片打开它,或稍后再试。</string>
|
||||
<string name="favorite_notice_published">已发布到中继</string>
|
||||
<string name="favorite_notice_uploaded">上传了一个文件</string>
|
||||
<string name="favorite_notice_paid">支付了一笔款项</string>
|
||||
<string name="favorite_app_still_loading">该应用尚未加载。稍后再试。</string>
|
||||
<string name="favorite_app_recent">最近</string>
|
||||
<string name="napplet_permissions">nApplet 权限</string>
|
||||
<string name="napplet_manage_permissions">管理权限</string>
|
||||
<string name="napplet_permissions_empty">尚无nApplet 权限</string>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.urlRoute
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.net.URLEncoder
|
||||
|
||||
class URIParserTest {
|
||||
@Test
|
||||
fun parsesEncodedUrlRoutes() {
|
||||
val url = "HTTPS://Example.com/path?b=2&a=1#section"
|
||||
val route = urlRoute("nostr:url?id=${URLEncoder.encode(url, Charsets.UTF_8.name())}")
|
||||
|
||||
assertEquals(Route.Url("https://example.com/path?b=2&a=1"), route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoresMalformedEncodedUrlRoutes() {
|
||||
assertNull(urlRoute("nostr:url?id=%"))
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class FilterPostsByUrlTest {
|
||||
@Test
|
||||
fun normalizesUrlAndQueriesNip22UrlScope() {
|
||||
val relay = NormalizedRelayUrl("wss://relay.example/")
|
||||
val filters = filterPostsByUrl("HTTPS://Example.com/path?b=2&a=1#section", setOf(relay), null)
|
||||
|
||||
assertEquals(1, filters.size)
|
||||
assertEquals(relay, filters[0].relay)
|
||||
assertEquals(CommentKinds, filters[0].filter.kinds)
|
||||
assertEquals(mapOf("I" to listOf("https://example.com/path?b=2&a=1")), filters[0].filter.tags)
|
||||
assertEquals(100, filters[0].filter.limit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsNoFiltersForMalformedUrls() {
|
||||
val relay = NormalizedRelayUrl("wss://relay.example/")
|
||||
|
||||
val filters = filterPostsByUrl("https://", setOf(relay), null)
|
||||
|
||||
assertTrue(filters.isEmpty())
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,12 @@
|
||||
"tag": "v1.12.6",
|
||||
"since": "2026-06-19T19:01:59-04:00",
|
||||
"translators": [
|
||||
{
|
||||
"user": "maxblake2015",
|
||||
"languages": [
|
||||
"Polish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"user": "rajs19420616",
|
||||
"languages": [
|
||||
@@ -89,15 +95,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"user": "summoner001",
|
||||
"user": "StellarStoic",
|
||||
"languages": [
|
||||
"Hungarian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"user": "maxblake2015",
|
||||
"languages": [
|
||||
"Polish"
|
||||
"Slovenian"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -107,15 +107,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"user": "BitByBit21",
|
||||
"user": "summoner001",
|
||||
"languages": [
|
||||
"Spanish"
|
||||
"Hungarian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"user": "StellarStoic",
|
||||
"user": "BitByBit21",
|
||||
"languages": [
|
||||
"Slovenian"
|
||||
"Spanish"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+4
-1
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.napplethost
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
import java.io.File
|
||||
|
||||
@@ -54,7 +55,9 @@ class NappletBlobCache(
|
||||
dir.mkdirs()
|
||||
val tmp = File(dir, "$sha256.tmp.${System.nanoTime()}")
|
||||
tmp.writeBytes(bytes)
|
||||
if (!tmp.renameTo(target)) tmp.delete()
|
||||
if (!tmp.renameTo(target) && !tmp.delete()) {
|
||||
Log.w("NappletBlobCache") { "Failed to delete leftover temp file ${tmp.absolutePath} after a failed rename" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -597,8 +597,28 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
liveUrl = startUrl,
|
||||
onNavigate = { loadAddress(it) },
|
||||
onConsole = { consolePanel?.toggle() },
|
||||
isFavoriteInitially = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false),
|
||||
onFavoriteToggle = { url, _ -> sendFavoriteToggle(url) },
|
||||
).also { controlSheet = it }
|
||||
|
||||
private fun sendFavoriteToggle(url: String) {
|
||||
val host =
|
||||
runCatching {
|
||||
android.net.Uri
|
||||
.parse(url)
|
||||
.host
|
||||
}.getOrNull()?.takeIf { it.isNotBlank() } ?: url
|
||||
val msg =
|
||||
Message.obtain(null, NappletIpc.MSG_TOGGLE_WEB_FAVORITE).apply {
|
||||
data =
|
||||
Bundle().apply {
|
||||
putString(NappletIpc.KEY_FAVORITE_URL, url)
|
||||
putString(NappletIpc.KEY_FAVORITE_LABEL, host)
|
||||
}
|
||||
}
|
||||
if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg)
|
||||
}
|
||||
|
||||
private fun buildConsolePanel(): View =
|
||||
NappletConsolePanel(this).also {
|
||||
it.onClearCallback = { controlSheet?.updateConsoleCount(0) }
|
||||
@@ -655,6 +675,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
private const val EXTRA_USE_TOR = "useTor"
|
||||
private const val EXTRA_TITLE = "title"
|
||||
private const val EXTRA_THEME = "theme"
|
||||
private const val EXTRA_IS_FAVORITE = "isFavorite"
|
||||
|
||||
fun intent(
|
||||
context: Context,
|
||||
@@ -663,6 +684,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
useTor: Boolean,
|
||||
title: String = "",
|
||||
theme: String = "SYSTEM",
|
||||
isFavorite: Boolean = false,
|
||||
): Intent =
|
||||
Intent()
|
||||
.setClassName(context, "com.vitorpamplona.amethyst.napplethost.NappletBrowserActivity")
|
||||
@@ -671,6 +693,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
.putExtra(EXTRA_USE_TOR, useTor)
|
||||
.putExtra(EXTRA_TITLE, title)
|
||||
.putExtra(EXTRA_THEME, theme)
|
||||
.putExtra(EXTRA_IS_FAVORITE, isFavorite)
|
||||
// Distinct task identity per URL for documentLaunchMode=intoExisting.
|
||||
.setData(Uri.parse(url))
|
||||
}
|
||||
|
||||
+47
@@ -67,6 +67,9 @@ class NappletControlSheet(
|
||||
// When non-null, a "Console" row is added to the pull-down sheet. The callback toggles the
|
||||
// browser's console log panel; the count label is updated via [updateConsoleCount].
|
||||
private val onConsole: (() -> Unit)? = null,
|
||||
// When non-null, a favorite toggle row is shown; called with the current URL and new isFavorite state.
|
||||
isFavoriteInitially: Boolean = false,
|
||||
private val onFavoriteToggle: ((url: String, isFavorite: Boolean) -> Unit)? = null,
|
||||
) : LinearLayout(context) {
|
||||
private val onSurface = resolveThemeColor(android.R.attr.textColorPrimary)
|
||||
private val dimmed = resolveThemeColor(android.R.attr.textColorSecondary)
|
||||
@@ -75,6 +78,7 @@ class NappletControlSheet(
|
||||
private var expanded = false
|
||||
private var torOn = torInitiallyOn
|
||||
private var currentUrl = liveUrl
|
||||
private var isFavorite = isFavoriteInitially
|
||||
|
||||
private val panel: LinearLayout
|
||||
private var torLabel: TextView? = null
|
||||
@@ -82,6 +86,7 @@ class NappletControlSheet(
|
||||
private var addressField: EditText? = null
|
||||
private var securityGlyph: TextView? = null
|
||||
private var consoleLabel: TextView? = null
|
||||
private var favoriteLabel: TextView? = null
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
@@ -156,6 +161,35 @@ class NappletControlSheet(
|
||||
},
|
||||
)
|
||||
}
|
||||
onFavoriteToggle?.let {
|
||||
val label =
|
||||
TextView(context).apply {
|
||||
text = context.getString(if (isFavorite) R.string.browser_favorite_remove else R.string.browser_favorite_add)
|
||||
setTextColor(onSurface)
|
||||
textSize = 15f
|
||||
setPadding(dp(8), 0, 0, 0)
|
||||
}
|
||||
favoriteLabel = label
|
||||
addView(
|
||||
LinearLayout(context).apply {
|
||||
orientation = HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
setPadding(dp(8), dp(10), dp(8), dp(10))
|
||||
isClickable = true
|
||||
setOnClickListener { toggleFavorite() }
|
||||
addView(
|
||||
TextView(context).apply {
|
||||
text = "★"
|
||||
setTextColor(dimmed)
|
||||
textSize = 18f
|
||||
width = dp(28)
|
||||
gravity = Gravity.CENTER
|
||||
},
|
||||
)
|
||||
addView(label)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun titleRow(): View =
|
||||
@@ -254,6 +288,19 @@ class NappletControlSheet(
|
||||
// Don't fight the user while they're editing the field.
|
||||
addressField?.takeIf { !it.hasFocus() }?.setText(url)
|
||||
securityGlyph?.text = securityGlyphFor(url)
|
||||
// Reset favorite state for the new URL (we don't know if it's a favorite without a round-trip).
|
||||
if (onFavoriteToggle != null) {
|
||||
isFavorite = false
|
||||
favoriteLabel?.text = context.getString(R.string.browser_favorite_add)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleFavorite() {
|
||||
val url = currentUrl?.takeIf { it.isNotBlank() } ?: return
|
||||
isFavorite = !isFavorite
|
||||
favoriteLabel?.text = context.getString(if (isFavorite) R.string.browser_favorite_remove else R.string.browser_favorite_add)
|
||||
collapse()
|
||||
onFavoriteToggle?.invoke(url, isFavorite)
|
||||
}
|
||||
|
||||
private fun securityGlyphFor(url: String): String =
|
||||
|
||||
@@ -89,6 +89,14 @@ object NappletIpc {
|
||||
*/
|
||||
const val MSG_RECORD_ICON = 10
|
||||
|
||||
/**
|
||||
* Host → broker (browser mode): toggle a URL in the main-process favorites registry. Carries
|
||||
* [KEY_FAVORITE_URL] and [KEY_FAVORITE_LABEL]. The broker adds the URL if it isn't already
|
||||
* a favorite, or removes it if it is — identical to the in-app star toggle on the home screen.
|
||||
* Fire-and-forget; no reply needed.
|
||||
*/
|
||||
const val MSG_TOGGLE_WEB_FAVORITE = 11
|
||||
|
||||
const val KEY_REQUEST_ID = "requestId"
|
||||
const val KEY_PAYLOAD = "payload"
|
||||
|
||||
@@ -107,6 +115,12 @@ object NappletIpc {
|
||||
/** The bare host (e.g. `example.com`) a browser Tor choice belongs to. */
|
||||
const val KEY_WEB_HOST = "webHost"
|
||||
|
||||
/** The full URL (e.g. `https://example.com`) to toggle as a web favorite. */
|
||||
const val KEY_FAVORITE_URL = "favoriteUrl"
|
||||
|
||||
/** A human-readable label for the favorited URL (typically the host). */
|
||||
const val KEY_FAVORITE_LABEL = "favoriteLabel"
|
||||
|
||||
/** Boolean: this sandbox surface is now foreground (true) or backgrounded (false). */
|
||||
const val KEY_FOREGROUND = "foreground"
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
<string name="napplet_net_tor_label">Loads over Tor</string>
|
||||
<string name="napplet_net_open_label">Loads over the open web</string>
|
||||
|
||||
<!-- Favorite toggle in the pull-down sheet -->
|
||||
<string name="browser_favorite_add">Add to favorites</string>
|
||||
<string name="browser_favorite_remove">Remove from favorites</string>
|
||||
|
||||
<!-- Loading / unavailable screens -->
|
||||
<string name="napplet_unavailable_title">Couldn\'t load “%1$s”</string>
|
||||
<string name="napplet_unavailable_subtitle">The publisher\'s servers may be offline, or you\'re not connected. You can try again.</string>
|
||||
|
||||
@@ -43,6 +43,11 @@ class UrlId(
|
||||
|
||||
fun toScope(url: String) = Rfc3986.normalizeAndRemoveFragment(url)
|
||||
|
||||
fun toScopeOrNull(url: String) =
|
||||
runCatching { toScope(url) }
|
||||
.getOrNull()
|
||||
?.takeIf { parse(it) != null }
|
||||
|
||||
fun toKind(url: String) = KIND
|
||||
|
||||
fun match(
|
||||
|
||||
Reference in New Issue
Block a user