Separate service autocomplete history from password counters
This commit is contained in:
parent
72fa3f9f86
commit
89469c6685
@ -18,12 +18,12 @@ data class DeterministicState(
|
||||
val serviceName: String = "",
|
||||
val masterPassword: CharArray = CharArray(0),
|
||||
val masterPasswordVisible: Boolean = false,
|
||||
val counter: Int = 1,
|
||||
val generatedPassword: String = "",
|
||||
val isLocked: Boolean = false,
|
||||
val clipboardTimerSeconds: Int = 0,
|
||||
val biometricEnabled: Boolean = false,
|
||||
val lockTimeoutMinutes: Int = 5,
|
||||
val showRotateWarning: Boolean = false,
|
||||
) {
|
||||
// CharArray doesn't support structural equality; override manually
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@ -32,24 +32,24 @@ data class DeterministicState(
|
||||
return serviceName == other.serviceName &&
|
||||
masterPassword.contentEquals(other.masterPassword) &&
|
||||
masterPasswordVisible == other.masterPasswordVisible &&
|
||||
counter == other.counter &&
|
||||
generatedPassword == other.generatedPassword &&
|
||||
isLocked == other.isLocked &&
|
||||
clipboardTimerSeconds == other.clipboardTimerSeconds &&
|
||||
biometricEnabled == other.biometricEnabled &&
|
||||
lockTimeoutMinutes == other.lockTimeoutMinutes
|
||||
lockTimeoutMinutes == other.lockTimeoutMinutes &&
|
||||
showRotateWarning == other.showRotateWarning
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = serviceName.hashCode()
|
||||
result = 31 * result + masterPassword.contentHashCode()
|
||||
result = 31 * result + masterPasswordVisible.hashCode()
|
||||
result = 31 * result + counter
|
||||
result = 31 * result + generatedPassword.hashCode()
|
||||
result = 31 * result + isLocked.hashCode()
|
||||
result = 31 * result + clipboardTimerSeconds
|
||||
result = 31 * result + biometricEnabled.hashCode()
|
||||
result = 31 * result + lockTimeoutMinutes
|
||||
result = 31 * result + showRotateWarning.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -58,6 +58,7 @@ class DeterministicViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
private val deviceSecretManager = DeviceSecretManager(app)
|
||||
val historyRepository = ServiceHistoryRepository(app)
|
||||
val counterRepository = ServiceCounterRepository(app)
|
||||
|
||||
private val _state = MutableStateFlow(DeterministicState())
|
||||
val state: StateFlow<DeterministicState> = _state.asStateFlow()
|
||||
@ -82,28 +83,43 @@ class DeterministicViewModel(app: Application) : AndroidViewModel(app) {
|
||||
fun toggleMasterPasswordVisible() =
|
||||
_state.update { it.copy(masterPasswordVisible = !it.masterPasswordVisible) }
|
||||
|
||||
fun incrementCounter() = _state.update { it.copy(counter = it.counter + 1) }
|
||||
|
||||
fun decrementCounter() = _state.update { if (it.counter > 1) it.copy(counter = it.counter - 1) else it }
|
||||
|
||||
fun selectService(name: String, counter: Int) =
|
||||
_state.update { it.copy(serviceName = name, counter = counter) }
|
||||
fun selectService(name: String) = _state.update { it.copy(serviceName = name) }
|
||||
|
||||
fun generate() {
|
||||
val s = _state.value
|
||||
if (s.serviceName.isBlank() || s.masterPassword.isEmpty()) return
|
||||
val counter = counterRepository.getCounter(s.serviceName)
|
||||
val deviceSecret = deviceSecretManager.getOrCreateSecret()
|
||||
val password = DeterministicPasswordGenerator.generate(
|
||||
masterPassword = s.masterPassword,
|
||||
deviceSecret = deviceSecret,
|
||||
serviceName = s.serviceName,
|
||||
counter = s.counter,
|
||||
counter = counter,
|
||||
options = SyllableOptions(),
|
||||
)
|
||||
historyRepository.upsert(s.serviceName, s.counter)
|
||||
historyRepository.upsert(s.serviceName)
|
||||
counterRepository.upsert(s.serviceName, counter)
|
||||
_state.update { it.copy(generatedPassword = password) }
|
||||
}
|
||||
|
||||
fun rotatePassword() {
|
||||
val s = _state.value
|
||||
if (s.serviceName.isBlank() || s.masterPassword.isEmpty()) return
|
||||
val newCounter = counterRepository.getCounter(s.serviceName) + 1
|
||||
counterRepository.upsert(s.serviceName, newCounter)
|
||||
val deviceSecret = deviceSecretManager.getOrCreateSecret()
|
||||
val password = DeterministicPasswordGenerator.generate(
|
||||
masterPassword = s.masterPassword,
|
||||
deviceSecret = deviceSecret,
|
||||
serviceName = s.serviceName,
|
||||
counter = newCounter,
|
||||
options = SyllableOptions(),
|
||||
)
|
||||
_state.update { it.copy(generatedPassword = password, showRotateWarning = true) }
|
||||
}
|
||||
|
||||
fun dismissRotateWarning() = _state.update { it.copy(showRotateWarning = false) }
|
||||
|
||||
fun copyAndScheduleClear(context: Context) {
|
||||
val password = _state.value.generatedPassword
|
||||
if (password.isEmpty()) return
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package cz.bugsy.passwordzebra.deterministic
|
||||
|
||||
import android.content.Context
|
||||
|
||||
class ServiceCounterRepository(context: Context) {
|
||||
|
||||
private val prefs = context.getSharedPreferences("service_counters", Context.MODE_PRIVATE)
|
||||
|
||||
fun getCounter(name: String): Int =
|
||||
getAll().firstOrNull { it.first == name }?.second ?: 1
|
||||
|
||||
fun upsert(name: String, counter: Int) {
|
||||
val entries = getAll().toMutableList()
|
||||
val idx = entries.indexOfFirst { it.first == name }
|
||||
if (idx >= 0) entries[idx] = name to counter else entries.add(name to counter)
|
||||
save(entries)
|
||||
}
|
||||
|
||||
fun getAll(): List<Pair<String, Int>> {
|
||||
val raw = prefs.getString(KEY_COUNTERS, "") ?: ""
|
||||
if (raw.isBlank()) return emptyList()
|
||||
return raw.split("|").mapNotNull { entry ->
|
||||
val colonIdx = entry.lastIndexOf(':')
|
||||
if (colonIdx < 0) return@mapNotNull null
|
||||
val name = entry.substring(0, colonIdx)
|
||||
val counter = entry.substring(colonIdx + 1).toIntOrNull() ?: return@mapNotNull null
|
||||
name to counter
|
||||
}
|
||||
}
|
||||
|
||||
private fun save(entries: List<Pair<String, Int>>) {
|
||||
val raw = entries.joinToString("|") { "${it.first}:${it.second}" }
|
||||
prefs.edit().putString(KEY_COUNTERS, raw).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_COUNTERS = "counters"
|
||||
}
|
||||
}
|
||||
@ -6,37 +6,33 @@ class ServiceHistoryRepository(context: Context) {
|
||||
|
||||
private val prefs = context.getSharedPreferences("service_history", Context.MODE_PRIVATE)
|
||||
|
||||
fun getServices(): List<Pair<String, Int>> {
|
||||
fun getNames(): List<String> {
|
||||
val raw = prefs.getString(KEY_SERVICES, "") ?: ""
|
||||
if (raw.isBlank()) return emptyList()
|
||||
return raw.split("|").mapNotNull { entry ->
|
||||
return raw.split("|").map { entry ->
|
||||
// Migration: strip counter from old "name:counter" format
|
||||
val colonIdx = entry.lastIndexOf(':')
|
||||
if (colonIdx < 0) return@mapNotNull null
|
||||
val name = entry.substring(0, colonIdx)
|
||||
val counter = entry.substring(colonIdx + 1).toIntOrNull() ?: return@mapNotNull null
|
||||
name to counter
|
||||
}
|
||||
if (colonIdx >= 0 && entry.substring(colonIdx + 1).toIntOrNull() != null) {
|
||||
entry.substring(0, colonIdx)
|
||||
} else {
|
||||
entry
|
||||
}
|
||||
}.filter { it.isNotBlank() }
|
||||
}
|
||||
|
||||
fun upsert(name: String, counter: Int) {
|
||||
val services = getServices().toMutableList()
|
||||
val idx = services.indexOfFirst { it.first == name }
|
||||
if (idx >= 0) {
|
||||
services[idx] = name to counter
|
||||
} else {
|
||||
services.add(0, name to counter)
|
||||
}
|
||||
save(services)
|
||||
fun upsert(name: String) {
|
||||
val names = getNames().toMutableList()
|
||||
names.remove(name)
|
||||
names.add(0, name)
|
||||
save(names)
|
||||
}
|
||||
|
||||
fun delete(name: String) {
|
||||
val services = getServices().filter { it.first != name }
|
||||
save(services)
|
||||
save(getNames().filter { it != name })
|
||||
}
|
||||
|
||||
private fun save(services: List<Pair<String, Int>>) {
|
||||
val raw = services.joinToString("|") { "${it.first}:${it.second}" }
|
||||
prefs.edit().putString(KEY_SERVICES, raw).apply()
|
||||
private fun save(names: List<String>) {
|
||||
prefs.edit().putString(KEY_SERVICES, names.joinToString("|")).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@ -98,13 +98,14 @@ fun AppNavigation() {
|
||||
viewModel = detViewModel,
|
||||
onNavigateToHistory = { navController.navigate(ROUTE_SERVICE_HISTORY) },
|
||||
onNavigateToSettings = { navController.navigate(ROUTE_SETTINGS) },
|
||||
onNavigateToExport = { navController.navigate(ROUTE_EXPORT_IMPORT) },
|
||||
)
|
||||
}
|
||||
composable(ROUTE_SERVICE_HISTORY) {
|
||||
ServiceHistoryScreen(
|
||||
viewModel = detViewModel,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onServiceSelected = { _, _ -> navController.popBackStack() },
|
||||
onServiceSelected = { _ -> navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
composable(ROUTE_SETTINGS) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package cz.bugsy.passwordzebra.ui.screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -11,36 +12,40 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
@ -54,6 +59,7 @@ fun DeterministicScreen(
|
||||
viewModel: DeterministicViewModel,
|
||||
onNavigateToHistory: () -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
onNavigateToExport: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
@ -62,6 +68,50 @@ fun DeterministicScreen(
|
||||
return
|
||||
}
|
||||
|
||||
if (state.showRotateWarning) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.dismissRotateWarning() },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.det_rotate_warning_title),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.det_rotate_warning_body))
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.dismissRotateWarning()
|
||||
onNavigateToExport()
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
),
|
||||
) {
|
||||
Text(stringResource(R.string.det_rotate_go_to_export))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.dismissRotateWarning() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.det_rotate_later),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@ -117,36 +167,6 @@ fun DeterministicScreen(
|
||||
},
|
||||
)
|
||||
|
||||
// Counter
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.det_counter),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = { viewModel.decrementCounter() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Remove,
|
||||
contentDescription = "-",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = state.counter.toString(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
IconButton(onClick = { viewModel.incrementCounter() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "+",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate button
|
||||
Button(
|
||||
onClick = { viewModel.generate() },
|
||||
@ -198,10 +218,23 @@ fun DeterministicScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate password button — shown only after a password has been generated
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.rotatePassword() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error,
|
||||
),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
||||
) {
|
||||
Icon(Icons.Default.Warning, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.det_rotate_password))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,9 @@ package cz.bugsy.passwordzebra.ui.screens
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import org.json.JSONArray
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
@ -68,17 +70,29 @@ fun ExportImportScreen(
|
||||
|
||||
val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||
val rawValue = result.contents ?: return@rememberLauncherForActivityResult
|
||||
// Parse passwordzebra://device-secret?v=1&secret=<base64>
|
||||
if (rawValue.startsWith("passwordzebra://device-secret")) {
|
||||
val secretParam = rawValue.substringAfter("secret=", "")
|
||||
if (secretParam.isNotEmpty()) {
|
||||
val decoded = try {
|
||||
Base64.decode(secretParam, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
} catch (_: Exception) { null }
|
||||
if (decoded != null && decoded.size == 32) {
|
||||
pendingImportSecret = decoded
|
||||
showConfirmDialog = true
|
||||
val uri = Uri.parse(rawValue)
|
||||
val secretParam = uri.getQueryParameter("secret") ?: ""
|
||||
if (secretParam.isNotEmpty()) {
|
||||
val decoded = try {
|
||||
Base64.decode(secretParam, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
} catch (_: Exception) { null }
|
||||
if (decoded != null && decoded.size == 32) {
|
||||
pendingImportSecret = decoded
|
||||
// Parse v2 services if present
|
||||
val servicesParam = uri.getQueryParameter("services")
|
||||
if (servicesParam != null) {
|
||||
try {
|
||||
val json = String(Base64.decode(servicesParam, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING))
|
||||
val arr = JSONArray(json)
|
||||
for (i in 0 until arr.length()) {
|
||||
val entry = arr.getJSONArray(i)
|
||||
val name = entry.getString(0)
|
||||
val counter = entry.getInt(1)
|
||||
viewModel.counterRepository.upsert(name, counter)
|
||||
}
|
||||
} catch (_: Exception) { /* ignore malformed services */ }
|
||||
}
|
||||
showConfirmDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,7 +199,16 @@ private fun ExportTab(viewModel: DeterministicViewModel) {
|
||||
LaunchedEffect(Unit) {
|
||||
val secret = viewModel.getDeviceSecret()
|
||||
val b64 = Base64.encodeToString(secret, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
val uri = "passwordzebra://device-secret?v=1&secret=$b64"
|
||||
val rotatedServices = viewModel.counterRepository.getAll().filter { it.second >= 2 }
|
||||
val uri = if (rotatedServices.isEmpty()) {
|
||||
"passwordzebra://backup?v=2&secret=$b64"
|
||||
} else {
|
||||
val json = JSONArray().apply {
|
||||
rotatedServices.forEach { (name, counter) -> put(JSONArray().put(name).put(counter)) }
|
||||
}.toString()
|
||||
val servicesB64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
"passwordzebra://backup?v=2&secret=$b64&services=$servicesB64"
|
||||
}
|
||||
qrBitmap = generateQrBitmap(uri, 512)
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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
|
||||
@ -37,10 +36,10 @@ import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
|
||||
fun ServiceHistoryScreen(
|
||||
viewModel: DeterministicViewModel,
|
||||
onNavigateBack: () -> Unit,
|
||||
onServiceSelected: (name: String, counter: Int) -> Unit,
|
||||
onServiceSelected: (name: String) -> Unit,
|
||||
) {
|
||||
// Read services on each recomposition so deletes reflect immediately
|
||||
var services by remember { mutableStateOf(viewModel.historyRepository.getServices()) }
|
||||
// Read names on each recomposition so deletes reflect immediately
|
||||
var services by remember { mutableStateOf(viewModel.historyRepository.getNames()) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@ -72,13 +71,13 @@ fun ServiceHistoryScreen(
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(services, key = { it.first }) { (name, counter) ->
|
||||
items(services, key = { it }) { name ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
viewModel.selectService(name, counter)
|
||||
onServiceSelected(name, counter)
|
||||
viewModel.selectService(name)
|
||||
onServiceSelected(name)
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
@ -92,15 +91,9 @@ fun ServiceHistoryScreen(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = "#$counter",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
IconButton(onClick = {
|
||||
viewModel.historyRepository.delete(name)
|
||||
services = viewModel.historyRepository.getServices()
|
||||
services = viewModel.historyRepository.getNames()
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
|
||||
@ -22,12 +22,16 @@
|
||||
<string name="det_service_name">Služba / Web</string>
|
||||
<string name="det_master_password">Hlavní heslo</string>
|
||||
<string name="det_toggle_pw_visibility">Zobrazit/skrýt heslo</string>
|
||||
<string name="det_counter">Čítač</string>
|
||||
<string name="det_options">Možnosti</string>
|
||||
<string name="det_generate">Generovat</string>
|
||||
<string name="det_tap_to_copy">Dotekem zkopírujte</string>
|
||||
<string name="det_history">Historie služeb</string>
|
||||
<string name="det_settings">Nastavení</string>
|
||||
<string name="det_rotate_password">Heslo kompromitováno — rotovat</string>
|
||||
<string name="det_rotate_warning_title">⚠ Aktualizujte QR zálohu — ihned!</string>
|
||||
<string name="det_rotate_warning_body">Pro tuto službu bylo vygenerováno nové heslo.\n\nVáš záložní QR kód tuto rotaci nezachycuje. Pokud ztratíte zařízení bez aktualizace QR kódu, TOTO HESLO BUDE TRVALE ZTRACENO — žádným jiným způsobem ho nelze obnovit.\n\nOstatní hesla nejsou dotčena.</string>
|
||||
<string name="det_rotate_go_to_export">Aktualizovat QR zálohu nyní</string>
|
||||
<string name="det_rotate_later">Udělám to později (riskantní)</string>
|
||||
|
||||
<!-- Historie služeb -->
|
||||
<string name="det_history_title">Historie služeb</string>
|
||||
@ -38,7 +42,7 @@
|
||||
<string name="det_export_import_title">Export / Import tajemství</string>
|
||||
<string name="det_export_tab">Export</string>
|
||||
<string name="det_import_tab">Import</string>
|
||||
<string name="det_export_body">Naskenujte tento QR kód na jiném zařízení pro přenos device secret. Uchovejte jej v tajnosti – kdokoli s tímto kódem může znovu vygenerovat vaše hesla.</string>
|
||||
<string name="det_export_body">Naskenujte tento QR kód na jiném zařízení pro obnovu trezoru. Obsahuje device secret a čítače rotovaných služeb. Uchovejte jej v tajnosti – kdokoli s tímto kódem a vaším hlavním heslem může znovu vygenerovat všechna vaše hesla.</string>
|
||||
<string name="det_export_qr_desc">QR kód pro export device secret</string>
|
||||
<string name="det_import_body">Naskenujte QR kód exportovaný z jiného zařízení pro import device secret.</string>
|
||||
<string name="det_scan_qr">Naskenovat QR kód</string>
|
||||
|
||||
@ -21,12 +21,16 @@
|
||||
<string name="det_service_name">Service / Website</string>
|
||||
<string name="det_master_password">Master Password</string>
|
||||
<string name="det_toggle_pw_visibility">Toggle password visibility</string>
|
||||
<string name="det_counter">Counter</string>
|
||||
<string name="det_options">Options</string>
|
||||
<string name="det_generate">Generate</string>
|
||||
<string name="det_tap_to_copy">Tap to copy</string>
|
||||
<string name="det_history">Service history</string>
|
||||
<string name="det_settings">Settings</string>
|
||||
<string name="det_rotate_password">Password Compromised — Rotate</string>
|
||||
<string name="det_rotate_warning_title">⚠ Update Your QR Backup — Now!</string>
|
||||
<string name="det_rotate_warning_body">A new password has been generated for this service.\n\nYour QR backup no longer reflects this rotation. If you lose this device without updating the QR code, THIS PASSWORD WILL BE PERMANENTLY LOST — it cannot be recovered any other way.\n\nYour other passwords are not affected.</string>
|
||||
<string name="det_rotate_go_to_export">Update QR Backup Now</string>
|
||||
<string name="det_rotate_later">I\'ll do it later (risky)</string>
|
||||
|
||||
<!-- Service history -->
|
||||
<string name="det_history_title">Service History</string>
|
||||
@ -37,7 +41,7 @@
|
||||
<string name="det_export_import_title">Export / Import Secret</string>
|
||||
<string name="det_export_tab">Export</string>
|
||||
<string name="det_import_tab">Import</string>
|
||||
<string name="det_export_body">Scan this QR code on another device to transfer your device secret. Keep it private — anyone with this code can regenerate your passwords.</string>
|
||||
<string name="det_export_body">Scan this QR code on another device to restore your vault. It contains your device secret and any rotated service counters. Keep it private — anyone with this code and your master password can regenerate all your passwords.</string>
|
||||
<string name="det_export_qr_desc">QR code for device secret export</string>
|
||||
<string name="det_import_body">Scan a QR code exported from another device to import its device secret.</string>
|
||||
<string name="det_scan_qr">Scan QR Code</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user