fix scanner + pdf creator
This commit is contained in:
+87
-26
@@ -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
|
||||||
|
|||||||
+31
@@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+74
@@ -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. */
|
||||||
|
|||||||
+4
@@ -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,
|
||||||
)
|
)
|
||||||
+27
@@ -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>
|
||||||
Reference in New Issue
Block a user