From bf720298fec917d7bc9652134b39793ecb40aca3 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <janne@kuschku.de> Date: Fri, 1 Dec 2023 00:03:16 +0100 Subject: [PATCH] wip: pre-navigation rewrite state --- .../de/chaosdorf/meteroid/MainActivity.kt | 2 +- .../meteroid/storage/AccountPreferences.kt | 2 +- .../storage/AccountPreferencesImpl.kt | 8 +- .../de/chaosdorf/meteroid/ui/AppRouter.kt | 209 +++++++++++------- .../de/chaosdorf/meteroid/ui/AppViewModel.kt | 41 ++-- .../meteroid/ui/NavigationViewModel.kt | 13 +- .../meteroid/ui/drinks/DrinkListFilterChip.kt | 6 +- .../meteroid/ui/drinks/DrinkListScreen.kt | 55 ++--- .../chaosdorf/meteroid/ui/drinks/DrinkTile.kt | 7 +- .../meteroid/ui/money/MoneyListScreen.kt | 57 +---- .../chaosdorf/meteroid/ui/money/MoneyTile.kt | 6 +- .../meteroid/ui/navigation/HomeSections.kt | 4 +- .../ui/navigation/MeteroidBottomBar.kt | 71 +++--- .../meteroid/ui/navigation/MeteroidTopBar.kt | 158 +++++++++---- .../meteroid/ui/servers/AddServerScreen.kt | 126 ++++------- .../meteroid/ui/servers/ServerListScreen.kt | 56 +++-- .../de/chaosdorf/meteroid/ui/theme/Color.kt | 80 ++++++- .../de/chaosdorf/meteroid/ui/theme/Theme.kt | 87 ++++++-- .../meteroid/ui/theme/ThemeGradient.kt | 58 +++++ .../ui/transactions/PurchaseListItem.kt | 9 +- .../ui/transactions/PurchaseListScreen.kt | 39 +--- .../meteroid/ui/users/UserListItem.kt | 84 +++++++ .../meteroid/ui/users/UserListScreen.kt | 66 +++--- .../meteroid/ui/users/UserListViewModel.kt | 6 +- .../meteroid/ui/wrapped/WrappedScreen.kt | 197 +++++++---------- .../de/chaosdorf/meteroid/util/PopUpToRoot.kt | 33 +++ .../meteroid/util/RememberAvatarPainter.kt | 51 +++++ .../de/chaosdorf/meteroid/model/PinnedUser.kt | 3 + 28 files changed, 941 insertions(+), 593 deletions(-) create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index 81481f3..9f09bde 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.initialRoute.value == null + viewModel.initialBackStack.value == null } setContent { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt index 3d0751f..ebec013 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt @@ -37,5 +37,5 @@ interface AccountPreferences { val state: Flow<State> suspend fun setServer(server: ServerId?) - suspend fun setUser(user: UserId?) + suspend fun setUser(server: ServerId, user: UserId?) } 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 62f4b80..e696e02 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt @@ -24,6 +24,7 @@ package de.chaosdorf.meteroid.storage +import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit @@ -42,6 +43,7 @@ class AccountPreferencesImpl @Inject constructor( dataStore.data.mapLatest { val serverId = it[SERVER_KEY] ?: -1L val userId = it[USER_KEY] ?: -1L + Log.i("AccountPreferences", "Restored account data: $userId@$serverId") AccountPreferences.State( if (serverId >= 0) ServerId(serverId) else null, @@ -50,13 +52,17 @@ class AccountPreferencesImpl @Inject constructor( } override suspend fun setServer(server: ServerId?) { + Log.i("AccountPreferences", "Setting account to -1@${server?.value ?: -1L}") dataStore.edit { it[SERVER_KEY] = server?.value ?: -1L + it[USER_KEY] = -1L } } - override suspend fun setUser(user: UserId?) { + override suspend fun setUser(server: ServerId, user: UserId?) { + Log.i("AccountPreferences", "Setting account to ${user?.value ?: -1L}@${server.value}") dataStore.edit { + it[SERVER_KEY] = server.value it[USER_KEY] = user?.value ?: -1L } } 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 b00553b..d10008b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt @@ -24,16 +24,28 @@ package de.chaosdorf.meteroid.ui +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier 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.UserId +import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel import de.chaosdorf.meteroid.ui.money.MoneyListScreen @@ -51,99 +63,130 @@ 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 de.chaosdorf.meteroid.util.popUpToRoot @Composable fun AppRouter(viewModel: AppViewModel) { val navController = rememberNavController() - val initialRoute by viewModel.initialRoute.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + val navigationViewModel = hiltViewModel<NavigationViewModel>() - LaunchedEffect(initialRoute) { - initialRoute?.let { - navController.navigate(it) - } + val initialBackStack by viewModel.initialBackStack.collectAsState() + val offline by viewModel.offline.collectAsState() + + navController.addOnDestinationChangedListener { _, _, arguments -> + val serverId = arguments?.getLong("server")?.let(::ServerId) + val userId = arguments?.getLong("user")?.let(::UserId) + + navigationViewModel.serverId.value = serverId + navigationViewModel.userId.value = userId } - NavHost(navController, startDestination = Routes.Servers.List) { - composable(Routes.Servers.Add) { _ -> - val hiltViewModel = hiltViewModel<AddServerViewModel>() - AddServerScreen(navController, hiltViewModel) + LaunchedEffect(initialBackStack) { + initialBackStack?.let { initialBackStack -> + navController.popUpToRoot() + for (entry in initialBackStack) { + navController.navigate(entry) + } } - 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) } - ) - } - */ - composable( - Routes.Users.List, - arguments = listOf( - navArgument("server") { type = NavType.LongType } - ) - ) { _ -> - 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 }, - ) - ) { _ -> - 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) - } - 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) - } - composable( - Routes.Home.History, - arguments = listOf( - navArgument("server") { type = NavType.LongType }, - navArgument("user") { type = NavType.LongType }, + } + + LaunchedEffect(offline) { + snackbarHostState.currentSnackbarData?.dismiss() + if (offline) { + snackbarHostState.showSnackbar( + message = "Unable to connect to server", + duration = SnackbarDuration.Indefinite ) - ) { _ -> - val hiltViewModel = hiltViewModel<TransactionViewModel>() - val navigationViewModel = hiltViewModel<NavigationViewModel>() - val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) } - val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) } - TransactionListScreen(hiltViewModel, topBar, bottomBar) } - 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) + } + + Box { + Scaffold( + topBar = { + MeteroidTopBar(navController, navigationViewModel, Modifier.align(Alignment.TopCenter)) + }, + bottomBar = { + MeteroidBottomBar(navController, navigationViewModel) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } + ) { paddingValues -> + NavHost( + navController, + startDestination = Routes.Servers.List, + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None }, + popEnterTransition = { EnterTransition.None }, + popExitTransition = { ExitTransition.None } + ) { + composable(Routes.Servers.Add) { _ -> + val hiltViewModel = hiltViewModel<AddServerViewModel>() + AddServerScreen(navController, hiltViewModel, paddingValues) + } + composable(Routes.Servers.List) { _ -> + val hiltViewModel = hiltViewModel<ServerListViewModel>() + ServerListScreen(navController, hiltViewModel, paddingValues) + } + /* + composable(Routes.Users.Add) { _ -> + AddUserScreen( + hiltViewModel(), + onAdd = { navController.navigate(Routes.Users.List) } + ) + } + */ + composable( + Routes.Users.List, + arguments = listOf( + navArgument("server") { type = NavType.LongType } + ) + ) { _ -> + val hiltViewModel = hiltViewModel<UserListViewModel>() + UserListScreen(navController, hiltViewModel, paddingValues) + } + composable( + Routes.Home.Purchase, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, + ) + ) { _ -> + val hiltViewModel = hiltViewModel<DrinkListViewModel>() + DrinkListScreen(navController, hiltViewModel, paddingValues) + } + composable( + Routes.Home.Deposit, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, + ) + ) { _ -> + val hiltViewModel = hiltViewModel<MoneyListViewModel>() + MoneyListScreen(navController, hiltViewModel, paddingValues) + } + composable( + Routes.Home.History, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, + ) + ) { _ -> + val hiltViewModel = hiltViewModel<TransactionViewModel>() + TransactionListScreen(hiltViewModel, paddingValues) + } + composable( + Routes.Home.Wrapped, + arguments = listOf( + navArgument("server") { type = NavType.LongType }, + navArgument("user") { type = NavType.LongType }, + ) + ) { _ -> + val hiltViewModel = hiltViewModel<WrappedViewModel>() + WrappedScreen(hiltViewModel, paddingValues) + } + } } } } 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 b465366..c940440 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt @@ -27,26 +27,27 @@ package de.chaosdorf.meteroid.ui 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.ServerId import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.model.UserRepository import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.sync.SyncManager import de.chaosdorf.meteroid.ui.navigation.Routes 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 javax.inject.Inject @HiltViewModel class AppViewModel @Inject constructor( - private val accountPreferences: AccountPreferences, + accountPreferences: AccountPreferences, private val serverRepository: ServerRepository, - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val syncManager: SyncManager ) : ViewModel() { - val initialRoute = accountPreferences.state.flatMapLatest { + val initialBackStack = accountPreferences.state.flatMapLatest { combine( serverRepository.countFlow(), if (it.server == null) flowOf(null) @@ -55,23 +56,29 @@ class AppViewModel @Inject constructor( else userRepository.getFlow(it.server, it.user) ) { serverCount, server, user -> if (user != null) { - Routes.Home.purchase(user.serverId, user.userId) + listOf( + Routes.Servers.List, + Routes.Users.list(user.serverId), + Routes.Home.purchase(user.serverId, user.userId) + ) } else if (server != null) { - Routes.Users.list(server.serverId) + listOf( + Routes.Servers.List, + Routes.Users.list(server.serverId) + ) } else if (serverCount > 0) { - Routes.Servers.List + listOf(Routes.Servers.List) } else { - Routes.Servers.Add + listOf(Routes.Servers.Add) } } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - suspend fun selectServer(server: ServerId) { - accountPreferences.setServer(server) - accountPreferences.setUser(null) - } - - suspend fun selectUser(user: UserId) { - accountPreferences.setUser(user) - } + val offline = accountPreferences.state.flatMapLatest { state -> + state.server?.let { serverId -> + serverRepository.getFlow(serverId) + } ?: flowOf(null) + }.mapLatest { server -> + server?.let { syncManager.checkOffline(it) } ?: false + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt index 77537ef..6346fe5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt @@ -24,7 +24,6 @@ package de.chaosdorf.meteroid.ui -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -34,30 +33,24 @@ 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.MutableStateFlow 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 serverId = MutableStateFlow<ServerId?>(null) + val userId = MutableStateFlow<UserId?>(null) val server = serverId.flatMapLatest { if (it == null) flowOf(null) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt index cdd66c3..cba1aa8 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -55,6 +56,9 @@ fun DrinkListFilterChip( ) } }, - onClick = onClick + onClick = onClick, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer + ) ) } 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 d454baf..3e7db6e 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 @@ -25,23 +25,17 @@ package de.chaosdorf.meteroid.ui.drinks import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow 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.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -51,33 +45,17 @@ import androidx.navigation.NavController fun DrinkListScreen( navController: NavController, viewModel: DrinkListViewModel, - topBar: @Composable () -> Unit, - bottomBar: @Composable () -> Unit, + contentPadding: PaddingValues = PaddingValues(), ) { - val account by viewModel.account.collectAsState() val drinks by viewModel.drinks.collectAsState() val filters by viewModel.filters.collectAsState() - val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(account) { - val offline = viewModel.checkOffline(account?.server) - snackbarHostState.currentSnackbarData?.dismiss() - if (offline) { - snackbarHostState.showSnackbar( - message = "Unable to connect to server", - duration = SnackbarDuration.Indefinite - ) - } - } - - Scaffold( - topBar = topBar, - bottomBar = bottomBar, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } - ) { paddingValues: PaddingValues -> - Column(Modifier.padding(paddingValues)) { + 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) @@ -93,15 +71,14 @@ fun DrinkListScreen( onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.CaffeineFree) } ) } - LazyVerticalGrid( - GridCells.Adaptive(104.dp), - modifier = Modifier.padding(horizontal = 8.dp) - ) { - items(drinks) { drink -> - DrinkTile(drink) { - viewModel.purchase(it, navController::navigateUp) - } - } + } + + items( + drinks, + key = { "${it.serverId}-${it.drinkId}" }, + ) { drink -> + DrinkTile(drink) { + viewModel.purchase(it, navController::navigateUp) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt index f079ffd..9a6d972 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -57,12 +56,14 @@ import coil.compose.rememberAsyncImagePainter import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.sample.SampleDrinkProvider import de.chaosdorf.meteroid.ui.PriceBadge +import de.chaosdorf.meteroid.ui.theme.secondaryGradient import java.math.BigDecimal @Preview(widthDp = 120, showBackground = true) @Composable fun DrinkTile( @PreviewParameter(SampleDrinkProvider::class) item: Drink, + modifier: Modifier = Modifier, onPurchase: (Drink) -> Unit = {} ) { val thumbPainter = rememberAsyncImagePainter( @@ -74,7 +75,7 @@ fun DrinkTile( ) Column( - modifier = Modifier + modifier = modifier .height(IntrinsicSize.Max) .alpha(if (item.active) 1.0f else 0.67f) .clip(RoundedCornerShape(8.dp)) @@ -89,7 +90,7 @@ fun DrinkTile( modifier = Modifier .aspectRatio(1.0f) .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) + .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient()) ) PriceBadge( item.price, 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 b7df727..ee78739 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 @@ -24,22 +24,12 @@ package de.chaosdorf.meteroid.ui.money -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column 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.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -48,42 +38,19 @@ import androidx.navigation.NavController fun MoneyListScreen( navController: NavController, viewModel: MoneyListViewModel, - topBar: @Composable () -> Unit, - bottomBar: @Composable () -> Unit, + contentPadding: PaddingValues = PaddingValues(), ) { - val account by viewModel.account.collectAsState() - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(account) { - val offline = viewModel.checkOffline(account?.server) - snackbarHostState.currentSnackbarData?.dismiss() - if (offline) { - snackbarHostState.showSnackbar( - message = "Unable to connect to server", - duration = SnackbarDuration.Indefinite - ) - } - } - - Scaffold( - topBar = topBar, - bottomBar = bottomBar, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } - ) { paddingValues: PaddingValues -> - Column { - LazyVerticalGrid( - GridCells.Adaptive(120.dp), - modifier = Modifier.padding(paddingValues), - contentPadding = PaddingValues(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - items(viewModel.money) { monetaryAmount -> - MoneyTile(monetaryAmount) { - viewModel.deposit(it, navController::navigateUp) - } - } + LazyVerticalGrid( + GridCells.Adaptive(104.dp), + contentPadding = contentPadding, + modifier = Modifier.padding(horizontal = 8.dp), + ) { + items( + viewModel.money, + key = { "${it.ordinal}" }, + ) { monetaryAmount -> + MoneyTile(monetaryAmount) { + viewModel.deposit(it, navController::navigateUp) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt index 5254c6f..c503e5e 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt @@ -25,16 +25,13 @@ package de.chaosdorf.meteroid.ui.money 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.IntrinsicSize import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.paddingFromBaseline -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,10 +44,11 @@ import de.chaosdorf.meteroid.ui.PriceBadge @Composable fun MoneyTile( item: MonetaryAmount, + modifier: Modifier = Modifier, onDeposit: (MonetaryAmount) -> Unit = {} ) { Box( - modifier = Modifier + modifier = modifier .height(IntrinsicSize.Max) .clip(RoundedCornerShape(8.dp)) .clickable { onDeposit(item) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt index 08f21cc..2ff9789 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt @@ -42,8 +42,8 @@ enum class HomeSections( override val iconActive: ImageVector, override val route: String ) : MeteroidNavSection { - PURCHASE("Drinks", MeteroidIcons.Outlined.WaterFull, MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase), - DEPOSIT("Money", Icons.Outlined.LocalAtm, Icons.TwoTone.LocalAtm, Routes.Home.Deposit), + PURCHASE("Purchase", MeteroidIcons.Outlined.WaterFull, MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase), + DEPOSIT("Deposit", Icons.Outlined.LocalAtm, Icons.TwoTone.LocalAtm, Routes.Home.Deposit), HISTORY("History", Icons.Outlined.History, Icons.TwoTone.History, Routes.Home.History), WRAPPED("Wrapped", Icons.Outlined.Celebration, Icons.TwoTone.Celebration, Routes.Home.Wrapped); } 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 9710b66..a8c0369 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 @@ -25,6 +25,7 @@ package de.chaosdorf.meteroid.ui.navigation import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text @@ -32,9 +33,10 @@ import androidx.compose.runtime.Composable 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 de.chaosdorf.meteroid.ui.theme.onPrimaryContainerTinted +import de.chaosdorf.meteroid.util.popUpToRoot import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @@ -52,42 +54,47 @@ fun MeteroidBottomBar( .month.let { it == Month.NOVEMBER || it == Month.DECEMBER } val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination - val activeRoute = if (user == null) HomeSections.PURCHASE + val activeRoute = if (user == null) null else HomeSections.entries.find { - it.withArguments( - "server" to user!!.serverId.value.toString(), - "user" to user!!.userId.value.toString() - ) == currentDestination?.route + it.route == currentDestination?.route } - NavigationBar { - for (route in HomeSections.entries) { - if (wrappedEnabled || route != HomeSections.WRAPPED) { - NavigationBarItem( - icon = { - Icon( - if (route == activeRoute) route.iconActive else route.icon, - contentDescription = route.title - ) - }, - label = { Text(route.title) }, - selected = route == activeRoute, - onClick = { - navController.navigate( - route.withArguments( - "server" to user!!.serverId.value.toString(), - "user" to user!!.userId.value.toString() + if (activeRoute != null) { + NavigationBar( + contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted + ) { + for (route in HomeSections.entries) { + if (wrappedEnabled || route != HomeSections.WRAPPED) { + NavigationBarItem( + icon = { + Icon( + if (route == activeRoute) route.iconActive else route.icon, + contentDescription = route.title, + tint = MaterialTheme.colorScheme.onPrimaryContainerTinted ) - ) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true + }, + label = { Text(route.title, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) }, + selected = route == activeRoute, + onClick = { + navController.popUpToRoot() + navController.navigate(Routes.Servers.List) + navController.navigate(Routes.Users.list(user!!.serverId)) + navController.navigate( + route.withArguments( + "server" to user!!.serverId.value.toString(), + "user" to user!!.userId.value.toString() + ) + ) + }, + enabled = when (route) { + HomeSections.PURCHASE, + HomeSections.DEPOSIT -> true + + HomeSections.HISTORY, + HomeSections.WRAPPED -> historyEnabled } - }, - enabled = route != HomeSections.HISTORY || historyEnabled - ) + ) + } } } } 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 41abc77..a018e1b 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 @@ -24,16 +24,21 @@ package de.chaosdorf.meteroid.ui.navigation +import android.util.Log +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.Icon @@ -42,32 +47,57 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface 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.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.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import coil.compose.AsyncImage -import de.chaosdorf.meteroid.ui.PriceBadge +import de.chaosdorf.meteroid.R import de.chaosdorf.meteroid.ui.NavigationViewModel +import de.chaosdorf.meteroid.ui.PriceBadge +import de.chaosdorf.meteroid.util.rememberAvatarPainter import okhttp3.HttpUrl.Companion.toHttpUrl @Composable fun MeteroidTopBar( navController: NavController, - viewModel: NavigationViewModel + viewModel: NavigationViewModel, + modifier: Modifier = Modifier ) { val server by viewModel.server.collectAsState() val user by viewModel.user.collectAsState() val pinned by viewModel.pinned.collectAsState() + val backstack by navController.currentBackStack.collectAsState() + val canNavigateUp = backstack.size > 2 + LaunchedEffect(backstack) { + val backstackEntries = backstack.map { + it.destination.route + ?.replace("{server}", it.arguments?.getLong("server")?.toString() ?: "{server}") + ?.replace("{user}", it.arguments?.getLong("user")?.toString() ?: "{user}") + } + Log.e("Navigation", "BACKSTACK: [${backstackEntries.joinToString(" › ")}]") + } + + val avatarPainter = rememberAvatarPainter( + user?.gravatarUrl, + 32.dp, 32.dp, + MaterialTheme.colorScheme.primary + ) + + val iconPainter = painterResource(R.drawable.ic_launcher) + Surface( - modifier = Modifier.padding(8.dp), + modifier = modifier + .padding(8.dp) + .height(64.dp), color = MaterialTheme.colorScheme.surface, shadowElevation = 6.dp, tonalElevation = 6.dp, @@ -75,49 +105,95 @@ fun MeteroidTopBar( onClick = { navController.navigateUp() } ) { Row(modifier = Modifier.padding(8.dp)) { - AsyncImage( - user?.gravatarUrl, - contentDescription = "User List", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.tertiary) - ) + if (user != null) { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer) + ) { + Image( + avatarPainter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.align(Alignment.Center) + ) + } + } else if (canNavigateUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), + modifier = Modifier + .padding(8.dp) + .size(32.dp) + ) + } else { + Image( + iconPainter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + ) + } Spacer(Modifier.width(16.dp)) Column( modifier = Modifier .align(Alignment.CenterVertically) .weight(1.0f, fill = true) ) { - 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 - ) + when { + user != null && server != null -> { + 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.Normal, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) + } + + server != null && server!!.name != null -> { + Text( + server!!.name!!, + fontWeight = FontWeight.SemiBold, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) + Text( + server!!.url.toHttpUrl().host, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), + fontWeight = FontWeight.Normal, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) + } + + server != null -> { + Text( + server!!.url.toHttpUrl().host, + fontWeight = FontWeight.SemiBold, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) + } + + else -> { + Text( + "Meteroid", + fontWeight = FontWeight.SemiBold, + overflow = TextOverflow.Ellipsis, + softWrap = false + ) + } } } Spacer(Modifier.width(16.dp)) 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 a801dff..f5ea039 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 @@ -24,8 +24,8 @@ package de.chaosdorf.meteroid.ui.servers -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -38,24 +38,19 @@ 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.Text import androidx.compose.material3.TextField -import androidx.compose.material3.TopAppBar 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.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.unit.dp 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 @@ -64,88 +59,63 @@ import okhttp3.HttpUrl.Companion.toHttpUrl fun AddServerScreen( navController: NavController, viewModel: AddServerViewModel, + contentPadding: PaddingValues = PaddingValues(), ) { val scope = rememberCoroutineScope() val url by viewModel.url.collectAsState() val server by viewModel.server.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 -> - Column( - Modifier - .padding(paddingValues) - .padding(16.dp, 8.dp) - ) { - TextField( - label = { Text("Server URL") }, - value = url, - onValueChange = { viewModel.url.value = it }, - modifier = Modifier.fillMaxWidth() - ) + 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) + 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 ) - 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) + 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.navigate(Routes.Servers.List) - } - }) { - Icon(Icons.Default.Add, contentDescription = "Add Server") + IconButton(onClick = { + scope.launch { + viewModel.addServer() + 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 5625e6f..19242ea 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 @@ -25,13 +25,11 @@ package de.chaosdorf.meteroid.ui.servers import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding +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.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -48,40 +46,36 @@ import okhttp3.HttpUrl.Companion.toHttpUrl fun ServerListScreen( navController: NavController, viewModel: ServerListViewModel, - topBar: @Composable () -> Unit, + contentPadding: PaddingValues = PaddingValues(), ) { val servers by viewModel.servers.collectAsState() - Scaffold(topBar = topBar) { paddingValues -> - Column { - LazyColumn(modifier = Modifier.padding(paddingValues)) { - items(servers) { server -> - ListItem( - headlineContent = { Text(server.name ?: server.url) }, - supportingContent = { if (server.name != null) Text(server.url.toHttpUrl().host) }, - leadingContent = { - AsyncImage( - server.logoUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(48.dp) - ) - }, - modifier = Modifier.clickable { - navController.navigate(Routes.Users.list(server.serverId)) - viewModel.selectServer(server.serverId) - } + LazyColumn(contentPadding = contentPadding) { + items(servers) { server -> + ListItem( + headlineContent = { Text(server.name ?: server.url) }, + supportingContent = { if (server.name != null) Text(server.url.toHttpUrl().host) }, + leadingContent = { + AsyncImage( + server.logoUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(48.dp) ) + }, + modifier = Modifier.clickable { + navController.navigate(Routes.Users.list(server.serverId)) + viewModel.selectServer(server.serverId) } - item { - ListItem( - headlineContent = { Text("Add Server") }, - modifier = Modifier.clickable { - navController.navigate(Routes.Servers.Add) - } - ) + ) + } + item { + ListItem( + headlineContent = { Text("Add Server") }, + modifier = Modifier.clickable { + navController.navigate(Routes.Servers.Add) } - } + ) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt index 33ce00f..e5687bd 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt @@ -1,11 +1,79 @@ package de.chaosdorf.meteroid.ui.theme +import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) +val md_theme_light_primary = Color(0xFF345CA8) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFD9E2FF) +val md_theme_light_onPrimaryContainer = Color(0xFF001A43) +val md_theme_light_secondary = Color(0xFF8B5000) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFFFDCBE) +val md_theme_light_onSecondaryContainer = Color(0xFF2C1600) +val md_theme_light_tertiary = Color(0xFFC0000A) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFFFDAD5) +val md_theme_light_onTertiaryContainer = Color(0xFF410001) +val md_theme_light_error = Color(0xFFC0000A) +val md_theme_light_errorContainer = Color(0xFFFFDAD5) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410001) +val md_theme_light_background = Color(0xFFFEFBFF) +val md_theme_light_onBackground = Color(0xFF1B1B1F) +val md_theme_light_surface = Color(0xFFFEFBFF) +val md_theme_light_onSurface = Color(0xFF1B1B1F) +val md_theme_light_surfaceVariant = Color(0xFFE1E2EC) +val md_theme_light_onSurfaceVariant = Color(0xFF44474F) +val md_theme_light_outline = Color(0xFF757780) +val md_theme_light_inverseOnSurface = Color(0xFFF2F0F4) +val md_theme_light_inverseSurface = Color(0xFF303034) +val md_theme_light_inversePrimary = Color(0xFFAFC6FF) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF345CA8) +val md_theme_light_outlineVariant = Color(0xFFC5C6D0) +val md_theme_light_scrim = Color(0xFF000000) -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val md_theme_dark_primary = Color(0xFFAFC6FF) +val md_theme_dark_onPrimary = Color(0xFF002D6C) +val md_theme_dark_primaryContainer = Color(0xFF15448F) +val md_theme_dark_onPrimaryContainer = Color(0xFFD9E2FF) +val md_theme_dark_secondary = Color(0xFFFFB870) +val md_theme_dark_onSecondary = Color(0xFF4A2800) +val md_theme_dark_secondaryContainer = Color(0xFF693C00) +val md_theme_dark_onSecondaryContainer = Color(0xFFFFDCBE) +val md_theme_dark_tertiary = Color(0xFFFFB4AA) +val md_theme_dark_onTertiary = Color(0xFF690003) +val md_theme_dark_tertiaryContainer = Color(0xFF930006) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFDAD5) +val md_theme_dark_error = Color(0xFFFFB4AA) +val md_theme_dark_errorContainer = Color(0xFF930006) +val md_theme_dark_onError = Color(0xFF690003) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD5) +val md_theme_dark_background = Color(0xFF1B1B1F) +val md_theme_dark_onBackground = Color(0xFFE3E2E6) +val md_theme_dark_surface = Color(0xFF1B1B1F) +val md_theme_dark_onSurface = Color(0xFFE3E2E6) +val md_theme_dark_surfaceVariant = Color(0xFF44474F) +val md_theme_dark_onSurfaceVariant = Color(0xFFC5C6D0) +val md_theme_dark_outline = Color(0xFF8F9099) +val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F) +val md_theme_dark_inverseSurface = Color(0xFFE3E2E6) +val md_theme_dark_inversePrimary = Color(0xFF345CA8) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFFAFC6FF) +val md_theme_dark_outlineVariant = Color(0xFF44474F) +val md_theme_dark_scrim = Color(0xFF000000) + + +val ColorScheme.secondaryGradient + get() = ThemeGradient( + listOf( + secondaryContainer.copy(0.2f).compositeOver(surface), + secondaryContainer, + ) + ) + +val ColorScheme.onPrimaryContainerTinted + get() = primary.copy(alpha = 0.4f).compositeOver(onPrimaryContainer) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt index db521b5..9affa43 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt @@ -32,33 +32,78 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.surfaceColorAtElevation 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 DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, ) -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, ) @Composable @@ -74,14 +119,18 @@ fun MeteroidTheme( if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme - else -> LightColorScheme + darkTheme -> DarkColors + else -> LightColors } val view = LocalView.current 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 } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt new file mode 100644 index 0000000..3de9104 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt @@ -0,0 +1,58 @@ +/* + * 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.theme + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TileMode + +class ThemeGradient(val colors: List<Color>) { + fun linearGradient( + start: Offset = Offset.Zero, + end: Offset = Offset.Infinite, + tileMode: TileMode = TileMode.Clamp + ) = Brush.linearGradient(colors, start, end, tileMode) + + fun verticalGradient( + startY: Float = 0.0f, + endY: Float = Float.POSITIVE_INFINITY, + tileMode: TileMode = TileMode.Clamp + ) = linearGradient( + start = Offset(0.0f, startY), + end = Offset(0.0f, endY), + tileMode = tileMode, + ) + + fun horizontalGradient( + startX: Float = 0.0f, + endX: Float = Float.POSITIVE_INFINITY, + tileMode: TileMode = TileMode.Clamp + ) = linearGradient( + start = Offset(startX, 0.0f), + end = Offset(endX, 0.0f), + tileMode = tileMode, + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt index 7376c6d..7b5eaad 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt @@ -49,6 +49,7 @@ import coil.compose.rememberAsyncImagePainter import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.Transaction import de.chaosdorf.meteroid.ui.PriceBadge +import de.chaosdorf.meteroid.ui.theme.secondaryGradient import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime @@ -59,7 +60,8 @@ import java.time.format.FormatStyle @Composable fun TransactionListItem( transaction: Transaction, - drink: Drink? + drink: Drink?, + modifier: Modifier = Modifier ) { val timestamp = transaction.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) @@ -81,8 +83,8 @@ fun TransactionListItem( modifier = Modifier .size(48.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) .aspectRatio(1.0f) + .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient()) ) { if (drink != null) { val thumbPainter = rememberAsyncImagePainter( @@ -121,6 +123,7 @@ fun TransactionListItem( transaction.difference, modifier = Modifier.padding(horizontal = 8.dp) ) - } + }, + modifier = modifier, ) } 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 30620f8..7d208d9 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 @@ -27,48 +27,23 @@ package de.chaosdorf.meteroid.ui.transactions import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember @Composable fun TransactionListScreen( viewModel: TransactionViewModel, - topBar: @Composable () -> Unit, - bottomBar: @Composable () -> Unit, + contentPadding: PaddingValues = PaddingValues(), ) { - val account by viewModel.account.collectAsState() val transactions by viewModel.transactions.collectAsState() - val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(account) { - val offline = viewModel.checkOffline(account?.server) - snackbarHostState.currentSnackbarData?.dismiss() - if (offline) { - snackbarHostState.showSnackbar( - message = "Unable to connect to server", - duration = SnackbarDuration.Indefinite - ) - } - } - - Scaffold( - topBar = topBar, - bottomBar = bottomBar, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } - ) { paddingValues: PaddingValues -> - LazyColumn(contentPadding = paddingValues) { - items(transactions) { (transaction, drink) -> - TransactionListItem(transaction, drink) - } + LazyColumn(contentPadding = contentPadding) { + items( + transactions, + key = { "${it.transaction.serverId}-${it.transaction.transactionId}" }, + ) { (transaction, drink) -> + TransactionListItem(transaction, drink) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt new file mode 100644 index 0000000..ba11f91 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt @@ -0,0 +1,84 @@ +/* + * 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.users + +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.UserId +import de.chaosdorf.meteroid.model.ServerId +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/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt index 38218bd..ef1c017 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,13 +24,13 @@ package de.chaosdorf.meteroid.ui.users +import androidx.compose.foundation.ExperimentalFoundationApi +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.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +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.runtime.Composable import androidx.compose.runtime.collectAsState @@ -40,55 +40,59 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import de.chaosdorf.meteroid.ui.navigation.Routes +@OptIn(ExperimentalFoundationApi::class) @Composable fun UserListScreen( navController: NavController, viewModel: UserListViewModel, - topBar: @Composable () -> Unit, + contentPadding: PaddingValues = PaddingValues(), ) { val users by viewModel.users.collectAsState() val pinnedUsers by viewModel.pinnedUsers.collectAsState() - Scaffold( - topBar = topBar, - ) { paddingValues -> - LazyVerticalGrid( - GridCells.Adaptive(80.dp), - modifier = Modifier.padding(horizontal = 8.dp), - contentPadding = paddingValues - ) { - if (pinnedUsers.isNotEmpty()) { - item("pinned", span = { GridItemSpan(maxLineSpan) }) { + 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) { user -> - UserTile(user) { - navController.navigate(Routes.Home.purchase(user.serverId, user.userId)) - } + items( + pinnedUsers, + key = { "pinned-${it.userId}" }, + ) { user -> + UserListItem(user) { serverId, userId -> + navController.navigate(Routes.Home.purchase(serverId, userId)) + viewModel.selectUser(serverId, userId) } } + } - for (character in 'A'..'Z') { - val group = users.filter { it.name.startsWith(character, ignoreCase = true) } - if (group.isNotEmpty()) { - item(character.toString(), span = { GridItemSpan(maxLineSpan) }) { + 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) { user -> - UserTile(user) { - navController.navigate(Routes.Home.purchase(user.serverId, user.userId)) - viewModel.selectUser(user.serverId, user.userId) - } + items( + group, + key = { "${it.serverId}-${it.userId}" }, + ) { user -> + UserListItem(user) { serverId, userId -> + navController.navigate(Routes.Home.purchase(serverId, userId)) + viewModel.selectUser(serverId, 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 468071c..0823feb 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,6 +24,7 @@ package de.chaosdorf.meteroid.ui.users +import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -61,9 +62,10 @@ class UserListViewModel @Inject constructor( }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) fun selectUser(serverId: ServerId, userId: UserId) { + Log.i("UserListViewModel", "Updating AccountPreferences: $serverId $userId") viewModelScope.launch { - preferences.setServer(serverId) - preferences.setUser(userId) + + preferences.setUser(serverId, 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 73848b6..6d8e2cc 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 @@ -30,16 +30,10 @@ 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.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState 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.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration @@ -52,121 +46,102 @@ import java.time.format.TextStyle @Composable fun WrappedScreen( viewModel: WrappedViewModel, - topBar: @Composable () -> Unit, - bottomBar: @Composable () -> Unit, + contentPadding: PaddingValues = PaddingValues(), ) { - val account by viewModel.account.collectAsState() val slides by viewModel.slides.collectAsState() - val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(account) { - val offline = viewModel.checkOffline(account?.server) - snackbarHostState.currentSnackbarData?.dismiss() - if (offline) { - snackbarHostState.showSnackbar( - message = "Unable to connect to server", - duration = SnackbarDuration.Indefinite - ) - } - } + LazyColumn(contentPadding = contentPadding) { + items( + slides, + key = { "${it::class.qualifiedName}" }, + ) { 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 + ) - Scaffold( - topBar = topBar, - bottomBar = bottomBar, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } - ) { paddingValues: PaddingValues -> - LazyColumn(contentPadding = paddingValues) { - items(slides) { 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) + ) + } + ) - 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.") } - ) - - 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 } - }, - 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) - ) - } - ) + 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, - LocalConfiguration.current.locale - ) - }s at ${slide.hour} o'clock." - ) - }, - leadingContent = { - val painter = painterResource(R.drawable.wrapped_clock) + is WrappedSlide.MostActive -> + ListItem( + headlineContent = { + Text( + "You were most active on ${ + slide.weekday.getDisplayName( + TextStyle.FULL, + LocalConfiguration.current.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) - ) - } - ) - } + Image( + painter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(72.dp) + ) + } + ) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt new file mode 100644 index 0000000..f350dde --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt @@ -0,0 +1,33 @@ +/* + * 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.util + +import androidx.navigation.NavController + +fun NavController.popUpToRoot() { + while (popBackStack()) { + // repeat + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt new file mode 100644 index 0000000..d1f83fa --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt @@ -0,0 +1,51 @@ +/* + * 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.util + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.RenderVectorGroup +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.unit.Dp +import coil.compose.AsyncImagePainter +import coil.compose.rememberAsyncImagePainter + +@Composable +fun rememberAvatarPainter(url: String?, iconWidth: Dp, iconHeight: Dp, iconTint: Color): AsyncImagePainter { + val personPainter = rememberVectorPainter( + defaultHeight = iconHeight, + defaultWidth = iconWidth, + viewportWidth = Icons.Filled.Person.viewportWidth, + viewportHeight = Icons.Filled.Person.viewportHeight, + name = Icons.Filled.Person.name, + tintColor = iconTint, + tintBlendMode = Icons.Filled.Person.tintBlendMode, + autoMirror = Icons.Filled.Person.autoMirror, + content = { _, _ -> RenderVectorGroup(group = Icons.Filled.Person.root) } + ) + return rememberAsyncImagePainter(url, fallback = personPainter) +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt index dae71d6..455a90c 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt @@ -59,6 +59,9 @@ interface PinnedUserRepository { @Query("SELECT userId FROM PinnedUser WHERE serverId = :serverId") fun getAllFlow(serverId: ServerId): Flow<List<UserId>> + @Query("SELECT User.* FROM PinnedUser JOIN User on PinnedUser.userId = User.userId AND PinnedUser.serverId = User.serverId") + fun getPinnedUsersFlow(): Flow<List<User>> + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(user: PinnedUser) -- GitLab