Исправлено много варнингов
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
11
app/src/main/res/values/plurals.xml
Normal file
11
app/src/main/res/values/plurals.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
@@ -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,25 +127,21 @@ 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)
|
||||
object : FilterInputStream(stream) {
|
||||
override fun close() {
|
||||
try {
|
||||
stream.close()
|
||||
`in`.close()
|
||||
} finally {
|
||||
resp.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitOperation(href: String) {
|
||||
repeat(OPERATION_POLL_MAX) {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -3,9 +3,9 @@ package com.github.nullptroma.wallenc.domain.interfaces
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Минимальная идентификация vault'а.
|
||||
* Минимальная идентификация хранилища ([IVaultInfo]).
|
||||
*
|
||||
* Намеренно «голая»: domain ничего не знает о брендах, локальности или статусе —
|
||||
* Намеренно «голая»: доменный слой не знает о брендах, локальности или статусе —
|
||||
* вся категоризация лежит во внешнем кольце (`:vault-api: VaultDescriptor`).
|
||||
*/
|
||||
interface IVaultInfo {
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 через колбэк, чтобы корректно встроиться
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user