Исправлено множество предупреждений
This commit is contained in:
@@ -35,7 +35,7 @@ class YandexDiskLiveIntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() = runBlocking {
|
fun tearDown(): Unit = runBlocking {
|
||||||
runCatching { repository.delete(probePath, permanently = true) }
|
runCatching { repository.delete(probePath, permanently = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,6 @@ object YandexTestCredentials {
|
|||||||
fun oauthToken(): String? =
|
fun oauthToken(): String? =
|
||||||
InstrumentationRegistry.getArguments().getString("yandex.oauth.token")?.takeIf { it.isNotBlank() }
|
InstrumentationRegistry.getArguments().getString("yandex.oauth.token")?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
fun userId(): String? =
|
|
||||||
InstrumentationRegistry.getArguments().getString("yandex.user.id")?.takeIf { it.isNotBlank() }
|
|
||||||
|
|
||||||
fun vaultUuid(): String? =
|
|
||||||
InstrumentationRegistry.getArguments().getString("yandex.vault.uuid")?.takeIf { it.isNotBlank() }
|
|
||||||
|
|
||||||
fun assumePresent(message: String = "Добавьте yandex.test.oauth.token в local.properties") {
|
fun assumePresent(message: String = "Добавьте yandex.test.oauth.token в local.properties") {
|
||||||
assumeFalse(message, oauthToken().isNullOrBlank())
|
assumeFalse(message, oauthToken().isNullOrBlank())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".tasks.TaskPipelineForegroundService"
|
android:name=".tasks.TaskPipelineForegroundService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:description="@string/fgs_task_pipeline_description" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -16,11 +16,19 @@ object UiStringModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideUiStringResolver(@ApplicationContext context: Context): UiStringResolver =
|
fun provideUiStringResolver(@ApplicationContext context: Context): UiStringResolver =
|
||||||
UiStringResolver { id, args ->
|
object : UiStringResolver {
|
||||||
if (args.isEmpty()) {
|
override fun invoke(id: Int, vararg formatArgs: Any): String =
|
||||||
|
if (formatArgs.isEmpty()) {
|
||||||
context.getString(id)
|
context.getString(id)
|
||||||
} else {
|
} else {
|
||||||
context.getString(id, *args)
|
context.getString(id, *formatArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun plurals(id: Int, quantity: Int, vararg formatArgs: Any): String =
|
||||||
|
if (formatArgs.isEmpty()) {
|
||||||
|
context.resources.getQuantityString(id, quantity)
|
||||||
|
} else {
|
||||||
|
context.resources.getQuantityString(id, quantity, *formatArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Хранение выбранного языка. SharedPreferences — для синхронного чтения в
|
* Хранение выбранного языка. SharedPreferences — для синхронного чтения в
|
||||||
@@ -50,7 +51,7 @@ internal object AppLocaleStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun persistLanguage(context: Context, language: AppLanguage) {
|
fun persistLanguage(context: Context, language: AppLanguage) {
|
||||||
prefs(context).edit().putString(PREFS_KEY_LANGUAGE, language.storageValue).apply()
|
prefs(context).edit { putString(PREFS_KEY_LANGUAGE, language.storageValue) }
|
||||||
applyLanguage(language)
|
applyLanguage(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ internal object AppLocaleStorage {
|
|||||||
runCatching {
|
runCatching {
|
||||||
val legacy = storage.legacyLocaleDataStore.data.first()[legacyLanguageKey]
|
val legacy = storage.legacyLocaleDataStore.data.first()[legacyLanguageKey]
|
||||||
if (legacy != null) {
|
if (legacy != null) {
|
||||||
prefs(storage).edit().putString(PREFS_KEY_LANGUAGE, legacy).apply()
|
prefs(storage).edit { putString(PREFS_KEY_LANGUAGE, legacy) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@
|
|||||||
<string name="task_notification_preparing">Подготовка…</string>
|
<string name="task_notification_preparing">Подготовка…</string>
|
||||||
<string name="task_notification_indeterminate">Выполняется…</string>
|
<string name="task_notification_indeterminate">Выполняется…</string>
|
||||||
<string name="task_notification_cancel">Отмена</string>
|
<string name="task_notification_cancel">Отмена</string>
|
||||||
|
<string name="fgs_task_pipeline_description">Показывает прогресс фоновых задач хранилищ и vault.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -6,4 +6,5 @@
|
|||||||
<string name="task_notification_preparing">Preparing…</string>
|
<string name="task_notification_preparing">Preparing…</string>
|
||||||
<string name="task_notification_indeterminate">Running…</string>
|
<string name="task_notification_indeterminate">Running…</string>
|
||||||
<string name="task_notification_cancel">Cancel</string>
|
<string name="task_notification_cancel">Cancel</string>
|
||||||
|
<string name="fgs_task_pipeline_description">Shows progress while storage and vault tasks run in the background.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.vault.auth
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope-ы Яндекс.OAuth, которые нам нужны: только app_folder + disk.info.
|
|
||||||
*
|
|
||||||
* Используется как ссылка для синхронизации с консолью Yandex OAuth.
|
|
||||||
* Сам Yandex Auth SDK для WEBVIEW-логина запрашивает scope-ы, выставленные
|
|
||||||
* у приложения в OAuth-консоли; мы держим список здесь, чтобы было одно место правды.
|
|
||||||
*/
|
|
||||||
object YandexOAuthScopes {
|
|
||||||
const val DISK_APP_FOLDER = "cloud_api:disk.app_folder"
|
|
||||||
const val DISK_INFO = "cloud_api:disk.info"
|
|
||||||
|
|
||||||
val ALL: Set<String> = setOf(DISK_APP_FOLDER, DISK_INFO)
|
|
||||||
}
|
|
||||||
@@ -9,13 +9,13 @@ import java.io.IOException
|
|||||||
|
|
||||||
fun Throwable.toVaultWallencException(): WallencException = when (this) {
|
fun Throwable.toVaultWallencException(): WallencException = when (this) {
|
||||||
is WallencException -> this
|
is WallencException -> this
|
||||||
is YandexDiskAuthException -> WallencException.Auth.Failed
|
is YandexDiskAuthException -> WallencException.Auth.Failed()
|
||||||
is HttpException -> WallencException.Network.HttpFailed(
|
is HttpException -> WallencException.Network.HttpFailed(
|
||||||
operation = "http",
|
operation = "http",
|
||||||
statusCode = code(),
|
statusCode = code(),
|
||||||
cause = this,
|
cause = this,
|
||||||
)
|
)
|
||||||
is FileNotFoundException -> WallencException.Storage.FileNotFound
|
is FileNotFoundException -> WallencException.Storage.FileNotFound()
|
||||||
is IOException -> mapVaultIo(this)
|
is IOException -> mapVaultIo(this)
|
||||||
is IllegalStateException -> mapIllegalState(this)
|
is IllegalStateException -> mapIllegalState(this)
|
||||||
is IllegalArgumentException -> mapIllegalArgument(this)
|
is IllegalArgumentException -> mapIllegalArgument(this)
|
||||||
@@ -26,29 +26,29 @@ private fun mapVaultIo(e: IOException): WallencException {
|
|||||||
val msg = e.message.orEmpty()
|
val msg = e.message.orEmpty()
|
||||||
return when {
|
return when {
|
||||||
msg.contains("OAuth token is missing", ignoreCase = true) ->
|
msg.contains("OAuth token is missing", ignoreCase = true) ->
|
||||||
WallencException.Auth.TokenMissing
|
WallencException.Auth.TokenMissing()
|
||||||
msg.contains("HTTP 423", ignoreCase = true) || msg.contains("423 after retries", ignoreCase = true) ->
|
msg.contains("HTTP 423", ignoreCase = true) || msg.contains("423 after retries", ignoreCase = true) ->
|
||||||
WallencException.Network.ResourceLocked
|
WallencException.Network.ResourceLocked()
|
||||||
msg.contains("async operation timed out", ignoreCase = true) ->
|
msg.contains("async operation timed out", ignoreCase = true) ->
|
||||||
WallencException.Network.OperationTimedOut
|
WallencException.Network.OperationTimedOut()
|
||||||
msg.contains("async operation failed", ignoreCase = true) ->
|
msg.contains("async operation failed", ignoreCase = true) ->
|
||||||
WallencException.Network.OperationFailed
|
WallencException.Network.OperationFailed()
|
||||||
else -> WallencException.Network.IoFailed(e)
|
else -> WallencException.Network.IoFailed(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapIllegalState(e: IllegalStateException): WallencException = when {
|
private fun mapIllegalState(e: IllegalStateException): WallencException = when {
|
||||||
e.message == "Not a file" -> WallencException.Storage.NotAFile
|
e.message == "Not a file" -> WallencException.Storage.NotAFile()
|
||||||
e.message == "Not a directory" -> WallencException.Storage.NotADirectory
|
e.message == "Not a directory" -> WallencException.Storage.NotADirectory()
|
||||||
e.message?.startsWith("Path segment is a file:") == true -> WallencException.Storage.PathIsFile
|
e.message?.startsWith("Path segment is a file:") == true -> WallencException.Storage.PathIsFile()
|
||||||
e.message?.startsWith("Cannot openWrite over directory:") == true ->
|
e.message?.startsWith("Cannot openWrite over directory:") == true ->
|
||||||
WallencException.Storage.CannotWriteOverDirectory
|
WallencException.Storage.CannotWriteOverDirectory()
|
||||||
e.message?.startsWith("Expected file after upload:") == true ->
|
e.message?.startsWith("Expected file after upload:") == true ->
|
||||||
WallencException.Storage.UnexpectedState
|
WallencException.Storage.UnexpectedState()
|
||||||
else -> WallencException.Storage.UnexpectedState
|
else -> WallencException.Storage.UnexpectedState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapIllegalArgument(e: IllegalArgumentException): WallencException = when {
|
private fun mapIllegalArgument(e: IllegalArgumentException): WallencException = when {
|
||||||
e.message == "Deleting root path is forbidden" -> WallencException.Storage.DeleteRootForbidden
|
e.message == "Deleting root path is forbidden" -> WallencException.Storage.DeleteRootForbidden()
|
||||||
else -> WallencException.Unknown(e)
|
else -> WallencException.Unknown(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import retrofit2.http.DELETE
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Headers
|
import retrofit2.http.Headers
|
||||||
import retrofit2.http.PATCH
|
import retrofit2.http.PATCH
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.http.PUT
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
import retrofit2.http.Url
|
import retrofit2.http.Url
|
||||||
@@ -46,13 +45,6 @@ interface YandexDiskApi {
|
|||||||
@Query("permanently") permanently: Boolean,
|
@Query("permanently") permanently: Boolean,
|
||||||
): Response<ResponseBody>
|
): Response<ResponseBody>
|
||||||
|
|
||||||
@POST("v1/disk/resources/move")
|
|
||||||
suspend fun moveResource(
|
|
||||||
@Query("from") from: String,
|
|
||||||
@Query("path") toPath: String,
|
|
||||||
@Query("overwrite") overwrite: Boolean = false,
|
|
||||||
): Response<ResponseBody>
|
|
||||||
|
|
||||||
@GET("v1/disk/resources/upload")
|
@GET("v1/disk/resources/upload")
|
||||||
suspend fun getUploadLink(
|
suspend fun getUploadLink(
|
||||||
@Query("path") path: String,
|
@Query("path") path: String,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class YandexDiskApiFactory(
|
|||||||
/**
|
/**
|
||||||
* [tokenProvider] вызывается на каждый HTTP-запрос к cloud-api (свежий токен из БД).
|
* [tokenProvider] вызывается на каждый HTTP-запрос к cloud-api (свежий токен из БД).
|
||||||
*/
|
*/
|
||||||
fun createAuthenticatedApi(tokenProvider: () -> String?): com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskApi {
|
fun createAuthenticatedApi(tokenProvider: () -> String?): YandexDiskApi {
|
||||||
val client = OkHttpClient.Builder()
|
val client = OkHttpClient.Builder()
|
||||||
.addInterceptor { chain ->
|
.addInterceptor { chain ->
|
||||||
val token = tokenProvider()
|
val token = tokenProvider()
|
||||||
|
|||||||
@@ -47,13 +47,6 @@ data class OperationStatusDto(
|
|||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
data class ApiErrorDto(
|
|
||||||
val message: String? = null,
|
|
||||||
val description: String? = null,
|
|
||||||
val error: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class CustomPropertiesPatchDto(
|
data class CustomPropertiesPatchDto(
|
||||||
@param:JsonProperty("custom_properties") val customProperties: Map<String, String>,
|
@param:JsonProperty("custom_properties") val customProperties: Map<String, String>,
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ class UnlockManager(
|
|||||||
rememberPassword: Boolean
|
rememberPassword: Boolean
|
||||||
): EncryptedStorage = withContext(ioDispatcher) {
|
): EncryptedStorage = withContext(ioDispatcher) {
|
||||||
return@withContext mutex.withLock {
|
return@withContext mutex.withLock {
|
||||||
val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing
|
val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing()
|
||||||
if (!Encryptor.checkKey(key, encInfo))
|
if (!Encryptor.checkKey(key, encInfo))
|
||||||
throw WallencException.Storage.IncorrectKey
|
throw WallencException.Storage.IncorrectKey()
|
||||||
|
|
||||||
val opened = _openedStorages.value.toMutableMap()
|
val opened = _openedStorages.value.toMutableMap()
|
||||||
val cur = opened[storage.uuid]
|
val cur = opened[storage.uuid]
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class EncryptedStorage private constructor(
|
|||||||
private val scope = CoroutineScope(ioDispatcher + job)
|
private val scope = CoroutineScope(ioDispatcher + job)
|
||||||
|
|
||||||
private val encInfo =
|
private val encInfo =
|
||||||
source.metaInfo.value.encInfo ?: throw WallencException.Storage.NotEncrypted
|
source.metaInfo.value.encInfo ?: throw WallencException.Storage.NotEncrypted()
|
||||||
|
|
||||||
override val isVirtualStorage: Boolean = true
|
override val isVirtualStorage: Boolean = true
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class EncryptedStorage private constructor(
|
|||||||
|
|
||||||
private fun checkKey() {
|
private fun checkKey() {
|
||||||
if (!Encryptor.checkKey(key, encInfo))
|
if (!Encryptor.checkKey(key, encInfo))
|
||||||
throw WallencException.Storage.IncorrectKey
|
throw WallencException.Storage.IncorrectKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKey(): EncryptKey = EncryptKey(key.bytes)
|
fun getKey(): EncryptKey = EncryptKey(key.bytes)
|
||||||
|
|||||||
@@ -128,18 +128,10 @@ class EncryptedStorageAccessor(
|
|||||||
return source.getFiles(encryptPath(systemHiddenDirName))
|
return source.getFiles(encryptPath(systemHiddenDirName))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun encryptEntity(file: IFile): IFile {
|
|
||||||
return CommonFile(encryptMeta(file.metaInfo))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decryptEntity(file: IFile): IFile {
|
private fun decryptEntity(file: IFile): IFile {
|
||||||
return CommonFile(decryptMeta(file.metaInfo))
|
return CommonFile(decryptMeta(file.metaInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun encryptEntity(dir: IDirectory): IDirectory {
|
|
||||||
return CommonDirectory(encryptMeta(dir.metaInfo), dir.elementsCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decryptEntity(dir: IDirectory): IDirectory {
|
private fun decryptEntity(dir: IDirectory): IDirectory {
|
||||||
return CommonDirectory(decryptMeta(dir.metaInfo), dir.elementsCount)
|
return CommonDirectory(decryptMeta(dir.metaInfo), dir.elementsCount)
|
||||||
}
|
}
|
||||||
@@ -303,12 +295,11 @@ class EncryptedStorageAccessor(
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val javaType = jackson.typeFactory.constructCollectionType(
|
val journalType = jackson.typeFactory.constructCollectionType(
|
||||||
List::class.java,
|
List::class.java,
|
||||||
StorageSyncJournalEntry::class.java,
|
StorageSyncJournalEntry::class.java,
|
||||||
)
|
)
|
||||||
@Suppress("UNCHECKED_CAST")
|
jackson.readValue<List<StorageSyncJournalEntry>>(bytes, journalType)
|
||||||
(jackson.readValue(bytes, javaType) as List<StorageSyncJournalEntry>)
|
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,10 +214,6 @@ class LocalStorageAccessor(
|
|||||||
val filePath = Path(filesystemBasePath.pathString, storagePath)
|
val filePath = Path(filesystemBasePath.pathString, storagePath)
|
||||||
return from(filesystemBasePath, filePath.toFile())
|
return from(filesystemBasePath, filePath.toFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun from(filesystemBasePath: Path, meta: IMetaInfo): LocalStorageFilePair? {
|
|
||||||
return from(filesystemBasePath, meta.path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +228,7 @@ class LocalStorageAccessor(
|
|||||||
dirCallback: (suspend (File, CommonDirectory) -> Unit)? = null
|
dirCallback: (suspend (File, CommonDirectory) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
if (!checkAvailable())
|
if (!checkAvailable())
|
||||||
throw WallencException.Storage.NotAvailable
|
throw WallencException.Storage.NotAvailable()
|
||||||
val basePath = Path(_filesystemBasePath.pathString, baseStoragePath)
|
val basePath = Path(_filesystemBasePath.pathString, baseStoragePath)
|
||||||
val workedFiles = mutableSetOf<String>()
|
val workedFiles = mutableSetOf<String>()
|
||||||
val workedMetaFiles = mutableSetOf<String>()
|
val workedMetaFiles = mutableSetOf<String>()
|
||||||
@@ -398,7 +394,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun getFileInfo(path: String): IFile {
|
override suspend fun getFileInfo(path: String): IFile {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw WallencException.Storage.UnexpectedState
|
?: throw WallencException.Storage.UnexpectedState()
|
||||||
return CommonFile(
|
return CommonFile(
|
||||||
metaInfo = pair.meta,
|
metaInfo = pair.meta,
|
||||||
)
|
)
|
||||||
@@ -406,7 +402,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun getDirInfo(path: String): IDirectory {
|
override suspend fun getDirInfo(path: String): IDirectory {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw WallencException.Storage.UnexpectedState
|
?: throw WallencException.Storage.UnexpectedState()
|
||||||
return CommonDirectory(
|
return CommonDirectory(
|
||||||
metaInfo = pair.meta,
|
metaInfo = pair.meta,
|
||||||
elementsCount = null
|
elementsCount = null
|
||||||
@@ -415,7 +411,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun setHidden(path: String, hidden: Boolean) {
|
override suspend fun setHidden(path: String, hidden: Boolean) {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw WallencException.Storage.UnexpectedState
|
?: throw WallencException.Storage.UnexpectedState()
|
||||||
if (pair.meta.isHidden == hidden)
|
if (pair.meta.isHidden == hidden)
|
||||||
return
|
return
|
||||||
val newMeta = pair.meta.copy(isHidden = hidden)
|
val newMeta = pair.meta.copy(isHidden = hidden)
|
||||||
@@ -442,7 +438,7 @@ class LocalStorageAccessor(
|
|||||||
val path = Path(_filesystemBasePath.pathString, storagePath)
|
val path = Path(_filesystemBasePath.pathString, storagePath)
|
||||||
val file = path.toFile()
|
val file = path.toFile()
|
||||||
if (file.exists() && file.isDirectory) {
|
if (file.exists() && file.isDirectory) {
|
||||||
throw WallencException.Storage.UnexpectedState
|
throw WallencException.Storage.UnexpectedState()
|
||||||
} else if(!file.exists()) {
|
} else if(!file.exists()) {
|
||||||
val parent = Path(storagePath).parent
|
val parent = Path(storagePath).parent
|
||||||
createDir(parent.pathString)
|
createDir(parent.pathString)
|
||||||
@@ -453,7 +449,7 @@ class LocalStorageAccessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
||||||
?: throw WallencException.Storage.UnexpectedState
|
?: throw WallencException.Storage.UnexpectedState()
|
||||||
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
|
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
|
||||||
writeMeta(pair.metaFile, newMeta)
|
writeMeta(pair.metaFile, newMeta)
|
||||||
_filesUpdates.emit(
|
_filesUpdates.emit(
|
||||||
@@ -470,13 +466,13 @@ class LocalStorageAccessor(
|
|||||||
val path = Path(_filesystemBasePath.pathString, storagePath)
|
val path = Path(_filesystemBasePath.pathString, storagePath)
|
||||||
val file = path.toFile()
|
val file = path.toFile()
|
||||||
if (file.exists() && !file.isDirectory) {
|
if (file.exists() && !file.isDirectory) {
|
||||||
throw WallencException.Storage.UnexpectedState
|
throw WallencException.Storage.UnexpectedState()
|
||||||
} else if(!file.exists()) {
|
} else if(!file.exists()) {
|
||||||
Files.createDirectories(path)
|
Files.createDirectories(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
|
||||||
?: throw WallencException.Storage.UnexpectedState
|
?: throw WallencException.Storage.UnexpectedState()
|
||||||
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant())
|
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant())
|
||||||
writeMeta(pair.metaFile, newMeta)
|
writeMeta(pair.metaFile, newMeta)
|
||||||
_dirsUpdates.emit(
|
_dirsUpdates.emit(
|
||||||
@@ -514,7 +510,7 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
||||||
if (path == "/" || path.isBlank()) {
|
if (path == "/" || path.isBlank()) {
|
||||||
throw WallencException.Storage.DeleteRootForbidden
|
throw WallencException.Storage.DeleteRootForbidden()
|
||||||
}
|
}
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
if (pair != null) {
|
if (pair != null) {
|
||||||
@@ -529,7 +525,7 @@ class LocalStorageAccessor(
|
|||||||
override suspend fun openWrite(path: String): OutputStream = withContext(ioDispatcher) {
|
override suspend fun openWrite(path: String): OutputStream = withContext(ioDispatcher) {
|
||||||
touchFileInternal(path, recordJournal = false)
|
touchFileInternal(path, recordJournal = false)
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw WallencException.Storage.FileNotFound
|
?: throw WallencException.Storage.FileNotFound()
|
||||||
return@withContext pair.file.outputStream().onClosed {
|
return@withContext pair.file.outputStream().onClosed {
|
||||||
CoroutineScope(ioDispatcher).launch {
|
CoroutineScope(ioDispatcher).launch {
|
||||||
touchFileInternal(path, recordJournal = false)
|
touchFileInternal(path, recordJournal = false)
|
||||||
@@ -541,13 +537,13 @@ class LocalStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun openRead(path: String): InputStream = withContext(ioDispatcher) {
|
override suspend fun openRead(path: String): InputStream = withContext(ioDispatcher) {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw WallencException.Storage.FileNotFound
|
?: throw WallencException.Storage.FileNotFound()
|
||||||
return@withContext pair.file.inputStream()
|
return@withContext pair.file.inputStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun moveToTrash(path: String) = withContext(ioDispatcher) {
|
override suspend fun moveToTrash(path: String) = withContext(ioDispatcher) {
|
||||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||||
?: throw WallencException.Storage.FileNotFound
|
?: throw WallencException.Storage.FileNotFound()
|
||||||
val newMeta = pair.meta.copy(isDeleted = true)
|
val newMeta = pair.meta.copy(isDeleted = true)
|
||||||
writeMeta(pair.metaFile, newMeta)
|
writeMeta(pair.metaFile, newMeta)
|
||||||
appendSyncEntry(path = path, operation = StorageSyncOperation.DELETE)
|
appendSyncEntry(path = path, operation = StorageSyncOperation.DELETE)
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class YandexStorageAccessor(
|
|||||||
} catch (e: YandexDiskAuthException) {
|
} catch (e: YandexDiskAuthException) {
|
||||||
reportAuthFailure()
|
reportAuthFailure()
|
||||||
_storageReady.value = false
|
_storageReady.value = false
|
||||||
throw WallencException.Auth.Failed
|
throw WallencException.Auth.Failed()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_storageReady.value = false
|
_storageReady.value = false
|
||||||
throw e.toVaultWallencException()
|
throw e.toVaultWallencException()
|
||||||
@@ -121,7 +121,7 @@ class YandexStorageAccessor(
|
|||||||
throw e
|
throw e
|
||||||
} catch (e: YandexDiskAuthException) {
|
} catch (e: YandexDiskAuthException) {
|
||||||
reportAuthFailure()
|
reportAuthFailure()
|
||||||
throw WallencException.Auth.Failed
|
throw WallencException.Auth.Failed()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
throw e.toVaultWallencException()
|
throw e.toVaultWallencException()
|
||||||
}
|
}
|
||||||
@@ -176,9 +176,7 @@ class YandexStorageAccessor(
|
|||||||
systemDirEnsured = true
|
systemDirEnsured = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun statsFileRel(): String = "/$SYSTEM_HIDDEN_DIRNAME/$STATS_FILENAME"
|
private fun statsDiskPath(): String = toDiskPath("/$SYSTEM_HIDDEN_DIRNAME/$STATS_FILENAME")
|
||||||
|
|
||||||
private fun statsDiskPath(): String = toDiskPath(statsFileRel())
|
|
||||||
|
|
||||||
private suspend fun readPersistedStats(): YandexVaultPersistedStats? {
|
private suspend fun readPersistedStats(): YandexVaultPersistedStats? {
|
||||||
val meta = guard { repo.getOrNull(statsDiskPath()) } ?: return null
|
val meta = guard { repo.getOrNull(statsDiskPath()) } ?: return null
|
||||||
@@ -452,13 +450,13 @@ class YandexStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun getFileInfo(path: String): IFile = withContext(ioDispatcher) {
|
override suspend fun getFileInfo(path: String): IFile = withContext(ioDispatcher) {
|
||||||
val r = guard { repo.get(toDiskPath(path)) }
|
val r = guard { repo.get(toDiskPath(path)) }
|
||||||
if (r.type != "file") throw WallencException.Storage.NotAFile
|
if (r.type != "file") throw WallencException.Storage.NotAFile()
|
||||||
r.toCommonFile(path)
|
r.toCommonFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDirInfo(path: String): IDirectory = withContext(ioDispatcher) {
|
override suspend fun getDirInfo(path: String): IDirectory = withContext(ioDispatcher) {
|
||||||
val r = guard { repo.get(toDiskPath(path)) }
|
val r = guard { repo.get(toDiskPath(path)) }
|
||||||
if (r.type != "dir") throw WallencException.Storage.NotADirectory
|
if (r.type != "dir") throw WallencException.Storage.NotADirectory()
|
||||||
r.toCommonDir(path)
|
r.toCommonDir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +495,7 @@ class YandexStorageAccessor(
|
|||||||
val diskPath = toDiskPath(acc)
|
val diskPath = toDiskPath(acc)
|
||||||
when (guard { repo.getOrNull(diskPath) }?.type) {
|
when (guard { repo.getOrNull(diskPath) }?.type) {
|
||||||
"dir" -> continue
|
"dir" -> continue
|
||||||
"file" -> throw WallencException.Storage.PathIsFile
|
"file" -> throw WallencException.Storage.PathIsFile()
|
||||||
else -> guard { repo.createFolder(diskPath) }
|
else -> guard { repo.createFolder(diskPath) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,7 +503,7 @@ class YandexStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
||||||
if (path == "/" || path.isBlank()) {
|
if (path == "/" || path.isBlank()) {
|
||||||
throw WallencException.Storage.DeleteRootForbidden
|
throw WallencException.Storage.DeleteRootForbidden()
|
||||||
}
|
}
|
||||||
val diskPath = toDiskPath(path)
|
val diskPath = toDiskPath(path)
|
||||||
val prior = guard { repo.getOrNull(diskPath) }
|
val prior = guard { repo.getOrNull(diskPath) }
|
||||||
@@ -538,14 +536,14 @@ class YandexStorageAccessor(
|
|||||||
val diskPath = toDiskPath(path)
|
val diskPath = toDiskPath(path)
|
||||||
val prior = guard { repo.getOrNull(diskPath) }
|
val prior = guard { repo.getOrNull(diskPath) }
|
||||||
if (prior?.type == "dir") {
|
if (prior?.type == "dir") {
|
||||||
throw WallencException.Storage.CannotWriteOverDirectory
|
throw WallencException.Storage.CannotWriteOverDirectory()
|
||||||
}
|
}
|
||||||
val hadFile = prior?.type == "file"
|
val hadFile = prior?.type == "file"
|
||||||
val priorSize = if (prior?.type == "file") prior.size ?: 0L else 0L
|
val priorSize = if (prior?.type == "file") prior.size ?: 0L else 0L
|
||||||
guard { repo.uploadFile(diskPath, tmp, overwrite = true) }
|
guard { repo.uploadFile(diskPath, tmp, overwrite = true) }
|
||||||
val after = guard { getMetadataAfterWrite(diskPath) }
|
val after = guard { getMetadataAfterWrite(diskPath) }
|
||||||
if (after.type != "file") {
|
if (after.type != "file") {
|
||||||
throw WallencException.Storage.UnexpectedState
|
throw WallencException.Storage.UnexpectedState()
|
||||||
}
|
}
|
||||||
val newSize = after.size ?: 0L
|
val newSize = after.size ?: 0L
|
||||||
_size.value = ((_size.value ?: 0L) + newSize - priorSize).coerceAtLeast(0L)
|
_size.value = ((_size.value ?: 0L) + newSize - priorSize).coerceAtLeast(0L)
|
||||||
@@ -605,12 +603,11 @@ class YandexStorageAccessor(
|
|||||||
return@withContext emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
val javaType = statsMapper.typeFactory.constructCollectionType(
|
val journalType = statsMapper.typeFactory.constructCollectionType(
|
||||||
List::class.java,
|
List::class.java,
|
||||||
StorageSyncJournalEntry::class.java,
|
StorageSyncJournalEntry::class.java,
|
||||||
)
|
)
|
||||||
@Suppress("UNCHECKED_CAST")
|
statsMapper.readValue<List<StorageSyncJournalEntry>>(bytes, journalType)
|
||||||
(statsMapper.readValue(bytes, javaType) as List<StorageSyncJournalEntry>)
|
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,16 +89,8 @@ class CloseHandledStreamExtension {
|
|||||||
return CloseHandledOutputStream(this, {}, callback)
|
return CloseHandledOutputStream(this, {}, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun InputStream.onClosed(callback: ()->Unit): InputStream {
|
|
||||||
return CloseHandledInputStream(this, {}, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun OutputStream.onClosing(callback: ()->Unit): OutputStream {
|
fun OutputStream.onClosing(callback: ()->Unit): OutputStream {
|
||||||
return CloseHandledOutputStream(this, callback) {}
|
return CloseHandledOutputStream(this, callback) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun InputStream.onClosing(callback: ()->Unit): InputStream {
|
|
||||||
return CloseHandledInputStream(this, callback) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ class LocalVault(
|
|||||||
private suspend fun readStorages() {
|
private suspend fun readStorages() {
|
||||||
val path = path.value
|
val path = path.value
|
||||||
if (path == null || !_isAvailable.value) {
|
if (path == null || !_isAvailable.value) {
|
||||||
throw WallencException.Storage.NotAvailable
|
throw WallencException.Storage.NotAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
val dirs = path.listFiles()?.filter { it.isDirectory }
|
val dirs = path.listFiles()?.filter { it.isDirectory }
|
||||||
@@ -81,7 +81,7 @@ class LocalVault(
|
|||||||
override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) {
|
override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) {
|
||||||
val path = path.value
|
val path = path.value
|
||||||
if (path == null || !_isAvailable.value) {
|
if (path == null || !_isAvailable.value) {
|
||||||
throw WallencException.Storage.NotAvailable
|
throw WallencException.Storage.NotAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
val storageUuid = UUID.randomUUID()
|
val storageUuid = UUID.randomUUID()
|
||||||
@@ -106,7 +106,7 @@ class LocalVault(
|
|||||||
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
||||||
val path = path.value
|
val path = path.value
|
||||||
if (path == null || !_isAvailable.value) {
|
if (path == null || !_isAvailable.value) {
|
||||||
throw WallencException.Storage.NotAvailable
|
throw WallencException.Storage.NotAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
val curStorages = _storages.value.toMutableList()
|
val curStorages = _storages.value.toMutableList()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class VaultThrowableMappingTest {
|
|||||||
@Test
|
@Test
|
||||||
fun mapsYandexDiskAuthToAuthFailed() {
|
fun mapsYandexDiskAuthToAuthFailed() {
|
||||||
val mapped = YandexDiskAuthException("unauthorized").toVaultWallencException()
|
val mapped = YandexDiskAuthException("unauthorized").toVaultWallencException()
|
||||||
assertEquals(WallencException.Auth.Failed, mapped)
|
assertTrue(mapped is WallencException.Auth.Failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -30,18 +30,18 @@ class VaultThrowableMappingTest {
|
|||||||
@Test
|
@Test
|
||||||
fun mapsMissingOAuthTokenIoToTokenMissing() {
|
fun mapsMissingOAuthTokenIoToTokenMissing() {
|
||||||
val mapped = IOException("Yandex OAuth token is missing").toVaultWallencException()
|
val mapped = IOException("Yandex OAuth token is missing").toVaultWallencException()
|
||||||
assertEquals(WallencException.Auth.TokenMissing, mapped)
|
assertTrue(mapped is WallencException.Auth.TokenMissing)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsFileNotFoundToStorageFileNotFound() {
|
fun mapsFileNotFoundToStorageFileNotFound() {
|
||||||
val mapped = FileNotFoundException("x").toVaultWallencException()
|
val mapped = FileNotFoundException("x").toVaultWallencException()
|
||||||
assertEquals(WallencException.Storage.FileNotFound, mapped)
|
assertTrue(mapped is WallencException.Storage.FileNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsIllegalStateNotAFile() {
|
fun mapsIllegalStateNotAFile() {
|
||||||
val mapped = IllegalStateException("Not a file").toVaultWallencException()
|
val mapped = IllegalStateException("Not a file").toVaultWallencException()
|
||||||
assertEquals(WallencException.Storage.NotAFile, mapped)
|
assertTrue(mapped is WallencException.Storage.NotAFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.vault.network.yandexdisk
|
package com.github.nullptroma.wallenc.domain.vault.network.yandexdisk
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.repository.YandexDiskRepository
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.jackson.JacksonConverterFactory
|
import retrofit2.converter.jackson.JacksonConverterFactory
|
||||||
@@ -13,16 +10,6 @@ object YandexDiskRepositoryTestFactory {
|
|||||||
|
|
||||||
private val jackson = jacksonObjectMapper().findAndRegisterModules()
|
private val jackson = jacksonObjectMapper().findAndRegisterModules()
|
||||||
|
|
||||||
fun create(
|
|
||||||
baseUrl: String,
|
|
||||||
oauthToken: String,
|
|
||||||
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
|
||||||
rawHttp: OkHttpClient = OkHttpClient(),
|
|
||||||
): YandexDiskRepository {
|
|
||||||
val api = createApi(baseUrl) { oauthToken }
|
|
||||||
return YandexDiskRepository(api, rawHttp, ioDispatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createApi(
|
fun createApi(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
tokenProvider: () -> String?,
|
tokenProvider: () -> String?,
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ package com.github.nullptroma.wallenc.domain.encrypt
|
|||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor.Companion.AES_SETTINGS
|
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor.Companion.AES_SETTINGS
|
||||||
import kotlinx.coroutines.DisposableHandle
|
import kotlinx.coroutines.DisposableHandle
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
|
||||||
import javax.crypto.CipherOutputStream
|
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
@@ -47,22 +43,6 @@ class EncryptorWithStaticIv(private var secretKey: SecretKey, iv: ByteArray) : D
|
|||||||
return decryptedBytes
|
return decryptedBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encryptStream(stream: OutputStream): OutputStream {
|
|
||||||
if(secretKey.isDestroyed)
|
|
||||||
throw Exception("Object was destroyed")
|
|
||||||
val cipher = Cipher.getInstance(AES_SETTINGS)
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) // инициализация шифратора
|
|
||||||
return CipherOutputStream(stream, cipher)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decryptStream(stream: InputStream): InputStream {
|
|
||||||
if(secretKey.isDestroyed)
|
|
||||||
throw Exception("Object was destroyed")
|
|
||||||
val cipher = Cipher.getInstance(AES_SETTINGS)
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
|
||||||
return CipherInputStream(stream, cipher)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
secretKey.destroy()
|
secretKey.destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,45 +10,44 @@ sealed class WallencException(
|
|||||||
) : Exception(message, cause) {
|
) : Exception(message, cause) {
|
||||||
|
|
||||||
sealed class Feature : WallencException() {
|
sealed class Feature : WallencException() {
|
||||||
data object StorageNotFound : Feature()
|
class StorageNotFound : Feature()
|
||||||
data object NeedsDecryptedView : Feature()
|
class NeedsDecryptedView : Feature()
|
||||||
data object SecretNotFound : Feature()
|
class SecretNotFound : Feature()
|
||||||
data object StorageNotWritable : Feature()
|
class StorageNotWritable : Feature()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Storage(cause: Throwable? = null) : WallencException(cause = cause) {
|
sealed class Storage(cause: Throwable? = null) : WallencException(cause = cause) {
|
||||||
data object NotAvailable : Storage()
|
class NotAvailable : Storage()
|
||||||
data object FileNotFound : Storage()
|
class FileNotFound : Storage()
|
||||||
data object IncorrectKey : Storage()
|
class IncorrectKey : Storage()
|
||||||
|
class EncInfoMissing : Storage()
|
||||||
|
class NotEncrypted : Storage()
|
||||||
|
class NotWritable : Storage()
|
||||||
|
class NotAFile : Storage()
|
||||||
|
class NotADirectory : Storage()
|
||||||
|
class PathIsFile : Storage()
|
||||||
|
class CannotWriteOverDirectory : Storage()
|
||||||
|
class DeleteRootForbidden : Storage()
|
||||||
|
class UnexpectedState : Storage()
|
||||||
data class IoFailed(override val cause: Throwable) : Storage(cause)
|
data class IoFailed(override val cause: Throwable) : Storage(cause)
|
||||||
data object EncInfoMissing : Storage()
|
|
||||||
data object NotEncrypted : Storage()
|
|
||||||
data object NotWritable : Storage()
|
|
||||||
data object NotAFile : Storage()
|
|
||||||
data object NotADirectory : Storage()
|
|
||||||
data object PathIsFile : Storage()
|
|
||||||
data object CannotWriteOverDirectory : Storage()
|
|
||||||
data object DeleteRootForbidden : Storage()
|
|
||||||
data object UnexpectedState : Storage()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ошибки аутентификации (OAuth, токен), без привязки к провайдеру. */
|
|
||||||
sealed class Auth : WallencException() {
|
sealed class Auth : WallencException() {
|
||||||
data object Failed : Auth()
|
class Failed : Auth()
|
||||||
data object TokenMissing : Auth()
|
class TokenMissing : Auth()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Сетевые и удалённые операции (HTTP, блокировки, таймауты). */
|
|
||||||
sealed class Network(cause: Throwable? = null) : WallencException(cause = cause) {
|
sealed class Network(cause: Throwable? = null) : WallencException(cause = cause) {
|
||||||
data class HttpFailed(
|
data class HttpFailed(
|
||||||
val operation: String,
|
val operation: String,
|
||||||
val statusCode: Int,
|
val statusCode: Int,
|
||||||
override val cause: Throwable? = null,
|
override val cause: Throwable? = null,
|
||||||
) : Network(cause)
|
) : Network(cause)
|
||||||
|
|
||||||
data class IoFailed(override val cause: Throwable) : Network(cause)
|
data class IoFailed(override val cause: Throwable) : Network(cause)
|
||||||
data object ResourceLocked : Network()
|
class ResourceLocked : Network()
|
||||||
data object OperationFailed : Network()
|
class OperationFailed : Network()
|
||||||
data object OperationTimedOut : Network()
|
class OperationTimedOut : Network()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Unknown(override val cause: Throwable?) : WallencException(cause = cause)
|
data class Unknown(override val cause: Throwable?) : WallencException(cause = cause)
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import java.io.IOException
|
|||||||
|
|
||||||
fun Throwable.toWallencException(): WallencException = when (this) {
|
fun Throwable.toWallencException(): WallencException = when (this) {
|
||||||
is WallencException -> this
|
is WallencException -> this
|
||||||
is FileNotFoundException -> WallencException.Storage.FileNotFound
|
is FileNotFoundException -> WallencException.Storage.FileNotFound()
|
||||||
is IOException -> WallencException.Network.IoFailed(this)
|
is IOException -> WallencException.Network.IoFailed(this)
|
||||||
else -> WallencException.Unknown(this)
|
else -> WallencException.Unknown(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Throwable.rethrowAsWallencException(): Nothing = throw toWallencException()
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.interfaces
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
interface IStorageExplorer {
|
|
||||||
val currentPath: StateFlow<String>
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// пока бесполезный интерфейс
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.errors
|
package com.github.nullptroma.wallenc.domain.errors
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertSame
|
import org.junit.Assert.assertSame
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -11,14 +10,14 @@ class WallencExceptionMappingTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun preservesWallencException() {
|
fun preservesWallencException() {
|
||||||
val original = WallencException.Feature.StorageNotFound
|
val original = WallencException.Feature.StorageNotFound()
|
||||||
assertSame(original, original.toWallencException())
|
assertSame(original, original.toWallencException())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsFileNotFoundException() {
|
fun mapsFileNotFoundException() {
|
||||||
val mapped = FileNotFoundException("missing").toWallencException()
|
val mapped = FileNotFoundException("missing").toWallencException()
|
||||||
assertEquals(WallencException.Storage.FileNotFound, mapped)
|
assertTrue(mapped is WallencException.Storage.FileNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.1.1"
|
agp = "9.2.1"
|
||||||
jacksonModuleKotlin = "2.21.3"
|
jacksonModuleKotlin = "2.21.3"
|
||||||
|
javaxInject = "1"
|
||||||
kotlin = "2.3.21"
|
kotlin = "2.3.21"
|
||||||
coreKtx = "1.18.0"
|
coreKtx = "1.18.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
@@ -32,11 +33,11 @@ datastore = "1.2.1"
|
|||||||
mockk = "1.14.9"
|
mockk = "1.14.9"
|
||||||
robolectric = "4.16.1"
|
robolectric = "4.16.1"
|
||||||
androidxArchCore = "2.2.0"
|
androidxArchCore = "2.2.0"
|
||||||
turbine = "1.2.1"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" }
|
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" }
|
||||||
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
||||||
|
javax-inject = { module = "javax.inject:javax.inject", version.ref = "javaxInject" }
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" }
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesCore" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesCore" }
|
||||||
@@ -45,7 +46,6 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "
|
|||||||
room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
||||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||||
androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidxArchCore" }
|
androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidxArchCore" }
|
||||||
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
|
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
||||||
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" }
|
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" }
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.infrastructure.android.db.app.repository
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.github.nullptroma.wallenc.infrastructure.android.db.app.dao.StorageMetaInfoDao
|
|
||||||
import com.github.nullptroma.wallenc.infrastructure.android.db.app.model.DbStorageMetaInfo
|
|
||||||
import com.github.nullptroma.wallenc.domain.vault.utils.IProvider
|
|
||||||
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class StorageMetaInfoRepository(
|
|
||||||
private val dao: StorageMetaInfoDao,
|
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
|
||||||
) {
|
|
||||||
fun getAllFlow() = dao.getAllFlow()
|
|
||||||
suspend fun getAll() = withContext(ioDispatcher) { dao.getAll() }
|
|
||||||
suspend fun getMeta(uuid: UUID): CommonStorageMetaInfo? = withContext(ioDispatcher) {
|
|
||||||
val json = dao.getMetaInfo(uuid)?.metaInfoJson ?: return@withContext null
|
|
||||||
return@withContext jackson.readValue(
|
|
||||||
json,
|
|
||||||
CommonStorageMetaInfo::class.java
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun observeMeta(uuid: UUID): Flow<CommonStorageMetaInfo> {
|
|
||||||
return dao.getMetaInfoFlow(uuid)
|
|
||||||
.map { jackson.readValue(it.metaInfoJson, CommonStorageMetaInfo::class.java) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setMeta(uuid: UUID, metaInfo: CommonStorageMetaInfo) = withContext(ioDispatcher) {
|
|
||||||
val json = jackson.writeValueAsString(metaInfo)
|
|
||||||
dao.add(DbStorageMetaInfo(uuid, json))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(uuid: UUID) = withContext(ioDispatcher) {
|
|
||||||
dao.delete(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createSingleStorageProvider(uuid: UUID): SingleStorageMetaInfoProvider {
|
|
||||||
return SingleStorageMetaInfoProvider(this, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SingleStorageMetaInfoProvider (
|
|
||||||
private val repo: StorageMetaInfoRepository,
|
|
||||||
val uuid: UUID
|
|
||||||
) : IProvider<CommonStorageMetaInfo> {
|
|
||||||
override suspend fun get(): CommonStorageMetaInfo? = repo.getMeta(uuid)
|
|
||||||
override suspend fun clear() = repo.delete(uuid)
|
|
||||||
override suspend fun set(value: CommonStorageMetaInfo) = repo.setMeta(uuid, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package com.github.nullptroma.wallenc.task.runtime
|
|||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -10,7 +9,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher
|
|||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -59,13 +57,13 @@ class TaskOrchestratorTest {
|
|||||||
title = "Fail",
|
title = "Fail",
|
||||||
dispatcher = dispatcher,
|
dispatcher = dispatcher,
|
||||||
work = { ctx ->
|
work = { ctx ->
|
||||||
ctx.fail(WallencException.Storage.FileNotFound)
|
ctx.fail(WallencException.Storage.FileNotFound())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
val task = orchestrator.pipelineState.value.tasks.first { it.id == id }
|
val task = orchestrator.pipelineState.value.tasks.first { it.id == id }
|
||||||
val failed = task.state as TaskRunState.Failed
|
val failed = task.state as TaskRunState.Failed
|
||||||
assertEquals(WallencException.Storage.FileNotFound, failed.error)
|
assertTrue(failed.error is WallencException.Storage.FileNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
import com.github.nullptroma.wallenc.ui.screens.ScreenRoute
|
|
||||||
import com.github.nullptroma.wallenc.ui.screens.main.MainRoute
|
import com.github.nullptroma.wallenc.ui.screens.main.MainRoute
|
||||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.tasks.TaskPipelineRoute
|
import com.github.nullptroma.wallenc.ui.screens.main.screens.tasks.TaskPipelineRoute
|
||||||
import com.github.nullptroma.wallenc.ui.screens.settings.SettingsRoute
|
import com.github.nullptroma.wallenc.ui.screens.settings.SettingsRoute
|
||||||
import com.github.nullptroma.wallenc.ui.screens.sync.StorageSyncRoute
|
import com.github.nullptroma.wallenc.ui.screens.sync.StorageSyncRoute
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class WallencViewModel @javax.inject.Inject constructor(savedStateHandle: SavedStateHandle) :
|
class WallencViewModel @javax.inject.Inject constructor(savedStateHandle: SavedStateHandle) :
|
||||||
@@ -27,10 +25,4 @@ class WallencViewModel @javax.inject.Inject constructor(savedStateHandle: SavedS
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun updateRoute(qualifiedName: String, route: ScreenRoute) {
|
|
||||||
routes = routes.toMutableMap().apply {
|
|
||||||
this[qualifiedName] = route
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.elements
|
package com.github.nullptroma.wallenc.ui.elements
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ExperimentalGetImage
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
@@ -28,11 +28,9 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
@@ -41,8 +39,8 @@ import com.google.mlkit.vision.common.InputImage
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@ExperimentalGetImage
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalGetImage::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun QrScannerDialog(
|
fun QrScannerDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.elements.indication
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.foundation.IndicationNodeFactory
|
|
||||||
import androidx.compose.foundation.interaction.InteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.PressInteraction
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
|
||||||
import androidx.compose.ui.graphics.drawscope.scale
|
|
||||||
import androidx.compose.ui.node.DelegatableNode
|
|
||||||
import androidx.compose.ui.node.DrawModifierNode
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
private class ScaleNode(private val interactionSource: InteractionSource) :
|
|
||||||
Modifier.Node(), DrawModifierNode {
|
|
||||||
|
|
||||||
var currentPressPosition: Offset = Offset.Zero
|
|
||||||
val animatedScalePercent = Animatable(1f)
|
|
||||||
|
|
||||||
private suspend fun animateToPressed(pressPosition: Offset) {
|
|
||||||
currentPressPosition = pressPosition
|
|
||||||
animatedScalePercent.animateTo(0.9f, spring())
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun animateToResting() {
|
|
||||||
animatedScalePercent.animateTo(1f, spring())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach() {
|
|
||||||
coroutineScope.launch {
|
|
||||||
interactionSource.interactions.collectLatest { interaction ->
|
|
||||||
when (interaction) {
|
|
||||||
is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
|
|
||||||
is PressInteraction.Release -> animateToResting()
|
|
||||||
is PressInteraction.Cancel -> animateToResting()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun ContentDrawScope.draw() {
|
|
||||||
scale(
|
|
||||||
scale = animatedScalePercent.value,
|
|
||||||
pivot = currentPressPosition
|
|
||||||
) {
|
|
||||||
this@draw.drawContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ScaleIndication : IndicationNodeFactory {
|
|
||||||
override fun create(interactionSource: InteractionSource): DelegatableNode {
|
|
||||||
return ScaleNode(interactionSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean = other === ScaleIndication
|
|
||||||
override fun hashCode() = 100
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.extensions
|
|
||||||
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
|
||||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.layout
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
|
|
||||||
fun Modifier.ignoreHorizontalParentPadding(horizontal: Dp): Modifier {
|
|
||||||
return this.layout { measurable, constraints ->
|
|
||||||
val overrideWidth = constraints.maxWidth + 2 * horizontal.roundToPx()
|
|
||||||
val placeable = measurable.measure(constraints.copy(maxWidth = overrideWidth))
|
|
||||||
layout(placeable.width, placeable.height) {
|
|
||||||
placeable.place(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Modifier.ignoreVerticalParentPadding(vertical: Dp): Modifier {
|
|
||||||
return this.layout { measurable, constraints ->
|
|
||||||
val overrideHeight = constraints.maxHeight + 2 * vertical.roundToPx()
|
|
||||||
val placeable = measurable.measure(constraints.copy(maxHeight = overrideHeight))
|
|
||||||
layout(placeable.width, placeable.height) {
|
|
||||||
placeable.place(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Modifier.gesturesDisabled(disabled: Boolean = true) =
|
|
||||||
if (disabled) {
|
|
||||||
pointerInput(Unit) {
|
|
||||||
awaitPointerEventScope {
|
|
||||||
// we should wait for all new pointer events
|
|
||||||
while (true) {
|
|
||||||
awaitPointerEvent(pass = PointerEventPass.Initial)
|
|
||||||
.changes
|
|
||||||
.forEach(PointerInputChange::consume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
@@ -6,50 +6,50 @@ import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
|
|||||||
import com.github.nullptroma.wallenc.vault.contract.VaultLinkFailure
|
import com.github.nullptroma.wallenc.vault.contract.VaultLinkFailure
|
||||||
|
|
||||||
fun WallencException.toUserNotification(): UserNotification.TextRes = when (this) {
|
fun WallencException.toUserNotification(): UserNotification.TextRes = when (this) {
|
||||||
WallencException.Feature.StorageNotFound ->
|
is WallencException.Feature.StorageNotFound ->
|
||||||
UserNotification.TextRes(R.string.error_storage_not_found)
|
UserNotification.TextRes(R.string.error_storage_not_found)
|
||||||
WallencException.Feature.NeedsDecryptedView ->
|
is WallencException.Feature.NeedsDecryptedView ->
|
||||||
UserNotification.TextRes(R.string.error_storage_locked_view)
|
UserNotification.TextRes(R.string.error_storage_locked_view)
|
||||||
WallencException.Feature.SecretNotFound ->
|
is WallencException.Feature.SecretNotFound ->
|
||||||
UserNotification.TextRes(R.string.error_secret_not_found)
|
UserNotification.TextRes(R.string.error_secret_not_found)
|
||||||
WallencException.Feature.StorageNotWritable ->
|
is WallencException.Feature.StorageNotWritable ->
|
||||||
UserNotification.TextRes(R.string.error_storage_not_writable)
|
UserNotification.TextRes(R.string.error_storage_not_writable)
|
||||||
|
|
||||||
WallencException.Storage.NotAvailable,
|
is WallencException.Storage.NotAvailable,
|
||||||
WallencException.Storage.NotWritable,
|
is WallencException.Storage.NotWritable,
|
||||||
->
|
->
|
||||||
UserNotification.TextRes(R.string.error_storage_not_writable)
|
UserNotification.TextRes(R.string.error_storage_not_writable)
|
||||||
WallencException.Storage.FileNotFound ->
|
is WallencException.Storage.FileNotFound ->
|
||||||
UserNotification.TextRes(R.string.error_file_not_found)
|
UserNotification.TextRes(R.string.error_file_not_found)
|
||||||
WallencException.Storage.IncorrectKey ->
|
is WallencException.Storage.IncorrectKey ->
|
||||||
UserNotification.TextRes(R.string.error_incorrect_password)
|
UserNotification.TextRes(R.string.error_incorrect_password)
|
||||||
WallencException.Storage.NotEncrypted ->
|
is WallencException.Storage.NotEncrypted ->
|
||||||
UserNotification.TextRes(R.string.error_storage_not_encrypted)
|
UserNotification.TextRes(R.string.error_storage_not_encrypted)
|
||||||
WallencException.Storage.EncInfoMissing ->
|
is WallencException.Storage.EncInfoMissing ->
|
||||||
UserNotification.TextRes(R.string.error_enc_info_missing)
|
UserNotification.TextRes(R.string.error_enc_info_missing)
|
||||||
WallencException.Storage.DeleteRootForbidden ->
|
is WallencException.Storage.DeleteRootForbidden ->
|
||||||
UserNotification.TextRes(R.string.error_delete_root_forbidden)
|
UserNotification.TextRes(R.string.error_delete_root_forbidden)
|
||||||
WallencException.Storage.NotAFile ->
|
is WallencException.Storage.NotAFile ->
|
||||||
UserNotification.TextRes(R.string.error_not_a_file)
|
UserNotification.TextRes(R.string.error_not_a_file)
|
||||||
WallencException.Storage.NotADirectory ->
|
is WallencException.Storage.NotADirectory ->
|
||||||
UserNotification.TextRes(R.string.error_not_a_directory)
|
UserNotification.TextRes(R.string.error_not_a_directory)
|
||||||
WallencException.Storage.PathIsFile ->
|
is WallencException.Storage.PathIsFile ->
|
||||||
UserNotification.TextRes(R.string.error_path_is_file)
|
UserNotification.TextRes(R.string.error_path_is_file)
|
||||||
WallencException.Storage.CannotWriteOverDirectory ->
|
is WallencException.Storage.CannotWriteOverDirectory ->
|
||||||
UserNotification.TextRes(R.string.error_cannot_write_over_directory)
|
UserNotification.TextRes(R.string.error_cannot_write_over_directory)
|
||||||
WallencException.Storage.UnexpectedState ->
|
is WallencException.Storage.UnexpectedState ->
|
||||||
UserNotification.TextRes(R.string.error_unexpected_state)
|
UserNotification.TextRes(R.string.error_unexpected_state)
|
||||||
is WallencException.Storage.IoFailed ->
|
is WallencException.Storage.IoFailed ->
|
||||||
UserNotification.TextRes(R.string.error_network)
|
UserNotification.TextRes(R.string.error_network)
|
||||||
|
|
||||||
WallencException.Auth.Failed,
|
is WallencException.Auth.Failed,
|
||||||
WallencException.Auth.TokenMissing,
|
is WallencException.Auth.TokenMissing,
|
||||||
->
|
->
|
||||||
UserNotification.TextRes(R.string.vault_link_error_auth)
|
UserNotification.TextRes(R.string.vault_link_error_auth)
|
||||||
WallencException.Network.ResourceLocked ->
|
is WallencException.Network.ResourceLocked ->
|
||||||
UserNotification.TextRes(R.string.error_disk_resource_locked)
|
UserNotification.TextRes(R.string.error_disk_resource_locked)
|
||||||
WallencException.Network.OperationFailed,
|
is WallencException.Network.OperationFailed,
|
||||||
WallencException.Network.OperationTimedOut,
|
is WallencException.Network.OperationTimedOut,
|
||||||
is WallencException.Network.HttpFailed,
|
is WallencException.Network.HttpFailed,
|
||||||
is WallencException.Network.IoFailed,
|
is WallencException.Network.IoFailed,
|
||||||
->
|
->
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.resources
|
package com.github.nullptroma.wallenc.ui.resources
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLine
|
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLine
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TaskLogLine.displayText(): String {
|
fun TaskLogLine.displayText(): String {
|
||||||
val key = logKey
|
val key = logKey
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
val context = LocalContext.current
|
return key.resolve(composableUiStringResolver())
|
||||||
val resolver = UiStringResolver { id, args ->
|
|
||||||
if (args.isEmpty()) {
|
|
||||||
context.getString(id)
|
|
||||||
} else {
|
|
||||||
context.getString(id, *args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return key.resolve(resolver)
|
|
||||||
}
|
}
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ fun TaskProgressLabel.resolve(resolver: UiStringResolver): String = when (this)
|
|||||||
TaskProgressLabel.SyncNoGroups -> resolver(R.string.sync_progress_no_groups)
|
TaskProgressLabel.SyncNoGroups -> resolver(R.string.sync_progress_no_groups)
|
||||||
TaskProgressLabel.SyncStarted -> resolver(R.string.sync_progress_started)
|
TaskProgressLabel.SyncStarted -> resolver(R.string.sync_progress_started)
|
||||||
TaskProgressLabel.SyncCompleted -> resolver(R.string.sync_progress_completed)
|
TaskProgressLabel.SyncCompleted -> resolver(R.string.sync_progress_completed)
|
||||||
is TaskProgressLabel.SyncPreparing -> resolver(R.string.sync_progress_preparing, groupCount)
|
is TaskProgressLabel.SyncPreparing ->
|
||||||
|
resolver.plurals(R.plurals.sync_progress_preparing, groupCount, groupCount)
|
||||||
|
|
||||||
is TaskProgressLabel.SyncGroupPreparing -> resolver(R.string.sync_progress_group_preparing, groupId)
|
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.SyncGroupNotFound -> resolver(R.string.sync_progress_group_not_found, groupId)
|
||||||
@@ -31,7 +32,7 @@ fun TaskProgressLabel.resolve(resolver: UiStringResolver): String = when (this)
|
|||||||
is TaskProgressLabel.SyncGroupNoJournalEntries ->
|
is TaskProgressLabel.SyncGroupNoJournalEntries ->
|
||||||
resolver(R.string.sync_progress_group_no_entries, groupId)
|
resolver(R.string.sync_progress_group_no_entries, groupId)
|
||||||
is TaskProgressLabel.SyncGroupProcessingEntries ->
|
is TaskProgressLabel.SyncGroupProcessingEntries ->
|
||||||
resolver(R.string.sync_progress_group_processing, groupId, count)
|
resolver.plurals(R.plurals.sync_progress_group_processing, count, groupId, count)
|
||||||
is TaskProgressLabel.SyncGroupEntryProgress ->
|
is TaskProgressLabel.SyncGroupEntryProgress ->
|
||||||
resolver(R.string.sync_progress_group_entry, groupId, current, total)
|
resolver(R.string.sync_progress_group_entry, groupId, current, total)
|
||||||
is TaskProgressLabel.SyncGroupCompleted ->
|
is TaskProgressLabel.SyncGroupCompleted ->
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.resources
|
package com.github.nullptroma.wallenc.ui.resources
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TaskProgressLabel.resolveText(): String {
|
fun TaskProgressLabel.resolveText(): String = resolve(composableUiStringResolver())
|
||||||
val context = LocalContext.current
|
|
||||||
return resolve(
|
|
||||||
UiStringResolver { id, args ->
|
|
||||||
if (args.isEmpty()) {
|
|
||||||
context.getString(id)
|
|
||||||
} else {
|
|
||||||
context.getString(id, *args)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.resources
|
package com.github.nullptroma.wallenc.ui.resources
|
||||||
|
|
||||||
|
import androidx.annotation.PluralsRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
/** Разрешение Android-строк для код-домена (ViewModel, без Compose). */
|
/** Разрешение Android-строк для код-домена (ViewModel, без Compose). */
|
||||||
fun interface UiStringResolver {
|
interface UiStringResolver {
|
||||||
operator fun invoke(@StringRes id: Int, vararg formatArgs: Any): String
|
operator fun invoke(@StringRes id: Int, vararg formatArgs: Any): String
|
||||||
|
|
||||||
|
fun plurals(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.github.nullptroma.wallenc.ui.resources
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun composableUiStringResolver(): UiStringResolver {
|
||||||
|
val resources = LocalResources.current
|
||||||
|
return remember(resources) {
|
||||||
|
object : UiStringResolver {
|
||||||
|
override fun invoke(id: Int, vararg formatArgs: Any): String =
|
||||||
|
if (formatArgs.isEmpty()) {
|
||||||
|
resources.getString(id)
|
||||||
|
} else {
|
||||||
|
resources.getString(id, *formatArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun plurals(id: Int, quantity: Int, vararg formatArgs: Any): String =
|
||||||
|
if (formatArgs.isEmpty()) {
|
||||||
|
resources.getQuantityString(id, quantity)
|
||||||
|
} else {
|
||||||
|
resources.getQuantityString(id, quantity, *formatArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun resolveString(@StringRes id: Int, vararg formatArgs: Any): String =
|
||||||
|
if (formatArgs.isEmpty()) {
|
||||||
|
stringResource(id)
|
||||||
|
} else {
|
||||||
|
stringResource(id, *formatArgs)
|
||||||
|
}
|
||||||
@@ -71,12 +71,6 @@ class MainViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateRoute(qualifiedName: String, route: ScreenRoute) {
|
|
||||||
routes = routes.toMutableMap().apply {
|
|
||||||
this[qualifiedName] = route
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapWorkStatus(
|
private fun mapWorkStatus(
|
||||||
fg: TaskForegroundUiState,
|
fg: TaskForegroundUiState,
|
||||||
pipe: PipelineState,
|
pipe: PipelineState,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -58,6 +59,7 @@ fun RemoteVaultsScreen(
|
|||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -215,9 +217,9 @@ fun RemoteVaultsScreen(
|
|||||||
is VaultLinkOutcome.Failed -> {
|
is VaultLinkOutcome.Failed -> {
|
||||||
val notification = outcome.reason.toUserNotification()
|
val notification = outcome.reason.toUserNotification()
|
||||||
val text = if (notification.formatArgs.isEmpty()) {
|
val text = if (notification.formatArgs.isEmpty()) {
|
||||||
context.getString(notification.id)
|
resources.getString(notification.id)
|
||||||
} else {
|
} else {
|
||||||
context.getString(
|
resources.getString(
|
||||||
notification.id,
|
notification.id,
|
||||||
*notification.formatArgs.toTypedArray(),
|
*notification.formatArgs.toTypedArray(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Notes
|
import androidx.compose.material.icons.automirrored.outlined.Notes
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
import androidx.compose.material.icons.outlined.Lock
|
||||||
import androidx.compose.material.icons.outlined.Notes
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class StorageHomeViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
storageUuid = storageUuid.toString(),
|
storageUuid = storageUuid.toString(),
|
||||||
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
errorNotification = WallencException.Feature.StorageNotFound().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -59,7 +59,7 @@ class StorageHomeViewModel @Inject constructor(
|
|||||||
textSecretsCount = secrets.size,
|
textSecretsCount = secrets.size,
|
||||||
canManageDomainData = canManageDomainData,
|
canManageDomainData = canManageDomainData,
|
||||||
errorNotification = if (isRawEncrypted) {
|
errorNotification = if (isRawEncrypted) {
|
||||||
WallencException.Feature.NeedsDecryptedView.toUserNotification()
|
WallencException.Feature.NeedsDecryptedView().toUserNotification()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
errorNotification = WallencException.Feature.StorageNotFound().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -58,7 +58,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -80,7 +80,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
isMutating = isMutating,
|
isMutating = isMutating,
|
||||||
secret = secret,
|
secret = secret,
|
||||||
errorNotification = if (secret == null) {
|
errorNotification = if (secret == null) {
|
||||||
WallencException.Feature.SecretNotFound.toUserNotification()
|
WallencException.Feature.SecretNotFound().toUserNotification()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
@@ -97,7 +97,7 @@ class TextSecretDetailsViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class TextSecretEditViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
errorNotification = WallencException.Feature.StorageNotFound().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -62,7 +62,7 @@ class TextSecretEditViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -100,7 +100,7 @@ class TextSecretEditViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class TextSecretsViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
errorNotification = WallencException.Feature.StorageNotFound().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -43,7 +43,7 @@ class TextSecretsViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
@@ -28,11 +28,9 @@ import androidx.compose.material.icons.filled.ContentCopy
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -80,7 +78,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
|
||||||
import com.github.nullptroma.wallenc.ui.elements.QrScannerDialog
|
import com.github.nullptroma.wallenc.ui.elements.QrScannerDialog
|
||||||
import com.github.nullptroma.wallenc.usecases.TwoFaCodeState
|
import com.github.nullptroma.wallenc.usecases.TwoFaCodeState
|
||||||
import com.github.nullptroma.wallenc.usecases.buildTwoFaCodeState
|
import com.github.nullptroma.wallenc.usecases.buildTwoFaCodeState
|
||||||
@@ -256,7 +253,6 @@ fun TwoFaTokensScreen(
|
|||||||
isBusy = uiState.isMutating,
|
isBusy = uiState.isMutating,
|
||||||
onDismiss = { creating = false },
|
onDismiss = { creating = false },
|
||||||
onSave = { issuer, account, secret, notes, digits, periodSeconds, algorithm ->
|
onSave = { issuer, account, secret, notes, digits, periodSeconds, algorithm ->
|
||||||
creating = false
|
|
||||||
viewModel.saveToken(
|
viewModel.saveToken(
|
||||||
existingId = null,
|
existingId = null,
|
||||||
issuer = issuer,
|
issuer = issuer,
|
||||||
@@ -267,6 +263,7 @@ fun TwoFaTokensScreen(
|
|||||||
periodSeconds = periodSeconds,
|
periodSeconds = periodSeconds,
|
||||||
algorithm = algorithm,
|
algorithm = algorithm,
|
||||||
)
|
)
|
||||||
|
creating = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -277,7 +274,6 @@ fun TwoFaTokensScreen(
|
|||||||
isBusy = uiState.isMutating,
|
isBusy = uiState.isMutating,
|
||||||
onDismiss = { editingToken = null },
|
onDismiss = { editingToken = null },
|
||||||
onSave = { issuer, account, secret, notes, digits, periodSeconds, algorithm ->
|
onSave = { issuer, account, secret, notes, digits, periodSeconds, algorithm ->
|
||||||
editingToken = null
|
|
||||||
viewModel.saveToken(
|
viewModel.saveToken(
|
||||||
existingId = token.id,
|
existingId = token.id,
|
||||||
issuer = issuer,
|
issuer = issuer,
|
||||||
@@ -288,6 +284,7 @@ fun TwoFaTokensScreen(
|
|||||||
periodSeconds = periodSeconds,
|
periodSeconds = periodSeconds,
|
||||||
algorithm = algorithm,
|
algorithm = algorithm,
|
||||||
)
|
)
|
||||||
|
editingToken = null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
|
errorNotification = WallencException.Feature.StorageNotFound().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -54,7 +54,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
state.value.copy(
|
state.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -96,7 +96,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -144,7 +144,7 @@ class TwoFaTokensViewModel @Inject constructor(
|
|||||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
|
errorNotification = WallencException.Feature.NeedsDecryptedView().toUserNotification(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.Tree
|
import com.github.nullptroma.wallenc.domain.datatypes.Tree
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
|
import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
|
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||||
@@ -14,19 +13,17 @@ import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
|||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
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.domain.tasks.VaultTaskStep
|
||||||
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||||
|
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||||
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
|
||||||
import com.github.nullptroma.wallenc.usecases.RemoveStorageUseCase
|
import com.github.nullptroma.wallenc.usecases.RemoveStorageUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.RenameStorageUseCase
|
import com.github.nullptroma.wallenc.usecases.RenameStorageUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.StorageFileManagementUseCase
|
import com.github.nullptroma.wallenc.usecases.StorageFileManagementUseCase
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
|
||||||
import com.github.nullptroma.wallenc.ui.extensions.toPrintable
|
|
||||||
import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
|
||||||
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@@ -36,7 +33,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Общая логика дерева storages для локального и удалённого vault (presentation).
|
* Общая логика дерева storages для локального и удалённого vault (presentation).
|
||||||
@@ -153,42 +149,6 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun printStorageInfoToLog(storage: IStorageInfo) {
|
|
||||||
val id = storage.uuid
|
|
||||||
if (isStorageTaskActive(id)) {
|
|
||||||
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
taskOrchestrator.enqueue(
|
|
||||||
title = uiStrings(R.string.task_title_dump_storage_log),
|
|
||||||
dispatcher = Dispatchers.IO,
|
|
||||||
busyStorageUuid = id,
|
|
||||||
work = { ctx ->
|
|
||||||
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.DumpStorageLog))
|
|
||||||
storageFileManagementUseCase.setStorage(storage)
|
|
||||||
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_enumerating))
|
|
||||||
val files: List<IFile>
|
|
||||||
val dirs: List<IDirectory>
|
|
||||||
val time = measureTimeMillis {
|
|
||||||
files = storageFileManagementUseCase.getAllFiles()
|
|
||||||
dirs = storageFileManagementUseCase.getAllDirs()
|
|
||||||
}
|
|
||||||
for (file in files) {
|
|
||||||
logger.debug("Files", file.metaInfo.toString())
|
|
||||||
}
|
|
||||||
for (dir in dirs) {
|
|
||||||
logger.debug("Dirs", dir.metaInfo.toString())
|
|
||||||
}
|
|
||||||
logger.debug("Time", "Time: $time ms")
|
|
||||||
logger.debug("Storage", storage.toPrintable())
|
|
||||||
ctx.log(
|
|
||||||
TaskLogLevel.Info,
|
|
||||||
uiStrings(R.string.task_log_enumerate_done, files.size, dirs.size, time),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createStorage() {
|
fun createStorage() {
|
||||||
if (!state.value.addStorageFabEnabled) {
|
if (!state.value.addStorageFabEnabled) {
|
||||||
logger.debug(TAG, "createStorage ignored (vault unavailable or FAB disabled)")
|
logger.debug(TAG, "createStorage ignored (vault unavailable or FAB disabled)")
|
||||||
|
|||||||
@@ -69,11 +69,10 @@ fun VaultBrowserScreen(
|
|||||||
null -> null
|
null -> null
|
||||||
}
|
}
|
||||||
LaunchedEffect(notificationText) {
|
LaunchedEffect(notificationText) {
|
||||||
if (notificationText != null) {
|
val text = notificationText ?: return@LaunchedEffect
|
||||||
Toast.makeText(context, notificationText, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
|
||||||
pendingNotification = null
|
pendingNotification = null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val fabEnabled = uiState.addStorageFabEnabled
|
val fabEnabled = uiState.addStorageFabEnabled
|
||||||
val fabBusy = uiState.vaultListMutationActive
|
val fabBusy = uiState.vaultListMutationActive
|
||||||
|
|||||||
@@ -366,11 +366,9 @@ fun StorageSyncScreen(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val groupId = pendingRemoveGroupId
|
val groupId = pendingRemoveGroupId ?: return@Button
|
||||||
pendingRemoveGroupId = null
|
|
||||||
if (groupId != null) {
|
|
||||||
viewModel.removeGroup(groupId)
|
viewModel.removeGroup(groupId)
|
||||||
}
|
pendingRemoveGroupId = null
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.sync_confirm_delete))
|
Text(text = stringResource(id = R.string.sync_confirm_delete))
|
||||||
@@ -399,11 +397,9 @@ fun StorageSyncScreen(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val payload = pendingRemoveStorage
|
val payload = pendingRemoveStorage ?: return@Button
|
||||||
pendingRemoveStorage = null
|
|
||||||
if (payload != null) {
|
|
||||||
viewModel.removeStorageFromGroup(payload.first, payload.second)
|
viewModel.removeStorageFromGroup(payload.first, payload.second)
|
||||||
}
|
pendingRemoveStorage = null
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.sync_confirm_delete))
|
Text(text = stringResource(id = R.string.sync_confirm_delete))
|
||||||
|
|||||||
15
ui/src/main/res/values-ru/plurals.xml
Normal file
15
ui/src/main/res/values-ru/plurals.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<plurals name="sync_progress_preparing">
|
||||||
|
<item quantity="one">Синхронизация: подготовка %d группы</item>
|
||||||
|
<item quantity="few">Синхронизация: подготовка %d групп</item>
|
||||||
|
<item quantity="many">Синхронизация: подготовка %d групп</item>
|
||||||
|
<item quantity="other">Синхронизация: подготовка %d групп</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="sync_progress_group_processing">
|
||||||
|
<item quantity="one">Синхронизация: группа «%1$s» — обработка %2$d записи</item>
|
||||||
|
<item quantity="few">Синхронизация: группа «%1$s» — обработка %2$d записей</item>
|
||||||
|
<item quantity="many">Синхронизация: группа «%1$s» — обработка %2$d записей</item>
|
||||||
|
<item quantity="other">Синхронизация: группа «%1$s» — обработка %2$d записей</item>
|
||||||
|
</plurals>
|
||||||
|
</resources>
|
||||||
@@ -15,13 +15,10 @@
|
|||||||
<string name="sync_progress_section_title">Синхронизация хранилищ</string>
|
<string name="sync_progress_section_title">Синхронизация хранилищ</string>
|
||||||
<string name="sync_groups_busy_section_title">Сохранение групп синхронизации</string>
|
<string name="sync_groups_busy_section_title">Сохранение групп синхронизации</string>
|
||||||
<string name="sync_run_now">Запустить синхронизацию</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_add_storage">Добавить хранилище в группу</string>
|
||||||
<string name="sync_remove_group">Удалить группу</string>
|
<string name="sync_remove_group">Удалить группу</string>
|
||||||
<string name="sync_group_empty">В группе нет хранилищ</string>
|
<string name="sync_group_empty">В группе нет хранилищ</string>
|
||||||
<string name="sync_remove_storage">Убрать хранилище из группы</string>
|
<string name="sync_remove_storage">Убрать хранилище из группы</string>
|
||||||
<string name="sync_picker_back">Назад</string>
|
|
||||||
<string name="sync_cd_picker_back">Закрыть выбор хранилища</string>
|
<string name="sync_cd_picker_back">Закрыть выбор хранилища</string>
|
||||||
<string name="sync_picker_title">Выбор хранилища для %1$s</string>
|
<string name="sync_picker_title">Выбор хранилища для %1$s</string>
|
||||||
<string name="sync_picker_add">Добавить</string>
|
<string name="sync_picker_add">Добавить</string>
|
||||||
@@ -31,7 +28,6 @@
|
|||||||
<string name="sync_picker_expand">Развернуть</string>
|
<string name="sync_picker_expand">Развернуть</string>
|
||||||
<string name="sync_picker_collapse">Свернуть</string>
|
<string name="sync_picker_collapse">Свернуть</string>
|
||||||
<string name="sync_fab_create_group_cd">Создать группу синхронизации</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_incompatible_warning">Несовместимые хранилища в группе: %1$d</string>
|
||||||
<string name="sync_group_policy_line">Политика шифрования группы: %1$s</string>
|
<string name="sync_group_policy_line">Политика шифрования группы: %1$s</string>
|
||||||
<string name="sync_group_policy_unset">Не определена (группа пуста)</string>
|
<string name="sync_group_policy_unset">Не определена (группа пуста)</string>
|
||||||
@@ -51,8 +47,6 @@
|
|||||||
<string name="sync_msg_only_plain_storage_allowed">В группы синхронизации можно добавлять только незашифрованные хранилища</string>
|
<string name="sync_msg_only_plain_storage_allowed">В группы синхронизации можно добавлять только незашифрованные хранилища</string>
|
||||||
<string name="sync_msg_storage_encryption_key_required">Для зашифрованного хранилища нужно знать пароль (откройте его перед добавлением)</string>
|
<string name="sync_msg_storage_encryption_key_required">Для зашифрованного хранилища нужно знать пароль (откройте его перед добавлением)</string>
|
||||||
<string name="sync_msg_storage_incompatible_encryption">Хранилище не совместимо с политикой шифрования группы</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_sync_already_running">Синхронизация уже выполняется</string>
|
||||||
<string name="sync_msg_blocked_during_sync">Дождитесь окончания синхронизации</string>
|
<string name="sync_msg_blocked_during_sync">Дождитесь окончания синхронизации</string>
|
||||||
<string name="sync_encryption_unknown">Неизвестно</string>
|
<string name="sync_encryption_unknown">Неизвестно</string>
|
||||||
@@ -208,7 +202,6 @@
|
|||||||
<string name="enc_status_not_encrypted">Не зашифровано</string>
|
<string name="enc_status_not_encrypted">Не зашифровано</string>
|
||||||
<string name="enc_status_encrypted_open">Зашифровано (открыто)</string>
|
<string name="enc_status_encrypted_open">Зашифровано (открыто)</string>
|
||||||
<string name="enc_status_encrypted">Зашифровано</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="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">Статус: %1$s, %2$s</string>
|
||||||
@@ -217,10 +210,8 @@
|
|||||||
<string name="storage_home_status_encrypted">зашифровано</string>
|
<string name="storage_home_status_encrypted">зашифровано</string>
|
||||||
<string name="storage_home_status_not_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_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_two_fa_subtitle">Коды и секреты двухфакторной аутентификации</string>
|
||||||
<string name="storage_home_text_secrets_title">Текстовые секреты (%1$d)</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_text_secrets_subtitle">Заметки, токены и произвольные пары ключ-значение</string>
|
||||||
<string name="storage_home_future_sections">Скоро здесь появятся Files, Media и другие типы данных.</string>
|
<string name="storage_home_future_sections">Скоро здесь появятся Files, Media и другие типы данных.</string>
|
||||||
<string name="two_fa_add_token">Добавить токен</string>
|
<string name="two_fa_add_token">Добавить токен</string>
|
||||||
@@ -231,17 +222,13 @@
|
|||||||
<string name="two_fa_field_account">Аккаунт</string>
|
<string name="two_fa_field_account">Аккаунт</string>
|
||||||
<string name="two_fa_field_secret">Секрет</string>
|
<string name="two_fa_field_secret">Секрет</string>
|
||||||
<string name="two_fa_field_notes_optional">Заметка (опционально)</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_algorithm">Алгоритм (SHA1, SHA256, SHA512)</string>
|
||||||
<string name="two_fa_field_digits_value">Количество цифр: %1$d</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_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_label">Обновление через</string>
|
<string name="two_fa_code_refresh_label">Обновление через</string>
|
||||||
<string name="two_fa_code_refresh_seconds">%1$d с</string>
|
<string name="two_fa_code_refresh_seconds">%1$d с</string>
|
||||||
<string name="two_fa_code_invalid_secret">Неверный секрет или формат</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_action">Сканировать QR</string>
|
||||||
<string name="two_fa_scan_qr_title">Сканирование QR-кода TOTP</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_scan_qr_invalid">QR-код не содержит валидный otpauth://totp URI</string>
|
||||||
@@ -259,9 +246,7 @@
|
|||||||
<string name="text_secret_copy_value">Скопировать значение</string>
|
<string name="text_secret_copy_value">Скопировать значение</string>
|
||||||
<string name="save">Сохранить</string>
|
<string name="save">Сохранить</string>
|
||||||
<string name="cancel">Отмена</string>
|
<string name="cancel">Отмена</string>
|
||||||
<string name="open">Открыть</string>
|
|
||||||
<string name="edit">Редактировать</string>
|
<string name="edit">Редактировать</string>
|
||||||
<string name="common_unknown">Неизвестно</string>
|
|
||||||
<string name="settings_language_section">Язык</string>
|
<string name="settings_language_section">Язык</string>
|
||||||
<string name="settings_language_system">Как в системе</string>
|
<string name="settings_language_system">Как в системе</string>
|
||||||
<string name="settings_language_english">English</string>
|
<string name="settings_language_english">English</string>
|
||||||
@@ -269,7 +254,6 @@
|
|||||||
<string name="task_pipeline_test_elapsed">Прошло: %1$d с / %2$d с</string>
|
<string name="task_pipeline_test_elapsed">Прошло: %1$d с / %2$d с</string>
|
||||||
<string name="text_secret_clipboard_fallback_label">значение</string>
|
<string name="text_secret_clipboard_fallback_label">значение</string>
|
||||||
<string name="sync_progress_no_groups">Синхронизация: группы не настроены</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_started">Синхронизация: запущена</string>
|
||||||
<string name="sync_progress_completed">Синхронизация: завершена</string>
|
<string name="sync_progress_completed">Синхронизация: завершена</string>
|
||||||
<string name="sync_progress_group_preparing">Синхронизация: группа «%1$s» — подготовка</string>
|
<string name="sync_progress_group_preparing">Синхронизация: группа «%1$s» — подготовка</string>
|
||||||
@@ -283,7 +267,6 @@
|
|||||||
<string name="sync_progress_group_cancelled">Синхронизация: группа «%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_journal">Синхронизация: группа «%1$s» — журнал %2$d/%3$d</string>
|
||||||
<string name="sync_progress_group_no_entries">Синхронизация: группа «%1$s» — нет записей в журнале</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_entry">Синхронизация: группа «%1$s» — запись %2$d/%3$d</string>
|
||||||
<string name="sync_progress_group_completed">Синхронизация: группа «%1$s» завершена</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_renewing_locks">Синхронизация: группа «%1$s» — продление блокировок</string>
|
||||||
@@ -293,7 +276,6 @@
|
|||||||
<string name="task_log_sync_finished">Синхронизация хранилищ завершена</string>
|
<string name="task_log_sync_finished">Синхронизация хранилищ завершена</string>
|
||||||
<string name="task_log_sync_failed">Синхронизация не удалась: %1$s</string>
|
<string name="task_log_sync_failed">Синхронизация не удалась: %1$s</string>
|
||||||
<string name="task_log_enumerating">Перечисление файлов и папок…</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_creating_storage">Создание хранилища…</string>
|
||||||
<string name="task_log_storage_created">Хранилище создано</string>
|
<string name="task_log_storage_created">Хранилище создано</string>
|
||||||
<string name="task_log_checking_storage">Проверка хранилища…</string>
|
<string name="task_log_checking_storage">Проверка хранилища…</string>
|
||||||
|
|||||||
11
ui/src/main/res/values/plurals.xml
Normal file
11
ui/src/main/res/values/plurals.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<plurals name="sync_progress_preparing">
|
||||||
|
<item quantity="one">Storage sync: preparing %d group</item>
|
||||||
|
<item quantity="other">Storage sync: preparing %d groups</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="sync_progress_group_processing">
|
||||||
|
<item quantity="one">Storage sync: group "%1$s" processing %2$d entry</item>
|
||||||
|
<item quantity="other">Storage sync: group "%1$s" processing %2$d entries</item>
|
||||||
|
</plurals>
|
||||||
|
</resources>
|
||||||
@@ -15,13 +15,10 @@
|
|||||||
<string name="sync_progress_section_title">Storage sync</string>
|
<string name="sync_progress_section_title">Storage sync</string>
|
||||||
<string name="sync_groups_busy_section_title">Saving sync groups</string>
|
<string name="sync_groups_busy_section_title">Saving sync groups</string>
|
||||||
<string name="sync_run_now">Run sync now</string>
|
<string name="sync_run_now">Run sync now</string>
|
||||||
<string name="sync_cd_run_now">Run sync now</string>
|
|
||||||
<string name="sync_refresh">Refresh</string>
|
|
||||||
<string name="sync_add_storage">Add storage to group</string>
|
<string name="sync_add_storage">Add storage to group</string>
|
||||||
<string name="sync_remove_group">Remove group</string>
|
<string name="sync_remove_group">Remove group</string>
|
||||||
<string name="sync_group_empty">No storages in this group</string>
|
<string name="sync_group_empty">No storages in this group</string>
|
||||||
<string name="sync_remove_storage">Remove storage from group</string>
|
<string name="sync_remove_storage">Remove storage from group</string>
|
||||||
<string name="sync_picker_back">Back</string>
|
|
||||||
<string name="sync_cd_picker_back">Close storage picker</string>
|
<string name="sync_cd_picker_back">Close storage picker</string>
|
||||||
<string name="sync_picker_title">Pick storage for %1$s</string>
|
<string name="sync_picker_title">Pick storage for %1$s</string>
|
||||||
<string name="sync_picker_add">Add</string>
|
<string name="sync_picker_add">Add</string>
|
||||||
@@ -31,7 +28,6 @@
|
|||||||
<string name="sync_picker_expand">Expand</string>
|
<string name="sync_picker_expand">Expand</string>
|
||||||
<string name="sync_picker_collapse">Collapse</string>
|
<string name="sync_picker_collapse">Collapse</string>
|
||||||
<string name="sync_fab_create_group_cd">Create sync group</string>
|
<string name="sync_fab_create_group_cd">Create sync group</string>
|
||||||
<string name="sync_group_mixed_encryption_warning">Mixed encryption in group: set a single mode</string>
|
|
||||||
<string name="sync_group_incompatible_warning">Incompatible storages in group: %1$d</string>
|
<string name="sync_group_incompatible_warning">Incompatible storages in group: %1$d</string>
|
||||||
<string name="sync_group_policy_line">Group encryption policy: %1$s</string>
|
<string name="sync_group_policy_line">Group encryption policy: %1$s</string>
|
||||||
<string name="sync_group_policy_unset">Not set (group is empty)</string>
|
<string name="sync_group_policy_unset">Not set (group is empty)</string>
|
||||||
@@ -51,8 +47,6 @@
|
|||||||
<string name="sync_msg_only_plain_storage_allowed">Only unencrypted storages can be added to sync groups</string>
|
<string name="sync_msg_only_plain_storage_allowed">Only unencrypted storages can be added to sync groups</string>
|
||||||
<string name="sync_msg_storage_encryption_key_required">Encrypted storage requires the password (open it before adding)</string>
|
<string name="sync_msg_storage_encryption_key_required">Encrypted storage requires the password (open it before adding)</string>
|
||||||
<string name="sync_msg_storage_incompatible_encryption">Storage is not compatible with the group encryption policy</string>
|
<string name="sync_msg_storage_incompatible_encryption">Storage is not compatible with the group encryption policy</string>
|
||||||
<string name="sync_msg_virtual_storage_not_supported">Cannot add an open virtual storage: sync works with raw storages</string>
|
|
||||||
<string name="sync_msg_task_enqueued">Sync task queued</string>
|
|
||||||
<string name="sync_msg_sync_already_running">Sync is already running</string>
|
<string name="sync_msg_sync_already_running">Sync is already running</string>
|
||||||
<string name="sync_msg_blocked_during_sync">Wait for sync to finish</string>
|
<string name="sync_msg_blocked_during_sync">Wait for sync to finish</string>
|
||||||
<string name="sync_encryption_unknown">Unknown</string>
|
<string name="sync_encryption_unknown">Unknown</string>
|
||||||
@@ -208,7 +202,6 @@
|
|||||||
<string name="enc_status_not_encrypted">Not encrypted</string>
|
<string name="enc_status_not_encrypted">Not encrypted</string>
|
||||||
<string name="enc_status_encrypted_open">Encrypted (open)</string>
|
<string name="enc_status_encrypted_open">Encrypted (open)</string>
|
||||||
<string name="enc_status_encrypted">Encrypted</string>
|
<string name="enc_status_encrypted">Encrypted</string>
|
||||||
<string name="text_edit_screen_title">Text</string>
|
|
||||||
<string name="text_edit_screen_placeholder">Content: %1$s</string>
|
<string name="text_edit_screen_placeholder">Content: %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">Status: %1$s, %2$s</string>
|
<string name="storage_home_status_line">Status: %1$s, %2$s</string>
|
||||||
@@ -217,10 +210,8 @@
|
|||||||
<string name="storage_home_status_encrypted">encrypted</string>
|
<string name="storage_home_status_encrypted">encrypted</string>
|
||||||
<string name="storage_home_status_not_encrypted">not encrypted</string>
|
<string name="storage_home_status_not_encrypted">not encrypted</string>
|
||||||
<string name="storage_home_two_fa_title">2FA tokens (%1$d)</string>
|
<string name="storage_home_two_fa_title">2FA tokens (%1$d)</string>
|
||||||
<string name="storage_home_open_two_fa">Open 2FA</string>
|
|
||||||
<string name="storage_home_two_fa_subtitle">Two-factor authentication codes and secrets</string>
|
<string name="storage_home_two_fa_subtitle">Two-factor authentication codes and secrets</string>
|
||||||
<string name="storage_home_text_secrets_title">Text secrets (%1$d)</string>
|
<string name="storage_home_text_secrets_title">Text secrets (%1$d)</string>
|
||||||
<string name="storage_home_open_text_secrets">Open text secrets</string>
|
|
||||||
<string name="storage_home_text_secrets_subtitle">Notes, tokens, and arbitrary key-value pairs</string>
|
<string name="storage_home_text_secrets_subtitle">Notes, tokens, and arbitrary key-value pairs</string>
|
||||||
<string name="storage_home_future_sections">Files, Media, and more will appear here soon.</string>
|
<string name="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">Add token</string>
|
||||||
@@ -231,17 +222,13 @@
|
|||||||
<string name="two_fa_field_account">Account</string>
|
<string name="two_fa_field_account">Account</string>
|
||||||
<string name="two_fa_field_secret">Secret</string>
|
<string name="two_fa_field_secret">Secret</string>
|
||||||
<string name="two_fa_field_notes_optional">Note (optional)</string>
|
<string name="two_fa_field_notes_optional">Note (optional)</string>
|
||||||
<string name="two_fa_field_digits">Code digits (usually 6 or 8)</string>
|
|
||||||
<string name="two_fa_field_period_seconds">Refresh period in seconds (usually 30)</string>
|
|
||||||
<string name="two_fa_field_algorithm">Algorithm (SHA1, SHA256, SHA512)</string>
|
<string name="two_fa_field_algorithm">Algorithm (SHA1, SHA256, SHA512)</string>
|
||||||
<string name="two_fa_field_digits_value">Digits: %1$d</string>
|
<string name="two_fa_field_digits_value">Digits: %1$d</string>
|
||||||
<string name="two_fa_field_period_seconds_value">Refresh period: %1$d s</string>
|
<string name="two_fa_field_period_seconds_value">Refresh period: %1$d s</string>
|
||||||
<string name="two_fa_code_unavailable">------</string>
|
<string name="two_fa_code_unavailable">------</string>
|
||||||
<string name="two_fa_code_refresh_in">Refresh in %1$d s</string>
|
|
||||||
<string name="two_fa_code_refresh_label">Refresh in</string>
|
<string name="two_fa_code_refresh_label">Refresh in</string>
|
||||||
<string name="two_fa_code_refresh_seconds">%1$d s</string>
|
<string name="two_fa_code_refresh_seconds">%1$d s</string>
|
||||||
<string name="two_fa_code_invalid_secret">Invalid secret or format</string>
|
<string name="two_fa_code_invalid_secret">Invalid secret or format</string>
|
||||||
<string name="two_fa_copy_code_hint">Tap to copy code</string>
|
|
||||||
<string name="two_fa_scan_qr_action">Scan QR</string>
|
<string name="two_fa_scan_qr_action">Scan QR</string>
|
||||||
<string name="two_fa_scan_qr_title">Scan TOTP QR code</string>
|
<string name="two_fa_scan_qr_title">Scan TOTP QR code</string>
|
||||||
<string name="two_fa_scan_qr_invalid">QR code does not contain a valid otpauth://totp URI</string>
|
<string name="two_fa_scan_qr_invalid">QR code does not contain a valid otpauth://totp URI</string>
|
||||||
@@ -259,9 +246,7 @@
|
|||||||
<string name="text_secret_copy_value">Copy value</string>
|
<string name="text_secret_copy_value">Copy value</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="open">Open</string>
|
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="common_unknown">Unknown</string>
|
|
||||||
<string name="settings_language_section">Language</string>
|
<string name="settings_language_section">Language</string>
|
||||||
<string name="settings_language_system">System default</string>
|
<string name="settings_language_system">System default</string>
|
||||||
<string name="settings_language_english">English</string>
|
<string name="settings_language_english">English</string>
|
||||||
@@ -269,7 +254,6 @@
|
|||||||
<string name="task_pipeline_test_elapsed">Elapsed: %1$d s / %2$d s</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="text_secret_clipboard_fallback_label">value</string>
|
||||||
<string name="sync_progress_no_groups">Storage sync: no groups configured</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_started">Storage sync: started</string>
|
||||||
<string name="sync_progress_completed">Storage sync: completed</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_preparing">Storage sync: group "%1$s" preparing</string>
|
||||||
@@ -283,7 +267,6 @@
|
|||||||
<string name="sync_progress_group_cancelled">Storage sync: group "%1$s" cancelled by newer run</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_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_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_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_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_renewing_locks">Storage sync: group "%1$s" renewing locks</string>
|
||||||
@@ -293,7 +276,6 @@
|
|||||||
<string name="task_log_sync_finished">Storage sync finished</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_sync_failed">Storage sync failed: %1$s</string>
|
||||||
<string name="task_log_enumerating">Enumerating files and directories…</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_creating_storage">Creating storage…</string>
|
||||||
<string name="task_log_storage_created">Storage created</string>
|
<string name="task_log_storage_created">Storage created</string>
|
||||||
<string name="task_log_checking_storage">Checking storage…</string>
|
<string name="task_log_checking_storage">Checking storage…</string>
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import org.junit.Test
|
|||||||
|
|
||||||
class TaskProgressLabelsTest {
|
class TaskProgressLabelsTest {
|
||||||
|
|
||||||
private val resolver = UiStringResolver { id, _ -> "res:$id" }
|
private val resolver = object : UiStringResolver {
|
||||||
|
override fun invoke(id: Int, vararg formatArgs: Any): String = "res:$id"
|
||||||
|
|
||||||
|
override fun plurals(id: Int, quantity: Int, vararg formatArgs: Any): String = "plural:$id"
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun syncNoGroups_mapsToStringRes() {
|
fun syncNoGroups_mapsToStringRes() {
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ class WallencUserNotificationMappingTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsFeatureStorageNotFound() {
|
fun mapsFeatureStorageNotFound() {
|
||||||
val notification = WallencException.Feature.StorageNotFound.toUserNotification()
|
val notification = WallencException.Feature.StorageNotFound().toUserNotification()
|
||||||
assertEquals(R.string.error_storage_not_found, notification.id)
|
assertEquals(R.string.error_storage_not_found, notification.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsStorageIncorrectKey() {
|
fun mapsStorageIncorrectKey() {
|
||||||
val notification = WallencException.Storage.IncorrectKey.toUserNotification()
|
val notification = WallencException.Storage.IncorrectKey().toUserNotification()
|
||||||
assertEquals(R.string.error_incorrect_password, notification.id)
|
assertEquals(R.string.error_incorrect_password, notification.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class TaskPipelineViewModelTest {
|
|||||||
val uiStrings = mockk<UiStringResolver>()
|
val uiStrings = mockk<UiStringResolver>()
|
||||||
every { uiStrings.invoke(any<Int>(), any()) } returns "Test task"
|
every { uiStrings.invoke(any<Int>(), any()) } returns "Test task"
|
||||||
every { uiStrings.invoke(any<Int>()) } returns "Test"
|
every { uiStrings.invoke(any<Int>()) } returns "Test"
|
||||||
|
every { uiStrings.plurals(any(), any(), any()) } returns "Plural"
|
||||||
val viewModel = TaskPipelineViewModel(orchestrator, uiStrings)
|
val viewModel = TaskPipelineViewModel(orchestrator, uiStrings)
|
||||||
|
|
||||||
viewModel.startTestTask(durationSec = 0, infinityIndeterminateProgress = false)
|
viewModel.startTestTask(durationSec = 0, infinityIndeterminateProgress = false)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("javax.inject:javax.inject:1")
|
compileOnly(libs.javax.inject)
|
||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|||||||
@@ -67,11 +67,4 @@ class ManageStoragesEncryptionUseCase @Inject constructor(
|
|||||||
unlockManager.close(storage)
|
unlockManager.close(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun changePassword(storage: IStorageInfo, newKey: EncryptKey, encryptPath: Boolean) {
|
|
||||||
if (storage !is IStorage) return
|
|
||||||
if (storage.metaInfo.value.encInfo == null) {
|
|
||||||
throw IllegalStateException("Storage is not encrypted")
|
|
||||||
}
|
|
||||||
storage.setEncInfo(Encryptor.generateEncryptionInfo(newKey, encryptPath))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class RunStorageSyncUseCase @Inject constructor(
|
|||||||
* @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
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
|||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroup
|
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroup
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
|
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
|
||||||
import java.util.Base64
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
fun storageEncryptionSecret(key: EncryptKey): String =
|
|
||||||
Base64.getEncoder().encodeToString(key.bytes)
|
|
||||||
|
|
||||||
fun isStorageCompatibleWithGroup(
|
fun isStorageCompatibleWithGroup(
|
||||||
storage: IStorage,
|
storage: IStorage,
|
||||||
group: StorageSyncGroup,
|
group: StorageSyncGroup,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.github.nullptroma.wallenc.usecases
|
|||||||
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 kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,3 @@ val List<DescribedVault>.locals: List<DescribedVault>
|
|||||||
|
|
||||||
val List<DescribedVault>.remotes: List<DescribedVault>
|
val List<DescribedVault>.remotes: List<DescribedVault>
|
||||||
get() = filter { it.descriptor is VaultDescriptor.LinkedRemote }
|
get() = filter { it.descriptor is VaultDescriptor.LinkedRemote }
|
||||||
|
|
||||||
fun List<DescribedVault>.byBrand(brand: CloudBrand): List<DescribedVault> = filter {
|
|
||||||
(it.descriptor as? VaultDescriptor.LinkedRemote)?.brand == brand
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user