diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt index 39880c07be311f29bcdf603b0e67c84b311cbed0..ef0339084614aa6da562ae354384c455df6ce624 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt @@ -24,6 +24,9 @@ package de.chaosdorf.mete.model +import kotlinx.serialization.Serializable + +@Serializable @JvmInline value class ServerId(val value: Long) { override fun toString() = value.toString() diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 86f5a5a98dcb9140ee0e94ecaabfde4ecea18809..fc70942eb38278e03007515aaf21a89d517e1593 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -88,7 +88,9 @@ dependencies { implementation(libs.androidx.room.ktx) 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.coil.compose) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index 88155e5d01941e26ede102391c8aec6a0ce60acc..c20fdae3230763e6db42afd74718e50b99201055 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -27,33 +27,168 @@ package de.chaosdorf.meteroid import android.os.Bundle import androidx.activity.ComponentActivity 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.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.get +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.* +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.withCreationCallback import de.chaosdorf.meteroid.theme.MeteroidTheme -import de.chaosdorf.meteroid.ui.MeteroidRouter - +import de.chaosdorf.meteroid.ui.* +import de.chaosdorf.meteroid.viewmodel.* @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { 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 { - viewModel.initialAccount.value == null + viewModel.setupComplete.value == null } setContent { - val initialAccount by viewModel.initialAccount.collectAsState() + val setupComplete by viewModel.setupComplete.collectAsState() MeteroidTheme { - if (initialAccount != null) { - MeteroidRouter(initialAccount!!) + if (setupComplete != null) { + 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) + } + } + ) + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt index f74c0256e259f76f2cc20fe5720a5016eda0510c..01a814eaed2144c8afd7b8e58b64aead528d7d7f 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt @@ -31,10 +31,12 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.longPreferencesKey import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest import javax.inject.Inject +@OptIn(ExperimentalCoroutinesApi::class) class AccountPreferencesImpl @Inject constructor( private val dataStore: DataStore<Preferences> ) : AccountPreferences { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt index 83927e00dc2967ad30c1e1c454424d9b85e129ed..83844ea67d6d91fc4f791679c6cdb92a02f0955c 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt @@ -30,10 +30,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat private val LightColors = lightColorScheme( @@ -104,7 +102,6 @@ private val DarkColors = darkColorScheme( @Composable fun MeteroidTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ dynamicColor: Boolean = false, content: @Composable () -> Unit ) { @@ -121,12 +118,8 @@ fun MeteroidTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - window.navigationBarColor = colorScheme.surfaceColorAtElevation(3.dp).toArgb() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - window.navigationBarDividerColor = colorScheme.surfaceColorAtElevation(3.dp).toArgb() - } - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !darkTheme } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt deleted file mode 100644 index c0861ac5f0dbe822352f32777f47e06bb8b7990f..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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() - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/BottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/BottomBar.kt new file mode 100644 index 0000000000000000000000000000000000000000..88db1368a9ab9cfb9f844c8682c1c7d9debfe608 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/BottomBar.kt @@ -0,0 +1,112 @@ +/* + * 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.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Celebration +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.LocalAtm +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBarItem +import androidx.compose.runtime.Composable +import androidx.navigation3.runtime.NavBackStack +import de.chaosdorf.meteroid.theme.icons.MeteroidIcons +import de.chaosdorf.meteroid.theme.icons.outlined.WaterFull + +@Composable +fun BottomBar( + backStack: NavBackStack, +) { + val currentRoute = backStack.lastOrNull() + + AnimatedVisibility( + visible = currentRoute is Routes.UserScope, + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }) + ) { + BottomAppBar { + NavigationBarItem( + selected = currentRoute is Routes.Purchase, + onClick = { + if (currentRoute is Routes.UserScope) { + backStack.remove(currentRoute) + backStack.add( + Routes.Purchase(currentRoute.serverId, currentRoute.userId) + ) + } + }, + icon = { + Icon(MeteroidIcons.Outlined.WaterFull, contentDescription = null) + }, + ) + NavigationBarItem( + selected = currentRoute is Routes.Deposit, + onClick = { + if (currentRoute is Routes.UserScope) { + backStack.remove(currentRoute) + backStack.add( + Routes.Deposit(currentRoute.serverId, currentRoute.userId) + ) + } + }, + icon = { + 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) + ) + } + }, + icon = { + 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) + ) + } + }, + icon = { + Icon(Icons.Outlined.Celebration, contentDescription = null) + }, + ) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/DepositRoute.kt similarity index 65% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/DepositRoute.kt index d05bf86d1823651e98768f108fa1f41151feb027..67eb64775cf2a8ba2ebf4fd2042cce24d834a092 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/DepositRoute.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,18 +22,24 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.navigation +package de.chaosdorf.meteroid.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically +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.UserViewModel @Composable -fun NavigationAnimationContainer( - visible: Boolean, content: @Composable () -> Unit +fun DepositRoute( + viewModel: UserViewModel, + backStack: NavBackStack, + contentPadding: PaddingValues, ) { - AnimatedVisibility(visible, enter = expandVertically(), exit = shrinkVertically()) { - content() + Column(Modifier.padding(contentPadding)) { + Text("Deposit ${viewModel.serverId} ${viewModel.userId}") } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/HistoryRoute.kt similarity index 62% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/HistoryRoute.kt index f6cbec97ea4118616f90cdf85328676da637fbcb..184bd4c52fbef6d41f0ac3be5930d5739172e03d 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/HistoryRoute.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,28 +22,24 @@ * 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.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavBackStack +import de.chaosdorf.meteroid.viewmodel.UserViewModel @Composable -fun TransactionHistoryScreen( - viewModel: TransactionHistoryViewModel, - contentPadding: PaddingValues = PaddingValues(), +fun HistoryRoute( + viewModel: UserViewModel, + backStack: NavBackStack, + contentPadding: PaddingValues, ) { - val transactions by viewModel.transactions.collectAsState() - - LazyColumn(contentPadding = contentPadding) { - items( - transactions, - key = { "transaction-${it.transaction.serverId}-${it.transaction.transactionId}" }, - ) { (transaction, drink) -> - TransactionHistoryItem(transaction, drink) - } + Column(Modifier.padding(contentPadding)) { + Text("History ${viewModel.serverId} ${viewModel.userId}") } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt deleted file mode 100644 index 45bba3a01fb16faed4fbb1c9e487303fe3981119..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * 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)) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt deleted file mode 100644 index 19dbfe5a3d668f2f3db5dec983abd810b6028e56..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt deleted file mode 100644 index f0c27244cb65a1f7a7603b7813a9da5bd689704b..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.material.icons.Icons -import androidx.compose.material.icons.outlined.Celebration -import androidx.compose.material.icons.outlined.History -import androidx.compose.material.icons.outlined.LocalAtm -import androidx.compose.material.icons.twotone.Celebration -import androidx.compose.material.icons.twotone.History -import androidx.compose.material.icons.twotone.LocalAtm -import androidx.compose.ui.graphics.vector.ImageVector -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.theme.icons.MeteroidIcons -import de.chaosdorf.meteroid.theme.icons.outlined.WaterFull -import de.chaosdorf.meteroid.theme.icons.twotone.WaterFull - -sealed class MeteroidScreen(val route: String) { - sealed class Home( - val label: String, - 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 - .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( - "Wrapped", - Icons.TwoTone.Celebration, - Icons.Outlined.Celebration, - "server/{server}/user/{user}/wrapped" - ) { - fun build(server: ServerId, user: UserId) = route - .replace("{server}", server.value.toString()) - .replace("{user}", user.value.toString()) - } - } - - data object UserList : MeteroidScreen("server/{server}/userList") { - fun build(server: ServerId) = route - .replace("{server}", server.value.toString()) - } - - data object AddServer : MeteroidScreen("addServer") { - fun build() = route - } - - data object Settings : MeteroidScreen("settings") { - fun build() = route - } - - companion object { - fun byRoute(route: String?) = when (route) { - 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 - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt similarity index 61% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt index a38f63060a6a2390be36737fe162872582545149..7a56bd15e96da89e27388f5320b75d17c0a0d342 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,18 +22,24 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.home.deposit +package de.chaosdorf.meteroid.ui -import androidx.annotation.DrawableRes -import de.chaosdorf.meteroid.R -import java.math.BigDecimal +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.UserViewModel -enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) { - MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50), - MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100), - MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200), - MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500), - MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000), - MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000), - MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000), +@Composable +fun PurchaseRoute( + viewModel: UserViewModel, + backStack: NavBackStack, + contentPadding: PaddingValues, +) { + Column(Modifier.padding(contentPadding)) { + Text("Purchase ${viewModel.serverId} ${viewModel.userId}") + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Routes.kt similarity index 50% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/Routes.kt index 265d989aadbda4586a144df70bfbcf1de0933a73..dacce6eb0bfc45c278c2bda6c3170d645c7054ad 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Routes.kt @@ -22,30 +22,57 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.navigation - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -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 - } - ) {} +package de.chaosdorf.meteroid.ui + +import androidx.navigation3.runtime.NavKey +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId +import kotlinx.serialization.Serializable + +sealed interface Routes : NavKey { + interface ServerScope { + val serverId: ServerId + } + + 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 } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/ServerListRoute.kt similarity index 55% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/ServerListRoute.kt index 20344554c820e7ab22adae06940ed3e34b572d2b..b5aefc1b68b149607fc0ad6f1d33006132563330 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/ServerListRoute.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,35 +22,42 @@ * 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.layout.requiredHeight -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Icon +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.ui.AvatarLayout +import androidx.navigation3.runtime.NavBackStack +import coil3.compose.AsyncImage +import de.chaosdorf.meteroid.viewmodel.ServerListViewModel @Composable -fun NavigationSettingsItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) { - val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height") +fun ServerListRoute( + viewModel: ServerListViewModel, + backStack: NavBackStack, + contentPadding: PaddingValues, +) { + val servers by viewModel.servers.collectAsState() - ListItem( - headlineContent = { Text("Settings") }, - leadingContent = { - AvatarLayout { - Icon(Icons.Filled.Settings, contentDescription = null) - } - }, - modifier = Modifier.requiredHeight(height) - .clickable(onClick = if (expanded) onClick else onExpand) - ) + LazyColumn(contentPadding = contentPadding) { + items(servers, { "server-${it.serverId}" }) { server -> + ListItem( + headlineContent = { Text(server.name ?: "Unknown") }, + supportingContent = { Text(server.url) }, + leadingContent = if (server.logoUrl != null) { + @Composable { AsyncImage(server.logoUrl, contentDescription = null) } + } else null, + modifier = Modifier.clickable { + backStack.add(Routes.UserList(server.serverId)) + }, + ) + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SettingsRoute.kt similarity index 79% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/SettingsRoute.kt index 898ff670076a1d846e7f631e71414820498bc5d8..6065bc8d0d414f5e9043022ca32adf81301a6901 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SettingsRoute.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.settings +package de.chaosdorf.meteroid.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -30,16 +30,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController +import androidx.navigation3.runtime.NavBackStack +import de.chaosdorf.meteroid.viewmodel.SettingsViewModel @Composable -fun SettingsScreen(navController: NavController, viewModel: SettingsViewModel, contentPadding: PaddingValues) { - Column( - Modifier - .padding(contentPadding) - .padding(16.dp, 8.dp) - ) { +fun SettingsRoute( + viewModel: SettingsViewModel, + backStack: NavBackStack, + contentPadding: PaddingValues, +) { + Column(Modifier.padding(contentPadding)) { Text("Settings") } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SetupRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SetupRoute.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a1b380555f708469f1b8a7a275d798c2f5918f3 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SetupRoute.kt @@ -0,0 +1,78 @@ +/* + * 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.* +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.navigation3.runtime.NavBackStack +import de.chaosdorf.meteroid.viewmodel.SetupViewModel +import kotlinx.coroutines.launch + +@Composable +fun SetupRoute( + viewModel: SetupViewModel, + backStack: NavBackStack, + contentPadding: PaddingValues, +) { + val coroutineScope = rememberCoroutineScope() + val serverUrl by viewModel.serverUrl.collectAsState() + val error by viewModel.error.collectAsState() + + Column(Modifier.padding(contentPadding)) { + error?.let { + Surface( + color = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ) { + Text(it) + } + } + TextField( + value = serverUrl, + 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") + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/TopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/TopBar.kt new file mode 100644 index 0000000000000000000000000000000000000000..3bdf179d882ef73c0f47fb2540b17250ec58ba8f --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/TopBar.kt @@ -0,0 +1,161 @@ +/* + * 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") + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/UserListRoute.kt similarity index 55% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/UserListRoute.kt index 9bc43f5e0994141974f668904b40c6b8c989b2b3..594c9a826e26633bf48fa9a304f4ff981acaf8b7 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/UserListRoute.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,35 +22,42 @@ * 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.layout.requiredHeight -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.Icon +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.ui.AvatarLayout +import androidx.navigation3.runtime.NavBackStack +import coil3.compose.AsyncImage +import de.chaosdorf.meteroid.viewmodel.UserListViewModel @Composable -fun NavigationAddServerItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) { - val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height") +fun UserListRoute( + viewModel: UserListViewModel, + backStack: NavBackStack, + contentPadding: PaddingValues, +) { + val users by viewModel.users.collectAsState() - ListItem( - headlineContent = { Text("Add Server") }, - leadingContent = { - AvatarLayout { - Icon(Icons.Default.Add, contentDescription = null) - } - }, - modifier = Modifier.requiredHeight(height) - .clickable(onClick = if (expanded) onClick else onExpand) - ) + LazyColumn(contentPadding = contentPadding) { + items(users, { "server-${it.serverId}/user-${it.userId}"}) { + ListItem( + headlineContent = { Text(it.name) }, + supportingContent = { Text(it.email ?: "") }, + leadingContent = if (it.gravatarUrl != null) { + @Composable { AsyncImage(it.gravatarUrl, contentDescription = null) } + } else null, + modifier = Modifier.clickable { + backStack.add(Routes.Purchase(it.serverId, it.userId)) + } + ) + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/WrappedRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/WrappedRoute.kt new file mode 100644 index 0000000000000000000000000000000000000000..52ecd9713421f0dfef05ee9c63071f3f96d4f8f2 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/WrappedRoute.kt @@ -0,0 +1,45 @@ +/* + * 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}") + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt deleted file mode 100644 index 789047bc7d895b5c7403e9d3082477007ba0d75c..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 - } - } - } - } - } - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt deleted file mode 100644 index 052ee599f2f4806582776a3480ed278fe52b049a..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.foundation.layout.RowScope -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import de.chaosdorf.meteroid.theme.onPrimaryContainerTinted -import de.chaosdorf.meteroid.ui.MeteroidScreen - -@Composable -fun RowScope.MeteroidBottomBarItem( - route: MeteroidScreen.Home, - active: Boolean, - onClick: () -> Unit, -) { - NavigationBarItem( - icon = { - Icon( - if (active) route.activeIcon else route.inactiveIcon, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainerTinted - ) - }, - label = { Text(route.label, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) }, - selected = active, - onClick = onClick, - colors = NavigationBarItemDefaults.colors( - indicatorColor = MaterialTheme.colorScheme.primaryContainer - ) - ) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt deleted file mode 100644 index 8a90ba385131642c3f0323287f0be815d4ebffd6..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.foundation.layout.padding -import androidx.compose.material3.Badge -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import java.math.BigDecimal - -@Composable -fun PriceBadge( - price: BigDecimal, - modifier: Modifier = Modifier, - containerColor: Color = - if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.error, - textColor: Color = - if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary - else MaterialTheme.colorScheme.onError, - textStyle: TextStyle = MaterialTheme.typography.labelLarge -) { - Badge( - containerColor = containerColor, - modifier = modifier - ) { - Text( - "%.02f €".format(price), - style = textStyle, - color = textColor, - modifier = Modifier.padding(8.dp, 4.dp) - ) - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt deleted file mode 100644 index 1e83c4f935582132652c5ee88f214fa9abc81047..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.deposit - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -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.res.painterResource -import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.ui.home.PriceBadge - -@Composable -fun DepositMoneyItem( - item: MonetaryAmount, - modifier: Modifier = Modifier, - onDeposit: (MonetaryAmount) -> Unit = {} -) { - Box( - modifier = modifier - .height(IntrinsicSize.Max) - .clip(RoundedCornerShape(8.dp)) - .clickable { onDeposit(item) } - ) { - Image( - painterResource(item.image), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .aspectRatio(1.0f) - ) - PriceBadge( - item.amount, - modifier = Modifier - .align(Alignment.BottomEnd) - .paddingFromBaseline(bottom = 24.dp) - ) - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt deleted file mode 100644 index 339520e5d35c339c0fd71585893adf9e37f1d861..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.deposit - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController - -@Composable -fun DepositScreen( - navController: NavController, - viewModel: DepositViewModel, - contentPadding: PaddingValues = PaddingValues(), -) { - LazyVerticalGrid( - GridCells.Adaptive(104.dp), - contentPadding = contentPadding, - modifier = Modifier.padding(horizontal = 8.dp), - ) { - items( - viewModel.money, - key = { "deposit-${it.ordinal}" }, - ) { monetaryAmount -> - DepositMoneyItem(monetaryAmount) { - viewModel.deposit(it, navController::navigateUp) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt deleted file mode 100644 index d3ca74c4b4ba60feec86aed15e62377910c98ef7..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.purchase - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil3.compose.rememberAsyncImagePainter -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.theme.secondaryGradient -import de.chaosdorf.meteroid.ui.home.PriceBadge -import kotlinx.coroutines.delay -import java.math.BigDecimal - -@Composable -fun PurchaseDrinkTile( - item: Drink, - modifier: Modifier = Modifier, - onPurchase: (Drink, Int) -> Unit = { _, _ -> } -) { - var purchaseCount by remember { mutableStateOf(0) } - val pendingPurchases = purchaseCount != 0 - - LaunchedEffect(purchaseCount) { - delay(2000L) - onPurchase(item, purchaseCount) - purchaseCount = 0 - } - - val thumbPainter = rememberAsyncImagePainter( - item.logoUrl - ) - val drinkPainter = rememberAsyncImagePainter( - item.originalLogoUrl, - error = thumbPainter - ) - - Column( - modifier = modifier - .height(IntrinsicSize.Max) - .alpha(if (item.active) 1.0f else 0.67f) - .clip(RoundedCornerShape(8.dp)) - .clickable { purchaseCount += 1 } - .padding(8.dp) - ) { - Box( - Modifier.aspectRatio(1.0f) - .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient(), CircleShape), - contentAlignment = Alignment.Center - ) { - Image( - drinkPainter, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier.alpha(if (pendingPurchases) 0.0f else 1.0f) - .clip(CircleShape) - ) - PriceBadge( - item.price, - modifier = Modifier - .alpha(if (pendingPurchases) 0.0f else 1.0f) - .align(Alignment.BottomEnd) - .paddingFromBaseline(bottom = 12.dp) - ) - Text( - "×$purchaseCount", - fontSize = 36.sp, - fontWeight = FontWeight.Light, - color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f), - textAlign = TextAlign.Center, - modifier = Modifier.alpha(if (pendingPurchases) 1.0f else 0.0f) - ) - } - Spacer(Modifier.height(4.dp)) - Text( - item.name, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.labelLarge, - ) - Spacer(Modifier.height(4.dp)) - Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { - val unitPrice = - if (item.volume <= BigDecimal.ZERO) null - else item.price / item.volume - - Text( - if (unitPrice == null) String.format("%.02fl", item.volume) - else String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt deleted file mode 100644 index 807b2eca4e238fd1b3feb725f2694e6358ac9f1f..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.purchase - -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun PurchaseFilterChip( - label: String, - selected: Boolean, - onClick: () -> Unit, -) { - FilterChip( - label = { - Text(label, style = MaterialTheme.typography.labelLarge) - }, - selected = selected, - leadingIcon = { - if (selected) { - Icon( - Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - } - }, - onClick = onClick, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer - ) - ) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt deleted file mode 100644 index 74f49ebbd24c61947207cae0fd3e3814200b7738..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.purchase - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun PurchaseScreen( - navController: NavController, - viewModel: PurchaseViewModel, - contentPadding: PaddingValues = PaddingValues(), -) { - val drinks by viewModel.drinks.collectAsState() - val filters by viewModel.filters.collectAsState() - - LazyVerticalGrid( - GridCells.Adaptive(104.dp), - contentPadding = contentPadding, - modifier = Modifier.padding(horizontal = 8.dp), - ) { - item("filter", span = { GridItemSpan(maxLineSpan) }) { - FlowRow( - modifier = Modifier.padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - PurchaseFilterChip( - label = "Active", - selected = filters.contains(PurchaseViewModel.Filter.Active), - onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.Active) } - ) - PurchaseFilterChip( - label = "Coffeine Free", - selected = filters.contains(PurchaseViewModel.Filter.CaffeineFree), - onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.CaffeineFree) } - ) - } - } - - items( - drinks, - key = { "drink-${it.serverId}-${it.drinkId}" }, - ) { drink -> - PurchaseDrinkTile(drink) { item, count -> - viewModel.purchase(item, count, navController::navigateUp) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt deleted file mode 100644 index 16302040e89779d76082a852e612071d6bc435f9..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.purchase - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.SyncManager -import de.chaosdorf.meteroid.util.update -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class PurchaseViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, - private val accountProvider: AccountProvider, - private val syncManager: SyncManager, - drinkRepository: DrinkRepository, -) : ViewModel() { - val serverId = ServerId(checkNotNull(savedStateHandle["server"])) - val userId = UserId(checkNotNull(savedStateHandle["user"])) - - val filters: StateFlow<Set<Filter>> = - savedStateHandle.getStateFlow("filters", setOf(Filter.Active)) - - val drinks: StateFlow<List<Drink>> = combine( - drinkRepository.getAllFlow(serverId), - filters - ) { drinks, filters -> - drinks.filter { item -> - filters.all { filter -> filter.matches(item) } - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - fun toggleFilter(filter: Filter) { - savedStateHandle.update<Set<Filter>>("filters", emptySet()) { filters -> - if (filters.contains(filter)) filters - filter - else filters + filter - } - } - - fun purchase(item: Drink, count: Int, onBack: () -> Unit) { - viewModelScope.launch { - accountProvider.account(serverId, userId)?.let { account -> - syncManager.purchase(account, item, count) - if (!account.pinned) { - onBack() - } - } - } - } - - fun sync() { - viewModelScope.launch { - accountProvider.account(serverId, userId)?.let { account -> - syncManager.sync(account.server, account.user, incremental = true) - } - } - } - - enum class Filter { - CaffeineFree, - Active; - - fun matches(item: Drink) = when (this) { - CaffeineFree -> item.caffeine == 0 - Active -> item.active - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt deleted file mode 100644 index 968d8fc7968b8824600087e2599f2af1d2809553..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.transactionhistory - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AttachMoney -import androidx.compose.material.icons.filled.QuestionMark -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -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.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import coil3.compose.rememberAsyncImagePainter -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.Transaction -import de.chaosdorf.meteroid.theme.secondaryGradient -import de.chaosdorf.meteroid.ui.home.PriceBadge -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.math.BigDecimal -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Composable -fun TransactionHistoryItem( - transaction: Transaction, - drink: Drink?, - modifier: Modifier = Modifier -) { - val timestamp = transaction.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) - val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) - - ListItem( - headlineContent = { - val label = when { - drink != null -> drink.name - transaction.difference > BigDecimal.ZERO -> "Deposit" - else -> "Unknown" - } - Text(label) - }, - supportingContent = { - Text(formatter.format(timestamp.toJavaLocalDateTime())) - }, - leadingContent = { - Box( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .aspectRatio(1.0f) - .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient()) - ) { - if (drink != null) { - val thumbPainter = rememberAsyncImagePainter( - drink.logoUrl - ) - val originalPainter = rememberAsyncImagePainter( - drink.originalLogoUrl, - error = thumbPainter - ) - - Image( - painter = originalPainter, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .align(Alignment.Center) - .fillMaxSize() - ) - } else if (transaction.difference > BigDecimal.ZERO) { - Icon( - Icons.Default.AttachMoney, - contentDescription = null, - modifier = Modifier.align(Alignment.Center) - ) - } else { - Icon( - Icons.Default.QuestionMark, - contentDescription = null, - modifier = Modifier.align(Alignment.Center) - ) - } - } - }, - trailingContent = { - PriceBadge( - transaction.difference, - modifier = Modifier.padding(horizontal = 8.dp) - ) - }, - modifier = modifier, - ) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt deleted file mode 100644 index 0ad8aacac4d956c84fd73bac2d1143fe576b6311..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.transactionhistory - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.TransactionRepository -import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.SyncManager -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import java.math.BigDecimal -import javax.inject.Inject -import kotlin.time.Duration.Companion.minutes - -@HiltViewModel -class TransactionHistoryViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val accountProvider: AccountProvider, - private val syncManager: SyncManager, - repository: TransactionRepository, - drinkRepository: DrinkRepository -) : ViewModel() { - private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) - private val userId = UserId(checkNotNull(savedStateHandle["user"])) - - val transactions: StateFlow<List<TransactionInfo>> = combine( - repository.getAllFlow(serverId, userId), drinkRepository.getAllFlow(serverId) - ) { transactions, drinks -> - transactions.map { transaction -> - TransactionInfo(transaction, - drinks.firstOrNull { drink -> drink.drinkId == transaction.drinkId }) - } - }.mapLatest { list -> - list.mergeAdjecentDeposits().filter { - it.drink != null || it.transaction.difference.abs() >= 0.01.toBigDecimal() - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - fun sync() { - viewModelScope.launch { - accountProvider.account(serverId, userId)?.let { account -> - syncManager.sync(account.server, account.user, incremental = true) - } - } - } -} - -fun List<TransactionInfo>.mergeAdjecentDeposits(): List<TransactionInfo> { - val result = mutableListOf<TransactionInfo>() - for (entry in this) { - val previous = result.lastOrNull() - if (previous != null && previous.transaction.difference > BigDecimal.ZERO && entry.transaction.difference > BigDecimal.ZERO && previous.drink == null && entry.drink == null && entry.transaction.timestamp.minus( - previous.transaction.timestamp - ) < 5.minutes - ) { - result.removeLast() - result.add( - entry.copy( - transaction = entry.transaction.copy( - difference = entry.transaction.difference + previous.transaction.difference - ) - ) - ) - } else { - result.add(entry) - } - } - return result -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt deleted file mode 100644 index 15e2038601aca582d78b1afe9f3e58be3254907d..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.transactionhistory - -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.Transaction - -data class TransactionInfo( - val transaction: Transaction, - val drink: Drink? -) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt deleted file mode 100644 index 9aaa2b41bb1f0eef3c24d3a50de2a5aebd2d0ea9..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.wrapped - -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ListItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import coil3.compose.rememberAsyncImagePainter -import de.chaosdorf.meteroid.R -import java.time.format.TextStyle - -@Composable -fun WrappedScreen( - viewModel: WrappedViewModel, - contentPadding: PaddingValues = PaddingValues(), -) { - val slides by viewModel.slides.collectAsState() - @Suppress("DEPRECATION") - val locale = LocalConfiguration.current.let { - if (VERSION.SDK_INT >= VERSION_CODES.N) it.locales.get(0) - else it.locale - } - - LazyColumn(contentPadding = contentPadding) { - items( - slides, - key = { "wrapped-${it::class.simpleName}" }, - ) { slide -> - when (slide) { - is WrappedSlide.MostBoughtDrink -> - ListItem( - headlineContent = { - Text("Your favorite drink is ${slide.drink.name}") - }, - supportingContent = { - Text("At least you enjoyed it ${slide.count} times this year.") - }, - leadingContent = { - val thumbPainter = rememberAsyncImagePainter( - slide.drink.logoUrl - ) - val drinkPainter = rememberAsyncImagePainter( - slide.drink.originalLogoUrl, - error = thumbPainter - ) - - Image( - drinkPainter, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier.size(72.dp) - ) - } - ) - - is WrappedSlide.Caffeine -> - ListItem( - headlineContent = { - Text("You consumed ${slide.total} mg of caffeine this year.") - }, - supportingContent = { - slide.wouldKill?.let { animal -> - Text("This could kill a medium-weight ${animal.name}. Glad you're still here.") - } - }, - leadingContent = { - val painter = painterResource( - when (slide.wouldKill) { - WrappedSlide.Caffeine.Animal.Squirrel -> R.drawable.wrapped_squirrel - WrappedSlide.Caffeine.Animal.Rat -> R.drawable.wrapped_rat - WrappedSlide.Caffeine.Animal.Cat -> R.drawable.wrapped_cat - WrappedSlide.Caffeine.Animal.Koala -> R.drawable.wrapped_koala - WrappedSlide.Caffeine.Animal.Lynx -> R.drawable.wrapped_lynx - WrappedSlide.Caffeine.Animal.Jaguar -> R.drawable.wrapped_jaguar - WrappedSlide.Caffeine.Animal.Reindeer -> R.drawable.wrapped_reindeer - WrappedSlide.Caffeine.Animal.Gorilla -> R.drawable.wrapped_gorilla - WrappedSlide.Caffeine.Animal.Lion -> R.drawable.wrapped_lion - WrappedSlide.Caffeine.Animal.Bear -> R.drawable.wrapped_bear - WrappedSlide.Caffeine.Animal.Moose -> R.drawable.wrapped_moose - else -> R.drawable.wrapped_coffee_beans - } - ) - - Image( - painter, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier.size(72.dp) - ) - } - ) - - is WrappedSlide.MostActive -> - ListItem( - headlineContent = { - Text( - "You were most active on ${ - slide.weekday.getDisplayName(TextStyle.FULL, locale) - }s at ${slide.hour} o'clock." - ) - }, - leadingContent = { - val painter = painterResource(R.drawable.wrapped_clock) - - Image( - painter, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier.size(72.dp) - ) - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt deleted file mode 100644 index ec264b6d77d268b361ef974abb7aff4af74c22a9..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.wrapped - -import de.chaosdorf.mete.model.DrinkId -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.Transaction -import kotlinx.datetime.DayOfWeek -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime - -sealed class WrappedSlide { - interface Factory { - fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> - ): WrappedSlide? - } - - data class MostBoughtDrink( - val drink: Drink, - val count: Int, - ) : WrappedSlide() { - companion object : Factory { - override fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> - ): WrappedSlide? = transactions - .mapNotNull { drinks[it.drinkId] } - .groupingBy { it } - .eachCount() - .maxByOrNull { it.value } - ?.let { (mostBoughtDrink, count) -> - MostBoughtDrink(mostBoughtDrink, count) - } - } - } - - data class Caffeine( - val total: Double, - val wouldKill: Animal?, - ) : WrappedSlide() { - enum class Animal(val lethalDosage: Double) { - Hamster(0.25), - Squirrel(0.3), - Rat(0.4), - GuineaPig(0.9), - Lemur(2.5), - Cat(5.0), - Koala(9.0), - Coyote(13.0), - Lynx(23.0), - Capybara(55.0), - Jaguar(81.0), - Reindeer(101.0), - Gorilla(140.0), - Lion(175.0), - Bear(278.0), - Moose(368.0), - Bison(540.0) - } - - companion object : Factory { - override fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> - ): WrappedSlide = transactions - .mapNotNull { drinks[it.drinkId] } - .mapNotNull { drink -> drink.caffeine?.let { it * drink.volume.toDouble() * 10 } } - .sum() - .let { dosage -> - Caffeine( - dosage, - Animal.entries - .sortedBy(Animal::lethalDosage) - .lastOrNull { it.lethalDosage < dosage } - ) - } - } - } - - data class MostActive( - val weekday: DayOfWeek, - val hour: Int, - ) : WrappedSlide() { - companion object : Factory { - override fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> - ): WrappedSlide? = transactions - .map { it.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) } - .groupingBy { Pair(it.dayOfWeek, it.hour) } - .eachCount() - .maxByOrNull { it.value } - ?.key - ?.let { (dayOfWeek, hour) -> - MostActive(dayOfWeek, hour) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt deleted file mode 100644 index 91e1f7d4666784c7874a3100c5ddfb4e82634769..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.wrapped - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.model.DrinkId -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.Transaction -import de.chaosdorf.meteroid.model.TransactionRepository -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import kotlinx.datetime.* -import javax.inject.Inject - -@HiltViewModel -class WrappedViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - repository: TransactionRepository, - drinkRepository: DrinkRepository, -) : ViewModel() { - private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) - private val userId = UserId(checkNotNull(savedStateHandle["user"])) - - private fun List<Transaction>.filterAudits(year: Int): List<Transaction> { - val yearBegin = LocalDateTime(year, Month.JANUARY, 1, 0, 0, 0) - .toInstant(TimeZone.UTC) - val yearEnd = LocalDateTime(year, Month.DECEMBER, 31, 23, 59, 59) - .toInstant(TimeZone.UTC) - return this.filter { - it.timestamp in yearBegin..yearEnd - } - } - - val slides: StateFlow<List<WrappedSlide>> = combine( - repository.getAllFlow(serverId, userId), - drinkRepository.getAllFlow(serverId) - ) { transactions, drinks -> - val drinkMap: Map<DrinkId, Drink> = drinks.associateBy(Drink::drinkId) - val factories = listOf( - WrappedSlide.MostBoughtDrink, - WrappedSlide.Caffeine, - WrappedSlide.MostActive - ) - val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) - val content = transactions.filterAudits(now.year) - factories.mapNotNull { it.create(content, drinkMap) } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt deleted file mode 100644 index 267caa347782514dbda46843504bb5ccb95d4bc9..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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.navigation - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.currentBackStackEntryAsState -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.storage.AccountPreferences -import de.chaosdorf.meteroid.sync.base.SyncHandler -import de.chaosdorf.meteroid.ui.MeteroidScreen -import de.chaosdorf.meteroid.util.findStartDestination - -@Composable -fun MeteroidNavigation(navController: NavController, viewModel: NavigationViewModel) { - val backStackEntry by navController.currentBackStackEntryAsState() - val activeRoute = MeteroidScreen.byRoute(backStackEntry?.destination?.route) - - val expanded by viewModel.expanded.collectAsState() - val account by viewModel.account.collectAsState() - val entries by viewModel.entries.collectAsState() - val syncState by viewModel.syncState.collectAsState() - - LaunchedEffect(navController) { - navController.addOnDestinationChangedListener { _, _, arguments -> - val serverId = arguments?.getLong("server")?.let(::ServerId) - val userId = arguments?.getLong("user")?.let(::UserId) - - viewModel.account.value = AccountPreferences.State(serverId, userId) - viewModel.expanded.value = false - } - } - - BackHandler(expanded) { - viewModel.expanded.value = false - } - - val verticalContentPadding: Dp by animateDpAsState(if (expanded) 4.dp else 0.dp, label = "verticalContentPadding") - val shadowElevation: Dp by animateDpAsState(if (expanded) 16.dp else 4.dp, label = "shadowElevation") - - Surface( - Modifier.fillMaxWidth().animateContentSize() - .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 128.dp), - shape = RoundedCornerShape(32.dp), - shadowElevation = shadowElevation, - tonalElevation = 8.dp - ) { - Box { - LazyColumn(contentPadding = PaddingValues(vertical = verticalContentPadding)) { - items(entries, key = NavigationElement::key) { entry -> - val isCurrent = entry.isCurrent(activeRoute, account?.server, account?.user) - - NavigationAnimationContainer( - expanded || isCurrent - ) { - when (entry) { - is NavigationElement.ServerElement -> NavigationServerItem(expanded, entry.server) { - viewModel.expanded.value = true - } - - is NavigationElement.UserElement -> NavigationUserItem(expanded, - entry.user, - entry.pinned, - viewModel::togglePin, - onExpand = { viewModel.expanded.value = true } - ) { - if (isCurrent) { - viewModel.expanded.value = false - } else { - navController.navigate( - MeteroidScreen.Home.Purchase.build( - entry.user.serverId, entry.user.userId - ) - ) { - launchSingleTop = true - restoreState = false - popUpTo(findStartDestination(navController.graph).id) { - saveState = false - } - } - viewModel.selectUser(entry.user.serverId, entry.user.userId) - } - } - - is NavigationElement.UserListElement -> NavigationUserListItem { - if (NavigationElement.ServerElement(entry.server) - .isCurrent(activeRoute, account?.server, account?.user) - ) { - viewModel.expanded.value = false - } else { - navController.navigate(MeteroidScreen.UserList.build(entry.server.serverId)) { - launchSingleTop = true - restoreState = false - popUpTo(findStartDestination(navController.graph).id) { - saveState = false - } - } - } - } - - NavigationElement.AddServerElement -> NavigationAddServerItem(expanded, - onExpand = { viewModel.expanded.value = true } - ) { - if (isCurrent) { - viewModel.expanded.value = false - } else { - navController.navigate(MeteroidScreen.AddServer.build()) - } - } - - NavigationElement.SettingsElement -> NavigationSettingsItem( - expanded, - onExpand = { viewModel.expanded.value = true } - ) { - if (isCurrent) { - viewModel.expanded.value = false - } else { - navController.navigate(MeteroidScreen.Settings.build()) - } - } - } - } - } - } - AnimatedVisibility(syncState == SyncHandler.State.Loading, enter = fadeIn(), exit = fadeOut()) { - LinearProgressIndicator(Modifier.align(Alignment.TopCenter).requiredHeight(2.dp).fillMaxWidth()) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt deleted file mode 100644 index 706d29924f2debd4e8084e0e7b472e443862a519..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.navigation - -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.User -import de.chaosdorf.meteroid.ui.MeteroidScreen - -sealed class NavigationElement(val key: String) { - abstract fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean - - data class ServerElement( - val server: Server, - ) : NavigationElement("navigation-${server.serverId}") { - - override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { - return route == MeteroidScreen.UserList && serverId == server.serverId - } - } - - data class UserElement( - val user: User, - val pinned: Boolean, - ) : NavigationElement("navigation-${user.serverId}-${user.userId}-${if (pinned) "pinned" else "current"}") { - override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { - return route is MeteroidScreen.Home && serverId == user.serverId && userId == user.userId - } - } - - data class UserListElement( - val server: Server, - ) : NavigationElement("navigation-${server.serverId}-userList") { - override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean = false - } - - data object AddServerElement : NavigationElement("navigation-addServer") { - - override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { - return route == MeteroidScreen.AddServer - } - } - - data object SettingsElement : NavigationElement("navigation-settings") { - - override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { - return route == MeteroidScreen.Settings - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt deleted file mode 100644 index 2092a71aa639e57025d30dc9c5293e6cc2c1709a..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.navigation - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.material3.ListItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.ui.ServerAvatar -import de.chaosdorf.meteroid.util.humanReadableHost -import okhttp3.HttpUrl.Companion.toHttpUrl - -@Composable -fun NavigationServerItem(expanded: Boolean, server: Server, onExpand: () -> Unit) { - val host = humanReadableHost(server.url.toHttpUrl()) - - ListItem( - headlineContent = { Text(server.name ?: host, maxLines = 1, overflow = TextOverflow.Ellipsis) }, - supportingContent = { if (server.name != null) Text(host, maxLines = 1, overflow = TextOverflow.Ellipsis) }, - leadingContent = { - ServerAvatar(server.logoUrl) - }, - modifier = Modifier.requiredHeight(64.dp).let { - if (expanded) it - else it.clickable(onClick = onExpand) - } - ) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt deleted file mode 100644 index b42ba6160e862de008cb734b93d10334d5e57c0a..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.navigation - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.StarOutline -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.User -import de.chaosdorf.meteroid.ui.UserAvatar -import de.chaosdorf.meteroid.ui.home.PriceBadge - -@Composable -fun NavigationUserItem( - expanded: Boolean, - user: User, - pinned: Boolean, - onTogglePin: (ServerId, UserId) -> Unit, - onExpand: () -> Unit, - onClick: () -> Unit, -) { - ListItem( - headlineContent = { Text(user.name, maxLines = 1, overflow = TextOverflow.Ellipsis) }, - supportingContent = { if (user.email != null) Text(user.email!!, maxLines = 1, overflow = TextOverflow.Ellipsis) }, - leadingContent = { UserAvatar(user.gravatarUrl) }, - trailingContent = { - Row(verticalAlignment = Alignment.CenterVertically) { - AnimatedVisibility(!expanded, enter = fadeIn(), exit = fadeOut()) { - IconButton(onClick = { onTogglePin(user.serverId, user.userId) }) { - Icon( - if (pinned) Icons.Default.Star else Icons.Default.StarOutline, - contentDescription = null - ) - } - } - PriceBadge(user.balance) - } - }, - modifier = Modifier.requiredHeight(64.dp) - .clickable(onClick = if (expanded) onClick else onExpand) - ) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt deleted file mode 100644 index 9a89652dd3453ed5fdf2916c0568214cdbf12494..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.navigation - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Group -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.ui.AvatarLayout - -@Composable -fun NavigationUserListItem(onClick: () -> Unit) { - Column { - ListItem( - headlineContent = { Text("All Users") }, - leadingContent = { - AvatarLayout { - Icon(Icons.Default.Group, contentDescription = null) - } - }, - modifier = Modifier.requiredHeight(48.dp).clickable(onClick = onClick) - ) - HorizontalDivider() - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt deleted file mode 100644 index e94f2be97aefa1a7ef00bb8e0afb42a19f492b1e..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.navigation - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.* -import de.chaosdorf.meteroid.storage.AccountPreferences -import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.SyncManager -import de.chaosdorf.meteroid.sync.base.SyncHandler -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class NavigationViewModel @Inject constructor( - serverRepository: ServerRepository, - userRepository: UserRepository, - pinnedUserRepository: PinnedUserRepository, - syncManager: SyncManager, - private val accountProvider: AccountProvider, - private val preferences: AccountPreferences -) : ViewModel() { - val expanded = MutableStateFlow(false) - val account = MutableStateFlow<AccountPreferences.State?>(null) - - val syncState = syncManager.syncState - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SyncHandler.State.Idle) - - private val servers: StateFlow<List<Server>> = serverRepository.getAllFlow() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - private val pinnedUsers: StateFlow<List<User>> = pinnedUserRepository.getPinnedUsersFlow() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - private val currentUser: StateFlow<User?> = account.flatMapLatest { account -> - if (account?.server == null || account.user == null) flowOf(null) - else userRepository.getFlow(account.server, account.user) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - val historyDisabled = currentUser.map { - it?.audit == false - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) - - val entries: StateFlow<List<NavigationElement>> = combine( - servers, - pinnedUsers, - currentUser, - ) { servers, pinnedUsers, currentUser -> - val entries = mutableListOf<NavigationElement>() - val userInList = currentUser != null && pinnedUsers.any { - it.serverId == currentUser.serverId && it.userId == currentUser.userId - } - - for (server in servers) { - entries.add(NavigationElement.ServerElement(server)) - if (currentUser != null && currentUser.serverId == server.serverId && !userInList) { - entries.add(NavigationElement.UserElement(currentUser, pinned = false)) - } - for (user in pinnedUsers) { - if (user.serverId == server.serverId) { - entries.add(NavigationElement.UserElement(user, pinned = true)) - } - } - entries.add(NavigationElement.UserListElement(server)) - } - - entries.add(NavigationElement.AddServerElement) - entries.add(NavigationElement.SettingsElement) - - entries - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - init { - viewModelScope.launch { - account.collectLatest { account -> - val serverId = account?.server - val userId = account?.user - if (serverId != null && userId != null) { - val server = serverRepository.get(serverId) - val user = userRepository.get(serverId, userId) - if (server != null && user != null) { - syncManager.sync(server, user, incremental = true) - } - } - } - } - } - - fun togglePin(serverId: ServerId, userId: UserId) { - viewModelScope.launch { - accountProvider.togglePin(serverId, userId) - } - } - - fun selectUser(serverId: ServerId, userId: UserId) { - Log.i("UserListViewModel", "Updating AccountPreferences: $serverId $userId") - viewModelScope.launch { - preferences.setUser(serverId, userId) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt deleted file mode 100644 index 46489c8cbcdb76944d1ade062eb46b381ba3cf13..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.servers - -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import coil3.compose.AsyncImage -import kotlinx.coroutines.launch -import okhttp3.HttpUrl.Companion.toHttpUrl - -@Composable -fun AddServerScreen( - navController: NavController, - viewModel: AddServerViewModel, - contentPadding: PaddingValues = PaddingValues(), -) { - val scope = rememberCoroutineScope() - val url by viewModel.url.collectAsState() - val server by viewModel.server.collectAsState() - - Column( - Modifier - .padding(contentPadding) - .padding(16.dp, 8.dp) - ) { - TextField( - label = { Text("Server URL") }, - value = url, - onValueChange = { viewModel.url.value = it }, - modifier = Modifier.fillMaxWidth() - ) - - server?.let { server -> - Card( - modifier = Modifier.padding(vertical = 8.dp) - ) { - Row(modifier = Modifier.padding(16.dp, 8.dp)) { - AsyncImage( - server.logoUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(48.dp) - ) - Spacer(Modifier.width(16.dp)) - Column(modifier = Modifier.align(Alignment.CenterVertically)) { - Text( - server.name!!, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.bodyMedium - ) - Text( - server.url.toHttpUrl().host, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.bodyMedium - ) - } - - Spacer( - Modifier - .width(16.dp) - .weight(1.0f) - ) - - IconButton(onClick = { - scope.launch { - viewModel.addServer() - navController.navigateUp() - } - }) { - Icon(Icons.Default.Add, contentDescription = "Add Server") - } - } - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt deleted file mode 100644 index f903e304e573d6dcc7efd3d66ecdc569b96b7e9d..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.userlist - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.ListItem -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.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.User -import de.chaosdorf.meteroid.util.rememberAvatarPainter - -@Composable -fun UserListItem( - item: User, - onSelect: (ServerId, UserId) -> Unit = { _, _ -> } -) { - val avatarPainter = rememberAvatarPainter( - item.gravatarUrl, - 32.dp, 32.dp, - MaterialTheme.colorScheme.secondary - ) - - ListItem( - headlineContent = { Text(item.name) }, - supportingContent = { - item.email?.let { email -> - Text(email) - } - }, - leadingContent = { - Box( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.secondaryContainer) - ) { - Image( - avatarPainter, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.align(Alignment.Center) - ) - } - }, - modifier = Modifier.clickable { - onSelect(item.serverId, item.userId) - } - ) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt deleted file mode 100644 index a7e6156d8a2b3f83c85579e5dd6530617c19449d..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.userlist - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import de.chaosdorf.meteroid.ui.MeteroidScreen -import de.chaosdorf.meteroid.util.findStartDestination - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun UserListScreen( - navController: NavController, - viewModel: UserListViewModel, - contentPadding: PaddingValues = PaddingValues(), -) { - val users by viewModel.users.collectAsState() - val pinnedUsers by viewModel.pinnedUsers.collectAsState() - - LazyColumn(contentPadding = contentPadding) { - if (pinnedUsers.isNotEmpty()) { - stickyHeader("pinned") { - ListItem(headlineContent = { - Text( - "Pinned", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 4.dp) - ) - }) - } - - items( - pinnedUsers, - key = { "pinned-${it.serverId}-${it.userId}" }, - ) { user -> - UserListItem(user) { serverId, userId -> - navController.navigate(MeteroidScreen.Home.Purchase.build(serverId, userId)) { - launchSingleTop = true - restoreState = false - popUpTo(findStartDestination(navController.graph).id) { - saveState = false - } - } - viewModel.selectUser(serverId, userId) - } - } - } - - for (character in 'A'..'Z') { - val group = users.filter { it.name.startsWith(character, ignoreCase = true) } - if (group.isNotEmpty()) { - stickyHeader(character.toString()) { - ListItem(headlineContent = { - Text( - "$character", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 4.dp) - ) - }) - } - - items( - group, - key = { "user-${it.serverId}-${it.userId}" }, - ) { user -> - UserListItem(user) { serverId, userId -> - navController.navigate(MeteroidScreen.Home.Purchase.build(serverId, userId)) { - launchSingleTop = true - restoreState = false - popUpTo(findStartDestination(navController.graph).id) { - saveState = false - } - } - viewModel.selectUser(serverId, userId) - } - } - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt deleted file mode 100644 index e50d80dd4258292317e8ba0614a2be15906b339f..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.userlist - -import android.util.Log -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.PinnedUserRepository -import de.chaosdorf.meteroid.model.User -import de.chaosdorf.meteroid.model.UserRepository -import de.chaosdorf.meteroid.storage.AccountPreferences -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class UserListViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - userRepository: UserRepository, - pinnedUserRepository: PinnedUserRepository, - private val preferences: AccountPreferences, -) : ViewModel() { - private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) - - val users: StateFlow<List<User>> = userRepository.getAllFlow(serverId) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - val pinnedUsers = combine( - userRepository.getAllFlow(serverId), - pinnedUserRepository.getAllFlow(serverId) - ) { users, pinned -> - users.filter { user -> pinned.contains(user.userId) } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - fun selectUser(serverId: ServerId, userId: UserId) { - Log.i("UserListViewModel", "Updating AccountPreferences: $serverId $userId") - viewModelScope.launch { - preferences.setUser(serverId, userId) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/InitViewModel.kt similarity index 66% rename from app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/InitViewModel.kt index 96447ecd0d6a17823c30ffbdb625f61ddb727d00..2885bc48e49255b490324d6c74fb6d8bc7371a6c 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/InitViewModel.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,28 +22,36 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid +package de.chaosdorf.meteroid.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.meteroid.model.ServerRepository -import de.chaosdorf.meteroid.storage.AccountPreferences import de.chaosdorf.meteroid.sync.SyncManager -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class MeteroidViewModel @Inject constructor( - accountPreferences: AccountPreferences, +@AssistedFactory +interface InitViewModelFactory { + fun create(): InitViewModel +} + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltViewModel(assistedFactory = InitViewModelFactory::class) +class InitViewModel @AssistedInject constructor( serverRepository: ServerRepository, - syncManager: SyncManager + syncManager: SyncManager, ) : ViewModel() { - val initialAccount: StateFlow<AccountPreferences.State?> = accountPreferences.state - .filterNotNull() - .take(1) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) + val setupComplete = serverRepository.getAllFlow() + .mapLatest { it.isNotEmpty() } + .stateIn(viewModelScope, WhileSubscribed(), null) init { viewModelScope.launch { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/ServerListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/ServerListViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..c42af5e3aca698448b245d0ffd6877e9454bef6e --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/ServerListViewModel.kt @@ -0,0 +1,47 @@ +/* + * 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.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.meteroid.model.ServerRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn + +@AssistedFactory +interface ServerListViewModelFactory { + fun create(): ServerListViewModel +} + +@HiltViewModel(assistedFactory = ServerListViewModelFactory::class) +class ServerListViewModel @AssistedInject constructor( + serverRepository: ServerRepository, +) : ViewModel() { + val servers = serverRepository.getAllFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SettingsViewModel.kt similarity index 71% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SettingsViewModel.kt index 35aa5a251c13ed8f7f1a279f723f1e305c10fa59..08110831a6d424b5f9801a7d8b6998b7488d8248 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SettingsViewModel.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,11 +22,20 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.settings +package de.chaosdorf.meteroid.viewmodel import androidx.lifecycle.ViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi -@HiltViewModel -class SettingsViewModel @Inject constructor() : ViewModel() +@AssistedFactory +interface SettingsViewModelFactory { + fun create(): SettingsViewModel +} + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltViewModel(assistedFactory = SettingsViewModelFactory::class) +class SettingsViewModel @AssistedInject constructor() : ViewModel() { +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SetupViewModel.kt similarity index 54% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SetupViewModel.kt index 8c1ee10134223eea50d93c2b5c25433bb857215a..51bf79bc000900ee9cd02173bd08cf4b7db22145 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SetupViewModel.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,38 +22,46 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.servers +package de.chaosdorf.meteroid.viewmodel import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.mete.model.MeteApiFactory import de.chaosdorf.mete.model.ServerId import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.sync.SyncManager import de.chaosdorf.meteroid.util.newServer -import kotlinx.coroutines.flow.* -import javax.inject.Inject -import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.MutableStateFlow -@HiltViewModel -class AddServerViewModel @Inject constructor( - private val factory: MeteApiFactory, - private val repository: ServerRepository -) : ViewModel() { - val url = MutableStateFlow("") +@AssistedFactory +interface SetupViewModelFactory { + fun create(): SetupViewModel +} - val server: StateFlow<Server?> = url - .debounce(300.milliseconds) - .mapLatest { factory.newServer(ServerId(-1), it) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) +@HiltViewModel(assistedFactory = SetupViewModelFactory::class) +class SetupViewModel @AssistedInject constructor( + private val apiFactory: MeteApiFactory, + private val serverRepository: ServerRepository, + private val syncManager: SyncManager, +) : ViewModel() { + val serverUrl = MutableStateFlow("http://192.168.1.41:8080") + val error = MutableStateFlow<String?>(null) - suspend fun addServer() { - val highestServerId = repository.getAll().maxOfOrNull { it.serverId.value } ?: 0 - val serverId = ServerId(highestServerId + 1) - val server = factory.newServer(serverId, url.value) - if (server != null) { - repository.save(server) - } + suspend fun add(): Server? { + val highestServerId = serverRepository.getAll().maxOfOrNull { it.serverId.value } ?: 0 + val serverId = ServerId(highestServerId + 1) + val server = apiFactory.newServer(serverId, serverUrl.value) + if (server != null) { + serverRepository.save(server) + syncManager.sync(server, null, incremental = false) + error.value = null + return server + } else { + error.value = "Server not found" + return null + } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/UserListViewModel.kt similarity index 56% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/UserListViewModel.kt index 82ad5ee33f2b712ac83e63d2336e30d70b6dcb92..42bb4c4af67061a2afb14c32d2a7cd63a77df459 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/UserListViewModel.kt @@ -1,7 +1,7 @@ /* * 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 * of this software and associated documentation files (the "Software"), to deal @@ -22,38 +22,34 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.home.deposit +package de.chaosdorf.meteroid.viewmodel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.mete.model.ServerId -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.SyncManager -import kotlinx.coroutines.launch -import javax.inject.Inject +import de.chaosdorf.meteroid.model.UserRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn -@HiltViewModel -class DepositViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val accountProvider: AccountProvider, - private val syncManager: SyncManager, -) : ViewModel() { - private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) - private val userId = UserId(checkNotNull(savedStateHandle["user"])) - val money: List<MonetaryAmount> = MonetaryAmount.entries +@AssistedFactory +interface UserListViewModelFactory { + fun create( + @Assisted("serverId") serverId: Long, + ): UserListViewModel +} + +@HiltViewModel(assistedFactory = UserListViewModelFactory::class) +class UserListViewModel @AssistedInject constructor( + @Assisted("serverId") serverId: Long, + userRepository: UserRepository, +) : ViewModel() { + val serverId: ServerId = ServerId(serverId) - fun deposit(item: MonetaryAmount, onBack: () -> Unit) { - viewModelScope.launch { - accountProvider.account(serverId, userId)?.let { account -> - syncManager.deposit(account, item.amount) - if (!account.pinned) { - onBack() - } - } - } - } + val users = userRepository.getAllFlow(this.serverId) + .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/UserViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/UserViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..b813133f28798974deeca14f62fa47d0537284cc --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/UserViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId + +@AssistedFactory +interface UserViewModelFactory { + fun create( + @Assisted("serverId") serverId: Long, + @Assisted("userId") userId: Long, + ): UserViewModel +} + +@HiltViewModel(assistedFactory = UserViewModelFactory::class) +class UserViewModel @AssistedInject constructor( + @Assisted("serverId") serverId: Long, + @Assisted("userId") userId: Long, +) : ViewModel() { + val serverId: ServerId = ServerId(serverId) + val userId: UserId = UserId(userId) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/WrappedViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/WrappedViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..b1e18fa4a8ffd303b87e4fa125ffeef633656486 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/WrappedViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId + +@AssistedFactory +interface WrappedViewModelFactory { + fun create( + @Assisted("serverId") serverId: Long, + @Assisted("userId") userId: Long, + ): WrappedViewModel +} + +@HiltViewModel(assistedFactory = WrappedViewModelFactory::class) +class WrappedViewModel @AssistedInject constructor( + @Assisted("serverId") serverId: Long, + @Assisted("userId") userId: Long, +) : ViewModel() { + val serverId: ServerId = ServerId(serverId) + val userId: UserId = UserId(userId) +} diff --git a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt index 3f2dd4766555d09763af9539e3f5bb4196bb4c77..713956bb8537da70928368d8f166485b507d1700 100644 --- a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt +++ b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt @@ -16,11 +16,11 @@ class AndroidApplicationConvention : Plugin<Project> { } extensions.configure<ApplicationExtension> { - compileSdk = 35 + compileSdk = 36 defaultConfig { minSdk = 21 - targetSdk = 35 + targetSdk = 36 applicationId = "${rootProject.group}.${rootProject.name.lowercase(Locale.ROOT)}" versionCode = cmd("git", "rev-list", "--count", "HEAD")?.toIntOrNull() ?: 1 diff --git a/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt index 0dd138372be5df5199f69d2e499218006ced97d8..8086c78cecbbf99cedda4146effebb821398ab6e 100644 --- a/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt +++ b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt @@ -12,7 +12,7 @@ class AndroidLibraryConvention : Plugin<Project> { } extensions.configure<LibraryExtension> { - compileSdk = 35 + compileSdk = 36 defaultConfig { minSdk = 21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a32d359245708d94754ac60569a3a0586be32a50..b82e2a669f1651ca76da16927cbc3c8c7384c53f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,8 @@ androidx-appcompat = "1.7.1" androidx-compose = "1.8.2" androidx-compose-compiler = "1.5.15" androidx-datastore = "1.1.7" -androidx-navigation = "2.9.0" +androidx-navigation3 = "1.0.0-alpha03" +androidx-navigation3-viewmodel = "1.0.0-alpha01" androidx-room = "2.7.1" androidx-splashscreen = "1.0.1" androidx-material3 = "1.3.2" @@ -62,7 +63,9 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converter-kotlinx = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } -androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } +androidx-navigation3-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-navigation3-viewmodel" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }