mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-06 03:18:23 +00:00
desktop: localize the entire UI through the Strings system
Route every remaining hardcoded English string across the desktop screens (login, unlock, incoming requests, applications + detail, relays, settings and its dialogs, sidebar/account switcher, tray menu and notifications, RememberType labels) through core/Strings, and make each composable observe Strings.currentLanguage so the whole interface recomposes on a live language switch. Reused Android string keys pick up the existing 14-language translations; desktop-only strings are added to strings_en.xml under a d_ prefix and fall back to English in other locales. RememberType now carries a labelKey resolved via label(language) instead of a hardcoded label. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQTVwy8RBj7spdEK3aEc3i
This commit is contained in:
@@ -28,6 +28,11 @@ for the JVM) and mirrors the mobile UI and permission model.
|
||||
- Native desktop layout: sidebar navigation with an account switcher, dense
|
||||
list views, and keyboard shortcuts
|
||||
- Light/dark theme using the Amber palette
|
||||
- Fully localized UI in 14 languages, switchable live under Settings →
|
||||
Language. The Android `strings.xml` translations (and event-kind
|
||||
descriptions) are bundled verbatim under `resources/i18n/strings_<lang>.xml`
|
||||
and loaded by `core/Strings.kt`; desktop-only strings live in
|
||||
`strings_en.xml` (prefixed `d_`) and fall back to English in other locales
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.greenart7c3.nostrsigner.desktop.core.AmberDesktop
|
||||
import com.greenart7c3.nostrsigner.desktop.core.DesktopAccount
|
||||
import com.greenart7c3.nostrsigner.desktop.core.PassphraseLock
|
||||
import com.greenart7c3.nostrsigner.desktop.core.SettingsStore
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import com.greenart7c3.nostrsigner.desktop.core.describe
|
||||
import com.greenart7c3.nostrsigner.desktop.ui.App
|
||||
import com.greenart7c3.nostrsigner.desktop.ui.NostrSignerTheme
|
||||
@@ -105,6 +106,7 @@ fun main() {
|
||||
val windowState = rememberWindowState(size = DpSize(1100.dp, 780.dp))
|
||||
val pending by AmberDesktop.engine.pending.collectAsState()
|
||||
val settings by SettingsStore.settings.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var windowVisible by remember { mutableStateOf(true) }
|
||||
val trayState = rememberTrayState()
|
||||
// AWT may claim support but fail to actually add the icon (e.g. bare
|
||||
@@ -121,7 +123,7 @@ fun main() {
|
||||
trayState.sendNotification(
|
||||
Notification(
|
||||
title = "Amber",
|
||||
message = "${request.appName} ${request.type.describe(request.kind)}",
|
||||
message = "${request.appName} ${request.type.describe(request.kind, language)}",
|
||||
type = Notification.Type.Info,
|
||||
),
|
||||
)
|
||||
@@ -136,18 +138,18 @@ fun main() {
|
||||
Tray(
|
||||
icon = painterResource("icon.png"),
|
||||
state = trayState,
|
||||
tooltip = if (pending.isEmpty()) "Amber — Nostr signer" else "Amber — ${pending.size} pending request(s)",
|
||||
tooltip = if (pending.isEmpty()) Strings.get("d_tray_tooltip", language) else Strings.format("d_tray_pending", pending.size, language = language),
|
||||
onAction = { windowVisible = true },
|
||||
menu = {
|
||||
Item(
|
||||
if (windowVisible) "Hide Amber" else "Open Amber",
|
||||
if (windowVisible) Strings.get("d_tray_hide", language) else Strings.get("d_tray_open", language),
|
||||
onClick = { windowVisible = !windowVisible },
|
||||
)
|
||||
if (lockStatus == PassphraseLock.Status.UNLOCKED) {
|
||||
Item("Lock now", onClick = { PassphraseLock.lock() })
|
||||
Item(Strings.get("d_lock_now", language), onClick = { PassphraseLock.lock() })
|
||||
}
|
||||
Separator()
|
||||
Item("Quit Amber", onClick = ::exitApplication)
|
||||
Item(Strings.get("d_tray_quit", language), onClick = ::exitApplication)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,17 +28,20 @@ enum class EncryptionType {
|
||||
}
|
||||
|
||||
/** Mirrors the Android `RememberType` screen codes so exports stay compatible. */
|
||||
enum class RememberType(val screenCode: Int, val label: String) {
|
||||
NEVER(0, "Just once"),
|
||||
ONE_MINUTE(1, "1 minute"),
|
||||
FIVE_MINUTES(2, "5 minutes"),
|
||||
TEN_MINUTES(3, "10 minutes"),
|
||||
ALWAYS(4, "Always"),
|
||||
ONE_HOUR(5, "1 hour"),
|
||||
ONE_DAY(6, "1 day"),
|
||||
ONE_WEEK(7, "1 week"),
|
||||
enum class RememberType(val screenCode: Int, val labelKey: String) {
|
||||
NEVER(0, "d_remember_never"),
|
||||
ONE_MINUTE(1, "d_remember_1m"),
|
||||
FIVE_MINUTES(2, "d_remember_5m"),
|
||||
TEN_MINUTES(3, "d_remember_10m"),
|
||||
ALWAYS(4, "d_remember_always"),
|
||||
ONE_HOUR(5, "d_remember_1h"),
|
||||
ONE_DAY(6, "d_remember_1d"),
|
||||
ONE_WEEK(7, "d_remember_1w"),
|
||||
;
|
||||
|
||||
/** Localized display label. */
|
||||
fun label(language: String = Strings.currentLanguage.value): String = Strings.get(labelKey, language)
|
||||
|
||||
fun acceptUntil(): Long = when (this) {
|
||||
ALWAYS -> Long.MAX_VALUE / 1000
|
||||
ONE_MINUTE -> TimeUtils.now() + 60
|
||||
@@ -219,6 +222,6 @@ fun SignerType.describe(kind: Int?, language: String = Strings.currentLanguage.v
|
||||
SignerType.SWITCH_RELAYS -> Strings.get("switch_relays", language)
|
||||
SignerType.LOGOUT -> Strings.get("logout", language)
|
||||
SignerType.INVALID -> Strings.get("invalid_request", language)
|
||||
SignerType.PING -> "Ping"
|
||||
SignerType.PING -> Strings.get("ping", language)
|
||||
else -> "${Strings.get("requests", language)} ${SignerDescriptions.permission(methodString(), kind, language)}"
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ object SignerDescriptions {
|
||||
"decrypt_event" -> Strings.get("decrypt_event", language)
|
||||
"encrypt_tag_array" -> Strings.get("encrypt_tag_array", language)
|
||||
"decrypt_tag_array" -> Strings.get("decrypt_tag_array", language)
|
||||
"ping" -> "Ping"
|
||||
"ping" -> Strings.get("ping", language)
|
||||
"sign_event" -> signEventDescription(kind, language)
|
||||
"connect" -> Strings.get("would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf", language)
|
||||
"nip" -> nipDescription(kind, language).ifEmpty { Strings.format("event_kind", kind.toString(), language = language) }
|
||||
|
||||
@@ -55,14 +55,15 @@ import com.greenart7c3.nostrsigner.desktop.core.AccountsStore
|
||||
import com.greenart7c3.nostrsigner.desktop.core.AmberDesktop
|
||||
import com.greenart7c3.nostrsigner.desktop.core.DesktopAccount
|
||||
import com.greenart7c3.nostrsigner.desktop.core.PassphraseLock
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import com.greenart7c3.nostrsigner.desktop.core.toShortenHex
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class Route(val title: String, val subtitle: String, val icon: ImageVector) {
|
||||
data object IncomingRequest : Route("Incoming requests", "Approve or reject what applications ask this signer to do", Icons.Default.Notifications)
|
||||
data object Applications : Route("Applications", "Connected applications and their permissions", Icons.Default.Apps)
|
||||
data object Relays : Route("Relays", "Default relays used by bunker connections", Icons.Default.CellTower)
|
||||
data object Settings : Route("Settings", "Account, security and application preferences", Icons.Default.Settings)
|
||||
sealed class Route(val titleKey: String, val subtitleKey: String, val icon: ImageVector) {
|
||||
data object IncomingRequest : Route("d_route_incoming_title", "d_route_incoming_sub", Icons.Default.Notifications)
|
||||
data object Applications : Route("applications", "d_route_applications_sub", Icons.Default.Apps)
|
||||
data object Relays : Route("relays", "d_route_relays_sub", Icons.Default.CellTower)
|
||||
data object Settings : Route("settings", "d_route_settings_sub", Icons.Default.Settings)
|
||||
}
|
||||
|
||||
val sidebarRoutes = listOf(Route.IncomingRequest, Route.Applications, Route.Relays, Route.Settings)
|
||||
@@ -77,6 +78,7 @@ fun App() {
|
||||
|
||||
val currentRoute by UiState.currentRoute.collectAsState()
|
||||
val selectedApplication by UiState.selectedApplication.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
Toaster.messages.collect { snackbarHostState.showSnackbar(it) }
|
||||
@@ -132,9 +134,9 @@ fun App() {
|
||||
val selectedApp = selectedApplication
|
||||
Column(Modifier.widthIn(max = 880.dp)) {
|
||||
if (selectedApp == null) {
|
||||
Text(currentRoute.title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold)
|
||||
Text(Strings.get(currentRoute.titleKey, language), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold)
|
||||
Text(
|
||||
currentRoute.subtitle,
|
||||
Strings.get(currentRoute.subtitleKey, language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -186,7 +188,7 @@ private fun Sidebar(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
)
|
||||
Text(
|
||||
"Nostr event signer",
|
||||
Strings.get("d_tagline"),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
@@ -218,6 +220,8 @@ private fun SidebarItem(
|
||||
shortcut: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
val title = Strings.get(route.titleKey, language)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface,
|
||||
@@ -229,10 +233,10 @@ private fun SidebarItem(
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(route.icon, route.title, Modifier.size(18.dp))
|
||||
Icon(route.icon, title, Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(
|
||||
route.title,
|
||||
title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -269,6 +273,7 @@ private fun SidebarItem(
|
||||
private fun AccountSwitcher(account: DesktopAccount) {
|
||||
val accounts by AccountsStore.accounts.collectAsState()
|
||||
val name by account.name.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -296,7 +301,7 @@ private fun AccountSwitcher(account: DesktopAccount) {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
name.ifBlank { "Account" },
|
||||
name.ifBlank { Strings.get("account", language) },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -309,7 +314,7 @@ private fun AccountSwitcher(account: DesktopAccount) {
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Icon(Icons.Default.UnfoldMore, "Switch account", Modifier.size(18.dp))
|
||||
Icon(Icons.Default.UnfoldMore, Strings.get("d_switch_account", language), Modifier.size(18.dp))
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
@@ -334,7 +339,7 @@ private fun AccountSwitcher(account: DesktopAccount) {
|
||||
}
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Add an account") },
|
||||
text = { Text(Strings.get("d_add_an_account", language)) },
|
||||
leadingIcon = { Icon(Icons.Default.Add, null, Modifier.size(18.dp)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
|
||||
+22
-19
@@ -36,6 +36,8 @@ import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.desktop.core.AmberDesktop
|
||||
import com.greenart7c3.nostrsigner.desktop.core.DesktopAccount
|
||||
import com.greenart7c3.nostrsigner.desktop.core.RememberType
|
||||
import com.greenart7c3.nostrsigner.desktop.core.SignerDescriptions
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import com.greenart7c3.nostrsigner.desktop.core.toShortenHex
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.text.DateFormat
|
||||
@@ -51,6 +53,7 @@ fun ApplicationDetailScreen(
|
||||
val store = AmberDesktop.store(account.npub)
|
||||
val apps by store.apps.collectAsState()
|
||||
val history by store.history.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
val app = apps.firstOrNull { it.app.key == appKey }
|
||||
var tab by remember { mutableStateOf(0) }
|
||||
|
||||
@@ -64,7 +67,7 @@ fun ApplicationDetailScreen(
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, Strings.get("d_back", language))
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
@@ -74,8 +77,8 @@ fun ApplicationDetailScreen(
|
||||
)
|
||||
Text(
|
||||
buildString {
|
||||
append("Key: ${app.app.key.toShortenHex()}")
|
||||
if (app.app.relays.isNotEmpty()) append(" · Relays: ${app.app.relays.joinToString()}")
|
||||
append(Strings.format("d_key_label", app.app.key.toShortenHex(), language = language))
|
||||
if (app.app.relays.isNotEmpty()) append(" · ${Strings.format("d_relays_label", app.app.relays.joinToString(), language = language)}")
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -88,23 +91,23 @@ fun ApplicationDetailScreen(
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Name") },
|
||||
label = { Text(Strings.get("name", language)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.widthIn(max = 420.dp).weight(1f, fill = false),
|
||||
)
|
||||
AmberOutlinedButton(
|
||||
text = "Save",
|
||||
text = Strings.get("save", language),
|
||||
onClick = {
|
||||
store.upsert(app.copy(app = app.app.copy(name = name)))
|
||||
Toaster.toast("Application updated")
|
||||
Toaster.toast(Strings.get("d_application_updated", language))
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
TabRow(selectedTabIndex = tab) {
|
||||
Tab(selected = tab == 0, onClick = { tab = 0 }, text = { Text("Permissions") })
|
||||
Tab(selected = tab == 1, onClick = { tab = 1 }, text = { Text("Activity") })
|
||||
Tab(selected = tab == 0, onClick = { tab = 0 }, text = { Text(Strings.get("permissions", language)) })
|
||||
Tab(selected = tab == 1, onClick = { tab = 1 }, text = { Text(Strings.get("d_activity", language)) })
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
@@ -121,17 +124,17 @@ fun ApplicationDetailScreen(
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
com.greenart7c3.nostrsigner.desktop.core.SignerDescriptions.permission(permission.type, permission.kind),
|
||||
SignerDescriptions.permission(permission.type, permission.kind, language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
val until = if (permission.acceptable) permission.acceptUntil else permission.rejectUntil
|
||||
val untilLabel = when {
|
||||
until >= Long.MAX_VALUE / 1000 -> "always"
|
||||
until > TimeUtils.now() -> "until ${DateFormat.getDateTimeInstance().format(Date(until * 1000))}"
|
||||
else -> "expired"
|
||||
until >= Long.MAX_VALUE / 1000 -> Strings.get("d_until_always", language)
|
||||
until > TimeUtils.now() -> Strings.format("d_until", DateFormat.getDateTimeInstance().format(Date(until * 1000)), language = language)
|
||||
else -> Strings.get("d_expired", language)
|
||||
}
|
||||
Text(
|
||||
(if (permission.acceptable) "Accept" else "Reject") + " · $untilLabel",
|
||||
(if (permission.acceptable) Strings.get("d_accept", language) else Strings.get("reject", language)) + " · $untilLabel",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -155,7 +158,7 @@ fun ApplicationDetailScreen(
|
||||
store.upsert(app.copy(permissions = newPermissions))
|
||||
},
|
||||
) {
|
||||
Icon(Icons.Default.Delete, "Delete permission")
|
||||
Icon(Icons.Default.Delete, Strings.get("d_delete_permission", language))
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
@@ -170,19 +173,19 @@ fun ApplicationDetailScreen(
|
||||
) {
|
||||
if (appHistory.isEmpty()) {
|
||||
item {
|
||||
Text("No activity yet", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(Strings.get("d_no_activity", language), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
items(appHistory.size) { index ->
|
||||
val entry = appHistory[index]
|
||||
Column(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||
Text(
|
||||
entry.type + (entry.kind?.let { " (kind $it)" } ?: ""),
|
||||
entry.type + (entry.kind?.let { " (${Strings.format("d_kind_paren", it, language = language)})" } ?: ""),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
"${DateFormat.getDateTimeInstance().format(Date(entry.time * 1000))} · " +
|
||||
if (entry.accepted) "accepted" else "rejected",
|
||||
if (entry.accepted) Strings.get("d_accepted", language) else Strings.get("d_rejected", language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
HorizontalDivider()
|
||||
@@ -193,7 +196,7 @@ fun ApplicationDetailScreen(
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AmberOutlinedButton(
|
||||
text = "Disconnect and delete application",
|
||||
text = Strings.get("d_disconnect_delete", language),
|
||||
onClick = {
|
||||
// Deleting drops this screen from composition, so run the
|
||||
// unsubscribe on the application scope (not this composable's)
|
||||
@@ -202,7 +205,7 @@ fun ApplicationDetailScreen(
|
||||
AmberDesktop.applicationIOScope.launch {
|
||||
store.delete(appKey)
|
||||
AmberDesktop.engine.updateFilter()
|
||||
Toaster.toast("Application removed")
|
||||
Toaster.toast(Strings.get("d_application_removed", language))
|
||||
}
|
||||
onBack()
|
||||
},
|
||||
|
||||
+23
-19
@@ -38,6 +38,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.desktop.core.AmberDesktop
|
||||
import com.greenart7c3.nostrsigner.desktop.core.DesktopAccount
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -49,20 +50,21 @@ fun ApplicationsScreen(
|
||||
) {
|
||||
val store = AmberDesktop.store(account.npub)
|
||||
val apps by store.apps.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var showNostrConnectDialog by remember { mutableStateOf(false) }
|
||||
var showBunkerDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
AmberButton(text = "Connect with nostrconnect://", onClick = { showNostrConnectDialog = true })
|
||||
AmberOutlinedButton(text = "Create a bunker connection", onClick = { showBunkerDialog = true })
|
||||
AmberButton(text = Strings.get("d_connect_nostrconnect", language), onClick = { showNostrConnectDialog = true })
|
||||
AmberOutlinedButton(text = Strings.get("d_create_bunker", language), onClick = { showBunkerDialog = true })
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (apps.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
"No applications connected yet",
|
||||
Strings.get("d_no_apps", language),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -87,10 +89,10 @@ fun ApplicationsScreen(
|
||||
)
|
||||
Text(
|
||||
buildString {
|
||||
append(if (app.app.isConnected) "Connected" else "Waiting for the app to connect")
|
||||
append(" · ${app.permissions.size} permissions")
|
||||
append(if (app.app.isConnected) Strings.get("d_app_connected", language) else Strings.get("d_app_waiting", language))
|
||||
append(" · ${Strings.format("d_permissions_count", app.permissions.size, language = language)}")
|
||||
if (app.app.lastUsed > 0) {
|
||||
append(" · last used ${DateFormat.getDateTimeInstance().format(Date(app.app.lastUsed * 1000))}")
|
||||
append(" · ${Strings.format("d_last_used", DateFormat.getDateTimeInstance().format(Date(app.app.lastUsed * 1000)), language = language)}")
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -124,13 +126,14 @@ private fun NostrConnectDialog(
|
||||
) {
|
||||
var uri by remember { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Add an application") },
|
||||
title = { Text(Strings.get("d_add_an_application", language)) },
|
||||
text = {
|
||||
Column {
|
||||
Text("Paste the nostrconnect:// URI shown by the application.")
|
||||
Text(Strings.get("d_paste_nostrconnect", language))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uri,
|
||||
@@ -145,7 +148,7 @@ private fun NostrConnectDialog(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (!uri.trim().startsWith("nostrconnect://")) {
|
||||
Toaster.toast("Invalid nostrconnect URI")
|
||||
Toaster.toast(Strings.get("d_invalid_nostrconnect", language))
|
||||
return@launch
|
||||
}
|
||||
val error = AmberDesktop.engine.addNostrConnect(uri, account)
|
||||
@@ -156,10 +159,10 @@ private fun NostrConnectDialog(
|
||||
}
|
||||
}
|
||||
},
|
||||
) { Text("Add") }
|
||||
) { Text(Strings.get("add", language)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
TextButton(onClick = onDismiss) { Text(Strings.get("cancel", language)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -173,10 +176,11 @@ private fun NewBunkerDialog(
|
||||
var bunkerUri by remember { mutableStateOf<String?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(if (bunkerUri == null) "Add a nsecBunker" else "Bunker connection created") },
|
||||
title = { Text(if (bunkerUri == null) Strings.get("d_add_nsecbunker", language) else Strings.get("d_bunker_created", language)) },
|
||||
text = {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()),
|
||||
@@ -184,12 +188,12 @@ private fun NewBunkerDialog(
|
||||
) {
|
||||
val uri = bunkerUri
|
||||
if (uri == null) {
|
||||
Text("Creates a bunker:// URI you can paste (or scan) in the client application.")
|
||||
Text(Strings.get("d_bunker_desc", language))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Application name") },
|
||||
label = { Text(Strings.get("d_application_name", language)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
@@ -205,7 +209,7 @@ private fun NewBunkerDialog(
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (name.isBlank()) {
|
||||
Toaster.toast("Name is required")
|
||||
Toaster.toast(Strings.get("d_name_required", language))
|
||||
return@TextButton
|
||||
}
|
||||
scope.launch {
|
||||
@@ -216,18 +220,18 @@ private fun NewBunkerDialog(
|
||||
)
|
||||
}
|
||||
},
|
||||
) { Text("Create") }
|
||||
) { Text(Strings.get("d_create", language)) }
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = {
|
||||
clipboard.setText(AnnotatedString(uri))
|
||||
Toaster.toast("Copied to the clipboard")
|
||||
Toaster.toast(Strings.get("d_copied_clipboard", language))
|
||||
},
|
||||
) { Text("Copy") }
|
||||
) { Text(Strings.get("copy", language).trim()) }
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(if (bunkerUri == null) "Cancel" else "Close") }
|
||||
TextButton(onClick = onDismiss) { Text(if (bunkerUri == null) Strings.get("cancel", language) else Strings.get("d_close", language)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -24,6 +25,7 @@ import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.greenart7c3.nostrsigner.desktop.core.RememberType
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import com.greenart7c3.nostrsigner.desktop.core.rememberTypeDisplayOrder
|
||||
import java.awt.image.BufferedImage
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -84,11 +86,12 @@ fun RememberTypeSelector(
|
||||
onValueChange: (RememberType) -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
OutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
shape = ButtonBorder,
|
||||
) {
|
||||
Text("Remember: ${value.label}", style = MaterialTheme.typography.bodySmall)
|
||||
Text(Strings.format("d_remember_prefix", value.label(language), language = language), style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
@@ -96,7 +99,7 @@ fun RememberTypeSelector(
|
||||
) {
|
||||
rememberTypeDisplayOrder.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(type.label) },
|
||||
text = { Text(type.label(language)) },
|
||||
onClick = {
|
||||
onValueChange(type)
|
||||
expanded = false
|
||||
@@ -130,7 +133,7 @@ fun QrCodeImage(
|
||||
}
|
||||
Image(
|
||||
bitmap = image,
|
||||
contentDescription = "QR code",
|
||||
contentDescription = Strings.get("d_qr_code"),
|
||||
modifier = modifier.size(size),
|
||||
)
|
||||
}
|
||||
|
||||
+8
-7
@@ -45,14 +45,15 @@ import com.greenart7c3.nostrsigner.desktop.core.toShortenHex
|
||||
@Composable
|
||||
fun IncomingRequestsScreen(account: DesktopAccount) {
|
||||
val pending by AmberDesktop.engine.pending.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
|
||||
if (pending.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("No requests waiting for approval", style = MaterialTheme.typography.titleMedium)
|
||||
Text(Strings.get("d_no_requests_title", language), style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Connect an application from the Applications tab and its signing requests will show up here.",
|
||||
Strings.get("d_no_requests_sub", language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
@@ -168,7 +169,7 @@ private fun RequestCard(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AmberButton(
|
||||
text = if (working) "Working…" else "Approve",
|
||||
text = if (working) Strings.get("d_working", language) else Strings.get("d_approve", language),
|
||||
enabled = !working,
|
||||
onClick = {
|
||||
working = true
|
||||
@@ -180,20 +181,20 @@ private fun RequestCard(
|
||||
if (req.type == SignerType.CONNECT) RememberType.ALWAYS else rememberType,
|
||||
grantedPermissions,
|
||||
)
|
||||
Toaster.toast("Request approved")
|
||||
Toaster.toast(Strings.get("d_request_approved", language))
|
||||
},
|
||||
)
|
||||
AmberOutlinedButton(
|
||||
text = "Reject",
|
||||
text = Strings.get("reject", language),
|
||||
onClick = {
|
||||
working = true
|
||||
AmberDesktop.engine.reject(req, rememberType)
|
||||
Toaster.toast("Request rejected")
|
||||
Toaster.toast(Strings.get("d_request_rejected", language))
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(
|
||||
"↑↓ select · ←→ remember · ${shortcutLabel("↵")} approve · ${shortcutLabel("↵", shift = true)} reject",
|
||||
Strings.format("d_shortcut_hint", shortcutLabel("↵"), shortcutLabel("↵", shift = true), language = language),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -39,6 +40,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.desktop.Session
|
||||
import com.greenart7c3.nostrsigner.desktop.core.AccountManager
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -47,6 +49,7 @@ fun LoginScreen(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var tab by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -70,15 +73,15 @@ fun LoginScreen(
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
"Nostr event signer",
|
||||
Strings.get("d_tagline", language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
TabRow(selectedTabIndex = tab) {
|
||||
Tab(selected = tab == 0, onClick = { tab = 0 }, text = { Text("Add a key") })
|
||||
Tab(selected = tab == 1, onClick = { tab = 1 }, text = { Text("Create a new key") })
|
||||
Tab(selected = tab == 0, onClick = { tab = 0 }, text = { Text(Strings.get("d_add_a_key", language)) })
|
||||
Tab(selected = tab == 1, onClick = { tab = 1 }, text = { Text(Strings.get("d_create_a_new_key", language)) })
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
@@ -90,7 +93,7 @@ fun LoginScreen(
|
||||
|
||||
if (hasAccounts) {
|
||||
TextButton(onClick = { Session.addingAccount.value = false }) {
|
||||
Text("Cancel")
|
||||
Text(Strings.get("cancel", language))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,6 +103,7 @@ fun LoginScreen(
|
||||
|
||||
@Composable
|
||||
private fun ImportKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var key by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var working by remember { mutableStateOf(false) }
|
||||
@@ -107,7 +111,7 @@ private fun ImportKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
OutlinedTextField(
|
||||
value = key,
|
||||
onValueChange = { key = it },
|
||||
label = { Text("nsec, ncryptsec, hex key or seed words") },
|
||||
label = { Text(Strings.get("d_import_key_label", language)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
@@ -117,7 +121,7 @@ private fun ImportKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
label = { Text(Strings.get("d_password", language)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
@@ -125,7 +129,7 @@ private fun ImportKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
AmberButton(
|
||||
text = if (working) "Adding…" else "Add key",
|
||||
text = if (working) Strings.get("d_adding", language) else Strings.get("d_add_key", language),
|
||||
fillWidth = true,
|
||||
enabled = key.isNotBlank() && !working,
|
||||
onClick = {
|
||||
@@ -141,7 +145,7 @@ private fun ImportKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
Session.onAccountAdded(account)
|
||||
},
|
||||
onFailure = {
|
||||
Toaster.toast(it.message ?: "Invalid key")
|
||||
Toaster.toast(it.message ?: Strings.get("d_invalid_key", language))
|
||||
},
|
||||
)
|
||||
working = false
|
||||
@@ -152,6 +156,7 @@ private fun ImportKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
|
||||
@Composable
|
||||
private fun NewKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var name by remember { mutableStateOf("") }
|
||||
var working by remember { mutableStateOf(false) }
|
||||
val seedWords = remember { AccountManager.generateSeedWords() }
|
||||
@@ -159,13 +164,13 @@ private fun NewKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Name (optional)") },
|
||||
label = { Text(Strings.get("d_name_optional", language)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Your seed words", style = MaterialTheme.typography.titleMedium)
|
||||
Text(Strings.get("d_your_seed_words", language), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"Write them down and keep them safe. They are the only backup of your new key.",
|
||||
Strings.get("d_seed_words_desc", language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@@ -186,7 +191,7 @@ private fun NewKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
AmberButton(
|
||||
text = if (working) "Creating…" else "Create account",
|
||||
text = if (working) Strings.get("d_creating", language) else Strings.get("d_create_account", language),
|
||||
fillWidth = true,
|
||||
enabled = !working,
|
||||
onClick = {
|
||||
@@ -203,7 +208,7 @@ private fun NewKeyPane(scope: kotlinx.coroutines.CoroutineScope) {
|
||||
Session.onAccountAdded(account)
|
||||
},
|
||||
onFailure = {
|
||||
Toaster.toast(it.message ?: "Failed to create the key")
|
||||
Toaster.toast(it.message ?: Strings.get("d_failed_create_key", language))
|
||||
},
|
||||
)
|
||||
working = false
|
||||
|
||||
@@ -31,12 +31,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.desktop.core.AmberDesktop
|
||||
import com.greenart7c3.nostrsigner.desktop.core.SettingsStore
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun RelaysScreen() {
|
||||
val settings by SettingsStore.settings.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var newRelay by remember { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -45,16 +47,16 @@ fun RelaysScreen() {
|
||||
OutlinedTextField(
|
||||
value = newRelay,
|
||||
onValueChange = { newRelay = it },
|
||||
label = { Text("wss://relay.example.com") },
|
||||
label = { Text(Strings.get("d_relay_hint", language)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.widthIn(max = 420.dp).weight(1f, fill = false),
|
||||
)
|
||||
AmberOutlinedButton(
|
||||
text = "Add",
|
||||
text = Strings.get("add", language),
|
||||
onClick = {
|
||||
val normalized = RelayUrlNormalizer.normalizeOrNull(newRelay.trim())
|
||||
if (normalized == null) {
|
||||
Toaster.toast("Invalid relay URL")
|
||||
Toaster.toast(Strings.get("d_invalid_relay", language))
|
||||
return@AmberOutlinedButton
|
||||
}
|
||||
SettingsStore.update {
|
||||
@@ -84,7 +86,7 @@ fun RelaysScreen() {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (settings.defaultRelays.size == 1) {
|
||||
Toaster.toast("At least one relay is required")
|
||||
Toaster.toast(Strings.get("d_one_relay_required", language))
|
||||
return@IconButton
|
||||
}
|
||||
SettingsStore.update {
|
||||
@@ -93,7 +95,7 @@ fun RelaysScreen() {
|
||||
scope.launch { AmberDesktop.engine.updateFilter() }
|
||||
},
|
||||
) {
|
||||
Icon(Icons.Default.Delete, "Remove relay")
|
||||
Icon(Icons.Default.Delete, Strings.get("d_remove_relay", language))
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
@@ -102,13 +104,13 @@ fun RelaysScreen() {
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AmberOutlinedButton(
|
||||
text = "Reconnect relays",
|
||||
text = Strings.get("d_reconnect_relays", language),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
AmberDesktop.engine.updateFilter()
|
||||
AmberDesktop.client.connect()
|
||||
AmberDesktop.client.reconnect(true)
|
||||
Toaster.toast("Reconnecting…")
|
||||
Toaster.toast(Strings.get("d_reconnecting", language))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -56,49 +56,50 @@ fun SettingsScreen(account: DesktopAccount) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val settings by SettingsStore.settings.collectAsState()
|
||||
val accounts by AccountsStore.accounts.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var showBackupDialog by remember { mutableStateOf(false) }
|
||||
var showLogsDialog by remember { mutableStateOf(false) }
|
||||
var showLogoutConfirm by remember { mutableStateOf<String?>(null) }
|
||||
var name by remember { mutableStateOf(account.name.value) }
|
||||
|
||||
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
||||
SectionTitle("Account")
|
||||
SectionTitle(Strings.get("account", language))
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Name") },
|
||||
label = { Text(Strings.get("name", language)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.widthIn(max = 420.dp).weight(1f, fill = false),
|
||||
)
|
||||
AmberOutlinedButton(
|
||||
text = "Save",
|
||||
text = Strings.get("save", language),
|
||||
onClick = {
|
||||
account.name.value = name
|
||||
Session.saveMeta(account)
|
||||
Toaster.toast("Saved")
|
||||
Toaster.toast(Strings.get("d_saved", language))
|
||||
},
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"Public key: ${account.npub}",
|
||||
Strings.format("d_public_key", account.npub, language = language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AmberButton(text = "Backup keys", onClick = { showBackupDialog = true })
|
||||
AmberButton(text = Strings.get("backup_keys", language), onClick = { showBackupDialog = true })
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionTitle("Sign policy")
|
||||
SectionTitle(Strings.get("d_sign_policy", language))
|
||||
Text(
|
||||
"Default policy applied to newly connected applications.",
|
||||
Strings.get("d_sign_policy_sub", language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
val policies = listOf(
|
||||
0 to "Basic permissions",
|
||||
1 to "Approve requested permissions",
|
||||
2 to "Sign everything",
|
||||
0 to Strings.get("d_policy_basic", language),
|
||||
1 to Strings.get("d_policy_approve", language),
|
||||
2 to Strings.get("d_policy_sign_all", language),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
policies.forEach { (value, label) ->
|
||||
@@ -114,9 +115,13 @@ fun SettingsScreen(account: DesktopAccount) {
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionTitle("Theme")
|
||||
SectionTitle(Strings.get("d_theme", language))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf<Pair<Boolean?, String>>(null to "System", false to "Light", true to "Dark").forEach { (value, label) ->
|
||||
listOf<Pair<Boolean?, String>>(
|
||||
null to Strings.get("d_theme_system", language),
|
||||
false to Strings.get("d_theme_light", language),
|
||||
true to Strings.get("d_theme_dark", language),
|
||||
).forEach { (value, label) ->
|
||||
FilterChip(
|
||||
selected = settings.darkTheme == value,
|
||||
onClick = { SettingsStore.update { it.copy(darkTheme = value) } },
|
||||
@@ -126,28 +131,28 @@ fun SettingsScreen(account: DesktopAccount) {
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionTitle(Strings.get("language"))
|
||||
SectionTitle(Strings.get("language", language))
|
||||
LanguagePicker()
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionTitle("Desktop")
|
||||
SectionTitle(Strings.get("d_desktop", language))
|
||||
if (isTraySupported) {
|
||||
SettingSwitch(
|
||||
title = "Keep running in the tray",
|
||||
description = "Closing the window minimizes Amber to the system tray so it keeps answering requests.",
|
||||
title = Strings.get("d_keep_in_tray", language),
|
||||
description = Strings.get("d_keep_in_tray_sub", language),
|
||||
checked = settings.closeToTray,
|
||||
onCheckedChange = { value -> SettingsStore.update { it.copy(closeToTray = value) } },
|
||||
)
|
||||
}
|
||||
SettingSwitch(
|
||||
title = "Notifications",
|
||||
description = "Show a system notification when a request needs your approval.",
|
||||
title = Strings.get("d_notifications", language),
|
||||
description = Strings.get("d_notifications_sub", language),
|
||||
checked = settings.showNotifications,
|
||||
onCheckedChange = { value -> SettingsStore.update { it.copy(showNotifications = value) } },
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionTitle("Accounts")
|
||||
SectionTitle(Strings.get("accounts", language))
|
||||
accounts.forEach { record ->
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
@@ -160,7 +165,7 @@ fun SettingsScreen(account: DesktopAccount) {
|
||||
)
|
||||
if (record.npub == account.npub) {
|
||||
Text(
|
||||
"Active",
|
||||
Strings.get("d_active", language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -168,31 +173,30 @@ fun SettingsScreen(account: DesktopAccount) {
|
||||
}
|
||||
if (record.npub != account.npub) {
|
||||
TextButton(onClick = { scope.launch { Session.switchTo(record.npub) } }) {
|
||||
Text("Switch")
|
||||
Text(Strings.get("d_switch", language))
|
||||
}
|
||||
}
|
||||
TextButton(onClick = { showLogoutConfirm = record.npub }) {
|
||||
Text("Log out")
|
||||
Text(Strings.get("d_log_out", language))
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AmberOutlinedButton(text = "Add an account", onClick = { Session.addingAccount.value = true })
|
||||
AmberOutlinedButton(text = Strings.get("d_add_an_account", language), onClick = { Session.addingAccount.value = true })
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionTitle("Security")
|
||||
SectionTitle(Strings.get("security", language))
|
||||
Text(
|
||||
"Keys are encrypted with AES-256; the encryption key is protected by: " +
|
||||
(DesktopKeyStore.passwordSourceDescription ?: "…"),
|
||||
Strings.format("d_security_keys_desc", DesktopKeyStore.passwordSourceDescription ?: "…", language = language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SecuritySection()
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionTitle("Diagnostics")
|
||||
AmberOutlinedButton(text = "View logs", onClick = { showLogsDialog = true })
|
||||
SectionTitle(Strings.get("d_diagnostics", language))
|
||||
AmberOutlinedButton(text = Strings.get("d_view_logs", language), onClick = { showLogsDialog = true })
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
@@ -205,18 +209,18 @@ fun SettingsScreen(account: DesktopAccount) {
|
||||
showLogoutConfirm?.let { npub ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLogoutConfirm = null },
|
||||
title = { Text("Log out?") },
|
||||
text = { Text("This deletes the account key and its connections from this device. Make sure the key is backed up.") },
|
||||
title = { Text(Strings.get("d_log_out_q", language)) },
|
||||
text = { Text(Strings.get("d_log_out_confirm", language)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showLogoutConfirm = null
|
||||
scope.launch { Session.logout(npub) }
|
||||
},
|
||||
) { Text("Log out") }
|
||||
) { Text(Strings.get("d_log_out", language)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showLogoutConfirm = null }) { Text("Cancel") }
|
||||
TextButton(onClick = { showLogoutConfirm = null }) { Text(Strings.get("cancel", language)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -274,35 +278,40 @@ private fun SecuritySection() {
|
||||
val scope = rememberCoroutineScope()
|
||||
val lockStatus by PassphraseLock.state.collectAsState()
|
||||
val settings by SettingsStore.settings.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var dialog by remember { mutableStateOf<PassphraseDialogMode?>(null) }
|
||||
var showRemoveConfirm by remember { mutableStateOf(false) }
|
||||
|
||||
if (lockStatus == PassphraseLock.Status.DISABLED) {
|
||||
Text(
|
||||
"Set a passphrase to keep your keys encrypted even against software " +
|
||||
"that can read your files. You will be asked for it when Amber starts.",
|
||||
Strings.get("d_passphrase_desc", language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
AmberButton(text = "Set a passphrase", onClick = { dialog = PassphraseDialogMode.SET })
|
||||
AmberButton(text = Strings.get("d_set_passphrase", language), onClick = { dialog = PassphraseDialogMode.SET })
|
||||
} else {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
AmberOutlinedButton(
|
||||
text = "Lock now",
|
||||
text = Strings.get("d_lock_now", language),
|
||||
onClick = { PassphraseLock.lock() },
|
||||
)
|
||||
AmberOutlinedButton(
|
||||
text = "Change passphrase",
|
||||
text = Strings.get("d_change_passphrase", language),
|
||||
onClick = { dialog = PassphraseDialogMode.CHANGE },
|
||||
)
|
||||
AmberOutlinedButton(
|
||||
text = "Remove passphrase",
|
||||
text = Strings.get("d_remove_passphrase", language),
|
||||
onClick = { showRemoveConfirm = true },
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Lock automatically after", style = MaterialTheme.typography.bodySmall)
|
||||
Text(Strings.get("d_lock_after", language), style = MaterialTheme.typography.bodySmall)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf(0 to "Never", 5 to "5 min", 15 to "15 min", 60 to "1 hour").forEach { (minutes, label) ->
|
||||
listOf(
|
||||
0 to Strings.get("d_lock_never", language),
|
||||
5 to Strings.get("d_lock_5min", language),
|
||||
15 to Strings.get("d_lock_15min", language),
|
||||
60 to Strings.get("d_lock_1hour", language),
|
||||
).forEach { (minutes, label) ->
|
||||
FilterChip(
|
||||
selected = settings.autoLockMinutes == minutes,
|
||||
onClick = {
|
||||
@@ -321,13 +330,9 @@ private fun SecuritySection() {
|
||||
if (showRemoveConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRemoveConfirm = false },
|
||||
title = { Text("Remove the passphrase?") },
|
||||
title = { Text(Strings.get("d_remove_passphrase_q", language)) },
|
||||
text = {
|
||||
Text(
|
||||
"The encryption key will go back to the OS credential store " +
|
||||
"(or a local file), and Amber will no longer ask for a " +
|
||||
"passphrase at startup.",
|
||||
)
|
||||
Text(Strings.get("d_remove_passphrase_desc", language))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
@@ -335,13 +340,13 @@ private fun SecuritySection() {
|
||||
showRemoveConfirm = false
|
||||
scope.launch {
|
||||
PassphraseLock.disable()
|
||||
Toaster.toast("Passphrase removed")
|
||||
Toaster.toast(Strings.get("d_passphrase_removed", language))
|
||||
}
|
||||
},
|
||||
) { Text("Remove") }
|
||||
) { Text(Strings.get("remove", language)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showRemoveConfirm = false }) { Text("Cancel") }
|
||||
TextButton(onClick = { showRemoveConfirm = false }) { Text(Strings.get("cancel", language)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -355,6 +360,7 @@ private fun PassphraseDialog(
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var current by remember { mutableStateOf("") }
|
||||
var new by remember { mutableStateOf("") }
|
||||
var confirm by remember { mutableStateOf("") }
|
||||
@@ -362,13 +368,11 @@ private fun PassphraseDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!working) onDismiss() },
|
||||
title = { Text(if (mode == PassphraseDialogMode.SET) "Set a passphrase" else "Change the passphrase") },
|
||||
title = { Text(if (mode == PassphraseDialogMode.SET) Strings.get("d_set_passphrase", language) else Strings.get("d_change_the_passphrase", language)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"The passphrase is never stored anywhere. If you forget it, the " +
|
||||
"only way back in is restoring your keys from a backup (nsec " +
|
||||
"or seed words).",
|
||||
Strings.get("d_passphrase_never_stored", language),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@@ -376,7 +380,7 @@ private fun PassphraseDialog(
|
||||
OutlinedTextField(
|
||||
value = current,
|
||||
onValueChange = { current = it },
|
||||
label = { Text("Current passphrase") },
|
||||
label = { Text(Strings.get("d_current_passphrase", language)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -386,7 +390,7 @@ private fun PassphraseDialog(
|
||||
OutlinedTextField(
|
||||
value = new,
|
||||
onValueChange = { new = it },
|
||||
label = { Text("New passphrase (min. 8 characters)") },
|
||||
label = { Text(Strings.get("d_new_passphrase", language)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -395,7 +399,7 @@ private fun PassphraseDialog(
|
||||
OutlinedTextField(
|
||||
value = confirm,
|
||||
onValueChange = { confirm = it },
|
||||
label = { Text("Repeat the new passphrase") },
|
||||
label = { Text(Strings.get("d_repeat_passphrase", language)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -407,11 +411,11 @@ private fun PassphraseDialog(
|
||||
enabled = !working,
|
||||
onClick = {
|
||||
if (new.length < 8) {
|
||||
Toaster.toast("Use at least 8 characters")
|
||||
Toaster.toast(Strings.get("d_use_8_chars", language))
|
||||
return@TextButton
|
||||
}
|
||||
if (new != confirm) {
|
||||
Toaster.toast("The passphrases do not match")
|
||||
Toaster.toast(Strings.get("d_passphrases_no_match", language))
|
||||
return@TextButton
|
||||
}
|
||||
working = true
|
||||
@@ -419,26 +423,26 @@ private fun PassphraseDialog(
|
||||
try {
|
||||
if (mode == PassphraseDialogMode.SET) {
|
||||
PassphraseLock.enable(new.toCharArray())
|
||||
Toaster.toast("Passphrase set")
|
||||
Toaster.toast(Strings.get("d_passphrase_set", language))
|
||||
onDismiss()
|
||||
} else {
|
||||
if (PassphraseLock.changePassphrase(current.toCharArray(), new.toCharArray())) {
|
||||
Toaster.toast("Passphrase changed")
|
||||
Toaster.toast(Strings.get("d_passphrase_changed", language))
|
||||
onDismiss()
|
||||
} else {
|
||||
Toaster.toast("Wrong current passphrase")
|
||||
Toaster.toast(Strings.get("d_wrong_current_passphrase", language))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toaster.toast(e.message ?: "Failed to update the passphrase")
|
||||
Toaster.toast(e.message ?: Strings.get("d_failed_update_passphrase", language))
|
||||
}
|
||||
working = false
|
||||
}
|
||||
},
|
||||
) { Text(if (working) "Working…" else "Save") }
|
||||
) { Text(if (working) Strings.get("d_working", language) else Strings.get("save", language)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(enabled = !working, onClick = onDismiss) { Text("Cancel") }
|
||||
TextButton(enabled = !working, onClick = onDismiss) { Text(Strings.get("cancel", language)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -450,6 +454,7 @@ private fun BackupDialog(
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var showSecret by remember { mutableStateOf(false) }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var ncryptsec by remember { mutableStateOf("") }
|
||||
@@ -457,15 +462,15 @@ private fun BackupDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Backup keys") },
|
||||
title = { Text(Strings.get("backup_keys", language)) },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Secret key (nsec)", style = MaterialTheme.typography.titleSmall)
|
||||
Text(Strings.get("d_secret_key_nsec", language), style = MaterialTheme.typography.titleSmall)
|
||||
if (showSecret) {
|
||||
Text(account.getNsec(), fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.bodySmall)
|
||||
if (seedWords.isNotBlank()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Seed words", style = MaterialTheme.typography.titleSmall)
|
||||
Text(Strings.get("d_seed_words", language), style = MaterialTheme.typography.titleSmall)
|
||||
Text(seedWords, fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
} else {
|
||||
@@ -474,7 +479,7 @@ private fun BackupDialog(
|
||||
showSecret = true
|
||||
scope.launch { seedWords = AccountManager.seedWords(account.npub) }
|
||||
},
|
||||
) { Text("Show") }
|
||||
) { Text(Strings.get("d_show", language)) }
|
||||
}
|
||||
Row {
|
||||
TextButton(
|
||||
@@ -482,17 +487,17 @@ private fun BackupDialog(
|
||||
clipboard.setText(AnnotatedString(account.getNsec()))
|
||||
account.didBackup = true
|
||||
Session.saveMeta(account)
|
||||
Toaster.toast("Secret key copied. Clear your clipboard after pasting it!")
|
||||
Toaster.toast(Strings.get("d_nsec_copied", language))
|
||||
},
|
||||
) { Text("Copy nsec") }
|
||||
) { Text(Strings.get("d_copy_nsec", language)) }
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text("Encrypted backup (ncryptsec)", style = MaterialTheme.typography.titleSmall)
|
||||
Text(Strings.get("d_encrypted_backup", language), style = MaterialTheme.typography.titleSmall)
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
label = { Text(Strings.get("d_password", language)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -508,20 +513,20 @@ private fun BackupDialog(
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (password.isBlank()) {
|
||||
Toaster.toast("Password is required")
|
||||
Toaster.toast(Strings.get("d_password_required", language))
|
||||
return@TextButton
|
||||
}
|
||||
ncryptsec = account.nip49Encrypt(password)
|
||||
clipboard.setText(AnnotatedString(ncryptsec))
|
||||
account.didBackup = true
|
||||
Session.saveMeta(account)
|
||||
Toaster.toast("Encrypted key copied to the clipboard")
|
||||
Toaster.toast(Strings.get("d_encrypted_key_copied", language))
|
||||
},
|
||||
) { Text("Encrypt and copy") }
|
||||
) { Text(Strings.get("d_encrypt_and_copy", language)) }
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Close") }
|
||||
TextButton(onClick = onDismiss) { Text(Strings.get("d_close", language)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -533,14 +538,15 @@ private fun LogsDialog(
|
||||
) {
|
||||
val store = AmberDesktop.store(account.npub)
|
||||
val logs by store.logs.collectAsState()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Logs") },
|
||||
title = { Text(Strings.get("d_logs", language)) },
|
||||
text = {
|
||||
Column(Modifier.heightIn(max = 420.dp).verticalScroll(rememberScrollState())) {
|
||||
if (logs.isEmpty()) {
|
||||
Text("No logs")
|
||||
Text(Strings.get("d_no_logs", language))
|
||||
}
|
||||
logs.sortedByDescending { it.time }.forEach { log ->
|
||||
Text(
|
||||
@@ -557,10 +563,10 @@ private fun LogsDialog(
|
||||
onClick = {
|
||||
store.clearLogs()
|
||||
},
|
||||
) { Text("Clear") }
|
||||
) { Text(Strings.get("d_clear", language)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Close") }
|
||||
TextButton(onClick = onDismiss) { Text(Strings.get("d_close", language)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.greenart7c3.nostrsigner.desktop.core.PassphraseLock
|
||||
import com.greenart7c3.nostrsigner.desktop.core.PendingBunkerRequest
|
||||
import com.greenart7c3.nostrsigner.desktop.core.RememberType
|
||||
import com.greenart7c3.nostrsigner.desktop.core.SignerType
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import com.greenart7c3.nostrsigner.desktop.core.rememberTypeDisplayOrder
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -163,14 +164,14 @@ fun handleShortcut(
|
||||
val rememberType = UiState.rememberChoiceFor(request.request.id)
|
||||
if (event.isShiftPressed) {
|
||||
AmberDesktop.engine.reject(request, rememberType)
|
||||
Toaster.toast("Request rejected")
|
||||
Toaster.toast(Strings.get("d_request_rejected"))
|
||||
} else {
|
||||
AmberDesktop.engine.approve(
|
||||
request,
|
||||
if (request.type == SignerType.CONNECT) RememberType.ALWAYS else rememberType,
|
||||
request.requestedPermissions,
|
||||
)
|
||||
Toaster.toast("Request approved")
|
||||
Toaster.toast(Strings.get("d_request_approved"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -25,11 +26,13 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.desktop.core.PassphraseLock
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun UnlockScreen() {
|
||||
val scope = rememberCoroutineScope()
|
||||
val language by Strings.currentLanguage.collectAsState()
|
||||
var passphrase by remember { mutableStateOf("") }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var working by remember { mutableStateOf(false) }
|
||||
@@ -41,7 +44,7 @@ fun UnlockScreen() {
|
||||
scope.launch {
|
||||
val ok = PassphraseLock.unlock(passphrase.toCharArray())
|
||||
if (!ok) {
|
||||
error = "Wrong passphrase"
|
||||
error = Strings.get("d_wrong_passphrase", language)
|
||||
} else {
|
||||
passphrase = ""
|
||||
}
|
||||
@@ -59,12 +62,12 @@ fun UnlockScreen() {
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text("Locked", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(Strings.get("d_locked", language), style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
OutlinedTextField(
|
||||
value = passphrase,
|
||||
onValueChange = { passphrase = it },
|
||||
label = { Text("Passphrase") },
|
||||
label = { Text(Strings.get("d_passphrase", language)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
@@ -76,7 +79,7 @@ fun UnlockScreen() {
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
AmberButton(
|
||||
text = if (working) "Unlocking…" else "Unlock",
|
||||
text = if (working) Strings.get("d_unlocking", language) else Strings.get("d_unlock", language),
|
||||
fillWidth = true,
|
||||
enabled = passphrase.isNotEmpty() && !working,
|
||||
onClick = ::submit,
|
||||
|
||||
@@ -754,4 +754,153 @@
|
||||
<string name="event_kind_30618">Repository state announcements</string>
|
||||
<string name="switch_relays">wants to switch relays</string>
|
||||
<string name="invalid_request">sent an invalid request</string>
|
||||
<!-- Desktop-specific UI strings (English supplement; fall back to English in other locales). -->
|
||||
<string name="d_tagline">Nostr event signer</string>
|
||||
<string name="d_route_incoming_title">Incoming requests</string>
|
||||
<string name="d_route_incoming_sub">Approve or reject what applications ask this signer to do</string>
|
||||
<string name="d_route_applications_sub">Connected applications and their permissions</string>
|
||||
<string name="d_route_relays_sub">Default relays used by bunker connections</string>
|
||||
<string name="d_route_settings_sub">Account, security and application preferences</string>
|
||||
<string name="d_switch_account">Switch account</string>
|
||||
<string name="d_add_an_account">Add an account</string>
|
||||
<string name="d_no_requests_title">No requests waiting for approval</string>
|
||||
<string name="d_no_requests_sub">Connect an application from the Applications tab and its signing requests will show up here.</string>
|
||||
<string name="d_working">Working…</string>
|
||||
<string name="d_approve">Approve</string>
|
||||
<string name="d_request_approved">Request approved</string>
|
||||
<string name="d_request_rejected">Request rejected</string>
|
||||
<string name="d_shortcut_hint">↑↓ select · ←→ remember · %1$s approve · %2$s reject</string>
|
||||
<string name="d_remember_prefix">Remember: %1$s</string>
|
||||
<string name="d_qr_code">QR code</string>
|
||||
<string name="d_saved">Saved</string>
|
||||
<string name="d_public_key">Public key: %1$s</string>
|
||||
<string name="d_sign_policy">Sign policy</string>
|
||||
<string name="d_sign_policy_sub">Default policy applied to newly connected applications.</string>
|
||||
<string name="d_policy_basic">Basic permissions</string>
|
||||
<string name="d_policy_approve">Approve requested permissions</string>
|
||||
<string name="d_policy_sign_all">Sign everything</string>
|
||||
<string name="d_theme">Theme</string>
|
||||
<string name="d_theme_system">System</string>
|
||||
<string name="d_theme_light">Light</string>
|
||||
<string name="d_theme_dark">Dark</string>
|
||||
<string name="d_desktop">Desktop</string>
|
||||
<string name="d_keep_in_tray">Keep running in the tray</string>
|
||||
<string name="d_keep_in_tray_sub">Closing the window minimizes Amber to the system tray so it keeps answering requests.</string>
|
||||
<string name="d_notifications">Notifications</string>
|
||||
<string name="d_notifications_sub">Show a system notification when a request needs your approval.</string>
|
||||
<string name="d_active">Active</string>
|
||||
<string name="d_switch">Switch</string>
|
||||
<string name="d_log_out">Log out</string>
|
||||
<string name="d_security_keys_desc">Keys are encrypted with AES-256; the encryption key is protected by: %1$s</string>
|
||||
<string name="d_diagnostics">Diagnostics</string>
|
||||
<string name="d_view_logs">View logs</string>
|
||||
<string name="d_log_out_q">Log out?</string>
|
||||
<string name="d_log_out_confirm">This deletes the account key and its connections from this device. Make sure the key is backed up.</string>
|
||||
<string name="d_passphrase_desc">Set a passphrase to keep your keys encrypted even against software that can read your files. You will be asked for it when Amber starts.</string>
|
||||
<string name="d_set_passphrase">Set a passphrase</string>
|
||||
<string name="d_lock_now">Lock now</string>
|
||||
<string name="d_change_passphrase">Change passphrase</string>
|
||||
<string name="d_remove_passphrase">Remove passphrase</string>
|
||||
<string name="d_lock_after">Lock automatically after</string>
|
||||
<string name="d_lock_never">Never</string>
|
||||
<string name="d_lock_5min">5 min</string>
|
||||
<string name="d_lock_15min">15 min</string>
|
||||
<string name="d_lock_1hour">1 hour</string>
|
||||
<string name="d_remove_passphrase_q">Remove the passphrase?</string>
|
||||
<string name="d_remove_passphrase_desc">The encryption key will go back to the OS credential store (or a local file), and Amber will no longer ask for a passphrase at startup.</string>
|
||||
<string name="d_passphrase_removed">Passphrase removed</string>
|
||||
<string name="d_change_the_passphrase">Change the passphrase</string>
|
||||
<string name="d_passphrase_never_stored">The passphrase is never stored anywhere. If you forget it, the only way back in is restoring your keys from a backup (nsec or seed words).</string>
|
||||
<string name="d_current_passphrase">Current passphrase</string>
|
||||
<string name="d_new_passphrase">New passphrase (min. 8 characters)</string>
|
||||
<string name="d_repeat_passphrase">Repeat the new passphrase</string>
|
||||
<string name="d_use_8_chars">Use at least 8 characters</string>
|
||||
<string name="d_passphrases_no_match">The passphrases do not match</string>
|
||||
<string name="d_passphrase_set">Passphrase set</string>
|
||||
<string name="d_passphrase_changed">Passphrase changed</string>
|
||||
<string name="d_wrong_current_passphrase">Wrong current passphrase</string>
|
||||
<string name="d_failed_update_passphrase">Failed to update the passphrase</string>
|
||||
<string name="d_secret_key_nsec">Secret key (nsec)</string>
|
||||
<string name="d_seed_words">Seed words</string>
|
||||
<string name="d_show">Show</string>
|
||||
<string name="d_copy_nsec">Copy nsec</string>
|
||||
<string name="d_nsec_copied">Secret key copied. Clear your clipboard after pasting it!</string>
|
||||
<string name="d_encrypted_backup">Encrypted backup (ncryptsec)</string>
|
||||
<string name="d_password">Password</string>
|
||||
<string name="d_encrypt_and_copy">Encrypt and copy</string>
|
||||
<string name="d_password_required">Password is required</string>
|
||||
<string name="d_encrypted_key_copied">Encrypted key copied to the clipboard</string>
|
||||
<string name="d_logs">Logs</string>
|
||||
<string name="d_no_logs">No logs</string>
|
||||
<string name="d_clear">Clear</string>
|
||||
<string name="d_close">Close</string>
|
||||
<string name="d_add_a_key">Add a key</string>
|
||||
<string name="d_create_a_new_key">Create a new key</string>
|
||||
<string name="d_import_key_label">nsec, ncryptsec, hex key or seed words</string>
|
||||
<string name="d_adding">Adding…</string>
|
||||
<string name="d_add_key">Add key</string>
|
||||
<string name="d_invalid_key">Invalid key</string>
|
||||
<string name="d_name_optional">Name (optional)</string>
|
||||
<string name="d_your_seed_words">Your seed words</string>
|
||||
<string name="d_seed_words_desc">Write them down and keep them safe. They are the only backup of your new key.</string>
|
||||
<string name="d_creating">Creating…</string>
|
||||
<string name="d_create_account">Create account</string>
|
||||
<string name="d_failed_create_key">Failed to create the key</string>
|
||||
<string name="d_locked">Locked</string>
|
||||
<string name="d_passphrase">Passphrase</string>
|
||||
<string name="d_wrong_passphrase">Wrong passphrase</string>
|
||||
<string name="d_unlocking">Unlocking…</string>
|
||||
<string name="d_unlock">Unlock</string>
|
||||
<string name="d_connect_nostrconnect">Connect with nostrconnect://</string>
|
||||
<string name="d_create_bunker">Create a bunker connection</string>
|
||||
<string name="d_no_apps">No applications connected yet</string>
|
||||
<string name="d_app_connected">Connected</string>
|
||||
<string name="d_app_waiting">Waiting for the app to connect</string>
|
||||
<string name="d_permissions_count">%1$s permissions</string>
|
||||
<string name="d_last_used">last used %1$s</string>
|
||||
<string name="d_add_an_application">Add an application</string>
|
||||
<string name="d_paste_nostrconnect">Paste the nostrconnect:// URI shown by the application.</string>
|
||||
<string name="d_invalid_nostrconnect">Invalid nostrconnect URI</string>
|
||||
<string name="d_add_nsecbunker">Add a nsecBunker</string>
|
||||
<string name="d_bunker_created">Bunker connection created</string>
|
||||
<string name="d_bunker_desc">Creates a bunker:// URI you can paste (or scan) in the client application.</string>
|
||||
<string name="d_application_name">Application name</string>
|
||||
<string name="d_create">Create</string>
|
||||
<string name="d_name_required">Name is required</string>
|
||||
<string name="d_copied_clipboard">Copied to the clipboard</string>
|
||||
<string name="d_back">Back</string>
|
||||
<string name="d_key_label">Key: %1$s</string>
|
||||
<string name="d_relays_label">Relays: %1$s</string>
|
||||
<string name="d_application_updated">Application updated</string>
|
||||
<string name="d_activity">Activity</string>
|
||||
<string name="d_until_always">always</string>
|
||||
<string name="d_until">until %1$s</string>
|
||||
<string name="d_expired">expired</string>
|
||||
<string name="d_accept">Accept</string>
|
||||
<string name="d_delete_permission">Delete permission</string>
|
||||
<string name="d_no_activity">No activity yet</string>
|
||||
<string name="d_kind_paren">kind %1$s</string>
|
||||
<string name="d_accepted">accepted</string>
|
||||
<string name="d_rejected">rejected</string>
|
||||
<string name="d_disconnect_delete">Disconnect and delete application</string>
|
||||
<string name="d_application_removed">Application removed</string>
|
||||
<string name="d_relay_hint">wss://relay.example.com</string>
|
||||
<string name="d_invalid_relay">Invalid relay URL</string>
|
||||
<string name="d_remove_relay">Remove relay</string>
|
||||
<string name="d_one_relay_required">At least one relay is required</string>
|
||||
<string name="d_reconnect_relays">Reconnect relays</string>
|
||||
<string name="d_reconnecting">Reconnecting…</string>
|
||||
<string name="d_remember_never">Just once</string>
|
||||
<string name="d_remember_1m">1 minute</string>
|
||||
<string name="d_remember_5m">5 minutes</string>
|
||||
<string name="d_remember_10m">10 minutes</string>
|
||||
<string name="d_remember_always">Always</string>
|
||||
<string name="d_remember_1h">1 hour</string>
|
||||
<string name="d_remember_1d">1 day</string>
|
||||
<string name="d_remember_1w">1 week</string>
|
||||
<string name="d_tray_tooltip">Amber — Nostr signer</string>
|
||||
<string name="d_tray_pending">Amber — %1$s pending request(s)</string>
|
||||
<string name="d_tray_hide">Hide Amber</string>
|
||||
<string name="d_tray_open">Open Amber</string>
|
||||
<string name="d_tray_quit">Quit Amber</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.greenart7c3.nostrsigner.desktop
|
||||
|
||||
import com.greenart7c3.nostrsigner.desktop.core.RememberType
|
||||
import com.greenart7c3.nostrsigner.desktop.core.SignerDescriptions
|
||||
import com.greenart7c3.nostrsigner.desktop.core.Strings
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -57,6 +58,24 @@ class StringsTest {
|
||||
assertTrue("should mention the kind number", text.contains("987654"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun desktopUiKeysResolveInEnglishAndFallBack() {
|
||||
// Desktop-only supplement keys exist in English...
|
||||
assertEquals("Approve", Strings.get("d_approve", "en"))
|
||||
assertEquals("Incoming requests", Strings.get("d_route_incoming_title", "en"))
|
||||
// ...and, absent from the translated files, fall back to the English text.
|
||||
assertEquals(Strings.get("d_approve", "en"), Strings.get("d_approve", "de"))
|
||||
assertEquals(Strings.get("d_route_incoming_title", "en"), Strings.get("d_route_incoming_title", "ja"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rememberTypeLabelsAreLocalized() {
|
||||
assertEquals("Just once", RememberType.NEVER.label("en"))
|
||||
assertEquals("Always", RememberType.ALWAYS.label("en"))
|
||||
assertEquals("1 day", RememberType.ONE_DAY.label("en"))
|
||||
assertEquals("1 week", RememberType.ONE_WEEK.label("en"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun permissionTypesAreLocalized() {
|
||||
assertEquals("Read your public key", SignerDescriptions.permission("get_public_key", null, "en"))
|
||||
|
||||
Reference in New Issue
Block a user