From c049be70c21c44ada83903e9928c4d9530625ef5 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <mail@justjanne.de> Date: Mon, 13 Nov 2023 02:42:58 +0100 Subject: [PATCH] wip: progress --- app/build.gradle.kts | 2 - .../chaosdorf/meteroid/di/DatabaseModule.kt | 6 + .../meteroid/sync/AccountProvider.kt | 25 +- .../de/chaosdorf/meteroid/sync/SyncHandler.kt | 1 + .../meteroid/{ui => sync}/SyncManager.kt | 5 +- .../de/chaosdorf/meteroid/ui/AppRouter.kt | 2 + .../de/chaosdorf/meteroid/ui/AppViewModel.kt | 4 +- .../meteroid/ui/drinks/DrinkListScreen.kt | 14 +- .../meteroid/ui/drinks/DrinkListViewModel.kt | 19 +- .../meteroid/ui/money/MoneyListScreen.kt | 14 +- .../meteroid/ui/money/MoneyListViewModel.kt | 20 +- .../meteroid/ui/navigation/MeteroidTopBar.kt | 19 +- .../meteroid/ui/servers/AddServerScreen.kt | 130 ++++++- .../meteroid/ui/servers/ServerListScreen.kt | 42 +- .../ui/transactions/PurchaseListScreen.kt | 2 +- .../ui/transactions/PurchaseViewModel.kt | 13 +- .../meteroid/ui/users/UserListScreen.kt | 125 ++++-- .../meteroid/ui/users/UserListViewModel.kt | 21 +- .../chaosdorf/meteroid/ui/users/UserTile.kt | 102 +++++ .../1.json | 70 +++- .../2.json | 364 ++++++++++++++++++ .../3.json | 364 ++++++++++++++++++ .../de/chaosdorf/meteroid/MeteroidDatabase.kt | 5 + .../chaosdorf/meteroid/model/AccountInfo.kt | 3 +- .../de/chaosdorf/meteroid/model/Drink.kt | 2 +- .../de/chaosdorf/meteroid/model/PinnedUser.kt | 70 ++++ .../chaosdorf/meteroid/model/Transaction.kt | 8 +- .../de/chaosdorf/meteroid/model/User.kt | 20 +- 28 files changed, 1362 insertions(+), 110 deletions(-) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ui => sync}/SyncManager.kt (93%) create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt create mode 100644 persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json create mode 100644 persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json create mode 100644 persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 64e87db..e9c4411 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,8 +31,6 @@ android { buildTypes { getByName("release") { - isMinifyEnabled = true - isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android.txt"), diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt index c531e4c..fc44d7d 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt @@ -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 diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt index 1fd9259..66c5b73 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt @@ -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)) + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncHandler.kt index 6bce340..b7a6199 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncHandler.kt @@ -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) { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt similarity index 93% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt index 48276de..cda175b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt @@ -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 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 6b2e654..f7237af 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt @@ -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) } ) } 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 751902f..853744c 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt @@ -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 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 b872aef..b699100 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 @@ -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) } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt index 045d375..162323d 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt @@ -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) + } } } } 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 78b8d66..42d4837 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 @@ -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) } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt index 7ba2e82..b568029 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt @@ -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) + } } } } 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 3a5d95e..5cc575b 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 @@ -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, 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 be30261..1860019 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,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 { - TextField( - label = { Text("Server URL") }, - value = url, - onValueChange = { viewModel.url.value = it } - ) + 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 }, + modifier = Modifier.fillMaxWidth() + ) - Button(onClick = { - scope.launch { - viewModel.addServer() - onAdd() - } - }) { - Text("Add Server") - } + 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 + ) + } - server?.let { server -> - Text(server.url) - Text(server.name ?: "null1") - AsyncImage(model = server.logoUrl, contentDescription = null) + Spacer( + Modifier + .width(16.dp) + .weight(1.0f)) + + IconButton(onClick = { + scope.launch { + viewModel.addServer() + onAdd() + } + }) { + 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 ff0c99b..937756b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt @@ -24,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) } ) } 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 d6ceedb..73ecb0c 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 @@ -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, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt index 8c053ee..14a8853 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt @@ -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> { 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 a6890ee..56aba29 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,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" - ) + Row { + AsyncImage( + server?.server?.logoUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(48.dp) + ) + Spacer(Modifier.width(16.dp)) + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + if (server?.server != null) { + if (server?.server?.name != null) { + Text( + server!!.server.name!!, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyMedium + ) + Text( + server!!.server.url.toHttpUrl().host, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), + fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.bodyMedium + ) + } else { + Text( + server!!.server.url, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyMedium + ) + } + } else { + Text( + "Meteroid", + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } }, navigationIcon = { - AsyncImage( - server?.server?.logoUrl, - contentDescription = "User List", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .border(1.dp, Color.White, CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) - ) + IconButton(onClick = { onBack() }) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) + } }, modifier = Modifier .shadow(4.dp) @@ -86,19 +122,40 @@ 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) + ) + } + + items(pinnedUsers) { user -> + UserTile(user, onSelect) + } } - item { - ListItem( - headlineContent = { Text("Add User") }, - modifier = Modifier.clickable { onAdd() } - ) + + 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) + } + } } } } 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 74029fd..112dfe6 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 @@ -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()) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt new file mode 100644 index 0000000..16a5f7c --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt @@ -0,0 +1,102 @@ +/* + * 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, + ) + } +} diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json index 9efb6d6..315971c 100644 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json @@ -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 diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json new file mode 100644 index 0000000..9432b0b --- /dev/null +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json @@ -0,0 +1,364 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "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 NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "drinkId", + "columnName": "drinkId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caffeine", + "columnName": "caffeine", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "drinkId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Server", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "serverId" + ] + } + ] + }, + { + "tableName": "Server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`serverId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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 NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "audit", + "columnName": "audit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Server", + "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" + ], + "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 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", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "drinkId", + "columnName": "drinkId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "difference", + "columnName": "difference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "transactionId" + ] + }, + "indices": [ + { + "name": "index_Transaction_serverId_userId", + "unique": false, + "columnNames": [ + "serverId", + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)" + }, + { + "name": "index_Transaction_serverId_drinkId", + "unique": false, + "columnNames": [ + "serverId", + "drinkId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)" + } + ], + "foreignKeys": [ + { + "table": "Server", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "serverId" + ] + }, + { + "table": "User", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId", + "userId" + ], + "referencedColumns": [ + "serverId", + "userId" + ] + }, + { + "table": "Drink", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId", + "drinkId" + ], + "referencedColumns": [ + "serverId", + "drinkId" + ] + } + ] + } + ], + "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, 'f794acceadd9ed28da1b218972b5e530')" + ] + } +} \ No newline at end of file diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json new file mode 100644 index 0000000..fdcae6c --- /dev/null +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json @@ -0,0 +1,364 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "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 NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "drinkId", + "columnName": "drinkId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caffeine", + "columnName": "caffeine", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "drinkId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Server", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "serverId" + ] + } + ] + }, + { + "tableName": "Server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`serverId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoUrl", + "columnName": "logoUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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 NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "audit", + "columnName": "audit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "userId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Server", + "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" + ], + "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 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", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "transactionId", + "columnName": "transactionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "drinkId", + "columnName": "drinkId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "difference", + "columnName": "difference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "transactionId" + ] + }, + "indices": [ + { + "name": "index_Transaction_serverId_userId", + "unique": false, + "columnNames": [ + "serverId", + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)" + }, + { + "name": "index_Transaction_serverId_drinkId", + "unique": false, + "columnNames": [ + "serverId", + "drinkId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)" + } + ], + "foreignKeys": [ + { + "table": "Server", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "serverId" + ] + }, + { + "table": "User", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId", + "userId" + ], + "referencedColumns": [ + "serverId", + "userId" + ] + }, + { + "table": "Drink", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "serverId", + "drinkId" + ], + "referencedColumns": [ + "serverId", + "drinkId" + ] + } + ] + } + ], + "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, 'f794acceadd9ed28da1b218972b5e530')" + ] + } +} \ No newline at end of file diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt index 8cf5fac..63251ae 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt @@ -24,11 +24,14 @@ package de.chaosdorf.meteroid +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository +import de.chaosdorf.meteroid.model.PinnedUser +import de.chaosdorf.meteroid.model.PinnedUserRepository import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.model.Transaction @@ -44,6 +47,7 @@ import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter Drink::class, Server::class, User::class, + PinnedUser::class, Transaction::class ], autoMigrations = [], @@ -58,5 +62,6 @@ abstract class MeteroidDatabase : RoomDatabase() { abstract fun drinks(): DrinkRepository abstract fun server(): ServerRepository abstract fun users(): UserRepository + abstract fun pinnedUsers(): PinnedUserRepository abstract fun transactions(): TransactionRepository } diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt index 581d020..cd9af86 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/AccountInfo.kt @@ -26,5 +26,6 @@ package de.chaosdorf.meteroid.model data class AccountInfo( val server: Server, - val user: User? + val user: User?, + val pinned: Boolean ) diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt index 6ea4886..fe82de5 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt @@ -39,7 +39,7 @@ import java.net.URI @Entity( primaryKeys = ["serverId", "drinkId"], foreignKeys = [ - ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE) + ForeignKey(Server::class, ["serverId"], ["serverId"]) ] ) data class Drink( diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt new file mode 100644 index 0000000..dae71d6 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt @@ -0,0 +1,70 @@ +/* + * 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.model + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import de.chaosdorf.mete.model.UserId +import kotlinx.coroutines.flow.Flow + +@Entity( + primaryKeys = ["serverId", "userId"], + foreignKeys = [ + ForeignKey(Server::class, ["serverId"], ["serverId"]), + ForeignKey(User::class, ["serverId", "userId"], ["serverId", "userId"]) + ] +) +data class PinnedUser( + val serverId: ServerId, + val userId: UserId +) + +@Dao +interface PinnedUserRepository { + @Query("SELECT 1 FROM PinnedUser WHERE serverId = :serverId AND userId = :userId LIMIT 1") + suspend fun isPinned(serverId: ServerId, userId: UserId): Boolean + + @Query("SELECT 1 FROM PinnedUser WHERE serverId = :serverId AND userId = :userId LIMIT 1") + fun isPinnedFlow(serverId: ServerId, userId: UserId): Flow<Boolean> + + @Query("SELECT userId FROM PinnedUser WHERE serverId = :serverId") + suspend fun getAll(serverId: ServerId): List<UserId> + + @Query("SELECT userId FROM PinnedUser WHERE serverId = :serverId") + fun getAllFlow(serverId: ServerId): Flow<List<UserId>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(user: PinnedUser) + + @Query("DELETE FROM PinnedUser WHERE serverId = :serverId AND userId = :userId") + suspend fun delete(serverId: ServerId, userId: UserId) + + @Query("DELETE FROM PinnedUser WHERE serverId = :serverId") + suspend fun deleteAll(serverId: ServerId) +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt index c564bbd..ac66251 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt @@ -42,18 +42,16 @@ import java.math.BigDecimal @Entity( primaryKeys = ["serverId", "transactionId"], foreignKeys = [ - ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE), + ForeignKey(Server::class, ["serverId"], ["serverId"]), ForeignKey( User::class, ["serverId", "userId"], - ["serverId", "userId"], - onDelete = ForeignKey.CASCADE + ["serverId", "userId"] ), ForeignKey( Drink::class, ["serverId", "drinkId"], - ["serverId", "drinkId"], - onDelete = ForeignKey.NO_ACTION + ["serverId", "drinkId"] ) ], indices = [ diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt index 6056168..8d06a2d 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt @@ -40,7 +40,7 @@ import java.util.Locale @Entity( primaryKeys = ["serverId", "userId"], foreignKeys = [ - ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE) + ForeignKey(Server::class, ["serverId"], ["serverId"]) ] ) data class User( @@ -53,6 +53,15 @@ data class User( val audit: Boolean, val redirect: Boolean, ) { + @OptIn(ExperimentalStdlibApi::class) + val gravatarUrl: String by lazy { + val normalised: String = email.lowercase(Locale.ROOT) + val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8) + val binaryHash: ByteArray = MessageDigest.getInstance("MD5").digest(binaryData) + val hash: String = binaryHash.toHexString() + "https://www.gravatar.com/avatar/$hash?d=404&s=640" + } + companion object { fun fromModel(server: Server, value: UserModel) = User( server.serverId, @@ -65,15 +74,6 @@ data class User( value.redirect, ) } - - @OptIn(ExperimentalStdlibApi::class) - fun gravatarUrl(): String { - val normalised: String = email.lowercase(Locale.ROOT) - val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8) - val binaryHash: ByteArray = MessageDigest.getInstance("MD5").digest(binaryData) - val hash: String = binaryHash.toHexString() - return "https://www.gravatar.com/avatar/$hash?d=404" - } } @Dao -- GitLab