From 6786bd40a05502808dd7a5acaf504f6a1a23a8e6 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <janne@kuschku.de> Date: Sun, 26 Nov 2023 00:19:39 +0100 Subject: [PATCH] chore: navigation cleanup part 1 --- .../de/chaosdorf/meteroid/MainActivity.kt | 2 +- .../meteroid/sync/AccountProvider.kt | 38 +--- .../de/chaosdorf/meteroid/sync/SyncManager.kt | 68 +++---- .../de/chaosdorf/meteroid/ui/AppRouter.kt | 183 +++++++++--------- .../de/chaosdorf/meteroid/ui/AppViewModel.kt | 59 +++--- .../meteroid/ui/NavigationViewModel.kt | 88 +++++++++ .../meteroid/ui/drinks/DrinkListScreen.kt | 38 +--- .../meteroid/ui/drinks/DrinkListViewModel.kt | 36 ++-- .../meteroid/ui/money/MoneyListScreen.kt | 38 +--- .../meteroid/ui/money/MoneyListViewModel.kt | 21 +- .../ui/navigation/MeteroidBottomBar.kt | 59 ++++-- .../ui/navigation/MeteroidNavSection.kt | 8 + .../meteroid/ui/navigation/MeteroidTopBar.kt | 87 +++++---- .../meteroid/ui/navigation/Routes.kt | 39 ++-- .../meteroid/ui/servers/AddServerScreen.kt | 28 +-- .../meteroid/ui/servers/ServerListScreen.kt | 58 ++---- .../ui/servers/ServerListViewModel.kt | 12 +- .../ui/transactions/PurchaseListScreen.kt | 23 +-- .../ui/transactions/PurchaseViewModel.kt | 76 +++----- .../meteroid/ui/users/UserListScreen.kt | 89 ++------- .../meteroid/ui/users/UserListViewModel.kt | 48 +++-- .../meteroid/ui/wrapped/WrappedScreen.kt | 23 +-- .../meteroid/ui/wrapped/WrappedViewModel.kt | 62 +++--- .../chaosdorf/meteroid/model/AccountInfo.kt | 2 +- .../de/chaosdorf/meteroid/model/Server.kt | 2 + 25 files changed, 529 insertions(+), 658 deletions(-) create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index 9bfc7b9..81481f3 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -44,7 +44,7 @@ class MainActivity : ComponentActivity() { val viewModel = viewModelProvider.get<AppViewModel>() installSplashScreen().setKeepOnScreenCondition { - viewModel.initState.value == AppViewModel.InitState.LOADING + viewModel.initialRoute.value == null } setContent { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt index 66c5b73..c445d74 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt @@ -31,40 +31,22 @@ import de.chaosdorf.meteroid.model.PinnedUserRepository import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.model.UserRepository -import de.chaosdorf.meteroid.storage.AccountPreferences -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import javax.inject.Inject class AccountProvider @Inject constructor( - accountPreferences: AccountPreferences, - serverRepository: ServerRepository, - userRepository: UserRepository, + private val serverRepository: ServerRepository, + private val userRepository: UserRepository, private val pinnedUserRepository: PinnedUserRepository, ) { - val account: Flow<AccountInfo?> = - accountPreferences.state.flatMapLatest { preferences -> - if (preferences.server == null) { - flowOf(null) - } else { - serverRepository.getFlow(preferences.server).flatMapLatest { server -> - if (server == null) { - flowOf(null) - } else if (preferences.user == null) { - flowOf(AccountInfo(server, null, false)) - } else { - combine( - userRepository.getFlow(server.serverId, preferences.user), - pinnedUserRepository.isPinnedFlow(server.serverId, preferences.user) - ) { user, pinned -> - AccountInfo(server, user, pinned) - } - } - } - } - } + fun accountFlow(serverId: ServerId, userId: UserId) = combine( + serverRepository.getFlow(serverId), + userRepository.getFlow(serverId, userId), + pinnedUserRepository.isPinnedFlow(serverId, userId) + ) { server, user, pinned -> + if (server == null || user == null) null + else AccountInfo(server, user, pinned) + } suspend fun togglePin(serverId: ServerId, userId: UserId) { if (pinnedUserRepository.isPinned(serverId, userId)) { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt index 5e9a4e3..e548fc1 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt @@ -32,8 +32,6 @@ import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.util.newServer -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import java.math.BigDecimal import javax.inject.Inject @@ -75,50 +73,44 @@ class SyncManager @Inject constructor( } suspend fun purchase(account: AccountInfo, drink: Drink) { - account.user?.let { user -> - val api = factory.newInstance(account.server.url) - try { - api.purchase(user.userId, drink.drinkId) - sync(account.server, user, incremental = true) - } catch (e: Exception) { - Log.e( - "Sync", - "Could not finish transaction for ${user.name} (${user.userId}) on ${account.server.url}", - e - ) - } + val api = factory.newInstance(account.server.url) + try { + api.purchase(account.user.userId, drink.drinkId) + sync(account.server, account.user, incremental = true) + } catch (e: Exception) { + Log.e( + "Sync", + "Could not finish transaction for ${account.user.name} (${account.user.userId}) on ${account.server.url}", + e + ) } } suspend fun deposit(account: AccountInfo, amount: BigDecimal) { - account.user?.let { user -> - try { - val api = factory.newInstance(account.server.url) - api.deposit(user.userId, amount) - sync(account.server, user, incremental = true) - } catch (e: Exception) { - Log.e( - "Sync", - "Could not finish transaction for ${user.name} (${user.userId}) on ${account.server.url}", - e - ) - } + try { + val api = factory.newInstance(account.server.url) + api.deposit(account.user.userId, amount) + sync(account.server, account.user, incremental = true) + } catch (e: Exception) { + Log.e( + "Sync", + "Could not finish transaction for ${account.user.name} (${account.user.userId}) on ${account.server.url}", + e + ) } } suspend fun withdraw(account: AccountInfo, amount: BigDecimal) { - account.user?.let { user -> - val api = factory.newInstance(account.server.url) - try { - api.withdraw(user.userId, amount) - sync(account.server, user, incremental = true) - } catch (e: Exception) { - Log.e( - "Sync", - "Could not finish transaction for ${user.name} (${user.userId}) on ${account.server.url}", - e - ) - } + val api = factory.newInstance(account.server.url) + try { + api.withdraw(account.user.userId, amount) + sync(account.server, account.user, incremental = true) + } catch (e: Exception) { + Log.e( + "Sync", + "Could not finish transaction for ${account.user.name} (${account.user.userId}) on ${account.server.url}", + e + ) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt index b450fb4..b00553b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt @@ -24,133 +24,126 @@ package de.chaosdorf.meteroid.ui -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavOptions +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel import de.chaosdorf.meteroid.ui.money.MoneyListScreen import de.chaosdorf.meteroid.ui.money.MoneyListViewModel +import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar +import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar import de.chaosdorf.meteroid.ui.navigation.Routes import de.chaosdorf.meteroid.ui.servers.AddServerScreen +import de.chaosdorf.meteroid.ui.servers.AddServerViewModel import de.chaosdorf.meteroid.ui.servers.ServerListScreen +import de.chaosdorf.meteroid.ui.servers.ServerListViewModel import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel import de.chaosdorf.meteroid.ui.users.UserListScreen +import de.chaosdorf.meteroid.ui.users.UserListViewModel import de.chaosdorf.meteroid.ui.wrapped.WrappedScreen import de.chaosdorf.meteroid.ui.wrapped.WrappedViewModel -import kotlinx.coroutines.launch @Composable fun AppRouter(viewModel: AppViewModel) { - val scope = rememberCoroutineScope() val navController = rememberNavController() - val initState by viewModel.initState.collectAsState() + val initialRoute by viewModel.initialRoute.collectAsState() - LaunchedEffect(initState) { - when (initState) { - AppViewModel.InitState.CREATE_SERVER -> navController.navigate( - Routes.Servers.Add, - NavOptions.Builder().setPopUpTo(Routes.Servers.Add, true).build() - ) + LaunchedEffect(initialRoute) { + initialRoute?.let { + navController.navigate(it) + } + } - AppViewModel.InitState.SELECT_SERVER -> navController.navigate( - Routes.Servers.List, - NavOptions.Builder().setPopUpTo(Routes.Servers.List, true).build() + NavHost(navController, startDestination = Routes.Servers.List) { + composable(Routes.Servers.Add) { _ -> + val hiltViewModel = hiltViewModel<AddServerViewModel>() + AddServerScreen(navController, hiltViewModel) + } + composable(Routes.Servers.List) { _ -> + val hiltViewModel = hiltViewModel<ServerListViewModel>() + val navigationViewModel = hiltViewModel<NavigationViewModel>() + val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) } + ServerListScreen(navController, hiltViewModel, topBar) + } + /* + composable(Routes.Users.Add) { _ -> + AddUserScreen( + hiltViewModel(), + onAdd = { navController.navigate(Routes.Users.List) } ) - - AppViewModel.InitState.SELECT_USER -> navController.navigate( - Routes.Users.List, - NavOptions.Builder().setPopUpTo(Routes.Users.List, true).build() + } + */ + composable( + Routes.Users.List, + arguments = listOf( + navArgument("server") { type = NavType.LongType } ) - - AppViewModel.InitState.HOME -> navController.navigate( - Routes.Home.Root, - NavOptions.Builder().setPopUpTo(Routes.Home.Root, true).build() + ) { _ -> + val hiltViewModel = hiltViewModel<UserListViewModel>() + val navigationViewModel = hiltViewModel<NavigationViewModel>() + val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) } + UserListScreen(navController, hiltViewModel, topBar) + } + composable( + Routes.Home.Purchase, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, ) - else -> Unit + ) { _ -> + val hiltViewModel = hiltViewModel<DrinkListViewModel>() + val navigationViewModel = hiltViewModel<NavigationViewModel>() + val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) } + val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) } + DrinkListScreen(navController, hiltViewModel, topBar, bottomBar) } - } - - NavHost(navController, startDestination = Routes.Servers.Root) { - navigation(route = Routes.Servers.Root, startDestination = Routes.Servers.List) { - composable(Routes.Servers.List) { _ -> - ServerListScreen( - hiltViewModel(), - onAdd = { navController.navigate(Routes.Servers.Add) }, - onSelect = { - scope.launch { - viewModel.selectServer(it) - navController.navigate(Routes.Users.List) - } - } - ) - } - composable(Routes.Servers.Add) { _ -> - AddServerScreen( - hiltViewModel(), - isFirst = initState == AppViewModel.InitState.CREATE_SERVER, - onBack = { navController.navigate(Routes.Servers.List) }, - onAdd = { navController.navigate(Routes.Servers.List) } - ) - } + composable( + Routes.Home.Deposit, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, + ) + ) { _ -> + val hiltViewModel = hiltViewModel<MoneyListViewModel>() + val navigationViewModel = hiltViewModel<NavigationViewModel>() + val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) } + val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) } + MoneyListScreen(navController, hiltViewModel, topBar, bottomBar) } - navigation(route = Routes.Users.Root, startDestination = Routes.Users.List) { - composable(Routes.Users.List) { _ -> - UserListScreen( - hiltViewModel(), - onAdd = { TODO() }, - onSelect = { - scope.launch { - viewModel.selectUser(it) - navController.navigate(Routes.Home.Root) - } - }, - onBack = { navController.navigate(Routes.Servers.Root) }, - ) - } - /* - composable(Routes.Users.Add) { _ -> - AddUserScreen( - hiltViewModel(), - onAdd = { navController.navigate(Routes.Users.List) } - ) - } - */ + composable( + Routes.Home.History, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, + ) + ) { _ -> + val hiltViewModel = hiltViewModel<TransactionViewModel>() + val navigationViewModel = hiltViewModel<NavigationViewModel>() + val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) } + val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) } + TransactionListScreen(hiltViewModel, topBar, bottomBar) } - navigation(route = Routes.Home.Root, startDestination = Routes.Home.Purchase) { - composable(Routes.Home.Purchase) { _ -> - val drinkListViewModel = hiltViewModel<DrinkListViewModel>() - DrinkListScreen(drinkListViewModel, navController::navigate) - } - composable(Routes.Home.Deposit) { _ -> - val moneyListViewModel = hiltViewModel<MoneyListViewModel>() - MoneyListScreen(moneyListViewModel, navController::navigate) - } - composable(Routes.Home.History) { _ -> - val transactionViewModel = hiltViewModel<TransactionViewModel>() - TransactionListScreen(transactionViewModel, navController::navigate) - } - composable(Routes.Home.Wrapped) { _ -> - val wrappedViewModel = hiltViewModel<WrappedViewModel>() - WrappedScreen(wrappedViewModel, navController::navigate) - } + composable( + Routes.Home.Wrapped, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, + ) + ) { _ -> + val hiltViewModel = hiltViewModel<WrappedViewModel>() + val navigationViewModel = hiltViewModel<NavigationViewModel>() + val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) } + val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) } + WrappedScreen(hiltViewModel, topBar, bottomBar) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt index 8e0cf09..b465366 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt @@ -28,48 +28,43 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.model.UserRepository import de.chaosdorf.meteroid.storage.AccountPreferences -import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.SyncManager +import de.chaosdorf.meteroid.ui.navigation.Routes import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel class AppViewModel @Inject constructor( - accountProvider: AccountProvider, private val accountPreferences: AccountPreferences, private val serverRepository: ServerRepository, - private val syncManager: SyncManager + private val userRepository: UserRepository ) : ViewModel() { - val initState: StateFlow<InitState> = accountPreferences.state - .flatMapLatest { preferences -> - serverRepository.getAllFlow() - .mapLatest { it.map(Server::serverId) } - .mapLatest { servers -> - if (servers.isEmpty()) InitState.CREATE_SERVER - else if (!servers.contains(preferences.server)) InitState.SELECT_SERVER - else if (preferences.user == null) InitState.SELECT_USER - else InitState.HOME - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), InitState.LOADING) - - init { - accountProvider.account.distinctUntilChanged().onEach { account -> - account?.let { (server, maybeUser) -> - syncManager.sync(server, maybeUser, incremental = true) + val initialRoute = accountPreferences.state.flatMapLatest { + combine( + serverRepository.countFlow(), + if (it.server == null) flowOf(null) + else serverRepository.getFlow(it.server), + if (it.server == null || it.user == null) flowOf(null) + else userRepository.getFlow(it.server, it.user) + ) { serverCount, server, user -> + if (user != null) { + Routes.Home.purchase(user.serverId, user.userId) + } else if (server != null) { + Routes.Users.list(server.serverId) + } else if (serverCount > 0) { + Routes.Servers.List + } else { + Routes.Servers.Add } - }.launchIn(viewModelScope) - } + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) suspend fun selectServer(server: ServerId) { accountPreferences.setServer(server) @@ -79,12 +74,4 @@ class AppViewModel @Inject constructor( suspend fun selectUser(user: UserId) { accountPreferences.setUser(user) } - - enum class InitState { - LOADING, - CREATE_SERVER, - SELECT_SERVER, - SELECT_USER, - HOME - } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt new file mode 100644 index 0000000..77537ef --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt @@ -0,0 +1,88 @@ +/* + * 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.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.meteroid.model.PinnedUserRepository +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.model.UserRepository +import de.chaosdorf.meteroid.sync.AccountProvider +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NavigationViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + serverRepository: ServerRepository, + userRepository: UserRepository, + pinnedUserRepository: PinnedUserRepository, + private val accountProvider: AccountProvider +) : ViewModel() { + private val serverId = savedStateHandle.getStateFlow<Long?>("server", null) + .mapLatest { it?.let(::ServerId) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + private val userId = savedStateHandle.getStateFlow<Long?>("user", null) + .mapLatest { it?.let(::UserId) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + val server = serverId.flatMapLatest { + if (it == null) flowOf(null) + else serverRepository.getFlow(it) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + val user = combine(serverId, userId) { serverId, userId -> + if (serverId == null || userId == null) null + else Pair(serverId, userId) + }.flatMapLatest { + if (it == null) flowOf(null) + else userRepository.getFlow(it.first, it.second) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + val pinned = combine(serverId, userId) { serverId, userId -> + if (serverId == null || userId == null) null + else Pair(serverId, userId) + }.flatMapLatest { + if (it == null) flowOf(null) + else pinnedUserRepository.isPinnedFlow(it.first, it.second) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + fun togglePin(serverId: ServerId, userId: UserId) { + viewModelScope.launch { + accountProvider.togglePin(serverId, userId) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt index 7333204..d454baf 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt @@ -44,30 +44,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation.NavOptions -import de.chaosdorf.meteroid.ui.navigation.HomeSections -import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar -import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar -import de.chaosdorf.meteroid.ui.navigation.Routes -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.time.Month +import androidx.navigation.NavController @OptIn(ExperimentalLayoutApi::class) @Composable fun DrinkListScreen( + navController: NavController, viewModel: DrinkListViewModel, - onNavigate: (String, NavOptions) -> Unit + topBar: @Composable () -> Unit, + bottomBar: @Composable () -> Unit, ) { - val onBack = remember { - { - onNavigate( - Routes.Users.List, - NavOptions.Builder().setPopUpTo(Routes.Users.List, false).build() - ) - } - } val account by viewModel.account.collectAsState() val drinks by viewModel.drinks.collectAsState() val filters by viewModel.filters.collectAsState() @@ -85,16 +71,8 @@ fun DrinkListScreen( } Scaffold( - topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) }, - bottomBar = { - MeteroidBottomBar( - currentRoute = HomeSections.PURCHASE, - historyEnabled = account?.user?.audit == true, - wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) - .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, - navigateTo = onNavigate - ) - }, + topBar = topBar, + bottomBar = bottomBar, snackbarHost = { SnackbarHost(hostState = snackbarHostState) } @@ -120,7 +98,9 @@ fun DrinkListScreen( modifier = Modifier.padding(horizontal = 8.dp) ) { items(drinks) { drink -> - DrinkTile(drink) { viewModel.purchase(it, onBack) } + DrinkTile(drink) { + viewModel.purchase(it, navController::navigateUp) + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt index 52c751e..470a91e 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt @@ -28,41 +28,39 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.meteroid.model.AccountInfo +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId 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.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DrinkListViewModel @Inject constructor( - private val accountProvider: AccountProvider, - repository: DrinkRepository, + private val savedStateHandle: SavedStateHandle, + accountProvider: AccountProvider, private val syncManager: SyncManager, - private val savedStateHandle: SavedStateHandle + drinkRepository: DrinkRepository, ) : ViewModel() { - val account: StateFlow<AccountInfo?> = accountProvider.account + private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) + private val userId = UserId(checkNotNull(savedStateHandle["user"])) + + val account = accountProvider.accountFlow(serverId, userId) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) val filters: StateFlow<Set<Filter>> = savedStateHandle.getStateFlow("filters", setOf(Filter.Active)) val drinks: StateFlow<List<Drink>> = combine( - accountProvider.account.flatMapLatest { account -> - account?.let { (server, _) -> - repository.getAllFlow(server.serverId) - } ?: flowOf(emptyList()) - }, + drinkRepository.getAllFlow(serverId), filters ) { drinks, filters -> drinks.filter { item -> @@ -88,20 +86,10 @@ class DrinkListViewModel @Inject constructor( } } - fun togglePin() { - account.value?.let { account -> - account.user?.let { user -> - viewModelScope.launch { - accountProvider.togglePin(account.server.serverId, user.userId) - } - } - } - } - fun sync() { - account.value?.let { (server, user) -> + account.value?.let { account -> viewModelScope.launch { - syncManager.sync(server, user, incremental = true) + syncManager.sync(account.server, account.user, incremental = true) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt index c6e9dfc..b7df727 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt @@ -42,29 +42,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation.NavOptions -import de.chaosdorf.meteroid.ui.navigation.HomeSections -import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar -import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar -import de.chaosdorf.meteroid.ui.navigation.Routes -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.time.Month +import androidx.navigation.NavController @Composable fun MoneyListScreen( + navController: NavController, viewModel: MoneyListViewModel, - onNavigate: (String, NavOptions) -> Unit + topBar: @Composable () -> Unit, + bottomBar: @Composable () -> Unit, ) { - val onBack = remember { - { - onNavigate( - Routes.Users.List, - NavOptions.Builder().setPopUpTo(Routes.Users.List, false).build() - ) - } - } val account by viewModel.account.collectAsState() val snackbarHostState = remember { SnackbarHostState() } @@ -80,16 +66,8 @@ fun MoneyListScreen( } Scaffold( - topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) }, - bottomBar = { - MeteroidBottomBar( - currentRoute = HomeSections.DEPOSIT, - historyEnabled = account?.user?.audit == true, - wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) - .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, - navigateTo = onNavigate - ) - }, + topBar = topBar, + bottomBar = bottomBar, snackbarHost = { SnackbarHost(hostState = snackbarHostState) } @@ -102,7 +80,9 @@ fun MoneyListScreen( horizontalArrangement = Arrangement.SpaceBetween, ) { items(viewModel.money) { monetaryAmount -> - MoneyTile(monetaryAmount) { viewModel.deposit(it, onBack) } + MoneyTile(monetaryAmount) { + viewModel.deposit(it, navController::navigateUp) + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt index eb94fb6..cd405b2 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt @@ -25,16 +25,17 @@ package de.chaosdorf.meteroid.ui.money import androidx.annotation.DrawableRes +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.R -import de.chaosdorf.meteroid.model.AccountInfo import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.sync.AccountProvider import de.chaosdorf.meteroid.sync.SyncManager import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.math.BigDecimal @@ -52,10 +53,14 @@ enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) { @HiltViewModel class MoneyListViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, private val accountProvider: AccountProvider, - private val syncManager: SyncManager + private val syncManager: SyncManager, ) : ViewModel() { - val account: StateFlow<AccountInfo?> = accountProvider.account + private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) + private val userId = UserId(checkNotNull(savedStateHandle["user"])) + + val account = accountProvider.accountFlow(serverId, userId) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) val money: List<MonetaryAmount> = MonetaryAmount.entries @@ -72,12 +77,8 @@ class MoneyListViewModel @Inject constructor( } fun togglePin() { - account.value?.let { account -> - account.user?.let { user -> - viewModelScope.launch { - accountProvider.togglePin(account.server.serverId, user.userId) - } - } + viewModelScope.launch { + accountProvider.togglePin(serverId, userId) } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt index 760314f..9710b66 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt @@ -29,38 +29,63 @@ import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavOptions +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.currentBackStackEntryAsState +import de.chaosdorf.meteroid.ui.NavigationViewModel +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.time.Month @Composable -fun <T : MeteroidNavSection> MeteroidBottomBar( - currentRoute: T, - navigateTo: (String, NavOptions) -> Unit, - historyEnabled: Boolean, - wrappedEnabled: Boolean, - modifier: Modifier = Modifier +fun MeteroidBottomBar( + navController: NavController, + viewModel: NavigationViewModel ) { + val user by viewModel.user.collectAsState() + val historyEnabled = user?.audit == true + val wrappedEnabled = Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .month.let { it == Month.NOVEMBER || it == Month.DECEMBER } + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val activeRoute = if (user == null) HomeSections.PURCHASE + else HomeSections.entries.find { + it.withArguments( + "server" to user!!.serverId.value.toString(), + "user" to user!!.userId.value.toString() + ) == currentDestination?.route + } + NavigationBar { for (route in HomeSections.entries) { if (wrappedEnabled || route != HomeSections.WRAPPED) { NavigationBarItem( icon = { Icon( - if (route == currentRoute) route.iconActive else route.icon, + if (route == activeRoute) route.iconActive else route.icon, contentDescription = route.title ) }, label = { Text(route.title) }, - selected = route == currentRoute, + selected = route == activeRoute, onClick = { - navigateTo( - route.route, - NavOptions.Builder() - .setPopUpTo(Routes.Home.Root, true) - .build() - ) + navController.navigate( + route.withArguments( + "server" to user!!.serverId.value.toString(), + "user" to user!!.userId.value.toString() + ) + ) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } }, - modifier = modifier, enabled = route != HomeSections.HISTORY || historyEnabled ) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt index d56b347..218a3d8 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt @@ -31,4 +31,12 @@ interface MeteroidNavSection { val icon: ImageVector val iconActive: ImageVector val route: String + + fun withArguments(vararg args: Pair<String, String>): String { + var result = route + for ((parameter, value) in args) { + result = result.replace("{$parameter}", value) + } + return result + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt index f0b6bc2..41abc77 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt @@ -42,6 +42,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -49,31 +51,32 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.navigation.NavOptions +import androidx.navigation.NavController import coil.compose.AsyncImage -import de.chaosdorf.meteroid.model.AccountInfo import de.chaosdorf.meteroid.ui.PriceBadge +import de.chaosdorf.meteroid.ui.NavigationViewModel import okhttp3.HttpUrl.Companion.toHttpUrl @Composable fun MeteroidTopBar( - account: AccountInfo?, - onNavigate: (String, NavOptions) -> Unit, - onTogglePin: () -> Unit + navController: NavController, + viewModel: NavigationViewModel ) { + val server by viewModel.server.collectAsState() + val user by viewModel.user.collectAsState() + val pinned by viewModel.pinned.collectAsState() + Surface( modifier = Modifier.padding(8.dp), color = MaterialTheme.colorScheme.surface, shadowElevation = 6.dp, tonalElevation = 6.dp, shape = RoundedCornerShape(32.dp), - onClick = { - onNavigate(Routes.Users.List, NavOptions.Builder().build()) - } + onClick = { navController.navigateUp() } ) { Row(modifier = Modifier.padding(8.dp)) { AsyncImage( - account?.user?.gravatarUrl, + user?.gravatarUrl, contentDescription = "User List", contentScale = ContentScale.Crop, modifier = Modifier @@ -87,48 +90,48 @@ fun MeteroidTopBar( .align(Alignment.CenterVertically) .weight(1.0f, fill = true) ) { - if (account != null) { - if (account.user != null) { - Text( - account.user!!.name, - fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - Text( - account.server.url.toHttpUrl().host, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), - fontWeight = FontWeight.Medium, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - } else { - Text( - account.server.url.toHttpUrl().host, - fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - } - } else { + if (server == null) { Text( "Meteroid", fontWeight = FontWeight.SemiBold, overflow = TextOverflow.Ellipsis, softWrap = false ) + } else if (user == null) { + Text( + server!!.url.toHttpUrl().host, + fontWeight = FontWeight.SemiBold, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) + } else { + Text( + user!!.name, + fontWeight = FontWeight.SemiBold, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) + Text( + server!!.url.toHttpUrl().host, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), + fontWeight = FontWeight.Medium, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) } } Spacer(Modifier.width(16.dp)) - IconButton(onClick = onTogglePin) { - Icon( - if (account?.pinned == true) Icons.Filled.PushPin - else Icons.Outlined.PushPin, - contentDescription = null - ) - } - Spacer(Modifier.width(8.dp)) - account?.user?.let { user -> + user?.let { user -> + IconButton(onClick = { + viewModel.togglePin(user.serverId, user.userId) + }) { + Icon( + if (pinned == true) Icons.Filled.PushPin + else Icons.Outlined.PushPin, + contentDescription = null + ) + } + Spacer(Modifier.width(8.dp)) PriceBadge( user.balance, modifier = Modifier diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt index a475733..2d7fe45 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt @@ -24,26 +24,35 @@ package de.chaosdorf.meteroid.ui.navigation -object Routes { - const val Init = "init" +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.meteroid.model.ServerId +object Routes { object Servers { - const val Root = "servers" - const val List = "$Root/list" - const val Add = "$Root/new" + const val List = "server" + const val Add = "server/create" } - object Users { - const val Root = "users" - const val List = "$Root/list" - //const val Add = "$Root/new" + const val List = "server/{server}" + fun list(server: ServerId) = List + .replace("{server}", server.value.toString()) } - object Home { - const val Root = "home" - const val Deposit = "$Root/deposit" - const val Purchase = "$Root/purchase" - const val History = "$Root/history" - const val Wrapped = "$Root/wrapped" + const val Purchase = "server/{server}/user/{user}/purchase" + const val Deposit = "server/{server}/user/{user}/deposit" + const val History = "server/{server}/user/{user}/history" + const val Wrapped = "server/{server}/user/{user}/wrapped" + fun purchase(server: ServerId, user: UserId) = Purchase + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) + fun deposit(server: ServerId, user: UserId) = Deposit + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) + fun history(server: ServerId, user: UserId) = History + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) + fun wrapped(server: ServerId, user: UserId) = Wrapped + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) } } 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 index 29ed7c6..a801dff 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt @@ -28,21 +28,17 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar @@ -57,18 +53,17 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController import coil.compose.AsyncImage import de.chaosdorf.meteroid.R +import de.chaosdorf.meteroid.ui.navigation.Routes import kotlinx.coroutines.launch import okhttp3.HttpUrl.Companion.toHttpUrl @Composable fun AddServerScreen( - viewModel: AddServerViewModel = viewModel(), - isFirst: Boolean = false, - onBack: () -> Unit = {}, - onAdd: () -> Unit = {} + navController: NavController, + viewModel: AddServerViewModel, ) { val scope = rememberCoroutineScope() val url by viewModel.url.collectAsState() @@ -95,13 +90,6 @@ fun AddServerScreen( } } }, - navigationIcon = { - if (!isFirst) { - IconButton(onClick = { onBack() }) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) - } - } - }, modifier = Modifier.shadow(4.dp) ) }, @@ -109,7 +97,8 @@ fun AddServerScreen( Column( Modifier .padding(paddingValues) - .padding(16.dp, 8.dp)) { + .padding(16.dp, 8.dp) + ) { TextField( label = { Text("Server URL") }, value = url, @@ -146,12 +135,13 @@ fun AddServerScreen( Spacer( Modifier .width(16.dp) - .weight(1.0f)) + .weight(1.0f) + ) IconButton(onClick = { scope.launch { viewModel.addServer() - onAdd() + navController.navigate(Routes.Servers.List) } }) { Icon(Icons.Default.Add, contentDescription = "Add Server") diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt index 577e81d..5625e6f 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt @@ -24,72 +24,35 @@ package de.chaosdorf.meteroid.ui.servers -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width 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.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController import coil.compose.AsyncImage -import de.chaosdorf.meteroid.R -import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.ui.navigation.Routes import okhttp3.HttpUrl.Companion.toHttpUrl -@Preview @Composable fun ServerListScreen( - viewModel: ServerListViewModel = viewModel(), - onAdd: () -> Unit = {}, - onSelect: (ServerId) -> Unit = {} + navController: NavController, + viewModel: ServerListViewModel, + topBar: @Composable () -> Unit, ) { val servers by viewModel.servers.collectAsState() - Scaffold( - topBar = { - TopAppBar( - title = { - Row { - Image( - painterResource(R.drawable.ic_launcher), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(48.dp) - ) - Spacer(Modifier.width(16.dp)) - Column(modifier = Modifier.align(Alignment.CenterVertically)) { - Text( - "Meteroid", - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.bodyMedium - ) - } - } - }, - modifier = Modifier.shadow(4.dp) - ) - } - ) { paddingValues -> + Scaffold(topBar = topBar) { paddingValues -> Column { LazyColumn(modifier = Modifier.padding(paddingValues)) { items(servers) { server -> @@ -104,13 +67,18 @@ fun ServerListScreen( modifier = Modifier.size(48.dp) ) }, - modifier = Modifier.clickable { onSelect(server.serverId) } + modifier = Modifier.clickable { + navController.navigate(Routes.Users.list(server.serverId)) + viewModel.selectServer(server.serverId) + } ) } item { ListItem( headlineContent = { Text("Add Server") }, - modifier = Modifier.clickable { onAdd() } + modifier = Modifier.clickable { + navController.navigate(Routes.Servers.Add) + } ) } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt index 32fb347..8e42801 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt @@ -28,16 +28,26 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.storage.AccountPreferences import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ServerListViewModel @Inject constructor( - serverRepository: ServerRepository + serverRepository: ServerRepository, + private val preferences: AccountPreferences ) : ViewModel() { val servers: StateFlow<List<Server>> = serverRepository.getAllFlow() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + fun selectServer(serverId: ServerId) { + viewModelScope.launch { + preferences.setServer(serverId) + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt index ee0a639..30620f8 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt @@ -36,19 +36,12 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.navigation.NavOptions -import de.chaosdorf.meteroid.ui.navigation.HomeSections -import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar -import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.time.Month @Composable fun TransactionListScreen( viewModel: TransactionViewModel, - onNavigate: (String, NavOptions) -> Unit + topBar: @Composable () -> Unit, + bottomBar: @Composable () -> Unit, ) { val account by viewModel.account.collectAsState() val transactions by viewModel.transactions.collectAsState() @@ -66,16 +59,8 @@ fun TransactionListScreen( } Scaffold( - topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) }, - bottomBar = { - MeteroidBottomBar( - currentRoute = HomeSections.HISTORY, - historyEnabled = account?.user?.audit == true, - wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) - .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, - navigateTo = onNavigate - ) - }, + topBar = topBar, + bottomBar = bottomBar, snackbarHost = { SnackbarHost(hostState = snackbarHostState) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt index 7242558..d3efc78 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt @@ -24,21 +24,20 @@ package de.chaosdorf.meteroid.ui.transactions -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.meteroid.model.AccountInfo +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.sync.AccountProvider import de.chaosdorf.meteroid.sync.SyncManager import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -48,47 +47,30 @@ import kotlin.time.Duration.Companion.minutes @HiltViewModel class TransactionViewModel @Inject constructor( - private val accountProvider: AccountProvider, + savedStateHandle: SavedStateHandle, + accountProvider: AccountProvider, + private val syncManager: SyncManager, repository: TransactionRepository, - drinkRepository: DrinkRepository, - private val syncManager: SyncManager + drinkRepository: DrinkRepository ) : ViewModel() { - val account: StateFlow<AccountInfo?> = accountProvider.account - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) + private val userId = UserId(checkNotNull(savedStateHandle["user"])) - val transactions: StateFlow<List<TransactionInfo>> = accountProvider.account - .flatMapLatest { account -> - account?.let { (server, maybeUser) -> - maybeUser?.let { user -> - combine( - repository.getAllFlow(server.serverId, user.userId), - drinkRepository.getAllFlow(server.serverId) - ) { transactions, drinks -> - transactions.map { transaction -> - TransactionInfo( - transaction, - drinks.firstOrNull { drink -> drink.drinkId == transaction.drinkId } - ) - } - } - } - } ?: flowOf(emptyList()) - }.mapLatest { list -> - list.mergeAdjecentDeposits().filter { - it.drink != null || - it.transaction.difference.abs() >= 0.01.toBigDecimal() - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + val account = accountProvider.accountFlow(serverId, userId) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - fun togglePin() { - account.value?.let { account -> - account.user?.let { user -> - viewModelScope.launch { - accountProvider.togglePin(account.server.serverId, user.userId) - } - } + 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() { account.value?.let { (server, user) -> @@ -98,21 +80,17 @@ class TransactionViewModel @Inject constructor( } } - suspend fun checkOffline(server: Server?): Boolean = - if (server == null) true - else syncManager.checkOffline(server) + suspend fun checkOffline(server: Server?): Boolean = if (server == null) true + else syncManager.checkOffline(server) } 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 + 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( diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt index 56aba29..38218bd 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt @@ -24,103 +24,33 @@ package de.chaosdorf.meteroid.ui.users -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width 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.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import de.chaosdorf.mete.model.UserId -import okhttp3.HttpUrl.Companion.toHttpUrl +import androidx.navigation.NavController +import de.chaosdorf.meteroid.ui.navigation.Routes @Composable fun UserListScreen( + navController: NavController, viewModel: UserListViewModel, - onAdd: () -> Unit = {}, - onSelect: (UserId) -> Unit = {}, - onBack: () -> Unit = {}, + topBar: @Composable () -> Unit, ) { - val server by viewModel.account.collectAsState() val users by viewModel.users.collectAsState() val pinnedUsers by viewModel.pinnedUsers.collectAsState() Scaffold( - topBar = { - TopAppBar( - title = { - Row { - AsyncImage( - server?.server?.logoUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(48.dp) - ) - Spacer(Modifier.width(16.dp)) - Column(modifier = Modifier.align(Alignment.CenterVertically)) { - if (server?.server != null) { - if (server?.server?.name != null) { - Text( - server!!.server.name!!, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.bodyMedium - ) - Text( - server!!.server.url.toHttpUrl().host, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.bodyMedium - ) - } else { - Text( - server!!.server.url, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.bodyMedium - ) - } - } else { - Text( - "Meteroid", - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } - }, - navigationIcon = { - IconButton(onClick = { onBack() }) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) - } - }, - modifier = Modifier - .shadow(4.dp) - .clickable { onBack() } - ) - } + topBar = topBar, ) { paddingValues -> LazyVerticalGrid( GridCells.Adaptive(80.dp), @@ -137,7 +67,9 @@ fun UserListScreen( } items(pinnedUsers) { user -> - UserTile(user, onSelect) + UserTile(user) { + navController.navigate(Routes.Home.purchase(user.serverId, user.userId)) + } } } @@ -153,7 +85,10 @@ fun UserListScreen( } items(group) { user -> - UserTile(user, onSelect) + UserTile(user) { + navController.navigate(Routes.Home.purchase(user.serverId, user.userId)) + viewModel.selectUser(user.serverId, user.userId) + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt index 112dfe6..468071c 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt @@ -24,50 +24,46 @@ package de.chaosdorf.meteroid.ui.users +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.meteroid.model.AccountInfo +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.PinnedUserRepository import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.model.UserRepository -import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.UserSyncHandler +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.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class UserListViewModel @Inject constructor( - accountProvider: AccountProvider, + savedStateHandle: SavedStateHandle, userRepository: UserRepository, pinnedUserRepository: PinnedUserRepository, + private val preferences: AccountPreferences, ) : ViewModel() { - val account: StateFlow<AccountInfo?> = accountProvider.account - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) - val users: StateFlow<List<User>> = accountProvider.account - .flatMapLatest { account -> - account?.let { (server, _) -> - userRepository.getAllFlow(server.serverId) - } ?: flowOf(emptyList()) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + val users: StateFlow<List<User>> = userRepository.getAllFlow(serverId) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - val pinnedUsers = accountProvider.account - .flatMapLatest { account -> - account?.let { (server, _) -> - combine( - userRepository.getAllFlow(server.serverId), - pinnedUserRepository.getAllFlow(server.serverId) - ) { users, pinned -> - users.filter { pinned.contains(it.userId) } - } - } ?: flowOf(emptyList()) - }.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) { + viewModelScope.launch { + preferences.setServer(serverId) + preferences.setUser(userId) + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt index b36cf87..73848b6 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt @@ -45,22 +45,15 @@ 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 androidx.navigation.NavOptions import coil.compose.rememberAsyncImagePainter import de.chaosdorf.meteroid.R -import de.chaosdorf.meteroid.ui.navigation.HomeSections -import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar -import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.time.Month import java.time.format.TextStyle @Composable fun WrappedScreen( viewModel: WrappedViewModel, - onNavigate: (String, NavOptions) -> Unit + topBar: @Composable () -> Unit, + bottomBar: @Composable () -> Unit, ) { val account by viewModel.account.collectAsState() val slides by viewModel.slides.collectAsState() @@ -78,16 +71,8 @@ fun WrappedScreen( } Scaffold( - topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) }, - bottomBar = { - MeteroidBottomBar( - currentRoute = HomeSections.WRAPPED, - historyEnabled = account?.user?.audit == true, - wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) - .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, - navigateTo = onNavigate - ) - }, + topBar = topBar, + bottomBar = bottomBar, snackbarHost = { SnackbarHost(hostState = snackbarHostState) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt index d50c28a..712b16c 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt @@ -24,14 +24,16 @@ package de.chaosdorf.meteroid.ui.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.meteroid.model.AccountInfo +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.Transaction import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.sync.AccountProvider @@ -39,10 +41,7 @@ import de.chaosdorf.meteroid.sync.SyncManager import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.Month @@ -53,12 +52,16 @@ import javax.inject.Inject @HiltViewModel class WrappedViewModel @Inject constructor( - private val accountProvider: AccountProvider, + savedStateHandle: SavedStateHandle, + accountProvider: AccountProvider, + private val syncManager: SyncManager, repository: TransactionRepository, drinkRepository: DrinkRepository, - private val syncManager: SyncManager ) : ViewModel() { - val account: StateFlow<AccountInfo?> = accountProvider.account + private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) + private val userId = UserId(checkNotNull(savedStateHandle["user"])) + + val account = accountProvider.accountFlow(serverId, userId) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) private fun List<Transaction>.filterAudits(year: Int): List<Transaction> { @@ -71,37 +74,20 @@ class WrappedViewModel @Inject constructor( } } - val slides: StateFlow<List<WrappedSlide>> = accountProvider.account - .flatMapLatest { account -> - account?.let { (server, maybeUser) -> - maybeUser?.let { user -> - combine( - repository.getAllFlow(server.serverId, user.userId), - drinkRepository.getAllFlow(server.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) } - } - } - } ?: flowOf(emptyList()) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - fun togglePin() { - account.value?.let { account -> - account.user?.let { user -> - viewModelScope.launch { - accountProvider.togglePin(account.server.serverId, user.userId) - } - } - } - } + 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()) suspend fun checkOffline(server: Server?): Boolean = if (server == null) true diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt index cd9af86..3c7150e 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt @@ -26,6 +26,6 @@ package de.chaosdorf.meteroid.model data class AccountInfo( val server: Server, - val user: User?, + val user: User, val pinned: Boolean ) diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt index 8036071..0e94a9b 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt @@ -51,6 +51,8 @@ interface ServerRepository { @Query("SELECT * FROM Server WHERE serverId = :id LIMIT 1") fun getFlow(id: ServerId): Flow<Server?> + @Query("SELECT count(*) FROM Server") + fun countFlow(): Flow<Int> @Query("SELECT * FROM Server") suspend fun getAll(): List<Server> -- GitLab