v1.0.63: live sync progress counters, pause/resume, .gitignore fix
Build & Release APK / build (push) Has been cancelled
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:
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user