3 Commits

Author SHA1 Message Date
Fabian Wolter
0455d82a40 refactor: start to move from voyager to navigation3
I want to move away from the easy but limited voyager library ind implement my own navigation
2026-02-24 21:51:45 +01:00
Fabian Wolter
9bba99ce60 fix: Changed SettingsTab index 2026-02-23 18:35:11 +01:00
Fabian Wolter
c215ae3ec4 feat: Added UI for the FolderPairTab
This commit adds a FolderPairCard composable.
Also fixed the warning: "Serializable object must implement 'readResolve'".
2026-02-23 18:29:23 +01:00
13 changed files with 255 additions and 134 deletions

View File

@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.jetbrains.kotlin.serialization)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
}
@@ -61,9 +61,7 @@ dependencies {
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.material3.adaptive.navigation3)
implementation(libs.kotlinx.serialization.core)
implementation(libs.bundles.voyager)
implementation(libs.kotlinx.serialization.json)
// Room
implementation(libs.androidx.room.runtime)

View File

@@ -5,8 +5,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.ExperimentalMaterial3Api
import cafe.adriel.voyager.navigator.Navigator
import com.fabisahne.cloudsync.ui.screens.HomeScreen
import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
class MainActivity : ComponentActivity() {
@@ -17,7 +15,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
CloudSyncTheme {
Navigator(HomeScreen)
// Navigator(HomeScreen)
}
}
}

View File

@@ -0,0 +1,27 @@
package com.fabisahne.cloudsync.navigation
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
@Serializable
sealed interface Route : NavKey {
// Screens
@Serializable
data object HomeScreen : Route, NavKey
@Serializable
data class FolderPairScreen(val id: Long) : Route, NavKey
// Tabs
@Serializable
data object AccountTab : Route, NavKey
@Serializable
data object FolderPairTab : Route, NavKey
@Serializable
data object HistoryTab : Route, NavKey
@Serializable
data object SettingsTab : Route, NavKey
}

View File

@@ -1,22 +0,0 @@
package com.fabisahne.cloudsync.ui.navigation
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.Tab
@Composable
fun RowScope.TabNavigationItem(tab: Tab) {
val tabNavigator = LocalTabNavigator.current
NavigationBarItem(
selected = tabNavigator.current.key == tab.key,
onClick = { tabNavigator.current = tab },
label = { Text(tab.options.title) },
icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) }
)
}

View File

@@ -8,25 +8,26 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import androidx.lifecycle.viewmodel.compose.viewModel
import com.fabisahne.cloudsync.viewmodels.FolderPairScreenViewModel
data class FolderPairScreen(
val id: Long
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Folder Pair") }
)
},
content = { contentPadding ->
Surface(modifier = Modifier.padding(contentPadding)) {
Text("ASDF: $id")
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FolderPairScreen(
modifier: Modifier = Modifier,
viewModel: FolderPairScreenViewModel = viewModel(),
id: Long
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Folder Pair") }
)
},
content = { contentPadding ->
Surface(modifier = modifier.padding(contentPadding)) {
Text("ASDF: $id")
}
)
}
}
)
}

View File

@@ -1,7 +1,12 @@
package com.fabisahne.cloudsync.ui.screens
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Route
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.Scaffold
@@ -10,53 +15,46 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.CurrentTab
import cafe.adriel.voyager.navigator.tab.TabNavigator
import com.fabisahne.cloudsync.ui.navigation.TabNavigationItem
import com.fabisahne.cloudsync.ui.tabs.AccountTab
import com.fabisahne.cloudsync.ui.tabs.FolderPairTab
import com.fabisahne.cloudsync.ui.tabs.HistoryTab
import com.fabisahne.cloudsync.ui.tabs.SettingsTab
object HomeScreen : Screen {
private val TABS = listOf(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
modifier: Modifier = Modifier
) {
val tabs = listOf(
HistoryTab,
FolderPairTab,
AccountTab,
SettingsTab,
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
TabNavigator(
HistoryTab
) { tabNavigator ->
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = tabNavigator.current.options.title) }
)
},
content = { padding ->
Surface(modifier = Modifier.padding(padding)) {
CurrentTab()
}
},
bottomBar = {
NavigationBar(windowInsets = NavigationBarDefaults.windowInsets) {
TABS.forEach {
TabNavigationItem(it)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Home Screen") }
)
},
content = { padding ->
Surface(modifier = modifier.padding(padding)) {
Text("Home Screen Content")
}
},
bottomBar = {
NavigationBar(windowInsets = NavigationBarDefaults.windowInsets) {
tabs.forEach { _ ->
Button({}) {
Icon(
Icons.Default.Route,
null
)
}
}
)
}
}
}
)
}

View File

