Files
SyncFlow/app/src/main/kotlin/com/syncflow/ui/log/LogScreen.kt
T
amir 422e8f0f0f feat: fix sync counters, polished activity rows, Files tab, new icon
- Fix SYNC_COMPLETED showing ↑0 ↓0 ✗0 when only deletions occurred: add ✕N
  for deleted files to the summary message (↑N ↓N ✕N ✗N format)
- Fix PairDetail Activity section showing raw "SYNC_STARTED" enum names and
  "remote" as a plain subtitle: replace dot-based EventRow with the same
  polished icon-bubble rows as the global Log tab
- Extract shared SyncEventRow composable + iconAndTint/label helpers to
  ui/shared/SyncEventRow.kt; both LogScreen and PairDetailScreen now use it
- Add Files tab (4th tab between Log and Accounts): folder browser showing
  all synced files per pair, grouped by subdirectory, with file-type icons,
  size, last-synced date, and a summary header (N files, total size)
- Add SyncFileStateDao.observeForPair() reactive Flow query for Files tab
- Completely redesign app icon: near-black radial gradient background with
  three bold directional arrows in an S-pattern (coral → silver → teal),
  each with gradient fills and tip-glow dots — entirely different from the
  typical circular sync-arrow style
- Bump version to 1.0.22 (build 23)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:05:28 +00:00

153 lines
5.5 KiB
Kotlin

package com.syncflow.ui.log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.syncflow.domain.model.SyncEventType
import com.syncflow.ui.shared.iconAndTint
import com.syncflow.ui.shared.label
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@Composable
fun LogScreen(
modifier: Modifier = Modifier,
vm: LogViewModel = hiltViewModel(),
) {
val entries by vm.entries.collectAsState()
if (entries.isEmpty()) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
Icons.Default.Notifications,
contentDescription = null,
modifier = Modifier.size(72.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.35f),
)
Text("No activity yet", style = MaterialTheme.typography.titleMedium)
Text(
"Sync events will appear here",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
} else {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(0.dp),
) {
// Group entries by calendar date
val grouped = entries.groupBy { entry ->
entry.event.timestamp.atZone(ZoneId.systemDefault()).toLocalDate()
}
grouped.forEach { (date, dayEntries) ->
item(key = date.toString()) {
Text(
text = date.toRelativeLabel(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp),
)
}
items(dayEntries, key = { it.event.id }) { entry ->
LogEntryRow(entry)
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
modifier = Modifier.padding(start = 52.dp),
)
}
}
item { Spacer(Modifier.height(80.dp)) }
}
}
}
@Composable
private fun LogEntryRow(entry: LogEntry) {
val (icon, tint) = entry.event.eventType.iconAndTint()
val timeFmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
val timeStr = entry.event.timestamp.atZone(ZoneId.systemDefault()).let { timeFmt.format(it) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
verticalAlignment = Alignment.Top,
) {
// Icon bubble
Surface(
shape = RoundedCornerShape(10.dp),
color = tint.copy(alpha = 0.12f),
modifier = Modifier.size(36.dp),
) {
Box(contentAlignment = Alignment.Center) {
Icon(icon, contentDescription = null, Modifier.size(18.dp), tint = tint)
}
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
entry.pairName,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.weight(1f, fill = false),
)
Text(
timeStr,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(Modifier.height(2.dp))
Text(
text = entry.event.eventType.label(),
style = MaterialTheme.typography.bodySmall,
)
val detail = entry.event.filePath ?: entry.event.message
if (detail != null) {
Text(
text = detail,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
)
}
}
}
}
private fun java.time.LocalDate.toRelativeLabel(): String {
val today = java.time.LocalDate.now()
return when {
this == today -> "Today"
this == today.minusDays(1) -> "Yesterday"
else -> DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).format(this)
}
}