diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt index a7feac18f7201d54eaf062963573813dee5f99bf..33c76ca752c587270267ca5f6cca01f5e1e35ef1 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt @@ -26,6 +26,7 @@ package de.chaosdorf.mete.v1 import de.chaosdorf.mete.UserId import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -33,7 +34,9 @@ data class UserModelV1( val id: UserId, val name: String, val email: String, + @SerialName("created_at") val createdAt: Instant, + @SerialName("updated_at") val updatedAt: Instant, val balance: Double, val active: Boolean, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index f2dd9cf0112cd47419bfa4faf7425fcbae1e5c20..81f13ca2bf9b138c8f9ca0ccc5065e386e6ca016 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -27,99 +27,15 @@ package de.chaosdorf.meteroid import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.twotone.Money -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.navigation -import androidx.navigation.compose.rememberNavController -import coil.compose.AsyncImage import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.v1.MeteApiV1Factory -import de.chaosdorf.meteroid.icons.MeteroidIcons -import de.chaosdorf.meteroid.icons.twotone.WaterFull -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId -import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.ui.AppRouter import de.chaosdorf.meteroid.ui.theme.MeteroidTheme -import de.chaosdorf.meteroid.util.findBestIcon -import de.chaosdorf.meteroid.util.resolve -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject -import kotlin.time.Duration.Companion.milliseconds @AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject - lateinit var accountPreferences: AccountPreferences - - @Inject - lateinit var serverRepository: ServerRepository - - @Inject - lateinit var drinkSyncHandler: DrinkSyncHandler - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val server = accountPreferences.state - .mapLatest { it.server } - .flatMapLatest { - it?.let { serverRepository.getFlow(it) } - ?: flowOf<Server?>(null) - } - - lifecycleScope.launch { - server.collectLatest { - it?.let { server -> - drinkSyncHandler.sync(server) - } - } - } - setContent { MeteroidTheme { AppRouter() @@ -127,247 +43,3 @@ class MainActivity : ComponentActivity() { } } } - -@HiltViewModel -class AppViewModel @Inject constructor( - private val accountPreferences: AccountPreferences, - private val serverRepository: ServerRepository, - private val drinkSyncHandler: DrinkSyncHandler -) : ViewModel() { - val server: StateFlow<Server?> = accountPreferences.state - .mapLatest { it.server } - .flatMapLatest { serverId -> - serverRepository.getAllFlow() - .mapLatest { list -> - list.firstOrNull { server -> server.serverId == serverId } - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - init { - viewModelScope.launch { - server.collectLatest { - it?.let { server -> - drinkSyncHandler.sync(server) - } - } - } - } - - suspend fun selectServer(server: ServerId) { - accountPreferences.setServer(server) - } -} - -@Composable -fun AppRouter(viewModel: AppViewModel = viewModel()) { - val scope = rememberCoroutineScope() - val server by viewModel.server.collectAsState() - val navController = rememberNavController() - - LaunchedEffect(server) { - if (server == null) { - navController.navigate("accounts/list") - } - } - - Scaffold(topBar = { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - if (currentDestination?.hierarchy?.any { it.route == "main" } == true) { - TopAppBar(title = { Text("Meteroid") }, navigationIcon = { - IconButton(onClick = { navController.navigate("accounts") }) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) - } - }) - } else if (currentDestination?.hierarchy?.any { it.route == "accounts/list" } == true) { - TopAppBar(title = { Text("Meteroid") }, navigationIcon = { - IconButton(onClick = { navController.navigate("main") }) { - Icon(Icons.Default.Close, contentDescription = null) - } - }) - } - }, bottomBar = { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - if (currentDestination?.hierarchy?.any { it.route == "main" } == true) { - BottomAppBar(actions = { - IconButton(onClick = { navController.navigate("main/purchase") }) { - Icon(MeteroidIcons.TwoTone.WaterFull, contentDescription = null) - } - IconButton(onClick = { navController.navigate("main/deposit") }) { - Icon(Icons.TwoTone.Money, contentDescription = null) - } - }) - } - }) { padding -> - NavHost(navController, startDestination = "main", Modifier.padding(padding)) { - navigation(route = "accounts", startDestination = "accounts/list") { - composable("accounts/list") { backStackEntry -> - ServerListScreen( - hiltViewModel(), - onAddServer = { navController.navigate("accounts/addServer") }, - onSelectServer = { - scope.launch { - viewModel.selectServer(it) - navController.navigate("main") - } - } - ) - } - composable("accounts/addServer") { backStackEntry -> - AddServerScreen( - hiltViewModel(), - onAddServer = { navController.navigate("accounts/list") } - ) - } - } - navigation(route = "main", startDestination = "main/purchase") { - /* - composable("users") { backStackEntry -> - val viewModel = hiltViewModel<UserListViewModel>() - UserListScreen(viewModel) - } - */ - composable("main/purchase") { backStackEntry -> - DrinkListScreen(hiltViewModel()) - } - composable("main/deposit") { backStackEntry -> - - } - } - } - } -} - -@HiltViewModel -class AddServerViewModel @Inject constructor( - private val repository: ServerRepository -) : ViewModel() { - val url = MutableStateFlow("") - - private suspend fun buildServer( - id: ServerId, - url: String - ): Server? = try { - val api = MeteApiV1Factory.newInstance(url) - val manifest = api.getManifest() - val icon = manifest?.findBestIcon() - Server( - id, - manifest?.name, - url, - icon?.resolve(url) - ) - } catch (_: Exception) { - null - } - - val server: StateFlow<Server?> = url - .debounce(300.milliseconds) - .mapLatest { buildServer(ServerId(-1), it) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - suspend fun addServer() { - val highestServerId = repository.getAll().maxOfOrNull { it.serverId.value } ?: 0 - val serverId = ServerId(highestServerId + 1) - val server = buildServer(serverId, url.value) - if (server != null) { - repository.save(server) - } - } -} - -@Composable -fun AddServerScreen(viewModel: AddServerViewModel, onAddServer: () -> 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 } - ) - - Button(onClick = { - scope.launch { - viewModel.addServer() - onAddServer() - } - }) { - Text("Add Server") - } - - server?.let { server -> - Text(server.url) - Text(server.name ?: "null1") - AsyncImage(model = server.logoUrl, contentDescription = null) - } - } -} - -@Preview -@Composable -fun ServerListScreen( - viewModel: ServerListViewModel = viewModel(), - onAddServer: () -> Unit = {}, - onSelectServer: (ServerId) -> Unit = {} -) { - val servers by viewModel.servers.collectAsState() - - LazyColumn { - items(servers) { server -> - ListItem( - headlineContent = { Text(server.name ?: server.url) }, - modifier = Modifier.clickable { - onSelectServer(server.serverId) - } - ) - } - item { - ListItem( - headlineContent = { Text("Add Server") }, - modifier = Modifier.clickable { - onAddServer() - } - ) - } - } -} - -@Preview -@Composable -fun DrinkListScreen( - viewModel: DrinkListViewModel = viewModel() -) { - val drinks by viewModel.drinks.collectAsState() - - LazyColumn { - items(drinks) { drink -> - ListItem(headlineContent = { Text(drink.name) }, - supportingContent = { Text("${drink.volume}l · ${drink.price}€") }) - } - } -} - -@HiltViewModel -class DrinkListViewModel @Inject constructor( - drinkRepository: DrinkRepository, - accountPreferences: AccountPreferences -) : ViewModel() { - private val serverId: Flow<ServerId?> = accountPreferences.state.mapLatest { it.server } - val drinks: StateFlow<List<Drink>> = serverId.flatMapLatest { - it?.let { serverId -> - drinkRepository.getAllFlow(serverId) - } ?: flowOf(emptyList()) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) -} - -@HiltViewModel -class ServerListViewModel @Inject constructor( - serverRepository: ServerRepository -) : ViewModel() { - val servers: StateFlow<List<Server>> = serverRepository.getAllFlow() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/PreferenceModule.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/PreferenceModule.kt index b985bb92a0a815c822a78cec08aa11a8f42f3e1d..97df1b96c39dd5bdbabf5107462662075bc8609f 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/PreferenceModule.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/PreferenceModule.kt @@ -34,8 +34,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import de.chaosdorf.meteroid.AccountPreferences -import de.chaosdorf.meteroid.AccountPreferencesImpl +import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.storage.AccountPreferencesImpl import javax.inject.Singleton val Context.accountDataStore: DataStore<Preferences> by preferencesDataStore(name = "account") diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt similarity index 91% rename from app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferences.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt index 37723f53b44b826527c4ad066841eaf126560885..75fdfb2c0b0a3711011d360533e1fdd3f41baf39 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferences.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid +package de.chaosdorf.meteroid.storage import de.chaosdorf.mete.UserId import de.chaosdorf.meteroid.model.ServerId @@ -30,12 +30,12 @@ import kotlinx.coroutines.flow.Flow interface AccountPreferences { data class State( - val server: ServerId?, - val user: UserId? + val server: ServerId?, + val user: UserId? ) val state: Flow<State> suspend fun setServer(server: ServerId?) - suspend fun setUser(userId: UserId?) + suspend fun setUser(user: UserId?) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt similarity index 88% rename from app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferencesImpl.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt index 33e61c4049a4fce31856f0c8383ad1614d8a9114..71a6be1d7ecb1b9215e474ab6bcbf1590a40da3e 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferencesImpl.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid +package de.chaosdorf.meteroid.storage import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -43,10 +43,10 @@ class AccountPreferencesImpl @Inject constructor( val serverId = it[SERVER_KEY] ?: -1L val userId = it[USER_KEY] ?: -1L - AccountPreferences.State( - if (serverId >= 0) ServerId(serverId) else null, - if (userId >= 0) UserId(userId) else null, - ) + AccountPreferences.State( + if (serverId >= 0) ServerId(serverId) else null, + if (userId >= 0) UserId(userId) else null, + ) } override suspend fun setServer(server: ServerId?) { @@ -55,9 +55,9 @@ class AccountPreferencesImpl @Inject constructor( } } - override suspend fun setUser(userId: UserId?) { + override suspend fun setUser(user: UserId?) { dataStore.edit { - it[SERVER_KEY] = userId?.value ?: -1L + it[USER_KEY] = user?.value ?: -1L } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/DrinkSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/DrinkSyncHandler.kt similarity index 96% rename from app/src/main/kotlin/de/chaosdorf/meteroid/DrinkSyncHandler.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/storage/DrinkSyncHandler.kt index ab6d7034db657be215566e2dabbb1bacea9a9d15..732f3ed1c05d366481e12a247e172f096bd3fee1 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/DrinkSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/DrinkSyncHandler.kt @@ -22,11 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid +package de.chaosdorf.meteroid.storage import androidx.room.withTransaction import de.chaosdorf.mete.DrinkId import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.meteroid.MeteroidDatabase import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.Server diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/SyncHandler.kt similarity index 75% rename from app/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/storage/SyncHandler.kt index dc777040a3825f6c748566cd2f9540ff71df33d0..40be77a41d187054aa92a1b86423ee6d6010a1fd 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/SyncHandler.kt @@ -22,8 +22,9 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid +package de.chaosdorf.meteroid.storage +import android.util.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,7 +32,17 @@ abstract class SyncHandler<Context, Entry, Key> { sealed class State { data object Idle : State() data object Loading : State() - data class Error(val message: String) : State() + data class Error(val message: String = "") : State() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return true + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } + } } abstract suspend fun withTransaction(block: suspend () -> Unit) @@ -48,7 +59,12 @@ abstract class SyncHandler<Context, Entry, Key> { val state: StateFlow<State> = _state suspend fun sync(context: Context) { - if (_state.compareAndSet(State.Idle, State.Loading)) { + if (_state.compareAndSet(State.Idle, State.Loading) || _state.compareAndSet( + State.Error(), + State.Loading + ) + ) { + Log.w(this::class.simpleName, "Started sync") try { val loadedEntries = loadCurrent(context) withTransaction { @@ -64,9 +80,13 @@ abstract class SyncHandler<Context, Entry, Key> { } } _state.value = State.Idle + Log.w(this::class.simpleName, "Finished sync") } catch (e: Exception) { + Log.e(this::class.simpleName, "Error while syncing data", e) _state.value = State.Error("Error while syncing data: $e") } + } else { + Log.w(this::class.simpleName, "Already syncing, disregarding sync request") } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/UserSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/UserSyncHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..906771a524a782b8a3d58383beb3b849d737890c --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/UserSyncHandler.kt @@ -0,0 +1,64 @@ +/* + * 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.storage + +import androidx.room.withTransaction +import de.chaosdorf.mete.UserId +import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.meteroid.MeteroidDatabase +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.model.UserRepository +import javax.inject.Inject + +class UserSyncHandler @Inject constructor( + private val db: MeteroidDatabase, + private val repository: UserRepository +) : SyncHandler<Server, User, UserSyncHandler.Key>() { + data class Key( + val server: ServerId, val user: UserId + ) + + override suspend fun withTransaction(block: suspend () -> Unit) = + db.withTransaction(block) + + override suspend fun store(entry: User) = + repository.save(entry) + + override suspend fun delete(key: Key) = + repository.delete(key.server, key.user) + + override fun entryToKey(entry: User) = Key(entry.serverId, entry.userId) + + override suspend fun loadStored(context: Server): List<User> = + repository.getAll(context.serverId) + + override suspend fun loadCurrent(context: Server): List<User> { + val api = MeteApiV1Factory.newInstance(context.url) + val loadedEntries = api.listUsers() + return loadedEntries.map { User.fromModelV1(context.serverId, it) } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt new file mode 100644 index 0000000000000000000000000000000000000000..b22f4ce8f05065532a28afbe460fdedd28ad1633 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt @@ -0,0 +1,142 @@ +/* + * 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 + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import de.chaosdorf.meteroid.ui.home.DrinkListScreen +import de.chaosdorf.meteroid.ui.home.DrinkListViewModel +import de.chaosdorf.meteroid.ui.home.HomeSections +import de.chaosdorf.meteroid.ui.servers.AddServerScreen +import de.chaosdorf.meteroid.ui.servers.ServerListScreen +import de.chaosdorf.meteroid.ui.users.UserListScreen +import kotlinx.coroutines.launch + +@Composable +fun AppRouter(viewModel: AppViewModel = viewModel()) { + val scope = rememberCoroutineScope() + val navController = rememberNavController() + val initState by viewModel.initState.collectAsState() + + LaunchedEffect(initState) { + when (initState) { + AppViewModel.InitState.LOADING -> navController.navigate(Routes.Init) + AppViewModel.InitState.CREATE_SERVER -> navController.navigate(Routes.Servers.Add) + AppViewModel.InitState.SELECT_SERVER -> navController.navigate(Routes.Servers.List) + AppViewModel.InitState.SELECT_USER -> navController.navigate(Routes.Users.List) + AppViewModel.InitState.HOME -> navController.navigate(Routes.Home.Root) + } + } + + NavHost(navController, startDestination = Routes.Init) { + composable(route = Routes.Init) { _ -> + Box { + Text("Loading") + } + } + + navigation(route = Routes.Servers.Root, startDestination = Routes.Servers.List) { + composable(Routes.Servers.List) { _ -> + ServerListScreen( + hiltViewModel(), + onAdd = { navController.navigate(Routes.Servers.Add) }, + onSelect = { + scope.launch { + viewModel.selectServer(it) + navController.navigate(Routes.Users.List) + } + } + ) + } + composable(Routes.Servers.Add) { _ -> + AddServerScreen( + hiltViewModel(), + onAdd = { navController.navigate(Routes.Servers.List) } + ) + } + } + navigation(route = Routes.Users.Root, startDestination = Routes.Users.List) { + composable(Routes.Users.List) { _ -> + UserListScreen( + hiltViewModel(), + onAdd = { TODO() }, + onSelect = { + scope.launch { + viewModel.selectUser(it) + navController.navigate(Routes.Home.Root) + } + }, + onBack = { navController.navigate(Routes.Servers.Root) }, + ) + } + /* + composable(Routes.Users.Add) { _ -> + AddUserScreen( + hiltViewModel(), + onAdd = { navController.navigate(Routes.Users.List) } + ) + } + */ + } + navigation(route = Routes.Home.Root, startDestination = Routes.Home.Purchase) { + composable(Routes.Home.Purchase) { _ -> + val drinkListViewModel = hiltViewModel<DrinkListViewModel>() + MeteroidScaffold( + routes = HomeSections.entries, + currentRoute = HomeSections.PURCHASE, + navigateTo = navController::navigate, + onBack = { navController.navigate(Routes.Users.Root) }, + ) { paddingValues -> + DrinkListScreen(drinkListViewModel, Modifier.padding(paddingValues)) + } + } + composable(Routes.Home.Deposit) { _ -> + MeteroidScaffold( + routes = HomeSections.entries, + currentRoute = HomeSections.DEPOSIT, + navigateTo = navController::navigate, + onBack = { navController.navigate(Routes.Users.Root) }, + ) { paddingValues -> + Box(Modifier.padding(paddingValues)) { + Text("TODO: Deposit") + } + } + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3781fd46a11af9cb723face86a61c2fe8a952fa3 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt @@ -0,0 +1,97 @@ +/* + * 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 + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.UserId +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.storage.DrinkSyncHandler +import de.chaosdorf.meteroid.storage.UserSyncHandler +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class AppViewModel @Inject constructor( + private val accountPreferences: AccountPreferences, + private val serverRepository: ServerRepository, + private val userSyncHandler: UserSyncHandler, + private val drinkSyncHandler: DrinkSyncHandler, +) : ViewModel() { + val initState: StateFlow<InitState> = accountPreferences.state + .flatMapLatest { preferences -> + serverRepository.getAllFlow() + .mapLatest { it.map(Server::serverId) } + .mapLatest { servers -> + if (servers.isEmpty()) InitState.CREATE_SERVER + else if (!servers.contains(preferences.server)) InitState.SELECT_SERVER + else if (preferences.user == null) InitState.SELECT_USER + else InitState.HOME + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), InitState.LOADING) + + private val server: StateFlow<Server?> = + accountPreferences.state.flatMapLatest { preferences -> + if (preferences.server == null) flowOf(null) + else serverRepository.getFlow(preferences.server) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + init { + server.onEach { server -> + if (server != null) { + userSyncHandler.sync(server) + drinkSyncHandler.sync(server) + } + }.launchIn(viewModelScope) + } + + suspend fun selectServer(server: ServerId) { + accountPreferences.setServer(server) + accountPreferences.setUser(null) + } + + suspend fun selectUser(user: UserId) { + accountPreferences.setUser(user) + } + + enum class InitState { + LOADING, + CREATE_SERVER, + SELECT_SERVER, + SELECT_USER, + HOME + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidNavSection.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidNavSection.kt new file mode 100644 index 0000000000000000000000000000000000000000..4a3a6092479780c5c0a0264dc877102e1841bf06 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidNavSection.kt @@ -0,0 +1,57 @@ +/* + * 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 + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector + +interface MeteroidNavSection { + val title: String + val icon: ImageVector + val route: String +} + +@Composable +fun <T : MeteroidNavSection> RowScope.MeteroidNavSections( + routes: Iterable<T>, + currentRoute: T, + navigateTo: (String) -> Unit, + modifier: Modifier = Modifier +) { + for (route in routes) { + NavigationBarItem( + icon = { Icon(route.icon, contentDescription = route.title) }, + label = { Text(route.title) }, + selected = route == currentRoute, + onClick = { navigateTo(route.route) }, + modifier = modifier + ) + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt new file mode 100644 index 0000000000000000000000000000000000000000..61340f69ba23abacd4d66ceb74da0fe94ff9d5df --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt @@ -0,0 +1,74 @@ +/* + * 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 + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex + +@Composable +fun <T : MeteroidNavSection> MeteroidScaffold( + routes: Iterable<T>, + currentRoute: T, + navigateTo: (String) -> Unit, + onBack: () -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Meteroid") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.Default.Menu, contentDescription = "Menu") + } + }, + modifier = Modifier + .padding(8.dp) + .shadow(4.dp, shape = RoundedCornerShape(8.dp)) + .zIndex(1.0f) + ) + }, + bottomBar = { + NavigationBar { + MeteroidNavSections(routes, currentRoute, navigateTo) + } + }, + content = content + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Routes.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Routes.kt new file mode 100644 index 0000000000000000000000000000000000000000..e06f5c2fee3833b6118151c546cec5ca931cda54 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Routes.kt @@ -0,0 +1,47 @@ +/* + * 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 + +object Routes { + const val Init = "init" + + object Servers { + const val Root = "servers" + const val List = "$Root/list" + const val Add = "$Root/new" + } + + object Users { + const val Root = "users" + const val List = "${Root}/list" + //const val Add = "${Root}/new" + } + + object Home { + const val Root = "home" + const val Deposit = "home/deposit" + const val Purchase = "home/purchase" + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/DrinkListScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..f80918e98f9189613e6201189aff3fff46ceef3f --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/DrinkListScreen.kt @@ -0,0 +1,62 @@ +/* + * 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.home + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import de.chaosdorf.meteroid.storage.SyncHandler + +@Preview +@Composable +fun DrinkListScreen( + viewModel: DrinkListViewModel = viewModel(), + @SuppressLint("ModifierParameter") modifier: Modifier = Modifier +) { + val drinks by viewModel.drinks.collectAsState() + val syncState by viewModel.syncState.collectAsState() + + Column(modifier = modifier) { + if (syncState == SyncHandler.State.Loading) { + LinearProgressIndicator() + } + LazyColumn { + items(drinks) { drink -> + ListItem(headlineContent = { Text(drink.name) }, + supportingContent = { Text("${drink.volume}l · ${drink.price}€") }) + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/DrinkListViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..331771d492ce0a43d5c8577778e509e9b1c12efa --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/DrinkListViewModel.kt @@ -0,0 +1,58 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.model.DrinkRepository +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.storage.DrinkSyncHandler +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +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 DrinkListViewModel @Inject constructor( + drinkRepository: DrinkRepository, + accountPreferences: AccountPreferences, + syncHandler: DrinkSyncHandler +) : ViewModel() { + private val serverId: Flow<ServerId?> = accountPreferences.state.mapLatest { it.server } + val drinks: StateFlow<List<Drink>> = serverId.flatMapLatest { + it?.let { serverId -> + drinkRepository.getAllFlow(serverId) + } ?: flowOf(emptyList()) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + val syncState = syncHandler.state +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/HomeSections.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/HomeSections.kt new file mode 100644 index 0000000000000000000000000000000000000000..80d9d5eab7542df27750aeac6e8d18d6f775e3eb --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/HomeSections.kt @@ -0,0 +1,42 @@ +/* + * 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.home + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Money +import androidx.compose.ui.graphics.vector.ImageVector +import de.chaosdorf.meteroid.icons.MeteroidIcons +import de.chaosdorf.meteroid.icons.twotone.WaterFull +import de.chaosdorf.meteroid.ui.MeteroidNavSection +import de.chaosdorf.meteroid.ui.Routes + +enum class HomeSections( + override val title: String, + override val icon: ImageVector, + override val route: String +) : MeteroidNavSection { + PURCHASE("Drinks", MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase), + DEPOSIT("Money", Icons.TwoTone.Money, Routes.Home.Deposit); +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..be30261b9bb53f15c2c4ed953a5626945083ad51 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.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.ui.servers + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import kotlinx.coroutines.launch + +@Composable +fun AddServerScreen( + viewModel: AddServerViewModel = viewModel(), + 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 } + ) + + Button(onClick = { + scope.launch { + viewModel.addServer() + onAdd() + } + }) { + Text("Add Server") + } + + server?.let { server -> + Text(server.url) + Text(server.name ?: "null1") + AsyncImage(model = server.logoUrl, contentDescription = null) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3689ee0d1a02a3b9c7cf08dabb87f01c9e308822 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt @@ -0,0 +1,81 @@ +/* + * 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.servers + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.util.findBestIcon +import de.chaosdorf.meteroid.util.resolve +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +@HiltViewModel +class AddServerViewModel @Inject constructor( + private val repository: ServerRepository +) : ViewModel() { + val url = MutableStateFlow("") + + private suspend fun buildServer( + id: ServerId, + url: String + ): Server? = try { + val api = MeteApiV1Factory.newInstance(url) + val manifest = api.getManifest() + val icon = manifest?.findBestIcon() + Server( + id, + manifest?.name, + url, + icon?.resolve(url) + ) + } catch (_: Exception) { + null + } + + val server: StateFlow<Server?> = url + .debounce(300.milliseconds) + .mapLatest { buildServer(ServerId(-1), it) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + suspend fun addServer() { + val highestServerId = repository.getAll().maxOfOrNull { it.serverId.value } ?: 0 + val serverId = ServerId(highestServerId + 1) + val server = buildServer(serverId, url.value) + if (server != null) { + repository.save(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 new file mode 100644 index 0000000000000000000000000000000000000000..b30db84d789c8883babd31304c1ce541a5a1999e --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt @@ -0,0 +1,62 @@ +/* + * 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.servers + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import de.chaosdorf.meteroid.model.ServerId + +@Preview +@Composable +fun ServerListScreen( + viewModel: ServerListViewModel = viewModel(), + onAdd: () -> Unit = {}, + onSelect: (ServerId) -> Unit = {} +) { + val servers by viewModel.servers.collectAsState() + LazyColumn { + items(servers) { server -> + ListItem( + headlineContent = { Text(server.name ?: server.url) }, + modifier = Modifier.clickable { onSelect(server.serverId) } + ) + } + item { + ListItem( + headlineContent = { Text("Add Server") }, + modifier = Modifier.clickable { onAdd() } + ) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..32fb347d8a03ca03b56f49b3eff280850ecc4989 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt @@ -0,0 +1,43 @@ +/* + * 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.servers + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.ServerRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class ServerListViewModel @Inject constructor( + serverRepository: ServerRepository +) : ViewModel() { + val servers: StateFlow<List<Server>> = serverRepository.getAllFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..678516dec2500c912ede880d8ac1dfcba61b0282 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt @@ -0,0 +1,101 @@ +/* + * 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.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +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.LinearProgressIndicator +import androidx.compose.material3.ListItem +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.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import de.chaosdorf.mete.UserId +import de.chaosdorf.meteroid.storage.SyncHandler + +@Preview +@Composable +fun UserListScreen( + viewModel: UserListViewModel = viewModel(), + onAdd: () -> Unit = {}, + onSelect: (UserId) -> Unit = {}, + onBack: () -> Unit = {}, +) { + val users by viewModel.users.collectAsState() + val syncState by viewModel.syncState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Meteroid") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") + } + }, + modifier = Modifier + .padding(8.dp) + .shadow(4.dp, shape = RoundedCornerShape(8.dp)) + .zIndex(1.0f) + ) + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + if (syncState == SyncHandler.State.Loading) { + LinearProgressIndicator() + } + LazyColumn { + items(users) { user -> + ListItem( + headlineContent = { Text(user.name) }, + modifier = Modifier.clickable { onSelect(user.userId) } + ) + } + item { + ListItem( + headlineContent = { Text("Add User") }, + modifier = Modifier.clickable { onAdd() } + ) + } + } + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..ef0f243759c6d82ad6d343e62c41a33bc4d0b4e8 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt @@ -0,0 +1,56 @@ +/* + * 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.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.model.UserRepository +import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.storage.UserSyncHandler +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +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( + accountPreferences: AccountPreferences, + userRepository: UserRepository, + syncHandler: UserSyncHandler +) : ViewModel() { + private val serverId = accountPreferences.state.mapLatest { it.server } + + val users: StateFlow<List<User>> = serverId.flatMapLatest { serverId -> + if (serverId == null) flowOf(emptyList()) + else userRepository.getAllFlow(serverId) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + val syncState = syncHandler.state +} diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json index e1642ac70ae3821d968691fc2935c820b1311726..f78e5632816d67577e2735d078c9603ac4814075 100644 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "74b0d505a7abb27362a1ee0986b700e0", + "identityHash": "bee2a6e9f2b7a72c80a0e97b71f55e51", "entities": [ { "tableName": "Drink", @@ -155,7 +155,7 @@ }, { "tableName": "User", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` REAL NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER 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, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` REAL NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "serverId", @@ -164,8 +164,8 @@ "notNull": true }, { - "fieldPath": "drinkId", - "columnName": "drinkId", + "fieldPath": "userId", + "columnName": "userId", "affinity": "INTEGER", "notNull": true }, @@ -222,7 +222,7 @@ "autoGenerate": false, "columnNames": [ "serverId", - "drinkId" + "userId" ] }, "indices": [], @@ -244,7 +244,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, '74b0d505a7abb27362a1ee0986b700e0')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bee2a6e9f2b7a72c80a0e97b71f55e51')" ] } } \ No newline at end of file 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 c01e4b1852ab47f9f1d1908a9f5aea33a3b0f05f..1c669a62ee67b9ad8fb508d8a8b94b3775d2e648 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt @@ -30,21 +30,20 @@ import androidx.room.ForeignKey import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import de.chaosdorf.mete.DrinkId import de.chaosdorf.mete.UserId import de.chaosdorf.mete.v1.UserModelV1 import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant @Entity( - primaryKeys = ["serverId", "drinkId"], + primaryKeys = ["serverId", "userId"], foreignKeys = [ ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE) ] ) data class User( val serverId: ServerId, - val drinkId: UserId, + val userId: UserId, val active: Boolean, val name: String, val email: String, @@ -72,24 +71,24 @@ data class User( @Dao interface UserRepository { - @Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1") - suspend fun get(serverId: ServerId, drinkId: DrinkId): Drink? + @Query("SELECT * FROM User WHERE serverId = :serverId AND userId = :userId LIMIT 1") + suspend fun get(serverId: ServerId, userId: UserId): User? - @Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1") - fun getFlow(serverId: ServerId, drinkId: DrinkId): Flow<Drink?> + @Query("SELECT * FROM User WHERE serverId = :serverId AND userId = :userId LIMIT 1") + fun getFlow(serverId: ServerId, userId: UserId): Flow<User?> - @Query("SELECT * FROM Drink WHERE serverId = :serverId") - suspend fun getAll(serverId: ServerId): List<Drink> + @Query("SELECT * FROM User WHERE serverId = :serverId") + suspend fun getAll(serverId: ServerId): List<User> - @Query("SELECT * FROM Drink WHERE serverId = :serverId") - fun getAllFlow(serverId: ServerId): Flow<List<Drink>> + @Query("SELECT * FROM User WHERE serverId = :serverId") + fun getAllFlow(serverId: ServerId): Flow<List<User>> @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(drink: Drink) + suspend fun save(user: User) - @Query("DELETE FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId") - suspend fun delete(serverId: ServerId, drinkId: DrinkId) + @Query("DELETE FROM User WHERE serverId = :serverId AND userId = :userId") + suspend fun delete(serverId: ServerId, userId: UserId) - @Query("DELETE FROM Drink WHERE serverId = :serverId") + @Query("DELETE FROM User WHERE serverId = :serverId") suspend fun deleteAll(serverId: ServerId) }