Исправлено много варнингов

This commit is contained in:
2026-05-11 22:18:15 +03:00
parent d176f2a464
commit 61bcaa95d8
29 changed files with 162 additions and 123 deletions

View File

@@ -3,7 +3,9 @@ package com.github.nullptroma.wallenc.app.tasks
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.graphics.Color
import android.os.IBinder
import android.view.View
@@ -52,7 +54,14 @@ class TaskPipelineForegroundService : Service() {
private val serviceJob = SupervisorJob()
private val serviceScope = CoroutineScope(serviceJob + Dispatchers.Main.immediate)
override fun onBind(intent: android.content.Intent?): IBinder? = null
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_CANCEL_ALL_TASKS && ::orchestrator.isInitialized) {
orchestrator.cancelAll()
}
return START_STICKY
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCreate() {
@@ -133,6 +142,11 @@ class TaskPipelineForegroundService : Service() {
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setOnlyAlertOnce(true)
.addAction(
0,
getString(R.string.task_notification_cancel),
cancelAllTasksPendingIntent(),
)
.build()
private fun buildAccumulatedNotification(tasks: List<TaskForegroundItem>): Notification {
@@ -141,19 +155,44 @@ class TaskPipelineForegroundService : Service() {
applyNotificationTemplateTextColor(big)
bindTaskRows(big, sorted)
val collapsedSubtext = when {
sorted.isEmpty() ->
getString(R.string.task_notification_preparing)
sorted.all { it.progress?.fraction == null } ->
getString(R.string.task_notification_indeterminate)
else -> resources.getQuantityString(
R.plurals.task_notification_group_subtext,
sorted.size,
sorted.size,
)
}
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.task_notification_title))
.setContentText(
getString(R.string.task_notification_group_subtext, sorted.size),
)
.setContentText(collapsedSubtext)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomBigContentView(big)
.addAction(
0,
getString(R.string.task_notification_cancel),
cancelAllTasksPendingIntent(),
)
.build()
}
private fun cancelAllTasksPendingIntent(): PendingIntent =
PendingIntent.getService(
this,
0,
Intent(this, TaskPipelineForegroundService::class.java).setAction(ACTION_CANCEL_ALL_TASKS),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
/**
* Uses only [android.R.attr.textColor] from [android.R.style.TextAppearance_Material_Notification_Title]
* so line size stays from layout (13sp) while color matches system notification title text.
@@ -199,7 +238,7 @@ class TaskPipelineForegroundService : Service() {
showTaskRow(remoteViews, i, sorted[i])
}
val remaining = n - (MAX_TASK_ROWS - 1)
showOverflowRow(remoteViews, MAX_TASK_ROWS - 1, remaining)
showOverflowRow(remoteViews, remaining)
}
advanceIndeterminateDotPhasesAfterBind(sorted, n)
}
@@ -254,14 +293,19 @@ class TaskPipelineForegroundService : Service() {
}
}
private fun showOverflowRow(remoteViews: RemoteViews, index: Int, remainingCount: Int) {
remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.VISIBLE)
private fun showOverflowRow(remoteViews: RemoteViews, remainingCount: Int) {
val overflowIndex = MAX_TASK_ROWS - 1
remoteViews.setViewVisibility(TASK_ROW_IDS[overflowIndex], View.VISIBLE)
remoteViews.setTextViewText(
TASK_TITLE_IDS[index],
getString(R.string.task_notification_more_tasks, remainingCount),
TASK_TITLE_IDS[overflowIndex],
resources.getQuantityString(
R.plurals.task_notification_more_tasks,
remainingCount,
remainingCount,
),
)
remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.GONE)
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[index], "")
remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[overflowIndex], View.GONE)
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[overflowIndex], "")
}
private fun indeterminateSubtitleWithDots(taskId: TaskId, baseLabel: String): String {
@@ -276,6 +320,9 @@ class TaskPipelineForegroundService : Service() {
}
companion object {
private const val ACTION_CANCEL_ALL_TASKS =
"com.github.nullptroma.wallenc.action.CANCEL_ALL_TASKS"
private const val CHANNEL_ID = "wallenc_task_pipeline"
private const val FOREGROUND_NOTIFICATION_ID = 1001

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="task_notification_group_subtext">
<item quantity="one">%d task running</item>
<item quantity="other">%d tasks running</item>
</plurals>
<plurals name="task_notification_more_tasks">
<item quantity="one">+%d more</item>
<item quantity="other">+%d more</item>
</plurals>
</resources>

View File

@@ -5,6 +5,4 @@
<string name="task_notification_preparing">Preparing…</string>
<string name="task_notification_indeterminate">Working…</string>
<string name="task_notification_cancel">Cancel</string>
<string name="task_notification_group_subtext">%d tasks running</string>
<string name="task_notification_more_tasks">+%d more</string>
</resources>

View File

@@ -17,7 +17,7 @@ dependencies {
implementation(libs.retrofit.converter.scalars)
implementation(libs.retrofit.converter.jackson)
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation(libs.okhttp3)
implementation(libs.kotlinx.coroutines.core)
testImplementation(libs.junit)

View File

@@ -6,9 +6,9 @@ import java.time.Instant
@JsonIgnoreProperties(ignoreUnknown = true)
data class DiskInfoDto(
@JsonProperty("trash_size") val trashSize: Long? = null,
@JsonProperty("total_space") val totalSpace: Long? = null,
@JsonProperty("used_space") val usedSpace: Long? = null,
@param:JsonProperty("trash_size") val trashSize: Long? = null,
@param:JsonProperty("total_space") val totalSpace: Long? = null,
@param:JsonProperty("used_space") val usedSpace: Long? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -36,10 +36,10 @@ data class ResourceDto(
val size: Long? = null,
val modified: Instant? = null,
val created: Instant? = null,
@JsonProperty("mime_type") val mimeType: String? = null,
@param:JsonProperty("mime_type") val mimeType: String? = null,
val md5: String? = null,
@JsonProperty("custom_properties") val customProperties: Map<String, Any?>? = null,
@JsonProperty("_embedded") val embedded: EmbeddedResourceListDto? = null,
@param:JsonProperty("custom_properties") val customProperties: Map<String, Any?>? = null,
@param:JsonProperty("_embedded") val embedded: EmbeddedResourceListDto? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -56,5 +56,5 @@ data class ApiErrorDto(
@JsonIgnoreProperties(ignoreUnknown = true)
data class CustomPropertiesPatchDto(
@JsonProperty("custom_properties") val customProperties: Map<String, String>,
@param:JsonProperty("custom_properties") val customProperties: Map<String, String>,
)

View File

@@ -13,7 +13,6 @@ import com.github.nullptroma.wallenc.infrastructure.network.yandexdisk.dto.Resou
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
@@ -21,6 +20,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import retrofit2.HttpException
import retrofit2.Response
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
@@ -74,20 +74,6 @@ class YandexDiskRepository(
}
}
suspend fun move(from: String, toPath: String, overwrite: Boolean = false): Unit =
withContext(ioDispatcher) {
val resp = wrapAuth { api.moveResource(from, toPath, overwrite) }
when (resp.code()) {
201 -> Unit
202 -> {
val link = resp.body()?.use { body -> parseLink(body) }
?: throw IOException("MOVE 202 without body")
awaitOperation(link.href)
}
else -> throw failure("move", resp)
}
}
suspend fun setCustomProperties(path: String, props: Map<String, String>): Unit =
withContext(ioDispatcher) {
val resp = wrapAuth {
@@ -141,21 +127,17 @@ class YandexDiskRepository(
resp.close()
throw IOException("Download failed: HTTP ${resp.code}")
}
val body = resp.body ?: run {
val body = resp.body
val stream = body?.byteStream() ?: run {
resp.close()
throw IOException("Download: empty body")
throw IOException("Download failed: missing body")
}
body.byteStream().let { stream ->
object : InputStream() {
override fun read(): Int = stream.read()
override fun read(b: ByteArray): Int = stream.read(b)
override fun read(b: ByteArray, off: Int, len: Int): Int = stream.read(b, off, len)
override fun close() {
try {
stream.close()
} finally {
resp.close()
}
object : FilterInputStream(stream) {
override fun close() {
try {
`in`.close()
} finally {
resp.close()
}
}
}
@@ -201,16 +183,5 @@ class YandexDiskRepository(
private val OCTET_STREAM = "application/octet-stream".toMediaType()
private const val OPERATION_POLL_DELAY_MS = 300L
private const val OPERATION_POLL_MAX = 200
fun parseOperationId(href: String): String? {
val url = href.toHttpUrlOrNull() ?: return null
url.queryParameter("id")?.let { return it }
val segments = url.pathSegments
val idx = segments.indexOf("operations")
if (idx >= 0 && idx + 1 < segments.size) {
return segments[idx + 1]
}
return null
}
}
}

View File

@@ -18,10 +18,10 @@ import java.io.InputStream
import java.util.UUID
/**
* Общий «скелет» storage'а: единая логика meta-info, rename, setEncInfo,
* clearAllContent и делегирования размера/доступности к [accessor].
* Общий «скелет» для [IStorage]: единая логика meta-info, rename, setEncInfo,
* clearAllContent и делегирование размеров и доступности в [accessor].
*
* Подклассы определяют только как создаётся [accessor], значение
* Подклассы определяют только способ создания [accessor], значение
* [isVirtualStorage] и (при необходимости) расширяют [init] своими шагами
* (например, проверкой ключа или инициализацией внешней связи).
*/
@@ -60,8 +60,8 @@ abstract class BaseStorage(
protected open fun metaInfoUuidPart(): String = uuid.toString()
/**
* Запускается единожды при старте storage'а. Подклассы могут переопределить,
* добавив свои шаги (init accessor'а, проверка ключа и т.п.). Обязательно
* Запускается единожды при первом использовании хранилища. Подклассы могут переопределить,
* добавив свои шаги (инициализацию [accessor], проверку ключа и т.п.). Обязательно
* должен в какой-то момент вызвать [readMetaInfo].
*/
open suspend fun init() {

View File

@@ -34,10 +34,10 @@ import java.time.Instant
import java.util.UUID
/**
* [IStorageAccessor] поверх папки приложения `app:/<storageUuid>/…` на Яндекс.Диске.
* Реализация [IStorageAccessor] для дерева файлов `app:/<storageUuid>/…` на Яндекс.Диске.
*
* [isAvailable] = доступность vault'а ([vaultAvailability]) **и** успешная локальная
* инициализация этого storage ([storageReady]).
* [isAvailable] объединяет доступность удалённого хранилища ([vaultAvailability])
* и успешную локальную инициализацию ([storageReady]) этого аксессора.
*/
class YandexStorageAccessor(
private val storageUuid: UUID,
@@ -265,7 +265,7 @@ class YandexStorageAccessor(
while (queue.isNotEmpty()) {
val rel = queue.removeFirst()
if (isSystemRel(rel)) continue
val (files, dirs) = listImmediateChildren(rel)
val (_, dirs) = listImmediateChildren(rel)
for (d in dirs) {
if (!isSystemRel(d.metaInfo.path)) {
out.add(d)
@@ -402,10 +402,12 @@ class YandexStorageAccessor(
override suspend fun openWriteSystemFile(name: String): OutputStream = withContext(ioDispatcher) {
ensureSystemDirExists()
val rel = "/$SYSTEM_HIDDEN_DIRNAME/$name"
val baos = ByteArrayOutputStream()
baos.onClosed {
val uploadBuffer = ByteArrayOutputStream()
uploadBuffer.onClosed {
runBlocking(ioDispatcher) {
guard { repo.uploadBytes(toDiskPath(rel), baos.toByteArray(), overwrite = true) }
guard {
repo.uploadBytes(toDiskPath(rel), uploadBuffer.toByteArray(), overwrite = true)
}
}
}
}

View File

@@ -31,7 +31,7 @@ import java.util.UUID
@OptIn(ExperimentalCoroutinesApi::class)
class VaultsManager(
private val ioDispatcher: CoroutineDispatcher,
localVault: IVault,
private val localVault: IVault,
keyRepo: StorageKeyMapStore,
private val yandexAccountStore: YandexAccountStore,
private val yandexUserInfoRepository: YandexUserInfoRepository,
@@ -40,8 +40,6 @@ class VaultsManager(
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
private val localVault: IVault = localVault
private val yandexVaults: StateFlow<List<IVault>> = yandexAccountStore.observeAll()
.map { rows ->
rows.map { row ->

View File

@@ -20,7 +20,7 @@ import kotlin.io.path.pathString
class LocalVault(
private val ioDispatcher: CoroutineDispatcher,
private val vaultRoot: File?,
vaultRoot: File?,
) : DescribedVault {
override val uuid: UUID = vaultRoot?.let { root ->
@@ -42,7 +42,7 @@ class LocalVault(
private val _availableSpace = MutableStateFlow<Long?>(null)
override val availableSpace: StateFlow<Long?> = _availableSpace
private val path = MutableStateFlow<File?>(vaultRoot)
private val path = MutableStateFlow(vaultRoot)
init {
CoroutineScope(ioDispatcher).launch {

View File

@@ -1,15 +1,13 @@
package com.github.nullptroma.wallenc.infrastructure.vaults.yandex
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
import com.github.nullptroma.wallenc.vault.contract.VaultRegistration
/**
* Регистрация удалённого vault'а Яндекс.Диска по результату OAuth.
* Регистрация удалённого хранилища Яндекс.Диска по результату OAuth.
*
* Живёт в `:data` (а не в `:vault-api`), потому что [VaultRegistration]
* намеренно не sealed — конкретные реализации лежат рядом со своим поставщиком.
* presentation никогда не открывает этот тип, только перепасовывает обратно
* в `VaultRegistrar.register(...)`.
* Слой presentation не раскрывает этот тип, только передаёт его в `VaultRegistrar.register(...)`.
*/
data class YandexRegistration(
val oauthToken: String,
@@ -17,6 +15,4 @@ data class YandexRegistration(
init {
require(oauthToken.isNotBlank()) { "oauthToken must not be blank" }
}
val brand: CloudBrand get() = CloudBrand.YANDEX
}

View File

@@ -64,10 +64,10 @@ class YandexVault(
_availableSpace.value = (total - used).coerceAtLeast(0L)
_vaultReachable.value = true
_storages.value = loadStoragesList()
} catch (e: YandexDiskAuthException) {
} catch (_: YandexDiskAuthException) {
_vaultReachable.value = false
_storages.value = emptyList()
} catch (e: Exception) {
} catch (_: Exception) {
_vaultReachable.value = false
_storages.value = emptyList()
}
@@ -118,7 +118,7 @@ class YandexVault(
},
)
storage.init()
_storages.value = _storages.value + storage
_storages.value += storage
storage
}

View File

@@ -4,9 +4,9 @@ import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import kotlinx.coroutines.flow.StateFlow
/**
* Контракт vault'а: коллекция [IStorage] с реактивным состоянием.
* Контракт хранилища ([IVault]): коллекция [IStorage] с реактивным состоянием.
*
* domain не различает локальные/удалённые/Yandex/etc. — это общий порт.
* Слой domain не различает локальные/удалённые/Yandex и т.д. — это общий порт.
*/
interface IVault : IVaultInfo {
val storages: StateFlow<List<IStorage>>

View File

@@ -3,9 +3,9 @@ package com.github.nullptroma.wallenc.domain.interfaces
import java.util.UUID
/**
* Минимальная идентификация vault'а.
* Минимальная идентификация хранилища ([IVaultInfo]).
*
* Намеренно «голая»: domain ничего не знает о брендах, локальности или статусе —
* Намеренно «голая»: доменный слой не знает о брендах, локальности или статусе —
* вся категоризация лежит во внешнем кольце (`:vault-api: VaultDescriptor`).
*/
interface IVaultInfo {

View File

@@ -3,10 +3,10 @@ package com.github.nullptroma.wallenc.domain.interfaces
import kotlinx.coroutines.flow.StateFlow
/**
* Единая точка доступа ко всем vault'ам приложения.
* Единая точка доступа ко всем подключённым хранилищам приложения.
*
* domain не различает категории vault'ов — потребители (presentation) фильтруют
* [vaults] через `:vault-api` (`VaultDescriptor`/`DescribedVault`).
* Доменный слой не различает категории vault — потребители в UI (presentation)
* фильтруют [vaults] через `:vault-api` (`VaultDescriptor` / `DescribedVault`).
*/
interface IVaultsManager {
val vaults: StateFlow<List<IVault>>

View File

@@ -1,25 +1,26 @@
[versions]
agp = "9.1.1"
jacksonModuleKotlin = "2.21.2"
kotlin = "2.3.20"
jacksonModuleKotlin = "2.21.3"
kotlin = "2.3.21"
coreKtx = "1.18.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinReflect = "2.3.0"
kotlinxCoroutinesCore = "1.10.2"
kotlinReflect = "2.3.21"
kotlinxCoroutinesCore = "1.11.0"
kotlinxSerializationJson = "1.11.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.13.0"
composeBom = "2026.03.01"
navigation = "2.9.7"
composeBom = "2026.05.00"
navigation = "2.9.8"
hiltNavigation = "1.3.0"
timber = "5.0.1"
yandexAuthSdk = "3.2.0"
daggerHilt = "2.59.2"
ksp = "2.3.6"
ksp = "2.3.7"
room = "2.8.4"
retrofit = "3.0.0"
okhttp = "5.3.2"
[libraries]
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" }
@@ -29,6 +30,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
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-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" }
androidx-hilt-lifecycle-viewmodel-compose = { group = "androidx.hilt", name = "hilt-lifecycle-viewmodel-compose", version.ref = "hiltNavigation" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
# Yandex
@@ -47,6 +49,7 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref =
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit" }
retrofit-converter-jackson = { group = "com.squareup.retrofit2", name = "converter-jackson", version.ref = "retrofit" }
okhttp3 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }

View File

@@ -1,6 +1,6 @@
#Sat Sep 07 01:04:14 MSK 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -8,7 +8,7 @@ class RoomFactory(private val context: Context) {
fun buildAppDb(): AppDb {
val room = Room.databaseBuilder(
context, AppDb::class.java, "app-db"
).fallbackToDestructiveMigration().build()
).fallbackToDestructiveMigration(dropAllTables = true).build()
return room
}
}

View File

@@ -22,8 +22,8 @@ class StorageKeyMapRepository(
dao.add(*dbModels.toTypedArray())
}
override suspend fun delete(vararg keymaps: StorageKeyMap) = withContext(ioDispatcher) {
val dbModels = keymaps.map { DbStorageKeyMap.fromModel(it) }
override suspend fun delete(vararg values: StorageKeyMap) = withContext(ioDispatcher) {
val dbModels = values.map { DbStorageKeyMap.fromModel(it) }
dao.delete(*dbModels.toTypedArray())
}
}

View File

@@ -51,6 +51,7 @@ kotlin {
dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
// Hilt
implementation(libs.dagger.hilt)

View File

@@ -21,7 +21,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -66,7 +66,8 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
TaskPipelineRoute::class.qualifiedName!! to NavBarItemData(
R.string.task_pipeline_title,
TaskPipelineRoute::class.qualifiedName!!,
Icons.AutoMirrored.Rounded.List
Icons.AutoMirrored.Rounded.List,
R.string.task_pipeline_open,
),
SettingsRoute::class.qualifiedName!! to NavBarItemData(
R.string.nav_label_settings,
@@ -89,7 +90,7 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
icon = {
if (navBarItemData.icon != null) Icon(
navBarItemData.icon,
contentDescription = stringResource(navBarItemData.nameStringResourceId)
contentDescription = stringResource(navBarItemData.iconContentDescriptionResourceId),
)
},
label = { Text(stringResource(navBarItemData.nameStringResourceId)) },

View File

@@ -2,4 +2,9 @@ package com.github.nullptroma.wallenc.ui.navigation
import androidx.compose.ui.graphics.vector.ImageVector
data class NavBarItemData(val nameStringResourceId: Int, val screenRouteClass: String, val icon: ImageVector?)
data class NavBarItemData(
val nameStringResourceId: Int,
val screenRouteClass: String,
val icon: ImageVector?,
val iconContentDescriptionResourceId: Int = nameStringResourceId,
)

View File

@@ -18,7 +18,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState

View File

@@ -42,7 +42,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
@@ -174,7 +174,13 @@ fun RemoteVaultsScreen(
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.remote_vaults_add_pick_provider),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(16.dp))
FilledTonalButton(
onClick = {
viewModel.setAddChoiceVisible(false)

View File

@@ -30,7 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel

View File

@@ -2,7 +2,7 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@Composable
fun LocalVaultScreen(

View File

@@ -1,9 +1,9 @@
package com.github.nullptroma.wallenc.vault.contract
/**
* Запуск OAuth-сценария привязки удалённого vault'а для конкретного [CloudBrand].
* Запуск OAuth-сценария привязки удалённого хранилища для конкретного [CloudBrand].
*
* Реализация в `:app` (привязана к Activity). presentation вызывает [beginLink]
* Реализация в `:app` (привязана к Activity). Слой presentation вызывает [beginLink]
* из контекста Compose-экрана и получает [VaultLinkOutcome] асинхронно.
*
* Намеренно императивный API через колбэк, чтобы корректно встроиться

View File

@@ -1,9 +1,9 @@
package com.github.nullptroma.wallenc.vault.contract
/**
* Результат сценария OAuth-линка нового удалённого vault'а.
* Результат сценария OAuth-линка нового удалённого хранилища.
*
* presentation сводит это к: успех → отдать `registration` в [VaultRegistrar.register];
* Слой presentation сводит это к: успех → отдать `registration` в [VaultRegistrar.register];
* cancel → ничего не делать; failure → показать сообщение.
*/
sealed interface VaultLinkOutcome {

View File

@@ -1,14 +1,14 @@
package com.github.nullptroma.wallenc.vault.contract
/**
* Маркер «полезной нагрузки» для регистрации удалённого vault'а через [VaultRegistrar].
* Маркер «полезной нагрузки» для регистрации удалённого хранилища через [VaultRegistrar].
*
* Намеренно НЕ sealed: конкретные реализации (`YandexRegistration`, …) живут в `:data`
* рядом с соответствующими реализациями vault'а, чтобы `:data` не разнесёшь по
* нескольким модулям без необходимости. Цена — отсутствие exhaustive-when через
* границу модуля, лечится fail-fast веткой `else` в `VaultsManager.register(...)`.
* рядом с соответствующими реализациями vault, чтобы не дробить модуль без нужды.
* Цена — отсутствие exhaustive-when через границу модуля; лечится fail-fast веткой
* `else` в `VaultsManager.register(...)`.
*
* presentation/app никогда не «открывают» этот тип — они только перепасовывают
* объект из [VaultLinkOutcome.Success] в [VaultRegistrar.register].
* Код приложения (presentation) не раскрывает этот тип — только передаёт экземпляр
* из [VaultLinkOutcome.Success] в [VaultRegistrar.register].
*/
interface VaultRegistration