add fichiers filters
This commit is contained in:
+439
-166
@@ -23,13 +23,17 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.ArrowUpward
|
||||||
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.Dashboard
|
||||||
|
import androidx.compose.material.icons.filled.Description
|
||||||
import androidx.compose.material.icons.filled.Folder
|
import androidx.compose.material.icons.filled.Folder
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -44,6 +48,7 @@ import androidx.compose.material3.OutlinedButton
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -55,12 +60,14 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.hilt.navigation.compose.hiltViewModel
|
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
|
||||||
@@ -70,12 +77,12 @@ 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.ui.components.FileCategoryIcon
|
import com.vaultdrop.mobile.ui.components.FileCategoryIcon
|
||||||
import com.vaultdrop.mobile.ui.components.FolderNameDialog
|
import com.vaultdrop.mobile.ui.components.FolderNameDialog
|
||||||
import com.vaultdrop.mobile.ui.components.MoveFolderPickerDialog
|
|
||||||
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
|
||||||
import com.vaultdrop.mobile.ui.components.rememberSelectionState
|
import com.vaultdrop.mobile.ui.components.rememberSelectionState
|
||||||
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.NavTab
|
import com.vaultdrop.mobile.ui.navigation.NavTab
|
||||||
import com.vaultdrop.mobile.ui.navigation.SelectionNavBar
|
import com.vaultdrop.mobile.ui.navigation.SelectionNavBar
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -88,7 +95,6 @@ fun FolderListScreen(
|
|||||||
selectedTab: NavTab,
|
selectedTab: NavTab,
|
||||||
onTabSelected: (NavTab) -> Unit,
|
onTabSelected: (NavTab) -> Unit,
|
||||||
onOpenDocument: (String) -> Unit,
|
onOpenDocument: (String) -> Unit,
|
||||||
onOpenFolder: (String) -> Unit,
|
|
||||||
onBuildPdf: (List<String>) -> Unit,
|
onBuildPdf: (List<String>) -> Unit,
|
||||||
syncViewModel: SyncViewModel,
|
syncViewModel: SyncViewModel,
|
||||||
connectionStatusViewModel: ConnectionStatusViewModel,
|
connectionStatusViewModel: ConnectionStatusViewModel,
|
||||||
@@ -102,7 +108,6 @@ fun FolderListScreen(
|
|||||||
val folderLabel = stringResource(R.string.folder)
|
val folderLabel = stringResource(R.string.folder)
|
||||||
val selection = rememberSelectionState()
|
val selection = rememberSelectionState()
|
||||||
var showCreateDialog by remember { mutableStateOf(false) }
|
var showCreateDialog by remember { mutableStateOf(false) }
|
||||||
var showMoveDialog by remember { mutableStateOf(false) }
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
val createError = uiState.createError
|
val createError = uiState.createError
|
||||||
@@ -121,8 +126,11 @@ fun FolderListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(showMoveDialog) {
|
LaunchedEffect(uiState.moveSuccess) {
|
||||||
if (showMoveDialog) viewModel.loadMoveFolders()
|
if (uiState.moveSuccess) {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.move_success))
|
||||||
|
viewModel.clearMoveSuccess()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Racine par défaut : dossier VaultDrop choisi au premier lancement.
|
// Racine par défaut : dossier VaultDrop choisi au premier lancement.
|
||||||
@@ -191,21 +199,31 @@ fun FolderListScreen(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = selection.active) { selection.clear() }
|
BackHandler(enabled = selection.active || uiState.moveMode) {
|
||||||
|
if (uiState.moveMode) {
|
||||||
|
viewModel.cancelMove()
|
||||||
|
} else {
|
||||||
|
selection.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
if (selection.active) {
|
when {
|
||||||
Text(stringResource(R.string.selection_count, selection.ids.size))
|
uiState.moveMode -> Text(stringResource(R.string.move_pick_title, selection.ids.size))
|
||||||
} else {
|
selection.active -> Text(stringResource(R.string.selection_count, selection.ids.size))
|
||||||
Text(stringResource(R.string.files))
|
else -> Text(stringResource(R.string.files))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (selection.active) {
|
if (uiState.moveMode || selection.active) {
|
||||||
IconButton(onClick = { selection.clear() }) {
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (uiState.moveMode) viewModel.cancelMove() else selection.clear()
|
||||||
|
},
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Close,
|
Icons.Filled.Close,
|
||||||
contentDescription = stringResource(R.string.selection_cancel),
|
contentDescription = stringResource(R.string.selection_cancel),
|
||||||
@@ -214,7 +232,7 @@ fun FolderListScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!selection.active) {
|
if (!uiState.moveMode && !selection.active) {
|
||||||
ServerStatusBadge(
|
ServerStatusBadge(
|
||||||
status = connectionStatus,
|
status = connectionStatus,
|
||||||
onClick = connectionStatusViewModel::checkNow,
|
onClick = connectionStatusViewModel::checkNow,
|
||||||
@@ -227,9 +245,17 @@ fun FolderListScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (selection.active) {
|
when {
|
||||||
SelectionNavBar(
|
uiState.moveMode -> MoveTargetBar(
|
||||||
onMove = { showMoveDialog = true },
|
fileCount = selection.ids.size,
|
||||||
|
onMoveHere = {
|
||||||
|
val ids = selection.ids.toList()
|
||||||
|
selection.clear()
|
||||||
|
viewModel.moveSelectionHere(ids)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
selection.active -> SelectionNavBar(
|
||||||
|
onMove = { viewModel.startMove() },
|
||||||
onBuildPdf = {
|
onBuildPdf = {
|
||||||
val ids = selection.ids.toList()
|
val ids = selection.ids.toList()
|
||||||
selection.clear()
|
selection.clear()
|
||||||
@@ -237,23 +263,39 @@ fun FolderListScreen(
|
|||||||
},
|
},
|
||||||
enabled = selection.ids.isNotEmpty(),
|
enabled = selection.ids.isNotEmpty(),
|
||||||
)
|
)
|
||||||
} else {
|
else -> FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
||||||
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { padding ->
|
) { padding ->
|
||||||
FolderListContent(
|
Column(
|
||||||
uiState = uiState,
|
modifier = Modifier
|
||||||
isImporting = importState.isImporting,
|
.padding(padding)
|
||||||
error = uiState.error ?: importState.error,
|
.fillMaxSize(),
|
||||||
selection = selection,
|
) {
|
||||||
onAddFolder = { pickFolderLauncher.launch(null) },
|
HomeViewSelector(
|
||||||
onCreateFolder = { showCreateDialog = true },
|
selected = uiState.view,
|
||||||
onOpenFolder = onOpenFolder,
|
enabled = !selection.active && !uiState.moveMode,
|
||||||
onOpenDocument = onOpenDocument,
|
onSelect = viewModel::selectView,
|
||||||
modifier = Modifier.padding(padding),
|
)
|
||||||
)
|
HomeViewContent(
|
||||||
|
view = uiState.view,
|
||||||
|
moveMode = uiState.moveMode,
|
||||||
|
atRoot = uiState.browseFolderId == null,
|
||||||
|
browseFolderName = uiState.browseFolderName,
|
||||||
|
subFolders = uiState.browseSubFolders,
|
||||||
|
sections = uiState.sections,
|
||||||
|
isImporting = importState.isImporting,
|
||||||
|
error = uiState.error ?: importState.error,
|
||||||
|
selection = selection,
|
||||||
|
onBrowseUp = viewModel::browseUp,
|
||||||
|
onOpenBrowseFolder = viewModel::openBrowseFolder,
|
||||||
|
onAddFolder = { pickFolderLauncher.launch(null) },
|
||||||
|
onCreateFolder = { showCreateDialog = true },
|
||||||
|
onOpenDocument = onOpenDocument,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCreateDialog) {
|
if (showCreateDialog) {
|
||||||
@@ -261,27 +303,378 @@ fun FolderListScreen(
|
|||||||
onDismiss = { showCreateDialog = false },
|
onDismiss = { showCreateDialog = false },
|
||||||
onConfirm = { name ->
|
onConfirm = { name ->
|
||||||
showCreateDialog = false
|
showCreateDialog = false
|
||||||
viewModel.createFolderInDefaultRoot(name)
|
viewModel.createFolderInBrowse(name)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val moveFolders = uiState.moveFolders
|
/** Sélecteur de layer de la page Fichiers — même habillage pill que la nav flottante. */
|
||||||
if (showMoveDialog && moveFolders != null) {
|
@Composable
|
||||||
MoveFolderPickerDialog(
|
private fun HomeViewSelector(
|
||||||
folders = moveFolders,
|
selected: HomeView,
|
||||||
fileCount = selection.ids.size,
|
enabled: Boolean,
|
||||||
onDismiss = {
|
onSelect: (HomeView) -> Unit,
|
||||||
showMoveDialog = false
|
modifier: Modifier = Modifier,
|
||||||
viewModel.closeMovePicker()
|
) {
|
||||||
},
|
Box(
|
||||||
onConfirm = { folderId ->
|
modifier = modifier
|
||||||
val ids = selection.ids.toList()
|
.fillMaxWidth()
|
||||||
selection.clear()
|
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 4.dp),
|
||||||
showMoveDialog = false
|
contentAlignment = Alignment.Center,
|
||||||
viewModel.moveSelectedFiles(ids, folderId)
|
) {
|
||||||
},
|
Surface(
|
||||||
|
shape = RoundedCornerShape(30.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
shadowElevation = 8.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 400.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
ViewSegment(
|
||||||
|
label = stringResource(R.string.view_files),
|
||||||
|
icon = Icons.Filled.Description,
|
||||||
|
selected = selected == HomeView.FILES,
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = { onSelect(HomeView.FILES) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
ViewSegment(
|
||||||
|
label = stringResource(R.string.view_folders),
|
||||||
|
icon = Icons.Filled.Folder,
|
||||||
|
selected = selected == HomeView.FOLDERS,
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = { onSelect(HomeView.FOLDERS) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
ViewSegment(
|
||||||
|
label = stringResource(R.string.view_dashboard),
|
||||||
|
icon = Icons.Filled.Dashboard,
|
||||||
|
selected = selected == HomeView.DASHBOARD,
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = { onSelect(HomeView.DASHBOARD) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ViewSegment(
|
||||||
|
label: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
selected: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val tint = if (selected) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
Surface(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
color = if (selected) MaterialTheme.colorScheme.primaryContainer
|
||||||
|
else MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 6.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = tint,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = tint,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Routage du contenu selon le layer affiché. */
|
||||||
|
@Composable
|
||||||
|
private fun HomeViewContent(
|
||||||
|
view: HomeView,
|
||||||
|
moveMode: Boolean,
|
||||||
|
atRoot: Boolean,
|
||||||
|
browseFolderName: String?,
|
||||||
|
subFolders: List<FolderEntity>,
|
||||||
|
sections: List<FileSection>,
|
||||||
|
isImporting: Boolean,
|
||||||
|
error: String?,
|
||||||
|
selection: SelectionState,
|
||||||
|
onBrowseUp: () -> Unit,
|
||||||
|
onOpenBrowseFolder: (String) -> Unit,
|
||||||
|
onAddFolder: () -> Unit,
|
||||||
|
onCreateFolder: () -> Unit,
|
||||||
|
onOpenDocument: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (view) {
|
||||||
|
HomeView.DASHBOARD -> DashboardPlaceholder(modifier = modifier)
|
||||||
|
HomeView.FILES -> FileGridContent(
|
||||||
|
sections = sections,
|
||||||
|
isImporting = isImporting,
|
||||||
|
error = error,
|
||||||
|
selection = selection,
|
||||||
|
onOpenDocument = onOpenDocument,
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
HomeView.FOLDERS -> FolderBrowserContent(
|
||||||
|
moveMode = moveMode,
|
||||||
|
atRoot = atRoot,
|
||||||
|
browseFolderName = browseFolderName,
|
||||||
|
subFolders = subFolders,
|
||||||
|
isImporting = isImporting,
|
||||||
|
error = error,
|
||||||
|
onBrowseUp = onBrowseUp,
|
||||||
|
onOpenBrowseFolder = onOpenBrowseFolder,
|
||||||
|
onAddFolder = onAddFolder,
|
||||||
|
onCreateFolder = onCreateFolder,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dashboard provisoire — vide pour l'instant. */
|
||||||
|
@Composable
|
||||||
|
private fun DashboardPlaceholder(modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.dashboard_coming_soon),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Layer Fichiers : tous les fichiers visibles, groupés par jour. */
|
||||||
|
@Composable
|
||||||
|
private fun FileGridContent(
|
||||||
|
sections: List<FileSection>,
|
||||||
|
isImporting: Boolean,
|
||||||
|
error: String?,
|
||||||
|
selection: SelectionState,
|
||||||
|
onOpenDocument: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
if (isImporting) {
|
||||||
|
item(key = "importing") {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error?.let { message ->
|
||||||
|
item(key = "error") {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.isEmpty() && !isImporting) {
|
||||||
|
item(key = "empty") {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_files_yet),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 32.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.forEach { section ->
|
||||||
|
item(key = "header-${section.dayKey}") {
|
||||||
|
SectionHeader(section.dayLabel)
|
||||||
|
}
|
||||||
|
items(section.rows, key = { it.key }) { row ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
FileCard(
|
||||||
|
file = row.left,
|
||||||
|
selection = selection,
|
||||||
|
onClick = { onOpenDocument(row.left.resourceId) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
if (row.right != null) {
|
||||||
|
FileCard(
|
||||||
|
file = row.right,
|
||||||
|
selection = selection,
|
||||||
|
onClick = { onOpenDocument(row.right.resourceId) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Layer Dossiers : explorateur hiérarchique, en place sur la page. */
|
||||||
|
@Composable
|
||||||
|
private fun FolderBrowserContent(
|
||||||
|
moveMode: Boolean,
|
||||||
|
atRoot: Boolean,
|
||||||
|
browseFolderName: String?,
|
||||||
|
subFolders: List<FolderEntity>,
|
||||||
|
isImporting: Boolean,
|
||||||
|
error: String?,
|
||||||
|
onBrowseUp: () -> Unit,
|
||||||
|
onOpenBrowseFolder: (String) -> Unit,
|
||||||
|
onAddFolder: () -> Unit,
|
||||||
|
onCreateFolder: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
item(key = "breadcrumb") {
|
||||||
|
// La racine VaultDrop n'est pas affichée : on ne peut pas remonter au-dessus.
|
||||||
|
if (atRoot) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp, bottom = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBrowseUp) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.ArrowUpward,
|
||||||
|
contentDescription = stringResource(R.string.browse_up),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (browseFolderName != null) {
|
||||||
|
Text(
|
||||||
|
text = browseFolderName,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!moveMode) {
|
||||||
|
item(key = "create") {
|
||||||
|
if (atRoot) {
|
||||||
|
Button(
|
||||||
|
onClick = onAddFolder,
|
||||||
|
enabled = !isImporting,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Add, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.add_folder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onCreateFolder,
|
||||||
|
enabled = !isImporting,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = if (atRoot) 8.dp else 4.dp, bottom = 4.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.CreateNewFolder, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.create_folder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isImporting) {
|
||||||
|
item(key = "importing") {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error?.let { message ->
|
||||||
|
item(key = "error") {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(subFolders, key = { it.resourceId }) { folder ->
|
||||||
|
FolderRow(folder, onClick = { onOpenBrowseFolder(folder.resourceId) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subFolders.isEmpty() && !isImporting) {
|
||||||
|
item(key = "empty") {
|
||||||
|
Text(
|
||||||
|
text = stringResource(if (atRoot) R.string.no_folders_yet else R.string.empty_folder),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 32.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,126 +746,6 @@ private fun Uri.displayName(context: Context): String? = runCatching {
|
|||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun FolderListContent(
|
|
||||||
uiState: FolderListUiState,
|
|
||||||
isImporting: Boolean,
|
|
||||||
error: String?,
|
|
||||||
selection: SelectionState,
|
|
||||||
onAddFolder: () -> Unit,
|
|
||||||
onCreateFolder: () -> Unit,
|
|
||||||
onOpenFolder: (String) -> Unit,
|
|
||||||
onOpenDocument: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = modifier.fillMaxSize(),
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
) {
|
|
||||||
item(key = "actions") {
|
|
||||||
Button(
|
|
||||||
onClick = onAddFolder,
|
|
||||||
enabled = !isImporting,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 12.dp),
|
|
||||||
) {
|
|
||||||
Icon(Icons.Filled.Add, contentDescription = null)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(stringResource(R.string.add_folder))
|
|
||||||
}
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onCreateFolder,
|
|
||||||
enabled = !isImporting,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp, bottom = 4.dp),
|
|
||||||
) {
|
|
||||||
Icon(Icons.Filled.CreateNewFolder, contentDescription = null)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(stringResource(R.string.create_folder))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isImporting) {
|
|
||||||
item(key = "importing") {
|
|
||||||
LinearProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 12.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error?.let { message ->
|
|
||||||
item(key = "error") {
|
|
||||||
Text(
|
|
||||||
text = message,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 12.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.subFolders.isNotEmpty()) {
|
|
||||||
item(key = "folders-header") {
|
|
||||||
SectionHeader(stringResource(R.string.folders_section))
|
|
||||||
}
|
|
||||||
items(uiState.subFolders, key = { it.resourceId }) { folder ->
|
|
||||||
FolderRow(folder, onClick = { onOpenFolder(folder.resourceId) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.sections.isEmpty() && !isImporting) {
|
|
||||||
item(key = "empty") {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.no_files_yet),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 32.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uiState.sections.forEach { section ->
|
|
||||||
item(key = "header-${section.dayKey}") {
|
|
||||||
SectionHeader(section.dayLabel)
|
|
||||||
}
|
|
||||||
items(section.rows, key = { it.key }) { row ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
FileCard(
|
|
||||||
file = row.left,
|
|
||||||
selection = selection,
|
|
||||||
onClick = { onOpenDocument(row.left.resourceId) },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
if (row.right != null) {
|
|
||||||
FileCard(
|
|
||||||
file = row.right,
|
|
||||||
selection = selection,
|
|
||||||
onClick = { onOpenDocument(row.right.resourceId) },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SectionHeader(label: String) {
|
private fun SectionHeader(label: String) {
|
||||||
Text(
|
Text(
|
||||||
@@ -484,7 +757,7 @@ private fun SectionHeader(label: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Carte dossier de la section Dossiers — ouvre le dossier au tap. */
|
/** Carte dossier de l'explorateur — descend d'un niveau au tap. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun FolderRow(folder: FolderEntity, onClick: () -> Unit) {
|
private fun FolderRow(folder: FolderEntity, onClick: () -> Unit) {
|
||||||
Card(
|
Card(
|
||||||
|
|||||||
+21
-3
@@ -3,6 +3,13 @@ package com.vaultdrop.mobile.ui.folderlist
|
|||||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||||
|
|
||||||
|
/** Layer d'affichage de la page Fichiers. */
|
||||||
|
enum class HomeView {
|
||||||
|
FILES,
|
||||||
|
FOLDERS,
|
||||||
|
DASHBOARD,
|
||||||
|
}
|
||||||
|
|
||||||
/** Section d'un jour dans la grille d'accueil — miroir de `FileSection` (app/index.tsx). */
|
/** Section d'un jour dans la grille d'accueil — miroir de `FileSection` (app/index.tsx). */
|
||||||
data class FileSection(
|
data class FileSection(
|
||||||
/** Début du jour en millis (fuseau local) — clé stable de la section. */
|
/** Début du jour en millis (fuseau local) — clé stable de la section. */
|
||||||
@@ -19,14 +26,25 @@ data class FilePair(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class FolderListUiState(
|
data class FolderListUiState(
|
||||||
val subFolders: List<FolderEntity> = emptyList(),
|
/** Segment actif de la page (Fichiers / Dossiers / Dashboard). */
|
||||||
|
val view: HomeView = HomeView.DASHBOARD,
|
||||||
|
/** Vrai pendant le déplacement d'une sélection : la vue Dossiers est forcée. */
|
||||||
|
val moveMode: Boolean = false,
|
||||||
|
/** Vue à restaurer après déplacement/annulation. */
|
||||||
|
val viewBeforeMove: HomeView? = null,
|
||||||
|
/** Dossier courant de l'explorateur Dossiers (null = racine par défaut). */
|
||||||
|
val browseFolderId: String? = null,
|
||||||
|
/** Nom du dossier courant (racine incluse) — fil d'ariane de l'explorateur. */
|
||||||
|
val browseFolderName: String? = null,
|
||||||
|
/** Sous-dossiers visibles du dossier courant de l'explorateur. */
|
||||||
|
val browseSubFolders: List<FolderEntity> = emptyList(),
|
||||||
val sections: List<FileSection> = emptyList(),
|
val sections: List<FileSection> = emptyList(),
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
/** Erreur transitoire de création de dossier — affichée en Snackbar puis effacée. */
|
/** Erreur transitoire de création de dossier — affichée en Snackbar puis effacée. */
|
||||||
val createError: String? = null,
|
val createError: String? = null,
|
||||||
/** Dossiers disponibles pour le picker de déplacement (null = pas chargé). */
|
|
||||||
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,
|
||||||
|
/** Succès transitoire de déplacement — affiché en Snackbar puis effacé. */
|
||||||
|
val moveSuccess: Boolean = false,
|
||||||
)
|
)
|
||||||
+109
-41
@@ -8,7 +8,6 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.vaultdrop.mobile.R
|
import com.vaultdrop.mobile.R
|
||||||
import com.vaultdrop.mobile.auth.TokenProvider
|
import com.vaultdrop.mobile.auth.TokenProvider
|
||||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
|
||||||
import com.vaultdrop.mobile.data.local.referenceDate
|
import com.vaultdrop.mobile.data.local.referenceDate
|
||||||
import com.vaultdrop.mobile.data.preferences.DefaultRootStore
|
import com.vaultdrop.mobile.data.preferences.DefaultRootStore
|
||||||
import com.vaultdrop.mobile.data.remote.ApiException
|
import com.vaultdrop.mobile.data.remote.ApiException
|
||||||
@@ -19,10 +18,12 @@ 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
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -52,10 +53,11 @@ class FolderListViewModel @Inject constructor(
|
|||||||
private val _defaultRootId = MutableStateFlow(defaultRootStore.get())
|
private val _defaultRootId = MutableStateFlow(defaultRootStore.get())
|
||||||
val defaultRootId: StateFlow<String?> = _defaultRootId.asStateFlow()
|
val defaultRootId: StateFlow<String?> = _defaultRootId.asStateFlow()
|
||||||
|
|
||||||
private var subFolderJob: Job? = null
|
/** Dossier courant de l'explorateur Dossiers (null = racine par défaut). */
|
||||||
|
private val _browseFolderId = MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeSubFolders()
|
observeBrowse()
|
||||||
observeFiles()
|
observeFiles()
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
@@ -109,56 +111,92 @@ class FolderListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sous-dossiers visibles de la racine par défaut, ré-abonnés quand la
|
* Sélectionne le layer affiché de la page Fichiers. Désactivé en
|
||||||
* racine change (onboarding). Un dossier créé y apparaît immédiatement.
|
* sélection/déplacement (ux : on ne change pas de vue dans ces modes).
|
||||||
*/
|
*/
|
||||||
private fun observeSubFolders() {
|
fun selectView(view: HomeView) {
|
||||||
|
_uiState.update { it.copy(view = view) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explorateur Dossiers : sous-dossiers du dossier courant (`null` = racine
|
||||||
|
* par défaut), réabonnés quand la racine ou la position change. Quand la
|
||||||
|
* racine change (onboarding), on revient à la racine et on affiche son nom.
|
||||||
|
*/
|
||||||
|
private fun observeBrowse() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_defaultRootId.collect { rootId ->
|
_defaultRootId.collect { rootId ->
|
||||||
subFolderJob?.cancel()
|
_browseFolderId.value = null
|
||||||
if (rootId == null) {
|
if (rootId == null) {
|
||||||
_uiState.update { it.copy(subFolders = emptyList()) }
|
_uiState.update { it.copy(browseFolderId = null, browseFolderName = null) }
|
||||||
return@collect
|
} else {
|
||||||
}
|
val rootName = folderRepository.getFolder(rootId)?.name
|
||||||
subFolderJob = viewModelScope.launch {
|
_uiState.update { it.copy(browseFolderId = null, browseFolderName = rootName) }
|
||||||
folderRepository.observeSubFolders(rootId).collect { subFolders ->
|
|
||||||
_uiState.update { it.copy(subFolders = subFolders) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_defaultRootId.combine(_browseFolderId) { root, browse -> browse ?: root }
|
||||||
|
.flatMapLatest { parentId ->
|
||||||
|
if (parentId == null) flowOf(emptyList())
|
||||||
|
else folderRepository.observeSubFolders(parentId)
|
||||||
|
}
|
||||||
|
.collect { subFolders ->
|
||||||
|
_uiState.update { it.copy(browseSubFolders = subFolders) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Descend dans l'explorateur Dossiers (aussi en mode déplacement). */
|
||||||
|
fun openBrowseFolder(folderId: String) {
|
||||||
|
_browseFolderId.value = folderId
|
||||||
|
viewModelScope.launch {
|
||||||
|
val folder = folderRepository.getFolder(folderId)
|
||||||
|
_uiState.update { it.copy(browseFolderId = folderId, browseFolderName = folder?.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remonte au parent du dossier courant (reste à la racine si déjà en haut). */
|
||||||
|
fun browseUp() {
|
||||||
|
val current = _browseFolderId.value
|
||||||
|
viewModelScope.launch {
|
||||||
|
val parent = current?.let { folderRepository.getFolder(it)?.parentResourceId }
|
||||||
|
_browseFolderId.value = parent
|
||||||
|
val name = parent?.let { folderRepository.getFolder(it)?.name }
|
||||||
|
_uiState.update { it.copy(browseFolderId = parent, browseFolderName = name) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée un dossier physique dans la racine par défaut (VaultDrop) puis
|
* Crée un dossier physique dans le dossier courant de l'explorateur (racine
|
||||||
* l'enregistre en Room comme enfant de la racine.
|
* par défaut si on est en haut) puis l'enregistre en Room comme son enfant.
|
||||||
*/
|
*/
|
||||||
fun createFolderInDefaultRoot(name: String) {
|
fun createFolderInBrowse(name: String) {
|
||||||
val trimmed = name.trim()
|
val trimmed = name.trim()
|
||||||
if (trimmed.isBlank()) {
|
if (trimmed.isBlank()) {
|
||||||
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_name_required)) }
|
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_name_required)) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val rootId = _defaultRootId.value
|
val targetId = _browseFolderId.value ?: _defaultRootId.value
|
||||||
val root = rootId?.let { folderRepository.getFolder(it) }
|
val target = targetId?.let { folderRepository.getFolder(it) }
|
||||||
if (root == null) {
|
if (target == null) {
|
||||||
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_error)) }
|
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_error)) }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val message = if (root.uri == null) {
|
val message = if (target.uri == null) {
|
||||||
context.getString(R.string.new_folder_cloud_only)
|
context.getString(R.string.new_folder_cloud_only)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.new_folder_error)
|
context.getString(R.string.new_folder_error)
|
||||||
}
|
}
|
||||||
val created = safFolderCreator.createFolder(root.uri, trimmed)
|
val created = safFolderCreator.createFolder(target.uri, trimmed)
|
||||||
if (created == null) {
|
if (created == null) {
|
||||||
_uiState.update { it.copy(createError = message) }
|
_uiState.update { it.copy(createError = message) }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
folderRepository.saveFolder(
|
folderRepository.saveFolder(
|
||||||
input = SaveFolderInput(uri = created.toString(), name = trimmed, exists = true, createdInApp = true),
|
input = SaveFolderInput(uri = created.toString(), name = trimmed, exists = true, createdInApp = true),
|
||||||
parentResourceId = root.resourceId,
|
parentResourceId = target.resourceId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,30 +206,60 @@ class FolderListViewModel @Inject constructor(
|
|||||||
_uiState.update { it.copy(createError = null) }
|
_uiState.update { it.copy(createError = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Charge les dossiers éligibles pour le picker de déplacement. */
|
/**
|
||||||
fun loadMoveFolders() {
|
* Entre en mode déplacement : vue Dossiers forcée, explorateur ramené à la
|
||||||
|
* racine pour un choix de destination prévisible.
|
||||||
|
*/
|
||||||
|
fun startMove() {
|
||||||
|
_browseFolderId.value = null
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val moveFolders = folderRepository.getCreatedInApp()
|
val rootName = _defaultRootId.value?.let { folderRepository.getFolder(it)?.name }
|
||||||
_uiState.update { it.copy(moveFolders = moveFolders) }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
viewBeforeMove = it.view,
|
||||||
|
view = HomeView.FOLDERS,
|
||||||
|
moveMode = true,
|
||||||
|
browseFolderId = null,
|
||||||
|
browseFolderName = rootName,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Déplace les fichiers vers le dossier cible puis ferme le picker. */
|
/** Annule le mode déplacement : restaure la vue, la sélection est conservée. */
|
||||||
fun moveSelectedFiles(resourceIds: List<String>, targetFolderId: String) {
|
fun cancelMove() {
|
||||||
viewModelScope.launch {
|
val previous = _uiState.value.viewBeforeMove ?: HomeView.DASHBOARD
|
||||||
runCatching { fileMover.moveFiles(resourceIds, targetFolderId) }
|
_uiState.update {
|
||||||
.onFailure {
|
it.copy(
|
||||||
_uiState.update { state ->
|
moveMode = false,
|
||||||
state.copy(moveError = context.getString(R.string.move_files_error))
|
view = previous,
|
||||||
}
|
viewBeforeMove = null,
|
||||||
}
|
)
|
||||||
_uiState.update { it.copy(moveFolders = null) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ferme le picker sans déplacer (annulation). */
|
/**
|
||||||
fun closeMovePicker() {
|
* Déplace la sélection dans le dossier courant de l'explorateur (la racine
|
||||||
_uiState.update { it.copy(moveFolders = null) }
|
* par défaut si on est en haut) puis sort du mode déplacement.
|
||||||
|
*/
|
||||||
|
fun moveSelectionHere(resourceIds: List<String>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val target = _browseFolderId.value ?: _defaultRootId.value
|
||||||
|
if (target == null || runCatching { fileMover.moveFiles(resourceIds, target) }.isFailure) {
|
||||||
|
_uiState.update { it.copy(moveError = context.getString(R.string.move_files_error)) }
|
||||||
|
} else {
|
||||||
|
_uiState.update { it.copy(moveSuccess = true) }
|
||||||
|
}
|
||||||
|
val previous = _uiState.value.viewBeforeMove ?: HomeView.DASHBOARD
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(moveMode = false, view = previous, viewBeforeMove = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Consomme le succès transitoire de déplacement (Snackbar). */
|
||||||
|
fun clearMoveSuccess() {
|
||||||
|
_uiState.update { it.copy(moveSuccess = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearMoveError() {
|
fun clearMoveError() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
@@ -20,6 +21,7 @@ import androidx.compose.material.icons.filled.Settings
|
|||||||
import androidx.compose.material.icons.outlined.Folder
|
import androidx.compose.material.icons.outlined.Folder
|
||||||
import androidx.compose.material.icons.outlined.Search
|
import androidx.compose.material.icons.outlined.Search
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -166,6 +168,50 @@ fun SelectionNavBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Barre du mode déplacement : bouton « Déplacer ici (N) » vers le dossier courant. */
|
||||||
|
@Composable
|
||||||
|
fun MoveTargetBar(
|
||||||
|
fileCount: Int,
|
||||||
|
onMoveHere: () -> Unit,
|
||||||
|
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(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onMoveHere,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(min = 52.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.DriveFileMove,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.move_here_count, fileCount),
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectionAction(
|
private fun SelectionAction(
|
||||||
label: String,
|
label: String,
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ fun NavGraph(
|
|||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = onTabSelected,
|
onTabSelected = onTabSelected,
|
||||||
onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
|
onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
|
||||||
onOpenFolder = { id -> navController.navigate(Routes.folder(id)) },
|
|
||||||
onBuildPdf = { ids -> navController.navigate(Routes.pdfBuilder(ids)) },
|
onBuildPdf = { ids -> navController.navigate(Routes.pdfBuilder(ids)) },
|
||||||
syncViewModel = syncViewModel,
|
syncViewModel = syncViewModel,
|
||||||
connectionStatusViewModel = connectionStatusViewModel,
|
connectionStatusViewModel = connectionStatusViewModel,
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
<string name="files">Files</string>
|
<string name="files">Files</string>
|
||||||
<string name="folder">Folder</string>
|
<string name="folder">Folder</string>
|
||||||
|
<string name="view_files">Files</string>
|
||||||
|
<string name="view_folders">Folders</string>
|
||||||
|
<string name="view_dashboard">Dashboard</string>
|
||||||
|
<string name="dashboard_coming_soon">Dashboard coming soon</string>
|
||||||
|
<string name="browse_up">Up</string>
|
||||||
|
<string name="move_here_count">Move here (%1$d)</string>
|
||||||
|
<string name="move_pick_title">Move %1$d file(s)…</string>
|
||||||
|
<string name="move_success">File(s) moved</string>
|
||||||
<string name="default_root_title">Choose your VaultDrop folder</string>
|
<string name="default_root_title">Choose your VaultDrop folder</string>
|
||||||
<string name="default_root_message">VaultDrop needs a folder to store all the PDFs it creates. You can also create a new VaultDrop folder right in the picker. This step is required, once only.</string>
|
<string name="default_root_message">VaultDrop needs a folder to store all the PDFs it creates. You can also create a new VaultDrop folder right in the picker. This step is required, once only.</string>
|
||||||
<string name="default_root_pick">Choose my VaultDrop folder</string>
|
<string name="default_root_pick">Choose my VaultDrop folder</string>
|
||||||
|
|||||||
@@ -8,6 +8,14 @@
|
|||||||
<string name="default_root_folder_label">VaultDrop</string>
|
<string name="default_root_folder_label">VaultDrop</string>
|
||||||
<string name="files">Fichiers</string>
|
<string name="files">Fichiers</string>
|
||||||
<string name="folder">Dossier</string>
|
<string name="folder">Dossier</string>
|
||||||
|
<string name="view_files">Fichiers</string>
|
||||||
|
<string name="view_folders">Dossiers</string>
|
||||||
|
<string name="view_dashboard">Dashboard</string>
|
||||||
|
<string name="dashboard_coming_soon">Dashboard à venir</string>
|
||||||
|
<string name="browse_up">Remonter</string>
|
||||||
|
<string name="move_here_count">Déplacer ici (%1$d)</string>
|
||||||
|
<string name="move_pick_title">Déplacer %1$d fichier(s)…</string>
|
||||||
|
<string name="move_success">Fichier(s) déplacé(s)</string>
|
||||||
<string name="add_folder">Ajouter un dossier</string>
|
<string name="add_folder">Ajouter un dossier</string>
|
||||||
<string name="create_folder">Créer un dossier</string>
|
<string name="create_folder">Créer un dossier</string>
|
||||||
<string name="new_folder_name_title">Nom du dossier</string>
|
<string name="new_folder_name_title">Nom du dossier</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user