create sub folders

This commit is contained in:
m
2026-09-12 19:21:36 +02:00
parent b7eb291c84
commit 06361ae62f
9 changed files with 109 additions and 34 deletions
@@ -1,5 +1,6 @@
package com.vaultdrop.mobile.features.saf package com.vaultdrop.mobile.features.saf
import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
@@ -11,8 +12,23 @@ import android.provider.DocumentsContract
*/ */
object SafUris { object SafUris {
/**
* Convertit une uri SAF en uri *document* utilisable par createDocument /
* moveDocument.
*
* Attention : `DocumentsContract.isTreeUri()` renvoie `true` pour toute uri
* commençant par `tree/`, y compris les uri *document* sous un tree
* (`tree/<treeId>/document/<docId>`). On doit donc les renvoyer telles
* quelles — sinon le `documentId` (ie. le sous-dossier) est remplacé par la
* racine de l'arbre, et un dossier/fichier ciblé sous un dossier imbriqué
* est créé/déplacé à la racine.
*/
fun toDocumentUri(uri: String?): Uri? { fun toDocumentUri(uri: String?): Uri? {
val raw = uri?.let(Uri::parse) ?: return null val raw = uri?.let(Uri::parse) ?: return null
val segments = raw.pathSegments
val isDocumentUnderTree = raw.scheme == ContentResolver.SCHEME_CONTENT &&
segments.size >= 3 && segments[0] == "tree" && segments[2] == "document"
if (isDocumentUnderTree) return raw
return if (DocumentsContract.isTreeUri(raw)) { return if (DocumentsContract.isTreeUri(raw)) {
DocumentsContract.buildDocumentUriUsingTree( DocumentsContract.buildDocumentUriUsingTree(
raw, raw,
@@ -0,0 +1,65 @@
package com.vaultdrop.mobile.ui.dashboard
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen(
selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit,
connectionStatusViewModel: ConnectionStatusViewModel,
) {
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.dashboard)) },
actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
},
)
},
bottomBar = {
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
},
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.dashboard_coming_soon),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp),
)
}
}
}
@@ -32,7 +32,6 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowUpward 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.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
@@ -352,14 +351,6 @@ private fun HomeViewSelector(
onClick = { onSelect(HomeView.FOLDERS) }, onClick = { onSelect(HomeView.FOLDERS) },
modifier = Modifier.weight(1f), 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),
)
} }
} }
} }
@@ -431,7 +422,6 @@ private fun HomeViewContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (view) { when (view) {
HomeView.DASHBOARD -> DashboardPlaceholder(modifier = modifier)
HomeView.FILES -> FileGridContent( HomeView.FILES -> FileGridContent(
sections = sections, sections = sections,
isImporting = isImporting, isImporting = isImporting,
@@ -456,22 +446,6 @@ private fun HomeViewContent(
} }
} }
/** 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. */ /** Layer Fichiers : tous les fichiers visibles, groupés par jour. */
@Composable @Composable
private fun FileGridContent( private fun FileGridContent(
@@ -7,7 +7,6 @@ import com.vaultdrop.mobile.data.local.entity.FolderEntity
enum class HomeView { enum class HomeView {
FILES, FILES,
FOLDERS, 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). */
@@ -26,8 +25,8 @@ data class FilePair(
) )
data class FolderListUiState( data class FolderListUiState(
/** Segment actif de la page (Fichiers / Dossiers / Dashboard). */ /** Segment actif de la page (Fichiers / Dossiers). */
val view: HomeView = HomeView.DASHBOARD, val view: HomeView = HomeView.FILES,
/** Vrai pendant le déplacement d'une sélection : la vue Dossiers est forcée. */ /** Vrai pendant le déplacement d'une sélection : la vue Dossiers est forcée. */
val moveMode: Boolean = false, val moveMode: Boolean = false,
/** Vue à restaurer après déplacement/annulation. */ /** Vue à restaurer après déplacement/annulation. */
@@ -236,7 +236,7 @@ class FolderListViewModel @Inject constructor(
/** Annule le mode déplacement : restaure la vue, la sélection est conservée. */ /** Annule le mode déplacement : restaure la vue, la sélection est conservée. */
fun cancelMove() { fun cancelMove() {
val previous = _uiState.value.viewBeforeMove ?: HomeView.DASHBOARD val previous = _uiState.value.viewBeforeMove ?: HomeView.FILES
_uiState.update { _uiState.update {
it.copy( it.copy(
moveMode = false, moveMode = false,
@@ -258,7 +258,7 @@ class FolderListViewModel @Inject constructor(
} else { } else {
_uiState.update { it.copy(moveSuccess = true) } _uiState.update { it.copy(moveSuccess = true) }
} }
val previous = _uiState.value.viewBeforeMove ?: HomeView.DASHBOARD val previous = _uiState.value.viewBeforeMove ?: HomeView.FILES
_uiState.update { _uiState.update {
it.copy(moveMode = false, view = previous, viewBeforeMove = null) it.copy(moveMode = false, view = previous, viewBeforeMove = null)
} }
@@ -13,11 +13,13 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
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.Dashboard
import androidx.compose.material.icons.filled.DriveFileMove import androidx.compose.material.icons.filled.DriveFileMove
import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.MergeType import androidx.compose.material.icons.filled.MergeType
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Dashboard
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
@@ -42,6 +44,7 @@ enum class NavTab(val route: String) {
FILES(Routes.FILES), FILES(Routes.FILES),
SEARCH(Routes.SEARCH), SEARCH(Routes.SEARCH),
SETTINGS(Routes.SETTINGS), SETTINGS(Routes.SETTINGS),
DASHBOARD(Routes.DASHBOARD),
} }
private data class TabItem( private data class TabItem(
@@ -53,6 +56,7 @@ private data class TabItem(
private val TAB_ITEMS = listOf( private val TAB_ITEMS = listOf(
TabItem(NavTab.FILES, Icons.Filled.Folder, Icons.Outlined.Folder, R.string.nav_files), TabItem(NavTab.FILES, Icons.Filled.Folder, Icons.Outlined.Folder, R.string.nav_files),
TabItem(NavTab.DASHBOARD, Icons.Filled.Dashboard, Icons.Outlined.Dashboard, R.string.nav_dashboard),
TabItem(NavTab.SEARCH, Icons.Filled.Search, Icons.Outlined.Search, R.string.nav_search), TabItem(NavTab.SEARCH, Icons.Filled.Search, Icons.Outlined.Search, R.string.nav_search),
TabItem(NavTab.SETTINGS, Icons.Filled.Settings, Icons.Outlined.Settings, R.string.nav_settings), TabItem(NavTab.SETTINGS, Icons.Filled.Settings, Icons.Outlined.Settings, R.string.nav_settings),
) )
@@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@@ -18,6 +19,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.auth.AuthViewModel import com.vaultdrop.mobile.ui.auth.AuthViewModel
import com.vaultdrop.mobile.ui.dashboard.DashboardScreen
import com.vaultdrop.mobile.ui.document.DocumentViewerScreen import com.vaultdrop.mobile.ui.document.DocumentViewerScreen
import com.vaultdrop.mobile.ui.folderdetail.FolderDetailScreen import com.vaultdrop.mobile.ui.folderdetail.FolderDetailScreen
import com.vaultdrop.mobile.ui.folderlist.FolderListScreen import com.vaultdrop.mobile.ui.folderlist.FolderListScreen
@@ -29,6 +31,7 @@ object Routes {
const val FILES = "files" const val FILES = "files"
const val SEARCH = "search" const val SEARCH = "search"
const val SETTINGS = "settings" const val SETTINGS = "settings"
const val DASHBOARD = "dashboard"
const val FOLDER_DETAIL = "folder/{folderResourceId}" const val FOLDER_DETAIL = "folder/{folderResourceId}"
const val ARG_FOLDER = "folderResourceId" const val ARG_FOLDER = "folderResourceId"
const val DOCUMENT = "document/{documentResourceId}" const val DOCUMENT = "document/{documentResourceId}"
@@ -45,6 +48,7 @@ private fun String?.toNavTab(): NavTab = when (this) {
Routes.FILES -> NavTab.FILES Routes.FILES -> NavTab.FILES
Routes.SEARCH -> NavTab.SEARCH Routes.SEARCH -> NavTab.SEARCH
Routes.SETTINGS -> NavTab.SETTINGS Routes.SETTINGS -> NavTab.SETTINGS
Routes.DASHBOARD -> NavTab.DASHBOARD
else -> NavTab.FILES else -> NavTab.FILES
} }
@@ -64,8 +68,12 @@ fun NavGraph(
val onTabSelected: (NavTab) -> Unit = { tab -> val onTabSelected: (NavTab) -> Unit = { tab ->
if (tab.route != selectedTab.route) { if (tab.route != selectedTab.route) {
navController.navigate(tab.route) { navController.navigate(tab.route) {
popUpTo(Routes.FILES) { inclusive = true } // Sauve l'entrée courante (Fichiers) au lieu de la détruire :
// le ViewModel (et sa position d'exploration) survit au changement
// d'onglet, sinon « Créer un dossier » retombe sur la racine.
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true launchSingleTop = true
restoreState = true
} }
} }
} }
@@ -102,6 +110,13 @@ fun NavGraph(
connectionStatusViewModel = connectionStatusViewModel, connectionStatusViewModel = connectionStatusViewModel,
) )
} }
composable(Routes.DASHBOARD) {
DashboardScreen(
selectedTab = selectedTab,
onTabSelected = onTabSelected,
connectionStatusViewModel = connectionStatusViewModel,
)
}
composable( composable(
route = Routes.FOLDER_DETAIL, route = Routes.FOLDER_DETAIL,
arguments = listOf( arguments = listOf(
@@ -5,7 +5,6 @@
<string name="folder">Folder</string> <string name="folder">Folder</string>
<string name="view_files">Files</string> <string name="view_files">Files</string>
<string name="view_folders">Folders</string> <string name="view_folders">Folders</string>
<string name="view_dashboard">Dashboard</string>
<string name="dashboard_coming_soon">Dashboard coming soon</string> <string name="dashboard_coming_soon">Dashboard coming soon</string>
<string name="browse_up">Up</string> <string name="browse_up">Up</string>
<string name="move_here_count">Move here (%1$d)</string> <string name="move_here_count">Move here (%1$d)</string>
@@ -58,6 +57,8 @@
<string name="nav_files">Files</string> <string name="nav_files">Files</string>
<string name="nav_search">Search</string> <string name="nav_search">Search</string>
<string name="nav_settings">Settings</string> <string name="nav_settings">Settings</string>
<string name="nav_dashboard">Dashboard</string>
<string name="dashboard">Dashboard</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="settings_dark_mode">Dark mode</string> <string name="settings_dark_mode">Dark mode</string>
<string name="settings_dark_mode_on">Dark theme active</string> <string name="settings_dark_mode_on">Dark theme active</string>
@@ -10,7 +10,6 @@
<string name="folder">Dossier</string> <string name="folder">Dossier</string>
<string name="view_files">Fichiers</string> <string name="view_files">Fichiers</string>
<string name="view_folders">Dossiers</string> <string name="view_folders">Dossiers</string>
<string name="view_dashboard">Dashboard</string>
<string name="dashboard_coming_soon">Dashboard à venir</string> <string name="dashboard_coming_soon">Dashboard à venir</string>
<string name="browse_up">Remonter</string> <string name="browse_up">Remonter</string>
<string name="move_here_count">Déplacer ici (%1$d)</string> <string name="move_here_count">Déplacer ici (%1$d)</string>
@@ -59,6 +58,8 @@
<string name="nav_files">Fichiers</string> <string name="nav_files">Fichiers</string>
<string name="nav_search">Recherche</string> <string name="nav_search">Recherche</string>
<string name="nav_settings">Réglages</string> <string name="nav_settings">Réglages</string>
<string name="nav_dashboard">Dashboard</string>
<string name="dashboard">Dashboard</string>
<string name="settings">Réglages</string> <string name="settings">Réglages</string>
<string name="settings_dark_mode">Mode sombre</string> <string name="settings_dark_mode">Mode sombre</string>
<string name="settings_dark_mode_on">Thème sombre actif</string> <string name="settings_dark_mode_on">Thème sombre actif</string>