Separate service autocomplete history from password counters

This commit is contained in:
pavelb 2026-03-04 23:31:58 +01:00
parent 72fa3f9f86
commit 89469c6685
9 changed files with 206 additions and 97 deletions

View File

@ -18,12 +18,12 @@ data class DeterministicState(
val serviceName: String = "", val serviceName: String = "",
val masterPassword: CharArray = CharArray(0), val masterPassword: CharArray = CharArray(0),
val masterPasswordVisible: Boolean = false, val masterPasswordVisible: Boolean = false,
val counter: Int = 1,
val generatedPassword: String = "", val generatedPassword: String = "",
val isLocked: Boolean = false, val isLocked: Boolean = false,
val clipboardTimerSeconds: Int = 0, val clipboardTimerSeconds: Int = 0,
val biometricEnabled: Boolean = false, val biometricEnabled: Boolean = false,
val lockTimeoutMinutes: Int = 5, val lockTimeoutMinutes: Int = 5,
val showRotateWarning: Boolean = false,
) { ) {
// CharArray doesn't support structural equality; override manually // CharArray doesn't support structural equality; override manually
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -32,24 +32,24 @@ data class DeterministicState(
return serviceName == other.serviceName && return serviceName == other.serviceName &&
masterPassword.contentEquals(other.masterPassword) && masterPassword.contentEquals(other.masterPassword) &&
masterPasswordVisible == other.masterPasswordVisible && masterPasswordVisible == other.masterPasswordVisible &&
counter == other.counter &&
generatedPassword == other.generatedPassword && generatedPassword == other.generatedPassword &&
isLocked == other.isLocked && isLocked == other.isLocked &&
clipboardTimerSeconds == other.clipboardTimerSeconds && clipboardTimerSeconds == other.clipboardTimerSeconds &&
biometricEnabled == other.biometricEnabled && biometricEnabled == other.biometricEnabled &&
lockTimeoutMinutes == other.lockTimeoutMinutes lockTimeoutMinutes == other.lockTimeoutMinutes &&
showRotateWarning == other.showRotateWarning
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = serviceName.hashCode() var result = serviceName.hashCode()
result = 31 * result + masterPassword.contentHashCode() result = 31 * result + masterPassword.contentHashCode()
result = 31 * result + masterPasswordVisible.hashCode() result = 31 * result + masterPasswordVisible.hashCode()
result = 31 * result + counter
result = 31 * result + generatedPassword.hashCode() result = 31 * result + generatedPassword.hashCode()
result = 31 * result + isLocked.hashCode() result = 31 * result + isLocked.hashCode()
result = 31 * result + clipboardTimerSeconds result = 31 * result + clipboardTimerSeconds
result = 31 * result + biometricEnabled.hashCode() result = 31 * result + biometricEnabled.hashCode()
result = 31 * result + lockTimeoutMinutes result = 31 * result + lockTimeoutMinutes
result = 31 * result + showRotateWarning.hashCode()
return result return result
} }
} }
@ -58,6 +58,7 @@ class DeterministicViewModel(app: Application) : AndroidViewModel(app) {
private val deviceSecretManager = DeviceSecretManager(app) private val deviceSecretManager = DeviceSecretManager(app)
val historyRepository = ServiceHistoryRepository(app) val historyRepository = ServiceHistoryRepository(app)
val counterRepository = ServiceCounterRepository(app)
private val _state = MutableStateFlow(DeterministicState()) private val _state = MutableStateFlow(DeterministicState())
val state: StateFlow<DeterministicState> = _state.asStateFlow() val state: StateFlow<DeterministicState> = _state.asStateFlow()
@ -82,28 +83,43 @@ class DeterministicViewModel(app: Application) : AndroidViewModel(app) {
fun toggleMasterPasswordVisible() = fun toggleMasterPasswordVisible() =
_state.update { it.copy(masterPasswordVisible = !it.masterPasswordVisible) } _state.update { it.copy(masterPasswordVisible = !it.masterPasswordVisible) }
fun incrementCounter() = _state.update { it.copy(counter = it.counter + 1) } fun selectService(name: String) = _state.update { it.copy(serviceName = name) }
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 generate() { fun generate() {
val s = _state.value val s = _state.value
if (s.serviceName.isBlank() || s.masterPassword.isEmpty()) return if (s.serviceName.isBlank() || s.masterPassword.isEmpty()) return
val counter = counterRepository.getCounter(s.serviceName)
val deviceSecret = deviceSecretManager.getOrCreateSecret() val deviceSecret = deviceSecretManager.getOrCreateSecret()
val password = DeterministicPasswordGenerator.generate( val password = DeterministicPasswordGenerator.generate(
masterPassword = s.masterPassword, masterPassword = s.masterPassword,
deviceSecret = deviceSecret, deviceSecret = deviceSecret,
serviceName = s.serviceName, serviceName = s.serviceName,
counter = s.counter, counter = counter,
options = SyllableOptions(), options = SyllableOptions(),
) )
historyRepository.upsert(s.serviceName, s.counter) historyRepository.upsert(s.serviceName)
counterRepository.upsert(s.serviceName, counter)
_state.update { it.copy(generatedPassword = password) } _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) { fun copyAndScheduleClear(context: Context) {
val password = _state.value.generatedPassword val password = _state.value.generatedPassword
if (password.isEmpty()) return if (password.isEmpty()) return

View File

@ -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"
}
}

View File

@ -6,37 +6,33 @@ class ServiceHistoryRepository(context: Context) {
private val prefs = context.getSharedPreferences("service_history", Context.MODE_PRIVATE) 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, "") ?: "" val raw = prefs.getString(KEY_SERVICES, "") ?: ""
if (raw.isBlank()) return emptyList() 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(':') val colonIdx = entry.lastIndexOf(':')
if (colonIdx < 0) return@mapNotNull null if (colonIdx >= 0 && entry.substring(colonIdx + 1).toIntOrNull() != null) {
val name = entry.substring(0, colonIdx) entry.substring(0, colonIdx)
val counter = entry.substring(colonIdx + 1).toIntOrNull() ?: return@mapNotNull null } else {
name to counter entry
} }
}.filter { it.isNotBlank() }
} }
fun upsert(name: String, counter: Int) { fun upsert(name: String) {
val services = getServices().toMutableList() val names = getNames().toMutableList()
val idx = services.indexOfFirst { it.first == name } names.remove(name)
if (idx >= 0) { names.add(0, name)
services[idx] = name to counter save(names)
} else {
services.add(0, name to counter)
}
save(services)
} }
fun delete(name: String) { fun delete(name: String) {
val services = getServices().filter { it.first != name } save(getNames().filter { it != name })
save(services)
} }
private fun save(services: List<Pair<String, Int>>) { private fun save(names: List<String>) {
val raw = services.joinToString("|") { "${it.first}:${it.second}" } prefs.edit().putString(KEY_SERVICES, names.joinToString("|")).apply()
prefs.edit().putString(KEY_SERVICES, raw).apply()
} }
companion object { companion object {

View File

@ -98,13 +98,14 @@ fun AppNavigation() {
viewModel = detViewModel, viewModel = detViewModel,
onNavigateToHistory = { navController.navigate(ROUTE_SERVICE_HISTORY) }, onNavigateToHistory = { navController.navigate(ROUTE_SERVICE_HISTORY) },
onNavigateToSettings = { navController.navigate(ROUTE_SETTINGS) }, onNavigateToSettings = { navController.navigate(ROUTE_SETTINGS) },
onNavigateToExport = { navController.navigate(ROUTE_EXPORT_IMPORT) },
) )
} }
composable(ROUTE_SERVICE_HISTORY) { composable(ROUTE_SERVICE_HISTORY) {
ServiceHistoryScreen( ServiceHistoryScreen(
viewModel = detViewModel, viewModel = detViewModel,
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onServiceSelected = { _, _ -> navController.popBackStack() }, onServiceSelected = { _ -> navController.popBackStack() },
) )
} }
composable(ROUTE_SETTINGS) { composable(ROUTE_SETTINGS) {

View File

@ -1,5 +1,6 @@
package cz.bugsy.passwordzebra.ui.screens package cz.bugsy.passwordzebra.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy 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.History
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff 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.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily 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.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
@ -54,6 +59,7 @@ fun DeterministicScreen(
viewModel: DeterministicViewModel, viewModel: DeterministicViewModel,
onNavigateToHistory: () -> Unit, onNavigateToHistory: () -> Unit,
onNavigateToSettings: () -> Unit, onNavigateToSettings: () -> Unit,
onNavigateToExport: () -> Unit,
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
@ -62,6 +68,50 @@ fun DeterministicScreen(
return 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( Scaffold(
topBar = { topBar = {
TopAppBar( 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 // Generate button
Button( Button(
onClick = { viewModel.generate() }, 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)) Spacer(modifier = Modifier.height(16.dp))
} }
} }
} }

View File

@ -3,7 +3,9 @@ package cz.bugsy.passwordzebra.ui.screens
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri
import android.util.Base64 import android.util.Base64
import org.json.JSONArray
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -68,17 +70,29 @@ fun ExportImportScreen(
val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result -> val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
val rawValue = result.contents ?: return@rememberLauncherForActivityResult val rawValue = result.contents ?: return@rememberLauncherForActivityResult
// Parse passwordzebra://device-secret?v=1&secret=<base64> val uri = Uri.parse(rawValue)
if (rawValue.startsWith("passwordzebra://device-secret")) { val secretParam = uri.getQueryParameter("secret") ?: ""
val secretParam = rawValue.substringAfter("secret=", "")
if (secretParam.isNotEmpty()) { if (secretParam.isNotEmpty()) {
val decoded = try { val decoded = try {
Base64.decode(secretParam, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) Base64.decode(secretParam, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
} catch (_: Exception) { null } } catch (_: Exception) { null }
if (decoded != null && decoded.size == 32) { if (decoded != null && decoded.size == 32) {
pendingImportSecret = decoded pendingImportSecret = decoded
showConfirmDialog = true // 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) { LaunchedEffect(Unit) {
val secret = viewModel.getDeviceSecret() val secret = viewModel.getDeviceSecret()
val b64 = Base64.encodeToString(secret, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) 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) qrBitmap = generateQrBitmap(uri, 512)
} }

View File

@ -20,7 +20,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -37,10 +36,10 @@ import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
fun ServiceHistoryScreen( fun ServiceHistoryScreen(
viewModel: DeterministicViewModel, viewModel: DeterministicViewModel,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onServiceSelected: (name: String, counter: Int) -> Unit, onServiceSelected: (name: String) -> Unit,
) { ) {
// Read services on each recomposition so deletes reflect immediately // Read names on each recomposition so deletes reflect immediately
var services by remember { mutableStateOf(viewModel.historyRepository.getServices()) } var services by remember { mutableStateOf(viewModel.historyRepository.getNames()) }
Scaffold( Scaffold(
topBar = { topBar = {
@ -72,13 +71,13 @@ fun ServiceHistoryScreen(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
items(services, key = { it.first }) { (name, counter) -> items(services, key = { it }) { name ->
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
viewModel.selectService(name, counter) viewModel.selectService(name)
onServiceSelected(name, counter) onServiceSelected(name)
}, },
) { ) {
Row( Row(
@ -92,15 +91,9 @@ fun ServiceHistoryScreen(
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
Text(
text = "#$counter",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 8.dp),
)
IconButton(onClick = { IconButton(onClick = {
viewModel.historyRepository.delete(name) viewModel.historyRepository.delete(name)
services = viewModel.historyRepository.getServices() services = viewModel.historyRepository.getNames()
}) { }) {
Icon( Icon(
Icons.Default.Delete, Icons.Default.Delete,

View File

@ -22,12 +22,16 @@
<string name="det_service_name">Služba / Web</string> <string name="det_service_name">Služba / Web</string>
<string name="det_master_password">Hlavní heslo</string> <string name="det_master_password">Hlavní heslo</string>
<string name="det_toggle_pw_visibility">Zobrazit/skrýt 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_options">Možnosti</string>
<string name="det_generate">Generovat</string> <string name="det_generate">Generovat</string>
<string name="det_tap_to_copy">Dotekem zkopírujte</string> <string name="det_tap_to_copy">Dotekem zkopírujte</string>
<string name="det_history">Historie služeb</string> <string name="det_history">Historie služeb</string>
<string name="det_settings">Nastavení</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 --> <!-- Historie služeb -->
<string name="det_history_title">Historie služeb</string> <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_import_title">Export / Import tajemství</string>
<string name="det_export_tab">Export</string> <string name="det_export_tab">Export</string>
<string name="det_import_tab">Import</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_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_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> <string name="det_scan_qr">Naskenovat QR kód</string>

View File

@ -21,12 +21,16 @@
<string name="det_service_name">Service / Website</string> <string name="det_service_name">Service / Website</string>
<string name="det_master_password">Master Password</string> <string name="det_master_password">Master Password</string>
<string name="det_toggle_pw_visibility">Toggle password visibility</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_options">Options</string>
<string name="det_generate">Generate</string> <string name="det_generate">Generate</string>
<string name="det_tap_to_copy">Tap to copy</string> <string name="det_tap_to_copy">Tap to copy</string>
<string name="det_history">Service history</string> <string name="det_history">Service history</string>
<string name="det_settings">Settings</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 --> <!-- Service history -->
<string name="det_history_title">Service History</string> <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_import_title">Export / Import Secret</string>
<string name="det_export_tab">Export</string> <string name="det_export_tab">Export</string>
<string name="det_import_tab">Import</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_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_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> <string name="det_scan_qr">Scan QR Code</string>