Улучшение UI/UX

This commit is contained in:
2026-05-17 11:54:02 +03:00
parent 5777f8e459
commit 555448d998
17 changed files with 330 additions and 139 deletions

View File

@@ -4,6 +4,11 @@ import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.JsonArray
@@ -20,9 +25,14 @@ import java.util.UUID
class ManageTextSecretsUseCase {
private val mutex = Mutex()
suspend fun list(storageInfo: IStorageInfo): List<TextSecretRecord> = mutex.withLock {
val storage = storageInfo as? IStorage ?: return@withLock emptyList()
readAll(storage)
fun observe(storageInfo: IStorageInfo): Flow<List<TextSecretRecord>> {
val storage = storageInfo as? IStorage ?: return flowOf(emptyList())
return merge(
flowOf(Unit),
storage.accessor.filesUpdates.map { Unit },
).map {
mutex.withLock { readAll(storage) }
}.distinctUntilChanged()
}
suspend fun get(storageInfo: IStorageInfo, id: String): TextSecretRecord? = mutex.withLock {

View File

@@ -3,6 +3,11 @@ package com.github.nullptroma.wallenc.usecases
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.JsonElement
@@ -16,9 +21,14 @@ import java.util.UUID
class ManageTwoFaTokensUseCase {
private val mutex = Mutex()
suspend fun list(storageInfo: IStorageInfo): List<TwoFaTokenRecord> = mutex.withLock {
val storage = storageInfo as? IStorage ?: return@withLock emptyList()
readAll(storage)
fun observe(storageInfo: IStorageInfo): Flow<List<TwoFaTokenRecord>> {
val storage = storageInfo as? IStorage ?: return flowOf(emptyList())
return merge(
flowOf(Unit),
storage.accessor.filesUpdates.map { Unit },
).map {
mutex.withLock { readAll(storage) }
}.distinctUntilChanged()
}
suspend fun get(storageInfo: IStorageInfo, id: String): TwoFaTokenRecord? = mutex.withLock {

View File

@@ -73,7 +73,7 @@ class StorageSyncEngine(
return
}
val leaseUntil = Instant.MAX
val leaseUntil = Instant.now().plusSeconds(SYNC_LOCK_LEASE_SECONDS)
val lockedAccessors = mutableListOf<IStorageAccessor>()
try {
reportProgress(null, "Storage sync: group \"$groupId\" acquiring locks")
@@ -223,4 +223,8 @@ class StorageSyncEngine(
}
return a.revision.createdAt.compareTo(b.revision.createdAt)
}
private companion object {
private const val SYNC_LOCK_LEASE_SECONDS: Long = 30 * 60
}
}

View File

@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
@@ -46,7 +47,7 @@ class StorageDomainUseCasesTest {
notes = "primary",
)
assertNotNull(created.id)
assertEquals(1, useCase.list(storage).size)
assertEquals(1, useCase.observe(storage).first().size)
val updated = useCase.update(
storageInfo = storage,
@@ -57,7 +58,7 @@ class StorageDomainUseCasesTest {
val removed = useCase.delete(storage, created.id)
assertTrue(removed)
assertTrue(useCase.list(storage).isEmpty())
assertTrue(useCase.observe(storage).first().isEmpty())
}
@Test
@@ -66,7 +67,7 @@ class StorageDomainUseCasesTest {
setDomainFile(StorageDomainDataFiles.TWO_FA_TOKENS_FILE, "not-json")
}
val useCase = ManageTwoFaTokensUseCase()
assertTrue(useCase.list(storage).isEmpty())
assertTrue(useCase.observe(storage).first().isEmpty())
}
@Test
@@ -82,7 +83,7 @@ class StorageDomainUseCasesTest {
TextSecretEntryRecord(label = null, value = "password"),
),
)
assertEquals(1, useCase.list(storage).size)
assertEquals(1, useCase.observe(storage).first().size)
val updated = useCase.update(
storageInfo = storage,
@@ -107,7 +108,7 @@ class StorageDomainUseCasesTest {
setDomainFile(StorageDomainDataFiles.TEXT_SECRETS_FILE, "{broken")
}
val useCase = ManageTextSecretsUseCase()
assertTrue(useCase.list(storage).isEmpty())
assertTrue(useCase.observe(storage).first().isEmpty())
}
}
@@ -143,11 +144,12 @@ private class FakeMetaInfo : IStorageMetaInfo {
private class FakeStorageAccessor : IStorageAccessor {
val dataFiles: MutableMap<String, ByteArray> = mutableMapOf()
private val systemFiles: MutableMap<String, ByteArray> = mutableMapOf()
private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>(extraBufferCapacity = 16)
override val size: StateFlow<Long?> = MutableStateFlow(0L)
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
override val filesUpdates: SharedFlow<DataPage<IFile>> = MutableSharedFlow()
override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates
override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = MutableSharedFlow()
override suspend fun getAllFiles(): List<IFile> = emptyList()
@@ -182,6 +184,7 @@ private class FakeStorageAccessor : IStorageAccessor {
return object : ByteArrayOutputStream() {
override fun close() {
dataFiles[path] = toByteArray()
_filesUpdates.tryEmit(DataPage(list = emptyList(), pageLength = 1, pageIndex = 0))
}
}
}