create folders
This commit is contained in:
+34
@@ -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
|
||||||
|
|||||||
+50
@@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+95
@@ -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
|
||||||
|
|||||||
+4
@@ -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,
|
||||||
)
|
)
|
||||||
+67
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user