diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 64e87db574fa38013b822565221a91c122f718f0..e9c44110caa5304ff18326f5611ac6ad4cc06a34 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 c531e4c8ef1ef2fc291038aab8ba21e8c54ee72c..fc44d7da169b16b73cda524d0d04e4e4e9ca2a0c 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 1fd925951ff0c5ddee10c012bb3dc2b72fd419d3..66c5b7311559271812d079b656dacc1bd8c0973b 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 6bce3403a0f01c15166d0b1f67a99ecd70bccf4b..b7a61994adc21e850d8dc80cc570fe74de637ba6 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 48276de223b25a2930a5d95c7a7185162f1e249d..cda175bb6c75bb5d0e04c16f26d472e2bc95b341 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 6b2e6540131ebd72501e8060defaa00c878ef57b..f7237af875345c6e1c6bb1e58c2d3e5207e6e2cc 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 751902f50c52a6588f40ba9236367a68a61bebd5..853744c36968967346db957c350110449dc7b3c9 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 b872aef201dbb3264ce068cb1b82c92d86c360ac..b69910067205b42baad8237a68506e47ad05a325 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 045d3757633f910dcec4f91aa6ae76b29371e698..162323de9ad50bf1c953685aa13fe66a21eb9952 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 78b8d667b6ae4abe6dc5577ea427d696e9eec2fb..42d4837b4001f1988f1d62b04fae51da67365397 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 7ba2e82985c2c5dd2dbf348d2263fcca492ca4c5..b568029242142dda2ad81ff71133ea290ab16c2f 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 3a5d95e85a3da122ca99cf6b4287be9c20cd99ba..5cc575b87cb1c33b52c0d42a6e544d277cdf2cfb 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 be30261b9bb53f15c2c4ed953a5626945083ad51..1860019658ec6730255b3fe66339b5d1a19f67c7 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 ff0c99bb0f4780eef72be228c453163238036b59..937756bf4e3e9d43cc9f02a10555a1f294928bb3 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 d6ceedbf2aa979303fb700761e513fc192a9a620..73ecb0ce9a7c36ca9226b4174b5db88e00493b52 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 8c053ee161ae6d11ec671834086164a549df2494..14a88535bcabeb3d6077289754cca962ae539b57 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 a6890ee323a8d65496333ce696ced87a530c2699..56aba29775041f6cfe124db213461a2715af9ce5 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 74029fd8617672231a6a33a36615c34b30941317..112dfe646c361dd20a9533e5505c2e3c2ced251b 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 0000000000000000000000000000000000000000..16a5f7ca550009d8661bf584636f5532c362da8f --- /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 9efb6d66778c527a3cf0e91a61123aa799eca474..315971c57694c8ba40ca571ae15663f067952a65 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 0000000000000000000000000000000000000000..9432b0b4678b1451c484d27bc989754fb4b5c105 --- /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 0000000000000000000000000000000000000000..fdcae6c5d6232018e113ffbf6490f692f6773f46 --- /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 8cf5fac65c306c37e61882c2a458597da1c18c95..63251aedabdef9726f662283626f9524c46fd7a0 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 581d020209ce703f670e6c144ec0d425c104c0fe..cd9af862ae68d104fcaa82745f783db2226ba16a 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 6ea48860e6b2ff82b43e85f391aff1db39e75022..fe82de5c3c428098d6ae2c2bc708ac28eeee1e25 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 0000000000000000000000000000000000000000..dae71d6133e85300e2d58640d763633fedfa2972 --- /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 c564bbdfb01c9543f815cd373b9384e64d1af3e1..ac662512f36498378124d296e93df268c076d18c 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 60561680db069844b9498cba1ef618947febd417..8d06a2d44f5738eb2664688b399b365fa79cbe8c 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