remove merge opt keep delete opt
This commit is contained in:
@@ -15,6 +15,9 @@ interface FileDao {
|
|||||||
@Query("SELECT * FROM files WHERE folder_resource_id = :folderResourceId AND \"exists\" = 1 ORDER BY name ASC")
|
@Query("SELECT * FROM files WHERE folder_resource_id = :folderResourceId AND \"exists\" = 1 ORDER BY name ASC")
|
||||||
fun observeByFolder(folderResourceId: String): Flow<List<FileEntity>>
|
fun observeByFolder(folderResourceId: String): Flow<List<FileEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM files WHERE folder_resource_id = :folderResourceId AND \"exists\" = 1 ORDER BY name ASC")
|
||||||
|
suspend fun getByFolderOnce(folderResourceId: String): List<FileEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM files WHERE \"exists\" = 1 ORDER BY COALESCE(last_modified, added_at) DESC, name ASC")
|
@Query("SELECT * FROM files WHERE \"exists\" = 1 ORDER BY COALESCE(last_modified, added_at) DESC, name ASC")
|
||||||
fun observeAllVisible(): Flow<List<FileEntity>>
|
fun observeAllVisible(): Flow<List<FileEntity>>
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ interface FolderDao {
|
|||||||
@Query("SELECT * FROM folders WHERE parent_resource_id = :parentResourceId ORDER BY name ASC")
|
@Query("SELECT * FROM folders WHERE parent_resource_id = :parentResourceId ORDER BY name ASC")
|
||||||
suspend fun getByParent(parentResourceId: String): List<FolderEntity>
|
suspend fun getByParent(parentResourceId: String): List<FolderEntity>
|
||||||
|
|
||||||
|
/** Snapshot des sous-dossiers visibles (filtre `exists = 1`) — opérations batch hors UI. */
|
||||||
|
@Query("SELECT * FROM folders WHERE parent_resource_id = :parentResourceId AND \"exists\" = 1 ORDER BY name ASC")
|
||||||
|
suspend fun getByParentOnce(parentResourceId: String): List<FolderEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM folders WHERE resource_id = :resourceId LIMIT 1")
|
@Query("SELECT * FROM folders WHERE resource_id = :resourceId LIMIT 1")
|
||||||
suspend fun getByResourceId(resourceId: String): FolderEntity?
|
suspend fun getByResourceId(resourceId: String): FolderEntity?
|
||||||
|
|
||||||
@@ -52,6 +56,10 @@ interface FolderDao {
|
|||||||
@Query("UPDATE folders SET \"exists\" = 0, updated_at = :updatedAt WHERE resource_id = :resourceId")
|
@Query("UPDATE folders SET \"exists\" = 0, updated_at = :updatedAt WHERE resource_id = :resourceId")
|
||||||
suspend fun markMissing(resourceId: String, updatedAt: Long)
|
suspend fun markMissing(resourceId: String, updatedAt: Long)
|
||||||
|
|
||||||
|
/** Re-parente des sous-dossiers vers un nouveau parent (fusion). */
|
||||||
|
@Query("UPDATE folders SET parent_resource_id = :newParentId, updated_at = :updatedAt WHERE resource_id IN (:resourceIds)")
|
||||||
|
suspend fun reparent(resourceIds: List<String>, newParentId: String, updatedAt: Long)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promote le placement après confirmation serveur (`POST /sync/ops` appliqué) :
|
* Promote le placement après confirmation serveur (`POST /sync/ops` appliqué) :
|
||||||
* `local-cloud` si une copie physique existe, sinon `cloud`. Idempotent.
|
* `local-cloud` si une copie physique existe, sinon `cloud`. Idempotent.
|
||||||
|
|||||||
+13
@@ -56,6 +56,19 @@ interface PendingOperationDao {
|
|||||||
""")
|
""")
|
||||||
suspend fun countCreateOperations(resourceId: String): Int
|
suspend fun countCreateOperations(resourceId: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variante sans filtre `resource_type` : utilisée par la suppression de
|
||||||
|
* dossiers (les dossiers sont poussés à la découverte avec `resource_type =
|
||||||
|
* 'folder'`, pas `'file'`).
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT COUNT(*) FROM pending_operations
|
||||||
|
WHERE resource_id = :resourceId
|
||||||
|
AND operation = 'create_resource'
|
||||||
|
AND status IN ('pending', 'synced')
|
||||||
|
""")
|
||||||
|
suspend fun countCreateOperationsAnyType(resourceId: String): Int
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM pending_operations WHERE status = 'pending'")
|
@Query("SELECT COUNT(*) FROM pending_operations WHERE status = 'pending'")
|
||||||
suspend fun countPending(): Int
|
suspend fun countPending(): Int
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ class FileRepository @Inject constructor(
|
|||||||
fun observeFiles(folderResourceId: String): Flow<List<FileEntity>> =
|
fun observeFiles(folderResourceId: String): Flow<List<FileEntity>> =
|
||||||
fileDao.observeByFolder(folderResourceId)
|
fileDao.observeByFolder(folderResourceId)
|
||||||
|
|
||||||
|
/** Snapshot des fichiers visibles d'un dossier (opérations batch hors UI). */
|
||||||
|
suspend fun getFilesInFolder(folderResourceId: String): List<FileEntity> =
|
||||||
|
fileDao.getByFolderOnce(folderResourceId)
|
||||||
|
|
||||||
/** Fichiers visibles de toute l'arborescence — écran d'accueil (grille par date). */
|
/** Fichiers visibles de toute l'arborescence — écran d'accueil (grille par date). */
|
||||||
fun observeAllVisible(): Flow<List<FileEntity>> = fileDao.observeAllVisible()
|
fun observeAllVisible(): Flow<List<FileEntity>> = fileDao.observeAllVisible()
|
||||||
|
|
||||||
|
|||||||
+23
@@ -44,6 +44,10 @@ class FolderRepository @Inject constructor(
|
|||||||
|
|
||||||
suspend fun getAll(): List<FolderEntity> = folderDao.getAll()
|
suspend fun getAll(): List<FolderEntity> = folderDao.getAll()
|
||||||
|
|
||||||
|
/** Snapshot des sous-dossiers visibles d'un dossier (opérations batch hors UI). */
|
||||||
|
suspend fun getChildren(parentResourceId: String): List<FolderEntity> =
|
||||||
|
folderDao.getByParentOnce(parentResourceId)
|
||||||
|
|
||||||
/** Dossiers créés dans l'app uniquement (cibles du picker de déplacement). */
|
/** Dossiers créés dans l'app uniquement (cibles du picker de déplacement). */
|
||||||
suspend fun getCreatedInApp(): List<FolderEntity> = folderDao.getCreatedInApp()
|
suspend fun getCreatedInApp(): List<FolderEntity> = folderDao.getCreatedInApp()
|
||||||
|
|
||||||
@@ -132,6 +136,25 @@ class FolderRepository @Inject constructor(
|
|||||||
/** Marque un dossier disparu de l'arborescence (`exists = 0`) — jamais supprimé. */
|
/** Marque un dossier disparu de l'arborescence (`exists = 0`) — jamais supprimé. */
|
||||||
suspend fun markMissing(resourceId: String, updatedAt: Long) =
|
suspend fun markMissing(resourceId: String, updatedAt: Long) =
|
||||||
folderDao.markMissing(resourceId, updatedAt)
|
folderDao.markMissing(resourceId, updatedAt)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-parente des sous-dossiers vers un nouveau parent (fusion de dossiers).
|
||||||
|
* Transaction atomique : reparent Room + outbox `move_resource` par dossier.
|
||||||
|
*/
|
||||||
|
suspend fun reparentSubFolders(resourceIds: List<String>, newParentId: String) {
|
||||||
|
if (resourceIds.isEmpty()) return
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
appDatabase.withTransaction {
|
||||||
|
folderDao.reparent(resourceIds, newParentId, now)
|
||||||
|
for (resourceId in resourceIds) {
|
||||||
|
outboxRepository.enqueueMoveResource(
|
||||||
|
resourceId = resourceId,
|
||||||
|
resourceType = "folder",
|
||||||
|
toFolderResourceId = newParentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SaveFolderInput(
|
data class SaveFolderInput(
|
||||||
|
|||||||
+9
@@ -149,6 +149,15 @@ class OutboxRepository @Inject constructor(
|
|||||||
suspend fun hasCreateOperation(resourceId: String): Boolean =
|
suspend fun hasCreateOperation(resourceId: String): Boolean =
|
||||||
pendingOperationDao.countCreateOperations(resourceId) > 0
|
pendingOperationDao.countCreateOperations(resourceId) > 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variante toutes ressources : un `create_resource` a-t-il jamais été
|
||||||
|
* journalisé pour ce dossier ? Un `delete_resource` n'est émis que si le
|
||||||
|
* serveur connaît déjà la ressource (les dossiers sont poussés à la
|
||||||
|
* découverte avec `resource_type = 'folder'`).
|
||||||
|
*/
|
||||||
|
suspend fun hasCreateOperationAnyType(resourceId: String): Boolean =
|
||||||
|
pendingOperationDao.countCreateOperationsAnyType(resourceId) > 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Un `move_resource` est-il encore en attente de push ? Tant que l'op est
|
* Un `move_resource` est-il encore en attente de push ? Tant que l'op est
|
||||||
* pendante, le `folder_resource_id` local reflète la cible future : ni le
|
* pendante, le `folder_resource_id` local reflète la cible future : ni le
|
||||||
|
|||||||
+110
-31
@@ -68,7 +68,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vaultdrop.mobile.R
|
import com.vaultdrop.mobile.R
|
||||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||||
|
import com.vaultdrop.mobile.data.local.entity.FileStatus
|
||||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||||
|
import com.vaultdrop.mobile.data.local.entity.FolderStatus
|
||||||
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
|
||||||
@@ -80,6 +82,7 @@ import com.vaultdrop.mobile.ui.components.FileCategoryIcon
|
|||||||
import com.vaultdrop.mobile.ui.components.FilePendingReviewBadge
|
import com.vaultdrop.mobile.ui.components.FilePendingReviewBadge
|
||||||
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
|
||||||
|
|
||||||
import com.vaultdrop.mobile.ui.components.SelectionState
|
import com.vaultdrop.mobile.ui.components.SelectionState
|
||||||
import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
|
import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
|
||||||
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
|
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
|
||||||
@@ -87,6 +90,7 @@ 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.components.reviewDelete
|
||||||
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
|
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
|
||||||
|
import com.vaultdrop.mobile.ui.navigation.FolderSelectionNavBar
|
||||||
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
|
||||||
import com.vaultdrop.mobile.ui.navigation.SelectionNavBar
|
import com.vaultdrop.mobile.ui.navigation.SelectionNavBar
|
||||||
@@ -116,6 +120,8 @@ fun FolderListScreen(
|
|||||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
var pendingDeleteMode by remember { mutableStateOf<FileDeleter.DeleteMode?>(null) }
|
var pendingDeleteMode by remember { mutableStateOf<FileDeleter.DeleteMode?>(null) }
|
||||||
var pendingDeleteReview by remember { mutableStateOf<DeleteReview?>(null) }
|
var pendingDeleteReview by remember { mutableStateOf<DeleteReview?>(null) }
|
||||||
|
var pendingFolderIds by remember { mutableStateOf<List<String>>(emptyList()) }
|
||||||
|
var pendingFileIds by remember { mutableStateOf<List<String>>(emptyList()) }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
val createError = uiState.createError
|
val createError = uiState.createError
|
||||||
@@ -261,29 +267,72 @@ fun FolderListScreen(
|
|||||||
viewModel.moveSelectionHere(ids)
|
viewModel.moveSelectionHere(ids)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
selection.active -> SelectionNavBar(
|
selection.active -> if (uiState.view == HomeView.FOLDERS) {
|
||||||
onMove = { viewModel.startMove() },
|
FolderSelectionNavBar(
|
||||||
onBuildPdf = {
|
onDeleteModeSelected = { mode ->
|
||||||
val ids = selection.ids.toList()
|
val selectedFolders = uiState.browseSubFolders.filter {
|
||||||
selection.clear()
|
it.resourceId in selection.ids
|
||||||
onBuildPdf(ids)
|
}
|
||||||
},
|
val selectedFiles = uiState.browseFiles.filter {
|
||||||
onDeleteModeSelected = { mode ->
|
it.resourceId in selection.ids
|
||||||
val allFiles = uiState.sections.flatMap { section ->
|
}
|
||||||
section.rows.flatMap { row -> listOfNotNull(row.left, row.right) }
|
val incompatible = when (mode) {
|
||||||
}
|
FileDeleter.DeleteMode.IN_CLOUD ->
|
||||||
val selectedFiles = allFiles.filter { it.resourceId in selection.ids }
|
selectedFolders.count { it.syncStatus == FolderStatus.LOCAL } +
|
||||||
val review = reviewDelete(selectedFiles, mode)
|
selectedFiles.count { it.syncStatus == FileStatus.LOCAL }
|
||||||
pendingDeleteMode = mode
|
FileDeleter.DeleteMode.LOCALLY ->
|
||||||
pendingDeleteReview = review
|
selectedFolders.count { it.syncStatus == FolderStatus.CLOUD } +
|
||||||
if (review.hasWarning) {
|
selectedFiles.count { it.syncStatus == FileStatus.CLOUD }
|
||||||
showDeleteWarning = true
|
FileDeleter.DeleteMode.FULL ->
|
||||||
} else {
|
selectedFolders.count {
|
||||||
showDeleteConfirm = true
|
it.syncStatus == FolderStatus.LOCAL || it.syncStatus == FolderStatus.CLOUD
|
||||||
}
|
} + selectedFiles.count {
|
||||||
},
|
it.syncStatus == FileStatus.LOCAL || it.syncStatus == FileStatus.CLOUD
|
||||||
enabled = selection.ids.isNotEmpty(),
|
}
|
||||||
)
|
}
|
||||||
|
pendingFolderIds = selectedFolders.map { it.resourceId }
|
||||||
|
pendingFileIds = selectedFiles.map { it.resourceId }
|
||||||
|
pendingDeleteMode = mode
|
||||||
|
pendingDeleteReview = DeleteReview(
|
||||||
|
mode = mode,
|
||||||
|
total = selectedFolders.size + selectedFiles.size,
|
||||||
|
incompatibleCount = incompatible,
|
||||||
|
)
|
||||||
|
if (incompatible > 0) {
|
||||||
|
showDeleteWarning = true
|
||||||
|
} else {
|
||||||
|
showDeleteConfirm = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteEnabled = selection.ids.isNotEmpty(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SelectionNavBar(
|
||||||
|
onMove = { viewModel.startMove() },
|
||||||
|
onBuildPdf = {
|
||||||
|
val ids = selection.ids.toList()
|
||||||
|
selection.clear()
|
||||||
|
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 }
|
||||||
|
pendingFolderIds = emptyList()
|
||||||
|
pendingFileIds = selectedFiles.map { it.resourceId }
|
||||||
|
val review = reviewDelete(selectedFiles, mode)
|
||||||
|
pendingDeleteMode = mode
|
||||||
|
pendingDeleteReview = review
|
||||||
|
if (review.hasWarning) {
|
||||||
|
showDeleteWarning = true
|
||||||
|
} else {
|
||||||
|
showDeleteConfirm = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = selection.ids.isNotEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
else -> FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -341,26 +390,36 @@ fun FolderListScreen(
|
|||||||
showDeleteWarning = false
|
showDeleteWarning = false
|
||||||
pendingDeleteMode = null
|
pendingDeleteMode = null
|
||||||
pendingDeleteReview = null
|
pendingDeleteReview = null
|
||||||
|
pendingFolderIds = emptyList()
|
||||||
|
pendingFileIds = emptyList()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDeleteConfirm && pendingDeleteMode != null) {
|
if (showDeleteConfirm && pendingDeleteMode != null) {
|
||||||
DeleteConfirmDialog(
|
DeleteConfirmDialog(
|
||||||
count = selection.ids.size,
|
count = pendingFolderIds.size + pendingFileIds.size,
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
val ids = selection.ids.toList()
|
|
||||||
val mode = pendingDeleteMode!!
|
val mode = pendingDeleteMode!!
|
||||||
|
val fileIds = selection.ids.toList()
|
||||||
selection.clear()
|
selection.clear()
|
||||||
showDeleteConfirm = false
|
showDeleteConfirm = false
|
||||||
pendingDeleteMode = null
|
pendingDeleteMode = null
|
||||||
pendingDeleteReview = null
|
pendingDeleteReview = null
|
||||||
viewModel.deleteSelectedFiles(ids, mode)
|
if (uiState.view == HomeView.FOLDERS) {
|
||||||
|
viewModel.deleteFolderSelection(pendingFolderIds, pendingFileIds, mode)
|
||||||
|
} else {
|
||||||
|
viewModel.deleteSelectedFiles(fileIds, mode)
|
||||||
|
}
|
||||||
|
pendingFolderIds = emptyList()
|
||||||
|
pendingFileIds = emptyList()
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
showDeleteConfirm = false
|
showDeleteConfirm = false
|
||||||
pendingDeleteMode = null
|
pendingDeleteMode = null
|
||||||
pendingDeleteReview = null
|
pendingDeleteReview = null
|
||||||
|
pendingFolderIds = emptyList()
|
||||||
|
pendingFileIds = emptyList()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -683,7 +742,11 @@ private fun FolderBrowserContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(subFolders, key = { it.resourceId }) { folder ->
|
items(subFolders, key = { it.resourceId }) { folder ->
|
||||||
FolderRow(folder, onClick = { onOpenBrowseFolder(folder.resourceId) })
|
FolderRow(
|
||||||
|
folder = folder,
|
||||||
|
selection = selection,
|
||||||
|
onClick = { onOpenBrowseFolder(folder.resourceId) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// En mode déplacement, le navigateur sert au choix de la cible : les
|
// En mode déplacement, le navigateur sert au choix de la cible : les
|
||||||
@@ -779,16 +842,29 @@ private fun SectionHeader(label: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Carte dossier de l'explorateur — descend d'un niveau au tap. */
|
/** Carte dossier de l'explorateur — descend d'un niveau au tap, clic long pour la sélection. */
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun FolderRow(folder: FolderEntity, onClick: () -> Unit) {
|
private fun FolderRow(
|
||||||
|
folder: FolderEntity,
|
||||||
|
selection: SelectionState,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val selected = folder.resourceId in selection.ids
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 8.dp),
|
.padding(bottom = 8.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if (selection.active) selection.toggle(folder.resourceId) else onClick()
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
if (!selection.active) selection.start(folder.resourceId)
|
||||||
|
},
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -816,6 +892,9 @@ private fun FolderRow(folder: FolderEntity, onClick: () -> Unit) {
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (selection.active) {
|
||||||
|
SelectionStatusIcon(selected = selected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -16,6 +16,7 @@ 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.FileDeleter
|
||||||
|
import com.vaultdrop.mobile.features.saf.FolderDeleter
|
||||||
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
|
||||||
@@ -45,6 +46,7 @@ class FolderListViewModel @Inject constructor(
|
|||||||
private val safFolderCreator: SafFolderCreator,
|
private val safFolderCreator: SafFolderCreator,
|
||||||
private val fileMover: FileMover,
|
private val fileMover: FileMover,
|
||||||
private val fileDeleter: FileDeleter,
|
private val fileDeleter: FileDeleter,
|
||||||
|
private val folderDeleter: FolderDeleter,
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -311,6 +313,30 @@ class FolderListViewModel @Inject constructor(
|
|||||||
_uiState.update { it.copy(deleteSuccess = false) }
|
_uiState.update { it.copy(deleteSuccess = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suppression d'une sélection mixte de l'explorateur Dossiers (dossiers
|
||||||
|
* et/ou fichiers) selon le mode choisi. Les dossiers sont supprimés avec
|
||||||
|
* leur contenu récursif, les fichiers individuellement.
|
||||||
|
*/
|
||||||
|
fun deleteFolderSelection(folderIds: List<String>, fileIds: List<String>, mode: FileDeleter.DeleteMode) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
var failed = 0
|
||||||
|
val folders = folderIds.mapNotNull { folderRepository.getFolder(it) }
|
||||||
|
if (folders.isNotEmpty()) {
|
||||||
|
failed += folderDeleter.deleteFolders(folders, mode).failed
|
||||||
|
}
|
||||||
|
val files = fileIds.mapNotNull { fileRepository.getFile(it) }
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
failed += fileDeleter.deleteFiles(files, mode).failed
|
||||||
|
}
|
||||||
|
if (failed > 0) {
|
||||||
|
_uiState.update { it.copy(deleteError = context.getString(R.string.delete_error)) }
|
||||||
|
} else {
|
||||||
|
_uiState.update { it.copy(deleteSuccess = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 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 {
|
||||||
|
|||||||
@@ -180,6 +180,40 @@ fun SelectionNavBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Barre d'actions multi-sélection propre aux dossiers : suppression. */
|
||||||
|
@Composable
|
||||||
|
fun FolderSelectionNavBar(
|
||||||
|
onDeleteModeSelected: (FileDeleter.DeleteMode) -> Unit,
|
||||||
|
deleteEnabled: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 20.dp, end = 20.dp, bottom = 16.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(30.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
shadowElevation = 8.dp,
|
||||||
|
modifier = Modifier.widthIn(max = 400.dp).fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
DeleteDropdownButton(
|
||||||
|
enabled = deleteEnabled,
|
||||||
|
onModeSelected = onDeleteModeSelected,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Barre du mode déplacement : bouton « Déplacer ici (N) » vers le dossier courant. */
|
/** Barre du mode déplacement : bouton « Déplacer ici (N) » vers le dossier courant. */
|
||||||
@Composable
|
@Composable
|
||||||
fun MoveTargetBar(
|
fun MoveTargetBar(
|
||||||
|
|||||||
@@ -109,6 +109,15 @@
|
|||||||
<string name="selection_count">%1$d selected</string>
|
<string name="selection_count">%1$d selected</string>
|
||||||
<string name="selection_assemble">Assemble PDF</string>
|
<string name="selection_assemble">Assemble PDF</string>
|
||||||
<string name="selection_cancel">Cancel selection</string>
|
<string name="selection_cancel">Cancel selection</string>
|
||||||
|
|
||||||
|
<!-- Folder merge (multi-select in the folder browser) -->
|
||||||
|
<string name="merge_folders">Merge</string>
|
||||||
|
<string name="merge_folders_title">Merge %1$d folder(s)</string>
|
||||||
|
<string name="merge_name_hint">e.g. important_documents</string>
|
||||||
|
<string name="merge_sources_label">Source folders — their content will be moved:</string>
|
||||||
|
<string name="merge_confirm">Merge</string>
|
||||||
|
<string name="merge_success">Folder(s) merged</string>
|
||||||
|
<string name="merge_error">Could not merge the folders</string>
|
||||||
<string name="pdf_builder_title">Assemble a PDF</string>
|
<string name="pdf_builder_title">Assemble a PDF</string>
|
||||||
<string name="pdf_builder_empty">Nothing here yet. Add files or a note to start assembling.</string>
|
<string name="pdf_builder_empty">Nothing here yet. Add files or a note to start assembling.</string>
|
||||||
<string name="pdf_builder_add_files">Add a file</string>
|
<string name="pdf_builder_add_files">Add a file</string>
|
||||||
|
|||||||
@@ -155,6 +155,15 @@
|
|||||||
<string name="selection_count">%1$d sélectionné(s)</string>
|
<string name="selection_count">%1$d sélectionné(s)</string>
|
||||||
<string name="selection_assemble">Assembler en PDF</string>
|
<string name="selection_assemble">Assembler en PDF</string>
|
||||||
<string name="selection_cancel">Annuler la sélection</string>
|
<string name="selection_cancel">Annuler la sélection</string>
|
||||||
|
|
||||||
|
<!-- Fusion de dossiers (multi-sélection de l\'explorateur Dossiers) -->
|
||||||
|
<string name="merge_folders">Fusionner</string>
|
||||||
|
<string name="merge_folders_title">Fusionner %1$d dossier(s)</string>
|
||||||
|
<string name="merge_name_hint">ex. documents_importants</string>
|
||||||
|
<string name="merge_sources_label">Dossiers sources — leur contenu sera déplacé :</string>
|
||||||
|
<string name="merge_confirm">Fusionner</string>
|
||||||
|
<string name="merge_success">Dossier(s) fusionné(s)</string>
|
||||||
|
<string name="merge_error">Impossible de fusionner les dossiers</string>
|
||||||
<string name="pdf_builder_title">Assembler un PDF</string>
|
<string name="pdf_builder_title">Assembler un PDF</string>
|
||||||
<string name="pdf_builder_empty">Aucun élément. Ajoute des fichiers ou une note pour commencer l\'assemblage.</string>
|
<string name="pdf_builder_empty">Aucun élément. Ajoute des fichiers ou une note pour commencer l\'assemblage.</string>
|
||||||
<string name="pdf_builder_add_files">Ajouter un fichier</string>
|
<string name="pdf_builder_add_files">Ajouter un fichier</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user