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