v1.0.25: multi-select files, unified notification, dark theme, icon redesign
- FilesScreen: long-press enters selection mode, bulk share/delete toolbar, BackHandler - FilesViewModel: ShareMultiple action, isSelectionMode/selectedCount state, download-to-cache for open/share - FileWatchService: recursive FileObserver per subdirectory; unified notification updated with sync result via WorkManager flow observation - SyncWorker: silent flag suppresses notifications when triggered by watcher; emits KEY_RESULT_SUMMARY output data - Passbolt-inspired dark theme (Red700/Red500 primary, near-black surface) - App icon: circular AutoSync-style sync arrows (cyan gradient, deep navy background) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
@@ -21,6 +22,7 @@ import javax.inject.Inject
|
||||
sealed class FileAction {
|
||||
data class Open(val file: File) : FileAction()
|
||||
data class Share(val file: File) : FileAction()
|
||||
data class ShareMultiple(val files: List<File>) : FileAction()
|
||||
data class Error(val message: String) : FileAction()
|
||||
}
|
||||
|
||||
@@ -57,6 +59,12 @@ class FilesViewModel @Inject constructor(
|
||||
private val _isDownloading = MutableStateFlow(false)
|
||||
val isDownloading: StateFlow<Boolean> = _isDownloading
|
||||
|
||||
private val _selectedKeys = MutableStateFlow<Set<String>>(emptySet())
|
||||
val isSelectionMode: StateFlow<Boolean> = _selectedKeys.map { it.isNotEmpty() }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
||||
val selectedCount: StateFlow<Int> = _selectedKeys.map { it.size }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), 0)
|
||||
|
||||
fun selectPair(id: Long) { _selectedPairId.value = id }
|
||||
|
||||
fun openFile(file: SyncFileStateEntity) {
|
||||
@@ -108,6 +116,44 @@ class FilesViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun isSelected(file: SyncFileStateEntity): Boolean = fileKey(file) in _selectedKeys.value
|
||||
|
||||
fun toggleSelection(file: SyncFileStateEntity) {
|
||||
val key = fileKey(file)
|
||||
_selectedKeys.update { if (key in it) it - key else it + key }
|
||||
}
|
||||
|
||||
fun clearSelection() { _selectedKeys.value = emptySet() }
|
||||
|
||||
fun deleteSelected() {
|
||||
viewModelScope.launch {
|
||||
val toDelete = files.value.filter { isSelected(it) }
|
||||
toDelete.forEach { file ->
|
||||
try {
|
||||
resolveFile(file, emitErrorIfMissing = false)?.delete()
|
||||
fileStateDao.delete(file.syncPairId, file.relativePath)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Bulk delete failed: ${file.relativePath}")
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
fun shareSelected() {
|
||||
viewModelScope.launch {
|
||||
val toShare = files.value.filter { isSelected(it) }
|
||||
val resolved = toShare.mapNotNull { resolveFile(it, emitErrorIfMissing = false) }
|
||||
if (resolved.isEmpty()) {
|
||||
_fileAction.emit(FileAction.Error("No local files available to share"))
|
||||
return@launch
|
||||
}
|
||||
_fileAction.emit(FileAction.ShareMultiple(resolved))
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileKey(file: SyncFileStateEntity) = "${file.syncPairId}:${file.relativePath}"
|
||||
|
||||
// ── Download-then-open/share ──────────────────────────────────────────────
|
||||
|
||||
private fun downloadAndOpen(file: SyncFileStateEntity) {
|
||||
|
||||
Reference in New Issue
Block a user