Переключение языка
This commit is contained in:
@@ -67,6 +67,8 @@ dependencies {
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Wallenc"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="37">
|
||||
<activity
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var yandexSignInService: YandexSignInService
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.github.nullptroma.wallenc.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.work.Configuration
|
||||
import com.github.nullptroma.wallenc.app.di.HiltWorkerFactoryEntryPoint
|
||||
import com.github.nullptroma.wallenc.app.locale.AppLocaleStorage
|
||||
import com.github.nullptroma.wallenc.app.sync.StorageSyncBootstrap
|
||||
import com.github.nullptroma.wallenc.app.tasks.TaskPipelineForegroundBootstrap
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
@@ -18,8 +20,15 @@ class WallencApplication : Application(), Configuration.Provider {
|
||||
@Inject
|
||||
lateinit var storageSyncBootstrap: StorageSyncBootstrap
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
AppLocaleStorage.applyStored(base)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
AppLocaleStorage.migrateLegacyDataStoreIfNeeded(this)
|
||||
AppLocaleStorage.applyStored(this)
|
||||
taskPipelineForegroundBootstrap.start()
|
||||
storageSyncBootstrap.start()
|
||||
}
|
||||
@@ -34,4 +43,4 @@ class WallencApplication : Application(), Configuration.Provider {
|
||||
.setWorkerFactory(factory)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.github.nullptroma.wallenc.app.di
|
||||
|
||||
import com.github.nullptroma.wallenc.app.locale.AppLocaleControllerImpl
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLocaleController
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class LocaleModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAppLocaleController(impl: AppLocaleControllerImpl): AppLocaleController
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.github.nullptroma.wallenc.app.locale
|
||||
|
||||
import android.content.Context
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLanguage
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLocaleController
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AppLocaleControllerImpl @Inject constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
) : AppLocaleController {
|
||||
|
||||
override val language: Flow<AppLanguage> = AppLocaleStorage.languageFlow(context)
|
||||
|
||||
override suspend fun setLanguage(language: AppLanguage) {
|
||||
AppLocaleStorage.persistLanguage(context, language)
|
||||
}
|
||||
|
||||
override suspend fun applyStoredLocale() {
|
||||
AppLocaleStorage.applyStored(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.github.nullptroma.wallenc.app.locale
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLanguage
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Хранение выбранного языка. SharedPreferences — для синхронного чтения в
|
||||
* [android.app.Application.attachBaseContext]; DataStore — только для миграции со старой версии.
|
||||
*/
|
||||
internal object AppLocaleStorage {
|
||||
|
||||
private const val PREFS_NAME = "app_locale"
|
||||
private const val PREFS_KEY_LANGUAGE = "app_language"
|
||||
|
||||
private val legacyLanguageKey = stringPreferencesKey(PREFS_KEY_LANGUAGE)
|
||||
|
||||
private val Context.legacyLocaleDataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = "app_locale",
|
||||
)
|
||||
|
||||
/** В [android.app.Application.attachBaseContext] [Context.getApplicationContext] ещё null. */
|
||||
private fun storageContext(context: Context): Context =
|
||||
context.applicationContext ?: context
|
||||
|
||||
private fun prefs(context: Context) =
|
||||
storageContext(context).getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
fun languageFlow(context: Context): Flow<AppLanguage> = callbackFlow {
|
||||
val storage = storageContext(context)
|
||||
val listener =
|
||||
android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == PREFS_KEY_LANGUAGE) {
|
||||
trySend(readLanguageSync(storage))
|
||||
}
|
||||
}
|
||||
trySend(readLanguageSync(storage))
|
||||
prefs(storage).registerOnSharedPreferenceChangeListener(listener)
|
||||
awaitClose { prefs(storage).unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}
|
||||
|
||||
fun persistLanguage(context: Context, language: AppLanguage) {
|
||||
prefs(context).edit().putString(PREFS_KEY_LANGUAGE, language.storageValue).apply()
|
||||
applyLanguage(language)
|
||||
}
|
||||
|
||||
fun readLanguageSync(context: Context): AppLanguage {
|
||||
val raw = prefs(context).getString(PREFS_KEY_LANGUAGE, null)
|
||||
return raw?.toAppLanguage() ?: AppLanguage.System
|
||||
}
|
||||
|
||||
/** Без блокировок; безопасно из [android.app.Application.attachBaseContext]. */
|
||||
fun applyStored(context: Context) {
|
||||
applyLanguage(readLanguageSync(context))
|
||||
}
|
||||
|
||||
fun applyLanguage(language: AppLanguage) {
|
||||
val localeList = when (language) {
|
||||
AppLanguage.System -> LocaleListCompat.getEmptyLocaleList()
|
||||
AppLanguage.English -> LocaleListCompat.forLanguageTags("en")
|
||||
AppLanguage.Russian -> LocaleListCompat.forLanguageTags("ru")
|
||||
}
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
|
||||
/** Однократно переносит значение из DataStore (если SP ещё пуст). */
|
||||
fun migrateLegacyDataStoreIfNeeded(context: Context) {
|
||||
val storage = storageContext(context)
|
||||
if (prefs(storage).contains(PREFS_KEY_LANGUAGE)) {
|
||||
return
|
||||
}
|
||||
runBlocking {
|
||||
runCatching {
|
||||
val legacy = storage.legacyLocaleDataStore.data.first()[legacyLanguageKey]
|
||||
if (legacy != null) {
|
||||
prefs(storage).edit().putString(PREFS_KEY_LANGUAGE, legacy).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toAppLanguage(): AppLanguage = when (this) {
|
||||
STORAGE_EN -> AppLanguage.English
|
||||
STORAGE_RU -> AppLanguage.Russian
|
||||
else -> AppLanguage.System
|
||||
}
|
||||
|
||||
private val AppLanguage.storageValue: String
|
||||
get() = when (this) {
|
||||
AppLanguage.System -> STORAGE_SYSTEM
|
||||
AppLanguage.English -> STORAGE_EN
|
||||
AppLanguage.Russian -> STORAGE_RU
|
||||
}
|
||||
|
||||
private const val STORAGE_SYSTEM = "system"
|
||||
private const val STORAGE_EN = "en"
|
||||
private const val STORAGE_RU = "ru"
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundItem
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.resources.resolve
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -43,6 +45,9 @@ class TaskPipelineForegroundService : Service() {
|
||||
@Inject
|
||||
lateinit var orchestrator: ITaskOrchestrator
|
||||
|
||||
@Inject
|
||||
lateinit var uiStrings: UiStringResolver
|
||||
|
||||
private var repeat = false
|
||||
private var canPush = true
|
||||
private var lastUiState: TaskForegroundUiState? = null
|
||||
@@ -291,7 +296,7 @@ class TaskPipelineForegroundService : Service() {
|
||||
remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.VISIBLE)
|
||||
remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.VISIBLE)
|
||||
remoteViews.setTextViewText(TASK_TITLE_IDS[index], task.title)
|
||||
val label = task.progress?.label?.trim().orEmpty()
|
||||
val label = task.progress?.label?.resolve(uiStrings).orEmpty()
|
||||
val fraction = task.progress?.fraction
|
||||
if (fraction != null) {
|
||||
if (label.isNotEmpty()) {
|
||||
|
||||
9
app/src/main/res/values-ru/strings.xml
Normal file
9
app/src/main/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Wallenc</string>
|
||||
<string name="task_notification_channel_name">Фоновые задачи</string>
|
||||
<string name="task_notification_title">Задачи Wallenc</string>
|
||||
<string name="task_notification_preparing">Подготовка…</string>
|
||||
<string name="task_notification_indeterminate">Выполняется…</string>
|
||||
<string name="task_notification_cancel">Отмена</string>
|
||||
</resources>
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Wallenc</string>
|
||||
<string name="task_notification_channel_name">Фоновые задачи</string>
|
||||
<string name="task_notification_title">Задачи Wallenc</string>
|
||||
<string name="task_notification_preparing">Подготовка…</string>
|
||||
<string name="task_notification_indeterminate">Выполняется…</string>
|
||||
<string name="task_notification_cancel">Отмена</string>
|
||||
<string name="task_notification_channel_name">Background tasks</string>
|
||||
<string name="task_notification_title">Wallenc tasks</string>
|
||||
<string name="task_notification_preparing">Preparing…</string>
|
||||
<string name="task_notification_indeterminate">Running…</string>
|
||||
<string name="task_notification_cancel">Cancel</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.Wallenc" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<style name="Theme.Wallenc" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- До первого кадра Compose и системный splash (12+) -->
|
||||
<item name="android:windowBackground">@color/splash_screen_background</item>
|
||||
<item name="android:windowSplashScreenBackground" tools:targetApi="31">
|
||||
|
||||
5
app/src/main/res/xml/locales_config.xml
Normal file
5
app/src/main/res/xml/locales_config.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en" />
|
||||
<locale android:name="ru" />
|
||||
</locale-config>
|
||||
@@ -7,6 +7,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -132,7 +133,7 @@ abstract class BaseStorage(
|
||||
onProgress(
|
||||
TaskProgress(
|
||||
fraction = done.toFloat() / total,
|
||||
label = "$done / $total",
|
||||
label = TaskProgressLabel.ClearContentProgress(done, total),
|
||||
),
|
||||
)
|
||||
coroutineContext.ensureActive()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.github.nullptroma.wallenc.domain.interfaces
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import java.util.UUID
|
||||
|
||||
enum class StorageSyncGroupEncryptionKind {
|
||||
@@ -24,10 +25,10 @@ interface IStorageSyncGroupStore {
|
||||
|
||||
interface IStorageSyncEngine {
|
||||
suspend fun syncAllGroups(
|
||||
reportProgress: (suspend (fraction: Float?, label: String?) -> Unit)? = null,
|
||||
reportProgress: (suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit)? = null,
|
||||
)
|
||||
suspend fun syncGroup(
|
||||
groupId: String,
|
||||
reportProgress: (suspend (fraction: Float?, label: String?) -> Unit)? = null,
|
||||
reportProgress: (suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit)? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||
interface TaskContext {
|
||||
val taskId: TaskId
|
||||
|
||||
suspend fun reportProgress(fraction: Float?, label: String?)
|
||||
suspend fun reportProgress(fraction: Float?, label: TaskProgressLabel? = null)
|
||||
|
||||
suspend fun reportProgress(progress: TaskProgress) = reportProgress(progress.fraction, progress.label)
|
||||
|
||||
fun log(level: TaskLogLevel, message: String)
|
||||
|
||||
fun log(level: TaskLogLevel, key: TaskLogKey)
|
||||
|
||||
fun fail(error: WallencException): Nothing
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.nullptroma.wallenc.domain.tasks
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||
|
||||
sealed class TaskLogKey {
|
||||
data object SyncStarted : TaskLogKey()
|
||||
data object SyncFinished : TaskLogKey()
|
||||
data class SyncFailed(val error: WallencException) : TaskLogKey()
|
||||
}
|
||||
@@ -3,5 +3,6 @@ package com.github.nullptroma.wallenc.domain.tasks
|
||||
data class TaskLogLine(
|
||||
val timestampMs: Long,
|
||||
val level: TaskLogLevel,
|
||||
val message: String,
|
||||
val message: String = "",
|
||||
val logKey: TaskLogKey? = null,
|
||||
)
|
||||
|
||||
@@ -3,5 +3,5 @@ package com.github.nullptroma.wallenc.domain.tasks
|
||||
data class TaskProgress(
|
||||
/** 0f..1f or null if indeterminate */
|
||||
val fraction: Float?,
|
||||
val label: String?,
|
||||
val label: TaskProgressLabel? = null,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.github.nullptroma.wallenc.domain.tasks
|
||||
|
||||
sealed class TaskProgressLabel {
|
||||
data object SyncNoGroups : TaskProgressLabel()
|
||||
data object SyncStarted : TaskProgressLabel()
|
||||
data object SyncCompleted : TaskProgressLabel()
|
||||
data class SyncPreparing(val groupCount: Int) : TaskProgressLabel()
|
||||
|
||||
data class SyncGroupPreparing(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupNotFound(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupSkippedTooFewStorages(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupSkippedIncompatibleEncryption(val groupId: String, val count: Int) : TaskProgressLabel()
|
||||
data class SyncGroupAcquiringLocks(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupLockProgress(val groupId: String, val current: Int, val total: Int) : TaskProgressLabel()
|
||||
data class SyncGroupLockFailed(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupReadingJournals(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupCancelled(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupJournalProgress(val groupId: String, val current: Int, val total: Int) : TaskProgressLabel()
|
||||
data class SyncGroupNoJournalEntries(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupProcessingEntries(val groupId: String, val count: Int) : TaskProgressLabel()
|
||||
data class SyncGroupEntryProgress(val groupId: String, val current: Int, val total: Int) : TaskProgressLabel()
|
||||
data class SyncGroupCompleted(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupRenewingLocks(val groupId: String) : TaskProgressLabel()
|
||||
data class SyncGroupLockRenewalFailed(val groupId: String) : TaskProgressLabel()
|
||||
|
||||
data class ClearContentProgress(val done: Int, val total: Int) : TaskProgressLabel()
|
||||
|
||||
data class VaultTask(val step: VaultTaskStep) : TaskProgressLabel()
|
||||
|
||||
data class TestElapsed(val elapsedSec: Int, val totalSec: Int) : TaskProgressLabel()
|
||||
}
|
||||
|
||||
enum class VaultTaskStep {
|
||||
DumpStorageLog,
|
||||
CreateStorage,
|
||||
EnableEncryption,
|
||||
DecryptRunning,
|
||||
CloseStorage,
|
||||
DisableEncryption,
|
||||
RenameStorage,
|
||||
RemoveStorage,
|
||||
ClearSyncLock,
|
||||
AddRemoteVault,
|
||||
RemoveRemoteVault,
|
||||
RetryRemoteVault,
|
||||
Save2FaToken,
|
||||
Delete2FaToken,
|
||||
SaveTextSecret,
|
||||
DeleteTextSecret,
|
||||
}
|
||||
@@ -26,6 +26,8 @@ hiltWork = "1.3.0"
|
||||
cameraX = "1.6.1"
|
||||
mlkitBarcode = "17.3.0"
|
||||
javaOtp = "0.4.0"
|
||||
appcompat = "1.7.1"
|
||||
datastore = "1.2.0"
|
||||
|
||||
[libraries]
|
||||
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" }
|
||||
@@ -60,6 +62,8 @@ androidx-hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref
|
||||
androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltWork" }
|
||||
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
|
||||
@@ -10,8 +10,10 @@ import com.github.nullptroma.wallenc.domain.tasks.TaskContext
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundItem
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogKey
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLine
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -143,11 +145,12 @@ class TaskOrchestrator(
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendLogLine(level: TaskLogLevel, message: String) {
|
||||
private fun appendLogLine(level: TaskLogLevel, message: String, logKey: TaskLogKey? = null) {
|
||||
val line = TaskLogLine(
|
||||
timestampMs = System.currentTimeMillis(),
|
||||
level = level,
|
||||
message = message,
|
||||
logKey = logKey,
|
||||
)
|
||||
synchronized(logLock) {
|
||||
if (logBuffer.size >= MAX_LOG_LINES) {
|
||||
@@ -180,7 +183,7 @@ class TaskOrchestrator(
|
||||
val ctx = TaskContextImpl(
|
||||
taskId = taskId,
|
||||
onRunningProgress = { p -> onRunningProgress(taskId, p) },
|
||||
appendLog = { level, msg -> appendLogLine(level, msg) },
|
||||
appendLog = { level, msg, key -> appendLogLine(level, msg, key) },
|
||||
)
|
||||
try {
|
||||
if (cancelRequested[taskId] == true) {
|
||||
@@ -213,14 +216,18 @@ class TaskOrchestrator(
|
||||
private class TaskContextImpl(
|
||||
override val taskId: TaskId,
|
||||
private val onRunningProgress: (TaskProgress) -> Unit,
|
||||
private val appendLog: (TaskLogLevel, String) -> Unit,
|
||||
private val appendLog: (TaskLogLevel, String, TaskLogKey?) -> Unit,
|
||||
) : TaskContext {
|
||||
override suspend fun reportProgress(fraction: Float?, label: String?) {
|
||||
override suspend fun reportProgress(fraction: Float?, label: TaskProgressLabel?) {
|
||||
onRunningProgress(TaskProgress(fraction, label))
|
||||
}
|
||||
|
||||
override fun log(level: TaskLogLevel, message: String) {
|
||||
appendLog(level, message)
|
||||
appendLog(level, message, null)
|
||||
}
|
||||
|
||||
override fun log(level: TaskLogLevel, key: TaskLogKey) {
|
||||
appendLog(level, "", key)
|
||||
}
|
||||
|
||||
override fun fail(error: WallencException): Nothing {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.List
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Sync
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -99,13 +100,11 @@ fun WallencNavRoot(
|
||||
StorageSyncRoute::class.qualifiedName!!,
|
||||
Icons.Rounded.Sync,
|
||||
),
|
||||
// Settings temporarily hidden from top-level menu.
|
||||
// Uncomment to restore:
|
||||
// SettingsRoute::class.qualifiedName!! to NavBarItemData(
|
||||
// R.string.nav_label_settings,
|
||||
// SettingsRoute::class.qualifiedName!!,
|
||||
// Icons.Rounded.Settings,
|
||||
// ),
|
||||
SettingsRoute::class.qualifiedName!! to NavBarItemData(
|
||||
R.string.nav_label_settings,
|
||||
SettingsRoute::class.qualifiedName!!,
|
||||
Icons.Rounded.Settings,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.github.nullptroma.wallenc.ui.locale
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
enum class AppLanguage {
|
||||
System,
|
||||
English,
|
||||
Russian,
|
||||
}
|
||||
|
||||
interface AppLocaleController {
|
||||
val language: Flow<AppLanguage>
|
||||
|
||||
suspend fun setLanguage(language: AppLanguage)
|
||||
|
||||
/** Applies persisted locale; call from [android.app.Application.onCreate]. */
|
||||
suspend fun applyStoredLocale()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.github.nullptroma.wallenc.ui.resources
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogKey
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
|
||||
fun TaskLogKey.resolve(resolver: UiStringResolver): String = when (this) {
|
||||
TaskLogKey.SyncStarted -> resolver(R.string.task_log_sync_started)
|
||||
TaskLogKey.SyncFinished -> resolver(R.string.task_log_sync_finished)
|
||||
is TaskLogKey.SyncFailed -> {
|
||||
val notification = error.toUserNotification().resolve(resolver)
|
||||
resolver(R.string.task_log_sync_failed, notification)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.github.nullptroma.wallenc.ui.resources
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLine
|
||||
|
||||
@Composable
|
||||
fun TaskLogLine.displayText(): String {
|
||||
val key = logKey
|
||||
if (key != null) {
|
||||
val context = LocalContext.current
|
||||
val resolver = UiStringResolver { id, args ->
|
||||
if (args.isEmpty()) {
|
||||
context.getString(id)
|
||||
} else {
|
||||
context.getString(id, *args)
|
||||
}
|
||||
}
|
||||
return key.resolve(resolver)
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.github.nullptroma.wallenc.ui.resources
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.VaultTaskStep
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
|
||||
fun TaskProgressLabel.resolve(resolver: UiStringResolver): String = when (this) {
|
||||
TaskProgressLabel.SyncNoGroups -> resolver(R.string.sync_progress_no_groups)
|
||||
TaskProgressLabel.SyncStarted -> resolver(R.string.sync_progress_started)
|
||||
TaskProgressLabel.SyncCompleted -> resolver(R.string.sync_progress_completed)
|
||||
is TaskProgressLabel.SyncPreparing -> resolver(R.string.sync_progress_preparing, groupCount)
|
||||
|
||||
is TaskProgressLabel.SyncGroupPreparing -> resolver(R.string.sync_progress_group_preparing, groupId)
|
||||
is TaskProgressLabel.SyncGroupNotFound -> resolver(R.string.sync_progress_group_not_found, groupId)
|
||||
is TaskProgressLabel.SyncGroupSkippedTooFewStorages ->
|
||||
resolver(R.string.sync_progress_group_skipped_few_storages, groupId)
|
||||
is TaskProgressLabel.SyncGroupSkippedIncompatibleEncryption ->
|
||||
resolver(R.string.sync_progress_group_skipped_incompatible, groupId, count)
|
||||
is TaskProgressLabel.SyncGroupAcquiringLocks ->
|
||||
resolver(R.string.sync_progress_group_acquiring_locks, groupId)
|
||||
is TaskProgressLabel.SyncGroupLockProgress ->
|
||||
resolver(R.string.sync_progress_group_lock, groupId, current, total)
|
||||
is TaskProgressLabel.SyncGroupLockFailed ->
|
||||
resolver(R.string.sync_progress_group_lock_failed, groupId)
|
||||
is TaskProgressLabel.SyncGroupReadingJournals ->
|
||||
resolver(R.string.sync_progress_group_reading_journals, groupId)
|
||||
is TaskProgressLabel.SyncGroupCancelled ->
|
||||
resolver(R.string.sync_progress_group_cancelled, groupId)
|
||||
is TaskProgressLabel.SyncGroupJournalProgress ->
|
||||
resolver(R.string.sync_progress_group_journal, groupId, current, total)
|
||||
is TaskProgressLabel.SyncGroupNoJournalEntries ->
|
||||
resolver(R.string.sync_progress_group_no_entries, groupId)
|
||||
is TaskProgressLabel.SyncGroupProcessingEntries ->
|
||||
resolver(R.string.sync_progress_group_processing, groupId, count)
|
||||
is TaskProgressLabel.SyncGroupEntryProgress ->
|
||||
resolver(R.string.sync_progress_group_entry, groupId, current, total)
|
||||
is TaskProgressLabel.SyncGroupCompleted ->
|
||||
resolver(R.string.sync_progress_group_completed, groupId)
|
||||
is TaskProgressLabel.SyncGroupRenewingLocks ->
|
||||
resolver(R.string.sync_progress_group_renewing_locks, groupId)
|
||||
is TaskProgressLabel.SyncGroupLockRenewalFailed ->
|
||||
resolver(R.string.sync_progress_group_lock_renewal_failed, groupId)
|
||||
|
||||
is TaskProgressLabel.ClearContentProgress ->
|
||||
resolver(R.string.task_progress_clear_content, done, total)
|
||||
|
||||
is TaskProgressLabel.VaultTask -> when (step) {
|
||||
VaultTaskStep.DumpStorageLog -> resolver(R.string.task_progress_dump_storage_log)
|
||||
VaultTaskStep.CreateStorage -> resolver(R.string.task_progress_create_storage)
|
||||
VaultTaskStep.EnableEncryption -> resolver(R.string.task_progress_enable_encryption)
|
||||
VaultTaskStep.DecryptRunning -> resolver(R.string.task_progress_decrypt_running)
|
||||
VaultTaskStep.CloseStorage -> resolver(R.string.task_progress_close_storage)
|
||||
VaultTaskStep.DisableEncryption -> resolver(R.string.task_progress_disable_encryption)
|
||||
VaultTaskStep.RenameStorage -> resolver(R.string.task_progress_rename_storage)
|
||||
VaultTaskStep.RemoveStorage -> resolver(R.string.task_progress_remove_storage)
|
||||
VaultTaskStep.ClearSyncLock -> resolver(R.string.task_progress_clear_sync_lock)
|
||||
VaultTaskStep.AddRemoteVault -> resolver(R.string.task_progress_add_remote_vault)
|
||||
VaultTaskStep.RemoveRemoteVault -> resolver(R.string.task_progress_remove_remote_vault)
|
||||
VaultTaskStep.RetryRemoteVault -> resolver(R.string.task_progress_retry_remote_vault)
|
||||
VaultTaskStep.Save2FaToken -> resolver(R.string.task_progress_save_2fa_token)
|
||||
VaultTaskStep.Delete2FaToken -> resolver(R.string.task_progress_delete_2fa_token)
|
||||
VaultTaskStep.SaveTextSecret -> resolver(R.string.task_progress_save_text_secret)
|
||||
VaultTaskStep.DeleteTextSecret -> resolver(R.string.task_progress_delete_text_secret)
|
||||
}
|
||||
|
||||
is TaskProgressLabel.TestElapsed ->
|
||||
resolver(R.string.task_pipeline_test_elapsed, elapsedSec, totalSec)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.github.nullptroma.wallenc.ui.resources
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
|
||||
@Composable
|
||||
fun TaskProgressLabel.resolveText(): String {
|
||||
val context = LocalContext.current
|
||||
return resolve(
|
||||
UiStringResolver { id, args ->
|
||||
if (args.isEmpty()) {
|
||||
context.getString(id)
|
||||
} else {
|
||||
context.getString(id, *args)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -14,3 +14,14 @@ fun UserNotification.resolveText(): String = when (this) {
|
||||
}
|
||||
is UserNotification.Plain -> message
|
||||
}
|
||||
|
||||
fun UserNotification.resolve(resolver: UiStringResolver): String = when (this) {
|
||||
is UserNotification.TextRes -> {
|
||||
if (formatArgs.isEmpty()) {
|
||||
resolver(id)
|
||||
} else {
|
||||
resolver(id, *formatArgs.toTypedArray())
|
||||
}
|
||||
}
|
||||
is UserNotification.Plain -> message
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.resources.resolve
|
||||
import com.github.nullptroma.wallenc.ui.screens.ScreenRoute
|
||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.remotes.RemoteVaultsRoute
|
||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.vault.LocalVaultRoute
|
||||
@@ -127,7 +128,7 @@ class MainViewModel @Inject constructor(
|
||||
private fun activeWorkStatusFromProgress(title: String, progress: TaskProgress?): MainWorkStatus.Active {
|
||||
val frac = progress?.fraction
|
||||
val indeterminate = progress == null || frac == null
|
||||
val label = progress?.label?.takeIf { it.isNotBlank() }
|
||||
val label = progress?.label?.resolve(uiStrings)
|
||||
val line = if (label != null) "$title$TITLE_LABEL_SEPARATOR$label" else title
|
||||
return MainWorkStatus.Active(
|
||||
line = line,
|
||||
|
||||
@@ -4,6 +4,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.VaultTaskStep
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
@@ -77,12 +79,12 @@ class RemoteVaultsViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_add_remote_vault))
|
||||
ctx.log(TaskLogLevel.Info, "Adding vault…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.AddRemoteVault))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_adding_vault))
|
||||
vaultRegistrar.register(registration)
|
||||
ctx.log(TaskLogLevel.Info, "Vault added")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_vault_added))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to add vault")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_add_vault_failed))
|
||||
} finally {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
setBusy(false)
|
||||
@@ -110,12 +112,12 @@ class RemoteVaultsViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_remove_remote_vault))
|
||||
ctx.log(TaskLogLevel.Info, "Removing remote vault…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.RemoveRemoteVault))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_removing_remote_vault))
|
||||
vaultRegistrar.unregister(uuid)
|
||||
ctx.log(TaskLogLevel.Info, "Remote vault removed")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_remote_vault_removed))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to remove vault")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_remove_vault_failed))
|
||||
} finally {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
setBusy(false)
|
||||
@@ -133,12 +135,12 @@ class RemoteVaultsViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_retry_remote_vault))
|
||||
ctx.log(TaskLogLevel.Info, "Retrying remote vault connection…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.RetryRemoteVault))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_retrying_vault))
|
||||
vaultRegistrar.retry(vaultUuid)
|
||||
ctx.log(TaskLogLevel.Info, "Retry requested")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_retry_requested))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to retry remote vault")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_retry_vault_failed))
|
||||
} finally {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
setBusy(false)
|
||||
|
||||
@@ -88,6 +88,7 @@ fun TextSecretDetailsScreen(
|
||||
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(secret.items) { item ->
|
||||
val clipboardFallbackLabel = stringResource(R.string.text_secret_clipboard_fallback_label)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.elevatedCardColors(
|
||||
@@ -118,7 +119,7 @@ fun TextSecretDetailsScreen(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val clipData = ClipData.newPlainText(
|
||||
item.label ?: "value",
|
||||
item.label ?: clipboardFallbackLabel,
|
||||
item.value,
|
||||
)
|
||||
clipboard.setClipEntry(clipData.toClipEntry())
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.VaultTaskStep
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
@@ -105,7 +107,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { ctx ->
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_delete_text_secret))
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.DeleteTextSecret))
|
||||
manageTextSecretsUseCase.delete(storage, secretId)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.VaultTaskStep
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
@@ -110,7 +112,7 @@ class TextSecretEditViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { ctx ->
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_save_text_secret))
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.SaveTextSecret))
|
||||
if (existingId == null) {
|
||||
manageTextSecretsUseCase.create(
|
||||
storageInfo = storage,
|
||||
|
||||
@@ -4,6 +4,8 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.VaultTaskStep
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||
@@ -104,7 +106,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { ctx ->
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_save_2fa_token))
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.Save2FaToken))
|
||||
if (existingId == null) {
|
||||
manageTwoFaTokensUseCase.create(
|
||||
storageInfo = storage,
|
||||
@@ -152,7 +154,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { ctx ->
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_delete_2fa_token))
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.Delete2FaToken))
|
||||
manageTwoFaTokensUseCase.delete(storage, id)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -36,6 +36,8 @@ import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.resources.displayText
|
||||
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
||||
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -97,7 +99,7 @@ fun TaskPipelineScreen(
|
||||
TaskLogLevel.Error -> "E"
|
||||
}
|
||||
Text(
|
||||
"[$prefix] ${line.message}",
|
||||
"[$prefix] ${line.displayText()}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -185,7 +187,7 @@ private fun TaskRow(task: PipelineTask, isRunning: Boolean) {
|
||||
else MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
val runningProgress = (task.state as? TaskRunState.Running)?.progress
|
||||
val progressLabel = runningProgress?.label?.takeIf { it.isNotBlank() }
|
||||
val progressLabel = runningProgress?.label?.resolveText()
|
||||
val stateLabel = when (val s = task.state) {
|
||||
TaskRunState.Queued -> stringResource(R.string.task_state_queued)
|
||||
is TaskRunState.Running ->
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.tasks
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -29,17 +30,17 @@ class TaskPipelineViewModel @Inject constructor(
|
||||
dispatcher = Dispatchers.Default,
|
||||
work = { ctx ->
|
||||
val steps = if (safeDurationSec == 0) 1 else safeDurationSec * 10
|
||||
ctx.log(TaskLogLevel.Info, "Test task started for ${safeDurationSec}s")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_test_started, safeDurationSec))
|
||||
for (step in 0..steps) {
|
||||
val fraction = step.toFloat() / steps.toFloat()
|
||||
val elapsedMs = (fraction * safeDurationSec * 1000).toInt()
|
||||
val elapsedSec = ((fraction * safeDurationSec * 1000).toInt()) / 1000
|
||||
ctx.reportProgress(
|
||||
fraction = if (infinityIndeterminateProgress) null else fraction,
|
||||
label = "Elapsed: ${elapsedMs / 1000}s / ${safeDurationSec}s",
|
||||
label = TaskProgressLabel.TestElapsed(elapsedSec, safeDurationSec),
|
||||
)
|
||||
if (step < steps) delay(100)
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, "Test task finished")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_test_finished))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.domain.tasks.VaultTaskStep
|
||||
import com.github.nullptroma.wallenc.usecases.GetOpenedStoragesUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageStoragesEncryptionUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageVaultUseCase
|
||||
@@ -162,9 +164,9 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = id,
|
||||
work = { ctx ->
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_dump_storage_log))
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.DumpStorageLog))
|
||||
storageFileManagementUseCase.setStorage(storage)
|
||||
ctx.log(TaskLogLevel.Info, "Enumerating files and directories…")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_enumerating))
|
||||
val files: List<IFile>
|
||||
val dirs: List<IDirectory>
|
||||
val time = measureTimeMillis {
|
||||
@@ -181,7 +183,7 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
logger.debug("Storage", storage.toPrintable())
|
||||
ctx.log(
|
||||
TaskLogLevel.Info,
|
||||
"Done: ${files.size} files, ${dirs.size} dirs in ${time}ms (see app log for lines)",
|
||||
uiStrings(R.string.task_log_enumerate_done, files.size, dirs.size, time),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -204,17 +206,17 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
locksVaultStorageList = true,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_create_storage))
|
||||
ctx.log(TaskLogLevel.Info, "Creating storage…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.CreateStorage))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_creating_storage))
|
||||
val uuid = resolveCreateVaultUuid()
|
||||
?: throw IllegalStateException("Vault is not available")
|
||||
logger.debug(TAG, "createStorage: vaultUuid=$uuid")
|
||||
val storage = manageVaultUseCase.createStorage(uuid)
|
||||
ctx.log(TaskLogLevel.Info, "Storage created")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_storage_created))
|
||||
logger.debug(TAG, "createStorage: done storageUuid=${storage.uuid}")
|
||||
} catch (e: Exception) {
|
||||
logger.debug(TAG, "createStorage failed: ${e.stackTraceToString()}")
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: e.toString())
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_add_vault_failed))
|
||||
throw e
|
||||
}
|
||||
},
|
||||
@@ -239,35 +241,35 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
busyStorageUuid = id,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_enable_encryption))
|
||||
ctx.log(TaskLogLevel.Info, "Checking storage…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.EnableEncryption))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_checking_storage))
|
||||
when (manageStoragesEncryptionUseCase.canEncrypt(storage)) {
|
||||
ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> {
|
||||
ctx.log(TaskLogLevel.Info, "Encrypting…")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encrypting))
|
||||
manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath)
|
||||
manageStoragesEncryptionUseCase.openStorage(storage, key, rememberPassword)
|
||||
ctx.log(TaskLogLevel.Info, "Encryption enabled")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encryption_enabled))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_enabled))
|
||||
}
|
||||
ManageStoragesEncryptionUseCase.CanEncryptResult.AlreadyEncrypted -> {
|
||||
ctx.log(TaskLogLevel.Info, "Storage is already encrypted")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_already_encrypted))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_storage_already_encrypted))
|
||||
}
|
||||
ManageStoragesEncryptionUseCase.CanEncryptResult.StorageIsNotEmpty -> {
|
||||
ctx.log(TaskLogLevel.Info, "Storage is not empty")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_not_empty))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_storage_not_empty))
|
||||
}
|
||||
ManageStoragesEncryptionUseCase.CanEncryptResult.StorageStateUnknown -> {
|
||||
ctx.log(TaskLogLevel.Info, "Cannot determine whether storage is empty")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_empty_unknown))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_storage_empty_state_unknown))
|
||||
}
|
||||
ManageStoragesEncryptionUseCase.CanEncryptResult.UnsupportedStorageType -> {
|
||||
ctx.log(TaskLogLevel.Info, "Unsupported storage type")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_unsupported_type))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_unsupported_storage_type))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to enable encryption")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_enable_encryption_failed))
|
||||
emitTaskError(e)
|
||||
}
|
||||
},
|
||||
@@ -287,12 +289,12 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
busyStorageUuid = id,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_decrypt_running))
|
||||
ctx.log(TaskLogLevel.Info, "Opening encrypted storage…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.DecryptRunning))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_opening_storage))
|
||||
manageStoragesEncryptionUseCase.openStorage(storage, key, rememberPassword)
|
||||
ctx.log(TaskLogLevel.Info, "Storage opened")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_storage_opened))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to open encrypted storage")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_open_storage_failed))
|
||||
emitTaskError(e)
|
||||
}
|
||||
},
|
||||
@@ -311,12 +313,12 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
busyStorageUuid = id,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_close_storage))
|
||||
ctx.log(TaskLogLevel.Info, "Closing storage…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.CloseStorage))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_closing_storage))
|
||||
manageStoragesEncryptionUseCase.closeStorage(storage)
|
||||
ctx.log(TaskLogLevel.Info, "Storage closed")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_storage_closed))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to close encrypted storage")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_close_storage_failed))
|
||||
emitTaskError(e)
|
||||
}
|
||||
},
|
||||
@@ -335,17 +337,18 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
busyStorageUuid = id,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_disable_encryption))
|
||||
ctx.log(TaskLogLevel.Info, "Disabling encryption…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.DisableEncryption))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_disabling_encryption))
|
||||
manageStoragesEncryptionUseCase.clearAndDisableEncryption(storage) { p ->
|
||||
val label = p.label?.takeIf { it.isNotBlank() }
|
||||
?: uiStrings(R.string.task_progress_disable_encryption)
|
||||
ctx.reportProgress(p.fraction, label)
|
||||
ctx.reportProgress(
|
||||
p.fraction,
|
||||
p.label ?: TaskProgressLabel.VaultTask(VaultTaskStep.DisableEncryption),
|
||||
)
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, "Encryption disabled")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encryption_disabled))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_disabled))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_disable_encryption_failed))
|
||||
emitTaskError(e)
|
||||
}
|
||||
},
|
||||
@@ -364,12 +367,12 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
busyStorageUuid = id,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_rename_storage))
|
||||
ctx.log(TaskLogLevel.Info, "Renaming…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.RenameStorage))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_renaming))
|
||||
renameStorageUseCase.rename(storage, newName)
|
||||
ctx.log(TaskLogLevel.Info, "Renamed")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_renamed))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Rename failed")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_rename_failed))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -388,12 +391,12 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
locksVaultStorageList = true,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_remove_storage))
|
||||
ctx.log(TaskLogLevel.Info, "Removing storage…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.RemoveStorage))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_removing_storage))
|
||||
removeStorageUseCase.remove(storage)
|
||||
ctx.log(TaskLogLevel.Info, "Removed")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_removed))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Remove failed")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_remove_failed))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -435,17 +438,17 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
try {
|
||||
val s = storage as? IStorage
|
||||
if (s == null) {
|
||||
ctx.log(TaskLogLevel.Error, "Invalid storage")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_invalid_storage))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_invalid_storage_for_sync_lock))
|
||||
return@enqueue
|
||||
}
|
||||
ctx.reportProgress(null, uiStrings(R.string.task_progress_clear_sync_lock))
|
||||
ctx.log(TaskLogLevel.Info, "Clearing sync lock…")
|
||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.ClearSyncLock))
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_clearing_sync_lock))
|
||||
s.accessor.forceClearSyncLock()
|
||||
ctx.log(TaskLogLevel.Info, "Sync lock cleared")
|
||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_sync_lock_cleared))
|
||||
_userNotifications.emit(UserNotification.TextRes(R.string.msg_sync_lock_cleared))
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "clear sync lock failed")
|
||||
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_clear_sync_lock_failed))
|
||||
emitTaskError(e)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,21 +2,86 @@ package com.github.nullptroma.wallenc.ui.screens.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLanguage
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(modifier: Modifier, viewModel: SettingsViewModel) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.settings_title))
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.settings_language_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Column(Modifier.selectableGroup()) {
|
||||
LanguageOption(
|
||||
label = stringResource(R.string.settings_language_system),
|
||||
selected = state.selectedLanguage == AppLanguage.System,
|
||||
onClick = { viewModel.setLanguage(AppLanguage.System) },
|
||||
)
|
||||
LanguageOption(
|
||||
label = stringResource(R.string.settings_language_english),
|
||||
selected = state.selectedLanguage == AppLanguage.English,
|
||||
onClick = { viewModel.setLanguage(AppLanguage.English) },
|
||||
)
|
||||
LanguageOption(
|
||||
label = stringResource(R.string.settings_language_russian),
|
||||
selected = state.selectedLanguage == AppLanguage.Russian,
|
||||
onClick = { viewModel.setLanguage(AppLanguage.Russian) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LanguageOption(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
role = Role.RadioButton,
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(selected = selected, onClick = onClick)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.ui.screens.settings
|
||||
|
||||
class SettingsScreenState
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLanguage
|
||||
|
||||
data class SettingsScreenState(
|
||||
val selectedLanguage: AppLanguage = AppLanguage.System,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
package com.github.nullptroma.wallenc.ui.screens.settings
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLanguage
|
||||
import com.github.nullptroma.wallenc.ui.locale.AppLocaleController
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @javax.inject.Inject constructor() :
|
||||
ViewModelBase<SettingsScreenState>(SettingsScreenState())
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val appLocaleController: AppLocaleController,
|
||||
) : ViewModelBase<SettingsScreenState>(SettingsScreenState()) {
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
appLocaleController.language.collect { language ->
|
||||
updateState(state.value.copy(selectedLanguage = language))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setLanguage(language: AppLanguage) {
|
||||
viewModelScope.launch {
|
||||
appLocaleController.setLanguage(language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.resources.resolve
|
||||
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||
import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
|
||||
import com.github.nullptroma.wallenc.usecases.ManageStorageSyncGroupsUseCase
|
||||
@@ -74,7 +75,7 @@ class StorageSyncViewModel @Inject constructor(
|
||||
Triple(
|
||||
syncRunning,
|
||||
progress?.fraction,
|
||||
progress?.label?.takeIf { it.isNotBlank() },
|
||||
progress?.label?.resolve(uiStrings),
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
337
ui/src/main/res/values-ru/strings.xml
Normal file
337
ui/src/main/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,337 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="nav_label_local_vault">Локальное хранилище</string>
|
||||
<string name="nav_cd_local_vault">Локальное хранилище</string>
|
||||
<string name="nav_label_remote_vaults">Удалённые хранилища</string>
|
||||
<string name="nav_cd_remote_vaults">Удалённые хранилища</string>
|
||||
<string name="nav_label_main">Главная</string>
|
||||
<string name="nav_label_sync">Синхронизация</string>
|
||||
<string name="nav_label_settings">Настройки</string>
|
||||
<string name="main_work_status_label">Статус:</string>
|
||||
<string name="main_status_multiple_tasks">Выполняется задач: %1$d</string>
|
||||
<string name="main_status_vault_scanning_storages">Сканирование vault: загрузка списка хранилищ…</string>
|
||||
<string name="settings_title">Настройки</string>
|
||||
<string name="sync_groups_title">Группы синхронизации</string>
|
||||
<string name="sync_progress_section_title">Синхронизация хранилищ</string>
|
||||
<string name="sync_groups_busy_section_title">Сохранение групп синхронизации</string>
|
||||
<string name="sync_run_now">Запустить синхронизацию</string>
|
||||
<string name="sync_cd_run_now">Запустить синхронизацию сейчас</string>
|
||||
<string name="sync_refresh">Обновить</string>
|
||||
<string name="sync_add_storage">Добавить хранилище в группу</string>
|
||||
<string name="sync_remove_group">Удалить группу</string>
|
||||
<string name="sync_group_empty">В группе нет хранилищ</string>
|
||||
<string name="sync_remove_storage">Убрать хранилище из группы</string>
|
||||
<string name="sync_picker_back">Назад</string>
|
||||
<string name="sync_cd_picker_back">Закрыть выбор хранилища</string>
|
||||
<string name="sync_picker_title">Выбор хранилища для %1$s</string>
|
||||
<string name="sync_picker_add">Добавить</string>
|
||||
<string name="sync_picker_added">Добавлено</string>
|
||||
<string name="sync_picker_cd_add">Добавить хранилище в группу</string>
|
||||
<string name="sync_picker_no_storages">В этом хранилище нет доступных каталогов</string>
|
||||
<string name="sync_picker_expand">Развернуть</string>
|
||||
<string name="sync_picker_collapse">Свернуть</string>
|
||||
<string name="sync_fab_create_group_cd">Создать группу синхронизации</string>
|
||||
<string name="sync_group_mixed_encryption_warning">В группе разное шифрование: задайте единый режим</string>
|
||||
<string name="sync_group_incompatible_warning">Несовместимые хранилища в группе: %1$d</string>
|
||||
<string name="sync_group_policy_line">Политика шифрования группы: %1$s</string>
|
||||
<string name="sync_group_policy_unset">Не определена (группа пуста)</string>
|
||||
<string name="sync_group_policy_plain">Только незашифрованные</string>
|
||||
<string name="sync_group_policy_password">Только зашифрованные с паролем группы</string>
|
||||
<string name="sync_remove_group_confirm_title">Удалить группу?</string>
|
||||
<string name="sync_remove_group_confirm_message">Удалить группу синхронизации «%1$s»?</string>
|
||||
<string name="sync_remove_storage_confirm_title">Убрать хранилище?</string>
|
||||
<string name="sync_remove_storage_confirm_message">Убрать хранилище «%1$s» из группы?</string>
|
||||
<string name="sync_confirm_delete">Удалить</string>
|
||||
<string name="sync_cancel">Отмена</string>
|
||||
<string name="sync_msg_group_created">Создана группа %1$s</string>
|
||||
<string name="sync_msg_group_removed">Группа удалена</string>
|
||||
<string name="sync_msg_storage_added">Хранилище добавлено в %1$s</string>
|
||||
<string name="sync_msg_storage_removed">Хранилище убрано из %1$s</string>
|
||||
<string name="sync_msg_storage_already_added">Хранилище уже добавлено в группу</string>
|
||||
<string name="sync_msg_only_plain_storage_allowed">В группы синхронизации можно добавлять только незашифрованные хранилища</string>
|
||||
<string name="sync_msg_storage_encryption_key_required">Для зашифрованного хранилища нужно знать пароль (откройте его перед добавлением)</string>
|
||||
<string name="sync_msg_storage_incompatible_encryption">Хранилище не совместимо с политикой шифрования группы</string>
|
||||
<string name="sync_msg_virtual_storage_not_supported">Нельзя добавлять открытое виртуальное хранилище: синхронизация работает с исходными raw storage</string>
|
||||
<string name="sync_msg_task_enqueued">Задача синхронизации поставлена в очередь</string>
|
||||
<string name="sync_msg_sync_already_running">Синхронизация уже выполняется</string>
|
||||
<string name="sync_msg_blocked_during_sync">Дождитесь окончания синхронизации</string>
|
||||
<string name="sync_encryption_unknown">Неизвестно</string>
|
||||
<string name="sync_storage_encryption_line">Шифрование: %1$s</string>
|
||||
<string name="sync_storage_missing_title">Не найдено в текущих vault</string>
|
||||
<string name="sync_storage_pending_vault_scan">Ожидание: список хранилищ в vault ещё загружается</string>
|
||||
<string name="sync_storage_not_in_vaults">Нет в дереве хранилищ (удалено, другой аккаунт или не прошёл init)</string>
|
||||
<string name="sync_storage_unreachable">Хранилище недоступно (vault или сеть)</string>
|
||||
<string name="no_name"><без имени></string>
|
||||
<string name="show_storage_item_menu">Меню хранилища</string>
|
||||
<string name="storage_row_task_running_cd">Выполняется операция с этим хранилищем</string>
|
||||
<string name="storage_menu_busy">%1$s (задача выполняется)</string>
|
||||
<string name="rename">Переименовать</string>
|
||||
<string name="remove">Удалить</string>
|
||||
<string name="encrypt">Шифрование</string>
|
||||
<string name="new_name_title">Новое имя</string>
|
||||
<string name="remove_confirmation_dialog">Удалить хранилище «%1$s»?</string>
|
||||
<string name="storage_lock_actions">Действия с шифрованием</string>
|
||||
<string name="storage_sync_lock_checking">Проверка блокировки…</string>
|
||||
<string name="storage_sync_unlock_action">Снять блокировку синхронизации</string>
|
||||
<string name="storage_sync_not_locked">Синхронизация не заблокирована</string>
|
||||
<string name="storage_field_available">Доступно: %1$s</string>
|
||||
<string name="storage_value_yes">да</string>
|
||||
<string name="storage_value_no">нет</string>
|
||||
<string name="storage_field_files">Файлов: %1$s</string>
|
||||
<string name="storage_field_size">Размер: %1$s</string>
|
||||
<string name="storage_field_virtual">Виртуальное: %1$s</string>
|
||||
<string name="storage_unavailable_hint">Хранилище недоступно</string>
|
||||
<string name="storage_menu_unavailable">Недоступно: %1$s</string>
|
||||
<string name="storage_status_not_encrypted">Не зашифровано</string>
|
||||
<string name="storage_status_encrypted_open">Зашифровано (открыто)</string>
|
||||
<string name="storage_status_encrypted_closed">Зашифровано (закрыто)</string>
|
||||
<string name="vault_fab_add_storage_cd">Создать хранилище</string>
|
||||
<string name="vault_fab_add_storage_disabled_cd">Создание недоступно: хранилище недоступно</string>
|
||||
<string name="vault_fab_add_storage_busy_cd">Создание хранилища уже выполняется</string>
|
||||
<string name="vault_msg_storage_pipeline_busy">С этим хранилищем уже выполняется операция</string>
|
||||
<string name="vault_msg_vault_list_mutation_busy">Список хранилищ сейчас меняется — подождите</string>
|
||||
<string name="vault_unavailable_banner">Хранилище недоступно. Проверьте сеть, путь или разблокировку.</string>
|
||||
<string name="vault_loading_storages">Загрузка списка хранилищ…</string>
|
||||
<string name="vault_empty_list_hint">В этом хранилище пока нет каталогов. Создайте хранилище кнопкой «+», когда оно доступно.</string>
|
||||
<string name="task_pipeline_title">Очередь задач</string>
|
||||
<string name="task_pipeline_jobs">Задачи</string>
|
||||
<string name="task_pipeline_log">Журнал</string>
|
||||
<string name="task_pipeline_cancel_all">Отменить все</string>
|
||||
<string name="task_pipeline_open">Открыть очередь задач</string>
|
||||
<string name="task_pipeline_run_test">Тестовая задача</string>
|
||||
<string name="task_pipeline_test_dialog_title">Параметры тестовой задачи</string>
|
||||
<string name="task_pipeline_test_dialog_duration">Длительность: %1$d с</string>
|
||||
<string name="task_pipeline_test_dialog_start">Запустить</string>
|
||||
<string name="task_pipeline_test_dialog_cancel">Отмена</string>
|
||||
<string name="task_pipeline_test_dialog_infinity">Бесконечно (неопределённый прогресс)</string>
|
||||
<string name="task_pipeline_test_running">Тестовая задача (%1$d с)</string>
|
||||
<string name="task_pipeline_test_running_infinity">Тестовая задача (%1$d с, ∞)</string>
|
||||
<string name="task_state_queued">В очереди</string>
|
||||
<string name="task_state_running">Выполняется</string>
|
||||
<string name="task_state_completed">Завершено</string>
|
||||
<string name="task_state_cancelled">Отменено</string>
|
||||
<string name="task_state_failed">Ошибка: %1$s</string>
|
||||
<string name="task_title_dump_storage_log">Выгрузка дерева в журнал</string>
|
||||
<string name="task_title_create_storage">Создание хранилища</string>
|
||||
<string name="task_title_enable_encryption">Включение шифрования</string>
|
||||
<string name="task_title_open_encrypted_storage">Расшифровка и открытие хранилища</string>
|
||||
<string name="task_progress_decrypt_running">Расшифровка…</string>
|
||||
<string name="task_progress_dump_storage_log">Сканирование дерева…</string>
|
||||
<string name="task_progress_create_storage">Создание хранилища…</string>
|
||||
<string name="task_progress_enable_encryption">Шифрование…</string>
|
||||
<string name="task_progress_close_storage">Закрытие хранилища…</string>
|
||||
<string name="task_progress_disable_encryption">Очистка содержимого…</string>
|
||||
<string name="task_progress_rename_storage">Переименование…</string>
|
||||
<string name="task_progress_remove_storage">Удаление…</string>
|
||||
<string name="task_progress_clear_sync_lock">Снятие блокировки…</string>
|
||||
<string name="task_progress_add_remote_vault">Добавление…</string>
|
||||
<string name="task_progress_remove_remote_vault">Удаление…</string>
|
||||
<string name="task_progress_retry_remote_vault">Подключение…</string>
|
||||
<string name="task_progress_save_2fa_token">Сохранение…</string>
|
||||
<string name="task_progress_delete_2fa_token">Удаление…</string>
|
||||
<string name="task_progress_save_text_secret">Сохранение…</string>
|
||||
<string name="task_progress_delete_text_secret">Удаление…</string>
|
||||
<string name="task_title_close_encrypted_storage">Закрытие зашифрованного хранилища</string>
|
||||
<string name="task_title_disable_encryption">Отключение шифрования</string>
|
||||
<string name="task_title_rename_storage">Переименование хранилища</string>
|
||||
<string name="task_title_remove_storage">Удаление хранилища</string>
|
||||
<string name="task_title_clear_sync_lock">Снятие блокировки синхронизации</string>
|
||||
<string name="task_title_add_remote_vault">Добавление удалённого хранилища</string>
|
||||
<string name="task_title_remove_remote_vault">Удаление удалённого хранилища</string>
|
||||
<string name="task_title_retry_remote_vault">Повторное подключение удалённого хранилища</string>
|
||||
<string name="task_title_storage_sync">Синхронизация хранилищ</string>
|
||||
<string name="task_title_storage_sync_background">Фоновая синхронизация хранилищ</string>
|
||||
<string name="task_title_save_2fa_token">Сохранение 2FA токена</string>
|
||||
<string name="task_title_delete_2fa_token">Удаление 2FA токена</string>
|
||||
<string name="task_title_save_text_secret">Сохранение текстового секрета</string>
|
||||
<string name="task_title_delete_text_secret">Удаление текстового секрета</string>
|
||||
<string name="error_storage_not_found">Хранилище не найдено</string>
|
||||
<string name="error_storage_locked_view">Откройте расшифрованное отображение storage для работы с этим разделом</string>
|
||||
<string name="error_secret_not_found">Секрет не найден</string>
|
||||
<string name="error_storage_not_writable">Хранилище недоступно для записи</string>
|
||||
<string name="error_file_not_found">Файл не найден</string>
|
||||
<string name="error_incorrect_password">Неверный пароль</string>
|
||||
<string name="error_storage_not_encrypted">Хранилище не зашифровано</string>
|
||||
<string name="error_enc_info_missing">Отсутствуют метаданные шифрования</string>
|
||||
<string name="error_delete_root_forbidden">Нельзя удалить корень хранилища</string>
|
||||
<string name="error_not_a_file">Ожидался файл</string>
|
||||
<string name="error_not_a_directory">Ожидалась папка</string>
|
||||
<string name="error_path_is_file">Путь указывает на файл, а не на папку</string>
|
||||
<string name="error_cannot_write_over_directory">Нельзя записать поверх папки</string>
|
||||
<string name="error_unexpected_state">Хранилище в неожиданном состоянии</string>
|
||||
<string name="error_network">Ошибка сети или сервера</string>
|
||||
<string name="error_disk_resource_locked">Ресурс временно заблокирован. Повторите позже.</string>
|
||||
<string name="error_unknown">Что-то пошло не так</string>
|
||||
<string name="sync_error_group_not_found">Группа синхронизации не найдена</string>
|
||||
<string name="vault_link_error_auth">Не удалось войти</string>
|
||||
<string name="vault_link_error_not_registered">Вход не готов. Перезапустите приложение.</string>
|
||||
<string name="vault_link_error_unknown">Не удалось войти</string>
|
||||
<string name="vault_link_error_unsupported_brand">Этот провайдер не поддерживается</string>
|
||||
<string name="msg_encryption_enabled">Шифрование включено</string>
|
||||
<string name="msg_storage_already_encrypted">Хранилище уже зашифровано</string>
|
||||
<string name="msg_storage_not_empty">Хранилище не пустое</string>
|
||||
<string name="msg_storage_empty_state_unknown">Не удалось определить, пусто ли хранилище</string>
|
||||
<string name="msg_unsupported_storage_type">Неподдерживаемый тип хранилища</string>
|
||||
<string name="msg_encryption_disabled">Шифрование отключено</string>
|
||||
<string name="msg_invalid_storage_for_sync_lock">Некорректное хранилище</string>
|
||||
<string name="msg_sync_lock_cleared">Блокировка синхронизации снята</string>
|
||||
<string name="remote_vaults_add_cd">Добавить удалённое хранилище</string>
|
||||
<string name="remote_vaults_empty_hint">Пока нет удалённых хранилищ. Нажмите «+», чтобы добавить Yandex.</string>
|
||||
<string name="remote_vaults_add_title">Добавить хранилище</string>
|
||||
<string name="remote_vaults_add_pick_provider">Выберите провайдера:</string>
|
||||
<string name="remote_vaults_provider_yandex">Яндекс</string>
|
||||
<string name="remote_vaults_add_cancel">Отмена</string>
|
||||
<string name="remote_vault_type_yandex">Яндекс</string>
|
||||
<string name="remote_vault_unavailable">Хранилище временно недоступно</string>
|
||||
<string name="remote_vault_retry_action">Повторить подключение</string>
|
||||
<string name="remote_vault_retrying">Пробую подключиться…</string>
|
||||
<string name="remote_vault_delete_cd">Удалить удалённое хранилище</string>
|
||||
<string name="remote_vault_remove_title">Удалить удалённое хранилище?</string>
|
||||
<string name="remote_vault_remove_message">Удалить «%1$s» с этого устройства? Данные на сервере не удаляются.</string>
|
||||
<string name="dialog_cancel">Отмена</string>
|
||||
<string name="dialog_ok">ОК</string>
|
||||
<string name="dialog_encryption_enable_title">Включить шифрование</string>
|
||||
<string name="dialog_password_label">Пароль</string>
|
||||
<string name="dialog_encrypt_paths">Шифровать пути</string>
|
||||
<string name="dialog_apply">Применить</string>
|
||||
<string name="dialog_open_encrypted_title">Открыть зашифрованное хранилище</string>
|
||||
<string name="dialog_remember_password">Запомнить пароль</string>
|
||||
<string name="dialog_open">Открыть</string>
|
||||
<string name="dialog_close">Закрыть</string>
|
||||
<string name="dialog_disable_encryption">Отключить шифрование</string>
|
||||
<string name="dialog_done">Готово</string>
|
||||
<string name="vault_type_local_device">Локальное устройство</string>
|
||||
<string name="vault_type_remote">Удалённое: %1$s</string>
|
||||
<string name="vault_type_unknown">Неизвестный тип</string>
|
||||
<string name="vault_title_local">Локальное хранилище</string>
|
||||
<string name="vault_title_unknown">Неизвестное хранилище</string>
|
||||
<string name="enc_status_not_encrypted">Не зашифровано</string>
|
||||
<string name="enc_status_encrypted_open">Зашифровано (открыто)</string>
|
||||
<string name="enc_status_encrypted">Зашифровано</string>
|
||||
<string name="text_edit_screen_title">Текст</string>
|
||||
<string name="text_edit_screen_placeholder">Содержимое: %1$s</string>
|
||||
<string name="storage_home_unnamed_storage">Storage</string>
|
||||
<string name="storage_home_status_line">Статус: %1$s, %2$s</string>
|
||||
<string name="storage_home_status_available">доступно</string>
|
||||
<string name="storage_home_status_unavailable">недоступно</string>
|
||||
<string name="storage_home_status_encrypted">зашифровано</string>
|
||||
<string name="storage_home_status_not_encrypted">не зашифровано</string>
|
||||
<string name="storage_home_two_fa_title">2FA токены (%1$d)</string>
|
||||
<string name="storage_home_open_two_fa">Открыть 2FA</string>
|
||||
<string name="storage_home_two_fa_subtitle">Коды и секреты двухфакторной аутентификации</string>
|
||||
<string name="storage_home_text_secrets_title">Текстовые секреты (%1$d)</string>
|
||||
<string name="storage_home_open_text_secrets">Открыть текстовые секреты</string>
|
||||
<string name="storage_home_text_secrets_subtitle">Заметки, токены и произвольные пары ключ-значение</string>
|
||||
<string name="storage_home_future_sections">Скоро здесь появятся Files, Media и другие типы данных.</string>
|
||||
<string name="two_fa_add_token">Добавить токен</string>
|
||||
<string name="two_fa_empty_state">Пока нет 2FA токенов</string>
|
||||
<string name="two_fa_create_title">Новый 2FA токен</string>
|
||||
<string name="two_fa_edit_title">Редактирование 2FA токена</string>
|
||||
<string name="two_fa_field_issuer">Сервис</string>
|
||||
<string name="two_fa_field_account">Аккаунт</string>
|
||||
<string name="two_fa_field_secret">Секрет</string>
|
||||
<string name="two_fa_field_notes_optional">Заметка (опционально)</string>
|
||||
<string name="two_fa_field_digits">Количество цифр кода (обычно 6 или 8)</string>
|
||||
<string name="two_fa_field_period_seconds">Период обновления в секундах (обычно 30)</string>
|
||||
<string name="two_fa_field_algorithm">Алгоритм (SHA1, SHA256, SHA512)</string>
|
||||
<string name="two_fa_field_digits_value">Количество цифр: %1$d</string>
|
||||
<string name="two_fa_field_period_seconds_value">Период обновления: %1$d с</string>
|
||||
<string name="two_fa_code_unavailable">------</string>
|
||||
<string name="two_fa_code_refresh_in">Обновление через %1$d с</string>
|
||||
<string name="two_fa_code_refresh_label">Обновление через</string>
|
||||
<string name="two_fa_code_refresh_seconds">%1$d с</string>
|
||||
<string name="two_fa_code_invalid_secret">Неверный секрет или формат</string>
|
||||
<string name="two_fa_copy_code_hint">Нажмите, чтобы скопировать код</string>
|
||||
<string name="two_fa_scan_qr_action">Сканировать QR</string>
|
||||
<string name="two_fa_scan_qr_title">Сканирование QR-кода TOTP</string>
|
||||
<string name="two_fa_scan_qr_invalid">QR-код не содержит валидный otpauth://totp URI</string>
|
||||
<string name="two_fa_camera_permission_required">Нужно разрешение на камеру для сканирования QR</string>
|
||||
<string name="text_secret_create">Создать секрет</string>
|
||||
<string name="text_secret_edit">Редактировать секрет</string>
|
||||
<string name="text_secret_title">Название</string>
|
||||
<string name="text_secret_empty_state">Пока нет текстовых секретов</string>
|
||||
<string name="text_secret_items_count">Элементов: %1$d</string>
|
||||
<string name="text_secret_item_without_label">Без названия</string>
|
||||
<string name="text_secret_more_fields">ещё полей: %1$d</string>
|
||||
<string name="text_secret_item_label_optional">Название (опционально)</string>
|
||||
<string name="text_secret_item_value">Значение</string>
|
||||
<string name="text_secret_add_item">Добавить пару</string>
|
||||
<string name="text_secret_copy_value">Скопировать значение</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="open">Открыть</string>
|
||||
<string name="edit">Редактировать</string>
|
||||
<string name="common_unknown">Неизвестно</string>
|
||||
<string name="settings_language_section">Язык</string>
|
||||
<string name="settings_language_system">Как в системе</string>
|
||||
<string name="settings_language_english">English</string>
|
||||
<string name="settings_language_russian">Русский</string>
|
||||
<string name="task_pipeline_test_elapsed">Прошло: %1$d с / %2$d с</string>
|
||||
<string name="text_secret_clipboard_fallback_label">значение</string>
|
||||
<string name="sync_progress_no_groups">Синхронизация: группы не настроены</string>
|
||||
<string name="sync_progress_preparing">Синхронизация: подготовка %1$d групп</string>
|
||||
<string name="sync_progress_started">Синхронизация: запущена</string>
|
||||
<string name="sync_progress_completed">Синхронизация: завершена</string>
|
||||
<string name="sync_progress_group_preparing">Синхронизация: группа «%1$s» — подготовка</string>
|
||||
<string name="sync_progress_group_not_found">Синхронизация: группа «%1$s» не найдена</string>
|
||||
<string name="sync_progress_group_skipped_few_storages">Синхронизация: группа «%1$s» пропущена (нужно минимум 2 хранилища)</string>
|
||||
<string name="sync_progress_group_skipped_incompatible">Синхронизация: группа «%1$s» пропущена (несовместимое шифрование: %2$d)</string>
|
||||
<string name="sync_progress_group_acquiring_locks">Синхронизация: группа «%1$s» — получение блокировок</string>
|
||||
<string name="sync_progress_group_lock">Синхронизация: группа «%1$s» — блокировка %2$d/%3$d</string>
|
||||
<string name="sync_progress_group_lock_failed">Синхронизация: группа «%1$s» — блокировка не получена, пропуск</string>
|
||||
<string name="sync_progress_group_reading_journals">Синхронизация: группа «%1$s» — чтение журналов</string>
|
||||
<string name="sync_progress_group_cancelled">Синхронизация: группа «%1$s» отменена новым запуском</string>
|
||||
<string name="sync_progress_group_journal">Синхронизация: группа «%1$s» — журнал %2$d/%3$d</string>
|
||||
<string name="sync_progress_group_no_entries">Синхронизация: группа «%1$s» — нет записей в журнале</string>
|
||||
<string name="sync_progress_group_processing">Синхронизация: группа «%1$s» — обработка %2$d записей</string>
|
||||
<string name="sync_progress_group_entry">Синхронизация: группа «%1$s» — запись %2$d/%3$d</string>
|
||||
<string name="sync_progress_group_completed">Синхронизация: группа «%1$s» завершена</string>
|
||||
<string name="sync_progress_group_renewing_locks">Синхронизация: группа «%1$s» — продление блокировок</string>
|
||||
<string name="sync_progress_group_lock_renewal_failed">Синхронизация: группа «%1$s» — не удалось продлить блокировку</string>
|
||||
<string name="task_progress_clear_content">%1$d / %2$d</string>
|
||||
<string name="task_log_sync_started">Синхронизация хранилищ запущена</string>
|
||||
<string name="task_log_sync_finished">Синхронизация хранилищ завершена</string>
|
||||
<string name="task_log_sync_failed">Синхронизация не удалась: %1$s</string>
|
||||
<string name="task_log_enumerating">Перечисление файлов и папок…</string>
|
||||
<string name="task_log_enumerate_done">Готово: %1$d файлов, %2$d папок за %3$d мс (подробности в журнале приложения)</string>
|
||||
<string name="task_log_creating_storage">Создание хранилища…</string>
|
||||
<string name="task_log_storage_created">Хранилище создано</string>
|
||||
<string name="task_log_checking_storage">Проверка хранилища…</string>
|
||||
<string name="task_log_encrypting">Шифрование…</string>
|
||||
<string name="task_log_encryption_enabled">Шифрование включено</string>
|
||||
<string name="task_log_already_encrypted">Хранилище уже зашифровано</string>
|
||||
<string name="task_log_not_empty">Хранилище не пустое</string>
|
||||
<string name="task_log_empty_unknown">Не удалось определить, пусто ли хранилище</string>
|
||||
<string name="task_log_unsupported_type">Неподдерживаемый тип хранилища</string>
|
||||
<string name="task_log_enable_encryption_failed">Не удалось включить шифрование</string>
|
||||
<string name="task_log_opening_storage">Открытие зашифрованного хранилища…</string>
|
||||
<string name="task_log_storage_opened">Хранилище открыто</string>
|
||||
<string name="task_log_open_storage_failed">Не удалось открыть хранилище</string>
|
||||
<string name="task_log_closing_storage">Закрытие хранилища…</string>
|
||||
<string name="task_log_storage_closed">Хранилище закрыто</string>
|
||||
<string name="task_log_close_storage_failed">Не удалось закрыть хранилище</string>
|
||||
<string name="task_log_disabling_encryption">Отключение шифрования…</string>
|
||||
<string name="task_log_encryption_disabled">Шифрование отключено</string>
|
||||
<string name="task_log_disable_encryption_failed">Не удалось отключить шифрование</string>
|
||||
<string name="task_log_renaming">Переименование…</string>
|
||||
<string name="task_log_renamed">Переименовано</string>
|
||||
<string name="task_log_rename_failed">Не удалось переименовать</string>
|
||||
<string name="task_log_removing_storage">Удаление хранилища…</string>
|
||||
<string name="task_log_removed">Удалено</string>
|
||||
<string name="task_log_remove_failed">Не удалось удалить</string>
|
||||
<string name="task_log_invalid_storage">Некорректное хранилище</string>
|
||||
<string name="task_log_clearing_sync_lock">Снятие блокировки синхронизации…</string>
|
||||
<string name="task_log_sync_lock_cleared">Блокировка синхронизации снята</string>
|
||||
<string name="task_log_clear_sync_lock_failed">Не удалось снять блокировку</string>
|
||||
<string name="task_log_adding_vault">Добавление хранилища…</string>
|
||||
<string name="task_log_vault_added">Хранилище добавлено</string>
|
||||
<string name="task_log_add_vault_failed">Не удалось добавить хранилище</string>
|
||||
<string name="task_log_removing_remote_vault">Удаление удалённого хранилища…</string>
|
||||
<string name="task_log_remote_vault_removed">Удалённое хранилище удалено</string>
|
||||
<string name="task_log_remove_vault_failed">Не удалось удалить хранилище</string>
|
||||
<string name="task_log_retrying_vault">Повторное подключение…</string>
|
||||
<string name="task_log_retry_requested">Повтор запрошен</string>
|
||||
<string name="task_log_retry_vault_failed">Не удалось повторить подключение</string>
|
||||
<string name="task_log_test_started">Тестовая задача запущена на %1$d с</string>
|
||||
<string name="task_log_test_finished">Тестовая задача завершена</string>
|
||||
</resources>
|
||||
@@ -1,290 +1,337 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="nav_label_local_vault">Локальное хранилище</string>
|
||||
<string name="nav_cd_local_vault">Локальное хранилище</string>
|
||||
<string name="nav_label_remote_vaults">Удалённые хранилища</string>
|
||||
<string name="nav_cd_remote_vaults">Удалённые хранилища</string>
|
||||
<string name="nav_label_main">Главная</string>
|
||||
<string name="nav_label_sync">Синхронизация</string>
|
||||
<string name="nav_label_settings">Настройки</string>
|
||||
|
||||
<string name="main_work_status_label">Статус:</string>
|
||||
<string name="main_status_multiple_tasks">Выполняется задач: %1$d</string>
|
||||
<string name="main_status_vault_scanning_storages">Сканирование vault: загрузка списка хранилищ…</string>
|
||||
|
||||
<string name="settings_title">Настройки</string>
|
||||
<string name="sync_groups_title">Группы синхронизации</string>
|
||||
<string name="sync_progress_section_title">Синхронизация хранилищ</string>
|
||||
<string name="sync_groups_busy_section_title">Сохранение групп синхронизации</string>
|
||||
<string name="sync_run_now">Запустить синхронизацию</string>
|
||||
<string name="sync_cd_run_now">Запустить синхронизацию сейчас</string>
|
||||
<string name="sync_refresh">Обновить</string>
|
||||
<string name="sync_add_storage">Добавить хранилище в группу</string>
|
||||
<string name="sync_remove_group">Удалить группу</string>
|
||||
<string name="sync_group_empty">В группе нет хранилищ</string>
|
||||
<string name="sync_remove_storage">Убрать хранилище из группы</string>
|
||||
<string name="sync_picker_back">Назад</string>
|
||||
<string name="sync_cd_picker_back">Закрыть выбор хранилища</string>
|
||||
<string name="sync_picker_title">Выбор хранилища для %1$s</string>
|
||||
<string name="sync_picker_add">Добавить</string>
|
||||
<string name="sync_picker_added">Добавлено</string>
|
||||
<string name="sync_picker_cd_add">Добавить хранилище в группу</string>
|
||||
<string name="sync_picker_no_storages">В этом хранилище нет доступных каталогов</string>
|
||||
<string name="sync_picker_expand">Развернуть</string>
|
||||
<string name="sync_picker_collapse">Свернуть</string>
|
||||
<string name="sync_fab_create_group_cd">Создать группу синхронизации</string>
|
||||
<string name="sync_group_mixed_encryption_warning">В группе разное шифрование: задайте единый режим</string>
|
||||
<string name="sync_group_incompatible_warning">Несовместимые хранилища в группе: %1$d</string>
|
||||
<string name="sync_group_policy_line">Политика шифрования группы: %1$s</string>
|
||||
<string name="sync_group_policy_unset">Не определена (группа пуста)</string>
|
||||
<string name="sync_group_policy_plain">Только незашифрованные</string>
|
||||
<string name="sync_group_policy_password">Только зашифрованные с паролем группы</string>
|
||||
<string name="sync_remove_group_confirm_title">Удалить группу?</string>
|
||||
<string name="sync_remove_group_confirm_message">Удалить группу синхронизации «%1$s»?</string>
|
||||
<string name="sync_remove_storage_confirm_title">Убрать хранилище?</string>
|
||||
<string name="sync_remove_storage_confirm_message">Убрать хранилище «%1$s» из группы?</string>
|
||||
<string name="sync_confirm_delete">Удалить</string>
|
||||
<string name="sync_cancel">Отмена</string>
|
||||
<string name="sync_msg_group_created">Создана группа %1$s</string>
|
||||
<string name="sync_msg_group_removed">Группа удалена</string>
|
||||
<string name="sync_msg_storage_added">Хранилище добавлено в %1$s</string>
|
||||
<string name="sync_msg_storage_removed">Хранилище убрано из %1$s</string>
|
||||
<string name="sync_msg_storage_already_added">Хранилище уже добавлено в группу</string>
|
||||
<string name="sync_msg_only_plain_storage_allowed">В группы синхронизации можно добавлять только незашифрованные хранилища</string>
|
||||
<string name="sync_msg_storage_encryption_key_required">Для зашифрованного хранилища нужно знать пароль (откройте его перед добавлением)</string>
|
||||
<string name="sync_msg_storage_incompatible_encryption">Хранилище не совместимо с политикой шифрования группы</string>
|
||||
<string name="sync_msg_virtual_storage_not_supported">Нельзя добавлять открытое виртуальное хранилище: синхронизация работает с исходными raw storage</string>
|
||||
<string name="sync_msg_task_enqueued">Задача синхронизации поставлена в очередь</string>
|
||||
<string name="sync_msg_sync_already_running">Синхронизация уже выполняется</string>
|
||||
<string name="sync_msg_blocked_during_sync">Дождитесь окончания синхронизации</string>
|
||||
<string name="sync_encryption_unknown">Неизвестно</string>
|
||||
<string name="sync_storage_encryption_line">Шифрование: %1$s</string>
|
||||
<string name="sync_storage_missing_title">Не найдено в текущих vault</string>
|
||||
<string name="sync_storage_pending_vault_scan">Ожидание: список хранилищ в vault ещё загружается</string>
|
||||
<string name="sync_storage_not_in_vaults">Нет в дереве хранилищ (удалено, другой аккаунт или не прошёл init)</string>
|
||||
<string name="sync_storage_unreachable">Хранилище недоступно (vault или сеть)</string>
|
||||
|
||||
<string name="no_name"><без имени></string>
|
||||
<string name="show_storage_item_menu">Меню хранилища</string>
|
||||
<string name="storage_row_task_running_cd">Выполняется операция с этим хранилищем</string>
|
||||
<string name="storage_menu_busy">%1$s (задача выполняется)</string>
|
||||
<string name="rename">Переименовать</string>
|
||||
<string name="remove">Удалить</string>
|
||||
<string name="encrypt">Шифрование</string>
|
||||
<string name="new_name_title">Новое имя</string>
|
||||
<string name="remove_confirmation_dialog">Удалить хранилище «%1$s»?</string>
|
||||
<string name="storage_lock_actions">Действия с шифрованием</string>
|
||||
<string name="storage_sync_lock_checking">Проверка блокировки…</string>
|
||||
<string name="storage_sync_unlock_action">Снять блокировку синхронизации</string>
|
||||
<string name="storage_sync_not_locked">Синхронизация не заблокирована</string>
|
||||
|
||||
<string name="storage_field_available">Доступно: %1$s</string>
|
||||
<string name="storage_value_yes">да</string>
|
||||
<string name="storage_value_no">нет</string>
|
||||
<string name="storage_field_files">Файлов: %1$s</string>
|
||||
<string name="storage_field_size">Размер: %1$s</string>
|
||||
<string name="storage_field_virtual">Виртуальное: %1$s</string>
|
||||
<string name="storage_unavailable_hint">Хранилище недоступно</string>
|
||||
<string name="storage_menu_unavailable">Недоступно: %1$s</string>
|
||||
|
||||
<string name="storage_status_not_encrypted">Не зашифровано</string>
|
||||
<string name="storage_status_encrypted_open">Зашифровано (открыто)</string>
|
||||
<string name="storage_status_encrypted_closed">Зашифровано (закрыто)</string>
|
||||
|
||||
<string name="vault_fab_add_storage_cd">Создать хранилище</string>
|
||||
<string name="vault_fab_add_storage_disabled_cd">Создание недоступно: хранилище недоступно</string>
|
||||
<string name="vault_fab_add_storage_busy_cd">Создание хранилища уже выполняется</string>
|
||||
<string name="vault_msg_storage_pipeline_busy">С этим хранилищем уже выполняется операция</string>
|
||||
<string name="vault_msg_vault_list_mutation_busy">Список хранилищ сейчас меняется — подождите</string>
|
||||
<string name="vault_unavailable_banner">Хранилище недоступно. Проверьте сеть, путь или разблокировку.</string>
|
||||
<string name="vault_loading_storages">Загрузка списка хранилищ…</string>
|
||||
<string name="vault_empty_list_hint">В этом хранилище пока нет каталогов. Создайте хранилище кнопкой «+», когда оно доступно.</string>
|
||||
|
||||
<string name="task_pipeline_title">Очередь задач</string>
|
||||
<string name="task_pipeline_jobs">Задачи</string>
|
||||
<string name="task_pipeline_log">Журнал</string>
|
||||
<string name="task_pipeline_cancel_all">Отменить все</string>
|
||||
<string name="task_pipeline_open">Открыть очередь задач</string>
|
||||
<string name="task_pipeline_run_test">Тестовая задача</string>
|
||||
<string name="task_pipeline_test_dialog_title">Параметры тестовой задачи</string>
|
||||
<string name="task_pipeline_test_dialog_duration">Длительность: %1$d с</string>
|
||||
<string name="task_pipeline_test_dialog_start">Запустить</string>
|
||||
<string name="task_pipeline_test_dialog_cancel">Отмена</string>
|
||||
<string name="task_pipeline_test_dialog_infinity">Бесконечно (неопределённый прогресс)</string>
|
||||
<string name="task_pipeline_test_running">Тестовая задача (%1$d с)</string>
|
||||
<string name="task_pipeline_test_running_infinity">Тестовая задача (%1$d с, ∞)</string>
|
||||
<string name="task_state_queued">В очереди</string>
|
||||
<string name="task_state_running">Выполняется</string>
|
||||
<string name="task_state_completed">Завершено</string>
|
||||
<string name="task_state_cancelled">Отменено</string>
|
||||
<string name="task_state_failed">Ошибка: %1$s</string>
|
||||
|
||||
<string name="task_title_dump_storage_log">Выгрузка дерева в журнал</string>
|
||||
<string name="task_title_create_storage">Создание хранилища</string>
|
||||
<string name="task_title_enable_encryption">Включение шифрования</string>
|
||||
<string name="task_title_open_encrypted_storage">Расшифровка и открытие хранилища</string>
|
||||
<string name="task_progress_decrypt_running">Расшифровка…</string>
|
||||
<string name="task_progress_dump_storage_log">Сканирование дерева…</string>
|
||||
<string name="task_progress_create_storage">Создание хранилища…</string>
|
||||
<string name="task_progress_enable_encryption">Шифрование…</string>
|
||||
<string name="task_progress_close_storage">Закрытие хранилища…</string>
|
||||
<string name="task_progress_disable_encryption">Очистка содержимого…</string>
|
||||
<string name="task_progress_rename_storage">Переименование…</string>
|
||||
<string name="task_progress_remove_storage">Удаление…</string>
|
||||
<string name="task_progress_clear_sync_lock">Снятие блокировки…</string>
|
||||
<string name="task_progress_add_remote_vault">Добавление…</string>
|
||||
<string name="task_progress_remove_remote_vault">Удаление…</string>
|
||||
<string name="task_progress_retry_remote_vault">Подключение…</string>
|
||||
<string name="task_progress_save_2fa_token">Сохранение…</string>
|
||||
<string name="task_progress_delete_2fa_token">Удаление…</string>
|
||||
<string name="task_progress_save_text_secret">Сохранение…</string>
|
||||
<string name="task_progress_delete_text_secret">Удаление…</string>
|
||||
<string name="task_title_close_encrypted_storage">Закрытие зашифрованного хранилища</string>
|
||||
<string name="task_title_disable_encryption">Отключение шифрования</string>
|
||||
<string name="task_title_rename_storage">Переименование хранилища</string>
|
||||
<string name="task_title_remove_storage">Удаление хранилища</string>
|
||||
<string name="task_title_clear_sync_lock">Снятие блокировки синхронизации</string>
|
||||
<string name="task_title_add_remote_vault">Добавление удалённого хранилища</string>
|
||||
<string name="task_title_remove_remote_vault">Удаление удалённого хранилища</string>
|
||||
<string name="task_title_retry_remote_vault">Повторное подключение удалённого хранилища</string>
|
||||
<string name="task_title_storage_sync">Синхронизация хранилищ</string>
|
||||
<string name="task_title_storage_sync_background">Фоновая синхронизация хранилищ</string>
|
||||
<string name="task_title_save_2fa_token">Сохранение 2FA токена</string>
|
||||
<string name="task_title_delete_2fa_token">Удаление 2FA токена</string>
|
||||
<string name="task_title_save_text_secret">Сохранение текстового секрета</string>
|
||||
<string name="task_title_delete_text_secret">Удаление текстового секрета</string>
|
||||
|
||||
<string name="error_storage_not_found">Хранилище не найдено</string>
|
||||
<string name="error_storage_locked_view">Откройте расшифрованное отображение storage для работы с этим разделом</string>
|
||||
<string name="error_secret_not_found">Секрет не найден</string>
|
||||
<string name="error_storage_not_writable">Хранилище недоступно для записи</string>
|
||||
<string name="error_file_not_found">Файл не найден</string>
|
||||
<string name="error_incorrect_password">Неверный пароль</string>
|
||||
<string name="error_storage_not_encrypted">Хранилище не зашифровано</string>
|
||||
<string name="error_enc_info_missing">Отсутствуют метаданные шифрования</string>
|
||||
<string name="error_delete_root_forbidden">Нельзя удалить корень хранилища</string>
|
||||
<string name="error_not_a_file">Ожидался файл</string>
|
||||
<string name="error_not_a_directory">Ожидалась папка</string>
|
||||
<string name="error_path_is_file">Путь указывает на файл, а не на папку</string>
|
||||
<string name="error_cannot_write_over_directory">Нельзя записать поверх папки</string>
|
||||
<string name="error_unexpected_state">Хранилище в неожиданном состоянии</string>
|
||||
<string name="error_network">Ошибка сети или сервера</string>
|
||||
<string name="error_disk_resource_locked">Ресурс временно заблокирован. Повторите позже.</string>
|
||||
<string name="error_unknown">Что-то пошло не так</string>
|
||||
<string name="sync_error_group_not_found">Группа синхронизации не найдена</string>
|
||||
<string name="vault_link_error_auth">Не удалось войти</string>
|
||||
<string name="vault_link_error_not_registered">Вход не готов. Перезапустите приложение.</string>
|
||||
<string name="vault_link_error_unknown">Не удалось войти</string>
|
||||
<string name="vault_link_error_unsupported_brand">Этот провайдер не поддерживается</string>
|
||||
|
||||
<string name="msg_encryption_enabled">Шифрование включено</string>
|
||||
<string name="msg_storage_already_encrypted">Хранилище уже зашифровано</string>
|
||||
<string name="msg_storage_not_empty">Хранилище не пустое</string>
|
||||
<string name="msg_storage_empty_state_unknown">Не удалось определить, пусто ли хранилище</string>
|
||||
<string name="msg_unsupported_storage_type">Неподдерживаемый тип хранилища</string>
|
||||
<string name="msg_failed_enable_encryption">Не удалось включить шифрование: %1$s</string>
|
||||
<string name="msg_failed_open_storage">Не удалось открыть хранилище: %1$s</string>
|
||||
<string name="msg_failed_close_storage">Не удалось закрыть хранилище: %1$s</string>
|
||||
<string name="msg_encryption_disabled">Шифрование отключено</string>
|
||||
<string name="msg_failed_disable_encryption">Не удалось отключить шифрование: %1$s</string>
|
||||
<string name="msg_invalid_storage_for_sync_lock">Некорректное хранилище</string>
|
||||
<string name="msg_sync_lock_cleared">Блокировка синхронизации снята</string>
|
||||
<string name="msg_sync_lock_clear_failed">Не удалось снять блокировку: %1$s</string>
|
||||
|
||||
<string name="remote_vaults_add_cd">Добавить удалённое хранилище</string>
|
||||
<string name="remote_vaults_empty_hint">Пока нет удалённых хранилищ. Нажмите «+», чтобы добавить Yandex.</string>
|
||||
<string name="remote_vaults_add_title">Добавить хранилище</string>
|
||||
<string name="remote_vaults_add_pick_provider">Выберите провайдера:</string>
|
||||
<string name="remote_vaults_provider_yandex">Яндекс</string>
|
||||
<string name="remote_vaults_add_cancel">Отмена</string>
|
||||
<string name="remote_vault_type_yandex">Яндекс</string>
|
||||
<string name="remote_vault_unavailable">Хранилище временно недоступно</string>
|
||||
<string name="remote_vault_retry_action">Повторить подключение</string>
|
||||
<string name="remote_vault_retrying">Пробую подключиться…</string>
|
||||
<string name="remote_vault_delete_cd">Удалить удалённое хранилище</string>
|
||||
<string name="remote_vault_remove_title">Удалить удалённое хранилище?</string>
|
||||
<string name="remote_vault_remove_message">Удалить «%1$s» с этого устройства? Данные на сервере не удаляются.</string>
|
||||
|
||||
<string name="dialog_cancel">Отмена</string>
|
||||
<string name="dialog_ok">ОК</string>
|
||||
<string name="dialog_encryption_enable_title">Включить шифрование</string>
|
||||
<string name="dialog_password_label">Пароль</string>
|
||||
<string name="dialog_encrypt_paths">Шифровать пути</string>
|
||||
<string name="dialog_apply">Применить</string>
|
||||
<string name="dialog_open_encrypted_title">Открыть зашифрованное хранилище</string>
|
||||
<string name="dialog_remember_password">Запомнить пароль</string>
|
||||
<string name="dialog_open">Открыть</string>
|
||||
<string name="dialog_close">Закрыть</string>
|
||||
<string name="dialog_disable_encryption">Отключить шифрование</string>
|
||||
<string name="dialog_done">Готово</string>
|
||||
|
||||
<string name="vault_type_local_device">Локальное устройство</string>
|
||||
<string name="vault_type_remote">Удалённое: %1$s</string>
|
||||
<string name="vault_type_unknown">Неизвестный тип</string>
|
||||
<string name="vault_title_local">Локальное хранилище</string>
|
||||
<string name="vault_title_unknown">Неизвестное хранилище</string>
|
||||
|
||||
<string name="enc_status_not_encrypted">Не зашифровано</string>
|
||||
<string name="enc_status_encrypted_open">Зашифровано (открыто)</string>
|
||||
<string name="enc_status_encrypted">Зашифровано</string>
|
||||
|
||||
<string name="text_edit_screen_title">Текст</string>
|
||||
<string name="text_edit_screen_placeholder">Содержимое: %1$s</string>
|
||||
|
||||
<string name="nav_label_local_vault">Local vault</string>
|
||||
<string name="nav_cd_local_vault">Local vault</string>
|
||||
<string name="nav_label_remote_vaults">Remote vaults</string>
|
||||
<string name="nav_cd_remote_vaults">Remote vaults</string>
|
||||
<string name="nav_label_main">Home</string>
|
||||
<string name="nav_label_sync">Sync</string>
|
||||
<string name="nav_label_settings">Settings</string>
|
||||
<string name="main_work_status_label">Status:</string>
|
||||
<string name="main_status_multiple_tasks">Running tasks: %1$d</string>
|
||||
<string name="main_status_vault_scanning_storages">Scanning vault: loading storage list…</string>
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="sync_groups_title">Sync groups</string>
|
||||
<string name="sync_progress_section_title">Storage sync</string>
|
||||
<string name="sync_groups_busy_section_title">Saving sync groups</string>
|
||||
<string name="sync_run_now">Run sync now</string>
|
||||
<string name="sync_cd_run_now">Run sync now</string>
|
||||
<string name="sync_refresh">Refresh</string>
|
||||
<string name="sync_add_storage">Add storage to group</string>
|
||||
<string name="sync_remove_group">Remove group</string>
|
||||
<string name="sync_group_empty">No storages in this group</string>
|
||||
<string name="sync_remove_storage">Remove storage from group</string>
|
||||
<string name="sync_picker_back">Back</string>
|
||||
<string name="sync_cd_picker_back">Close storage picker</string>
|
||||
<string name="sync_picker_title">Pick storage for %1$s</string>
|
||||
<string name="sync_picker_add">Add</string>
|
||||
<string name="sync_picker_added">Added</string>
|
||||
<string name="sync_picker_cd_add">Add storage to group</string>
|
||||
<string name="sync_picker_no_storages">No available folders in this vault</string>
|
||||
<string name="sync_picker_expand">Expand</string>
|
||||
<string name="sync_picker_collapse">Collapse</string>
|
||||
<string name="sync_fab_create_group_cd">Create sync group</string>
|
||||
<string name="sync_group_mixed_encryption_warning">Mixed encryption in group: set a single mode</string>
|
||||
<string name="sync_group_incompatible_warning">Incompatible storages in group: %1$d</string>
|
||||
<string name="sync_group_policy_line">Group encryption policy: %1$s</string>
|
||||
<string name="sync_group_policy_unset">Not set (group is empty)</string>
|
||||
<string name="sync_group_policy_plain">Unencrypted only</string>
|
||||
<string name="sync_group_policy_password">Password-encrypted with group password</string>
|
||||
<string name="sync_remove_group_confirm_title">Remove group?</string>
|
||||
<string name="sync_remove_group_confirm_message">Remove sync group "%1$s"?</string>
|
||||
<string name="sync_remove_storage_confirm_title">Remove storage?</string>
|
||||
<string name="sync_remove_storage_confirm_message">Remove storage "%1$s" from the group?</string>
|
||||
<string name="sync_confirm_delete">Delete</string>
|
||||
<string name="sync_cancel">Cancel</string>
|
||||
<string name="sync_msg_group_created">Created group %1$s</string>
|
||||
<string name="sync_msg_group_removed">Group removed</string>
|
||||
<string name="sync_msg_storage_added">Storage added to %1$s</string>
|
||||
<string name="sync_msg_storage_removed">Storage removed from %1$s</string>
|
||||
<string name="sync_msg_storage_already_added">Storage is already in the group</string>
|
||||
<string name="sync_msg_only_plain_storage_allowed">Only unencrypted storages can be added to sync groups</string>
|
||||
<string name="sync_msg_storage_encryption_key_required">Encrypted storage requires the password (open it before adding)</string>
|
||||
<string name="sync_msg_storage_incompatible_encryption">Storage is not compatible with the group encryption policy</string>
|
||||
<string name="sync_msg_virtual_storage_not_supported">Cannot add an open virtual storage: sync works with raw storages</string>
|
||||
<string name="sync_msg_task_enqueued">Sync task queued</string>
|
||||
<string name="sync_msg_sync_already_running">Sync is already running</string>
|
||||
<string name="sync_msg_blocked_during_sync">Wait for sync to finish</string>
|
||||
<string name="sync_encryption_unknown">Unknown</string>
|
||||
<string name="sync_storage_encryption_line">Encryption: %1$s</string>
|
||||
<string name="sync_storage_missing_title">Not found in current vaults</string>
|
||||
<string name="sync_storage_pending_vault_scan">Waiting: vault storage list is still loading</string>
|
||||
<string name="sync_storage_not_in_vaults">Not in storage tree (removed, different account, or init failed)</string>
|
||||
<string name="sync_storage_unreachable">Storage unavailable (vault or network)</string>
|
||||
<string name="no_name"><no name></string>
|
||||
<string name="show_storage_item_menu">Storage menu</string>
|
||||
<string name="storage_row_task_running_cd">Operation in progress for this storage</string>
|
||||
<string name="storage_menu_busy">%1$s (task running)</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="encrypt">Encryption</string>
|
||||
<string name="new_name_title">New name</string>
|
||||
<string name="remove_confirmation_dialog">Remove storage "%1$s"?</string>
|
||||
<string name="storage_lock_actions">Encryption actions</string>
|
||||
<string name="storage_sync_lock_checking">Checking lock…</string>
|
||||
<string name="storage_sync_unlock_action">Clear sync lock</string>
|
||||
<string name="storage_sync_not_locked">Sync is not locked</string>
|
||||
<string name="storage_field_available">Available: %1$s</string>
|
||||
<string name="storage_value_yes">yes</string>
|
||||
<string name="storage_value_no">no</string>
|
||||
<string name="storage_field_files">Files: %1$s</string>
|
||||
<string name="storage_field_size">Size: %1$s</string>
|
||||
<string name="storage_field_virtual">Virtual: %1$s</string>
|
||||
<string name="storage_unavailable_hint">Storage unavailable</string>
|
||||
<string name="storage_menu_unavailable">Unavailable: %1$s</string>
|
||||
<string name="storage_status_not_encrypted">Not encrypted</string>
|
||||
<string name="storage_status_encrypted_open">Encrypted (open)</string>
|
||||
<string name="storage_status_encrypted_closed">Encrypted (locked)</string>
|
||||
<string name="vault_fab_add_storage_cd">Create storage</string>
|
||||
<string name="vault_fab_add_storage_disabled_cd">Create unavailable: vault offline</string>
|
||||
<string name="vault_fab_add_storage_busy_cd">Storage creation already running</string>
|
||||
<string name="vault_msg_storage_pipeline_busy">An operation is already running for this storage</string>
|
||||
<string name="vault_msg_vault_list_mutation_busy">Storage list is changing — please wait</string>
|
||||
<string name="vault_unavailable_banner">Vault unavailable. Check network, path, or unlock.</string>
|
||||
<string name="vault_loading_storages">Loading storage list…</string>
|
||||
<string name="vault_empty_list_hint">No folders yet. Create storage with "+" when available.</string>
|
||||
<string name="task_pipeline_title">Task queue</string>
|
||||
<string name="task_pipeline_jobs">Tasks</string>
|
||||
<string name="task_pipeline_log">Log</string>
|
||||
<string name="task_pipeline_cancel_all">Cancel all</string>
|
||||
<string name="task_pipeline_open">Open task queue</string>
|
||||
<string name="task_pipeline_run_test">Test task</string>
|
||||
<string name="task_pipeline_test_dialog_title">Test task parameters</string>
|
||||
<string name="task_pipeline_test_dialog_duration">Duration: %1$d s</string>
|
||||
<string name="task_pipeline_test_dialog_start">Start</string>
|
||||
<string name="task_pipeline_test_dialog_cancel">Cancel</string>
|
||||
<string name="task_pipeline_test_dialog_infinity">Infinite (indeterminate progress)</string>
|
||||
<string name="task_pipeline_test_running">Test task (%1$d s)</string>
|
||||
<string name="task_pipeline_test_running_infinity">Test task (%1$d s, ∞)</string>
|
||||
<string name="task_state_queued">Queued</string>
|
||||
<string name="task_state_running">Running</string>
|
||||
<string name="task_state_completed">Completed</string>
|
||||
<string name="task_state_cancelled">Cancelled</string>
|
||||
<string name="task_state_failed">Error: %1$s</string>
|
||||
<string name="task_title_dump_storage_log">Export tree to log</string>
|
||||
<string name="task_title_create_storage">Create storage</string>
|
||||
<string name="task_title_enable_encryption">Enable encryption</string>
|
||||
<string name="task_title_open_encrypted_storage">Decrypt and open storage</string>
|
||||
<string name="task_progress_decrypt_running">Decrypting…</string>
|
||||
<string name="task_progress_dump_storage_log">Scanning tree…</string>
|
||||
<string name="task_progress_create_storage">Creating storage…</string>
|
||||
<string name="task_progress_enable_encryption">Encrypting…</string>
|
||||
<string name="task_progress_close_storage">Closing storage…</string>
|
||||
<string name="task_progress_disable_encryption">Clearing content…</string>
|
||||
<string name="task_progress_rename_storage">Renaming…</string>
|
||||
<string name="task_progress_remove_storage">Removing…</string>
|
||||
<string name="task_progress_clear_sync_lock">Clearing sync lock…</string>
|
||||
<string name="task_progress_add_remote_vault">Adding…</string>
|
||||
<string name="task_progress_remove_remote_vault">Removing…</string>
|
||||
<string name="task_progress_retry_remote_vault">Connecting…</string>
|
||||
<string name="task_progress_save_2fa_token">Saving…</string>
|
||||
<string name="task_progress_delete_2fa_token">Removing…</string>
|
||||
<string name="task_progress_save_text_secret">Saving…</string>
|
||||
<string name="task_progress_delete_text_secret">Removing…</string>
|
||||
<string name="task_title_close_encrypted_storage">Close encrypted storage</string>
|
||||
<string name="task_title_disable_encryption">Disable encryption</string>
|
||||
<string name="task_title_rename_storage">Rename storage</string>
|
||||
<string name="task_title_remove_storage">Remove storage</string>
|
||||
<string name="task_title_clear_sync_lock">Clear sync lock</string>
|
||||
<string name="task_title_add_remote_vault">Add remote vault</string>
|
||||
<string name="task_title_remove_remote_vault">Remove remote vault</string>
|
||||
<string name="task_title_retry_remote_vault">Retry remote vault connection</string>
|
||||
<string name="task_title_storage_sync">Storage sync</string>
|
||||
<string name="task_title_storage_sync_background">Background storage sync</string>
|
||||
<string name="task_title_save_2fa_token">Save 2FA token</string>
|
||||
<string name="task_title_delete_2fa_token">Delete 2FA token</string>
|
||||
<string name="task_title_save_text_secret">Save text secret</string>
|
||||
<string name="task_title_delete_text_secret">Delete text secret</string>
|
||||
<string name="error_storage_not_found">Storage not found</string>
|
||||
<string name="error_storage_locked_view">Open decrypted storage view to use this section</string>
|
||||
<string name="error_secret_not_found">Secret not found</string>
|
||||
<string name="error_storage_not_writable">Storage is not writable</string>
|
||||
<string name="error_file_not_found">File not found</string>
|
||||
<string name="error_incorrect_password">Incorrect password</string>
|
||||
<string name="error_storage_not_encrypted">Storage is not encrypted</string>
|
||||
<string name="error_enc_info_missing">Encryption metadata missing</string>
|
||||
<string name="error_delete_root_forbidden">Cannot delete storage root</string>
|
||||
<string name="error_not_a_file">Expected a file</string>
|
||||
<string name="error_not_a_directory">Expected a folder</string>
|
||||
<string name="error_path_is_file">Path points to a file, not a folder</string>
|
||||
<string name="error_cannot_write_over_directory">Cannot write over a folder</string>
|
||||
<string name="error_unexpected_state">Storage in unexpected state</string>
|
||||
<string name="error_network">Network or server error</string>
|
||||
<string name="error_disk_resource_locked">Resource is temporarily locked. Try again later.</string>
|
||||
<string name="error_unknown">Something went wrong</string>
|
||||
<string name="sync_error_group_not_found">Sync group not found</string>
|
||||
<string name="vault_link_error_auth">Sign-in failed</string>
|
||||
<string name="vault_link_error_not_registered">Sign-in is not ready. Restart the app.</string>
|
||||
<string name="vault_link_error_unknown">Sign-in failed</string>
|
||||
<string name="vault_link_error_unsupported_brand">This provider is not supported</string>
|
||||
<string name="msg_encryption_enabled">Encryption enabled</string>
|
||||
<string name="msg_storage_already_encrypted">Storage is already encrypted</string>
|
||||
<string name="msg_storage_not_empty">Storage is not empty</string>
|
||||
<string name="msg_storage_empty_state_unknown">Could not determine if storage is empty</string>
|
||||
<string name="msg_unsupported_storage_type">Unsupported storage type</string>
|
||||
<string name="msg_encryption_disabled">Encryption disabled</string>
|
||||
<string name="msg_invalid_storage_for_sync_lock">Invalid storage</string>
|
||||
<string name="msg_sync_lock_cleared">Sync lock cleared</string>
|
||||
<string name="remote_vaults_add_cd">Add remote vault</string>
|
||||
<string name="remote_vaults_empty_hint">No remote vaults yet. Tap "+" to add Yandex.</string>
|
||||
<string name="remote_vaults_add_title">Add vault</string>
|
||||
<string name="remote_vaults_add_pick_provider">Choose a provider:</string>
|
||||
<string name="remote_vaults_provider_yandex">Yandex</string>
|
||||
<string name="remote_vaults_add_cancel">Cancel</string>
|
||||
<string name="remote_vault_type_yandex">Yandex</string>
|
||||
<string name="remote_vault_unavailable">Vault temporarily unavailable</string>
|
||||
<string name="remote_vault_retry_action">Retry connection</string>
|
||||
<string name="remote_vault_retrying">Connecting…</string>
|
||||
<string name="remote_vault_delete_cd">Remove remote vault</string>
|
||||
<string name="remote_vault_remove_title">Remove remote vault?</string>
|
||||
<string name="remote_vault_remove_message">Remove "%1$s" from this device? Server data is not deleted.</string>
|
||||
<string name="dialog_cancel">Cancel</string>
|
||||
<string name="dialog_ok">OK</string>
|
||||
<string name="dialog_encryption_enable_title">Enable encryption</string>
|
||||
<string name="dialog_password_label">Password</string>
|
||||
<string name="dialog_encrypt_paths">Encrypt paths</string>
|
||||
<string name="dialog_apply">Apply</string>
|
||||
<string name="dialog_open_encrypted_title">Open encrypted storage</string>
|
||||
<string name="dialog_remember_password">Remember password</string>
|
||||
<string name="dialog_open">Open</string>
|
||||
<string name="dialog_close">Close</string>
|
||||
<string name="dialog_disable_encryption">Disable encryption</string>
|
||||
<string name="dialog_done">Done</string>
|
||||
<string name="vault_type_local_device">Local device</string>
|
||||
<string name="vault_type_remote">Remote: %1$s</string>
|
||||
<string name="vault_type_unknown">Unknown type</string>
|
||||
<string name="vault_title_local">Local vault</string>
|
||||
<string name="vault_title_unknown">Unknown vault</string>
|
||||
<string name="enc_status_not_encrypted">Not encrypted</string>
|
||||
<string name="enc_status_encrypted_open">Encrypted (open)</string>
|
||||
<string name="enc_status_encrypted">Encrypted</string>
|
||||
<string name="text_edit_screen_title">Text</string>
|
||||
<string name="text_edit_screen_placeholder">Content: %1$s</string>
|
||||
<string name="storage_home_unnamed_storage">Storage</string>
|
||||
<string name="storage_home_status_line">Статус: %1$s, %2$s</string>
|
||||
<string name="storage_home_status_available">доступно</string>
|
||||
<string name="storage_home_status_unavailable">недоступно</string>
|
||||
<string name="storage_home_status_encrypted">зашифровано</string>
|
||||
<string name="storage_home_status_not_encrypted">не зашифровано</string>
|
||||
<string name="storage_home_two_fa_title">2FA токены (%1$d)</string>
|
||||
<string name="storage_home_open_two_fa">Открыть 2FA</string>
|
||||
<string name="storage_home_two_fa_subtitle">Коды и секреты двухфакторной аутентификации</string>
|
||||
<string name="storage_home_text_secrets_title">Текстовые секреты (%1$d)</string>
|
||||
<string name="storage_home_open_text_secrets">Открыть текстовые секреты</string>
|
||||
<string name="storage_home_text_secrets_subtitle">Заметки, токены и произвольные пары ключ-значение</string>
|
||||
<string name="storage_home_future_sections">Скоро здесь появятся Files, Media и другие типы данных.</string>
|
||||
|
||||
<string name="two_fa_add_token">Добавить токен</string>
|
||||
<string name="two_fa_empty_state">Пока нет 2FA токенов</string>
|
||||
<string name="two_fa_create_title">Новый 2FA токен</string>
|
||||
<string name="two_fa_edit_title">Редактирование 2FA токена</string>
|
||||
<string name="two_fa_field_issuer">Сервис</string>
|
||||
<string name="two_fa_field_account">Аккаунт</string>
|
||||
<string name="two_fa_field_secret">Секрет</string>
|
||||
<string name="two_fa_field_notes_optional">Заметка (опционально)</string>
|
||||
<string name="two_fa_field_digits">Количество цифр кода (обычно 6 или 8)</string>
|
||||
<string name="two_fa_field_period_seconds">Период обновления в секундах (обычно 30)</string>
|
||||
<string name="two_fa_field_algorithm">Алгоритм (SHA1, SHA256, SHA512)</string>
|
||||
<string name="two_fa_field_digits_value">Количество цифр: %1$d</string>
|
||||
<string name="two_fa_field_period_seconds_value">Период обновления: %1$d с</string>
|
||||
<string name="storage_home_status_line">Status: %1$s, %2$s</string>
|
||||
<string name="storage_home_status_available">available</string>
|
||||
<string name="storage_home_status_unavailable">unavailable</string>
|
||||
<string name="storage_home_status_encrypted">encrypted</string>
|
||||
<string name="storage_home_status_not_encrypted">not encrypted</string>
|
||||
<string name="storage_home_two_fa_title">2FA tokens (%1$d)</string>
|
||||
<string name="storage_home_open_two_fa">Open 2FA</string>
|
||||
<string name="storage_home_two_fa_subtitle">Two-factor authentication codes and secrets</string>
|
||||
<string name="storage_home_text_secrets_title">Text secrets (%1$d)</string>
|
||||
<string name="storage_home_open_text_secrets">Open text secrets</string>
|
||||
<string name="storage_home_text_secrets_subtitle">Notes, tokens, and arbitrary key-value pairs</string>
|
||||
<string name="storage_home_future_sections">Files, Media, and more will appear here soon.</string>
|
||||
<string name="two_fa_add_token">Add token</string>
|
||||
<string name="two_fa_empty_state">No 2FA tokens yet</string>
|
||||
<string name="two_fa_create_title">New 2FA token</string>
|
||||
<string name="two_fa_edit_title">Edit 2FA token</string>
|
||||
<string name="two_fa_field_issuer">Service</string>
|
||||
<string name="two_fa_field_account">Account</string>
|
||||
<string name="two_fa_field_secret">Secret</string>
|
||||
<string name="two_fa_field_notes_optional">Note (optional)</string>
|
||||
<string name="two_fa_field_digits">Code digits (usually 6 or 8)</string>
|
||||
<string name="two_fa_field_period_seconds">Refresh period in seconds (usually 30)</string>
|
||||
<string name="two_fa_field_algorithm">Algorithm (SHA1, SHA256, SHA512)</string>
|
||||
<string name="two_fa_field_digits_value">Digits: %1$d</string>
|
||||
<string name="two_fa_field_period_seconds_value">Refresh period: %1$d s</string>
|
||||
<string name="two_fa_code_unavailable">------</string>
|
||||
<string name="two_fa_code_refresh_in">Обновление через %1$d с</string>
|
||||
<string name="two_fa_code_refresh_label">Обновление через</string>
|
||||
<string name="two_fa_code_refresh_seconds">%1$d с</string>
|
||||
<string name="two_fa_code_invalid_secret">Неверный секрет или формат</string>
|
||||
<string name="two_fa_copy_code_hint">Нажмите, чтобы скопировать код</string>
|
||||
<string name="two_fa_scan_qr_action">Сканировать QR</string>
|
||||
<string name="two_fa_scan_qr_title">Сканирование QR-кода TOTP</string>
|
||||
<string name="two_fa_scan_qr_invalid">QR-код не содержит валидный otpauth://totp URI</string>
|
||||
<string name="two_fa_camera_permission_required">Нужно разрешение на камеру для сканирования QR</string>
|
||||
|
||||
<string name="text_secret_create">Создать секрет</string>
|
||||
<string name="text_secret_edit">Редактировать секрет</string>
|
||||
<string name="text_secret_title">Название</string>
|
||||
<string name="text_secret_empty_state">Пока нет текстовых секретов</string>
|
||||
<string name="text_secret_items_count">Элементов: %1$d</string>
|
||||
<string name="text_secret_item_without_label">Без названия</string>
|
||||
<string name="text_secret_more_fields">ещё полей: %1$d</string>
|
||||
<string name="text_secret_item_label_optional">Название (опционально)</string>
|
||||
<string name="text_secret_item_value">Значение</string>
|
||||
<string name="text_secret_add_item">Добавить пару</string>
|
||||
<string name="text_secret_copy_value">Скопировать значение</string>
|
||||
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="open">Открыть</string>
|
||||
<string name="edit">Редактировать</string>
|
||||
|
||||
<string name="common_unknown">Неизвестно</string>
|
||||
<string name="two_fa_code_refresh_in">Refresh in %1$d s</string>
|
||||
<string name="two_fa_code_refresh_label">Refresh in</string>
|
||||
<string name="two_fa_code_refresh_seconds">%1$d s</string>
|
||||
<string name="two_fa_code_invalid_secret">Invalid secret or format</string>
|
||||
<string name="two_fa_copy_code_hint">Tap to copy code</string>
|
||||
<string name="two_fa_scan_qr_action">Scan QR</string>
|
||||
<string name="two_fa_scan_qr_title">Scan TOTP QR code</string>
|
||||
<string name="two_fa_scan_qr_invalid">QR code does not contain a valid otpauth://totp URI</string>
|
||||
<string name="two_fa_camera_permission_required">Camera permission is required to scan QR</string>
|
||||
<string name="text_secret_create">Create secret</string>
|
||||
<string name="text_secret_edit">Edit secret</string>
|
||||
<string name="text_secret_title">Title</string>
|
||||
<string name="text_secret_empty_state">No text secrets yet</string>
|
||||
<string name="text_secret_items_count">Items: %1$d</string>
|
||||
<string name="text_secret_item_without_label">Untitled</string>
|
||||
<string name="text_secret_more_fields">more fields: %1$d</string>
|
||||
<string name="text_secret_item_label_optional">Label (optional)</string>
|
||||
<string name="text_secret_item_value">Value</string>
|
||||
<string name="text_secret_add_item">Add field</string>
|
||||
<string name="text_secret_copy_value">Copy value</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="open">Open</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="common_unknown">Unknown</string>
|
||||
<string name="settings_language_section">Language</string>
|
||||
<string name="settings_language_system">System default</string>
|
||||
<string name="settings_language_english">English</string>
|
||||
<string name="settings_language_russian">Russian</string>
|
||||
<string name="task_pipeline_test_elapsed">Elapsed: %1$d s / %2$d s</string>
|
||||
<string name="text_secret_clipboard_fallback_label">value</string>
|
||||
<string name="sync_progress_no_groups">Storage sync: no groups configured</string>
|
||||
<string name="sync_progress_preparing">Storage sync: preparing %1$d groups</string>
|
||||
<string name="sync_progress_started">Storage sync: started</string>
|
||||
<string name="sync_progress_completed">Storage sync: completed</string>
|
||||
<string name="sync_progress_group_preparing">Storage sync: group "%1$s" preparing</string>
|
||||
<string name="sync_progress_group_not_found">Storage sync: group "%1$s" not found</string>
|
||||
<string name="sync_progress_group_skipped_few_storages">Storage sync: group "%1$s" skipped (need at least 2 storages)</string>
|
||||
<string name="sync_progress_group_skipped_incompatible">Storage sync: group "%1$s" skipped (incompatible encryption: %2$d)</string>
|
||||
<string name="sync_progress_group_acquiring_locks">Storage sync: group "%1$s" acquiring locks</string>
|
||||
<string name="sync_progress_group_lock">Storage sync: group "%1$s" lock %2$d/%3$d</string>
|
||||
<string name="sync_progress_group_lock_failed">Storage sync: group "%1$s" lock failed, group skipped</string>
|
||||
<string name="sync_progress_group_reading_journals">Storage sync: group "%1$s" reading journals</string>
|
||||
<string name="sync_progress_group_cancelled">Storage sync: group "%1$s" cancelled by newer run</string>
|
||||
<string name="sync_progress_group_journal">Storage sync: group "%1$s" journal %2$d/%3$d</string>
|
||||
<string name="sync_progress_group_no_entries">Storage sync: group "%1$s" no journal entries</string>
|
||||
<string name="sync_progress_group_processing">Storage sync: group "%1$s" processing %2$d entries</string>
|
||||
<string name="sync_progress_group_entry">Storage sync: group "%1$s" entry %2$d/%3$d</string>
|
||||
<string name="sync_progress_group_completed">Storage sync: group "%1$s" completed</string>
|
||||
<string name="sync_progress_group_renewing_locks">Storage sync: group "%1$s" renewing locks</string>
|
||||
<string name="sync_progress_group_lock_renewal_failed">Storage sync: group "%1$s" lock renewal failed</string>
|
||||
<string name="task_progress_clear_content">%1$d / %2$d</string>
|
||||
<string name="task_log_sync_started">Storage sync started</string>
|
||||
<string name="task_log_sync_finished">Storage sync finished</string>
|
||||
<string name="task_log_sync_failed">Storage sync failed: %1$s</string>
|
||||
<string name="task_log_enumerating">Enumerating files and directories…</string>
|
||||
<string name="task_log_enumerate_done">Done: %1$d files, %2$d dirs in %3$d ms (see app log for lines)</string>
|
||||
<string name="task_log_creating_storage">Creating storage…</string>
|
||||
<string name="task_log_storage_created">Storage created</string>
|
||||
<string name="task_log_checking_storage">Checking storage…</string>
|
||||
<string name="task_log_encrypting">Encrypting…</string>
|
||||
<string name="task_log_encryption_enabled">Encryption enabled</string>
|
||||
<string name="task_log_already_encrypted">Storage is already encrypted</string>
|
||||
<string name="task_log_not_empty">Storage is not empty</string>
|
||||
<string name="task_log_empty_unknown">Cannot determine whether storage is empty</string>
|
||||
<string name="task_log_unsupported_type">Unsupported storage type</string>
|
||||
<string name="task_log_enable_encryption_failed">Failed to enable encryption</string>
|
||||
<string name="task_log_opening_storage">Opening encrypted storage…</string>
|
||||
<string name="task_log_storage_opened">Storage opened</string>
|
||||
<string name="task_log_open_storage_failed">Failed to open encrypted storage</string>
|
||||
<string name="task_log_closing_storage">Closing storage…</string>
|
||||
<string name="task_log_storage_closed">Storage closed</string>
|
||||
<string name="task_log_close_storage_failed">Failed to close encrypted storage</string>
|
||||
<string name="task_log_disabling_encryption">Disabling encryption…</string>
|
||||
<string name="task_log_encryption_disabled">Encryption disabled</string>
|
||||
<string name="task_log_disable_encryption_failed">Failed to disable encryption</string>
|
||||
<string name="task_log_renaming">Renaming…</string>
|
||||
<string name="task_log_renamed">Renamed</string>
|
||||
<string name="task_log_rename_failed">Rename failed</string>
|
||||
<string name="task_log_removing_storage">Removing storage…</string>
|
||||
<string name="task_log_removed">Removed</string>
|
||||
<string name="task_log_remove_failed">Remove failed</string>
|
||||
<string name="task_log_invalid_storage">Invalid storage</string>
|
||||
<string name="task_log_clearing_sync_lock">Clearing sync lock…</string>
|
||||
<string name="task_log_sync_lock_cleared">Sync lock cleared</string>
|
||||
<string name="task_log_clear_sync_lock_failed">Failed to clear sync lock</string>
|
||||
<string name="task_log_adding_vault">Adding vault…</string>
|
||||
<string name="task_log_vault_added">Vault added</string>
|
||||
<string name="task_log_add_vault_failed">Failed to add vault</string>
|
||||
<string name="task_log_removing_remote_vault">Removing remote vault…</string>
|
||||
<string name="task_log_remote_vault_removed">Remote vault removed</string>
|
||||
<string name="task_log_remove_vault_failed">Failed to remove vault</string>
|
||||
<string name="task_log_retrying_vault">Retrying remote vault connection…</string>
|
||||
<string name="task_log_retry_requested">Retry requested</string>
|
||||
<string name="task_log_retry_vault_failed">Failed to retry remote vault</string>
|
||||
<string name="task_log_test_started">Test task started for %1$d s</string>
|
||||
<string name="task_log_test_finished">Test task finished</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.nullptroma.wallenc.ui.resources
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.VaultTaskStep
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class TaskProgressLabelsTest {
|
||||
|
||||
private val resolver = UiStringResolver { id, _ -> "res:$id" }
|
||||
|
||||
@Test
|
||||
fun syncNoGroups_mapsToStringRes() {
|
||||
val text = TaskProgressLabel.SyncNoGroups.resolve(resolver)
|
||||
assertEquals("res:${R.string.sync_progress_no_groups}", text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun vaultTask_mapsToStringRes() {
|
||||
val text = TaskProgressLabel.VaultTask(VaultTaskStep.Save2FaToken).resolve(resolver)
|
||||
assertEquals("res:${R.string.task_progress_save_2fa_token}", text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearContentProgress_mapsToStringRes() {
|
||||
val text = TaskProgressLabel.ClearContentProgress(3, 10).resolve(resolver)
|
||||
assertEquals("res:${R.string.task_progress_clear_content}", text)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogKey
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -28,6 +30,7 @@ class RunStorageSyncUseCase(
|
||||
* @param logReason техническая метка для логов (не для UI)
|
||||
* @return false, если синхронизация уже в очереди или выполняется — новая задача не создана
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun enqueue(displayTitle: String, logReason: String): Boolean {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
return false
|
||||
@@ -39,17 +42,16 @@ class RunStorageSyncUseCase(
|
||||
dispatcher = Dispatchers.IO,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.log(TaskLogLevel.Info, "Storage sync started, reason=$logReason")
|
||||
ctx.reportProgress(null, "Storage sync: started")
|
||||
ctx.log(TaskLogLevel.Info, TaskLogKey.SyncStarted)
|
||||
ctx.reportProgress(null, TaskProgressLabel.SyncStarted)
|
||||
syncEngine.syncAllGroups { fraction, label ->
|
||||
ctx.reportProgress(fraction, label)
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, "Storage sync finished")
|
||||
ctx.reportProgress(null, "Storage sync: completed")
|
||||
ctx.log(TaskLogLevel.Info, TaskLogKey.SyncFinished)
|
||||
ctx.reportProgress(null, TaskProgressLabel.SyncCompleted)
|
||||
} catch (e: Exception) {
|
||||
val err = e.toWallencException()
|
||||
ctx.log(TaskLogLevel.Error, "Storage sync failed: $err")
|
||||
ctx.reportProgress(null, "Storage sync: failed - $err")
|
||||
ctx.log(TaskLogLevel.Error, TaskLogKey.SyncFailed(err))
|
||||
ctx.fail(err)
|
||||
} finally {
|
||||
running.set(false)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncOperation
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncOperation
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -26,29 +27,29 @@ class StorageSyncEngine(
|
||||
private val syncGeneration = AtomicLong(0)
|
||||
|
||||
override suspend fun syncAllGroups(
|
||||
reportProgress: (suspend (fraction: Float?, label: String?) -> Unit)?,
|
||||
reportProgress: (suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit)?,
|
||||
): Unit = withContext(Dispatchers.IO) {
|
||||
val reporter = reportProgress ?: { _: Float?, _: String? -> }
|
||||
val reporter = reportProgress ?: { _: Float?, _: TaskProgressLabel? -> }
|
||||
val groups = groupStore.getGroups()
|
||||
if (groups.isEmpty()) {
|
||||
reporter(null, "Storage sync: no groups configured")
|
||||
reporter(null, TaskProgressLabel.SyncNoGroups)
|
||||
return@withContext
|
||||
}
|
||||
reporter(null, "Storage sync: preparing ${groups.size} groups")
|
||||
reporter(null, TaskProgressLabel.SyncPreparing(groups.size))
|
||||
for (group in groups) {
|
||||
syncGroupInternal(
|
||||
groupId = group.id,
|
||||
reportProgress = reporter,
|
||||
)
|
||||
}
|
||||
reporter(null, "Storage sync: completed")
|
||||
reporter(null, TaskProgressLabel.SyncCompleted)
|
||||
}
|
||||
|
||||
override suspend fun syncGroup(
|
||||
groupId: String,
|
||||
reportProgress: (suspend (fraction: Float?, label: String?) -> Unit)?,
|
||||
reportProgress: (suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit)?,
|
||||
): Unit = withContext(Dispatchers.IO) {
|
||||
val reporter = reportProgress ?: { _: Float?, _: String? -> }
|
||||
val reporter = reportProgress ?: { _: Float?, _: TaskProgressLabel? -> }
|
||||
syncGroupInternal(
|
||||
groupId = groupId,
|
||||
reportProgress = reporter,
|
||||
@@ -57,20 +58,20 @@ class StorageSyncEngine(
|
||||
|
||||
private suspend fun syncGroupInternal(
|
||||
groupId: String,
|
||||
reportProgress: suspend (fraction: Float?, label: String?) -> Unit,
|
||||
reportProgress: suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit,
|
||||
) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" preparing")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupPreparing(groupId))
|
||||
val mutex = groupMutexes.getOrPut(groupId) { Mutex() }
|
||||
mutex.withLock {
|
||||
val generationSnapshot = syncGeneration.incrementAndGet()
|
||||
val group = groupStore.getGroups().firstOrNull { it.id == groupId }
|
||||
if (group == null) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" not found")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupNotFound(groupId))
|
||||
return
|
||||
}
|
||||
val storages = resolveStorages(group.storageUuids)
|
||||
if (storages.size < 2) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" skipped (need at least 2 storages)")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupSkippedTooFewStorages(groupId))
|
||||
return
|
||||
}
|
||||
val incompatible = storages.filterNot { storage ->
|
||||
@@ -83,7 +84,7 @@ class StorageSyncEngine(
|
||||
if (incompatible.isNotEmpty()) {
|
||||
reportProgress(
|
||||
null,
|
||||
"Storage sync: group \"$groupId\" skipped (incompatible encryption: ${incompatible.size})",
|
||||
TaskProgressLabel.SyncGroupSkippedIncompatibleEncryption(groupId, incompatible.size),
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -91,12 +92,15 @@ class StorageSyncEngine(
|
||||
var leaseUntil = Instant.now().plusSeconds(SYNC_LOCK_LEASE_SECONDS)
|
||||
val lockedAccessors = mutableListOf<IStorageAccessor>()
|
||||
try {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" acquiring locks")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupAcquiringLocks(groupId))
|
||||
for ((lockIndex, storage) in storages.withIndex()) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" lock ${lockIndex + 1}/${storages.size}")
|
||||
reportProgress(
|
||||
null,
|
||||
TaskProgressLabel.SyncGroupLockProgress(groupId, lockIndex + 1, storages.size),
|
||||
)
|
||||
val locked = storage.accessor.tryAcquireSyncLock(holderId, leaseUntil)
|
||||
if (!locked) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" lock failed, group skipped")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupLockFailed(groupId))
|
||||
return
|
||||
}
|
||||
lockedAccessors.add(storage.accessor)
|
||||
@@ -105,7 +109,7 @@ class StorageSyncEngine(
|
||||
val latestByPath = mutableMapOf<String, StorageSyncJournalEntry>()
|
||||
val entriesByStorage = mutableMapOf<UUID, Map<String, StorageSyncJournalEntry>>()
|
||||
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" reading journals")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupReadingJournals(groupId))
|
||||
for ((journalIndex, storage) in storages.withIndex()) {
|
||||
leaseUntil = renewLocksIfNeeded(
|
||||
groupId = groupId,
|
||||
@@ -114,10 +118,13 @@ class StorageSyncEngine(
|
||||
reportProgress = reportProgress,
|
||||
) ?: return
|
||||
if (syncGeneration.get() != generationSnapshot) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" cancelled by newer run")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupCancelled(groupId))
|
||||
return
|
||||
}
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" journal ${journalIndex + 1}/${storages.size}")
|
||||
reportProgress(
|
||||
null,
|
||||
TaskProgressLabel.SyncGroupJournalProgress(groupId, journalIndex + 1, storages.size),
|
||||
)
|
||||
val latestEntries = latestByPath(storage.accessor.readSyncJournal())
|
||||
entriesByStorage[storage.uuid] = latestEntries
|
||||
for ((path, entry) in latestEntries) {
|
||||
@@ -130,11 +137,14 @@ class StorageSyncEngine(
|
||||
|
||||
val mergedEntries = latestByPath.entries.toList()
|
||||
if (mergedEntries.isEmpty()) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" no journal entries")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupNoJournalEntries(groupId))
|
||||
return
|
||||
}
|
||||
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" processing ${mergedEntries.size} entries")
|
||||
reportProgress(
|
||||
null,
|
||||
TaskProgressLabel.SyncGroupProcessingEntries(groupId, mergedEntries.size),
|
||||
)
|
||||
for ((pathIndex, merged) in mergedEntries.withIndex()) {
|
||||
leaseUntil = renewLocksIfNeeded(
|
||||
groupId = groupId,
|
||||
@@ -144,9 +154,12 @@ class StorageSyncEngine(
|
||||
) ?: return
|
||||
val path = merged.key
|
||||
val winnerEntry = merged.value
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" entry ${pathIndex + 1}/${mergedEntries.size}")
|
||||
reportProgress(
|
||||
null,
|
||||
TaskProgressLabel.SyncGroupEntryProgress(groupId, pathIndex + 1, mergedEntries.size),
|
||||
)
|
||||
if (syncGeneration.get() != generationSnapshot) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" cancelled by newer run")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupCancelled(groupId))
|
||||
return
|
||||
}
|
||||
val sourceStorage = findSourceStorage(storages, entriesByStorage, path, winnerEntry)
|
||||
@@ -172,7 +185,7 @@ class StorageSyncEngine(
|
||||
}
|
||||
}
|
||||
}
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" completed")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupCompleted(groupId))
|
||||
} finally {
|
||||
for (accessor in lockedAccessors) {
|
||||
runCatching {
|
||||
@@ -187,20 +200,20 @@ class StorageSyncEngine(
|
||||
groupId: String,
|
||||
lockedAccessors: List<IStorageAccessor>,
|
||||
currentLeaseUntil: Instant,
|
||||
reportProgress: suspend (fraction: Float?, label: String?) -> Unit,
|
||||
reportProgress: suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit,
|
||||
): Instant? {
|
||||
val now = Instant.now()
|
||||
if (currentLeaseUntil.isAfter(now.plusSeconds(SYNC_LOCK_RENEW_MARGIN_SECONDS))) {
|
||||
return currentLeaseUntil
|
||||
}
|
||||
val nextLeaseUntil = now.plusSeconds(SYNC_LOCK_LEASE_SECONDS)
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" renewing locks")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupRenewingLocks(groupId))
|
||||
for (accessor in lockedAccessors) {
|
||||
val renewed = runCatching {
|
||||
accessor.tryAcquireSyncLock(holderId, nextLeaseUntil)
|
||||
}.getOrElse { false }
|
||||
if (!renewed) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" lock renewal failed")
|
||||
reportProgress(null, TaskProgressLabel.SyncGroupLockRenewalFailed(groupId))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user