first design draft
This commit is contained in:
@@ -61,4 +61,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
||||||
implementation(libs.androidx.material3.adaptive.navigation3)
|
implementation(libs.androidx.material3.adaptive.navigation3)
|
||||||
implementation(libs.kotlinx.serialization.core)
|
implementation(libs.kotlinx.serialization.core)
|
||||||
|
|
||||||
|
implementation(libs.bundles.voyager)
|
||||||
}
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.fabisahne.cloudsync
|
|
||||||
|
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.compose.ui.test.performTextInput
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Rule
|
|
||||||
import java.text.NumberFormat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("com.fabisahne.cloudsync", appContext.packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val composeTestRule = createComposeRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun calculateTip_20PercentTip() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
CloudSyncTheme {
|
|
||||||
TipTimeLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
composeTestRule.onNodeWithText("Bill Amount")
|
|
||||||
.performTextInput("10")
|
|
||||||
|
|
||||||
val expected = NumberFormat.getCurrencyInstance().format(1.5)
|
|
||||||
composeTestRule.onNodeWithText("Tip Amount: $expected").assertExists(
|
|
||||||
"No node with this text was found."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,23 +4,22 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material.icons.filled.Home
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import androidx.compose.material3.Icon
|
import com.fabisahne.cloudsync.ui.screens.HomeScreen
|
||||||
import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
|
import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
CloudSyncTheme {
|
CloudSyncTheme {
|
||||||
Icon(
|
Navigator(HomeScreen)
|
||||||
Icons.Filled.Home,
|
|
||||||
contentDescription = "asdf",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
app/src/main/java/com/fabisahne/cloudsync/ui/SyncApp.kt
Normal file
2
app/src/main/java/com/fabisahne/cloudsync/ui/SyncApp.kt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
package com.fabisahne.cloudsync.ui
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package com.fabisahne.cloudsync.ui
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package com.fabisahne.cloudsync.ui.navigation
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
|
||||||
import androidx.navigation3.runtime.NavBackStack
|
|
||||||
import androidx.navigation3.runtime.NavEntry
|
|
||||||
import androidx.navigation3.runtime.NavEntryDecorator
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
|
||||||
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberNavigationState(
|
|
||||||
startKey: NavKey,
|
|
||||||
topLevelKeys: Set<NavKey>,
|
|
||||||
): NavigationState {
|
|
||||||
val topLevelStack = rememberNavBackStack(startKey)
|
|
||||||
val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) }
|
|
||||||
|
|
||||||
return remember(startKey, topLevelKeys) {
|
|
||||||
NavigationState(
|
|
||||||
startKey = startKey,
|
|
||||||
topLevelStack = topLevelStack,
|
|
||||||
subStacks = subStacks
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NavigationState(
|
|
||||||
val startKey: NavKey,
|
|
||||||
val topLevelStack: NavBackStack<NavKey>,
|
|
||||||
val subStacks: Map<NavKey, NavBackStack<NavKey>>
|
|
||||||
) {
|
|
||||||
val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() }
|
|
||||||
|
|
||||||
val topLevelKeys
|
|
||||||
get() = subStacks.keys
|
|
||||||
|
|
||||||
val currentSubStack: NavBackStack<NavKey>
|
|
||||||
get() = subStacks[currentTopLevelKey]
|
|
||||||
?: error("Sub stack for $currentTopLevelKey does not exist")
|
|
||||||
|
|
||||||
val currentKey: NavKey by derivedStateOf { currentSubStack.last() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NavigationState.toEntries(
|
|
||||||
entryProvider: (NavKey) -> NavEntry<NavKey>,
|
|
||||||
): SnapshotStateList<NavEntry<NavKey>> {
|
|
||||||
val decoratedEntries = subStacks.mapValues { (_, stack) ->
|
|
||||||
val decorators = listOf<NavEntryDecorator<NavKey>>(
|
|
||||||
rememberSaveableStateHolderNavEntryDecorator(),
|
|
||||||
rememberViewModelStoreNavEntryDecorator(),
|
|
||||||
)
|
|
||||||
rememberDecoratedNavEntries(
|
|
||||||
backStack = stack,
|
|
||||||
entryDecorators = decorators,
|
|
||||||
entryProvider = entryProvider,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return topLevelStack
|
|
||||||
.flatMap { decoratedEntries[it] ?: emptyList() }
|
|
||||||
.toMutableStateList()
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package com.fabisahne.cloudsync.ui.navigation
|
|
||||||
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
|
||||||
|
|
||||||
class Navigator(val state: NavigationState) {
|
|
||||||
fun navigate(key: NavKey) {
|
|
||||||
when (key) {
|
|
||||||
state.currentTopLevelKey -> clearSubStack()
|
|
||||||
in state.topLevelKeys -> goToTopLevel(key)
|
|
||||||
else -> goToKey(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun goBack() {
|
|
||||||
when (state.currentKey) {
|
|
||||||
state.startKey -> error("You cannot go back from the start route")
|
|
||||||
state.currentTopLevelKey -> state.topLevelStack.removeLastOrNull()
|
|
||||||
else -> state.currentSubStack.removeLastOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goToKey(key: NavKey) {
|
|
||||||
state.currentSubStack.apply {
|
|
||||||
remove(key)
|
|
||||||
add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goToTopLevel(key: NavKey) {
|
|
||||||
state.topLevelStack.apply {
|
|
||||||
if (key == state.startKey) {
|
|
||||||
clear()
|
|
||||||
} else {
|
|
||||||
remove(key)
|
|
||||||
}
|
|
||||||
add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearSubStack() {
|
|
||||||
state.currentSubStack.run {
|
|
||||||
if (size > 1) subList(1, size).clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.fabisahne.cloudsync.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
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
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.fabisahne.cloudsync.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarDefaults
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
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(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.fabisahne.cloudsync.ui.tabs
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.AccountBox
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
|
||||||
|
|
||||||
|
object AccountTab : Tab {
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable
|
||||||
|
get() {
|
||||||
|
val title = "Accounts"
|
||||||
|
val icon = rememberVectorPainter(Icons.Default.AccountBox)
|
||||||
|
|
||||||
|
return remember {
|
||||||
|
TabOptions(
|
||||||
|
index = 2u,
|
||||||
|
title = title,
|
||||||
|
icon = icon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
Text("Cloud Accounts")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.fabisahne.cloudsync.ui.tabs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.absoluteOffset
|
||||||
|
import androidx.compose.foundation.layout.absolutePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.FolderCopy
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
|
||||||
|
object FolderPairTab : Tab {
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable
|
||||||
|
get() {
|
||||||
|
val title = "Folder Pairs"
|
||||||
|
val icon = rememberVectorPainter(Icons.Default.FolderCopy)
|
||||||
|
|
||||||
|
return remember {
|
||||||
|
TabOptions(
|
||||||
|
index = 1u,
|
||||||
|
title = title,
|
||||||
|
icon = icon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
// Text("Folder Pairs")
|
||||||
|
// FloatingActionButton(
|
||||||
|
//
|
||||||
|
// modifier = Modifier.absolute(),
|
||||||
|
// onClick = {}
|
||||||
|
// ) {
|
||||||
|
// Icon(Icons.Default.Add, "Add Pair")
|
||||||
|
// }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, "Add Pair")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Text("Pairs here", Modifier.padding(paddingValues))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
app/src/main/java/com/fabisahne/cloudsync/ui/tabs/HistoryTab.kt
Normal file
141
app/src/main/java/com/fabisahne/cloudsync/ui/tabs/HistoryTab.kt
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package com.fabisahne.cloudsync.ui.tabs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
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.History
|
||||||
|
import androidx.compose.material.icons.outlined.History
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.rememberVectorPainter
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import com.fabisahne.cloudsync.ui.screens.FolderPairScreen
|
||||||
|
import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
object HistoryTab : Tab {
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable
|
||||||
|
get() {
|
||||||
|
val isSelected = LocalTabNavigator.current.current.key == key
|
||||||
|
val icon = if (isSelected)
|
||||||
|
Icons.Outlined.History
|
||||||
|
else
|
||||||
|
Icons.Default.History
|
||||||
|
return TabOptions(
|
||||||
|
index = 0u,
|
||||||
|
title = "History",
|
||||||
|
icon = rememberVectorPainter(icon),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow.parent!!
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(5) { index ->
|
||||||
|
HistoryItem(
|
||||||
|
"Account",
|
||||||
|
"Folder Pair $index",
|
||||||
|
LocalDate.now().minusDays(index.toLong()),
|
||||||
|
isError = index % 2 == 0,
|
||||||
|
onClick = { navigator.push(FolderPairScreen(index.toLong())) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryItem(
|
||||||
|
account: String,
|
||||||
|
pairName: String,
|
||||||
|
date: LocalDate,
|
||||||
|
isError: Boolean = false,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val (containerColor, contentColor) = if (isError)
|
||||||
|
MaterialTheme.colorScheme.errorContainer to MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
else
|
||||||
|
CardDefaults.cardColors().containerColor to CardDefaults.cardColors().contentColor
|
||||||
|
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(90.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = containerColor,
|
||||||
|
contentColor = contentColor
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("$account | $pairName", style = MaterialTheme.typography.titleMedium)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
if (isError) "Error" else "Success",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(date.toString(), style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun HistoryCardPreviewLight() {
|
||||||
|
CloudSyncTheme {
|
||||||
|
HistoryItem(
|
||||||
|
"Account",
|
||||||
|
"Folder Pair",
|
||||||
|
LocalDate.now(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun HistoryCardPreviewDark() {
|
||||||
|
CloudSyncTheme(
|
||||||
|
darkTheme = true
|
||||||
|
) {
|
||||||
|
HistoryItem(
|
||||||
|
"Account",
|
||||||
|
"Folder Pair",
|
||||||
|
LocalDate.now()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
|
||||||
|
object SettingsTab : Tab {
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable
|
||||||
|
get() {
|
||||||
|
val title = "Settings"
|
||||||
|
val icon = rememberVectorPainter(Icons.Default.Settings)
|
||||||
|
|
||||||
|
return remember {
|
||||||
|
TabOptions(
|
||||||
|
index = 2u,
|
||||||
|
title = title,
|
||||||
|
icon = icon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
Text("Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.fabisahne.cloudsync.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
|
||||||
val Pink40 = Color(0xFF7D5260)
|
|
||||||
@@ -1,30 +1,24 @@
|
|||||||
package com.fabisahne.cloudsync.ui.theme
|
package com.fabisahne.cloudsync.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
|
||||||
primary = Purple80,
|
|
||||||
secondary = PurpleGrey80,
|
|
||||||
tertiary = Pink80
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CloudSyncTheme(
|
fun CloudSyncTheme(
|
||||||
dynamicColor: Boolean = true,
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val context = LocalContext.current
|
||||||
dynamicColor -> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
dynamicDarkColorScheme(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> DarkColorScheme
|
val colorScheme = if (darkTheme)
|
||||||
}
|
dynamicDarkColorScheme(context)
|
||||||
|
else
|
||||||
|
dynamicLightColorScheme(context)
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.fabisahne.cloudsync
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import java.text.NumberFormat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
fun addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun calculateTip_20Percent() {
|
|
||||||
val amount = 10.00
|
|
||||||
val tipPercent = 20.00
|
|
||||||
|
|
||||||
val expected = NumberFormat.getCurrencyInstance().format(2)
|
|
||||||
|
|
||||||
assertEquals(expected, calculateTip(amount, tipPercent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ lifecycleRuntimeKtx = "2.10.0"
|
|||||||
activityCompose = "1.12.2"
|
activityCompose = "1.12.2"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.0.21"
|
||||||
composeBom = "2024.09.00"
|
composeBom = "2024.09.00"
|
||||||
compileSdk = "36"
|
voyager = "1.1.0-beta03"
|
||||||
|
|
||||||
nav3Core = "1.0.0"
|
nav3Core = "1.0.0"
|
||||||
lifeCycleViewmodelNav3 = "2.10.0"
|
lifeCycleViewmodelNav3 = "2.10.0"
|
||||||
@@ -40,7 +40,15 @@ androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecy
|
|||||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
|
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
|
||||||
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
|
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
|
||||||
|
|
||||||
|
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" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
|
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
|
||||||
|
|
||||||
|
[bundles]
|
||||||
|
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
|
||||||
Reference in New Issue
Block a user