Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • justJanne/meteroid
1 result
Select Git revision
Show changes
Showing
with 611 additions and 495 deletions
......@@ -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
)
)
}
......@@ -25,81 +25,37 @@
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.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
contentPadding: PaddingValues = PaddingValues(),
) {
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()
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 = { 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
)
},
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)
......@@ -115,13 +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, onBack) }
}
items(
drinks,
key = { "${it.serverId}-${it.drinkId}" },
) { drink ->
DrinkTile(drink) {
viewModel.purchase(it, navController::navigateUp)
}
}
}
......
......@@ -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,12 +86,10 @@ class DrinkListViewModel @Inject constructor(
}
}
fun togglePin() {
fun sync() {
account.value?.let { account ->
account.user?.let { user ->
viewModelScope.launch {
accountProvider.togglePin(account.server.serverId, user.userId)
}
syncManager.sync(account.server, account.user, incremental = true)
}
}
}
......
......@@ -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,11 +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(
......@@ -73,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))
......@@ -88,7 +90,7 @@ fun DrinkTile(
modifier = Modifier
.aspectRatio(1.0f)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
.background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient())
)
PriceBadge(
item.price,
......@@ -109,8 +111,13 @@ fun DrinkTile(
)
Spacer(Modifier.height(4.dp))
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
val unitPrice =
if (item.volume <= BigDecimal.ZERO) null
else item.price / item.volume
Text(
String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume),
if (unitPrice == null) String.format("%.02fl", item.volume)
else String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
......
......@@ -24,86 +24,33 @@
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.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
contentPadding: PaddingValues = PaddingValues(),
) {
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() }
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 = { 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
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { paddingValues: PaddingValues ->
Column {
LazyVerticalGrid(
GridCells.Adaptive(120.dp),
modifier = Modifier.padding(paddingValues),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
GridCells.Adaptive(104.dp),
contentPadding = contentPadding,
modifier = Modifier.padding(horizontal = 8.dp),
) {
items(viewModel.money) { monetaryAmount ->
MoneyTile(monetaryAmount) { viewModel.deposit(it, onBack) }
}
items(
viewModel.money,
key = { "${it.ordinal}" },
) { monetaryAmount ->
MoneyTile(monetaryAmount) {
viewModel.deposit(it, navController::navigateUp)
}
}
}
......
......@@ -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)
}
}
accountProvider.togglePin(serverId, userId)
}
}
......
......@@ -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) }
......
......@@ -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);
}
......@@ -25,45 +25,77 @@
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
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.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
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) null
else HomeSections.entries.find {
it.route == currentDestination?.route
}
if (activeRoute != null) {
NavigationBar(
contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted
) {
NavigationBar {
for (route in HomeSections.entries) {
if (wrappedEnabled || route != HomeSections.WRAPPED) {
NavigationBarItem(
icon = {
Icon(
if (route == currentRoute) route.iconActive else route.icon,
contentDescription = route.title
if (route == activeRoute) route.iconActive else route.icon,
contentDescription = route.title,
tint = MaterialTheme.colorScheme.onPrimaryContainerTinted
)
},
label = { Text(route.title) },
selected = route == currentRoute,
label = { Text(route.title, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) },
selected = route == activeRoute,
onClick = {
navigateTo(
route.route,
NavOptions.Builder()
.setPopUpTo(Routes.Home.Root, true)
.build()
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()
)
)
},
modifier = modifier,
enabled = route != HomeSections.HISTORY || historyEnabled
enabled = when (route) {
HomeSections.PURCHASE,
HomeSections.DEPOSIT -> true
HomeSections.HISTORY,
HomeSections.WRAPPED -> historyEnabled
}
)
}
}
}
}
}
......@@ -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
}
}
......@@ -24,103 +24,190 @@
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.material.icons.twotone.PushPin
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.NavOptions
import coil.compose.AsyncImage
import de.chaosdorf.meteroid.model.AccountInfo
import androidx.navigation.NavController
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
import okhttp3.internal.toCanonicalHost
@Composable
fun MeteroidTopBar(
account: AccountInfo?,
onNavigate: (String, NavOptions) -> Unit,
onTogglePin: () -> Unit,
navController: NavController,
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,
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,
contentDescription = "User List",
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)
.background(MaterialTheme.colorScheme.tertiary)
)
}
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.align(Alignment.CenterVertically)) {
if (account != null) {
if (account.user != null) {
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1.0f, fill = true)
) {
when {
user != null && server != null -> {
Text(
account.user!!.name,
fontWeight = FontWeight.SemiBold
user!!.name,
fontWeight = FontWeight.SemiBold,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
Text(
account.server.url.toHttpUrl().host,
server!!.url.toHttpUrl().host,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
fontWeight = FontWeight.Medium
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
}
server != null && server!!.name != null -> {
Text(
server!!.name!!,
fontWeight = FontWeight.SemiBold,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
} else {
Text(
account.server.url.toHttpUrl().host,
fontWeight = FontWeight.SemiBold
server!!.url.toHttpUrl().host,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
}
} else {
server != null -> {
Text(
server!!.url.toHttpUrl().host,
fontWeight = FontWeight.SemiBold,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
}
else -> {
Text(
"Meteroid",
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
}
}
Spacer(
Modifier
.weight(1.0f)
.width(16.dp))
IconButton(onClick = onTogglePin) {
}
Spacer(Modifier.width(16.dp))
user?.let { user ->
IconButton(onClick = {
viewModel.togglePin(user.serverId, user.userId)
}) {
Icon(
if (account?.pinned == true) Icons.Filled.PushPin
if (pinned == true) Icons.Filled.PushPin
else Icons.Outlined.PushPin,
contentDescription = null
)
}
Spacer(Modifier.width(8.dp))
account?.user?.let { user ->
PriceBadge(
user.balance,
modifier = Modifier
......
......@@ -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())
}
}
......@@ -24,92 +24,52 @@
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.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
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.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,
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.mipmap.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
)
}
}
},
navigationIcon = {
if (!isFirst) {
IconButton(onClick = { onBack() }) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
}
},
modifier = Modifier.shadow(4.dp)
)
},
) { paddingValues ->
Column(
Modifier
.padding(paddingValues)
.padding(16.dp, 8.dp)) {
.padding(contentPadding)
.padding(16.dp, 8.dp)
) {
TextField(
label = { Text("Server URL") },
value = url,
......@@ -146,12 +106,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")
......@@ -161,4 +122,3 @@ fun AddServerScreen(
}
}
}
}
......@@ -24,74 +24,33 @@
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.PaddingValues
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,
contentPadding: PaddingValues = PaddingValues(),
) {
val servers by viewModel.servers.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Row {
Image(
painterResource(R.mipmap.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 {
LazyColumn(modifier = Modifier.padding(paddingValues)) {
LazyColumn(contentPadding = contentPadding) {
items(servers) { server ->
ListItem(
headlineContent = { Text(server.name ?: server.url) },
......@@ -104,16 +63,19 @@ 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)
}
)
}
}
}
......@@ -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)
}
}
}
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)
......@@ -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
}
}
......
/*
* 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,
)
}
......@@ -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,
)
}
......@@ -27,63 +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
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
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 = { 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
)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { paddingValues: PaddingValues ->
LazyColumn(contentPadding = paddingValues) {
items(transactions) { (transaction, drink) ->
LazyColumn(contentPadding = contentPadding) {
items(
transactions,
key = { "${it.transaction.serverId}-${it.transaction.transactionId}" },
) { (transaction, drink) ->
TransactionListItem(transaction, drink)
}
}
}
}