Skip to content
Snippets Groups Projects
Unverified Commit ed4829ba authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

refactor: recreate app structure

parent d7ceabe9
Branches
No related tags found
No related merge requests found
Showing
with 719 additions and 626 deletions
...@@ -24,6 +24,9 @@ ...@@ -24,6 +24,9 @@
package de.chaosdorf.mete.model package de.chaosdorf.mete.model
import kotlinx.serialization.Serializable
@Serializable
@JvmInline @JvmInline
value class ServerId(val value: Long) { value class ServerId(val value: Long) {
override fun toString() = value.toString() override fun toString() = value.toString()
......
...@@ -88,7 +88,9 @@ dependencies { ...@@ -88,7 +88,9 @@ dependencies {
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.paging) implementation(libs.androidx.room.paging)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.viewmodel)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.coil.compose) implementation(libs.coil.compose)
......
...@@ -27,33 +27,168 @@ package de.chaosdorf.meteroid ...@@ -27,33 +27,168 @@ package de.chaosdorf.meteroid
import android.os.Bundle 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.viewModels
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.core.snap
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.lifecycle.get import androidx.navigation3.runtime.*
import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.withCreationCallback
import de.chaosdorf.meteroid.theme.MeteroidTheme import de.chaosdorf.meteroid.theme.MeteroidTheme
import de.chaosdorf.meteroid.ui.MeteroidRouter import de.chaosdorf.meteroid.ui.*
import de.chaosdorf.meteroid.viewmodel.*
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val viewModelProvider = ViewModelProvider(this)
val viewModel = viewModelProvider.get<MeteroidViewModel>() val viewModel by viewModels<InitViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<InitViewModelFactory> { factory ->
factory.create()
}
}
)
installSplashScreen().setKeepOnScreenCondition { installSplashScreen().setKeepOnScreenCondition {
viewModel.initialAccount.value == null viewModel.setupComplete.value == null
} }
setContent { setContent {
val initialAccount by viewModel.initialAccount.collectAsState() val setupComplete by viewModel.setupComplete.collectAsState()
MeteroidTheme { MeteroidTheme {
if (initialAccount != null) { if (setupComplete != null) {
MeteroidRouter(initialAccount!!) val backStack: NavBackStack = rememberNavBackStack(
if (setupComplete == true) Routes.ServerList else Routes.Setup
)
Scaffold(
topBar = { TopBar(backStack) },
bottomBar = { BottomBar(backStack) }
) { paddingValues ->
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
// BEGIN WORKAROUND
// TODO FIXED IN navigation3 1.0.0-alpha04 Jun 18
transitionSpec = {
ContentTransform(
fadeIn(animationSpec = snap()),
fadeOut(animationSpec = snap())
)
},
popTransitionSpec = {
ContentTransform(
fadeIn(animationSpec = snap()),
fadeOut(animationSpec = snap())
)
},
predictivePopTransitionSpec = {
ContentTransform(
fadeIn(animationSpec = snap()),
fadeOut(animationSpec = snap())
)
},
// END
entryProvider = entryProvider {
entry<Routes.Setup> {
val viewModel by viewModels<SetupViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<SetupViewModelFactory> { factory ->
factory.create()
}
}
)
SetupRoute(viewModel, backStack, paddingValues)
}
entry<Routes.Settings> {
val viewModel by viewModels<SettingsViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<SettingsViewModelFactory> { factory ->
factory.create()
}
}
)
SettingsRoute(viewModel, backStack, paddingValues)
}
entry<Routes.ServerList> {
val viewModel by viewModels<ServerListViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<ServerListViewModelFactory> { factory ->
factory.create()
}
}
)
ServerListRoute(viewModel, backStack, paddingValues)
}
entry<Routes.UserList> {
val viewModel by viewModels<UserListViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<UserListViewModelFactory> { factory ->
factory.create(it.serverId.value)
}
}
)
UserListRoute(viewModel, backStack, paddingValues)
}
entry<Routes.Purchase> {
val viewModel by viewModels<UserViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<UserViewModelFactory> { factory ->
factory.create(it.serverId.value, it.userId.value)
}
}
)
PurchaseRoute(viewModel, backStack, paddingValues)
}
entry<Routes.Deposit> {
val viewModel by viewModels<UserViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<UserViewModelFactory> { factory ->
factory.create(it.serverId.value, it.userId.value)
}
}
)
DepositRoute(viewModel, backStack, paddingValues)
}
entry<Routes.History> {
val viewModel by viewModels<UserViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<UserViewModelFactory> { factory ->
factory.create(it.serverId.value, it.userId.value)
}
}
)
HistoryRoute(viewModel, backStack, paddingValues)
}
entry<Routes.Wrapped> {
val viewModel by viewModels<WrappedViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<WrappedViewModelFactory> { factory ->
factory.create(it.serverId.value, it.userId.value)
}
}
)
WrappedRoute(viewModel, backStack, paddingValues)
}
}
)
}
} }
} }
} }
......
...@@ -31,10 +31,12 @@ import androidx.datastore.preferences.core.edit ...@@ -31,10 +31,12 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId import de.chaosdorf.mete.model.UserId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
class AccountPreferencesImpl @Inject constructor( class AccountPreferencesImpl @Inject constructor(
private val dataStore: DataStore<Preferences> private val dataStore: DataStore<Preferences>
) : AccountPreferences { ) : AccountPreferences {
......
...@@ -30,10 +30,8 @@ import androidx.compose.foundation.isSystemInDarkTheme ...@@ -30,10 +30,8 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
private val LightColors = lightColorScheme( private val LightColors = lightColorScheme(
...@@ -104,7 +102,6 @@ private val DarkColors = darkColorScheme( ...@@ -104,7 +102,6 @@ private val DarkColors = darkColorScheme(
@Composable @Composable
fun MeteroidTheme( fun MeteroidTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false, dynamicColor: Boolean = false,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
...@@ -121,12 +118,8 @@ fun MeteroidTheme( ...@@ -121,12 +118,8 @@ fun MeteroidTheme(
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
val window = (view.context as Activity).window val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
window.navigationBarColor = colorScheme.surfaceColorAtElevation(3.dp).toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !darkTheme
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = colorScheme.surfaceColorAtElevation(3.dp).toArgb()
}
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
} }
} }
......
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import de.chaosdorf.meteroid.theme.icons.MeteroidIcons
import de.chaosdorf.meteroid.theme.icons.filled.WaterFull
@Preview
@Composable
fun UserAvatar(
source: String? = null,
) {
var success by remember { mutableStateOf(false) }
AvatarLayout(
Modifier.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
) {
if (!success) {
Icon(
Icons.Filled.Person,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
AsyncImage(
source,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
onSuccess = { success = true },
onError = { success = false },
onLoading = { success = false },
)
}
}
@Preview
@Composable
fun ServerAvatar(
source: String? = null,
) {
var success by remember { mutableStateOf(false) }
AvatarLayout {
if (!success) {
Icon(
MeteroidIcons.Filled.WaterFull,
contentDescription = null
)
}
AsyncImage(
source,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
onSuccess = { success = true },
onError = { success = false },
onLoading = { success = false },
)
}
}
@Composable
fun AvatarLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Box(
modifier.size(36.dp),
contentAlignment = Alignment.Center
) {
content()
}
}
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -24,95 +24,89 @@ ...@@ -24,95 +24,89 @@
package de.chaosdorf.meteroid.ui package de.chaosdorf.meteroid.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Celebration import androidx.compose.material.icons.outlined.Celebration
import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.LocalAtm import androidx.compose.material.icons.outlined.LocalAtm
import androidx.compose.material.icons.twotone.Celebration import androidx.compose.material3.BottomAppBar
import androidx.compose.material.icons.twotone.History import androidx.compose.material3.Icon
import androidx.compose.material.icons.twotone.LocalAtm import androidx.compose.material3.NavigationBarItem
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.runtime.Composable
import de.chaosdorf.mete.model.ServerId import androidx.navigation3.runtime.NavBackStack
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.theme.icons.MeteroidIcons import de.chaosdorf.meteroid.theme.icons.MeteroidIcons
import de.chaosdorf.meteroid.theme.icons.outlined.WaterFull import de.chaosdorf.meteroid.theme.icons.outlined.WaterFull
import de.chaosdorf.meteroid.theme.icons.twotone.WaterFull
sealed class MeteroidScreen(val route: String) { @Composable
sealed class Home( fun BottomBar(
val label: String, backStack: NavBackStack,
val activeIcon: ImageVector,
val inactiveIcon: ImageVector,
route: String
) : MeteroidScreen(route) {
data object Purchase : Home(
"Purchase",
MeteroidIcons.TwoTone.WaterFull,
MeteroidIcons.Outlined.WaterFull,
"server/{server}/user/{user}/purchase"
) { ) {
fun build(server: ServerId, user: UserId) = route val currentRoute = backStack.lastOrNull()
.replace("{server}", server.value.toString())
.replace("{user}", user.value.toString())
}
data object Deposit : Home(
"Deposit",
Icons.TwoTone.LocalAtm,
Icons.Outlined.LocalAtm,
"server/{server}/user/{user}/deposit"
) {
fun build(server: ServerId, user: UserId) = route
.replace("{server}", server.value.toString())
.replace("{user}", user.value.toString())
}
data object History : Home(
"History",
Icons.TwoTone.History,
Icons.Outlined.History,
"server/{server}/user/{user}/history"
) {
fun build(server: ServerId, user: UserId) = route
.replace("{server}", server.value.toString())
.replace("{user}", user.value.toString())
}
data object Wrapped : Home( AnimatedVisibility(
"Wrapped", visible = currentRoute is Routes.UserScope,
Icons.TwoTone.Celebration, enter = slideInVertically(initialOffsetY = { it }),
Icons.Outlined.Celebration, exit = slideOutVertically(targetOffsetY = { it })
"server/{server}/user/{user}/wrapped"
) { ) {
fun build(server: ServerId, user: UserId) = route BottomAppBar {
.replace("{server}", server.value.toString()) NavigationBarItem(
.replace("{user}", user.value.toString()) selected = currentRoute is Routes.Purchase,
} onClick = {
if (currentRoute is Routes.UserScope) {
backStack.remove(currentRoute)
backStack.add(
Routes.Purchase(currentRoute.serverId, currentRoute.userId)
)
} }
},
data object UserList : MeteroidScreen("server/{server}/userList") { icon = {
fun build(server: ServerId) = route Icon(MeteroidIcons.Outlined.WaterFull, contentDescription = null)
.replace("{server}", server.value.toString()) },
)
NavigationBarItem(
selected = currentRoute is Routes.Deposit,
onClick = {
if (currentRoute is Routes.UserScope) {
backStack.remove(currentRoute)
backStack.add(
Routes.Deposit(currentRoute.serverId, currentRoute.userId)
)
} }
},
data object AddServer : MeteroidScreen("addServer") { icon = {
fun build() = route Icon(Icons.Outlined.LocalAtm, contentDescription = null)
},
)
NavigationBarItem(
selected = currentRoute is Routes.History,
onClick = {
if (currentRoute is Routes.UserScope) {
backStack.remove(currentRoute)
backStack.add(
Routes.History(currentRoute.serverId, currentRoute.userId)
)
} }
},
data object Settings : MeteroidScreen("settings") { icon = {
fun build() = route Icon(Icons.Outlined.History, contentDescription = null)
},
)
NavigationBarItem(
selected = currentRoute is Routes.Wrapped,
onClick = {
if (currentRoute is Routes.UserScope) {
backStack.remove(currentRoute)
backStack.add(
Routes.Wrapped(currentRoute.serverId, currentRoute.userId)
)
} }
},
companion object { icon = {
fun byRoute(route: String?) = when (route) { Icon(Icons.Outlined.Celebration, contentDescription = null)
Home.Purchase.route -> Home.Purchase },
Home.Deposit.route -> Home.Deposit )
Home.History.route -> Home.History
Home.Wrapped.route -> Home.Wrapped
UserList.route -> UserList
AddServer.route -> AddServer
Settings.route -> Settings
else -> null
} }
} }
} }
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -22,18 +22,24 @@ ...@@ -22,18 +22,24 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.navigation package de.chaosdorf.meteroid.ui
import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column
import androidx.compose.animation.expandVertically import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavBackStack
import de.chaosdorf.meteroid.viewmodel.UserViewModel
@Composable @Composable
fun NavigationAnimationContainer( fun DepositRoute(
visible: Boolean, content: @Composable () -> Unit viewModel: UserViewModel,
backStack: NavBackStack,
contentPadding: PaddingValues,
) { ) {
AnimatedVisibility(visible, enter = expandVertically(), exit = shrinkVertically()) { Column(Modifier.padding(contentPadding)) {
content() Text("Deposit ${viewModel.serverId} ${viewModel.userId}")
} }
} }
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -22,28 +22,24 @@ ...@@ -22,28 +22,24 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.home.transactionhistory package de.chaosdorf.meteroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier
import androidx.compose.runtime.getValue import androidx.navigation3.runtime.NavBackStack
import de.chaosdorf.meteroid.viewmodel.UserViewModel
@Composable @Composable
fun TransactionHistoryScreen( fun HistoryRoute(
viewModel: TransactionHistoryViewModel, viewModel: UserViewModel,
contentPadding: PaddingValues = PaddingValues(), backStack: NavBackStack,
contentPadding: PaddingValues,
) { ) {
val transactions by viewModel.transactions.collectAsState() Column(Modifier.padding(contentPadding)) {
Text("History ${viewModel.serverId} ${viewModel.userId}")
LazyColumn(contentPadding = contentPadding) {
items(
transactions,
key = { "transaction-${it.transaction.serverId}-${it.transaction.transactionId}" },
) { (transaction, drink) ->
TransactionHistoryItem(transaction, drink)
}
} }
} }
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui
import android.util.Log
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.ui.home.deposit.DepositScreen
import de.chaosdorf.meteroid.ui.home.deposit.DepositViewModel
import de.chaosdorf.meteroid.ui.home.purchase.PurchaseScreen
import de.chaosdorf.meteroid.ui.home.purchase.PurchaseViewModel
import de.chaosdorf.meteroid.ui.home.transactionhistory.TransactionHistoryScreen
import de.chaosdorf.meteroid.ui.home.transactionhistory.TransactionHistoryViewModel
import de.chaosdorf.meteroid.ui.home.wrapped.WrappedScreen
import de.chaosdorf.meteroid.ui.home.wrapped.WrappedViewModel
import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel
import de.chaosdorf.meteroid.ui.servers.AddServerScreen
import de.chaosdorf.meteroid.ui.servers.AddServerViewModel
import de.chaosdorf.meteroid.ui.settings.SettingsScreen
import de.chaosdorf.meteroid.ui.settings.SettingsViewModel
import de.chaosdorf.meteroid.ui.userlist.UserListScreen
import de.chaosdorf.meteroid.ui.userlist.UserListViewModel
import de.chaosdorf.meteroid.util.toFancyString
import kotlinx.coroutines.flow.collectLatest
@Composable
fun MeteroidRouter(
initialAccount: AccountPreferences.State,
) {
val navController = rememberNavController()
val navigationViewModel = hiltViewModel<NavigationViewModel>()
LaunchedEffect(navController) {
navController.currentBackStack.collectLatest {
Log.i("MeteroidRouter", "Navigation: ${it.toFancyString()}")
}
}
MeteroidScaffold(navController, navigationViewModel) { paddingValues ->
NavHost(
navController = navController,
startDestination = MeteroidScreen.Home.Purchase.route,
enterTransition = { fadeIn() },
exitTransition = { fadeOut() },
popEnterTransition = { fadeIn() },
popExitTransition = { fadeOut() },
) {
composable(
MeteroidScreen.Home.Purchase.route, arguments = listOf(
navArgument("server") {
type = NavType.LongType
defaultValue = initialAccount.server?.value ?: -1L
},
navArgument("user") {
type = NavType.LongType
defaultValue = initialAccount.user?.value ?: -1L
},
)
) { entry ->
val serverId = entry.arguments?.getLong("server")?.let(::ServerId)
?: ServerId(-1L)
val userId = entry.arguments?.getLong("user")?.let(::UserId)
?: UserId(-1L)
LaunchedEffect(serverId, userId) {
if (!serverId.isValid() || !userId.isValid()) {
navigationViewModel.expanded.value = true
}
}
if (serverId.isValid() && userId.isValid()) {
val viewModel: PurchaseViewModel = hiltViewModel(
key = MeteroidScreen.Home.Purchase.build(serverId, userId)
)
PurchaseScreen(navController, viewModel, PaddingValues(top = 96.dp))
}
}
composable(
MeteroidScreen.Home.Deposit.route, arguments = listOf(
navArgument("server") {
type = NavType.LongType
},
navArgument("user") {
type = NavType.LongType
},
)
) {
val viewModel: DepositViewModel = hiltViewModel()
DepositScreen(navController, viewModel, PaddingValues(top = 96.dp))
}
composable(
MeteroidScreen.Home.History.route, arguments = listOf(
navArgument("server") {
type = NavType.LongType
},
navArgument("user") {
type = NavType.LongType
},
)
) {
val viewModel: TransactionHistoryViewModel = hiltViewModel()
TransactionHistoryScreen(viewModel, PaddingValues(top = 96.dp))
}
composable(
MeteroidScreen.Home.Wrapped.route, arguments = listOf(
navArgument("server") {
type = NavType.LongType
},
navArgument("user") {
type = NavType.LongType
},
)
) {
val viewModel: WrappedViewModel = hiltViewModel()
WrappedScreen(viewModel, PaddingValues(top = 96.dp))
}
composable(
MeteroidScreen.UserList.route, arguments = listOf(
navArgument("server") {
type = NavType.LongType
},
)
) {
val viewModel: UserListViewModel = hiltViewModel()
UserListScreen(navController, viewModel, PaddingValues(top = 96.dp))
}
composable(MeteroidScreen.AddServer.route) {
val viewModel: AddServerViewModel = hiltViewModel()
AddServerScreen(navController, viewModel, PaddingValues(top = 96.dp))
}
composable(MeteroidScreen.Settings.route) {
val viewModel: SettingsViewModel = hiltViewModel()
SettingsScreen(navController, viewModel, PaddingValues(top = 96.dp))
}
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import de.chaosdorf.meteroid.ui.home.MeteroidBottomBar
import de.chaosdorf.meteroid.ui.navigation.MeteroidNavigation
import de.chaosdorf.meteroid.ui.navigation.NavigationScrim
import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel
@Composable
fun MeteroidScaffold(
navController: NavController,
viewModel: NavigationViewModel,
content: @Composable (PaddingValues) -> Unit
) {
Column {
Surface(
modifier = Modifier
.fillMaxWidth()
.windowInsetsTopHeight(WindowInsets.statusBars),
color = MaterialTheme.colorScheme.scrim.copy(alpha = .38f),
) {}
Box(Modifier.weight(1f)) {
Scaffold(
bottomBar = { MeteroidBottomBar(navController, viewModel) },
content = content
)
Surface(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.windowInsetsBottomHeight(WindowInsets.navigationBars),
color = MaterialTheme.colorScheme.scrim.copy(alpha = .38f),
) {}
NavigationScrim(viewModel)
MeteroidNavigation(navController, viewModel)
}
}
}
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -22,18 +22,24 @@ ...@@ -22,18 +22,24 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.home.deposit package de.chaosdorf.meteroid.ui
import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Column
import de.chaosdorf.meteroid.R import androidx.compose.foundation.layout.PaddingValues
import java.math.BigDecimal import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavBackStack
import de.chaosdorf.meteroid.viewmodel.UserViewModel
enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) { @Composable
MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50), fun PurchaseRoute(
MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100), viewModel: UserViewModel,
MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200), backStack: NavBackStack,
MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500), contentPadding: PaddingValues,
MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000), ) {
MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000), Column(Modifier.padding(contentPadding)) {
MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000), Text("Purchase ${viewModel.serverId} ${viewModel.userId}")
}
} }
...@@ -22,30 +22,57 @@ ...@@ -22,30 +22,57 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.navigation package de.chaosdorf.meteroid.ui
import androidx.compose.animation.AnimatedVisibility import androidx.navigation3.runtime.NavKey
import androidx.compose.animation.fadeIn import de.chaosdorf.mete.model.ServerId
import androidx.compose.animation.fadeOut import de.chaosdorf.mete.model.UserId
import androidx.compose.foundation.clickable import kotlinx.serialization.Serializable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme sealed interface Routes : NavKey {
import androidx.compose.material3.Surface interface ServerScope {
import androidx.compose.runtime.Composable val serverId: ServerId
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@Composable
fun NavigationScrim(viewModel: NavigationViewModel) {
val expanded by viewModel.expanded.collectAsState()
AnimatedVisibility(expanded, enter = fadeIn(), exit = fadeOut()) {
Surface(
color = MaterialTheme.colorScheme.scrim.copy(alpha = 0.2f),
modifier = Modifier.fillMaxSize().clickable {
viewModel.expanded.value = false
} }
) {}
interface UserScope : ServerScope {
val userId: UserId
} }
@Serializable
data object Setup : Routes
@Serializable
data object Settings : Routes
@Serializable
data object ServerList : Routes
@Serializable
data class UserList(
override val serverId: ServerId
) : Routes, ServerScope
@Serializable
data class Purchase(
override val serverId: ServerId,
override val userId: UserId
) : Routes, UserScope
@Serializable
data class Deposit(
override val serverId: ServerId,
override val userId: UserId
) : Routes, UserScope
@Serializable
data class History(
override val serverId: ServerId,
override val userId: UserId
) : Routes, UserScope
@Serializable
data class Wrapped(
override val serverId: ServerId,
override val userId: UserId
) : Routes, UserScope
} }
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -22,35 +22,42 @@ ...@@ -22,35 +22,42 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.navigation package de.chaosdorf.meteroid.ui
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.icons.Icons import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.filled.Settings import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.navigation3.runtime.NavBackStack
import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage
import de.chaosdorf.meteroid.ui.AvatarLayout import de.chaosdorf.meteroid.viewmodel.ServerListViewModel
@Composable @Composable
fun NavigationSettingsItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) { fun ServerListRoute(
val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height") viewModel: ServerListViewModel,
backStack: NavBackStack,
contentPadding: PaddingValues,
) {
val servers by viewModel.servers.collectAsState()
LazyColumn(contentPadding = contentPadding) {
items(servers, { "server-${it.serverId}" }) { server ->
ListItem( ListItem(
headlineContent = { Text("Settings") }, headlineContent = { Text(server.name ?: "Unknown") },
leadingContent = { supportingContent = { Text(server.url) },
AvatarLayout { leadingContent = if (server.logoUrl != null) {
Icon(Icons.Filled.Settings, contentDescription = null) @Composable { AsyncImage(server.logoUrl, contentDescription = null) }
} } else null,
modifier = Modifier.clickable {
backStack.add(Routes.UserList(server.serverId))
}, },
modifier = Modifier.requiredHeight(height)
.clickable(onClick = if (expanded) onClick else onExpand)
) )
} }
}
}
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.settings package de.chaosdorf.meteroid.ui
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
...@@ -30,16 +30,16 @@ import androidx.compose.foundation.layout.padding ...@@ -30,16 +30,16 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavBackStack
import androidx.navigation.NavController import de.chaosdorf.meteroid.viewmodel.SettingsViewModel
@Composable @Composable
fun SettingsScreen(navController: NavController, viewModel: SettingsViewModel, contentPadding: PaddingValues) { fun SettingsRoute(
Column( viewModel: SettingsViewModel,
Modifier backStack: NavBackStack,
.padding(contentPadding) contentPadding: PaddingValues,
.padding(16.dp, 8.dp)
) { ) {
Column(Modifier.padding(contentPadding)) {
Text("Settings") Text("Settings")
} }
} }
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -22,36 +22,57 @@ ...@@ -22,36 +22,57 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.home.deposit package de.chaosdorf.meteroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.material3.*
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavBackStack
import androidx.navigation.NavController import de.chaosdorf.meteroid.viewmodel.SetupViewModel
import kotlinx.coroutines.launch
@Composable @Composable
fun DepositScreen( fun SetupRoute(
navController: NavController, viewModel: SetupViewModel,
viewModel: DepositViewModel, backStack: NavBackStack,
contentPadding: PaddingValues = PaddingValues(), contentPadding: PaddingValues,
) { ) {
LazyVerticalGrid( val coroutineScope = rememberCoroutineScope()
GridCells.Adaptive(104.dp), val serverUrl by viewModel.serverUrl.collectAsState()
contentPadding = contentPadding, val error by viewModel.error.collectAsState()
modifier = Modifier.padding(horizontal = 8.dp),
Column(Modifier.padding(contentPadding)) {
error?.let {
Surface(
color = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
) { ) {
items( Text(it)
viewModel.money, }
key = { "deposit-${it.ordinal}" }, }
) { monetaryAmount -> TextField(
DepositMoneyItem(monetaryAmount) { value = serverUrl,
viewModel.deposit(it, navController::navigateUp) onValueChange = { viewModel.serverUrl.value = it },
)
Button(
onClick = {
coroutineScope.launch {
val server = viewModel.add()
if (server != null) {
backStack.removeLastOrNull()
backStack.add(Routes.ServerList)
backStack.add(Routes.UserList(server.serverId))
} }
} }
} }
) {
Text("Save")
}
}
} }
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2025 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui
import androidx.compose.animation.*
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation3.runtime.NavBackStack
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun TopBar(backStack: NavBackStack) {
val currentRoute = backStack.lastOrNull()
if (currentRoute !is Routes.Setup) {
var open by remember { mutableStateOf(false) }
val transition = updateTransition(open, label = "transition")
val backgroundAlpha = transition.animateFloat { if (it) .2f else 0f }
val elevation = transition.animateDp { if (it) 0.dp else 4.dp }
Surface(
modifier = Modifier
.windowInsetsPadding(WindowInsets.statusBars)
.padding(8.dp)
.height(56.dp),
shape = RoundedCornerShape(28.dp),
shadowElevation = elevation.value,
tonalElevation = 4.dp,
onClick = { open = true }
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(currentRoute.toString())
}
}
if (open || transition.currentState || transition.isRunning) {
Dialog(
onDismissRequest = { open = false },
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Box(
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim.copy(alpha = backgroundAlpha.value))
.clickable { open = false }
) {
Surface(
modifier = Modifier
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(8.dp),
shape = RoundedCornerShape(28.dp),
shadowElevation = 4.dp - elevation.value,
tonalElevation = 4.dp,
) {
Column {
transition.AnimatedVisibility(
visible = { it || currentRoute == Routes.ServerList },
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable {
backStack.add(Routes.ServerList)
open = false
}.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("ServerList")
}
}
transition.AnimatedVisibility(
visible = { it || (currentRoute is Routes.UserScope && currentRoute.serverId == ServerId(1) && currentRoute.userId == UserId(1)) },
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable {
backStack.add(Routes.Purchase(ServerId(1), UserId(1)))
open = false
}.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("User(ServerId(1),UserId(1))")
}
}
transition.AnimatedVisibility(
visible = { it || currentRoute == Routes.Settings },
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable {
backStack.add(Routes.Settings)
open = false
}.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("Settings")
}
}
}
}
}
}
}
}
}
/* /*
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2013-2023 Chaosdorf e.V. * Copyright (c) 2013-2025 Chaosdorf e.V.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -22,35 +22,42 @@ ...@@ -22,35 +22,42 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.navigation package de.chaosdorf.meteroid.ui
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.icons.Icons import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.filled.Add import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.navigation3.runtime.NavBackStack
import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage
import de.chaosdorf.meteroid.ui.AvatarLayout import de.chaosdorf.meteroid.viewmodel.UserListViewModel
@Composable @Composable
fun NavigationAddServerItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) { fun UserListRoute(
val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height") viewModel: UserListViewModel,
backStack: NavBackStack,
contentPadding: PaddingValues,
) {
val users by viewModel.users.collectAsState()
LazyColumn(contentPadding = contentPadding) {
items(users, { "server-${it.serverId}/user-${it.userId}"}) {
ListItem( ListItem(
headlineContent = { Text("Add Server") }, headlineContent = { Text(it.name) },
leadingContent = { supportingContent = { Text(it.email ?: "") },
AvatarLayout { leadingContent = if (it.gravatarUrl != null) {
Icon(Icons.Default.Add, contentDescription = null) @Composable { AsyncImage(it.gravatarUrl, contentDescription = null) }
} else null,
modifier = Modifier.clickable {
backStack.add(Routes.Purchase(it.serverId, it.userId))
} }
},
modifier = Modifier.requiredHeight(height)
.clickable(onClick = if (expanded) onClick else onExpand)
) )
} }
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2025 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavBackStack
import de.chaosdorf.meteroid.viewmodel.WrappedViewModel
@Composable
fun WrappedRoute(
viewModel: WrappedViewModel,
backStack: NavBackStack,
contentPadding: PaddingValues,
) {
Column(Modifier.padding(contentPadding)) {
Text("Wrapped ${viewModel.serverId} ${viewModel.userId}")
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui.home
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import de.chaosdorf.meteroid.ui.MeteroidScreen
import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel
import de.chaosdorf.meteroid.util.findStartDestination
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
@Composable
fun MeteroidBottomBar(navController: NavController, viewModel: NavigationViewModel) {
val backStackEntry by navController.currentBackStackEntryAsState()
val activeRoute = MeteroidScreen.byRoute(backStackEntry?.destination?.route)
val account by viewModel.account.collectAsState()
val server = account?.server
val user = account?.user
val historyDisabled by viewModel.historyDisabled.collectAsState()
val wrappedEnabled = Clock.System.now()
.toLocalDateTime(TimeZone.currentSystemDefault())
.month.let { it == java.time.Month.DECEMBER }
AnimatedVisibility(
activeRoute is MeteroidScreen.Home && server != null && user != null,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it })
) {
NavigationBar(
contentColor = MaterialTheme.colorScheme.primary
) {
MeteroidBottomBarItem(
MeteroidScreen.Home.Purchase,
activeRoute == MeteroidScreen.Home.Purchase,
) {
if (server != null && user != null) {
navController.navigate(MeteroidScreen.Home.Purchase.build(server, user)) {
launchSingleTop = true
restoreState = false
popUpTo(findStartDestination(navController.graph).id) {
saveState = false
}
}
}
}
MeteroidBottomBarItem(
MeteroidScreen.Home.Deposit,
activeRoute == MeteroidScreen.Home.Deposit,
) {
if (server != null && user != null) {
navController.navigate(MeteroidScreen.Home.Deposit.build(server, user)) {
launchSingleTop = true
restoreState = false
popUpTo(findStartDestination(navController.graph).id) {
saveState = false
}
}
}
}
if (!historyDisabled) {
MeteroidBottomBarItem(
MeteroidScreen.Home.History,
activeRoute == MeteroidScreen.Home.History,
) {
if (server != null && user != null) {
navController.navigate(MeteroidScreen.Home.History.build(server, user)) {
launchSingleTop = true
restoreState = false
popUpTo(findStartDestination(navController.graph).id) {
saveState = false
}
}
}
}
if (wrappedEnabled) {
MeteroidBottomBarItem(
MeteroidScreen.Home.Wrapped,
activeRoute == MeteroidScreen.Home.Wrapped,
) {
if (server != null && user != null) {
navController.navigate(MeteroidScreen.Home.Wrapped.build(server, user)) {
launchSingleTop = true
restoreState = false
popUpTo(findStartDestination(navController.graph).id) {
saveState = false
}
}
}
}
}
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment