From 803d14f4edb7887948bc4888eae1253f29f652ba Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <mail@justjanne.de> Date: Tue, 31 Oct 2023 01:18:10 +0100 Subject: [PATCH] wip: initial version with hilt --- app/build.gradle.kts | 3 + .../chaosdorf/meteroid/AccountPreferences.kt | 21 +- .../meteroid/AccountPreferencesImpl.kt | 68 ++++ .../main/kotlin/de/chaosdorf/meteroid/App.kt | 38 -- .../de/chaosdorf/meteroid/DrinkSyncHandler.kt | 63 ++++ .../de/chaosdorf/meteroid/MainActivity.kt | 339 +++++++++++++++++- .../de/chaosdorf/meteroid/SyncHandler.kt | 61 ++-- .../meteroid/di/AddServerViewModel.kt | 116 ------ .../chaosdorf/meteroid/di/DatabaseModule.kt | 47 ++- .../meteroid/di/MainLayoutViewModel.kt | 75 ---- ...nkListViewModel.kt => PreferenceModule.kt} | 50 +-- .../de/chaosdorf/meteroid/di/RootViewModel.kt | 142 -------- .../meteroid/di/ServerSelectionViewModel.kt | 92 ----- .../chaosdorf/meteroid/di/SetupViewModel.kt | 126 ------- .../de/chaosdorf/meteroid/routes/InitRoute.kt | 33 -- .../chaosdorf/meteroid/routes/RootRouter.kt | 39 -- .../chaosdorf/meteroid/routes/SetupRouter.kt | 38 -- .../meteroid/routes/main/DrinkList.kt | 43 --- .../meteroid/routes/main/MainLayoutRoute.kt | 74 ---- .../meteroid/routes/setup/AddServerRoute.kt | 95 ----- .../routes/setup/ServerSelectionRoute.kt | 91 ----- .../meteroid/routes/setup/SetupView.kt | 38 -- gradle/libs.versions.toml | 2 + .../1.json | 140 +++++++- .../2.json | 144 -------- .../3.json | 144 -------- .../de/chaosdorf/meteroid/MeteroidDatabase.kt | 14 +- .../de/chaosdorf/meteroid/model/Drink.kt | 51 +-- .../de/chaosdorf/meteroid/model/Server.kt | 28 +- .../de/chaosdorf/meteroid/model/User.kt | 95 +++++ 30 files changed, 846 insertions(+), 1464 deletions(-) rename persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt => app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferences.kt (80%) create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferencesImpl.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/App.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/DrinkSyncHandler.kt rename persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt => app/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt (50%) delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt rename persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt => app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt (51%) delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt rename app/src/main/kotlin/de/chaosdorf/meteroid/di/{DrinkListViewModel.kt => PreferenceModule.kt} (53%) delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt delete mode 100644 persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json delete mode 100644 persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json create mode 100644 persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5692fb9..1efc153 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -89,9 +89,12 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation(libs.coil.compose) + implementation(libs.hilt.navigation) implementation(libs.hilt.android) ksp(libs.hilt.compiler) + implementation("androidx.datastore:datastore-preferences:1.0.0") + implementation(project(":api")) implementation(project(":persistence")) diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferences.kt similarity index 80% rename from persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferences.kt index 26dfb12..37723f5 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/Repository.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferences.kt @@ -24,15 +24,18 @@ package de.chaosdorf.meteroid +import de.chaosdorf.mete.UserId +import de.chaosdorf.meteroid.model.ServerId import kotlinx.coroutines.flow.Flow -interface Repository<K, V> { - fun getKey(value: V): K - suspend fun get(key: K): V? - fun getFlow(key: K): Flow<V?> - suspend fun getAll(): List<V> - fun getAllFlow(): Flow<List<V>> - suspend fun save(value: V) - suspend fun delete(key: K) - suspend fun deleteAll() +interface AccountPreferences { + data class State( + val server: ServerId?, + val user: UserId? + ) + + val state: Flow<State> + + suspend fun setServer(server: ServerId?) + suspend fun setUser(userId: UserId?) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferencesImpl.kt new file mode 100644 index 0000000..33e61c4 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/AccountPreferencesImpl.kt @@ -0,0 +1,68 @@ +/* + * 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 + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import de.chaosdorf.mete.UserId +import de.chaosdorf.meteroid.model.ServerId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import javax.inject.Inject + +class AccountPreferencesImpl @Inject constructor( + private val dataStore: DataStore<Preferences> +) : AccountPreferences { + + override val state: Flow<AccountPreferences.State> = + dataStore.data.mapLatest { + 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, + ) + } + + override suspend fun setServer(server: ServerId?) { + dataStore.edit { + it[SERVER_KEY] = server?.value ?: -1L + } + } + + override suspend fun setUser(userId: UserId?) { + dataStore.edit { + it[SERVER_KEY] = userId?.value ?: -1L + } + } + + private companion object { + val SERVER_KEY = longPreferencesKey("serverId") + val USER_KEY = longPreferencesKey("userId") + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/App.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/App.kt deleted file mode 100644 index 710454e..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/App.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import de.chaosdorf.meteroid.di.RootViewModel -import de.chaosdorf.meteroid.routes.RootRouter - -@Composable -fun App(viewModel: RootViewModel) { - val route by viewModel.route.collectAsState() - - RootRouter(route) -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/DrinkSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/DrinkSyncHandler.kt new file mode 100644 index 0000000..ab6d703 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/DrinkSyncHandler.kt @@ -0,0 +1,63 @@ +/* + * 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 + +import androidx.room.withTransaction +import de.chaosdorf.mete.DrinkId +import de.chaosdorf.mete.v1.MeteApiV1Factory +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 javax.inject.Inject + +class DrinkSyncHandler @Inject constructor( + private val db: MeteroidDatabase, + private val repository: DrinkRepository +) : SyncHandler<Server, Drink, DrinkSyncHandler.Key>() { + data class Key( + val server: ServerId, val drink: DrinkId + ) + + override suspend fun withTransaction(block: suspend () -> Unit) = + db.withTransaction(block) + + override suspend fun store(entry: Drink) = + repository.save(entry) + + override suspend fun delete(key: Key) = + repository.delete(key.server, key.drink) + + override fun entryToKey(entry: Drink) = Key(entry.serverId, entry.drinkId) + + override suspend fun loadStored(context: Server): List<Drink> = + repository.getAll(context.serverId) + + override suspend fun loadCurrent(context: Server): List<Drink> { + val api = MeteApiV1Factory.newInstance(context.url) + val loadedEntries = api.listDrinks() + return loadedEntries.map { Drink.fromModelV1(context.serverId, it) } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index af1770b..f2dd9cf 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -27,28 +27,347 @@ 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 de.chaosdorf.meteroid.di.RootViewModel -import de.chaosdorf.meteroid.di.RootViewModelFactory +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.theme.MeteroidTheme -import kotlinx.coroutines.MainScope - +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() { - private val rootViewModelFactory = object : RootViewModelFactory {} - private lateinit var rootViewModel: RootViewModel + @Inject + lateinit var accountPreferences: AccountPreferences + + @Inject + lateinit var serverRepository: ServerRepository + + @Inject + lateinit var drinkSyncHandler: DrinkSyncHandler override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val scope = MainScope() - rootViewModel = - rootViewModelFactory.newInstance(scope, applicationContext) + + 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 { - App(rootViewModel) + AppRouter() } } } } + +@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/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt similarity index 50% rename from persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt index 7528399..dc77704 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/RepositorySyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt @@ -27,33 +27,46 @@ package de.chaosdorf.meteroid import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class RepositorySyncHandler<K, V>( - private val withTransaction: suspend (block: suspend () -> Any?) -> Any?, - private val repository: Repository<K, V>, - private val loader: suspend () -> List<V>, -) : SyncHandler { - private val _state = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle) - override val state: StateFlow<SyncHandler.State> = _state - - override suspend fun doSync() { - _state.value = SyncHandler.State.Syncing - val values = +abstract class SyncHandler<Context, Entry, Key> { + sealed class State { + data object Idle : State() + data object Loading : State() + data class Error(val message: String) : State() + } + + abstract suspend fun withTransaction(block: suspend () -> Unit) + abstract suspend fun loadCurrent(context: Context): List<Entry> + + abstract suspend fun loadStored(context: Context): List<Entry> + + abstract fun entryToKey(entry: Entry): Key + + abstract suspend fun delete(key: Key) + abstract suspend fun store(entry: Entry) + + private val _state = MutableStateFlow<State>(State.Idle) + val state: StateFlow<State> = _state + + suspend fun sync(context: Context) { + if (_state.compareAndSet(State.Idle, State.Loading)) { try { - loader() + val loadedEntries = loadCurrent(context) + withTransaction { + val storedEntries = loadStored(context) + val storedKeys = storedEntries.map(::entryToKey).toSet() + val loadedKeys = loadedEntries.map(::entryToKey).toSet() + val removedKeys = storedKeys - loadedKeys + for (removedKey in removedKeys) { + delete(removedKey) + } + for (loadedEntry in loadedEntries) { + store(loadedEntry) + } + } + _state.value = State.Idle } catch (e: Exception) { - _state.value = SyncHandler.State.Error("Error while syncing: $e") - return - } - withTransaction { - val existing = repository.getAll().map(repository::getKey).toSet() - val deletedEntries = existing - values.map(repository::getKey).toSet() - for (deletedEntry in deletedEntries) { - repository.delete(deletedEntry) - } - for (entry in values) { - repository.save(entry) + _state.value = State.Error("Error while syncing data: $e") } } - _state.value = SyncHandler.State.Idle } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt deleted file mode 100644 index 0b07b4c..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/AddServerViewModel.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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.di - -import android.util.Log -import de.chaosdorf.mete.PwaManifest -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId -import de.chaosdorf.meteroid.util.await -import de.chaosdorf.meteroid.util.findBestIcon -import de.chaosdorf.meteroid.util.resolve -import kotlinx.coroutines.CoroutineScope -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.mapNotNull -import kotlinx.coroutines.flow.stateIn -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.OkHttpClient -import okhttp3.Request - - -interface AddServerViewModelFactory { - fun newInstance( - scope: CoroutineScope, - isFirstServer: Boolean, - onSubmit: (url: String, manifest: PwaManifest?) -> Unit, - onCancel: () -> Unit - ) = AddServerViewModelImpl(scope, isFirstServer, onSubmit, onCancel) -} - -interface AddServerViewModel { - val url: MutableStateFlow<String> - val server: StateFlow<Server?> - val loading: StateFlow<Boolean> - val isFirstServer: Boolean - fun submit() - fun cancel() -} - -class AddServerViewModelImpl( - scope: CoroutineScope, - override val isFirstServer: Boolean, - private val onSubmit: (url: String, manifest: PwaManifest?) -> Unit, - private val onCancel: () -> Unit -) : AddServerViewModel { - private val json = Json { - ignoreUnknownKeys = true - } - private val httpClient = OkHttpClient() - override val url = MutableStateFlow("") - private val _loading = MutableStateFlow(false) - override val loading = _loading - - @OptIn(ExperimentalSerializationApi::class) - private val manifest = url.debounce(300).mapNotNull { address -> - _loading.value = true - try { - val url = address.toHttpUrl().resolve("manifest.json") - val call = httpClient.newCall(Request.Builder().url(url!!).build()) - val body = call.await() - val manifest = json.decodeFromStream<PwaManifest>(body.byteStream()) - Pair(address, manifest) - } catch (_: Exception) { - null - } finally { - _loading.value = false - } - }.stateIn(scope, SharingStarted.WhileSubscribed(), null) - - override val server = manifest.mapLatest { pair -> - pair?.let { (url, manifest) -> - Server( - id = ServerId(-1), - url = url, - name = manifest.name, - logoUrl = manifest.findBestIcon()?.resolve(url) - ) - } - }.stateIn(scope, SharingStarted.WhileSubscribed(), null) - - override fun submit() { - onSubmit(url.value, manifest.value?.second) - } - - override fun cancel() { - onCancel() - } -} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt similarity index 51% rename from persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt index 8a671a8..a8606f1 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/SyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt @@ -22,18 +22,45 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid +package de.chaosdorf.meteroid.di -import kotlinx.coroutines.flow.StateFlow +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import de.chaosdorf.meteroid.MeteroidDatabase +import de.chaosdorf.meteroid.model.DrinkRepository +import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.model.UserRepository +import javax.inject.Singleton -interface SyncHandler { - val state: StateFlow<State> - suspend fun doSync() +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + @Singleton + @Provides + fun provideDatabase( + @ApplicationContext context: Context + ): MeteroidDatabase = Room + .databaseBuilder(context, MeteroidDatabase::class.java, "mete") + .build() - sealed class State { - data object Idle : State() - data object Syncing : State() - data class Error(val message: String) : State() - } + @Provides + fun provideDrinkRepository( + database: MeteroidDatabase + ): DrinkRepository = database.drinks() + + @Provides + fun provideUserRepository( + database: MeteroidDatabase + ): UserRepository = database.users() + + @Provides + fun provideServerRepository( + database: MeteroidDatabase + ): ServerRepository = database.server() } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt deleted file mode 100644 index 0e732b3..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/MainLayoutViewModel.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.di - -import androidx.room.withTransaction -import de.chaosdorf.mete.v1.MeteApiV1Factory -import de.chaosdorf.meteroid.MeteroidDatabase -import de.chaosdorf.meteroid.RepositorySyncHandler -import de.chaosdorf.meteroid.SyncHandler -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.Server -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch - - -interface MainLayoutViewModelFactory { - fun newInstance( - scope: CoroutineScope, db: MeteroidDatabase, server: Server, onOpenServerSelection: () -> Unit - ): MainLayoutViewModel = MainLayoutViewModelImpl(scope, db, server, onOpenServerSelection) -} - -interface MainLayoutViewModel { - val server: Server - val syncState: StateFlow<SyncHandler.State> - val drinkListViewModel: DrinkListViewModel - - fun openServerSelection() -} - -class MainLayoutViewModelImpl( - scope: CoroutineScope, - db: MeteroidDatabase, - override val server: Server, - private val onOpenServerSelection: () -> Unit -) : MainLayoutViewModel { - private val api = MeteApiV1Factory.newInstance(server.url) - override val drinkListViewModel = DrinkListViewModelImpl(scope, db.drinks()) - private val syncHandler = RepositorySyncHandler(db::withTransaction, db.drinks()) { - api.listDrinks().map { Drink.fromModelV1(it) } - } - override val syncState: StateFlow<SyncHandler.State> = syncHandler.state - - init { - scope.launch { - syncHandler.doSync() - } - } - - override fun openServerSelection() { - onOpenServerSelection() - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/PreferenceModule.kt similarity index 53% rename from app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/di/PreferenceModule.kt index f8a4644..b985bb9 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DrinkListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/PreferenceModule.kt @@ -24,29 +24,37 @@ package de.chaosdorf.meteroid.di -import de.chaosdorf.mete.DrinkId -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.Repository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import dagger.Binds +import dagger.Module +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 javax.inject.Singleton -interface DrinkViewModelFactory { - fun newInstance( - scope: CoroutineScope, - repository: Repository<DrinkId, Drink> - ) = DrinkListViewModelImpl(scope, repository) -} +val Context.accountDataStore: DataStore<Preferences> by preferencesDataStore(name = "account") -interface DrinkListViewModel { - val drinks: StateFlow<List<Drink>> +@Module +@InstallIn(SingletonComponent::class) +object PreferenceModule { + @Singleton + @Provides + fun provideAccountPreferences( + @ApplicationContext context: Context + ): DataStore<Preferences> = context.accountDataStore } -class DrinkListViewModelImpl( - scope: CoroutineScope, - repository: Repository<DrinkId, Drink> -) : DrinkListViewModel { - override val drinks: StateFlow<List<Drink>> = repository.getAllFlow() - .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) +@Module +@InstallIn(SingletonComponent::class) +abstract class AccountPreferenceModule { + @Binds + abstract fun bindsAccountPreferences( + impl: AccountPreferencesImpl + ): AccountPreferences } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt deleted file mode 100644 index 4e9ced5..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/RootViewModel.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.di - -import android.content.Context -import androidx.room.Room -import de.chaosdorf.mete.v1.MeteApiV1Factory -import de.chaosdorf.meteroid.MeteroidDatabase -import de.chaosdorf.meteroid.Repository -import de.chaosdorf.meteroid.SyncHandler -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId -import de.chaosdorf.meteroid.util.findBestIcon -import de.chaosdorf.meteroid.util.resolve -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -interface RootViewModelFactory { - fun newInstance( - scope: CoroutineScope, context: Context - ): RootViewModel = RootViewModelImpl(scope, context) -} - -interface RootViewModel { - val route: StateFlow<RootRoute> -} - -class ServerListSyncHandler( - private val repository: Repository<ServerId, Server>, -) : SyncHandler { - private val _state = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle) - override val state = _state - - override suspend fun doSync() { - _state.value = SyncHandler.State.Syncing - for (server in repository.getAll()) { - val api = MeteApiV1Factory.newInstance(server.url) - val manifest = api.getManifest() - val updated = server.copy( - name = manifest?.name, - logoUrl = manifest?.findBestIcon()?.resolve(server.url) - ) - repository.save(updated) - } - _state.value = SyncHandler.State.Idle - } -} - -class RootViewModelImpl( - scope: CoroutineScope, context: Context -) : RootViewModel { - private val db = Room.databaseBuilder(context, MeteroidDatabase::class.java, "mete").build() - - private val syncHandler = ServerListSyncHandler(db.server()) - - private val setupViewModelFactory: SetupViewModelFactory = object : SetupViewModelFactory {} - - private val mainLayoutViewModelFactory: MainLayoutViewModelFactory = - object : MainLayoutViewModelFactory {} - - private val _serverSelectionOpen = MutableStateFlow(false) - private val _serverId: MutableStateFlow<ServerId?> = MutableStateFlow(null) - private val _server: StateFlow<Server?> = _serverId.flatMapLatest { - it?.let { db.server().getFlow(it) } ?: flowOf(null) - }.stateIn(scope, SharingStarted.WhileSubscribed(), null) - override val route: StateFlow<RootRoute> = - combine(_server, _serverSelectionOpen) { server, serverSelectionOpen -> - if (server == null || serverSelectionOpen) { - RootRoute.Setup( - setupViewModelFactory.newInstance( - scope, - db, - db.server(), - server != null, - ::onSelect, - ::onCloseServerSelection - ) - ) - } else { - RootRoute.MainLayout( - mainLayoutViewModelFactory.newInstance( - scope, - db, - server, - ::onOpenServerSelection - ) - ) - } - }.stateIn(scope, SharingStarted.WhileSubscribed(), RootRoute.Init) - - init { - scope.launch { syncHandler.doSync() } - } - - private fun onSelect(server: ServerId) { - _serverId.value = server - _serverSelectionOpen.value = false - } - - private fun onOpenServerSelection() { - _serverSelectionOpen.value = true - } - - private fun onCloseServerSelection() { - _serverSelectionOpen.value = true - } -} - -sealed class RootRoute { - data object Init : RootRoute() - data class Setup(val viewModel: SetupViewModel) : RootRoute() - data class MainLayout(val viewModel: MainLayoutViewModel) : RootRoute() -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt deleted file mode 100644 index 639df3d..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/ServerSelectionViewModel.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.di - -import de.chaosdorf.meteroid.Repository -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - - -interface ServerSelectionViewModelFactory { - fun newInstance( - scope: CoroutineScope, - repository: Repository<ServerId, Server>, - hasSelectedServer: Boolean, - onAddServer: () -> Unit, - onSelect: (server: ServerId) -> Unit, - onClose: () -> Unit - ) = ServerSelectionViewModelImpl( - scope, - repository, - hasSelectedServer, - onAddServer, - onSelect, - onClose - ) -} - -interface ServerSelectionViewModel { - val servers: StateFlow<List<Server>> - val hasSelectedServer: Boolean - fun addServer() - fun select(server: ServerId) - fun remove(server: ServerId) - fun close() -} - -class ServerSelectionViewModelImpl( - private val scope: CoroutineScope, - private val repository: Repository<ServerId, Server>, - override val hasSelectedServer: Boolean, - private val onAddServer: () -> Unit, - private val onSelect: (server: ServerId) -> Unit, - private val onClose: () -> Unit -) : ServerSelectionViewModel { - override val servers: StateFlow<List<Server>> = - repository.getAllFlow().stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) - - override fun addServer() { - onAddServer() - } - - override fun select(server: ServerId) { - onSelect(server) - } - - override fun remove(server: ServerId) { - scope.launch { - repository.delete(server) - } - } - - override fun close() { - onClose() - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt deleted file mode 100644 index 16bbde1..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/SetupViewModel.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.di - -import androidx.room.withTransaction -import de.chaosdorf.mete.PwaManifest -import de.chaosdorf.meteroid.MeteroidDatabase -import de.chaosdorf.meteroid.Repository -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId -import de.chaosdorf.meteroid.util.findBestIcon -import de.chaosdorf.meteroid.util.resolve -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - - -interface SetupViewModelFactory { - fun newInstance( - scope: CoroutineScope, - db: MeteroidDatabase, - repository: Repository<ServerId, Server>, - hasSelectedServer: Boolean, - onSelect: (server: ServerId) -> Unit, - onClose: () -> Unit - ): SetupViewModel = SetupViewModelImpl( - scope, db, repository, hasSelectedServer, onSelect, onClose - ) -} - -interface SetupViewModel { - val route: StateFlow<SetupRoute> -} - -class SetupViewModelImpl( - private val scope: CoroutineScope, - private val db: MeteroidDatabase, - private val repository: Repository<ServerId, Server>, - private val hasSelectedServer: Boolean, - private val onSelect: (server: ServerId) -> Unit, - private val onClose: () -> Unit -) : SetupViewModel { - private val addServerViewModelFactory = object : AddServerViewModelFactory {} - private val serverSelectionViewModelFactory = object : ServerSelectionViewModelFactory {} - private val isFirstServer = repository.getAllFlow().map(List<Server>::isEmpty) - private val isAddingServer = MutableStateFlow(false) - override val route: StateFlow<SetupRoute> = - combine(isFirstServer, isAddingServer) { isFirstServer, addingServer -> - if (addingServer || isFirstServer) { - SetupRoute.AddServer( - addServerViewModelFactory.newInstance( - scope, isFirstServer, ::onSubmit, ::onCancel - ) - ) - } else { - SetupRoute.ServerSelection( - serverSelectionViewModelFactory.newInstance( - scope, repository, hasSelectedServer, ::onAddServer, onSelect, onClose - ) - ) - } - }.stateIn( - scope, SharingStarted.WhileSubscribed(), SetupRoute.ServerSelection( - serverSelectionViewModelFactory.newInstance( - scope, repository, hasSelectedServer, ::onAddServer, onSelect, onClose - ) - ) - ) - - private fun onAddServer() { - isAddingServer.value = true - } - - private fun onSubmit(url: String, manifest: PwaManifest?) { - isAddingServer.value = false - scope.launch { - db.withTransaction { - val lastId = repository.getAll().map(Server::id).maxByOrNull(ServerId::value)?.value ?: 0 - repository.save( - Server( - id = ServerId(lastId + 1), - url = url, - name = manifest?.name, - logoUrl = manifest?.findBestIcon()?.resolve(url) - ) - ) - } - } - } - - private fun onCancel() { - isAddingServer.value = false - } -} - -sealed class SetupRoute { - data class ServerSelection(val viewModel: ServerSelectionViewModel) : SetupRoute() - data class AddServer(val viewModel: AddServerViewModel) : SetupRoute() -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt deleted file mode 100644 index a31fbd8..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/InitRoute.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.routes - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable - -@Composable -fun InitRoute() { - Text("Loading…") -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt deleted file mode 100644 index c58ef42..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/RootRouter.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.routes - -import androidx.compose.runtime.Composable -import de.chaosdorf.meteroid.di.RootRoute -import de.chaosdorf.meteroid.routes.main.MainLayoutRoute -import de.chaosdorf.meteroid.routes.setup.SetupView - -@Composable -fun RootRouter(route: RootRoute) { - when (route) { - RootRoute.Init -> InitRoute() - is RootRoute.MainLayout -> MainLayoutRoute(viewModel = route.viewModel) - is RootRoute.Setup -> SetupView(viewModel = route.viewModel) - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt deleted file mode 100644 index 0f7b05e..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/SetupRouter.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.routes - -import androidx.compose.runtime.Composable -import de.chaosdorf.meteroid.di.SetupRoute -import de.chaosdorf.meteroid.routes.setup.AddServerRoute -import de.chaosdorf.meteroid.routes.setup.ServerSelectionRoute - -@Composable -fun SetupRouter(route: SetupRoute) { - when (route) { - is SetupRoute.AddServer -> AddServerRoute(viewModel = route.viewModel) - is SetupRoute.ServerSelection -> ServerSelectionRoute(viewModel = route.viewModel) - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt deleted file mode 100644 index 67907a2..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/DrinkList.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.routes.main - -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import de.chaosdorf.meteroid.di.DrinkListViewModel - -@Composable -fun DrinkList(viewModel: DrinkListViewModel) { - val drinks by viewModel.drinks.collectAsState() - LazyColumn { - items(drinks) { drink -> - Text(drink.name) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt deleted file mode 100644 index e009649..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/main/MainLayoutRoute.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.routes.main - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator -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 de.chaosdorf.meteroid.RepositorySyncHandler -import de.chaosdorf.meteroid.SyncHandler -import de.chaosdorf.meteroid.di.MainLayoutViewModel - -@Composable -fun MainLayoutRoute(viewModel: MainLayoutViewModel) { - val syncState by viewModel.syncState.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { - Text("Meteroid") - }, - navigationIcon = { - Icon( - Icons.AutoMirrored.Default.ArrowBack, - modifier = Modifier.clickable { - viewModel.openServerSelection() - }, - contentDescription = "Back" - ) - } - ) - } - ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - if (syncState == SyncHandler.State.Syncing) { - LinearProgressIndicator() - } - DrinkList(viewModel.drinkListViewModel) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt deleted file mode 100644 index 0866e26..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/AddServerRoute.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.routes.setup - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.LinearProgressIndicator -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.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import coil.compose.AsyncImage -import de.chaosdorf.meteroid.di.AddServerViewModel - -@Composable -fun AddServerRoute(viewModel: AddServerViewModel) { - val url by viewModel.url.collectAsState() - val server by viewModel.server.collectAsState() - val loading by viewModel.loading.collectAsState() - - Scaffold( - topBar = { - TopAppBar(title = { - Text("Add Server") - }) - } - ) { paddingValues -> - Column(modifier = Modifier.padding(paddingValues)) { - if (loading) { - LinearProgressIndicator() - } - - TextField( - label = { - Text("Server URL") - }, - value = url, - onValueChange = { value -> - viewModel.url.value = value - } - ) - - Button( - onClick = viewModel::submit - ) { - Text("Save") - } - - if (!viewModel.isFirstServer) { - Button( - onClick = viewModel::cancel - ) { - Text("Cancel") - } - } - - server?.let { - ListItem( - headlineContent = { Text(it.name ?: it.url) }, - leadingContent = { - AsyncImage(model = it.logoUrl, contentDescription = null) - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt deleted file mode 100644 index bee5ecc..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/ServerSelectionRoute.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.routes.setup - -import androidx.compose.foundation.clickable -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.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -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 coil.compose.AsyncImage -import de.chaosdorf.meteroid.di.ServerSelectionViewModel - -@Composable -fun ServerSelectionRoute(viewModel: ServerSelectionViewModel) { - val servers by viewModel.servers.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { Text("Select Server") }, - ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { viewModel.addServer() } - ) { - Icon( - Icons.Default.Add, - contentDescription = "Select Server" - ) - } - } - ) { paddingValues -> - LazyColumn(modifier = Modifier.padding(paddingValues)) { - items(servers) { server -> - ListItem( - modifier = Modifier.clickable { - viewModel.select(server.id) - }, - headlineContent = { Text(server.name ?: server.url) }, - leadingContent = { - AsyncImage(model = server.logoUrl, contentDescription = null) - }, - trailingContent = { - Icon( - Icons.Default.Delete, - modifier = Modifier.clickable { - viewModel.remove(server.id) - }, - contentDescription = "Delete" - ) - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt deleted file mode 100644 index 63de80b..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/routes/setup/SetupView.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.routes.setup - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import de.chaosdorf.meteroid.routes.SetupRouter -import de.chaosdorf.meteroid.di.SetupViewModel - -@Composable -fun SetupView(viewModel: SetupViewModel) { - val route by viewModel.route.collectAsState() - - SetupRouter(route) -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1cd49f7..6d134b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ androidx-compose-material = "1.5.0-alpha04" androidx-compose-material3 = "1.2.0-alpha10" androidx-compose-runtimetracing = "1.0.0-alpha04" androidx-compose-tooling = "1.6.0-alpha08" +androidx-hilt = "1.0.0" androidx-navigation = "2.7.4" androidx-room = "2.6.0" coil = "2.4.0" @@ -52,6 +53,7 @@ coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "dagger-hilt" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger-hilt" } +hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" } okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" } retrofit-core = { module = "com.squareup.retrofit2:retrofit", version = "2.9.0" } retrofit-converter-kotlinx = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version = "1.0.0" } diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json index ab7f5e7..e1642ac 100644 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json @@ -2,15 +2,27 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa", + "identityHash": "74b0d505a7abb27362a1ee0986b700e0", "entities": [ { "tableName": "Drink", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "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` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { - "fieldPath": "id", - "columnName": "id", + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "drinkId", + "columnName": "drinkId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", "affinity": "INTEGER", "notNull": true }, @@ -38,12 +50,6 @@ "affinity": "REAL", "notNull": true }, - { - "fieldPath": "active", - "columnName": "active", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "createdAt", "columnName": "createdAt", @@ -90,19 +96,32 @@ "primaryKey": { "autoGenerate": false, "columnNames": [ - "id" + "serverId", + "drinkId" ] }, "indices": [], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "Server", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "serverId" + ] + } + ] }, { "tableName": "Server", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))", + "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": "id", - "columnName": "id", + "fieldPath": "serverId", + "columnName": "serverId", "affinity": "INTEGER", "notNull": true }, @@ -128,17 +147,104 @@ "primaryKey": { "autoGenerate": false, "columnNames": [ - "id" + "serverId" ] }, "indices": [], "foreignKeys": [] + }, + { + "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 )", + "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": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "balance", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "audit", + "columnName": "audit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "drinkId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Server", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "serverId" + ] + } + ] } ], "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, 'e8f8c6c1efa75d20c3aa764efc3037aa')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '74b0d505a7abb27362a1ee0986b700e0')" ] } } \ 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 deleted file mode 100644 index 846eb8c..0000000 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/2.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa", - "entities": [ - { - "tableName": "Drink", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "volume", - "columnName": "volume", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "caffeine", - "columnName": "caffeine", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "price", - "columnName": "price", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "active", - "columnName": "active", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "createdAt", - "columnName": "createdAt", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "updatedAt", - "columnName": "updatedAt", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "logoUrl", - "columnName": "logoUrl", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoFileName", - "columnName": "logoFileName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoContentType", - "columnName": "logoContentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoFileSize", - "columnName": "logoFileSize", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "logoUpdatedAt", - "columnName": "logoUpdatedAt", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Server", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "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": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "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, 'e8f8c6c1efa75d20c3aa764efc3037aa')" - ] - } -} \ 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 deleted file mode 100644 index 67da874..0000000 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/3.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa", - "entities": [ - { - "tableName": "Drink", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "volume", - "columnName": "volume", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "caffeine", - "columnName": "caffeine", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "price", - "columnName": "price", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "active", - "columnName": "active", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "createdAt", - "columnName": "createdAt", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "updatedAt", - "columnName": "updatedAt", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "logoUrl", - "columnName": "logoUrl", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoFileName", - "columnName": "logoFileName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoContentType", - "columnName": "logoContentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoFileSize", - "columnName": "logoFileSize", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "logoUpdatedAt", - "columnName": "logoUpdatedAt", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Server", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "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": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "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, 'e8f8c6c1efa75d20c3aa764efc3037aa')" - ] - } -} \ 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 20c64ac..d360cc3 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt @@ -29,21 +29,25 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.DrinkDao +import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerDao +import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.model.UserRepository import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter @Database( version = 1, entities = [ Drink::class, - Server::class + Server::class, + User::class ], autoMigrations = [], ) @TypeConverters(value = [KotlinDatetimeTypeConverter::class]) abstract class MeteroidDatabase : RoomDatabase() { - abstract fun drinks(): DrinkDao - abstract fun server(): ServerDao + abstract fun drinks(): DrinkRepository + abstract fun server(): ServerRepository + abstract fun users(): UserRepository } 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 1bf745e..c943546 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt @@ -26,25 +26,29 @@ 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.PrimaryKey import androidx.room.Query import de.chaosdorf.mete.DrinkId import de.chaosdorf.mete.v1.DrinkModelV1 -import de.chaosdorf.meteroid.Repository import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant -@Entity +@Entity( + primaryKeys = ["serverId", "drinkId"], + foreignKeys = [ + ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE) + ] +) data class Drink( - @PrimaryKey - val id: DrinkId, + val serverId: ServerId, + val drinkId: DrinkId, + val active: Boolean, val name: String, val volume: Double, val caffeine: Int?, val price: Double, - val active: Boolean, val createdAt: Instant, val updatedAt: Instant, val logoUrl: String, @@ -54,13 +58,14 @@ data class Drink( val logoUpdatedAt: Instant ) { companion object { - fun fromModelV1(value: DrinkModelV1) = Drink( + fun fromModelV1(serverId: ServerId, value: DrinkModelV1) = Drink( + serverId, value.id, + value.active, value.name, value.bottleSize, value.caffeine, value.price, - value.active, value.createdAt, value.updatedAt, value.logoUrl, @@ -73,27 +78,25 @@ data class Drink( } @Dao -interface DrinkDao : Repository<DrinkId, Drink> { - override fun getKey(value: Drink): DrinkId = value.id - - @Query("SELECT * FROM Drink WHERE id = :id LIMIT 1") - override suspend fun get(id: DrinkId): Drink? +interface DrinkRepository { + @Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1") + suspend fun get(serverId: ServerId, drinkId: DrinkId): Drink? - @Query("SELECT * FROM Drink WHERE id = :id LIMIT 1") - override fun getFlow(id: DrinkId): Flow<Drink?> + @Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1") + fun getFlow(serverId: ServerId, drinkId: DrinkId): Flow<Drink?> - @Query("SELECT * FROM Drink") - override suspend fun getAll(): List<Drink> + @Query("SELECT * FROM Drink WHERE serverId = :serverId") + suspend fun getAll(serverId: ServerId): List<Drink> - @Query("SELECT * FROM Drink") - override fun getAllFlow(): Flow<List<Drink>> + @Query("SELECT * FROM Drink WHERE serverId = :serverId") + fun getAllFlow(serverId: ServerId): Flow<List<Drink>> @Insert(onConflict = OnConflictStrategy.REPLACE) - override suspend fun save(drink: Drink) + suspend fun save(drink: Drink) - @Query("DELETE FROM Drink WHERE id = :id") - override suspend fun delete(id: DrinkId) + @Query("DELETE FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId") + suspend fun delete(serverId: ServerId, drinkId: DrinkId) - @Query("DELETE FROM Drink") - override suspend fun deleteAll() + @Query("DELETE FROM Drink WHERE serverId = :serverId") + suspend fun deleteAll(serverId: ServerId) } diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt index c735ca8..8036071 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt @@ -30,8 +30,6 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.PrimaryKey import androidx.room.Query -import de.chaosdorf.mete.PwaManifest -import de.chaosdorf.meteroid.Repository import kotlinx.coroutines.flow.Flow @JvmInline @@ -40,34 +38,32 @@ value class ServerId(val value: Long) @Entity data class Server( @PrimaryKey - val id: ServerId, + val serverId: ServerId, val name: String?, val url: String, val logoUrl: String? ) @Dao -interface ServerDao : Repository<ServerId, Server> { - override fun getKey(value: Server): ServerId = value.id +interface ServerRepository { + @Query("SELECT * FROM Server WHERE serverId = :id LIMIT 1") + suspend fun get(id: ServerId): Server? - @Query("SELECT * FROM Server WHERE id = :id LIMIT 1") - override suspend fun get(id: ServerId): Server? - - @Query("SELECT * FROM Server WHERE id = :id LIMIT 1") - override fun getFlow(id: ServerId): Flow<Server?> + @Query("SELECT * FROM Server WHERE serverId = :id LIMIT 1") + fun getFlow(id: ServerId): Flow<Server?> @Query("SELECT * FROM Server") - override suspend fun getAll(): List<Server> + suspend fun getAll(): List<Server> @Query("SELECT * FROM Server") - override fun getAllFlow(): Flow<List<Server>> + fun getAllFlow(): Flow<List<Server>> @Insert(onConflict = OnConflictStrategy.REPLACE) - override suspend fun save(drink: Server) + suspend fun save(server: Server) - @Query("DELETE FROM Server WHERE id = :id") - override suspend fun delete(id: ServerId) + @Query("DELETE FROM Server WHERE serverId = :id") + suspend fun delete(id: ServerId) @Query("DELETE FROM Server") - override suspend fun deleteAll() + suspend fun deleteAll() } diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt new file mode 100644 index 0000000..c01e4b1 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt @@ -0,0 +1,95 @@ +/* + * 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.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"], + foreignKeys = [ + ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE) + ] +) +data class User( + val serverId: ServerId, + val drinkId: UserId, + val active: Boolean, + val name: String, + val email: String, + val balance: Double, + val audit: Boolean, + val redirect: Boolean, + val createdAt: Instant, + val updatedAt: Instant, +) { + companion object { + fun fromModelV1(serverId: ServerId, value: UserModelV1) = User( + serverId, + value.id, + value.active, + value.name, + value.email, + value.balance, + value.audit, + value.redirect, + value.createdAt, + value.updatedAt + ) + } +} + +@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 Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1") + fun getFlow(serverId: ServerId, drinkId: DrinkId): Flow<Drink?> + + @Query("SELECT * FROM Drink WHERE serverId = :serverId") + suspend fun getAll(serverId: ServerId): List<Drink> + + @Query("SELECT * FROM Drink WHERE serverId = :serverId") + fun getAllFlow(serverId: ServerId): Flow<List<Drink>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(drink: Drink) + + @Query("DELETE FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId") + suspend fun delete(serverId: ServerId, drinkId: DrinkId) + + @Query("DELETE FROM Drink WHERE serverId = :serverId") + suspend fun deleteAll(serverId: ServerId) +} -- GitLab