Добавлен foreground сервис
This commit is contained in:
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.List
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -34,6 +35,8 @@ import com.github.nullptroma.wallenc.presentation.navigation.rememberNavigationS
|
||||
import com.github.nullptroma.wallenc.presentation.screens.main.MainRoute
|
||||
import com.github.nullptroma.wallenc.presentation.screens.main.MainScreen
|
||||
import com.github.nullptroma.wallenc.presentation.screens.main.MainViewModel
|
||||
import com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks.TaskPipelineScreen
|
||||
import com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks.TaskPipelineRoute
|
||||
import com.github.nullptroma.wallenc.presentation.screens.settings.SettingsRoute
|
||||
import com.github.nullptroma.wallenc.presentation.screens.settings.SettingsScreen
|
||||
import com.github.nullptroma.wallenc.presentation.screens.settings.SettingsViewModel
|
||||
@@ -65,6 +68,11 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
|
||||
MainRoute::class.qualifiedName!! to NavBarItemData(
|
||||
R.string.nav_label_main, MainRoute::class.qualifiedName!!, Icons.Rounded.Menu
|
||||
),
|
||||
TaskPipelineRoute::class.qualifiedName!! to NavBarItemData(
|
||||
R.string.task_pipeline_title,
|
||||
TaskPipelineRoute::class.qualifiedName!!,
|
||||
Icons.AutoMirrored.Rounded.List
|
||||
),
|
||||
SettingsRoute::class.qualifiedName!! to NavBarItemData(
|
||||
R.string.nav_label_settings,
|
||||
SettingsRoute::class.qualifiedName!!,
|
||||
@@ -125,6 +133,15 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
|
||||
}) {
|
||||
SettingsScreen(Modifier.padding(innerPaddings), settingsViewModel)
|
||||
}
|
||||
composable<TaskPipelineRoute>(enterTransition = {
|
||||
fadeIn(tween(200))
|
||||
}, exitTransition = {
|
||||
fadeOut(tween(200))
|
||||
}) {
|
||||
TaskPipelineScreen(
|
||||
modifier = Modifier.padding(innerPaddings)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import com.github.nullptroma.wallenc.presentation.screens.ScreenRoute
|
||||
import com.github.nullptroma.wallenc.presentation.screens.main.MainRoute
|
||||
import com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks.TaskPipelineRoute
|
||||
import com.github.nullptroma.wallenc.presentation.screens.settings.SettingsRoute
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlin.collections.set
|
||||
@@ -18,6 +19,7 @@ class WallencViewModel @javax.inject.Inject constructor(savedStateHandle: SavedS
|
||||
mutableStateOf(
|
||||
mapOf<String, ScreenRoute>(
|
||||
MainRoute::class.qualifiedName!! to MainRoute(),
|
||||
TaskPipelineRoute::class.qualifiedName!! to TaskPipelineRoute(),
|
||||
SettingsRoute::class.qualifiedName!! to SettingsRoute()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -101,10 +101,11 @@ fun MainScreen(
|
||||
val route: LocalVaultRoute = it.toRoute()
|
||||
LocalVaultScreen(
|
||||
modifier = Modifier.padding(innerPaddings),
|
||||
viewModel = localVaultViewModel
|
||||
) { text ->
|
||||
navState.push(TextEditRoute(text))
|
||||
}
|
||||
viewModel = localVaultViewModel,
|
||||
openTextEdit = { text ->
|
||||
navState.push(TextEditRoute(text))
|
||||
},
|
||||
)
|
||||
}
|
||||
composable<RemoteVaultsRoute>(enterTransition = {
|
||||
fadeIn(tween(200))
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -38,7 +37,7 @@ import kotlinx.coroutines.flow.collect
|
||||
fun LocalVaultScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: LocalVaultViewModel = hiltViewModel(),
|
||||
openTextEdit: (String) -> Unit
|
||||
openTextEdit: (String) -> Unit,
|
||||
) {
|
||||
|
||||
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
||||
@@ -50,7 +49,10 @@ fun LocalVaultScreen(
|
||||
}
|
||||
|
||||
Box {
|
||||
Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
viewModel.createStorage()
|
||||
|
||||
@@ -13,6 +13,9 @@ import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUse
|
||||
import com.github.nullptroma.wallenc.domain.usecases.RemoveStorageUseCase
|
||||
import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase
|
||||
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.PipelineWork
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.presentation.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -32,6 +35,7 @@ class LocalVaultViewModel @Inject constructor(
|
||||
private val storageFileManagementUseCase: StorageFileManagementUseCase,
|
||||
private val manageStoragesEncryptionUseCase: ManageStoragesEncryptionUseCase,
|
||||
private val renameStorageUseCase: RenameStorageUseCase,
|
||||
private val taskOrchestrator: ITaskOrchestrator,
|
||||
private val logger: ILogger
|
||||
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf(), true)) {
|
||||
private val _messages = MutableSharedFlow<String>()
|
||||
@@ -111,11 +115,15 @@ class LocalVaultViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun createStorage() {
|
||||
tasksCount++
|
||||
viewModelScope.launch {
|
||||
manageLocalVaultUseCase.createStorage()
|
||||
tasksCount--
|
||||
}
|
||||
taskOrchestrator.enqueue(
|
||||
title = "Create storage",
|
||||
requiresForeground = false,
|
||||
work = PipelineWork { ctx ->
|
||||
ctx.log(TaskLogLevel.Info, "Creating storage…")
|
||||
manageLocalVaultUseCase.createStorage()
|
||||
ctx.log(TaskLogLevel.Info, "Storage created")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private val runningStorages = mutableSetOf<java.util.UUID>()
|
||||
@@ -186,14 +194,23 @@ class LocalVaultViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun disableEncryption(storage: IStorageInfo) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
manageStoragesEncryptionUseCase.disableEncryption(storage)
|
||||
_messages.emit("Encryption disabled")
|
||||
} catch (e: Exception) {
|
||||
_messages.emit(e.message ?: "Failed to disable encryption")
|
||||
}
|
||||
}
|
||||
taskOrchestrator.enqueue(
|
||||
title = "Disable encryption",
|
||||
requiresForeground = true,
|
||||
work = PipelineWork { ctx ->
|
||||
try {
|
||||
ctx.log(TaskLogLevel.Info, "Disabling encryption…")
|
||||
manageStoragesEncryptionUseCase.clearAndDisableEncryption(storage) { p ->
|
||||
ctx.reportProgress(p)
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, "Encryption disabled")
|
||||
_messages.emit("Encryption disabled")
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed")
|
||||
_messages.emit(e.message ?: "Failed to disable encryption")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun rename(storage: IStorageInfo, newName: String) {
|
||||
@@ -203,9 +220,19 @@ class LocalVaultViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun remove(storage: IStorageInfo) {
|
||||
viewModelScope.launch {
|
||||
removeStorageUseCase.remove(storage)
|
||||
}
|
||||
taskOrchestrator.enqueue(
|
||||
title = "Remove storage",
|
||||
requiresForeground = true,
|
||||
work = PipelineWork { ctx ->
|
||||
try {
|
||||
ctx.log(TaskLogLevel.Info, "Removing storage…")
|
||||
removeStorageUseCase.remove(storage)
|
||||
ctx.log(TaskLogLevel.Info, "Removed")
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Remove failed")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun getStorageStatus(storage: IStorageInfo): String {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks
|
||||
|
||||
import com.github.nullptroma.wallenc.presentation.screens.ScreenRoute
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
class TaskPipelineRoute : ScreenRoute()
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.presentation.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TaskPipelineScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: TaskPipelineViewModel = hiltViewModel(),
|
||||
) {
|
||||
val pipeline by viewModel.orchestrator.pipelineState.collectAsStateWithLifecycle()
|
||||
val logs by viewModel.orchestrator.logLines.collectAsStateWithLifecycle()
|
||||
val hasAnyTask = pipeline.tasks.isNotEmpty()
|
||||
val currentTask = pipeline.tasks.firstOrNull { it.id == pipeline.currentTaskId }
|
||||
var showTestDialog by remember { mutableStateOf(false) }
|
||||
var testDurationSec by remember { mutableFloatStateOf(10f) }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
title = { Text(stringResource(R.string.task_pipeline_title)) },
|
||||
)
|
||||
},
|
||||
) { inner ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(inner)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Button(onClick = { showTestDialog = true }) {
|
||||
Text(stringResource(R.string.task_pipeline_run_test))
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.task_pipeline_current_task),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
if (currentTask == null) {
|
||||
Text(
|
||||
stringResource(R.string.task_pipeline_no_current_task),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
} else {
|
||||
TaskRow(task = currentTask, isCurrent = true)
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = { viewModel.orchestrator.cancelCurrent() },
|
||||
enabled = currentTask != null,
|
||||
) {
|
||||
Text(stringResource(R.string.task_pipeline_cancel_current))
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.orchestrator.cancelAll() },
|
||||
enabled = hasAnyTask,
|
||||
) {
|
||||
Text(stringResource(R.string.task_pipeline_cancel_all))
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.task_pipeline_jobs),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(pipeline.tasks, key = { it.id.uuid }) { task ->
|
||||
TaskRow(task = task, isCurrent = task.id == pipeline.currentTaskId)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.task_pipeline_log),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(logs.size) { i ->
|
||||
val line = logs[i]
|
||||
val prefix = when (line.level) {
|
||||
TaskLogLevel.Debug -> "D"
|
||||
TaskLogLevel.Info -> "I"
|
||||
TaskLogLevel.Warn -> "W"
|
||||
TaskLogLevel.Error -> "E"
|
||||
}
|
||||
Text(
|
||||
"[$prefix] ${line.message}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showTestDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showTestDialog = false },
|
||||
title = { Text(stringResource(R.string.task_pipeline_test_dialog_title)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.task_pipeline_test_dialog_duration,
|
||||
testDurationSec.toInt(),
|
||||
)
|
||||
)
|
||||
Slider(
|
||||
value = testDurationSec,
|
||||
onValueChange = { testDurationSec = it },
|
||||
valueRange = 0f..60f,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.startTestTask(testDurationSec.toInt())
|
||||
showTestDialog = false
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.task_pipeline_test_dialog_start))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showTestDialog = false }) {
|
||||
Text(stringResource(R.string.task_pipeline_test_dialog_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TaskRow(task: PipelineTask, isCurrent: Boolean) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
task.title,
|
||||
style = if (isCurrent) MaterialTheme.typography.titleSmall
|
||||
else MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
val stateLabel = when (val s = task.state) {
|
||||
TaskRunState.Queued -> stringResource(R.string.task_state_queued)
|
||||
is TaskRunState.Running -> stringResource(R.string.task_state_running)
|
||||
TaskRunState.Completed -> stringResource(R.string.task_state_completed)
|
||||
TaskRunState.Cancelled -> stringResource(R.string.task_state_cancelled)
|
||||
is TaskRunState.Failed -> stringResource(R.string.task_state_failed, s.message)
|
||||
}
|
||||
Text(stateLabel, style = MaterialTheme.typography.bodySmall)
|
||||
if (task.state is TaskRunState.Running) {
|
||||
val frac = (task.state as TaskRunState.Running).progress?.fraction
|
||||
if (frac != null) {
|
||||
LinearProgressIndicator(
|
||||
progress = { frac },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp),
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.tasks
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.PipelineWork
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TaskPipelineViewModel @Inject constructor(
|
||||
val orchestrator: ITaskOrchestrator,
|
||||
) : ViewModel() {
|
||||
|
||||
fun startTestTask(durationSec: Int) {
|
||||
val safeDurationSec = durationSec.coerceIn(0, 60)
|
||||
orchestrator.enqueue(
|
||||
title = "Test task (${safeDurationSec}s)",
|
||||
requiresForeground = true,
|
||||
work = PipelineWork { ctx ->
|
||||
val steps = if (safeDurationSec == 0) 1 else safeDurationSec * 10
|
||||
ctx.log(TaskLogLevel.Info, "Test task started for ${safeDurationSec}s")
|
||||
for (step in 0..steps) {
|
||||
val fraction = step.toFloat() / steps.toFloat()
|
||||
val elapsedMs = (fraction * safeDurationSec * 1000).toInt()
|
||||
ctx.reportProgress(
|
||||
fraction = fraction,
|
||||
label = "Elapsed: ${elapsedMs / 1000}s / ${safeDurationSec}s",
|
||||
)
|
||||
if (step < steps) delay(100)
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, "Test task finished")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,23 @@
|
||||
<string name="remove_confirmation_dialog">Delete storage "%1$s"?</string>
|
||||
<string name="storage_lock_actions">Storage encryption actions</string>
|
||||
|
||||
<string name="task_pipeline_title">Task pipeline</string>
|
||||
<string name="task_pipeline_jobs">Jobs</string>
|
||||
<string name="task_pipeline_log">Log</string>
|
||||
<string name="task_pipeline_cancel_current">Cancel current</string>
|
||||
<string name="task_pipeline_cancel_all">Cancel all</string>
|
||||
<string name="task_pipeline_open">Open task pipeline</string>
|
||||
<string name="task_pipeline_run_test">Run test task</string>
|
||||
<string name="task_pipeline_current_task">Current task</string>
|
||||
<string name="task_pipeline_no_current_task">No running task</string>
|
||||
<string name="task_pipeline_test_dialog_title">Test task setup</string>
|
||||
<string name="task_pipeline_test_dialog_duration">Duration: %1$d s</string>
|
||||
<string name="task_pipeline_test_dialog_start">Start</string>
|
||||
<string name="task_pipeline_test_dialog_cancel">Cancel</string>
|
||||
<string name="task_state_queued">Queued</string>
|
||||
<string name="task_state_running">Running</string>
|
||||
<string name="task_state_completed">Completed</string>
|
||||
<string name="task_state_cancelled">Cancelled</string>
|
||||
<string name="task_state_failed">Failed: %1$s</string>
|
||||
|
||||
</resources>
|
||||
Reference in New Issue
Block a user