feat(v1.8.0): IMPL_04 Backup Settings - Improved Progress Display
- Remove in-button spinner from SettingsButton and SettingsOutlinedButton
- Buttons keep text during loading and become disabled (enabled=false)
- Add BackupProgressCard component with LinearProgressIndicator + status text
- Add backupStatusText StateFlow in SettingsViewModel
- Add 3-phase status system: In Progress → Completion → Clear
- Show success completion status for 2 seconds ("Backup created!", etc.)
- Show error status for 3 seconds ("Backup failed", etc.)
- Status auto-clears after delay (no manual intervention needed)
- Update createBackup() with completion delay and status messages
- Update restoreFromFile() with completion delay and status messages
- Update restoreFromServer() with completion delay and status messages
- Remove all redundant toast messages from backup/restore operations
- Add exception logging (Logger.e) to replace swallowed exceptions
- Integrate BackupProgressCard in BackupSettingsScreen (visible during operations)
- Add delayed restore execution (200ms) to ensure dialog closes before progress shows
- Add DIALOG_CLOSE_DELAY_MS constant (200ms)
- Add STATUS_CLEAR_DELAY_SUCCESS_MS constant (2000ms)
- Add STATUS_CLEAR_DELAY_ERROR_MS constant (3000ms)
- Add 6 new backup completion/error strings (EN + DE)
- Import kotlinx.coroutines.delay for status delay functionality
This commit is contained in:
@@ -14,6 +14,7 @@ import dev.dettmer.simplenotes.sync.WebDavSyncService
|
|||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
@@ -40,6 +41,8 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SettingsViewModel"
|
private const val TAG = "SettingsViewModel"
|
||||||
private const val CONNECTION_TIMEOUT_MS = 3000
|
private const val CONNECTION_TIMEOUT_MS = 3000
|
||||||
|
private const val STATUS_CLEAR_DELAY_SUCCESS_MS = 2000L // 2s for successful operations
|
||||||
|
private const val STATUS_CLEAR_DELAY_ERROR_MS = 3000L // 3s for errors (more important)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
@@ -211,6 +214,10 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _isBackupInProgress = MutableStateFlow(false)
|
private val _isBackupInProgress = MutableStateFlow(false)
|
||||||
val isBackupInProgress: StateFlow<Boolean> = _isBackupInProgress.asStateFlow()
|
val isBackupInProgress: StateFlow<Boolean> = _isBackupInProgress.asStateFlow()
|
||||||
|
|
||||||
|
// v1.8.0: Descriptive backup status text
|
||||||
|
private val _backupStatusText = MutableStateFlow("")
|
||||||
|
val backupStatusText: StateFlow<String> = _backupStatusText.asStateFlow()
|
||||||
|
|
||||||
private val _showToast = MutableSharedFlow<String>()
|
private val _showToast = MutableSharedFlow<String>()
|
||||||
val showToast: SharedFlow<String> = _showToast.asSharedFlow()
|
val showToast: SharedFlow<String> = _showToast.asSharedFlow()
|
||||||
|
|
||||||
@@ -671,18 +678,27 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
fun createBackup(uri: Uri, password: String? = null) {
|
fun createBackup(uri: Uri, password: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isBackupInProgress.value = true
|
_isBackupInProgress.value = true
|
||||||
|
_backupStatusText.value = getString(R.string.backup_progress_creating)
|
||||||
try {
|
try {
|
||||||
val result = backupManager.createBackup(uri, password)
|
val result = backupManager.createBackup(uri, password)
|
||||||
val message = if (result.success) {
|
|
||||||
getString(R.string.toast_backup_success, result.message ?: "")
|
// Phase 2: Show completion status
|
||||||
|
_backupStatusText.value = if (result.success) {
|
||||||
|
getString(R.string.backup_progress_complete)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.toast_backup_failed, result.error ?: "")
|
getString(R.string.backup_progress_failed)
|
||||||
}
|
}
|
||||||
emitToast(message)
|
|
||||||
|
// Phase 3: Clear after delay
|
||||||
|
delay(if (result.success) STATUS_CLEAR_DELAY_SUCCESS_MS else STATUS_CLEAR_DELAY_ERROR_MS)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emitToast(getString(R.string.toast_backup_failed, e.message ?: ""))
|
Logger.e(TAG, "Failed to create backup", e)
|
||||||
|
_backupStatusText.value = getString(R.string.backup_progress_failed)
|
||||||
|
delay(STATUS_CLEAR_DELAY_ERROR_MS)
|
||||||
} finally {
|
} finally {
|
||||||
_isBackupInProgress.value = false
|
_isBackupInProgress.value = false
|
||||||
|
_backupStatusText.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -690,18 +706,27 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
fun restoreFromFile(uri: Uri, mode: RestoreMode, password: String? = null) {
|
fun restoreFromFile(uri: Uri, mode: RestoreMode, password: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isBackupInProgress.value = true
|
_isBackupInProgress.value = true
|
||||||
|
_backupStatusText.value = getString(R.string.backup_progress_restoring)
|
||||||
try {
|
try {
|
||||||
val result = backupManager.restoreBackup(uri, mode, password)
|
val result = backupManager.restoreBackup(uri, mode, password)
|
||||||
val message = if (result.success) {
|
|
||||||
getString(R.string.toast_restore_success, result.importedNotes)
|
// Phase 2: Show completion status
|
||||||
|
_backupStatusText.value = if (result.success) {
|
||||||
|
getString(R.string.restore_progress_complete)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.toast_restore_failed, result.error ?: "")
|
getString(R.string.restore_progress_failed)
|
||||||
}
|
}
|
||||||
emitToast(message)
|
|
||||||
|
// Phase 3: Clear after delay
|
||||||
|
delay(if (result.success) STATUS_CLEAR_DELAY_SUCCESS_MS else STATUS_CLEAR_DELAY_ERROR_MS)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emitToast(getString(R.string.toast_restore_failed, e.message ?: ""))
|
Logger.e(TAG, "Failed to restore backup from file", e)
|
||||||
|
_backupStatusText.value = getString(R.string.restore_progress_failed)
|
||||||
|
delay(STATUS_CLEAR_DELAY_ERROR_MS)
|
||||||
} finally {
|
} finally {
|
||||||
_isBackupInProgress.value = false
|
_isBackupInProgress.value = false
|
||||||
|
_backupStatusText.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,22 +757,30 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
fun restoreFromServer(mode: RestoreMode) {
|
fun restoreFromServer(mode: RestoreMode) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isBackupInProgress.value = true
|
_isBackupInProgress.value = true
|
||||||
|
_backupStatusText.value = getString(R.string.backup_progress_restoring_server)
|
||||||
try {
|
try {
|
||||||
emitToast(getString(R.string.restore_progress))
|
|
||||||
val syncService = WebDavSyncService(getApplication())
|
val syncService = WebDavSyncService(getApplication())
|
||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
syncService.restoreFromServer(mode)
|
syncService.restoreFromServer(mode)
|
||||||
}
|
}
|
||||||
val message = if (result.isSuccess) {
|
|
||||||
getString(R.string.toast_restore_success, result.restoredCount)
|
// Phase 2: Show completion status
|
||||||
|
_backupStatusText.value = if (result.isSuccess) {
|
||||||
|
getString(R.string.restore_server_progress_complete)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.toast_restore_failed, result.errorMessage ?: "")
|
getString(R.string.restore_server_progress_failed)
|
||||||
}
|
}
|
||||||
emitToast(message)
|
|
||||||
|
// Phase 3: Clear after delay
|
||||||
|
delay(if (result.isSuccess) STATUS_CLEAR_DELAY_SUCCESS_MS else STATUS_CLEAR_DELAY_ERROR_MS)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emitToast(getString(R.string.toast_error, e.message ?: ""))
|
Logger.e(TAG, "Failed to restore from server", e)
|
||||||
|
_backupStatusText.value = getString(R.string.restore_server_progress_failed)
|
||||||
|
delay(STATUS_CLEAR_DELAY_ERROR_MS)
|
||||||
} finally {
|
} finally {
|
||||||
_isBackupInProgress.value = false
|
_isBackupInProgress.value = false
|
||||||
|
_backupStatusText.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package dev.dettmer.simplenotes.ui.settings.components
|
package dev.dettmer.simplenotes.ui.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
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.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -11,12 +15,14 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary filled button for settings actions
|
* Primary filled button for settings actions
|
||||||
* v1.5.0: Jetpack Compose Settings Redesign
|
* v1.5.0: Jetpack Compose Settings Redesign
|
||||||
|
* v1.8.0: Button keeps text during loading, just becomes disabled
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsButton(
|
fun SettingsButton(
|
||||||
@@ -31,20 +37,13 @@ fun SettingsButton(
|
|||||||
enabled = enabled && !isLoading,
|
enabled = enabled && !isLoading,
|
||||||
modifier = modifier.fillMaxWidth()
|
modifier = modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
if (isLoading) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.height(20.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outlined secondary button for settings actions
|
* Outlined secondary button for settings actions
|
||||||
|
* v1.8.0: Button keeps text during loading, just becomes disabled
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsOutlinedButton(
|
fun SettingsOutlinedButton(
|
||||||
@@ -59,17 +58,9 @@ fun SettingsOutlinedButton(
|
|||||||
enabled = enabled && !isLoading,
|
enabled = enabled && !isLoading,
|
||||||
modifier = modifier.fillMaxWidth()
|
modifier = modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
if (isLoading) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.height(20.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Danger/destructive button for settings actions
|
* Danger/destructive button for settings actions
|
||||||
@@ -159,3 +150,48 @@ fun SettingsDivider(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1.8.0: Backup progress indicator shown above buttons
|
||||||
|
* Replaces the ugly in-button spinner with a clear status display
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BackupProgressCard(
|
||||||
|
statusText: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = androidx.compose.material3.CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = statusText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
androidx.compose.material3.LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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.runtime.mutableStateOf
|
||||||
@@ -27,6 +28,7 @@ import dev.dettmer.simplenotes.R
|
|||||||
import dev.dettmer.simplenotes.backup.RestoreMode
|
import dev.dettmer.simplenotes.backup.RestoreMode
|
||||||
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
|
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
|
||||||
import dev.dettmer.simplenotes.ui.settings.components.BackupPasswordDialog
|
import dev.dettmer.simplenotes.ui.settings.components.BackupPasswordDialog
|
||||||
|
import dev.dettmer.simplenotes.ui.settings.components.BackupProgressCard
|
||||||
import dev.dettmer.simplenotes.ui.settings.components.RadioOption
|
import dev.dettmer.simplenotes.ui.settings.components.RadioOption
|
||||||
import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
|
import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
|
||||||
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
|
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
|
||||||
@@ -39,6 +41,10 @@ import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
// v1.8.0: Delay for dialog close animation before starting restore
|
||||||
|
private const val DIALOG_CLOSE_DELAY_MS = 200L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backup and restore settings screen
|
* Backup and restore settings screen
|
||||||
@@ -60,6 +66,10 @@ fun BackupSettingsScreen(
|
|||||||
var pendingRestoreUri by remember { mutableStateOf<Uri?>(null) }
|
var pendingRestoreUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
var selectedRestoreMode by remember { mutableStateOf(RestoreMode.MERGE) }
|
var selectedRestoreMode by remember { mutableStateOf(RestoreMode.MERGE) }
|
||||||
|
|
||||||
|
// v1.8.0: Trigger for delayed restore execution (after dialog closes)
|
||||||
|
var triggerRestore by remember { mutableStateOf(0) }
|
||||||
|
var pendingRestoreAction by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
|
|
||||||
// 🔐 v1.7.0: Encryption state
|
// 🔐 v1.7.0: Encryption state
|
||||||
var encryptBackup by remember { mutableStateOf(false) }
|
var encryptBackup by remember { mutableStateOf(false) }
|
||||||
var showEncryptionPasswordDialog by remember { mutableStateOf(false) }
|
var showEncryptionPasswordDialog by remember { mutableStateOf(false) }
|
||||||
@@ -91,6 +101,15 @@ fun BackupSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.8.0: Delayed restore execution after dialog closes
|
||||||
|
LaunchedEffect(triggerRestore) {
|
||||||
|
if (triggerRestore > 0) {
|
||||||
|
delay(DIALOG_CLOSE_DELAY_MS) // Wait for dialog close animation
|
||||||
|
pendingRestoreAction?.invoke()
|
||||||
|
pendingRestoreAction = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsScaffold(
|
SettingsScaffold(
|
||||||
title = stringResource(R.string.backup_settings_title),
|
title = stringResource(R.string.backup_settings_title),
|
||||||
onBack = onBack
|
onBack = onBack
|
||||||
@@ -108,6 +127,16 @@ fun BackupSettingsScreen(
|
|||||||
text = stringResource(R.string.backup_auto_info)
|
text = stringResource(R.string.backup_auto_info)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// v1.8.0: Progress indicator (visible during backup/restore)
|
||||||
|
if (isBackupInProgress) {
|
||||||
|
val backupStatus by viewModel.backupStatusText.collectAsState()
|
||||||
|
BackupProgressCard(
|
||||||
|
statusText = backupStatus.ifEmpty {
|
||||||
|
stringResource(R.string.backup_progress_creating)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Local Backup Section
|
// Local Backup Section
|
||||||
@@ -234,6 +263,8 @@ fun BackupSettingsScreen(
|
|||||||
when (restoreSource) {
|
when (restoreSource) {
|
||||||
RestoreSource.LocalFile -> {
|
RestoreSource.LocalFile -> {
|
||||||
pendingRestoreUri?.let { uri ->
|
pendingRestoreUri?.let { uri ->
|
||||||
|
// v1.8.0: Schedule restore with delay for dialog close
|
||||||
|
pendingRestoreAction = {
|
||||||
// 🔐 v1.7.0: Check if backup is encrypted
|
// 🔐 v1.7.0: Check if backup is encrypted
|
||||||
viewModel.checkBackupEncryption(
|
viewModel.checkBackupEncryption(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
@@ -246,10 +277,16 @@ fun BackupSettingsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
triggerRestore++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RestoreSource.Server -> {
|
RestoreSource.Server -> {
|
||||||
|
// v1.8.0: Schedule restore with delay for dialog close
|
||||||
|
pendingRestoreAction = {
|
||||||
viewModel.restoreFromServer(selectedRestoreMode)
|
viewModel.restoreFromServer(selectedRestoreMode)
|
||||||
}
|
}
|
||||||
|
triggerRestore++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
|
|||||||
@@ -443,6 +443,21 @@
|
|||||||
<string name="about_changelog_title">Changelog</string>
|
<string name="about_changelog_title">Changelog</string>
|
||||||
<string name="about_changelog_subtitle">Vollständige Versionshistorie</string>
|
<string name="about_changelog_subtitle">Vollständige Versionshistorie</string>
|
||||||
|
|
||||||
|
<!-- v1.8.0: Backup Progress -->
|
||||||
|
<string name="backup_progress_creating">Backup wird erstellt…</string>
|
||||||
|
<string name="backup_progress_restoring">Backup wird wiederhergestellt…</string>
|
||||||
|
<string name="backup_progress_restoring_server">Notizen werden vom Server heruntergeladen…</string>
|
||||||
|
|
||||||
|
<!-- v1.8.0: Backup Progress - Completion -->
|
||||||
|
<string name="backup_progress_complete">Backup erstellt!</string>
|
||||||
|
<string name="restore_progress_complete">Wiederherstellung abgeschlossen!</string>
|
||||||
|
<string name="restore_server_progress_complete">Download abgeschlossen!</string>
|
||||||
|
|
||||||
|
<!-- v1.8.0: Backup Progress - Error -->
|
||||||
|
<string name="backup_progress_failed">Backup fehlgeschlagen</string>
|
||||||
|
<string name="restore_progress_failed">Wiederherstellung fehlgeschlagen</string>
|
||||||
|
<string name="restore_server_progress_failed">Download fehlgeschlagen</string>
|
||||||
|
|
||||||
<string name="about_privacy_title">🔒 Datenschutz</string>
|
<string name="about_privacy_title">🔒 Datenschutz</string>
|
||||||
<string name="about_privacy_text">Diese App sammelt keine Daten. Alle Notizen werden nur lokal auf deinem Gerät und auf deinem eigenen WebDAV-Server gespeichert. Keine Telemetrie, keine Werbung.</string>
|
<string name="about_privacy_text">Diese App sammelt keine Daten. Alle Notizen werden nur lokal auf deinem Gerät und auf deinem eigenen WebDAV-Server gespeichert. Keine Telemetrie, keine Werbung.</string>
|
||||||
|
|
||||||
|
|||||||
@@ -443,6 +443,21 @@
|
|||||||
<string name="about_changelog_title">Changelog</string>
|
<string name="about_changelog_title">Changelog</string>
|
||||||
<string name="about_changelog_subtitle">Full version history</string>
|
<string name="about_changelog_subtitle">Full version history</string>
|
||||||
|
|
||||||
|
<!-- v1.8.0: Backup Progress -->
|
||||||
|
<string name="backup_progress_creating">Creating backup…</string>
|
||||||
|
<string name="backup_progress_restoring">Restoring backup…</string>
|
||||||
|
<string name="backup_progress_restoring_server">Downloading notes from server…</string>
|
||||||
|
|
||||||
|
<!-- v1.8.0: Backup Progress - Completion -->
|
||||||
|
<string name="backup_progress_complete">Backup created!</string>
|
||||||
|
<string name="restore_progress_complete">Restore complete!</string>
|
||||||
|
<string name="restore_server_progress_complete">Download complete!</string>
|
||||||
|
|
||||||
|
<!-- v1.8.0: Backup Progress - Error -->
|
||||||
|
<string name="backup_progress_failed">Backup failed</string>
|
||||||
|
<string name="restore_progress_failed">Restore failed</string>
|
||||||
|
<string name="restore_server_progress_failed">Download failed</string>
|
||||||
|
|
||||||
<string name="about_privacy_title">🔒 Privacy</string>
|
<string name="about_privacy_title">🔒 Privacy</string>
|
||||||
<string name="about_privacy_text">This app collects no data. All notes are stored only locally on your device and on your own WebDAV server. No telemetry, no ads.</string>
|
<string name="about_privacy_text">This app collects no data. All notes are stored only locally on your device and on your own WebDAV server. No telemetry, no ads.</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user