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