mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-04 16:34:29 +02:00
feat(app/ui): tasks root media preview
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
parent
ae15ad7ce9
commit
e45e96908b
@ -1,26 +1,35 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.pages
|
package me.rhunk.snapenhance.ui.manager.pages
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import coil.request.ImageRequest
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -32,13 +41,10 @@ import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
|
|||||||
import me.rhunk.snapenhance.common.util.ktx.longHashCode
|
import me.rhunk.snapenhance.common.util.ktx.longHashCode
|
||||||
import me.rhunk.snapenhance.download.DownloadProcessor
|
import me.rhunk.snapenhance.download.DownloadProcessor
|
||||||
import me.rhunk.snapenhance.download.FFMpegProcessor
|
import me.rhunk.snapenhance.download.FFMpegProcessor
|
||||||
import me.rhunk.snapenhance.task.PendingTask
|
import me.rhunk.snapenhance.task.*
|
||||||
import me.rhunk.snapenhance.task.PendingTaskListener
|
|
||||||
import me.rhunk.snapenhance.task.Task
|
|
||||||
import me.rhunk.snapenhance.task.TaskStatus
|
|
||||||
import me.rhunk.snapenhance.task.TaskType
|
|
||||||
import me.rhunk.snapenhance.ui.manager.Routes
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.OnLifecycleEvent
|
import me.rhunk.snapenhance.ui.util.OnLifecycleEvent
|
||||||
|
import me.rhunk.snapenhance.ui.util.coil.cacheKey
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
@ -138,29 +144,6 @@ class TasksRootSection : Routes.Route() {
|
|||||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (taskSelection.size == 1) {
|
|
||||||
val selectionExists by rememberAsyncMutableState(defaultValue = false) {
|
|
||||||
taskSelection.firstOrNull()?.second?.exists() == true
|
|
||||||
}
|
|
||||||
if (selectionExists) {
|
|
||||||
taskSelection.firstOrNull()?.second?.let { documentFile ->
|
|
||||||
IconButton(onClick = {
|
|
||||||
runCatching {
|
|
||||||
context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(documentFile.uri, documentFile.type)
|
|
||||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
})
|
|
||||||
taskSelection.clear()
|
|
||||||
}.onFailure {
|
|
||||||
context.log.error("Failed to open file ${taskSelection.first().second}", it)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.OpenInNew, contentDescription = "Open")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskSelection.size > 1) {
|
if (taskSelection.size > 1) {
|
||||||
val canMergeSelection by rememberAsyncMutableState(defaultValue = false, keys = arrayOf(taskSelection.size)) {
|
val canMergeSelection by rememberAsyncMutableState(defaultValue = false, keys = arrayOf(taskSelection.size)) {
|
||||||
taskSelection.all { it.second?.type?.contains("video") == true }
|
taskSelection.all { it.second?.type?.contains("video") == true }
|
||||||
@ -305,13 +288,45 @@ class TasksRootSection : Routes.Route() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleSelection() {
|
||||||
|
if (isSelected) {
|
||||||
|
taskSelection.removeIf { it.first == task }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskSelection.add(task to documentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openFile() {
|
||||||
|
if (!isDocumentFileReadable || documentFile == null) return
|
||||||
|
runCatching {
|
||||||
|
context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(documentFile!!.uri, documentFile!!.type)
|
||||||
|
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
})
|
||||||
|
}.onFailure {
|
||||||
|
context.log.error("Failed to open file ${documentFile?.uri}", it)
|
||||||
|
context.shortToast(translation["failed_to_open_file"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedCard(modifier = modifier
|
OutlinedCard(modifier = modifier
|
||||||
.clickable {
|
.pointerInput(Unit) {
|
||||||
if (isSelected) {
|
detectTapGestures(
|
||||||
taskSelection.removeIf { it.first == task }
|
onTap = {
|
||||||
return@clickable
|
if (taskSelection.size > 0) {
|
||||||
}
|
toggleSelection()
|
||||||
taskSelection.add(task to documentFile)
|
return@detectTapGestures
|
||||||
|
}
|
||||||
|
openFile()
|
||||||
|
},
|
||||||
|
onLongPress = {
|
||||||
|
if (taskSelection.size > 0) {
|
||||||
|
openFile()
|
||||||
|
return@detectTapGestures
|
||||||
|
}
|
||||||
|
toggleSelection()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.let {
|
.let {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
@ -319,21 +334,46 @@ class TasksRootSection : Routes.Route() {
|
|||||||
.border(2.dp, MaterialTheme.colorScheme.primary)
|
.border(2.dp, MaterialTheme.colorScheme.primary)
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
} else it
|
} else it
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(15.dp),
|
modifier = Modifier.padding(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier.padding(end = 15.dp)
|
modifier = Modifier
|
||||||
|
.padding(end = 15.dp)
|
||||||
|
.size(50.dp)
|
||||||
|
.clipToBounds(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
var loadFailed by remember { mutableStateOf(false) }
|
||||||
documentFile?.let {
|
documentFile?.let {
|
||||||
when {
|
if (taskStatus.isFinalStage() && isDocumentFileReadable && !loadFailed && (documentFileMimeType.contains("image") || documentFileMimeType.contains("video"))) {
|
||||||
!isDocumentFileReadable -> Icon(Icons.Filled.DeleteOutline, contentDescription = "File not found")
|
Image(
|
||||||
documentFileMimeType.contains("image") -> Icon(Icons.Filled.Image, contentDescription = "Image")
|
painter = rememberAsyncImagePainter(
|
||||||
documentFileMimeType.contains("video") -> Icon(Icons.Filled.Videocam, contentDescription = "Video")
|
model = ImageRequest.Builder(context.androidContext)
|
||||||
documentFileMimeType.contains("audio") -> Icon(Icons.Filled.MusicNote, contentDescription = "Audio")
|
.data(it.uri)
|
||||||
else -> Icon(Icons.Filled.FileCopy, contentDescription = "File")
|
.cacheKey(it.uri.toString())
|
||||||
|
.placeholder(ColorDrawable(MaterialTheme.colorScheme.surfaceVariant.toArgb()))
|
||||||
|
.build(),
|
||||||
|
imageLoader = context.imageLoader,
|
||||||
|
onError = { loadFailed = true }
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(50.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
when {
|
||||||
|
!isDocumentFileReadable -> Icon(Icons.Filled.DeleteOutline, contentDescription = "File not found")
|
||||||
|
documentFileMimeType.contains("image") -> Icon(Icons.Filled.Image, contentDescription = "Image")
|
||||||
|
documentFileMimeType.contains("video") -> Icon(Icons.Filled.Videocam, contentDescription = "Video")
|
||||||
|
documentFileMimeType.contains("audio") -> Icon(Icons.Filled.MusicNote, contentDescription = "Audio")
|
||||||
|
else -> Icon(Icons.Filled.FileCopy, contentDescription = "File")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
when (task.type) {
|
when (task.type) {
|
||||||
@ -409,12 +449,12 @@ class TasksRootSection : Routes.Route() {
|
|||||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
recentTasks = remember { mutableStateListOf() }
|
recentTasks = rememberSaveable { mutableStateListOf() }
|
||||||
var lastFetchedTaskId: Long? by remember { mutableStateOf(null) }
|
var lastFetchedTaskId: Long? by rememberSaveable { mutableStateOf(null) }
|
||||||
|
|
||||||
fun fetchNewRecentTasks() {
|
fun fetchNewRecentTasks() {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val tasks = context.taskManager.fetchStoredTasks(lastFetchedTaskId ?: Long.MAX_VALUE)
|
val tasks = context.taskManager.fetchStoredTasks(lastFetchedTaskId ?: Long.MAX_VALUE, limit = 20)
|
||||||
if (tasks.isNotEmpty()) {
|
if (tasks.isNotEmpty()) {
|
||||||
lastFetchedTaskId = tasks.keys.last()
|
lastFetchedTaskId = tasks.keys.last()
|
||||||
val activeTaskIds = activeTasks.map { it.taskId }
|
val activeTaskIds = activeTasks.map { it.taskId }
|
||||||
@ -464,7 +504,7 @@ class TasksRootSection : Routes.Route() {
|
|||||||
TaskCard(modifier = Modifier.padding(8.dp), task)
|
TaskCard(modifier = Modifier.padding(8.dp), task)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
LaunchedEffect(remember { derivedStateOf { scrollState.firstVisibleItemIndex } }) {
|
LaunchedEffect(remember { derivedStateOf { scrollState.firstVisibleItemIndex } }) {
|
||||||
fetchNewRecentTasks()
|
fetchNewRecentTasks()
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@
|
|||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"no_tasks": "No tasks",
|
"no_tasks": "No tasks",
|
||||||
|
"failed_to_open_file": "Failed to open file",
|
||||||
"merge_files_toast": "Merging {count} files",
|
"merge_files_toast": "Merging {count} files",
|
||||||
"remove_selected_tasks_title": "Are you sure you want to remove selected tasks?",
|
"remove_selected_tasks_title": "Are you sure you want to remove selected tasks?",
|
||||||
"remove_all_tasks_title": "Are you sure you want to remove all tasks?",
|
"remove_all_tasks_title": "Are you sure you want to remove all tasks?",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user