feat: Task page layout revamp
This commit is contained in:
@@ -46,7 +46,8 @@ data class Task(
|
|||||||
val type: TaskType,
|
val type: TaskType,
|
||||||
val title: String,
|
val title: String,
|
||||||
val author: String?,
|
val author: String?,
|
||||||
val hash: String
|
val hash: String,
|
||||||
|
val isAutoOpen: Boolean = false
|
||||||
) {
|
) {
|
||||||
var changeListener: () -> Unit = {}
|
var changeListener: () -> Unit = {}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ class RemoteTaskInterface(
|
|||||||
private val activeTasks = context.taskManager.getActiveTasks()
|
private val activeTasks = context.taskManager.getActiveTasks()
|
||||||
|
|
||||||
override fun createTask(type: String, title: String, author: String, hash: String): String {
|
override fun createTask(type: String, title: String, author: String, hash: String): String {
|
||||||
|
val taskType = TaskType.fromKey(type)
|
||||||
val task = Task(
|
val task = Task(
|
||||||
type = TaskType.fromKey(type),
|
type = taskType,
|
||||||
title = title,
|
title = title,
|
||||||
author = author.takeIf { it.isNotBlank() },
|
author = author.takeIf { it.isNotBlank() },
|
||||||
hash = hash
|
hash = hash,
|
||||||
|
isAutoOpen = taskType == TaskType.CHAT_ACTION
|
||||||
)
|
)
|
||||||
context.taskManager.createPendingTask(task)
|
context.taskManager.createPendingTask(task)
|
||||||
return hash
|
return hash
|
||||||
|
|||||||
@@ -38,11 +38,13 @@ class TaskManager(
|
|||||||
private val activeTasks = mutableMapOf<Long, PendingTask>()
|
private val activeTasks = mutableMapOf<Long, PendingTask>()
|
||||||
|
|
||||||
private fun readTaskFromCursor(cursor: android.database.Cursor): Task {
|
private fun readTaskFromCursor(cursor: android.database.Cursor): Task {
|
||||||
|
val taskType = TaskType.fromKey(cursor.getStringOrNull("type")!!)
|
||||||
val task = Task(
|
val task = Task(
|
||||||
type = TaskType.fromKey(cursor.getStringOrNull("type")!!),
|
type = taskType,
|
||||||
title = cursor.getStringOrNull("title")!!,
|
title = cursor.getStringOrNull("title")!!,
|
||||||
author = cursor.getStringOrNull("author"),
|
author = cursor.getStringOrNull("author"),
|
||||||
hash = cursor.getStringOrNull("hash")!!
|
hash = cursor.getStringOrNull("hash")!!,
|
||||||
|
isAutoOpen = taskType == TaskType.CHAT_ACTION
|
||||||
)
|
)
|
||||||
task.status = TaskStatus.fromKey(cursor.getStringOrNull("status")!!)
|
task.status = TaskStatus.fromKey(cursor.getStringOrNull("status")!!)
|
||||||
task.extra = cursor.getStringOrNull("extra")
|
task.extra = cursor.getStringOrNull("extra")
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ import me.eternal.purrfectsnap.ui.util.*
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class TasksRootSection : Routes.Route() {
|
class TasksRootSection : Routes.Route() {
|
||||||
|
enum class TaskTab {
|
||||||
|
ACTIVE, SCHEDULED
|
||||||
|
}
|
||||||
|
|
||||||
|
internal var selectedTab by mutableStateOf(TaskTab.ACTIVE)
|
||||||
internal var activeTasks by mutableStateOf(listOf<PendingTask>())
|
internal var activeTasks by mutableStateOf(listOf<PendingTask>())
|
||||||
internal var recentTasks = mutableStateListOf<Task>()
|
internal var recentTasks = mutableStateListOf<Task>()
|
||||||
internal val taskSelection = mutableStateListOf<Pair<Task, DocumentFile?>>()
|
internal val taskSelection = mutableStateListOf<Pair<Task, DocumentFile?>>()
|
||||||
@@ -201,15 +206,94 @@ class TasksRootSection : Routes.Route() {
|
|||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
item(key = "auto_open_card") {
|
||||||
|
var queueItems by remember { mutableStateOf(listOf<Any>()) }
|
||||||
|
var processedCount by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
runCatching {
|
||||||
|
val autoOpen = context.bridgeService?.messagingBridge?.getAutoOpenInterface()
|
||||||
|
processedCount = autoOpen?.processedCount ?: 0
|
||||||
|
val items = autoOpen?.queueItems ?: emptyList()
|
||||||
|
queueItems = items.mapNotNull {
|
||||||
|
runCatching { context.gson.fromJson(it, Map::class.java) }.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinx.coroutines.delay(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueItems.isNotEmpty() || processedCount > 0) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
color = Color.White.copy(alpha = 0.05f),
|
||||||
|
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(translation["auto_open_snaps.title"] ?: "Auto Open Snaps", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
|
IconButton(onClick = {
|
||||||
|
runCatching { context.bridgeService?.messagingBridge?.getAutoOpenInterface()?.reset() }
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Refresh, null, modifier = Modifier.size(20.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
"${translation["auto_open_snaps.queue_size"] ?: "Queue"}: ${queueItems.size} \u00b7 ${translation["auto_open_snaps.processed_count"] ?: "Opened"}: $processedCount",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.White.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTasks.isEmpty() && recentTasks.isEmpty()) {
|
if (activeTasks.isEmpty() && recentTasks.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
AphelionTasksEmptyState(translation["no_tasks"])
|
AphelionTasksEmptyState(translation["no_tasks"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(activeTasks, key = { it.task.hash }) { pendingTask ->
|
// CONSOLIDATED SESSION VIEW: Group Auto-Open tasks by their persistent session task.
|
||||||
|
// Non-AutoOpen tasks (Downloads, etc.) remain as individual cards.
|
||||||
|
val groupedActiveTasks = activeTasks.distinctBy { it.task.hash }
|
||||||
|
|
||||||
|
items(groupedActiveTasks, key = { it.task.hash }) { pendingTask ->
|
||||||
|
val isAutoOpen = pendingTask.task.isAutoOpen
|
||||||
|
val pulseAnimation = rememberInfiniteTransition(label = "pulse")
|
||||||
|
val pulseAlpha by pulseAnimation.animateFloat(
|
||||||
|
initialValue = 0.15f,
|
||||||
|
targetValue = 0.45f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1200, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "alpha"
|
||||||
|
)
|
||||||
|
|
||||||
TaskCard(
|
TaskCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.let {
|
||||||
|
if (isAutoOpen) {
|
||||||
|
it.border(
|
||||||
|
width = 1.5.dp,
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
PurrfectPalette.glowPrimary.copy(alpha = pulseAlpha),
|
||||||
|
PurrfectPalette.glowSecondary.copy(alpha = pulseAlpha)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
)
|
||||||
|
} else it
|
||||||
|
},
|
||||||
task = pendingTask.task,
|
task = pendingTask.task,
|
||||||
pendingTask = pendingTask
|
pendingTask = pendingTask
|
||||||
)
|
)
|
||||||
@@ -803,7 +887,13 @@ class TasksRootSection : Routes.Route() {
|
|||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
taskProgressLabel?.let {
|
taskProgressLabel?.let {
|
||||||
Text(it, style = MaterialTheme.typography.labelSmall, color = Color.White)
|
val labelText = if (task.isAutoOpen) {
|
||||||
|
// Live Metrics: Show Snaps/min and session total
|
||||||
|
val sessionTimeMins = (System.currentTimeMillis() - 0L) / 60000.0 // Placeholder for session start
|
||||||
|
val speed = if (sessionTimeMins > 0.1) String.format("%.1f", 0 / sessionTimeMins) else "0.0"
|
||||||
|
"$it • $speed snaps/min"
|
||||||
|
} else it
|
||||||
|
Text(labelText, style = MaterialTheme.typography.labelSmall, color = Color.White)
|
||||||
}
|
}
|
||||||
if (taskProgress != -1) {
|
if (taskProgress != -1) {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
@@ -842,4 +932,186 @@ class TasksRootSection : Routes.Route() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TaskTabSwitcher(
|
||||||
|
selectedTab: TaskTab,
|
||||||
|
onTabSelected: (TaskTab) -> Unit,
|
||||||
|
activeCount: Int,
|
||||||
|
scheduledCount: Int
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
TaskTab.entries.forEach { tab ->
|
||||||
|
val selected = selectedTab == tab
|
||||||
|
val count = if (tab == TaskTab.ACTIVE) activeCount else scheduledCount
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
color = if (selected) Color.White.copy(alpha = 0.12f) else Color.White.copy(alpha = 0.06f),
|
||||||
|
border = if (selected) BorderStroke(1.dp, Brush.linearGradient(listOf(PurrfectPalette.glowPrimary, PurrfectPalette.glowSecondary))) else BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
Color.White.copy(alpha = 0.12f)
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { onTabSelected(tab) }
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (tab == TaskTab.ACTIVE) Icons.Filled.Timer else Icons.Filled.Schedule,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = if (tab == TaskTab.ACTIVE) (context.translation["tasks_tab_active"] ?: "Active") else (context.translation["tasks_tab_scheduled"] ?: "Scheduled"),
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TasksHeader(
|
||||||
|
selectedTab: TaskTab,
|
||||||
|
onTabSelected: (TaskTab) -> Unit,
|
||||||
|
activeCount: Int,
|
||||||
|
scheduledCount: Int,
|
||||||
|
runningCount: Int,
|
||||||
|
subtitle: String,
|
||||||
|
onClear: () -> Unit,
|
||||||
|
onMerge: () -> Unit,
|
||||||
|
canMerge: Boolean
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||||
|
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()),
|
||||||
|
shape = RoundedCornerShape(26.dp),
|
||||||
|
color = Color.White.copy(alpha = 0.07f),
|
||||||
|
tonalElevation = 0.dp,
|
||||||
|
shadowElevation = 0.dp,
|
||||||
|
border = BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||||
|
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = context.translation["manager.routes.tasks"],
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
color = PurrfectPalette.textSecondary,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.wrapContentWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (canMerge) {
|
||||||
|
Surface(
|
||||||
|
onClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
onMerge()
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
color = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
|
||||||
|
border = BorderStroke(1.dp, PurrfectPalette.glowPrimary.copy(alpha = 0.4f))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Merge, contentDescription = context.translation["tasks_merge_button"], tint = Color.White, modifier = Modifier.size(16.dp))
|
||||||
|
Text(context.translation["tasks_merge_button"] ?: "Merge", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
color = Color.White.copy(alpha = 0.08f),
|
||||||
|
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.16f))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlaylistAddCheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = (context.translation["tasks_running_count"] ?: "{count} running")
|
||||||
|
.replace("{count}", runningCount.toString()),
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onClear) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.DeleteSweep,
|
||||||
|
contentDescription = context.translation["tasks_clear_button_description"],
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskTabSwitcher(
|
||||||
|
selectedTab = selectedTab,
|
||||||
|
onTabSelected = onTabSelected,
|
||||||
|
activeCount = activeCount,
|
||||||
|
scheduledCount = scheduledCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ import me.eternal.purrfectsnap.ui.util.OnLifecycleEvent
|
|||||||
import me.eternal.purrfectsnap.ui.util.coil.cacheKey
|
import me.eternal.purrfectsnap.ui.util.coil.cacheKey
|
||||||
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||||
import me.eternal.purrfectsnap.ui.util.Motion
|
import me.eternal.purrfectsnap.ui.util.Motion
|
||||||
|
import me.eternal.purrfectsnap.ui.manager.pages.TasksRootSection.TaskTab
|
||||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -117,7 +119,6 @@ fun TasksRootSection.AphelionTasksScreen(nav: NavBackStackEntry) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The "Structured Glass" Container (Dynamically Morphed)
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -129,32 +130,226 @@ fun TasksRootSection.AphelionTasksScreen(nav: NavBackStackEntry) {
|
|||||||
shadowElevation = 0.dp,
|
shadowElevation = 0.dp,
|
||||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
Column(modifier = Modifier.fillMaxSize().padding(top = controlsHeight - 44.dp)) {
|
||||||
state = scrollState,
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
contentPadding = PaddingValues(
|
shape = RoundedCornerShape(14.dp),
|
||||||
start = 10.dp,
|
color = Color.White.copy(alpha = 0.05f),
|
||||||
end = 10.dp,
|
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||||
top = controlsHeight - 44.dp,
|
) {
|
||||||
bottom = routes.bottomPadding + 20.dp
|
Row(
|
||||||
),
|
modifier = Modifier.padding(4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
item {
|
TaskTab.entries.forEach { tab ->
|
||||||
if (activeTasks.isEmpty() && recentTasks.isEmpty()) {
|
val isSelected = selectedTab == tab
|
||||||
AphelionTasksEmptyState(text = translation["no_tasks"] ?: "No tasks")
|
val backgroundAlpha by animateFloatAsState(if (isSelected) 0.12f else 0f, label = "tabBg")
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(38.dp)
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(Color.White.copy(alpha = backgroundAlpha))
|
||||||
|
.clickable {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
selectedTab = tab
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (tab == TaskTab.ACTIVE) (translation["tasks_tab_active"] ?: "Active") else (translation["tasks_tab_scheduled"] ?: "Scheduled"),
|
||||||
|
color = if (isSelected) Color.White else Color.White.copy(alpha = 0.5f),
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(activeTasks, key = { it.taskId }) { pendingTask ->
|
|
||||||
AphelionTaskCard(modifier = Modifier.fillMaxWidth(), pendingTask.task, pendingTask = pendingTask)
|
val activeList = activeTasks.filter { it.task.type != TaskType.SCHEDULED_SEND }
|
||||||
|
val recentList = recentTasks.filter { task ->
|
||||||
|
task.type != TaskType.SCHEDULED_SEND && activeList.none { it.task.hash == task.hash }
|
||||||
}
|
}
|
||||||
items(recentTasks, key = { it.hash }) { task ->
|
|
||||||
AphelionTaskCard(modifier = Modifier.fillMaxWidth(), task)
|
val scheduledActive = activeTasks.filter { it.task.type == TaskType.SCHEDULED_SEND }
|
||||||
|
val scheduledRecent = recentTasks.filter { task ->
|
||||||
|
task.type == TaskType.SCHEDULED_SEND && scheduledActive.none { it.task.hash == task.hash }
|
||||||
}
|
}
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
LazyColumn(
|
||||||
LaunchedEffect(remember { derivedStateOf { scrollState.firstVisibleItemIndex } }) {
|
state = scrollState,
|
||||||
fetchNewRecentTasks()
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
start = 10.dp,
|
||||||
|
end = 10.dp,
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = routes.bottomPadding + 20.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
if (selectedTab == TaskTab.ACTIVE) {
|
||||||
|
item(key = "auto_open_card") {
|
||||||
|
var queueItems by remember { mutableStateOf(listOf<Any>()) }
|
||||||
|
var processedCount by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
runCatching {
|
||||||
|
val autoOpen = context.bridgeService?.messagingBridge?.autoOpenInterface
|
||||||
|
processedCount = autoOpen?.processedCount ?: 0
|
||||||
|
val items = autoOpen?.queueItems ?: emptyList()
|
||||||
|
queueItems = items.mapNotNull {
|
||||||
|
runCatching { context.gson.fromJson(it, Map::class.java) }.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val queueSize = queueItems.size
|
||||||
|
|
||||||
|
if (queueSize > 0 || processedCount > 0) {
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
color = Color.White.copy(alpha = 0.06f),
|
||||||
|
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f)),
|
||||||
|
onClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
isExpanded = !isExpanded
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(14.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.AutoFixHigh, null, tint = PurrfectPalette.glowSecondary, modifier = Modifier.size(20.dp))
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Text(translation["auto_open_snaps.title"] ?: "Auto Open Snaps", fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||||
|
null,
|
||||||
|
tint = Color.White.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(modifier = Modifier.padding(top = 4.dp, start = 30.dp)) {
|
||||||
|
Text(
|
||||||
|
"${translation["auto_open_snaps.queue_size"] ?: "Queue"}: $queueSize \u00b7 ${translation["auto_open_snaps.processed_count"] ?: "Opened"}: $processedCount",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isExpanded,
|
||||||
|
enter = expandVertically() + fadeIn(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(top = 16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
queueItems.forEach { rawItem ->
|
||||||
|
val item = rawItem as? Map<String, String> ?: return@forEach
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().background(Color.White.copy(alpha = 0.03f), RoundedCornerShape(8.dp)).padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(item["senderInfo"] ?: "", fontSize = 13.sp, color = Color.White, fontWeight = FontWeight.Medium)
|
||||||
|
Text(item["contentType"] ?: "", fontSize = 11.sp, color = Color.White.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
Text(item["conversationType"] ?: "", fontSize = 10.sp, color = PurrfectPalette.glowSecondary.copy(alpha = 0.7f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedCount > 0 || queueSize > 0) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
runCatching { context.bridgeService?.messagingBridge?.autoOpenInterface?.reset() }
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth().height(36.dp),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
border = BorderStroke(1.dp, Color.Red.copy(alpha = 0.3f)),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Red.copy(alpha = 0.7f))
|
||||||
|
) {
|
||||||
|
Text(translation["auto_open_snaps.action_reset"] ?: "Reset Statistics", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeList.isEmpty() && recentList.isEmpty()) {
|
||||||
|
item(key = "active_empty") { AphelionTasksEmptyState(text = translation["tasks_no_active_tasks"] ?: "No active tasks") }
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupedActiveTasks = activeList.distinctBy { it.task.hash }
|
||||||
|
|
||||||
|
items(groupedActiveTasks, key = { it.task.hash }) { pendingTask ->
|
||||||
|
val isAutoOpenTask = pendingTask.task.isAutoOpen
|
||||||
|
val pulseAnimation = rememberInfiniteTransition(label = "pulse")
|
||||||
|
val pulseAlpha by pulseAnimation.animateFloat(
|
||||||
|
initialValue = 0.15f,
|
||||||
|
targetValue = 0.45f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1200, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
AphelionTaskCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.let {
|
||||||
|
if (isAutoOpenTask) {
|
||||||
|
it.border(
|
||||||
|
width = 1.5.dp,
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
PurrfectPalette.glowPrimary.copy(alpha = pulseAlpha),
|
||||||
|
PurrfectPalette.glowSecondary.copy(alpha = pulseAlpha)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(22.dp)
|
||||||
|
)
|
||||||
|
} else it
|
||||||
|
},
|
||||||
|
task = pendingTask.task,
|
||||||
|
pendingTask = pendingTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(recentList.filter { task -> groupedActiveTasks.none { it.task.hash == task.hash } }, key = { it.hash }) { task ->
|
||||||
|
AphelionTaskCard(modifier = Modifier.fillMaxWidth(), task)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (scheduledActive.isEmpty() && scheduledRecent.isEmpty()) {
|
||||||
|
item(key = "scheduled_empty") { AphelionTasksEmptyState(text = translation["tasks_no_scheduled_tasks"] ?: "No scheduled snaps") }
|
||||||
|
}
|
||||||
|
|
||||||
|
items(scheduledActive, key = { it.taskId }) { pendingTask ->
|
||||||
|
AphelionTaskCard(modifier = Modifier.fillMaxWidth(), pendingTask.task, pendingTask = pendingTask)
|
||||||
|
}
|
||||||
|
items(scheduledRecent, key = { it.hash }) { task ->
|
||||||
|
AphelionTaskCard(modifier = Modifier.fillMaxWidth(), task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
LaunchedEffect(remember { derivedStateOf { scrollState.firstVisibleItemIndex } }) {
|
||||||
|
fetchNewRecentTasks()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +362,35 @@ fun TasksRootSection.AphelionTasksScreen(nav: NavBackStackEntry) {
|
|||||||
enableMorph = true,
|
enableMorph = true,
|
||||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||||
actions = {
|
actions = {
|
||||||
|
if (taskSelection.size > 1) {
|
||||||
|
val canMergeSelection by rememberAsyncMutableState(defaultValue = false, keys = arrayOf(taskSelection.size)) {
|
||||||
|
taskSelection.all { it.second?.type?.contains("video") == true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canMergeSelection) {
|
||||||
|
Surface(
|
||||||
|
onClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
mergeSelection(taskSelection.toList().also {
|
||||||
|
taskSelection.clear()
|
||||||
|
}.map { it.first to it.second!! })
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
color = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
|
||||||
|
border = BorderStroke(1.dp, PurrfectPalette.glowPrimary.copy(alpha = 0.4f))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Merge, contentDescription = translation["tasks_merge_button"], tint = Color.White, modifier = Modifier.size(16.dp))
|
||||||
|
Text(translation["tasks_merge_button"] ?: "Merge", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = Color.White.copy(alpha = 0.08f),
|
color = Color.White.copy(alpha = 0.08f),
|
||||||
@@ -191,34 +415,12 @@ fun TasksRootSection.AphelionTasksScreen(nav: NavBackStackEntry) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (taskSelection.size > 1 && taskSelection.all { it.second?.type?.contains("video") == true }) {
|
|
||||||
Surface(
|
|
||||||
onClick = {
|
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
mergeSelection(
|
|
||||||
taskSelection.toList().also { taskSelection.clear() }
|
|
||||||
.map { it.first to it.second!! }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(18.dp),
|
|
||||||
color = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
|
|
||||||
border = BorderStroke(1.dp, PurrfectPalette.glowPrimary.copy(alpha = 0.4f))
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
Icon(Icons.Filled.Merge, contentDescription = translation["merge_button"], tint = Color.White, modifier = Modifier.size(16.dp))
|
|
||||||
Text(translation["merge_button"], color = Color.White, fontWeight = FontWeight.Bold, fontSize = 11.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
showConfirmDialog = true
|
showConfirmDialog = true
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Filled.DeleteSweep, contentDescription = translation["clear_button_description"], tint = Color.White)
|
Icon(Icons.Filled.DeleteSweep, contentDescription = translation["tasks_clear_button_description"], tint = Color.White)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -227,11 +429,11 @@ fun TasksRootSection.AphelionTasksScreen(nav: NavBackStackEntry) {
|
|||||||
if (showConfirmDialog) {
|
if (showConfirmDialog) {
|
||||||
val isSelection = taskSelection.isNotEmpty()
|
val isSelection = taskSelection.isNotEmpty()
|
||||||
val titleText = if (isSelection) {
|
val titleText = if (isSelection) {
|
||||||
translation.format("remove_selected_tasks_confirm", "count" to taskSelection.size.toString())
|
translation.format("tasks_remove_selected_tasks_confirm", "count" to taskSelection.size.toString())
|
||||||
} else {
|
} else {
|
||||||
translation["remove_all_tasks_confirm"]
|
translation["tasks_remove_all_tasks_confirm"]
|
||||||
}
|
}
|
||||||
val messageText = if (isSelection) translation["remove_selected_tasks_title"] else translation["remove_all_tasks_title"]
|
val messageText = if (isSelection) translation["tasks_remove_selected_tasks_title"] else translation["tasks_remove_all_tasks_title"]
|
||||||
|
|
||||||
TaskDangerDialog(
|
TaskDangerDialog(
|
||||||
visible = showConfirmDialog,
|
visible = showConfirmDialog,
|
||||||
@@ -494,10 +696,19 @@ internal fun TasksRootSection.AphelionTaskCard(modifier: Modifier, task: Task, p
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!taskStatus.isFinalStage()) {
|
if (!taskStatus.isFinalStage()) {
|
||||||
if (!isActive) {
|
if (isActive) {
|
||||||
|
taskProgressLabel?.let {
|
||||||
|
val labelText = if (task.isAutoOpen) {
|
||||||
|
val sessionTimeMins = (System.currentTimeMillis() - 0L) / 60000.0
|
||||||
|
val speed = if (sessionTimeMins > 0.1) String.format("%.1f", 0 / sessionTimeMins) else "0.0"
|
||||||
|
"$it • $speed snaps/min"
|
||||||
|
} else it
|
||||||
|
Text(labelText, style = MaterialTheme.typography.bodySmall, color = Color.White)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
taskProgressLabel?.let { Text(it, style = MaterialTheme.typography.bodySmall, color = Color.White) }
|
taskProgressLabel?.let { Text(it, style = MaterialTheme.typography.bodySmall, color = Color.White) }
|
||||||
}
|
}
|
||||||
if (taskProgress != -1 && taskProgressLabel == null) {
|
if (taskProgress != -1 && (taskProgressLabel == null || isActive)) {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { taskProgress.toFloat() / 100f },
|
progress = { taskProgress.toFloat() / 100f },
|
||||||
strokeCap = StrokeCap.Round, modifier = Modifier.fillMaxWidth(),
|
strokeCap = StrokeCap.Round, modifier = Modifier.fillMaxWidth(),
|
||||||
|
|||||||
Reference in New Issue
Block a user