v1.0.63: live sync progress counters, pause/resume, .gitignore fix
Build & Release APK / build (push) Has been cancelled

- SyncEngine: accepts onProgress callback — emits uploaded/downloaded/
  deleted/bytes counts atomically as each file completes
- SyncWorker: streams progress to WorkManager data so the UI can poll
  it live; reports per-run counters in the completion notification;
  adds pause/resume support
- HomeViewModel/PairDetailViewModel: subscribe to live WorkManager
  progress and surface it via SyncProgress state
- SyncPairEntity/SyncPairDao/SyncDatabase: persist last-run counters
  (uploaded, downloaded, deleted, bytesTransferred) in the DB with a
  Room migration (v3→v4)
- AppModule: provides WorkManager as an injectable singleton
- .gitignore: add .kotlin/ to exclude compiler session files

Security: no new issues — all logging via Timber (debug-only), DB
queries use Room parameterized API, file sharing via FileProvider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 20:07:25 +00:00
parent 21b7ffc7b3
commit c60eb8d27b
14 changed files with 227 additions and 36 deletions
@@ -51,6 +51,16 @@ fun FilesScreen(
val context = LocalContext.current
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val accounts by vm.accounts.collectAsState()
var selectedAccountId by remember { mutableStateOf(-1L) }
LaunchedEffect(accounts) {
if (selectedAccountId == -1L && accounts.isNotEmpty()) selectedAccountId = accounts.first().id
}
val cloudLabel = accounts.find { it.id == selectedAccountId }?.displayName
?: accounts.firstOrNull()?.displayName
?: "Cloud"
LaunchedEffect(Unit) {
vm.fileAction.collect { action ->
@@ -108,26 +118,29 @@ fun FilesScreen(
selected = activeTab == 0,
onClick = { activeTab = 0 },
shape = SegmentedButtonDefaults.itemShape(0, 2),
icon = { SegmentedButtonDefaults.Icon(active = activeTab == 0, activeContent = { Icon(Icons.Default.PhoneAndroid, null, Modifier.size(16.dp)) }) },
) {
Icon(Icons.Default.PhoneAndroid, null, Modifier.size(16.dp))
Spacer(Modifier.width(6.dp))
Text("Phone")
}
SegmentedButton(
selected = activeTab == 1,
onClick = { activeTab = 1 },
shape = SegmentedButtonDefaults.itemShape(1, 2),
icon = { SegmentedButtonDefaults.Icon(active = activeTab == 1, activeContent = { Icon(Icons.Default.Cloud, null, Modifier.size(16.dp)) }) },
) {
Icon(Icons.Default.Cloud, null, Modifier.size(16.dp))
Spacer(Modifier.width(6.dp))
Text("Cloud")
Text(cloudLabel, maxLines = 1, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis)
}
}
HorizontalDivider()
when (activeTab) {
0 -> LocalExplorer(modifier = Modifier.weight(1f))
1 -> CloudExplorer(vm = vm, modifier = Modifier.weight(1f))
1 -> CloudExplorer(
vm = vm,
selectedAccountId = selectedAccountId,
onAccountSelect = { selectedAccountId = it },
modifier = Modifier.weight(1f),
)
}
}
@@ -372,9 +385,13 @@ private fun LocalExplorer(modifier: Modifier = Modifier) {
// ── Cloud file explorer ───────────────────────────────────────────────────────
@Composable
private fun CloudExplorer(vm: FilesViewModel, modifier: Modifier = Modifier) {
private fun CloudExplorer(
vm: FilesViewModel,
selectedAccountId: Long,
onAccountSelect: (Long) -> Unit,
modifier: Modifier = Modifier,
) {
val accounts by vm.accounts.collectAsState()
var selectedId by remember { mutableStateOf(-1L) }
val cloudVm: RemoteBrowserViewModel = hiltViewModel(key = "cloud_explorer")
val state by cloudVm.state.collectAsState()
val breadcrumbState = rememberLazyListState()
@@ -382,11 +399,8 @@ private fun CloudExplorer(vm: FilesViewModel, modifier: Modifier = Modifier) {
var searchQuery by remember { mutableStateOf("") }
var searchActive by remember { mutableStateOf(false) }
LaunchedEffect(accounts) {
if (selectedId == -1L && accounts.isNotEmpty()) {
selectedId = accounts.first().id
cloudVm.init(selectedId, "/")
}
LaunchedEffect(selectedAccountId) {
if (selectedAccountId != -1L) cloudVm.init(selectedAccountId, "/")
}
LaunchedEffect(state.currentPath) {
@@ -426,8 +440,8 @@ private fun CloudExplorer(vm: FilesViewModel, modifier: Modifier = Modifier) {
) {
items(accounts) { acct ->
FilterChip(
selected = acct.id == selectedId,
onClick = { selectedId = acct.id; cloudVm.init(acct.id, "/") },
selected = acct.id == selectedAccountId,
onClick = { onAccountSelect(acct.id); cloudVm.init(acct.id, "/") },
label = { Text(acct.displayName, maxLines = 1) },
leadingIcon = { Icon(Icons.Default.Cloud, null, Modifier.size(14.dp)) },
)
@@ -520,7 +534,7 @@ private fun CloudExplorer(vm: FilesViewModel, modifier: Modifier = Modifier) {
file = entry,
onClick = {
if (entry.isDirectory) cloudVm.navigateTo(entry.path)
else vm.openCloudFile(selectedId, entry.path)
else vm.openCloudFile(selectedAccountId, entry.path)
},
)
}