This commit is contained in:
m
2026-09-11 13:43:11 +02:00
parent 3a57532b40
commit 93c9719969
7 changed files with 267 additions and 1 deletions
@@ -36,10 +36,14 @@ 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.FolderEntity import com.vaultdrop.mobile.data.local.entity.FolderEntity
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun FolderListScreen( fun FolderListScreen(
selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit,
onOpenFolder: (String) -> Unit, onOpenFolder: (String) -> Unit,
viewModel: FolderListViewModel = hiltViewModel(), viewModel: FolderListViewModel = hiltViewModel(),
) { ) {
@@ -56,6 +60,9 @@ fun FolderListScreen(
}, },
) )
}, },
bottomBar = {
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
},
) { padding -> ) { padding ->
FolderListContent( FolderListContent(
uiState = uiState, uiState = uiState,
@@ -0,0 +1,121 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package com.vaultdrop.mobile.ui.navigation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vaultdrop.mobile.R
enum class NavTab(val route: String) {
FILES(Routes.FILES),
SEARCH(Routes.SEARCH),
SETTINGS(Routes.SETTINGS),
}
private data class TabItem(
val tab: NavTab,
val filled: ImageVector,
val outlined: ImageVector,
val labelRes: Int,
)
private val TAB_ITEMS = listOf(
TabItem(NavTab.FILES, Icons.Filled.Folder, Icons.Outlined.Folder, R.string.nav_files),
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),
)
@Composable
fun FloatingNavBar(
selected: NavTab,
onSelect: (NavTab) -> 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(vertical = 8.dp, horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
TAB_ITEMS.forEach { item ->
val isActive = item.tab == selected
val icon = if (isActive) item.filled else item.outlined
val tint = if (isActive) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant
Surface(
onClick = { onSelect(item.tab) },
shape = RoundedCornerShape(20.dp),
color = if (isActive) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surface,
modifier = Modifier.weight(1f),
) {
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 = stringResource(item.labelRes),
color = tint,
fontSize = 11.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = if (isActive) FontWeight.Bold else FontWeight.Medium,
modifier = Modifier.padding(top = 2.dp),
)
}
}
}
}
}
}
}
}
@@ -1,34 +1,74 @@
package com.vaultdrop.mobile.ui.navigation package com.vaultdrop.mobile.ui.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
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
import com.vaultdrop.mobile.ui.search.SearchScreen
import com.vaultdrop.mobile.ui.settings.SettingsScreen
object Routes { object Routes {
const val FILES = "files" const val FILES = "files"
const val SEARCH = "search"
const val SETTINGS = "settings"
const val FOLDER_DETAIL = "folder/{folderResourceId}" const val FOLDER_DETAIL = "folder/{folderResourceId}"
const val ARG_FOLDER = "folderResourceId" const val ARG_FOLDER = "folderResourceId"
fun folder(folderResourceId: String): String = "folder/$folderResourceId" fun folder(folderResourceId: String): String = "folder/$folderResourceId"
} }
private fun String?.toNavTab(): NavTab = when (this) {
Routes.FILES -> NavTab.FILES
Routes.SEARCH -> NavTab.SEARCH
Routes.SETTINGS -> NavTab.SETTINGS
else -> NavTab.FILES
}
@Composable @Composable
fun NavGraph() { fun NavGraph() {
val navController = rememberNavController() val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
val selectedTab = backStackEntry?.destination?.route.toNavTab()
val onTabSelected: (NavTab) -> Unit = { tab ->
if (tab.route != selectedTab.route) {
navController.navigate(tab.route) {
popUpTo(Routes.FILES) { inclusive = true }
launchSingleTop = true
}
}
}
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Routes.FILES, startDestination = Routes.FILES,
) { ) {
composable(Routes.FILES) { composable(Routes.FILES) {
FolderListScreen( FolderListScreen(
selectedTab = selectedTab,
onTabSelected = onTabSelected,
onOpenFolder = { id -> navController.navigate(Routes.folder(id)) }, onOpenFolder = { id -> navController.navigate(Routes.folder(id)) },
) )
} }
composable(Routes.SEARCH) {
SearchScreen(
selectedTab = selectedTab,
onTabSelected = onTabSelected,
)
}
composable(Routes.SETTINGS) {
SettingsScreen(
selectedTab = selectedTab,
onTabSelected = onTabSelected,
)
}
composable( composable(
route = Routes.FOLDER_DETAIL, route = Routes.FOLDER_DETAIL,
arguments = listOf( arguments = listOf(
@@ -0,0 +1,42 @@
package com.vaultdrop.mobile.ui.search
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchScreen(
selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(title = { Text(stringResource(R.string.search)) })
},
bottomBar = {
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
},
) { padding ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.search),
textAlign = TextAlign.Center,
)
}
}
}
@@ -0,0 +1,42 @@
package com.vaultdrop.mobile.ui.settings
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(title = { Text(stringResource(R.string.settings)) })
},
bottomBar = {
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
},
) { padding ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.settings),
textAlign = TextAlign.Center,
)
}
}
}
@@ -14,6 +14,13 @@
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<string name="back">Back</string> <string name="back">Back</string>
<!-- Navigation -->
<string name="nav_files">Files</string>
<string name="nav_search">Search</string>
<string name="nav_settings">Settings</string>
<string name="search">Search</string>
<string name="settings">Settings</string>
<!-- Login --> <!-- Login -->
<string name="login_title">Sign in</string> <string name="login_title">Sign in</string>
<string name="login_subtitle">Sign in to your account</string> <string name="login_subtitle">Sign in to your account</string>
@@ -15,6 +15,13 @@
<string name="refresh">Actualiser</string> <string name="refresh">Actualiser</string>
<string name="back">Retour</string> <string name="back">Retour</string>
<!-- Navigation -->
<string name="nav_files">Fichiers</string>
<string name="nav_search">Recherche</string>
<string name="nav_settings">Réglages</string>
<string name="search">Recherche</string>
<string name="settings">Réglages</string>
<!-- Login --> <!-- Login -->
<string name="login_title">Connexion</string> <string name="login_title">Connexion</string>
<string name="login_subtitle">Connecte-toi à ton compte</string> <string name="login_subtitle">Connecte-toi à ton compte</string>