Исправлено много варнингов
This commit is contained in:
@@ -3,7 +3,9 @@ package com.github.nullptroma.wallenc.app.tasks
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -52,7 +54,14 @@ class TaskPipelineForegroundService : Service() {
|
|||||||
private val serviceJob = SupervisorJob()
|
private val serviceJob = SupervisorJob()
|
||||||
private val serviceScope = CoroutineScope(serviceJob + Dispatchers.Main.immediate)
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -133,6 +142,11 @@ class TaskPipelineForegroundService : Service() {
|
|||||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
|
.addAction(
|
||||||
|
0,
|
||||||
|
getString(R.string.task_notification_cancel),
|
||||||
|
cancelAllTasksPendingIntent(),
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun buildAccumulatedNotification(tasks: List<TaskForegroundItem>): Notification {
|
private fun buildAccumulatedNotification(tasks: List<TaskForegroundItem>): Notification {
|
||||||
@@ -141,19 +155,44 @@ class TaskPipelineForegroundService : Service() {
|
|||||||
applyNotificationTemplateTextColor(big)
|
applyNotificationTemplateTextColor(big)
|
||||||
bindTaskRows(big, sorted)
|
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)
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle(getString(R.string.task_notification_title))
|
.setContentTitle(getString(R.string.task_notification_title))
|
||||||
.setContentText(
|
.setContentText(collapsedSubtext)
|
||||||
getString(R.string.task_notification_group_subtext, sorted.size),
|
|
||||||
)
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
||||||
.setCustomBigContentView(big)
|
.setCustomBigContentView(big)
|
||||||
|
.addAction(
|
||||||
|
0,
|
||||||
|
getString(R.string.task_notification_cancel),
|
||||||
|
cancelAllTasksPendingIntent(),
|
||||||
|
)
|
||||||
.build()
|
.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]
|
* 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.
|
* 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])
|
showTaskRow(remoteViews, i, sorted[i])
|
||||||
}
|
}
|
||||||
val remaining = n - (MAX_TASK_ROWS - 1)
|
val remaining = n - (MAX_TASK_ROWS - 1)
|
||||||
showOverflowRow(remoteViews, MAX_TASK_ROWS - 1, remaining)
|
showOverflowRow(remoteViews, remaining)
|
||||||
}
|
}
|
||||||
advanceIndeterminateDotPhasesAfterBind(sorted, n)
|
advanceIndeterminateDotPhasesAfterBind(sorted, n)
|
||||||
}
|
}
|
||||||
@@ -254,14 +293,19 @@ class TaskPipelineForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showOverflowRow(remoteViews: RemoteViews, index: Int, remainingCount: Int) {
|
private fun showOverflowRow(remoteViews: RemoteViews, remainingCount: Int) {
|
||||||
remoteViews.setViewVisibility(TASK_ROW_IDS[index], View.VISIBLE)
|
val overflowIndex = MAX_TASK_ROWS - 1
|
||||||
|
remoteViews.setViewVisibility(TASK_ROW_IDS[overflowIndex], View.VISIBLE)
|
||||||
remoteViews.setTextViewText(
|
remoteViews.setTextViewText(
|
||||||
TASK_TITLE_IDS[index],
|
TASK_TITLE_IDS[overflowIndex],
|
||||||
getString(R.string.task_notification_more_tasks, remainingCount),
|
resources.getQuantityString(
|
||||||
|
R.plurals.task_notification_more_tasks,
|
||||||
|
remainingCount,
|
||||||
|
remainingCount,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[index], View.GONE)
|
remoteViews.setViewVisibility(TASK_LABEL_BAR_ROW_IDS[overflowIndex], View.GONE)
|
||||||
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[index], "")
|
remoteViews.setTextViewText(TASK_SUBTITLE_IDS[overflowIndex], "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun indeterminateSubtitleWithDots(taskId: TaskId, baseLabel: String): String {
|
private fun indeterminateSubtitleWithDots(taskId: TaskId, baseLabel: String): String {
|
||||||
@@ -276,6 +320,9 @@ class TaskPipelineForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 CHANNEL_ID = "wallenc_task_pipeline"
|
||||||
private const val FOREGROUND_NOTIFICATION_ID = 1001
|
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_preparing">Preparing…</string>
|
||||||
<string name="task_notification_indeterminate">Working…</string>
|
<string name="task_notification_indeterminate">Working…</string>
|
||||||
<string name="task_notification_cancel">Cancel</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>
|
</resources>
|
||||||
@@ -17,7 +17,7 @@ dependencies {
|
|||||||
implementation(libs.retrofit.converter.scalars)
|
implementation(libs.retrofit.converter.scalars)
|
||||||
implementation(libs.retrofit.converter.jackson)
|
implementation(libs.retrofit.converter.jackson)
|
||||||
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation(libs.okhttp3)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import java.time.Instant
|
|||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class DiskInfoDto(
|
data class DiskInfoDto(
|
||||||
@JsonProperty("trash_size") val trashSize: Long? = null,
|
@param:JsonProperty("trash_size") val trashSize: Long? = null,
|
||||||
@JsonProperty("total_space") val totalSpace: Long? = null,
|
@param:JsonProperty("total_space") val totalSpace: Long? = null,
|
||||||
@JsonProperty("used_space") val usedSpace: Long? = null,
|
@param:JsonProperty("used_space") val usedSpace: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -36,10 +36,10 @@ data class ResourceDto(
|
|||||||
val size: Long? = null,
|
val size: Long? = null,
|
||||||
val modified: Instant? = null,
|
val modified: Instant? = null,
|
||||||
val created: 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,
|
val md5: String? = null,
|
||||||
@JsonProperty("custom_properties") val customProperties: Map<String, Any?>? = null,
|
@param:JsonProperty("custom_properties") val customProperties: Map<String, Any?>? = null,
|
||||||
@JsonProperty("_embedded") val embedded: EmbeddedResourceListDto? = null,
|
@param:JsonProperty("_embedded") val embedded: EmbeddedResourceListDto? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -56,5 +56,5 @@ data class ApiErrorDto(
|
|||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class CustomPropertiesPatchDto(
|
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.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
@@ -21,6 +20,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import java.io.FilterInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
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 =
|
suspend fun setCustomProperties(path: String, props: Map<String, String>): Unit =
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
val resp = wrapAuth {
|
val resp = wrapAuth {
|
||||||
@@ -141,21 +127,17 @@ class YandexDiskRepository(
|
|||||||
resp.close()
|
resp.close()
|
||||||
throw IOException("Download failed: HTTP ${resp.code}")
|
throw IOException("Download failed: HTTP ${resp.code}")
|
||||||
}
|
}
|
||||||
val body = resp.body ?: run {
|
val body = resp.body
|
||||||
|
val stream = body?.byteStream() ?: run {
|
||||||
resp.close()
|
resp.close()
|
||||||
throw IOException("Download: empty body")
|
throw IOException("Download failed: missing body")
|
||||||
}
|
}
|
||||||
body.byteStream().let { stream ->
|
object : FilterInputStream(stream) {
|
||||||
object : InputStream() {
|
override fun close() {
|
||||||
override fun read(): Int = stream.read()
|
try {
|
||||||
override fun read(b: ByteArray): Int = stream.read(b)
|
`in`.close()
|
||||||
override fun read(b: ByteArray, off: Int, len: Int): Int = stream.read(b, off, len)
|
} finally {
|
||||||
override fun close() {
|
resp.close()
|
||||||
try {
|
|
||||||
stream.close()
|
|
||||||
} finally {
|
|
||||||
resp.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,16 +183,5 @@ class YandexDiskRepository(
|
|||||||
private val OCTET_STREAM = "application/octet-stream".toMediaType()
|
private val OCTET_STREAM = "application/octet-stream".toMediaType()
|
||||||
private const val OPERATION_POLL_DELAY_MS = 300L
|
private const val OPERATION_POLL_DELAY_MS = 300L
|
||||||
private const val OPERATION_POLL_MAX = 200
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Общий «скелет» storage'а: единая логика meta-info, rename, setEncInfo,
|
* Общий «скелет» для [IStorage]: единая логика meta-info, rename, setEncInfo,
|
||||||
* clearAllContent и делегирования размера/доступности к [accessor].
|
* clearAllContent и делегирование размеров и доступности в [accessor].
|
||||||
*
|
*
|
||||||
* Подклассы определяют только как создаётся [accessor], значение
|
* Подклассы определяют только способ создания [accessor], значение
|
||||||
* [isVirtualStorage] и (при необходимости) расширяют [init] своими шагами
|
* [isVirtualStorage] и (при необходимости) расширяют [init] своими шагами
|
||||||
* (например, проверкой ключа или инициализацией внешней связи).
|
* (например, проверкой ключа или инициализацией внешней связи).
|
||||||
*/
|
*/
|
||||||
@@ -60,8 +60,8 @@ abstract class BaseStorage(
|
|||||||
protected open fun metaInfoUuidPart(): String = uuid.toString()
|
protected open fun metaInfoUuidPart(): String = uuid.toString()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запускается единожды при старте storage'а. Подклассы могут переопределить,
|
* Запускается единожды при первом использовании хранилища. Подклассы могут переопределить,
|
||||||
* добавив свои шаги (init accessor'а, проверка ключа и т.п.). Обязательно
|
* добавив свои шаги (инициализацию [accessor], проверку ключа и т.п.). Обязательно
|
||||||
* должен в какой-то момент вызвать [readMetaInfo].
|
* должен в какой-то момент вызвать [readMetaInfo].
|
||||||
*/
|
*/
|
||||||
open suspend fun init() {
|
open suspend fun init() {
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ import java.time.Instant
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [IStorageAccessor] поверх папки приложения `app:/<storageUuid>/…` на Яндекс.Диске.
|
* Реализация [IStorageAccessor] для дерева файлов `app:/<storageUuid>/…` на Яндекс.Диске.
|
||||||
*
|
*
|
||||||
* [isAvailable] = доступность vault'а ([vaultAvailability]) **и** успешная локальная
|
* [isAvailable] объединяет доступность удалённого хранилища ([vaultAvailability])
|
||||||
* инициализация этого storage ([storageReady]).
|
* и успешную локальную инициализацию ([storageReady]) этого аксессора.
|
||||||
*/
|
*/
|
||||||
class YandexStorageAccessor(
|
class YandexStorageAccessor(
|
||||||
private val storageUuid: UUID,
|
private val storageUuid: UUID,
|
||||||
@@ -265,7 +265,7 @@ class YandexStorageAccessor(
|
|||||||
while (queue.isNotEmpty()) {
|
while (queue.isNotEmpty()) {
|
||||||
val rel = queue.removeFirst()
|
val rel = queue.removeFirst()
|
||||||
if (isSystemRel(rel)) continue
|
if (isSystemRel(rel)) continue
|
||||||
val (files, dirs) = listImmediateChildren(rel)
|
val (_, dirs) = listImmediateChildren(rel)
|
||||||
for (d in dirs) {
|
for (d in dirs) {
|
||||||
if (!isSystemRel(d.metaInfo.path)) {
|
if (!isSystemRel(d.metaInfo.path)) {
|
||||||
out.add(d)
|
out.add(d)
|
||||||
@@ -402,10 +402,12 @@ class YandexStorageAccessor(
|
|||||||
override suspend fun openWriteSystemFile(name: String): OutputStream = withContext(ioDispatcher) {
|
override suspend fun openWriteSystemFile(name: String): OutputStream = withContext(ioDispatcher) {
|
||||||
ensureSystemDirExists()
|
ensureSystemDirExists()
|
||||||
val rel = "/$SYSTEM_HIDDEN_DIRNAME/$name"
|
val rel = "/$SYSTEM_HIDDEN_DIRNAME/$name"
|
||||||
val baos = ByteArrayOutputStream()
|
val uploadBuffer = ByteArrayOutputStream()
|
||||||
baos.onClosed {
|
uploadBuffer.onClosed {
|
||||||
runBlocking(ioDispatcher) {
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class VaultsManager(
|
class VaultsManager(
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
localVault: IVault,
|
private val localVault: IVault,
|
||||||
keyRepo: StorageKeyMapStore,
|
keyRepo: StorageKeyMapStore,
|
||||||
private val yandexAccountStore: YandexAccountStore,
|
private val yandexAccountStore: YandexAccountStore,
|
||||||
private val yandexUserInfoRepository: YandexUserInfoRepository,
|
private val yandexUserInfoRepository: YandexUserInfoRepository,
|
||||||
@@ -40,8 +40,6 @@ class VaultsManager(
|
|||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
||||||
|
|
||||||
private val localVault: IVault = localVault
|
|
||||||
|
|
||||||
private val yandexVaults: StateFlow<List<IVault>> = yandexAccountStore.observeAll()
|
private val yandexVaults: StateFlow<List<IVault>> = yandexAccountStore.observeAll()
|
||||||
.map { rows ->
|
.map { rows ->
|
||||||
rows.map { row ->
|
rows.map { row ->
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import kotlin.io.path.pathString
|
|||||||
|
|
||||||
class LocalVault(
|
class LocalVault(
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
private val vaultRoot: File?,
|
vaultRoot: File?,
|
||||||
) : DescribedVault {
|
) : DescribedVault {
|
||||||
|
|
||||||
override val uuid: UUID = vaultRoot?.let { root ->
|
override val uuid: UUID = vaultRoot?.let { root ->
|
||||||
@@ -42,7 +42,7 @@ class LocalVault(
|
|||||||
private val _availableSpace = MutableStateFlow<Long?>(null)
|
private val _availableSpace = MutableStateFlow<Long?>(null)
|
||||||
override val availableSpace: StateFlow<Long?> = _availableSpace
|
override val availableSpace: StateFlow<Long?> = _availableSpace
|
||||||
|
|
||||||
private val path = MutableStateFlow<File?>(vaultRoot)
|
private val path = MutableStateFlow(vaultRoot)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
CoroutineScope(ioDispatcher).launch {
|
CoroutineScope(ioDispatcher).launch {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package com.github.nullptroma.wallenc.infrastructure.vaults.yandex
|
package com.github.nullptroma.wallenc.infrastructure.vaults.yandex
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
|
||||||
import com.github.nullptroma.wallenc.vault.contract.VaultRegistration
|
import com.github.nullptroma.wallenc.vault.contract.VaultRegistration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Регистрация удалённого vault'а Яндекс.Диска по результату OAuth.
|
* Регистрация удалённого хранилища Яндекс.Диска по результату OAuth.
|
||||||
*
|
*
|
||||||
* Живёт в `:data` (а не в `:vault-api`), потому что [VaultRegistration]
|
* Живёт в `:data` (а не в `:vault-api`), потому что [VaultRegistration]
|
||||||
* намеренно не sealed — конкретные реализации лежат рядом со своим поставщиком.
|
* намеренно не sealed — конкретные реализации лежат рядом со своим поставщиком.
|
||||||
* presentation никогда не открывает этот тип, только перепасовывает обратно
|
* Слой presentation не раскрывает этот тип, только передаёт его в `VaultRegistrar.register(...)`.
|
||||||
* в `VaultRegistrar.register(...)`.
|
|
||||||
*/
|
*/
|
||||||
data class YandexRegistration(
|
data class YandexRegistration(
|
||||||
val oauthToken: String,
|
val oauthToken: String,
|
||||||
@@ -17,6 +15,4 @@ data class YandexRegistration(
|
|||||||
init {
|
init {
|
||||||
require(oauthToken.isNotBlank()) { "oauthToken must not be blank" }
|
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)
|
_availableSpace.value = (total - used).coerceAtLeast(0L)
|
||||||
_vaultReachable.value = true
|
_vaultReachable.value = true
|
||||||
_storages.value = loadStoragesList()
|
_storages.value = loadStoragesList()
|
||||||
} catch (e: YandexDiskAuthException) {
|
} catch (_: YandexDiskAuthException) {
|
||||||
_vaultReachable.value = false
|
_vaultReachable.value = false
|
||||||
_storages.value = emptyList()
|
_storages.value = emptyList()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
_vaultReachable.value = false
|
_vaultReachable.value = false
|
||||||
_storages.value = emptyList()
|
_storages.value = emptyList()
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ class YandexVault(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
storage.init()
|
storage.init()
|
||||||
_storages.value = _storages.value + storage
|
_storages.value += storage
|
||||||
storage
|
storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Контракт vault'а: коллекция [IStorage] с реактивным состоянием.
|
* Контракт хранилища ([IVault]): коллекция [IStorage] с реактивным состоянием.
|
||||||
*
|
*
|
||||||
* domain не различает локальные/удалённые/Yandex/etc. — это общий порт.
|
* Слой domain не различает локальные/удалённые/Yandex и т.д. — это общий порт.
|
||||||
*/
|
*/
|
||||||
interface IVault : IVaultInfo {
|
interface IVault : IVaultInfo {
|
||||||
val storages: StateFlow<List<IStorage>>
|
val storages: StateFlow<List<IStorage>>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package com.github.nullptroma.wallenc.domain.interfaces
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Минимальная идентификация vault'а.
|
* Минимальная идентификация хранилища ([IVaultInfo]).
|
||||||
*
|
*
|
||||||
* Намеренно «голая»: domain ничего не знает о брендах, локальности или статусе —
|
* Намеренно «голая»: доменный слой не знает о брендах, локальности или статусе —
|
||||||
* вся категоризация лежит во внешнем кольце (`:vault-api: VaultDescriptor`).
|
* вся категоризация лежит во внешнем кольце (`:vault-api: VaultDescriptor`).
|
||||||
*/
|
*/
|
||||||
interface IVaultInfo {
|
interface IVaultInfo {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package com.github.nullptroma.wallenc.domain.interfaces
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Единая точка доступа ко всем vault'ам приложения.
|
* Единая точка доступа ко всем подключённым хранилищам приложения.
|
||||||
*
|
*
|
||||||
* domain не различает категории vault'ов — потребители (presentation) фильтруют
|
* Доменный слой не различает категории vault — потребители в UI (presentation)
|
||||||
* [vaults] через `:vault-api` (`VaultDescriptor`/`DescribedVault`).
|
* фильтруют [vaults] через `:vault-api` (`VaultDescriptor` / `DescribedVault`).
|
||||||
*/
|
*/
|
||||||
interface IVaultsManager {
|
interface IVaultsManager {
|
||||||
val vaults: StateFlow<List<IVault>>
|
val vaults: StateFlow<List<IVault>>
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.1.1"
|
agp = "9.1.1"
|
||||||
jacksonModuleKotlin = "2.21.2"
|
jacksonModuleKotlin = "2.21.3"
|
||||||
kotlin = "2.3.20"
|
kotlin = "2.3.21"
|
||||||
coreKtx = "1.18.0"
|
coreKtx = "1.18.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
kotlinReflect = "2.3.0"
|
kotlinReflect = "2.3.21"
|
||||||
kotlinxCoroutinesCore = "1.10.2"
|
kotlinxCoroutinesCore = "1.11.0"
|
||||||
kotlinxSerializationJson = "1.11.0"
|
kotlinxSerializationJson = "1.11.0"
|
||||||
lifecycleRuntimeKtx = "2.10.0"
|
lifecycleRuntimeKtx = "2.10.0"
|
||||||
activityCompose = "1.13.0"
|
activityCompose = "1.13.0"
|
||||||
composeBom = "2026.03.01"
|
composeBom = "2026.05.00"
|
||||||
navigation = "2.9.7"
|
navigation = "2.9.8"
|
||||||
hiltNavigation = "1.3.0"
|
hiltNavigation = "1.3.0"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
yandexAuthSdk = "3.2.0"
|
yandexAuthSdk = "3.2.0"
|
||||||
daggerHilt = "2.59.2"
|
daggerHilt = "2.59.2"
|
||||||
ksp = "2.3.6"
|
ksp = "2.3.7"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
retrofit = "3.0.0"
|
retrofit = "3.0.0"
|
||||||
|
okhttp = "5.3.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" }
|
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" }
|
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" }
|
||||||
|
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" }
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
|
|
||||||
# Yandex
|
# 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 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
retrofit-converter-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars", 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" }
|
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" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
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
|
#Sat Sep 07 01:04:14 MSK 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class RoomFactory(private val context: Context) {
|
|||||||
fun buildAppDb(): AppDb {
|
fun buildAppDb(): AppDb {
|
||||||
val room = Room.databaseBuilder(
|
val room = Room.databaseBuilder(
|
||||||
context, AppDb::class.java, "app-db"
|
context, AppDb::class.java, "app-db"
|
||||||
).fallbackToDestructiveMigration().build()
|
).fallbackToDestructiveMigration(dropAllTables = true).build()
|
||||||
return room
|
return room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ class StorageKeyMapRepository(
|
|||||||
dao.add(*dbModels.toTypedArray())
|
dao.add(*dbModels.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(vararg keymaps: StorageKeyMap) = withContext(ioDispatcher) {
|
override suspend fun delete(vararg values: StorageKeyMap) = withContext(ioDispatcher) {
|
||||||
val dbModels = keymaps.map { DbStorageKeyMap.fromModel(it) }
|
val dbModels = values.map { DbStorageKeyMap.fromModel(it) }
|
||||||
dao.delete(*dbModels.toTypedArray())
|
dao.delete(*dbModels.toTypedArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
|
||||||
|
|
||||||
// Hilt
|
// Hilt
|
||||||
implementation(libs.dagger.hilt)
|
implementation(libs.dagger.hilt)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
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.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
@@ -66,7 +66,8 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
|
|||||||
TaskPipelineRoute::class.qualifiedName!! to NavBarItemData(
|
TaskPipelineRoute::class.qualifiedName!! to NavBarItemData(
|
||||||
R.string.task_pipeline_title,
|
R.string.task_pipeline_title,
|
||||||
TaskPipelineRoute::class.qualifiedName!!,
|
TaskPipelineRoute::class.qualifiedName!!,
|
||||||
Icons.AutoMirrored.Rounded.List
|
Icons.AutoMirrored.Rounded.List,
|
||||||
|
R.string.task_pipeline_open,
|
||||||
),
|
),
|
||||||
SettingsRoute::class.qualifiedName!! to NavBarItemData(
|
SettingsRoute::class.qualifiedName!! to NavBarItemData(
|
||||||
R.string.nav_label_settings,
|
R.string.nav_label_settings,
|
||||||
@@ -89,7 +90,7 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
|
|||||||
icon = {
|
icon = {
|
||||||
if (navBarItemData.icon != null) Icon(
|
if (navBarItemData.icon != null) Icon(
|
||||||
navBarItemData.icon,
|
navBarItemData.icon,
|
||||||
contentDescription = stringResource(navBarItemData.nameStringResourceId)
|
contentDescription = stringResource(navBarItemData.iconContentDescriptionResourceId),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(navBarItemData.nameStringResourceId)) },
|
label = { Text(stringResource(navBarItemData.nameStringResourceId)) },
|
||||||
|
|||||||
@@ -2,4 +2,9 @@ package com.github.nullptroma.wallenc.ui.navigation
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.Modifier
|
||||||
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.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
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.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
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.ui.R
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
|
||||||
@@ -174,7 +174,13 @@ fun RemoteVaultsScreen(
|
|||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
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(
|
FilledTonalButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setAddChoiceVisible(false)
|
viewModel.setAddChoiceVisible(false)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LocalVaultScreen(
|
fun LocalVaultScreen(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.github.nullptroma.wallenc.vault.contract
|
package com.github.nullptroma.wallenc.vault.contract
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запуск OAuth-сценария привязки удалённого vault'а для конкретного [CloudBrand].
|
* Запуск OAuth-сценария привязки удалённого хранилища для конкретного [CloudBrand].
|
||||||
*
|
*
|
||||||
* Реализация в `:app` (привязана к Activity). presentation вызывает [beginLink]
|
* Реализация в `:app` (привязана к Activity). Слой presentation вызывает [beginLink]
|
||||||
* из контекста Compose-экрана и получает [VaultLinkOutcome] асинхронно.
|
* из контекста Compose-экрана и получает [VaultLinkOutcome] асинхронно.
|
||||||
*
|
*
|
||||||
* Намеренно императивный API через колбэк, чтобы корректно встроиться
|
* Намеренно императивный API через колбэк, чтобы корректно встроиться
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.github.nullptroma.wallenc.vault.contract
|
package com.github.nullptroma.wallenc.vault.contract
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Результат сценария OAuth-линка нового удалённого vault'а.
|
* Результат сценария OAuth-линка нового удалённого хранилища.
|
||||||
*
|
*
|
||||||
* presentation сводит это к: успех → отдать `registration` в [VaultRegistrar.register];
|
* Слой presentation сводит это к: успех → отдать `registration` в [VaultRegistrar.register];
|
||||||
* cancel → ничего не делать; failure → показать сообщение.
|
* cancel → ничего не делать; failure → показать сообщение.
|
||||||
*/
|
*/
|
||||||
sealed interface VaultLinkOutcome {
|
sealed interface VaultLinkOutcome {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.github.nullptroma.wallenc.vault.contract
|
package com.github.nullptroma.wallenc.vault.contract
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Маркер «полезной нагрузки» для регистрации удалённого vault'а через [VaultRegistrar].
|
* Маркер «полезной нагрузки» для регистрации удалённого хранилища через [VaultRegistrar].
|
||||||
*
|
*
|
||||||
* Намеренно НЕ sealed: конкретные реализации (`YandexRegistration`, …) живут в `:data`
|
* Намеренно НЕ sealed: конкретные реализации (`YandexRegistration`, …) живут в `:data`
|
||||||
* рядом с соответствующими реализациями vault'а, чтобы `:data` не разнесёшь по
|
* рядом с соответствующими реализациями vault, чтобы не дробить модуль без нужды.
|
||||||
* нескольким модулям без необходимости. Цена — отсутствие exhaustive-when через
|
* Цена — отсутствие exhaustive-when через границу модуля; лечится fail-fast веткой
|
||||||
* границу модуля, лечится fail-fast веткой `else` в `VaultsManager.register(...)`.
|
* `else` в `VaultsManager.register(...)`.
|
||||||
*
|
*
|
||||||
* presentation/app никогда не «открывают» этот тип — они только перепасовывают
|
* Код приложения (presentation) не раскрывает этот тип — только передаёт экземпляр
|
||||||
* объект из [VaultLinkOutcome.Success] в [VaultRegistrar.register].
|
* из [VaultLinkOutcome.Success] в [VaultRegistrar.register].
|
||||||
*/
|
*/
|
||||||
interface VaultRegistration
|
interface VaultRegistration
|
||||||
|
|||||||
Reference in New Issue
Block a user