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

wip: progress

parent ade48fa5
Branches
No related tags found
No related merge requests found
Showing
with 543 additions and 93 deletions
......@@ -31,8 +31,6 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
......
......@@ -35,6 +35,7 @@ import de.chaosdorf.mete.model.MeteApiFactory
import de.chaosdorf.mete.v1.MeteApiV1Factory
import de.chaosdorf.meteroid.MeteroidDatabase
import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.PinnedUserRepository
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.TransactionRepository
import de.chaosdorf.meteroid.model.UserRepository
......@@ -62,6 +63,11 @@ object DatabaseModule {
database: MeteroidDatabase
): UserRepository = database.users()
@Provides
fun providePinnedUserRepository(
database: MeteroidDatabase
): PinnedUserRepository = database.pinnedUsers()
@Provides
fun provideTransactionRepository(
database: MeteroidDatabase
......
......@@ -24,20 +24,25 @@
package de.chaosdorf.meteroid.sync
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.AccountInfo
import de.chaosdorf.meteroid.model.PinnedUser
import de.chaosdorf.meteroid.model.PinnedUserRepository
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
class AccountProvider @Inject constructor(
accountPreferences: AccountPreferences,
serverRepository: ServerRepository,
userRepository: UserRepository,
private val pinnedUserRepository: PinnedUserRepository,
) {
val account: Flow<AccountInfo?> =
accountPreferences.state.flatMapLatest { preferences ->
......@@ -48,12 +53,24 @@ class AccountProvider @Inject constructor(
if (server == null) {
flowOf(null)
} else if (preferences.user == null) {
flowOf(AccountInfo(server, null))
flowOf(AccountInfo(server, null, false))
} else {
userRepository.getFlow(server.serverId, preferences.user)
.mapLatest { user -> AccountInfo(server, user) }
combine(
userRepository.getFlow(server.serverId, preferences.user),
pinnedUserRepository.isPinnedFlow(server.serverId, preferences.user)
) { user, pinned ->
AccountInfo(server, user, pinned)
}
}
}
}
}
suspend fun togglePin(serverId: ServerId, userId: UserId) {
if (pinnedUserRepository.isPinned(serverId, userId)) {
pinnedUserRepository.delete(serverId, userId)
} else {
pinnedUserRepository.save(PinnedUser(serverId, userId))
}
}
}
......@@ -73,6 +73,7 @@ abstract class SyncHandler<Context, Entry, Key> {
val loadedKeys = loadedEntries.map(::entryToKey).toSet()
val removedKeys = storedKeys - loadedKeys
for (removedKey in removedKeys) {
Log.e("SyncHandler", "deleting: $removedKey")
delete(removedKey)
}
for (loadedEntry in loadedEntries) {
......
......@@ -22,16 +22,13 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui
package de.chaosdorf.meteroid.sync
import de.chaosdorf.mete.model.MeteApiFactory
import de.chaosdorf.meteroid.model.AccountInfo
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.sync.DrinkSyncHandler
import de.chaosdorf.meteroid.sync.TransactionSyncHandler
import de.chaosdorf.meteroid.sync.UserSyncHandler
import java.math.BigDecimal
import javax.inject.Inject
......
......@@ -116,6 +116,8 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
composable(Routes.Servers.Add) { _ ->
AddServerScreen(
hiltViewModel(),
isFirst = initState == AppViewModel.InitState.CREATE_SERVER,
onBack = { navController.navigate(Routes.Servers.List) },
onAdd = { navController.navigate(Routes.Servers.List) }
)
}
......
......@@ -33,9 +33,7 @@ import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.sync.DrinkSyncHandler
import de.chaosdorf.meteroid.sync.TransactionSyncHandler
import de.chaosdorf.meteroid.sync.UserSyncHandler
import de.chaosdorf.meteroid.sync.SyncManager
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
......
......@@ -37,12 +37,14 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
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
@OptIn(ExperimentalLayoutApi::class)
@Composable
......@@ -50,12 +52,20 @@ fun DrinkListScreen(
viewModel: DrinkListViewModel,
onNavigate: (String, NavOptions) -> Unit
) {
val onBack = remember {
{
onNavigate(
Routes.Users.List,
NavOptions.Builder().setPopUpTo(Routes.Users.List, false).build()
)
}
}
val account by viewModel.account.collectAsState()
val drinks by viewModel.drinks.collectAsState()
val filters by viewModel.filters.collectAsState()
Scaffold(
topBar = { MeteroidTopBar(account, onNavigate) },
topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) },
bottomBar = {
MeteroidBottomBar(
currentRoute = HomeSections.PURCHASE,
......@@ -85,7 +95,7 @@ fun DrinkListScreen(
modifier = Modifier.padding(horizontal = 8.dp)
) {
items(drinks) { drink ->
DrinkTile(drink, viewModel::purchase)
DrinkTile(drink) { viewModel.purchase(it, onBack) }
}
}
}
......
......@@ -32,7 +32,7 @@ import de.chaosdorf.meteroid.model.AccountInfo
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.ui.SyncManager
import de.chaosdorf.meteroid.sync.SyncManager
import de.chaosdorf.meteroid.util.update
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
......@@ -45,7 +45,7 @@ import javax.inject.Inject
@HiltViewModel
class DrinkListViewModel @Inject constructor(
accountProvider: AccountProvider,
private val accountProvider: AccountProvider,
repository: DrinkRepository,
private val syncManager: SyncManager,
private val savedStateHandle: SavedStateHandle
......@@ -76,10 +76,23 @@ class DrinkListViewModel @Inject constructor(
}
}
fun purchase(item: Drink) {
fun purchase(item: Drink, onBack: () -> Unit) {
account.value?.let { account ->
viewModelScope.launch {
syncManager.purchase(account, item)
if (!account.pinned) {
onBack()
}
}
}
}
fun togglePin() {
account.value?.let { account ->
account.user?.let { user ->
viewModelScope.launch {
accountProvider.togglePin(account.server.serverId, user.userId)
}
}
}
}
......
......@@ -35,22 +35,32 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
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
@Composable
fun MoneyListScreen(
viewModel: MoneyListViewModel,
onNavigate: (String, NavOptions) -> Unit
) {
val onBack = remember {
{
onNavigate(
Routes.Users.List,
NavOptions.Builder().setPopUpTo(Routes.Users.List, false).build()
)
}
}
val account by viewModel.account.collectAsState()
Scaffold(
topBar = { MeteroidTopBar(account, onNavigate) },
topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) },
bottomBar = {
MeteroidBottomBar(
currentRoute = HomeSections.DEPOSIT,
......@@ -67,7 +77,7 @@ fun MoneyListScreen(
horizontalArrangement = Arrangement.SpaceBetween,
) {
items(viewModel.money) { monetaryAmount ->
MoneyTile(monetaryAmount, viewModel::deposit)
MoneyTile(monetaryAmount) { viewModel.deposit(it, onBack) }
}
}
}
......
......@@ -28,11 +28,10 @@ import androidx.annotation.DrawableRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.MeteApiFactory
import de.chaosdorf.meteroid.R
import de.chaosdorf.meteroid.model.AccountInfo
import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.ui.SyncManager
import de.chaosdorf.meteroid.sync.SyncManager
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
......@@ -52,7 +51,7 @@ enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) {
@HiltViewModel
class MoneyListViewModel @Inject constructor(
accountProvider: AccountProvider,
private val accountProvider: AccountProvider,
private val syncManager: SyncManager
) : ViewModel() {
val account: StateFlow<AccountInfo?> = accountProvider.account
......@@ -60,10 +59,23 @@ class MoneyListViewModel @Inject constructor(
val money: List<MonetaryAmount> = MonetaryAmount.entries
fun deposit(item: MonetaryAmount) {
fun deposit(item: MonetaryAmount, onBack: () -> Unit) {
account.value?.let { account ->
viewModelScope.launch {
syncManager.deposit(account, item.amount)
if (!account.pinned) {
onBack()
}
}
}
}
fun togglePin() {
account.value?.let { account ->
account.user?.let { user ->
viewModelScope.launch {
accountProvider.togglePin(account.server.serverId, user.userId)
}
}
}
}
......
......@@ -33,6 +33,12 @@ 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.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
......@@ -53,7 +59,8 @@ import okhttp3.internal.toCanonicalHost
@Composable
fun MeteroidTopBar(
account: AccountInfo?,
onNavigate: (String, NavOptions) -> Unit
onNavigate: (String, NavOptions) -> Unit,
onTogglePin: () -> Unit,
) {
Surface(
modifier = Modifier.padding(8.dp),
......@@ -67,7 +74,7 @@ fun MeteroidTopBar(
) {
Row(modifier = Modifier.padding(8.dp)) {
AsyncImage(
account?.user?.gravatarUrl(),
account?.user?.gravatarUrl,
contentDescription = "User List",
contentScale = ContentScale.Crop,
modifier = Modifier
......@@ -105,6 +112,14 @@ fun MeteroidTopBar(
Modifier
.weight(1.0f)
.width(16.dp))
IconButton(onClick = onTogglePin) {
Icon(
if (account?.pinned == true) Icons.Filled.PushPin
else Icons.Outlined.PushPin,
contentDescription = null
)
}
Spacer(Modifier.width(8.dp))
account?.user?.let { user ->
PriceBadge(
user.balance,
......
......@@ -24,47 +24,141 @@
package de.chaosdorf.meteroid.ui.servers
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
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 coil.compose.AsyncImage
import de.chaosdorf.meteroid.R
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
@Composable
fun AddServerScreen(
viewModel: AddServerViewModel = viewModel(),
isFirst: Boolean = false,
onBack: () -> Unit = {},
onAdd: () -> Unit = {}
) {
val scope = rememberCoroutineScope()
val url by viewModel.url.collectAsState()
val server by viewModel.server.collectAsState()
Column {
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)) {
TextField(
label = { Text("Server URL") },
value = url,
onValueChange = { viewModel.url.value = it }
onValueChange = { viewModel.url.value = it },
modifier = Modifier.fillMaxWidth()
)
server?.let { server ->
Card(
modifier = Modifier.padding(vertical = 8.dp)
) {
Row(modifier = Modifier.padding(16.dp, 8.dp)) {
AsyncImage(
server.logoUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(48.dp)
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.align(Alignment.CenterVertically)) {
Text(
server.name!!,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium
)
Text(
server.url.toHttpUrl().host,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
fontWeight = FontWeight.Medium,
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(
Modifier
.width(16.dp)
.weight(1.0f))
Button(onClick = {
IconButton(onClick = {
scope.launch {
viewModel.addServer()
onAdd()
}
}) {
Text("Add Server")
Icon(Icons.Default.Add, contentDescription = "Add Server")
}
}
}
}
server?.let { server ->
Text(server.url)
Text(server.name ?: "null1")
AsyncImage(model = server.logoUrl, contentDescription = null)
}
}
}
......@@ -24,24 +24,37 @@
package de.chaosdorf.meteroid.ui.servers
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import de.chaosdorf.meteroid.R
import de.chaosdorf.meteroid.model.ServerId
import okhttp3.HttpUrl.Companion.toHttpUrl
@Preview
@Composable
......@@ -55,7 +68,24 @@ fun ServerListScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text("Meteroid") },
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)
)
}
......@@ -65,7 +95,15 @@ fun ServerListScreen(
items(servers) { server ->
ListItem(
headlineContent = { Text(server.name ?: server.url) },
supportingContent = { if (server.name != null) Text(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 { onSelect(server.serverId) }
)
}
......
......@@ -49,7 +49,7 @@ fun TransactionListScreen(
val transactions by viewModel.transactions.collectAsState()
Scaffold(
topBar = { MeteroidTopBar(account, onNavigate) },
topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) },
bottomBar = {
MeteroidBottomBar(
currentRoute = HomeSections.HISTORY,
......
......@@ -40,13 +40,14 @@ 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 java.math.BigDecimal
import javax.inject.Inject
import kotlin.time.Duration.Companion.minutes
@HiltViewModel
class TransactionViewModel @Inject constructor(
accountProvider: AccountProvider,
private val accountProvider: AccountProvider,
repository: TransactionRepository,
drinkRepository: DrinkRepository
) : ViewModel() {
......@@ -74,6 +75,16 @@ class TransactionViewModel @Inject constructor(
list.mergeAdjecentDeposits()
.filter { it.drink != null || it.transaction.difference != BigDecimal.ZERO }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun togglePin() {
account.value?.let { account ->
account.user?.let { user ->
viewModelScope.launch {
accountProvider.togglePin(account.server.serverId, user.userId)
}
}
}
}
}
fun List<TransactionInfo>.mergeAdjecentDeposits(): List<TransactionInfo> {
......
......@@ -24,15 +24,21 @@
package de.chaosdorf.meteroid.ui.users
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ListItem
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
......@@ -40,14 +46,16 @@ 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.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import de.chaosdorf.mete.model.UserId
import okhttp3.HttpUrl.Companion.toHttpUrl
@Composable
fun UserListScreen(
......@@ -58,27 +66,55 @@ fun UserListScreen(
) {
val server by viewModel.account.collectAsState()
val users by viewModel.users.collectAsState()
val pinnedUsers by viewModel.pinnedUsers.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
server?.server?.name
?: "Meteroid"
)
},
navigationIcon = {
Row {
AsyncImage(
server?.server?.logoUrl,
contentDescription = "User List",
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.border(1.dp, Color.White, CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
modifier = Modifier.size(48.dp)
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.align(Alignment.CenterVertically)) {
if (server?.server != null) {
if (server?.server?.name != null) {
Text(
server!!.server.name!!,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium
)
Text(
server!!.server.url.toHttpUrl().host,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
fontWeight = FontWeight.Medium,
style = MaterialTheme.typography.bodyMedium
)
} else {
Text(
server!!.server.url,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium
)
}
} else {
Text(
"Meteroid",
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
},
navigationIcon = {
IconButton(onClick = { onBack() }) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
},
modifier = Modifier
.shadow(4.dp)
......@@ -86,20 +122,41 @@ fun UserListScreen(
)
}
) { paddingValues ->
LazyColumn(modifier = Modifier.padding(paddingValues)) {
items(users) { user ->
ListItem(
headlineContent = { Text(user.name) },
supportingContent = { Text(user.email) },
modifier = Modifier.clickable { onSelect(user.userId) }
LazyVerticalGrid(
GridCells.Adaptive(80.dp),
modifier = Modifier.padding(horizontal = 8.dp),
contentPadding = paddingValues
) {
if (pinnedUsers.isNotEmpty()) {
item("pinned", span = { GridItemSpan(maxLineSpan) }) {
Text(
"Pinned",
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 4.dp)
)
}
item {
ListItem(
headlineContent = { Text("Add User") },
modifier = Modifier.clickable { onAdd() }
items(pinnedUsers) { user ->
UserTile(user, onSelect)
}
}
for (character in 'A'..'Z') {
val group = users.filter { it.name.startsWith(character, ignoreCase = true) }
if (group.isNotEmpty()) {
item(character.toString(), span = { GridItemSpan(maxLineSpan) }) {
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, onSelect)
}
}
}
}
}
}
......@@ -28,21 +28,26 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.meteroid.model.AccountInfo
import de.chaosdorf.meteroid.model.PinnedUserRepository
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.sync.UserSyncHandler
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class UserListViewModel @Inject constructor(
accountProvider: AccountProvider,
repository: UserRepository
userRepository: UserRepository,
pinnedUserRepository: PinnedUserRepository,
) : ViewModel() {
val account: StateFlow<AccountInfo?> = accountProvider.account
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
......@@ -50,7 +55,19 @@ class UserListViewModel @Inject constructor(
val users: StateFlow<List<User>> = accountProvider.account
.flatMapLatest { account ->
account?.let { (server, _) ->
repository.getAllFlow(server.serverId)
userRepository.getAllFlow(server.serverId)
} ?: flowOf(emptyList())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val pinnedUsers = accountProvider.account
.flatMapLatest { account ->
account?.let { (server, _) ->
combine(
userRepository.getAllFlow(server.serverId),
pinnedUserRepository.getAllFlow(server.serverId)
) { users, pinned ->
users.filter { pinned.contains(it.userId) }
}
} ?: flowOf(emptyList())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
}
/*
* 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.Column
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
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.sample.SampleDrinkProvider
import de.chaosdorf.meteroid.ui.PriceBadge
@Composable
fun UserTile(
item: User,
onSelect: (UserId) -> Unit = {}
) {
val avatarPainter = rememberAsyncImagePainter(
item.gravatarUrl
)
Column(
modifier = Modifier
.height(IntrinsicSize.Max)
.alpha(if (item.active) 1.0f else 0.67f)
.clip(RoundedCornerShape(8.dp))
.clickable { onSelect(item.userId) }
.padding(8.dp)
) {
Box {
Image(
avatarPainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.aspectRatio(1.0f)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
)
}
Spacer(Modifier.height(4.dp))
Text(
item.name,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.labelLarge,
)
}
}
......@@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "7aca7cdc33cbadb81643b32c5838f2a9",
"identityHash": "f794acceadd9ed28da1b218972b5e530",
"entities": [
{
"tableName": "Drink",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` TEXT NOT NULL, `caffeine` INTEGER, `price` TEXT NOT NULL, `logoUrl` TEXT NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` TEXT NOT NULL, `caffeine` INTEGER, `price` TEXT NOT NULL, `logoUrl` TEXT NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
......@@ -68,7 +68,7 @@
"foreignKeys": [
{
"table": "Server",
"onDelete": "CASCADE",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
......@@ -119,7 +119,7 @@
},
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` TEXT NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` TEXT NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
......@@ -181,7 +181,46 @@
"foreignKeys": [
{
"table": "Server",
"onDelete": "CASCADE",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
],
"referencedColumns": [
"serverId"
]
}
]
},
{
"tableName": "PinnedUser",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"serverId",
"userId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "Server",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
......@@ -189,12 +228,25 @@
"referencedColumns": [
"serverId"
]
},
{
"table": "User",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"serverId",
"userId"
],
"referencedColumns": [
"serverId",
"userId"
]
}
]
},
{
"tableName": "Transaction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `transactionId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `transactionId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `transactionId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `transactionId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
......@@ -265,7 +317,7 @@
"foreignKeys": [
{
"table": "Server",
"onDelete": "CASCADE",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
......@@ -276,7 +328,7 @@
},
{
"table": "User",
"onDelete": "CASCADE",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"serverId",
......@@ -306,7 +358,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7aca7cdc33cbadb81643b32c5838f2a9')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f794acceadd9ed28da1b218972b5e530')"
]
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment