create folders

This commit is contained in:
m
2026-09-12 15:07:52 +02:00
parent 5f90cc9059
commit bada082c35
8 changed files with 265 additions and 0 deletions
@@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons 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.Folder import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.MergeType import androidx.compose.material.icons.filled.MergeType
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
@@ -26,11 +27,16 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
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
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.res.stringResource import androidx.compose.ui.res.stringResource
@@ -44,6 +50,7 @@ import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.data.local.entity.FolderEntity 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.ui.components.FileCategoryIcon import com.vaultdrop.mobile.ui.components.FileCategoryIcon
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
@@ -64,11 +71,21 @@ fun FolderDetailScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle() val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val selection = rememberSelectionState() val selection = rememberSelectionState()
var showCreateDialog by remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(uiState.folderMissing) { LaunchedEffect(uiState.folderMissing) {
if (uiState.folderMissing) onBack() if (uiState.folderMissing) onBack()
} }
LaunchedEffect(uiState.createError) {
val createError = uiState.createError
if (createError != null) {
snackbarHostState.showSnackbar(createError)
viewModel.clearCreateError()
}
}
BackHandler(enabled = selection.active) { selection.clear() } BackHandler(enabled = selection.active) { selection.clear() }
Scaffold( Scaffold(
@@ -114,6 +131,12 @@ fun FolderDetailScreen(
) )
} }
} else { } else {
IconButton(onClick = { showCreateDialog = true }) {
Icon(
Icons.Filled.CreateNewFolder,
contentDescription = stringResource(R.string.create_folder),
)
}
ServerStatusBadge( ServerStatusBadge(
status = connectionStatus, status = connectionStatus,
onClick = connectionStatusViewModel::checkNow, onClick = connectionStatusViewModel::checkNow,
@@ -125,6 +148,7 @@ fun FolderDetailScreen(
}, },
) )
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { padding -> ) { padding ->
FolderDetailContent( FolderDetailContent(
subFolders = uiState.subFolders, subFolders = uiState.subFolders,
@@ -137,6 +161,16 @@ fun FolderDetailScreen(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
) )
} }
if (showCreateDialog) {
FolderNameDialog(
onDismiss = { showCreateDialog = false },
onConfirm = { name ->
showCreateDialog = false
viewModel.createFolder(name)
},
)
}
} }
@Composable @Composable
@@ -1,14 +1,19 @@
package com.vaultdrop.mobile.ui.folderdetail package com.vaultdrop.mobile.ui.folderdetail
import android.content.Context
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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.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.features.saf.SafFolderCreator
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
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
@@ -23,6 +28,8 @@ data class FolderDetailUiState(
val files: List<FileEntity> = emptyList(), val files: List<FileEntity> = emptyList(),
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val error: String? = null, val error: String? = null,
/** Erreur transitoire de création — affichée en Snackbar puis effacée. */
val createError: String? = null,
) )
@HiltViewModel @HiltViewModel
@@ -31,6 +38,8 @@ class FolderDetailViewModel @Inject constructor(
private val folderRepository: FolderRepository, private val folderRepository: FolderRepository,
private val fileRepository: FileRepository, private val fileRepository: FileRepository,
private val tokenProvider: TokenProvider, private val tokenProvider: TokenProvider,
private val safFolderCreator: SafFolderCreator,
@ApplicationContext private val context: Context,
) : ViewModel() { ) : ViewModel() {
private val folderResourceId: String = private val folderResourceId: String =
@@ -78,4 +87,45 @@ class FolderDetailViewModel @Inject constructor(
_uiState.update { it.copy(isRefreshing = false) } _uiState.update { it.copy(isRefreshing = false) }
} }
} }
/**
* Crée un sous-dossier physique dans le dossier courant puis l'enregistre
* en Room. Impossible si le dossier parent est cloud-only (pas d'uri SAF).
*/
fun createFolder(name: String) {
val trimmed = name.trim()
if (trimmed.isBlank()) {
_uiState.update {
it.copy(createError = context.getString(R.string.new_folder_name_required))
}
return
}
viewModelScope.launch {
val folder = _uiState.value.folder
?: run {
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_error)) }
return@launch
}
val physical = folder.uri != null
val created = safFolderCreator.createFolder(folder.uri, trimmed)
if (created == null) {
val message = if (physical) {
context.getString(R.string.new_folder_error)
} else {
context.getString(R.string.new_folder_cloud_only)
}
_uiState.update { it.copy(createError = message) }
return@launch
}
folderRepository.saveFolder(
input = SaveFolderInput(uri = created.toString(), name = trimmed, exists = true),
parentResourceId = folder.resourceId,
)
}
}
/** Consomme une erreur transitoire de création (Snackbar). */
fun clearCreateError() {
_uiState.update { it.copy(createError = null) }
}
} }
@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -28,6 +29,8 @@ 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.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.CreateNewFolder
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.MergeType 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.Button import androidx.compose.material3.Button
@@ -38,7 +41,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
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
@@ -60,9 +66,11 @@ 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.FolderEntity
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.ui.components.FileCategoryIcon import com.vaultdrop.mobile.ui.components.FileCategoryIcon
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
@@ -79,6 +87,7 @@ 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,
@@ -91,6 +100,16 @@ fun FolderListScreen(
val context = LocalContext.current val context = LocalContext.current
val folderLabel = stringResource(R.string.folder) val folderLabel = stringResource(R.string.folder)
val selection = rememberSelectionState() val selection = rememberSelectionState()
var showCreateDialog by remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
val createError = uiState.createError
LaunchedEffect(createError) {
if (createError != null) {
snackbarHostState.showSnackbar(createError)
viewModel.clearCreateError()
}
}
// 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)
@@ -210,6 +229,7 @@ fun FolderListScreen(
bottomBar = { bottomBar = {
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected) FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { padding -> ) { padding ->
FolderListContent( FolderListContent(
uiState = uiState, uiState = uiState,
@@ -217,10 +237,22 @@ fun FolderListScreen(
error = uiState.error ?: importState.error, error = uiState.error ?: importState.error,
selection = selection, selection = selection,
onAddFolder = { pickFolderLauncher.launch(null) }, onAddFolder = { pickFolderLauncher.launch(null) },
onCreateFolder = { showCreateDialog = true },
onOpenFolder = onOpenFolder,
onOpenDocument = onOpenDocument, onOpenDocument = onOpenDocument,
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
) )
} }
if (showCreateDialog) {
FolderNameDialog(
onDismiss = { showCreateDialog = false },
onConfirm = { name ->
showCreateDialog = false
viewModel.createFolderInDefaultRoot(name)
},
)
}
} }
/** Écran obligatoire du premier lancement : choix/création de la racine VaultDrop. */ /** Écran obligatoire du premier lancement : choix/création de la racine VaultDrop. */
@@ -298,6 +330,8 @@ private fun FolderListContent(
error: String?, error: String?,
selection: SelectionState, selection: SelectionState,
onAddFolder: () -> Unit, onAddFolder: () -> Unit,
onCreateFolder: () -> Unit,
onOpenFolder: (String) -> Unit,
onOpenDocument: (String) -> Unit, onOpenDocument: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@@ -317,6 +351,17 @@ private fun FolderListContent(
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.add_folder)) 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) { if (isImporting) {
@@ -342,6 +387,15 @@ private fun FolderListContent(
} }
} }
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) { if (uiState.sections.isEmpty() && !isImporting) {
item(key = "empty") { item(key = "empty") {
Text( Text(
@@ -400,6 +454,47 @@ private fun SectionHeader(label: String) {
) )
} }
/** Carte dossier de la section Dossiers — ouvre le dossier au tap. */
@Composable
private fun FolderRow(folder: FolderEntity, onClick: () -> Unit) {
Card(
onClick = onClick,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Icon(
imageVector = Icons.Filled.Folder,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp),
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = folder.name,
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
fontWeight = FontWeight.Medium,
)
Text(
text = stringResource(R.string.folder),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
/** Carte fichier — clic long pour la sélection multi-fichiers, clic pour ouvrir. */ /** Carte fichier — clic long pour la sélection multi-fichiers, clic pour ouvrir. */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -1,6 +1,7 @@
package com.vaultdrop.mobile.ui.folderlist 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
/** 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(
@@ -18,7 +19,10 @@ data class FilePair(
) )
data class FolderListUiState( data class FolderListUiState(
val subFolders: 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. */
val createError: String? = null,
) )
@@ -8,14 +8,17 @@ 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
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.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
@@ -36,6 +39,7 @@ class FolderListViewModel @Inject constructor(
private val fileRepository: FileRepository, private val fileRepository: FileRepository,
private val tokenProvider: TokenProvider, private val tokenProvider: TokenProvider,
private val defaultRootStore: DefaultRootStore, private val defaultRootStore: DefaultRootStore,
private val safFolderCreator: SafFolderCreator,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : ViewModel() { ) : ViewModel() {
@@ -46,7 +50,10 @@ 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
init { init {
observeSubFolders()
observeFiles() observeFiles()
refresh() refresh()
} }
@@ -94,6 +101,66 @@ class FolderListViewModel @Inject constructor(
return saved.resourceId return saved.resourceId
} }
/**
* Sous-dossiers visibles de la racine par défaut, ré-abonnés quand la
* racine change (onboarding). Un dossier créé y apparaît immédiatement.
*/
private fun observeSubFolders() {
viewModelScope.launch {
_defaultRootId.collect { rootId ->
subFolderJob?.cancel()
if (rootId == null) {
_uiState.update { it.copy(subFolders = emptyList()) }
return@collect
}
subFolderJob = viewModelScope.launch {
folderRepository.observeSubFolders(rootId).collect { subFolders ->
_uiState.update { it.copy(subFolders = subFolders) }
}
}
}
}
}
/**
* Crée un dossier physique dans la racine par défaut (VaultDrop) puis
* l'enregistre en Room comme enfant de la racine.
*/
fun createFolderInDefaultRoot(name: String) {
val trimmed = name.trim()
if (trimmed.isBlank()) {
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_name_required)) }
return
}
viewModelScope.launch {
val rootId = _defaultRootId.value
val root = rootId?.let { folderRepository.getFolder(it) }
if (root == null) {
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_error)) }
return@launch
}
val message = if (root.uri == null) {
context.getString(R.string.new_folder_cloud_only)
} else {
context.getString(R.string.new_folder_error)
}
val created = safFolderCreator.createFolder(root.uri, trimmed)
if (created == null) {
_uiState.update { it.copy(createError = message) }
return@launch
}
folderRepository.saveFolder(
input = SaveFolderInput(uri = created.toString(), name = trimmed, exists = true),
parentResourceId = root.resourceId,
)
}
}
/** Consomme une erreur transitoire de création (Snackbar). */
fun clearCreateError() {
_uiState.update { it.copy(createError = null) }
}
/** 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 {
@@ -80,6 +80,7 @@ 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,
@@ -8,6 +8,13 @@
<string name="default_root_pick">Choose my VaultDrop folder</string> <string name="default_root_pick">Choose my VaultDrop folder</string>
<string name="default_root_folder_label">VaultDrop</string> <string name="default_root_folder_label">VaultDrop</string>
<string name="add_folder">Add a folder</string> <string name="add_folder">Add a folder</string>
<string name="create_folder">Create a folder</string>
<string name="new_folder_name_title">Folder name</string>
<string name="new_folder_name_hint">e.g. invoices</string>
<string name="new_folder_name_required">The folder name cannot be empty.</string>
<string name="new_folder_cloud_only">This folder is cloud-only — import a physical folder first to create subfolders inside it.</string>
<string name="new_folder_error">Could not create the folder.</string>
<string name="folders_section">Folders</string>
<string name="no_folders_yet">No folders yet. Tap "Add a folder" to sync a folder.</string> <string name="no_folders_yet">No folders yet. Tap "Add a folder" to sync a folder.</string>
<string name="no_files_yet">No files yet. Tap "Add a folder" to sync your files.</string> <string name="no_files_yet">No files yet. Tap "Add a folder" to sync your files.</string>
<string name="empty_folder">Empty folder — next sync will refresh it.</string> <string name="empty_folder">Empty folder — next sync will refresh it.</string>
@@ -9,6 +9,13 @@
<string name="files">Fichiers</string> <string name="files">Fichiers</string>
<string name="folder">Dossier</string> <string name="folder">Dossier</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="new_folder_name_title">Nom du dossier</string>
<string name="new_folder_name_hint">ex. factures</string>
<string name="new_folder_name_required">Le nom du dossier ne peut pas être vide.</string>
<string name="new_folder_cloud_only">Ce dossier est disponible uniquement sur le cloud — importe d\'abord un dossier physique pour y créer des sous-dossiers.</string>
<string name="new_folder_error">Impossible de créer le dossier.</string>
<string name="folders_section">Dossiers</string>
<string name="no_folders_yet">Aucun dossier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser un dossier.</string> <string name="no_folders_yet">Aucun dossier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser un dossier.</string>
<string name="no_files_yet">Aucun fichier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser tes fichiers.</string> <string name="no_files_yet">Aucun fichier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser tes fichiers.</string>
<string name="empty_folder">Dossier vide — le prochain syncDevice l\'actualisera.</string> <string name="empty_folder">Dossier vide — le prochain syncDevice l\'actualisera.</string>