fix scanner + pdf creator

This commit is contained in:
m
2026-09-14 09:37:53 +02:00
parent 696395eb68
commit 7fff65af4f
10 changed files with 366 additions and 26 deletions
@@ -17,9 +17,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.CreateNewFolder import androidx.compose.material.icons.filled.CreateNewFolder
import androidx.compose.material.icons.filled.DriveFileMove
import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.MergeType
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -60,6 +58,12 @@ import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.components.SyncStatusAction import com.vaultdrop.mobile.ui.components.SyncStatusAction
import com.vaultdrop.mobile.ui.components.rememberSelectionState import com.vaultdrop.mobile.ui.components.rememberSelectionState
import com.vaultdrop.mobile.features.saf.FileDeleter
import com.vaultdrop.mobile.ui.components.DeleteConfirmDialog
import com.vaultdrop.mobile.ui.components.DeleteReview
import com.vaultdrop.mobile.ui.components.DeleteWarningDialog
import com.vaultdrop.mobile.ui.components.reviewDelete
import com.vaultdrop.mobile.ui.navigation.SelectionNavBar
import java.util.Locale import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -79,7 +83,12 @@ fun FolderDetailScreen(
val selection = rememberSelectionState() val selection = rememberSelectionState()
var showCreateDialog by remember { mutableStateOf(false) } var showCreateDialog by remember { mutableStateOf(false) }
var showMoveDialog by remember { mutableStateOf(false) } var showMoveDialog by remember { mutableStateOf(false) }
var showDeleteWarning by remember { mutableStateOf(false) }
var showDeleteConfirm by remember { mutableStateOf(false) }
var pendingDeleteMode by remember { mutableStateOf<FileDeleter.DeleteMode?>(null) }
var pendingDeleteReview by remember { mutableStateOf<DeleteReview?>(null) }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val deletedLabel = stringResource(R.string.delete)
LaunchedEffect(uiState.folderMissing) { LaunchedEffect(uiState.folderMissing) {
if (uiState.folderMissing) onBack() if (uiState.folderMissing) onBack()
@@ -101,6 +110,21 @@ fun FolderDetailScreen(
} }
} }
LaunchedEffect(uiState.deleteError) {
val deleteError = uiState.deleteError
if (deleteError != null) {
snackbarHostState.showSnackbar(deleteError)
viewModel.clearDeleteError()
}
}
LaunchedEffect(uiState.deleteSuccess) {
if (uiState.deleteSuccess) {
snackbarHostState.showSnackbar(deletedLabel)
viewModel.clearDeleteSuccess()
}
}
LaunchedEffect(showMoveDialog) { LaunchedEffect(showMoveDialog) {
if (showMoveDialog) viewModel.loadMoveFolders() if (showMoveDialog) viewModel.loadMoveFolders()
} }
@@ -135,30 +159,7 @@ fun FolderDetailScreen(
} }
}, },
actions = { actions = {
if (selection.active) { if (!selection.active) {
IconButton(
onClick = { showMoveDialog = true },
enabled = selection.ids.isNotEmpty(),
) {
Icon(
Icons.Filled.DriveFileMove,
contentDescription = stringResource(R.string.move_files),
)
}
IconButton(
onClick = {
val ids = selection.ids.toList()
selection.clear()
onBuildPdf(ids)
},
enabled = selection.ids.isNotEmpty(),
) {
Icon(
Icons.Filled.MergeType,
contentDescription = stringResource(R.string.selection_assemble),
)
}
} else {
IconButton(onClick = { showCreateDialog = true }) { IconButton(onClick = { showCreateDialog = true }) {
Icon( Icon(
Icons.Filled.CreateNewFolder, Icons.Filled.CreateNewFolder,
@@ -178,6 +179,30 @@ fun FolderDetailScreen(
) )
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
bottomBar = {
if (selection.active) {
SelectionNavBar(
onMove = { showMoveDialog = true },
onBuildPdf = {
val ids = selection.ids.toList()
selection.clear()
onBuildPdf(ids)
},
onDeleteModeSelected = { mode ->
val files = uiState.files.filter { it.resourceId in selection.ids }
val review = reviewDelete(files, mode)
pendingDeleteMode = mode
pendingDeleteReview = review
if (review.hasWarning) {
showDeleteWarning = true
} else {
showDeleteConfirm = true
}
},
enabled = selection.ids.isNotEmpty(),
)
}
},
) { padding -> ) { padding ->
FolderDetailContent( FolderDetailContent(
subFolders = uiState.subFolders, subFolders = uiState.subFolders,
@@ -218,6 +243,42 @@ fun FolderDetailScreen(
}, },
) )
} }
val deleteReview = pendingDeleteReview
if (showDeleteWarning && deleteReview != null) {
DeleteWarningDialog(
review = deleteReview,
onContinue = {
showDeleteWarning = false
showDeleteConfirm = true
},
onDismiss = {
showDeleteWarning = false
pendingDeleteMode = null
pendingDeleteReview = null
},
)
}
if (showDeleteConfirm && pendingDeleteMode != null) {
DeleteConfirmDialog(
count = selection.ids.size,
onConfirm = {
val ids = selection.ids.toList()
val mode = pendingDeleteMode!!
selection.clear()
showDeleteConfirm = false
pendingDeleteMode = null
pendingDeleteReview = null
viewModel.deleteSelectedFiles(ids, mode)
},
onDismiss = {
showDeleteConfirm = false
pendingDeleteMode = null
pendingDeleteReview = null
},
)
}
} }
@Composable @Composable
@@ -11,6 +11,7 @@ import com.vaultdrop.mobile.data.local.entity.FolderEntity
import com.vaultdrop.mobile.data.repository.FileRepository import com.vaultdrop.mobile.data.repository.FileRepository
import com.vaultdrop.mobile.data.repository.FolderRepository import com.vaultdrop.mobile.data.repository.FolderRepository
import com.vaultdrop.mobile.data.repository.SaveFolderInput import com.vaultdrop.mobile.data.repository.SaveFolderInput
import com.vaultdrop.mobile.features.saf.FileDeleter
import com.vaultdrop.mobile.features.saf.FileMover import com.vaultdrop.mobile.features.saf.FileMover
import com.vaultdrop.mobile.features.saf.SafFolderCreator import com.vaultdrop.mobile.features.saf.SafFolderCreator
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -35,6 +36,10 @@ data class FolderDetailUiState(
val moveFolders: List<FolderEntity>? = null, val moveFolders: List<FolderEntity>? = null,
/** Erreur transitoire de déplacement — affichée en Snackbar puis effacée. */ /** Erreur transitoire de déplacement — affichée en Snackbar puis effacée. */
val moveError: String? = null, val moveError: String? = null,
/** Erreur transitoire de suppression — affichée en Snackbar puis effacée. */
val deleteError: String? = null,
/** Succès transitoire de suppression — affiché en Snackbar puis effacé. */
val deleteSuccess: Boolean = false,
) )
@HiltViewModel @HiltViewModel
@@ -45,6 +50,7 @@ class FolderDetailViewModel @Inject constructor(
private val tokenProvider: TokenProvider, private val tokenProvider: TokenProvider,
private val safFolderCreator: SafFolderCreator, private val safFolderCreator: SafFolderCreator,
private val fileMover: FileMover, private val fileMover: FileMover,
private val fileDeleter: FileDeleter,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : ViewModel() { ) : ViewModel() {
@@ -165,4 +171,29 @@ class FolderDetailViewModel @Inject constructor(
fun clearMoveError() { fun clearMoveError() {
_uiState.update { it.copy(moveError = null) } _uiState.update { it.copy(moveError = null) }
} }
/** Supprime les fichiers sélectionnés selon le mode choisi. */
fun deleteSelectedFiles(resourceIds: List<String>, mode: FileDeleter.DeleteMode) {
viewModelScope.launch {
val files = resourceIds.mapNotNull { fileRepository.getFile(it) }
if (files.isEmpty()) {
_uiState.update { it.copy(deleteError = context.getString(R.string.delete_error)) }
return@launch
}
val report = fileDeleter.deleteFiles(files, mode)
if (report.failed > 0) {
_uiState.update { it.copy(deleteError = context.getString(R.string.delete_error)) }
} else {
_uiState.update { it.copy(deleteSuccess = true) }
}
}
}
fun clearDeleteError() {
_uiState.update { it.copy(deleteError = null) }
}
fun clearDeleteSuccess() {
_uiState.update { it.copy(deleteSuccess = false) }
}
} }
@@ -72,6 +72,10 @@ import com.vaultdrop.mobile.data.local.entity.FolderEntity
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.features.saf.safDisplayName import com.vaultdrop.mobile.features.saf.safDisplayName
import com.vaultdrop.mobile.features.sync.SyncViewModel import com.vaultdrop.mobile.features.sync.SyncViewModel
import com.vaultdrop.mobile.features.saf.FileDeleter
import com.vaultdrop.mobile.ui.components.DeleteConfirmDialog
import com.vaultdrop.mobile.ui.components.DeleteReview
import com.vaultdrop.mobile.ui.components.DeleteWarningDialog
import com.vaultdrop.mobile.ui.components.FileCategoryIcon import com.vaultdrop.mobile.ui.components.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.FileSyncStatusIcon import com.vaultdrop.mobile.ui.components.FileSyncStatusIcon
import com.vaultdrop.mobile.ui.components.FolderNameDialog import com.vaultdrop.mobile.ui.components.FolderNameDialog
@@ -80,6 +84,7 @@ import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.components.SyncStatusAction import com.vaultdrop.mobile.ui.components.SyncStatusAction
import com.vaultdrop.mobile.ui.components.rememberSelectionState import com.vaultdrop.mobile.ui.components.rememberSelectionState
import com.vaultdrop.mobile.ui.components.reviewDelete
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.MoveTargetBar import com.vaultdrop.mobile.ui.navigation.MoveTargetBar
import com.vaultdrop.mobile.ui.navigation.NavTab import com.vaultdrop.mobile.ui.navigation.NavTab
@@ -106,6 +111,10 @@ fun FolderListScreen(
val context = LocalContext.current val context = LocalContext.current
val selection = rememberSelectionState() val selection = rememberSelectionState()
var showCreateDialog by remember { mutableStateOf(false) } var showCreateDialog by remember { mutableStateOf(false) }
var showDeleteWarning by remember { mutableStateOf(false) }
var showDeleteConfirm by remember { mutableStateOf(false) }
var pendingDeleteMode by remember { mutableStateOf<FileDeleter.DeleteMode?>(null) }
var pendingDeleteReview by remember { mutableStateOf<DeleteReview?>(null) }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val createError = uiState.createError val createError = uiState.createError
@@ -131,6 +140,21 @@ fun FolderListScreen(
} }
} }
LaunchedEffect(uiState.deleteError) {
val deleteError = uiState.deleteError
if (deleteError != null) {
snackbarHostState.showSnackbar(deleteError)
viewModel.clearDeleteError()
}
}
LaunchedEffect(uiState.deleteSuccess) {
if (uiState.deleteSuccess) {
snackbarHostState.showSnackbar(context.getString(R.string.delete))
viewModel.clearDeleteSuccess()
}
}
// Racine par défaut : dossier VaultDrop choisi au premier lancement. // Racine par défaut : dossier VaultDrop choisi au premier lancement.
val defaultRootLabel = stringResource(R.string.default_root_folder_label) val defaultRootLabel = stringResource(R.string.default_root_folder_label)
var pendingDefaultPick by remember { mutableStateOf<Uri?>(null) } var pendingDefaultPick by remember { mutableStateOf<Uri?>(null) }
@@ -243,6 +267,20 @@ fun FolderListScreen(
selection.clear() selection.clear()
onBuildPdf(ids) onBuildPdf(ids)
}, },
onDeleteModeSelected = { mode ->
val allFiles = uiState.sections.flatMap { section ->
section.rows.flatMap { row -> listOfNotNull(row.left, row.right) }
}
val selectedFiles = allFiles.filter { it.resourceId in selection.ids }
val review = reviewDelete(selectedFiles, mode)
pendingDeleteMode = mode
pendingDeleteReview = review
if (review.hasWarning) {
showDeleteWarning = true
} else {
showDeleteConfirm = true
}
},
enabled = selection.ids.isNotEmpty(), enabled = selection.ids.isNotEmpty(),
) )
else -> FloatingNavBar(selected = selectedTab, onSelect = onTabSelected) else -> FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
@@ -288,6 +326,42 @@ fun FolderListScreen(
}, },
) )
} }
val deleteReview = pendingDeleteReview
if (showDeleteWarning && deleteReview != null) {
DeleteWarningDialog(
review = deleteReview,
onContinue = {
showDeleteWarning = false
showDeleteConfirm = true
},
onDismiss = {
showDeleteWarning = false
pendingDeleteMode = null
pendingDeleteReview = null
},
)
}
if (showDeleteConfirm && pendingDeleteMode != null) {
DeleteConfirmDialog(
count = selection.ids.size,
onConfirm = {
val ids = selection.ids.toList()
val mode = pendingDeleteMode!!
selection.clear()
showDeleteConfirm = false
pendingDeleteMode = null
pendingDeleteReview = null
viewModel.deleteSelectedFiles(ids, mode)
},
onDismiss = {
showDeleteConfirm = false
pendingDeleteMode = null
pendingDeleteReview = null
},
)
}
} }
/** Sélecteur de layer de la page Fichiers — même habillage pill que la nav flottante. */ /** Sélecteur de layer de la page Fichiers — même habillage pill que la nav flottante. */
@@ -46,4 +46,8 @@ data class FolderListUiState(
val moveError: String? = null, val moveError: String? = null,
/** Succès transitoire de déplacement — affiché en Snackbar puis effacé. */ /** Succès transitoire de déplacement — affiché en Snackbar puis effacé. */
val moveSuccess: Boolean = false, val moveSuccess: Boolean = false,
/** Erreur transitoire de suppression — affichée en Snackbar puis effacée. */
val deleteError: String? = null,
/** Succès transitoire de suppression — affiché en Snackbar puis effacé. */
val deleteSuccess: Boolean = false,
) )
@@ -15,6 +15,7 @@ import com.vaultdrop.mobile.data.repository.FileRepository
import com.vaultdrop.mobile.data.repository.FolderRepository import com.vaultdrop.mobile.data.repository.FolderRepository
import com.vaultdrop.mobile.data.repository.SaveFolderInput import com.vaultdrop.mobile.data.repository.SaveFolderInput
import com.vaultdrop.mobile.features.saf.FileMover import com.vaultdrop.mobile.features.saf.FileMover
import com.vaultdrop.mobile.features.saf.FileDeleter
import com.vaultdrop.mobile.features.saf.SafFolderCreator import com.vaultdrop.mobile.features.saf.SafFolderCreator
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@@ -43,6 +44,7 @@ class FolderListViewModel @Inject constructor(
private val defaultRootStore: DefaultRootStore, private val defaultRootStore: DefaultRootStore,
private val safFolderCreator: SafFolderCreator, private val safFolderCreator: SafFolderCreator,
private val fileMover: FileMover, private val fileMover: FileMover,
private val fileDeleter: FileDeleter,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : ViewModel() { ) : ViewModel() {
@@ -274,6 +276,31 @@ class FolderListViewModel @Inject constructor(
_uiState.update { it.copy(moveError = null) } _uiState.update { it.copy(moveError = null) }
} }
/** Supprime les fichiers sélectionnés selon le mode choisi. */
fun deleteSelectedFiles(resourceIds: List<String>, mode: FileDeleter.DeleteMode) {
viewModelScope.launch {
val files = resourceIds.mapNotNull { fileRepository.getFile(it) }
if (files.isEmpty()) {
_uiState.update { it.copy(deleteError = context.getString(R.string.delete_error)) }
return@launch
}
val report = fileDeleter.deleteFiles(files, mode)
if (report.failed > 0) {
_uiState.update { it.copy(deleteError = context.getString(R.string.delete_error)) }
} else {
_uiState.update { it.copy(deleteSuccess = true) }
}
}
}
fun clearDeleteError() {
_uiState.update { it.copy(deleteError = null) }
}
fun clearDeleteSuccess() {
_uiState.update { it.copy(deleteSuccess = false) }
}
/** Grille d'accueil : tous les fichiers visibles, groupés par jour (date de référence). */ /** Grille d'accueil : tous les fichiers visibles, groupés par jour (date de référence). */
private fun observeFiles() { private fun observeFiles() {
viewModelScope.launch { viewModelScope.launch {
@@ -39,6 +39,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vaultdrop.mobile.R import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.ui.components.DeleteDropdownButton
import com.vaultdrop.mobile.features.saf.FileDeleter
enum class NavTab(val route: String) { enum class NavTab(val route: String) {
FILES(Routes.FILES), FILES(Routes.FILES),
@@ -133,6 +135,7 @@ fun FloatingNavBar(
fun SelectionNavBar( fun SelectionNavBar(
onMove: () -> Unit, onMove: () -> Unit,
onBuildPdf: () -> Unit, onBuildPdf: () -> Unit,
onDeleteModeSelected: (FileDeleter.DeleteMode) -> Unit,
enabled: Boolean, enabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@@ -167,6 +170,11 @@ fun SelectionNavBar(
onClick = onBuildPdf, onClick = onBuildPdf,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
DeleteDropdownButton(
enabled = enabled,
onModeSelected = onDeleteModeSelected,
modifier = Modifier.weight(1f),
)
} }
} }
} }
@@ -57,6 +57,10 @@ import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.domain.FileCategory import com.vaultdrop.mobile.domain.FileCategory
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.features.sync.SyncViewModel import com.vaultdrop.mobile.features.sync.SyncViewModel
import com.vaultdrop.mobile.features.saf.FileDeleter
import com.vaultdrop.mobile.ui.components.DeleteConfirmDialog
import com.vaultdrop.mobile.ui.components.DeleteReview
import com.vaultdrop.mobile.ui.components.DeleteWarningDialog
import com.vaultdrop.mobile.ui.components.FileCategoryIcon import com.vaultdrop.mobile.ui.components.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.FileSyncStatusIcon import com.vaultdrop.mobile.ui.components.FileSyncStatusIcon
import com.vaultdrop.mobile.ui.components.MoveFolderPickerDialog import com.vaultdrop.mobile.ui.components.MoveFolderPickerDialog
@@ -67,6 +71,7 @@ import com.vaultdrop.mobile.ui.components.SyncStatusAction
import com.vaultdrop.mobile.ui.components.color import com.vaultdrop.mobile.ui.components.color
import com.vaultdrop.mobile.ui.components.icon import com.vaultdrop.mobile.ui.components.icon
import com.vaultdrop.mobile.ui.components.rememberSelectionState import com.vaultdrop.mobile.ui.components.rememberSelectionState
import com.vaultdrop.mobile.ui.components.reviewDelete
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab import com.vaultdrop.mobile.ui.navigation.NavTab
import com.vaultdrop.mobile.ui.navigation.SelectionNavBar import com.vaultdrop.mobile.ui.navigation.SelectionNavBar
@@ -87,9 +92,16 @@ fun SearchScreen(
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle() val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val moveFolders by viewModel.moveFolders.collectAsStateWithLifecycle() val moveFolders by viewModel.moveFolders.collectAsStateWithLifecycle()
val moveError by viewModel.moveError.collectAsStateWithLifecycle() val moveError by viewModel.moveError.collectAsStateWithLifecycle()
val deleteError by viewModel.deleteError.collectAsStateWithLifecycle()
val deleteSuccess by viewModel.deleteSuccess.collectAsStateWithLifecycle()
val selection = rememberSelectionState() val selection = rememberSelectionState()
var showMoveDialog by remember { mutableStateOf(false) } var showMoveDialog by remember { mutableStateOf(false) }
var showDeleteWarning by remember { mutableStateOf(false) }
var showDeleteConfirm by remember { mutableStateOf(false) }
var pendingDeleteMode by remember { mutableStateOf<FileDeleter.DeleteMode?>(null) }
var pendingDeleteReview by remember { mutableStateOf<DeleteReview?>(null) }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val deletedLabel = stringResource(R.string.delete)
LaunchedEffect(moveError) { LaunchedEffect(moveError) {
val message = moveError val message = moveError
@@ -99,6 +111,21 @@ fun SearchScreen(
} }
} }
LaunchedEffect(deleteError) {
val message = deleteError
if (message != null) {
snackbarHostState.showSnackbar(message)
viewModel.clearDeleteError()
}
}
LaunchedEffect(deleteSuccess) {
if (deleteSuccess) {
snackbarHostState.showSnackbar(deletedLabel)
viewModel.clearDeleteSuccess()
}
}
LaunchedEffect(showMoveDialog) { LaunchedEffect(showMoveDialog) {
if (showMoveDialog) viewModel.loadMoveFolders() if (showMoveDialog) viewModel.loadMoveFolders()
} }
@@ -145,6 +172,17 @@ fun SearchScreen(
selection.clear() selection.clear()
onBuildPdf(ids) onBuildPdf(ids)
}, },
onDeleteModeSelected = { mode ->
val selectedFiles = uiState.results.filter { it.resourceId in selection.ids }
val review = reviewDelete(selectedFiles, mode)
pendingDeleteMode = mode
pendingDeleteReview = review
if (review.hasWarning) {
showDeleteWarning = true
} else {
showDeleteConfirm = true
}
},
enabled = selection.ids.isNotEmpty(), enabled = selection.ids.isNotEmpty(),
) )
} else { } else {
@@ -180,6 +218,42 @@ fun SearchScreen(
}, },
) )
} }
val deleteReview = pendingDeleteReview
if (showDeleteWarning && deleteReview != null) {
DeleteWarningDialog(
review = deleteReview,
onContinue = {
showDeleteWarning = false
showDeleteConfirm = true
},
onDismiss = {
showDeleteWarning = false
pendingDeleteMode = null
pendingDeleteReview = null
},
)
}
if (showDeleteConfirm && pendingDeleteMode != null) {
DeleteConfirmDialog(
count = selection.ids.size,
onConfirm = {
val ids = selection.ids.toList()
val mode = pendingDeleteMode!!
selection.clear()
showDeleteConfirm = false
pendingDeleteMode = null
pendingDeleteReview = null
viewModel.deleteSelectedFiles(ids, mode)
},
onDismiss = {
showDeleteConfirm = false
pendingDeleteMode = null
pendingDeleteReview = null
},
)
}
} }
@Composable @Composable
@@ -9,6 +9,7 @@ import com.vaultdrop.mobile.data.local.entity.FolderEntity
import com.vaultdrop.mobile.data.repository.FileRepository import com.vaultdrop.mobile.data.repository.FileRepository
import com.vaultdrop.mobile.data.repository.FolderRepository import com.vaultdrop.mobile.data.repository.FolderRepository
import com.vaultdrop.mobile.features.saf.FileMover import com.vaultdrop.mobile.features.saf.FileMover
import com.vaultdrop.mobile.features.saf.FileDeleter
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -31,6 +32,7 @@ class SearchViewModel @Inject constructor(
private val fileRepository: FileRepository, private val fileRepository: FileRepository,
private val folderRepository: FolderRepository, private val folderRepository: FolderRepository,
private val fileMover: FileMover, private val fileMover: FileMover,
private val fileDeleter: FileDeleter,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : ViewModel() { ) : ViewModel() {
@@ -43,6 +45,12 @@ class SearchViewModel @Inject constructor(
private val _moveError = MutableStateFlow<String?>(null) private val _moveError = MutableStateFlow<String?>(null)
val moveError: StateFlow<String?> = _moveError.asStateFlow() val moveError: StateFlow<String?> = _moveError.asStateFlow()
private val _deleteError = MutableStateFlow<String?>(null)
val deleteError: StateFlow<String?> = _deleteError.asStateFlow()
private val _deleteSuccess = MutableStateFlow(false)
val deleteSuccess: StateFlow<Boolean> = _deleteSuccess.asStateFlow()
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
val uiState: StateFlow<SearchUiState> = combine( val uiState: StateFlow<SearchUiState> = combine(
_searchQuery.debounce(SEARCH_DEBOUNCE_MS), _searchQuery.debounce(SEARCH_DEBOUNCE_MS),
@@ -96,6 +104,31 @@ class SearchViewModel @Inject constructor(
_moveError.value = null _moveError.value = null
} }
/** Supprime les fichiers sélectionnés selon le mode choisi. */
fun deleteSelectedFiles(resourceIds: List<String>, mode: FileDeleter.DeleteMode) {
viewModelScope.launch {
val files = resourceIds.mapNotNull { fileRepository.getFile(it) }
if (files.isEmpty()) {
_deleteError.value = context.getString(R.string.delete_error)
return@launch
}
val report = fileDeleter.deleteFiles(files, mode)
if (report.failed > 0) {
_deleteError.value = context.getString(R.string.delete_error)
} else {
_deleteSuccess.value = true
}
}
}
fun clearDeleteError() {
_deleteError.value = null
}
fun clearDeleteSuccess() {
_deleteSuccess.value = false
}
private data class CategoryQuery(val query: String, val category: String?) private data class CategoryQuery(val query: String, val category: String?)
companion object { companion object {
@@ -133,4 +133,18 @@
<string name="pdf_builder_name_save">Save</string> <string name="pdf_builder_name_save">Save</string>
<string name="pdf_builder_saved">PDF created</string> <string name="pdf_builder_saved">PDF created</string>
<string name="pdf_generated_folder">Generated PDFs</string> <string name="pdf_generated_folder">Generated PDFs</string>
<!-- Delete multi-select -->
<string name="delete">Delete</string>
<string name="delete_locally">Delete locally</string>
<string name="delete_in_cloud">Delete from cloud</string>
<string name="delete_full">Delete everywhere</string>
<string name="delete_confirm_title">Delete %1$d document(s)?</string>
<string name="delete_confirm_message">This action cannot be undone.</string>
<string name="delete_warning_title">Documents not synced</string>
<string name="delete_warning_local_only_msg">%1$d out of %2$d document(s) are not synced to the cloud and will not be deleted from the server.</string>
<string name="delete_warning_cloud_only_msg">%1$d out of %2$d document(s) have no local copy and will not be deleted from this device.</string>
<string name="delete_confirm_button">Confirm</string>
<string name="delete_cancel">Cancel</string>
<string name="delete_error">Error while deleting</string>
</resources> </resources>
@@ -213,4 +213,18 @@
<string name="scan_error_page">Impossible d\'enregistrer la page.</string> <string name="scan_error_page">Impossible d\'enregistrer la page.</string>
<string name="scan_error_export">Impossible d\'exporter les pages.</string> <string name="scan_error_export">Impossible d\'exporter les pages.</string>
<string name="scan_export_done">Pages exportées</string> <string name="scan_export_done">Pages exportées</string>
<!-- Suppression multi-sélection -->
<string name="delete">Supprimer</string>
<string name="delete_locally">Supprimer localement</string>
<string name="delete_in_cloud">Supprimer dans le cloud</string>
<string name="delete_full">Supprimer partout</string>
<string name="delete_confirm_title">Supprimer %1$d document(s) ?</string>
<string name="delete_confirm_message">Cette action est irréversible.</string>
<string name="delete_warning_title">Documents non synchronisés</string>
<string name="delete_warning_local_only_msg">%1$d document(s) sur %2$d n\'est/sont pas synchronisés avec le cloud et ne seront pas supprimés du serveur.</string>
<string name="delete_warning_cloud_only_msg">%1$d document(s) sur %2$d n\'ont pas de copie locale et ne seront pas supprimés de cet appareil.</string>
<string name="delete_confirm_button">Confirmer</string>
<string name="delete_cancel">Annuler</string>
<string name="delete_error">Erreur lors de la suppression</string>
</resources> </resources>