@@ -11,6 +11,8 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
object AccountTab : Tab {
private fun readResolve(): Any = AccountTab
override val options: TabOptions
@Composable
get() {

View File

@@ -4,14 +4,23 @@ import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FolderCopy
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -19,17 +28,30 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.fabisahne.cloudsync.data.AppDatabase
import com.fabisahne.cloudsync.data.FolderPair
import com.fabisahne.cloudsync.ui.screens.FolderPairScreen
import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
import kotlinx.coroutines.launch
import androidx.core.net.toUri
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
object FolderPairTab : Tab {
private fun readResolve(): Any = FolderPairTab
override val options: TabOptions
@Composable
get() {
@@ -47,38 +69,37 @@ object FolderPairTab : Tab {
@Composable
override fun Content() {
// Text("Folder Pairs")
// FloatingActionButton(
//
// modifier = Modifier.absolute(),
// onClick = {}
// ) {
// Icon(Icons.Default.Add, "Add Pair")
// }
val navigator = LocalNavigator.currentOrThrow.parent!!
val context = LocalContext.current
val scope = rememberCoroutineScope()
val dao = remember { AppDatabase.getDatabase(context).folderPairDao() }
val folderPairs by dao.getAll().collectAsState(initial = emptyList())
val directoryPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
) { uri: Uri? ->
uri?.let {
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(it, takeFlags)
val directoryPickerLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? ->
uri?.let {
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(it, takeFlags)
// Save to Room
scope.launch {
dao.insert(
FolderPair(
name = "New Folder Pair",
localDir = it.toString(),
cloudPath = "/backup"
// Save to Room
scope.launch {
dao.insert(
FolderPair(
name = "New Folder Pair",
localDir = it.toString(),
cloudPath = "/backup"
)
)
)
}
}
}
val deletePair = { pair: FolderPair ->
scope.launch {
dao.delete(pair)
}
}
Scaffold(
@@ -95,16 +116,98 @@ object FolderPairTab : Tab {
LazyColumn(modifier = Modifier.padding(paddingValues)) {
items(folderPairs.size) { pairIdx ->
val pair = folderPairs[pairIdx]
ListItem(
headlineContent = { Text(pair.name) },
supportingContent = {
Text(
pair.localDir.toUri().lastPathSegment ?: pair.localDir
)
FolderPairCard(
pair,
onDelete = {
deletePair(pair)
},
onClick = {
navigator.push(FolderPairScreen(pair.id))
}
)
}
}
}
}
}
@Composable
fun FolderPairCard(folderPair: FolderPair, onDelete: () -> Unit = {}, onClick: () -> Unit = {}) {
val instant = Instant.ofEpochMilli(folderPair.createdAt)
val dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
Card(
onClick = onClick,
modifier = Modifier
.padding(8.dp, 8.dp)
.height(100.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(10.dp, 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(dateTime.format(formatter), style = MaterialTheme.typography.labelMedium)
Text(folderPair.name, style = MaterialTheme.typography.titleMedium)
Text(
folderPair.localDir.toUri().lastPathSegment ?: folderPair.localDir,
style = MaterialTheme.typography.labelMedium
)
}
Button(
onClick = onDelete
) {
Icon(
Icons.Default.Delete,
null,
)
}
}
}
}
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
@Composable
fun FolderPairsCardPreview() {
val folderPair = FolderPair(
name = "Sample Folder Pair",
localDir = "sample/local/folder",
cloudPath = "sample/cloud/path"
)
CloudSyncTheme {
Column {
FolderPairCard(folderPair)
FolderPairCard(folderPair)
}
}
}
@Preview(backgroundColor = 0xFF000000, showBackground = true)
@Composable
fun FolderPairsCardPreviewDark() {
val folderPair = FolderPair(
name = "Sample Folder Pair",
localDir = "sample/local/folder",
cloudPath = "sample/cloud/path"
)
CloudSyncTheme(
darkTheme = true
) {
Column {
FolderPairCard(folderPair)
FolderPairCard(folderPair)
}
}
}

View File

@@ -32,6 +32,8 @@ import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
import java.time.LocalDate
object HistoryTab : Tab {
private fun readResolve(): Any = HistoryTab
override val options: TabOptions
@Composable
get() {

View File

@@ -1,7 +1,6 @@
package com.fabisahne.cloudsync.ui.tabs
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -11,6 +10,8 @@ import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
object SettingsTab : Tab {
private fun readResolve(): Any = SettingsTab
override val options: TabOptions
@Composable
get() {
@@ -19,7 +20,7 @@ object SettingsTab : Tab {
return remember {
TabOptions(
index = 2u,
index = 3u,
title = title,
icon = icon
)

View File

@@ -0,0 +1,20 @@
package com.fabisahne.cloudsync.viewmodels
import androidx.lifecycle.ViewModel
import com.fabisahne.cloudsync.data.FolderPair
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class FolderPairScreenViewModel(
private val id: Long
) : ViewModel() {
private val _state = MutableStateFlow(
FolderPair(
id = id,
name = "ASDF",
localDir = "asdf/asdf",
cloudPath = "cloud/folder"
)
)
val state = _state.asStateFlow()
}

View File

@@ -2,4 +2,5 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
}

View File

@@ -5,18 +5,18 @@ junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.2"
kotlin = "2.2.10"
composeBom = "2024.09.00"
activityCompose = "1.12.4"
kotlin = "2.3.10"
composeBom = "2026.02.00"
voyager = "1.1.0-beta03"
room = "2.7.1"
room = "2.8.4"
ksp = "2.3.6"
nav3Core = "1.0.0"
nav3Core = "1.0.1"
lifeCycleViewmodelNav3 = "2.10.0"
kotlinSerialization = "2.2.21"
kotlinxSerializationCore = "1.9.0"
material3AdaptiveNav3 = "1.3.0-alpha06"
kotlinSerialization = "2.3.10"
kotlinxSerializationCore = "1.10.0"
material3AdaptiveNav3 = "1.3.0-alpha08"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -40,14 +40,9 @@ androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", vers
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifeCycleViewmodelNav3" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" }
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
# Voyager
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
# Room
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
@@ -56,8 +51,5 @@ androidx-room-compiler = { group = "androidx.room", name = "room-compiler", vers
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
[bundles]
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }