diff --git a/.editorconfig b/.editorconfig
index d9f5985b38a1a7bed21654eb1178b3412c73106f..4b100144d125236365e3089d5ca4476a7158b88c 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,11 +5,11 @@ insert_final_newline = true
 indent_style = space
 indent_size = 4
 
-[{*.mod, *.dtd, *.ent, *.elt}]
+[{*.mod,*.dtd,*.ent,*.elt}]
 indent_style = space
 indent_size = 2
 
-[{*.jhm, *.rng, *.wsdl, *.fxml, *.xslt, *.jrxml, *.ant, *.xul, *.xsl, *.xsd, *.tld, *.jnlp, *.xml}]
+[{*.jhm,*.rng,*.wsdl,*.fxml,*.xslt,*.jrxml,*.ant,*.xul,*.xsl,*.xsd,*.tld,*.jnlp,*.xml}]
 indent_style = space
 indent_size = 2
 
@@ -21,10 +21,10 @@ indent_size = 2
 indent_style = space
 indent_size = 2
 
-[{*.kts, *.kt}]
+[{*.kts,*.kt}]
 indent_style = space
 indent_size = 2
 
-[{*.yml, *.yaml}]
+[{*.yml,*.yaml}]
 indent_style = space
 indent_size = 2
diff --git a/README.md b/README.md
index d57404abbccc91b1cefa46bb6e469e4b17c12228..996a6b7bcd694fefa82ec4886cf8212cc788d962 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,11 @@
 ![logo](/metadata/en-US/images/featureGraphic.png)
 
 ### What
+
 Small Android application to use with mete, the Matekasse of Chaosdorf.
 
 ### Where
+
 * [Chaosdorf Wiki](https://wiki.chaosdorf.de/Meteroid)
 * [Google Play](https://play.google.com/store/apps/details?id=de.chaosdorf.meteroid2)
 * [F-Droid](https://f-droid.org/repository/browse/?fdid=de.chaosdorf.meteroid)
@@ -20,6 +22,7 @@ Small Android application to use with mete, the Matekasse of Chaosdorf.
 ![your logs](/metadata/en-US/images/phoneScreenshots/3.png)
 
 ### License
+
 The MIT License (MIT)
 
 Copyright (c) 2013-2016 Chaosdorf e.V.
@@ -46,17 +49,20 @@ THE SOFTWARE.
 
 The meteroid app stores a few things permanently on your device
 ("permanently" meaning, until you uninstall the app or clear its data):
+
 * the URL of your chosen mete instance
 * your user ID (just a number that identifies you)
 * your last five used items
 
 Additionally, some data may be cached temporarily:
+
 * your avatar
 * the avatars of other users
 * the images of drinks
 
 The rest of the data is kept in your mete instance. Please check its terms
 and make sure you trust its operators:
+
 * your chosen name (this can just be a nickname)
 * your e-mail address (optional)
 * your usage history (optional)
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
index 11454caf4ec4f1bf510f7f5bbd01081d21722863..49bac13ab00a0865841c98fe941f82ee24c596c3 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
 @JvmInline
 value class BarcodeId(val value: Long) {
   override fun toString() = value.toString()
+  fun isValid() = value >= 0L
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
index 4670a5f508c3481e33545108f39d1292bb12594a..1677d2ceabd51c6e79e6e5963e9dd6ef83a8e84c 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
 @JvmInline
 value class DrinkId(val value: Long) {
   override fun toString() = value.toString()
+  fun isValid() = value >= 0L
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
index ae0cee0199d3b3edd57a6a77c7ec6d1f6f6c0dd6..054f40c808e7e9896ef9e720a66d29a25dc16995 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
@@ -24,9 +24,6 @@
 
 package de.chaosdorf.mete.model
 
-import kotlinx.datetime.Instant
-import kotlinx.datetime.LocalDate
-import retrofit2.http.Query
 import java.math.BigDecimal
 
 interface MeteApi {
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt
new file mode 100644
index 0000000000000000000000000000000000000000..39880c07be311f29bcdf603b0e67c84b311cbed0
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.mete.model
+
+@JvmInline
+value class ServerId(val value: Long) {
+  override fun toString() = value.toString()
+  fun isValid() = value >= 0L
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
index 8b3a85e9462b794b507b18efa18d6426fa8f79b9..e1136cb73625675d42c7d6c1f3f3f57369f46483 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
 @JvmInline
 value class TransactionId(val value: Long) {
   override fun toString() = value.toString()
+  fun isValid() = value >= 0L
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
index 0eaacf4cd8980a88c6345034e324d89468110f63..b8f169c56cdfcdfce128aba38246a2479550285c 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
 @JvmInline
 value class UserId(val value: Long) {
   override fun toString() = value.toString()
+  fun isValid() = value >= 0L
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
index cc9f921706cbdf72c4c540809d589ff011430977..f0037dd8d3ef09b578259ee1471cb71abc952d1b 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
@@ -24,20 +24,11 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.model.BarcodeId
-import de.chaosdorf.mete.model.DrinkId
-import de.chaosdorf.mete.model.MeteApi
-import de.chaosdorf.mete.model.PwaManifest
-import de.chaosdorf.mete.model.TransactionSummaryModel
-import de.chaosdorf.mete.model.UserId
-import kotlinx.datetime.Instant
-import kotlinx.datetime.LocalDate
-import retrofit2.Response
+import de.chaosdorf.mete.model.*
 import retrofit2.http.GET
 import retrofit2.http.Path
 import retrofit2.http.Query
 import java.math.BigDecimal
-import java.time.Year
 
 internal interface MeteApiV1 : MeteApi {
   @GET("manifest.json")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8f3c4ae0b7802375e36e5acc930c88d30597358f..4aa80fa731550837833d1a05530150bef89b427e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
-  <uses-permission android:name="android.permission.INTERNET" />
+  <uses-permission android:name="android.permission.INTERNET"/>
 
   <application
     android:name=".MeteroidApplication"
@@ -9,16 +9,17 @@
     android:icon="@mipmap/ic_launcher"
     android:label="@string/application_name"
     android:supportsRtl="true"
+    android:hardwareAccelerated="true"
     android:usesCleartextTraffic="true">
     <activity
       android:name=".MainActivity"
       android:exported="true"
       android:theme="@style/Theme.Meteroid.SplashScreen">
       <intent-filter>
-        <action android:name="android.intent.action.MAIN" />
-        <action android:name="android.intent.action.VIEW" />
+        <action android:name="android.intent.action.MAIN"/>
+        <action android:name="android.intent.action.VIEW"/>
 
-        <category android:name="android.intent.category.LAUNCHER" />
+        <category android:name="android.intent.category.LAUNCHER"/>
       </intent-filter>
     </activity>
   </application>
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
index 9f09bde7fb819782f343fe4b01fc5d84cf31ef42..88155e5d01941e26ede102391c8aec6a0ce60acc 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
@@ -27,13 +27,14 @@ package de.chaosdorf.meteroid
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.get
 import dagger.hilt.android.AndroidEntryPoint
-import de.chaosdorf.meteroid.ui.AppRouter
-import de.chaosdorf.meteroid.ui.AppViewModel
-import de.chaosdorf.meteroid.ui.theme.MeteroidTheme
+import de.chaosdorf.meteroid.theme.MeteroidTheme
+import de.chaosdorf.meteroid.ui.MeteroidRouter
 
 
 @AndroidEntryPoint
@@ -41,15 +42,19 @@ class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     val viewModelProvider = ViewModelProvider(this)
-    val viewModel = viewModelProvider.get<AppViewModel>()
+    val viewModel = viewModelProvider.get<MeteroidViewModel>()
 
     installSplashScreen().setKeepOnScreenCondition {
-      viewModel.initialBackStack.value == null
+      viewModel.initialAccount.value == null
     }
 
     setContent {
+      val initialAccount by viewModel.initialAccount.collectAsState()
+
       MeteroidTheme {
-        AppRouter(viewModel)
+        if (initialAccount != null) {
+          MeteroidRouter(initialAccount!!)
+        }
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt
similarity index 72%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt
index 8e428018e60f9066cbd93fca483f202a6d5c34cf..96447ecd0d6a17823c30ffbdb625f61ddb727d00 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt
@@ -22,32 +22,36 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.servers
+package de.chaosdorf.meteroid
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
 import de.chaosdorf.meteroid.storage.AccountPreferences
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.stateIn
+import de.chaosdorf.meteroid.sync.SyncManager
+import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 @HiltViewModel
-class ServerListViewModel @Inject constructor(
+class MeteroidViewModel @Inject constructor(
+  accountPreferences: AccountPreferences,
   serverRepository: ServerRepository,
-  private val preferences: AccountPreferences
+  syncManager: SyncManager
 ) : ViewModel() {
-  val servers: StateFlow<List<Server>> = serverRepository.getAllFlow()
-    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+  val initialAccount: StateFlow<AccountPreferences.State?> = accountPreferences.state
+    .filterNotNull()
+    .take(1)
+    .stateIn(viewModelScope, SharingStarted.Eagerly, null)
 
-  fun selectServer(serverId: ServerId) {
+  init {
     viewModelScope.launch {
-      preferences.setServer(serverId)
+      serverRepository.getAllFlow().collectLatest { list ->
+        for (server in list) {
+          syncManager.sync(server, null, incremental = true)
+        }
+      }
     }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
index fc44d7da169b16b73cda524d0d04e4e4e9ca2a0c..71011f46607e09ce8c31dad9ecb3f865f646c474 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
@@ -34,11 +34,7 @@ import dagger.hilt.components.SingletonComponent
 import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.mete.v1.MeteApiV1Factory
 import de.chaosdorf.meteroid.MeteroidDatabase
-import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.PinnedUserRepository
-import de.chaosdorf.meteroid.model.ServerRepository
-import de.chaosdorf.meteroid.model.TransactionRepository
-import de.chaosdorf.meteroid.model.UserRepository
+import de.chaosdorf.meteroid.model.*
 import javax.inject.Singleton
 
 
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
index ebec013765cd18134d9875b51bd8597effefb52f..932b7ab4ba741c1a8ffe34c9dde332c4f4711ca6 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
@@ -24,8 +24,8 @@
 
 package de.chaosdorf.meteroid.storage
 
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.ServerId
 import kotlinx.coroutines.flow.Flow
 
 interface AccountPreferences {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
index e696e02aa68801ae1c56b39752a51e63d3b4e539..f74c0256e259f76f2cc20fe5720a5016eda0510c 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
@@ -29,8 +29,8 @@ 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.model.ServerId
 import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.ServerId
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.mapLatest
 import javax.inject.Inject
@@ -38,7 +38,6 @@ 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
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt
index 000fe2d76e1b111bf76556a31a8fa8f874bcbec0..0a37a4eca1c4b29a5a8736876e8186263f8ff297 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt
@@ -24,13 +24,10 @@
 
 package de.chaosdorf.meteroid.sync
 
+import android.util.Log
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.AccountInfo
-import de.chaosdorf.meteroid.model.PinnedUser
-import de.chaosdorf.meteroid.model.PinnedUserRepository
-import de.chaosdorf.meteroid.model.ServerId
-import de.chaosdorf.meteroid.model.ServerRepository
-import de.chaosdorf.meteroid.model.UserRepository
+import de.chaosdorf.meteroid.model.*
 import javax.inject.Inject
 
 class AccountProvider @Inject constructor(
@@ -49,8 +46,10 @@ class AccountProvider @Inject constructor(
 
   suspend fun togglePin(serverId: ServerId, userId: UserId) {
     if (pinnedUserRepository.isPinned(serverId, userId)) {
+      Log.e("DEBUG", "Unpinning $serverId, $userId")
       pinnedUserRepository.delete(serverId, userId)
     } else {
+      Log.e("DEBUG", "Pinning $serverId, $userId")
       pinnedUserRepository.save(PinnedUser(serverId, userId))
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
index b60189cf1f7d50e8938b861dbd754666e2d84a71..2af1ccefa2838cd46ee78e20c7c6bb743e951989 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
@@ -27,11 +27,11 @@ package de.chaosdorf.meteroid.sync
 import androidx.room.withTransaction
 import de.chaosdorf.mete.model.DrinkId
 import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.meteroid.MeteroidDatabase
 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.sync.base.BaseSyncHandler
 import javax.inject.Inject
 
@@ -44,7 +44,7 @@ class DrinkSyncHandler @Inject constructor(
     val server: ServerId, val drink: DrinkId
   )
 
-  override suspend fun <T>withTransaction(block: suspend () -> T): T =
+  override suspend fun <T> withTransaction(block: suspend () -> T): T =
     db.withTransaction(block)
 
   override suspend fun store(entry: Drink) =
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt
index 6f8563e8b12c8340dc6a3be6f7bf19aa165df0e6..359efb124fc894b144c77e0807f02f90ead7c4b3 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt
@@ -26,12 +26,10 @@ package de.chaosdorf.meteroid.sync
 
 import android.util.Log
 import de.chaosdorf.mete.model.MeteApiFactory
-import de.chaosdorf.meteroid.model.AccountInfo
-import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.ServerRepository
-import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.model.*
+import de.chaosdorf.meteroid.sync.base.SyncHandler
 import de.chaosdorf.meteroid.util.newServer
+import kotlinx.coroutines.flow.combine
 import java.math.BigDecimal
 import javax.inject.Inject
 
@@ -42,6 +40,16 @@ class SyncManager @Inject constructor(
   private val drinkSyncHandler: DrinkSyncHandler,
   private val transactionSyncHandler: TransactionSyncHandler
 ) {
+  val syncState = combine(
+    userSyncHandler.state,
+    drinkSyncHandler.state,
+    transactionSyncHandler.state
+  ) { states ->
+    states.find { it is SyncHandler.State.Error }
+      ?: states.find { it == SyncHandler.State.Loading }
+      ?: SyncHandler.State.Idle
+  }
+
   suspend fun checkOffline(server: Server): Boolean {
     val updated = factory.newServer(server.serverId, server.url)
     return if (updated == null) {
@@ -76,14 +84,16 @@ class SyncManager @Inject constructor(
     }
   }
 
-  suspend fun purchase(account: AccountInfo, drink: Drink) {
+  suspend fun purchase(account: AccountInfo, drink: Drink, count: Int) {
     val api = factory.newInstance(account.server.url)
     try {
       Log.i(
         "SyncManager",
         "Syncing purchase of ${drink.drinkId}/${drink.drinkId} for ${account.user.name}/${account.user.userId}"
       )
-      api.purchase(account.user.userId, drink.drinkId)
+      for (i in 0 until count) {
+        api.purchase(account.user.userId, drink.drinkId)
+      }
       sync(account.server, account.user, incremental = true)
     } catch (e: Exception) {
       Log.e(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt
index e9797e920269ed6075625fa00167ffbc24fb3878..88a584121cb0fdccbb6537ca726e525f8823d436 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt
@@ -26,20 +26,15 @@ package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
 import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.TransactionId
 import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.Transaction
 import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.sync.base.BaseIncrementalSyncHandler
-import kotlinx.datetime.Clock
-import kotlinx.datetime.DateTimeUnit
-import kotlinx.datetime.LocalDate
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.plus
-import kotlinx.datetime.toLocalDateTime
+import kotlinx.datetime.*
 import javax.inject.Inject
 
 class TransactionSyncHandler @Inject constructor(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
index 0f50531c4e5c95f2c30af6dca0e560e53d7632dd..13a0847be57dcb4c1d1f37eca3b15264173e42bc 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
@@ -26,10 +26,10 @@ package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
 import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.model.UserRepository
 import de.chaosdorf.meteroid.sync.base.BaseSyncHandler
@@ -44,7 +44,7 @@ class UserSyncHandler @Inject constructor(
     val server: ServerId, val user: UserId
   )
 
-  override suspend fun <T>withTransaction(block: suspend () -> T): T =
+  override suspend fun <T> withTransaction(block: suspend () -> T): T =
     db.withTransaction(block)
 
   override suspend fun store(entry: User) =
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt
index 0da621755a3ae4336e25e1623d131a4f22bf0232..27269b1d1ccf7bdefbf236691106a58cc1555601 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt
@@ -37,8 +37,8 @@ abstract class BaseIncrementalSyncHandler<Context, Entry, Key> :
   }
 
   override suspend fun syncIncremental(context: Context) {
-    if (syncState.compareAndSet(State.Idle, State.Loading) ||
-      syncState.compareAndSet(State.Error(), State.Loading)
+    if (syncState.compareAndSet(SyncHandler.State.Idle, SyncHandler.State.Loading) ||
+      syncState.compareAndSet(SyncHandler.State.Error(), SyncHandler.State.Loading)
     ) {
       Log.w(this::class.simpleName, "Started incremental sync")
       try {
@@ -65,11 +65,11 @@ abstract class BaseIncrementalSyncHandler<Context, Entry, Key> :
             }
           }
         }
-        syncState.value = State.Idle
+        syncState.value = SyncHandler.State.Idle
         Log.w(this::class.simpleName, "Finished incremental sync")
       } catch (e: Exception) {
         Log.e(this::class.simpleName, "Error while syncing data", e)
-        syncState.value = State.Error("Error while syncing data: $e")
+        syncState.value = SyncHandler.State.Error("Error while syncing data: $e")
       }
     } else {
       Log.w(this::class.simpleName, "Already syncing, disregarding sync request")
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt
index 00d33c17f74ae242e6eaa66755c147f1bbdee3c6..d9a5aa1d7919f0fcd37149e7cf5970284f195806 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt
@@ -28,24 +28,8 @@ import android.util.Log
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
-abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> {
-  sealed class State {
-    data object Idle : State()
-    data object Loading : State()
-    data class Error(val message: String = "") : State() {
-      override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return true
-      }
-
-      override fun hashCode(): Int {
-        return javaClass.hashCode()
-      }
-    }
-  }
-
-  abstract suspend fun <T>withTransaction(block: suspend () -> T): T
+abstract class BaseSyncHandler<Context, Entry, Key> : SyncHandler<Context> {
+  abstract suspend fun <T> withTransaction(block: suspend () -> T): T
   abstract suspend fun loadCurrent(context: Context): List<Entry>
 
   abstract suspend fun loadStored(context: Context): List<Entry>
@@ -55,12 +39,13 @@ abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> {
   abstract suspend fun delete(key: Key)
   abstract suspend fun store(entry: Entry)
 
-  protected val syncState = MutableStateFlow<State>(State.Idle)
-  val state: StateFlow<State> = syncState
+  protected val syncState = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle)
+  val state: StateFlow<SyncHandler.State> = syncState
 
   override suspend fun sync(context: Context) {
-    if (syncState.compareAndSet(State.Idle, State.Loading) ||
-      syncState.compareAndSet(State.Error(), State.Loading)) {
+    if (syncState.compareAndSet(SyncHandler.State.Idle, SyncHandler.State.Loading) ||
+      syncState.compareAndSet(SyncHandler.State.Error(), SyncHandler.State.Loading)
+    ) {
       Log.w(this::class.simpleName, "Started sync")
       try {
         val loadedEntries = loadCurrent(context)
@@ -77,11 +62,11 @@ abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> {
             store(loadedEntry)
           }
         }
-        syncState.value = State.Idle
+        syncState.value = SyncHandler.State.Idle
         Log.w(this::class.simpleName, "Finished sync")
       } catch (e: Exception) {
         Log.e(this::class.simpleName, "Error while syncing data", e)
-        syncState.value = State.Error("Error while syncing data: $e")
+        syncState.value = SyncHandler.State.Error("Error while syncing data: $e")
       }
     } else {
       Log.w(this::class.simpleName, "Already syncing, disregarding sync request")
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt
index 7046454852242e021d13a64684d63b46d18e4686..db02ae6d99c6edbff69d212475b17efe34bd63a5 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt
@@ -26,4 +26,20 @@ package de.chaosdorf.meteroid.sync.base
 
 interface SyncHandler<Context> {
   suspend fun sync(context: Context)
+
+  sealed class State {
+    data object Idle : State()
+    data object Loading : State()
+    data class Error(val message: String = "") : State() {
+      override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return true
+      }
+
+      override fun hashCode(): Int {
+        return javaClass.hashCode()
+      }
+    }
+  }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt
similarity index 74%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt
index e5687bd6dbc786aa5cbbf245c9556c154b4e9519..75577f73d858a18ee133e104233004a183580d46 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt
@@ -1,4 +1,28 @@
-package de.chaosdorf.meteroid.ui.theme
+/*
+ * 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.theme
 
 import androidx.compose.material3.ColorScheme
 import androidx.compose.ui.graphics.Color
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt
similarity index 93%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt
index 9affa43474069a9213ac68f9a7d1829bb3186976..83927e00dc2967ad30c1e1c454424d9b85e129ed 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt
@@ -22,17 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.theme
+package de.chaosdorf.meteroid.theme
 
 import android.app.Activity
 import android.os.Build
 import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.material3.*
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.SideEffect
 import androidx.compose.ui.graphics.toArgb
@@ -110,7 +105,7 @@ private val DarkColors = darkColorScheme(
 fun MeteroidTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   // Dynamic color is available on Android 12+
-  dynamicColor: Boolean = true,
+  dynamicColor: Boolean = false,
   content: @Composable () -> Unit
 ) {
   val colorScheme = when {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt
similarity index 92%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt
index 3de9104176787027cb604c102f7efa0cace00d48..9e61bf409df12428d22a235fa88271659a137918 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.theme
+package de.chaosdorf.meteroid.theme
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Brush
@@ -31,9 +31,9 @@ import androidx.compose.ui.graphics.TileMode
 
 class ThemeGradient(val colors: List<Color>) {
   fun linearGradient(
-      start: Offset = Offset.Zero,
-      end: Offset = Offset.Infinite,
-      tileMode: TileMode = TileMode.Clamp
+    start: Offset = Offset.Zero,
+    end: Offset = Offset.Infinite,
+    tileMode: TileMode = TileMode.Clamp
   ) = Brush.linearGradient(colors, start, end, tileMode)
 
   fun verticalGradient(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Type.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Type.kt
similarity index 95%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Type.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/Type.kt
index e6759ded7701183e45a771ab7713993b1a15df77..afd7a9efe90f0a1bfccba0f79d6a8a963464be72 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Type.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Type.kt
@@ -1,4 +1,4 @@
-package de.chaosdorf.meteroid.ui.theme
+package de.chaosdorf.meteroid.theme
 
 import androidx.compose.material3.Typography
 import androidx.compose.ui.text.TextStyle
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/MeteroidIcons.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/MeteroidIcons.kt
similarity index 96%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/MeteroidIcons.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/MeteroidIcons.kt
index 35ad107d1bf6d809bedde55c329bcb7b409d75c1..4489383abe08a15014823f92144aa4cc2ec8db40 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/MeteroidIcons.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/MeteroidIcons.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.icons
+package de.chaosdorf.meteroid.theme.icons
 
 object MeteroidIcons {
   object Outlined
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/filled/WaterFull.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/filled/WaterFull.kt
similarity index 58%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/filled/WaterFull.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/filled/WaterFull.kt
index 138d742bad8077c5b177675560f3978e86c83e8b..c4303fcf7d7d4910e8b690601c94ab9f77af56fe 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/filled/WaterFull.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/filled/WaterFull.kt
@@ -1,35 +1,27 @@
 /*
- * The MIT License (MIT)
+ * Copyright 2023 The Android Open Source Project
  *
- * Copyright (c) 2013-2023 Chaosdorf e.V.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
  *
- * 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:
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * 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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
 
-package de.chaosdorf.meteroid.icons.filled
+package de.chaosdorf.meteroid.theme.icons.filled
 
 import androidx.compose.material.icons.materialPath
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
-import de.chaosdorf.meteroid.icons.MeteroidIcons
+import de.chaosdorf.meteroid.theme.icons.MeteroidIcons
 
-public val MeteroidIcons.Filled.WaterFull: ImageVector
+val MeteroidIcons.Filled.WaterFull: ImageVector
   get() {
     if (_waterFull != null) {
       return _waterFull!!
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/outlined/WaterFull.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/outlined/WaterFull.kt
similarity index 95%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/outlined/WaterFull.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/outlined/WaterFull.kt
index 8f3d07520c90fd97f0f0d25e04fc382d5bd791cf..37d165368f2649e9a636aa8bbf0014de68a29424 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/outlined/WaterFull.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/outlined/WaterFull.kt
@@ -22,14 +22,14 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.icons.outlined
+package de.chaosdorf.meteroid.theme.icons.outlined
 
 import androidx.compose.material.icons.materialPath
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
-import de.chaosdorf.meteroid.icons.MeteroidIcons
+import de.chaosdorf.meteroid.theme.icons.MeteroidIcons
 
-public val MeteroidIcons.Outlined.WaterFull: ImageVector
+val MeteroidIcons.Outlined.WaterFull: ImageVector
   get() {
     if (_waterFull != null) {
       return _waterFull!!
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/twotone/WaterFull.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/twotone/WaterFull.kt
similarity index 96%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/twotone/WaterFull.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/twotone/WaterFull.kt
index d3eef3891ede3bf5de46246711ed4167c0594efb..772c5e56ba75b8b9f2df28a4bcacec21b1c1f638 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/twotone/WaterFull.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/twotone/WaterFull.kt
@@ -22,14 +22,14 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.icons.twotone
+package de.chaosdorf.meteroid.theme.icons.twotone
 
 import androidx.compose.material.icons.materialPath
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
-import de.chaosdorf.meteroid.icons.MeteroidIcons
+import de.chaosdorf.meteroid.theme.icons.MeteroidIcons
 
-public val MeteroidIcons.TwoTone.WaterFull: ImageVector
+val MeteroidIcons.TwoTone.WaterFull: ImageVector
   get() {
     if (_waterFull != null) {
       return _waterFull!!
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
deleted file mode 100644
index d10008bf48b8f8197e204f4ea8cfb819018897d8..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
+++ /dev/null
@@ -1,192 +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.ui
-
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.foundation.layout.Box
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.navigation.NavType
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navArgument
-import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.ServerId
-import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen
-import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel
-import de.chaosdorf.meteroid.ui.money.MoneyListScreen
-import de.chaosdorf.meteroid.ui.money.MoneyListViewModel
-import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
-import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
-import de.chaosdorf.meteroid.ui.navigation.Routes
-import de.chaosdorf.meteroid.ui.servers.AddServerScreen
-import de.chaosdorf.meteroid.ui.servers.AddServerViewModel
-import de.chaosdorf.meteroid.ui.servers.ServerListScreen
-import de.chaosdorf.meteroid.ui.servers.ServerListViewModel
-import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen
-import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel
-import de.chaosdorf.meteroid.ui.users.UserListScreen
-import de.chaosdorf.meteroid.ui.users.UserListViewModel
-import de.chaosdorf.meteroid.ui.wrapped.WrappedScreen
-import de.chaosdorf.meteroid.ui.wrapped.WrappedViewModel
-import de.chaosdorf.meteroid.util.popUpToRoot
-
-@Composable
-fun AppRouter(viewModel: AppViewModel) {
-  val navController = rememberNavController()
-  val snackbarHostState = remember { SnackbarHostState() }
-  val navigationViewModel = hiltViewModel<NavigationViewModel>()
-
-  val initialBackStack by viewModel.initialBackStack.collectAsState()
-  val offline by viewModel.offline.collectAsState()
-
-  navController.addOnDestinationChangedListener { _, _, arguments ->
-    val serverId = arguments?.getLong("server")?.let(::ServerId)
-    val userId = arguments?.getLong("user")?.let(::UserId)
-
-    navigationViewModel.serverId.value = serverId
-    navigationViewModel.userId.value = userId
-  }
-
-  LaunchedEffect(initialBackStack) {
-    initialBackStack?.let { initialBackStack ->
-      navController.popUpToRoot()
-      for (entry in initialBackStack) {
-        navController.navigate(entry)
-      }
-    }
-  }
-
-  LaunchedEffect(offline) {
-    snackbarHostState.currentSnackbarData?.dismiss()
-    if (offline) {
-      snackbarHostState.showSnackbar(
-        message = "Unable to connect to server",
-        duration = SnackbarDuration.Indefinite
-      )
-    }
-  }
-
-  Box {
-    Scaffold(
-      topBar = {
-        MeteroidTopBar(navController, navigationViewModel, Modifier.align(Alignment.TopCenter))
-      },
-      bottomBar = {
-        MeteroidBottomBar(navController, navigationViewModel)
-      },
-      snackbarHost = {
-        SnackbarHost(hostState = snackbarHostState)
-      }
-    ) { paddingValues ->
-      NavHost(
-        navController,
-        startDestination = Routes.Servers.List,
-        enterTransition = { EnterTransition.None },
-        exitTransition = { ExitTransition.None },
-        popEnterTransition = { EnterTransition.None },
-        popExitTransition = { ExitTransition.None }
-      ) {
-        composable(Routes.Servers.Add) { _ ->
-          val hiltViewModel = hiltViewModel<AddServerViewModel>()
-          AddServerScreen(navController, hiltViewModel, paddingValues)
-        }
-        composable(Routes.Servers.List) { _ ->
-          val hiltViewModel = hiltViewModel<ServerListViewModel>()
-          ServerListScreen(navController, hiltViewModel, paddingValues)
-        }
-        /*
-      composable(Routes.Users.Add) { _ ->
-        AddUserScreen(
-          hiltViewModel(),
-          onAdd = { navController.navigate(Routes.Users.List) }
-        )
-      }
-       */
-        composable(
-          Routes.Users.List,
-          arguments = listOf(
-            navArgument("server") { type = NavType.LongType }
-          )
-        ) { _ ->
-          val hiltViewModel = hiltViewModel<UserListViewModel>()
-          UserListScreen(navController, hiltViewModel, paddingValues)
-        }
-        composable(
-          Routes.Home.Purchase,
-          arguments = listOf(
-            navArgument("server") { type = NavType.LongType },
-            navArgument("user") { type = NavType.LongType },
-          )
-        ) { _ ->
-          val hiltViewModel = hiltViewModel<DrinkListViewModel>()
-          DrinkListScreen(navController, hiltViewModel, paddingValues)
-        }
-        composable(
-          Routes.Home.Deposit,
-          arguments = listOf(
-            navArgument("server") { type = NavType.LongType },
-            navArgument("user") { type = NavType.LongType },
-          )
-        ) { _ ->
-          val hiltViewModel = hiltViewModel<MoneyListViewModel>()
-          MoneyListScreen(navController, hiltViewModel, paddingValues)
-        }
-        composable(
-          Routes.Home.History,
-          arguments = listOf(
-            navArgument("server") { type = NavType.LongType },
-            navArgument("user") { type = NavType.LongType },
-          )
-        ) { _ ->
-          val hiltViewModel = hiltViewModel<TransactionViewModel>()
-          TransactionListScreen(hiltViewModel, paddingValues)
-        }
-        composable(
-          Routes.Home.Wrapped,
-          arguments = listOf(
-            navArgument("server") { type = NavType.LongType },
-            navArgument("user") { type = NavType.LongType },
-          )
-        ) { _ ->
-          val hiltViewModel = hiltViewModel<WrappedViewModel>()
-          WrappedScreen(hiltViewModel, paddingValues)
-        }
-      }
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
deleted file mode 100644
index cd55bf253e66d32b41082ab46188b3eca513f00c..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
+++ /dev/null
@@ -1,97 +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.ui
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.meteroid.model.ServerRepository
-import de.chaosdorf.meteroid.model.UserRepository
-import de.chaosdorf.meteroid.storage.AccountPreferences
-import de.chaosdorf.meteroid.sync.SyncManager
-import de.chaosdorf.meteroid.ui.navigation.Routes
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-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
-
-@HiltViewModel
-class AppViewModel @Inject constructor(
-  accountPreferences: AccountPreferences,
-  private val serverRepository: ServerRepository,
-  private val userRepository: UserRepository,
-  private val syncManager: SyncManager
-) : ViewModel() {
-  val initialBackStack = accountPreferences.state.flatMapLatest {
-    combine(
-      serverRepository.countFlow(),
-      if (it.server == null) flowOf(null)
-      else serverRepository.getFlow(it.server),
-      if (it.server == null || it.user == null) flowOf(null)
-      else userRepository.getFlow(it.server, it.user)
-    ) { serverCount, server, user ->
-      if (user != null) {
-        listOf(
-          Routes.Servers.List,
-          Routes.Users.list(user.serverId),
-          Routes.Home.purchase(user.serverId, user.userId)
-        )
-      } else if (server != null) {
-        listOf(
-          Routes.Servers.List,
-          Routes.Users.list(server.serverId)
-        )
-      } else if (serverCount > 0) {
-        listOf(Routes.Servers.List)
-      } else {
-        listOf(Routes.Servers.Add)
-      }
-    }
-  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
-
-  val offline = accountPreferences.state.flatMapLatest { state ->
-    state.server?.let { serverId ->
-      serverRepository.getFlow(serverId)
-    } ?: flowOf(null)
-  }.mapLatest { server ->
-    server?.let { syncManager.checkOffline(it) } ?: false
-  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
-
-  init {
-    viewModelScope.launch {
-      serverRepository.getAllFlow().distinctUntilChanged().collectLatest { list ->
-        for (server in list) {
-          syncManager.sync(server, null, incremental = false)
-        }
-      }
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt
new file mode 100644
index 0000000000000000000000000000000000000000..eb711a2986ef5049bea38bdbd59f12a432730c74
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt
@@ -0,0 +1,114 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import de.chaosdorf.meteroid.theme.icons.MeteroidIcons
+import de.chaosdorf.meteroid.theme.icons.filled.WaterFull
+
+@Preview
+@Composable
+fun UserAvatar(
+  source: String? = null,
+) {
+  var success by remember { mutableStateOf(false) }
+
+  AvatarLayout(
+    Modifier.clip(CircleShape)
+      .background(MaterialTheme.colorScheme.primaryContainer)
+  ) {
+    if (!success) {
+      Icon(
+        Icons.Filled.Person,
+        contentDescription = null,
+        tint = MaterialTheme.colorScheme.primary
+      )
+    }
+    AsyncImage(
+      source,
+      contentDescription = null,
+      contentScale = ContentScale.Crop,
+      modifier = Modifier.fillMaxSize(),
+      onSuccess = { success = true },
+      onError = { success = false },
+      onLoading = { success = false },
+    )
+  }
+}
+
+@Preview
+@Composable
+fun ServerAvatar(
+  source: String? = null,
+) {
+  var success by remember { mutableStateOf(false) }
+
+  AvatarLayout {
+    if (!success) {
+      Icon(
+        MeteroidIcons.Filled.WaterFull,
+        contentDescription = null
+      )
+    }
+    AsyncImage(
+      source,
+      contentDescription = null,
+      contentScale = ContentScale.Crop,
+      modifier = Modifier.fillMaxSize(),
+      onSuccess = { success = true },
+      onError = { success = false },
+      onLoading = { success = false },
+    )
+  }
+}
+
+@Composable
+fun AvatarLayout(
+  modifier: Modifier = Modifier,
+  content: @Composable () -> Unit
+) {
+  Box(
+    modifier.size(36.dp),
+    contentAlignment = Alignment.Center
+  ) {
+    content()
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3f10300bfafa6774218de42d14b18c793f43e661
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt
@@ -0,0 +1,173 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import android.util.Log
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
+import de.chaosdorf.mete.model.ServerId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.storage.AccountPreferences
+import de.chaosdorf.meteroid.ui.home.deposit.DepositScreen
+import de.chaosdorf.meteroid.ui.home.deposit.DepositViewModel
+import de.chaosdorf.meteroid.ui.home.purchase.PurchaseScreen
+import de.chaosdorf.meteroid.ui.home.purchase.PurchaseViewModel
+import de.chaosdorf.meteroid.ui.home.transactionhistory.TransactionHistoryScreen
+import de.chaosdorf.meteroid.ui.home.transactionhistory.TransactionHistoryViewModel
+import de.chaosdorf.meteroid.ui.home.wrapped.WrappedScreen
+import de.chaosdorf.meteroid.ui.home.wrapped.WrappedViewModel
+import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel
+import de.chaosdorf.meteroid.ui.servers.AddServerScreen
+import de.chaosdorf.meteroid.ui.servers.AddServerViewModel
+import de.chaosdorf.meteroid.ui.settings.SettingsScreen
+import de.chaosdorf.meteroid.ui.settings.SettingsViewModel
+import de.chaosdorf.meteroid.ui.userlist.UserListScreen
+import de.chaosdorf.meteroid.ui.userlist.UserListViewModel
+import de.chaosdorf.meteroid.util.toFancyString
+import kotlinx.coroutines.flow.collectLatest
+
+@Composable
+fun MeteroidRouter(
+  initialAccount: AccountPreferences.State,
+) {
+  val navController = rememberNavController()
+  val navigationViewModel = hiltViewModel<NavigationViewModel>()
+
+  LaunchedEffect(navController) {
+    navController.currentBackStack.collectLatest {
+      Log.i("MeteroidRouter", "Navigation: ${it.toFancyString()}")
+    }
+  }
+
+  MeteroidScaffold(navController, navigationViewModel) { paddingValues ->
+    NavHost(
+      navController = navController,
+      startDestination = MeteroidScreen.Home.Purchase.route,
+      enterTransition = { fadeIn() },
+      exitTransition = { fadeOut() },
+      popEnterTransition = { fadeIn() },
+      popExitTransition = { fadeOut() },
+      modifier = Modifier.padding(paddingValues)
+    ) {
+      composable(
+        MeteroidScreen.Home.Purchase.route, arguments = listOf(
+          navArgument("server") {
+            type = NavType.LongType
+            initialAccount.server?.let { defaultValue = it.value }
+          },
+          navArgument("user") {
+            type = NavType.LongType
+            initialAccount.user?.let { defaultValue = it.value }
+          },
+        )
+      ) { entry ->
+        val serverId = entry.arguments?.getLong("server")?.let(::ServerId)
+        val userId = entry.arguments?.getLong("user")?.let(::UserId)
+
+        LaunchedEffect(serverId, userId) {
+          if (serverId == null || userId == null) {
+            navigationViewModel.expanded.value = true
+          }
+        }
+
+        if (serverId != null && userId != null) {
+          val viewModel: PurchaseViewModel = hiltViewModel(
+            key = MeteroidScreen.Home.Purchase.build(serverId, userId)
+          )
+          PurchaseScreen(navController, viewModel, PaddingValues(top = 96.dp))
+        }
+      }
+      composable(
+        MeteroidScreen.Home.Deposit.route, arguments = listOf(
+          navArgument("server") {
+            type = NavType.LongType
+          },
+          navArgument("user") {
+            type = NavType.LongType
+          },
+        )
+      ) {
+        val viewModel: DepositViewModel = hiltViewModel()
+        DepositScreen(navController, viewModel, PaddingValues(top = 96.dp))
+      }
+      composable(
+        MeteroidScreen.Home.History.route, arguments = listOf(
+          navArgument("server") {
+            type = NavType.LongType
+          },
+          navArgument("user") {
+            type = NavType.LongType
+          },
+        )
+      ) {
+        val viewModel: TransactionHistoryViewModel = hiltViewModel()
+        TransactionHistoryScreen(viewModel, PaddingValues(top = 96.dp))
+      }
+      composable(
+        MeteroidScreen.Home.Wrapped.route, arguments = listOf(
+          navArgument("server") {
+            type = NavType.LongType
+          },
+          navArgument("user") {
+            type = NavType.LongType
+          },
+        )
+      ) {
+        val viewModel: WrappedViewModel = hiltViewModel()
+        WrappedScreen(viewModel, PaddingValues(top = 96.dp))
+      }
+      composable(
+        MeteroidScreen.UserList.route, arguments = listOf(
+          navArgument("server") {
+            type = NavType.LongType
+          },
+        )
+      ) {
+        val viewModel: UserListViewModel = hiltViewModel()
+        UserListScreen(navController, viewModel, PaddingValues(top = 96.dp))
+      }
+      composable(MeteroidScreen.AddServer.route) {
+        val viewModel: AddServerViewModel = hiltViewModel()
+        AddServerScreen(navController, viewModel, PaddingValues(top = 96.dp))
+      }
+      composable(MeteroidScreen.Settings.route) {
+        val viewModel: SettingsViewModel = hiltViewModel()
+        SettingsScreen(navController, viewModel, PaddingValues(top = 96.dp))
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2867b477adf23681eec65172e895dd72435fba8a
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt
@@ -0,0 +1,51 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavController
+import de.chaosdorf.meteroid.ui.home.MeteroidBottomBar
+import de.chaosdorf.meteroid.ui.navigation.MeteroidNavigation
+import de.chaosdorf.meteroid.ui.navigation.NavigationScrim
+import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel
+
+@Composable
+fun MeteroidScaffold(
+  navController: NavController,
+  viewModel: NavigationViewModel,
+  content: @Composable (PaddingValues) -> Unit
+) {
+  Box {
+    Scaffold(
+      bottomBar = { MeteroidBottomBar(navController, viewModel) },
+      content = content
+    )
+    NavigationScrim(viewModel)
+    MeteroidNavigation(navController, viewModel)
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f0c27244cb65a1f7a7603b7813a9da5bd689704b
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt
@@ -0,0 +1,118 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Celebration
+import androidx.compose.material.icons.outlined.History
+import androidx.compose.material.icons.outlined.LocalAtm
+import androidx.compose.material.icons.twotone.Celebration
+import androidx.compose.material.icons.twotone.History
+import androidx.compose.material.icons.twotone.LocalAtm
+import androidx.compose.ui.graphics.vector.ImageVector
+import de.chaosdorf.mete.model.ServerId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.theme.icons.MeteroidIcons
+import de.chaosdorf.meteroid.theme.icons.outlined.WaterFull
+import de.chaosdorf.meteroid.theme.icons.twotone.WaterFull
+
+sealed class MeteroidScreen(val route: String) {
+  sealed class Home(
+    val label: String,
+    val activeIcon: ImageVector,
+    val inactiveIcon: ImageVector,
+    route: String
+  ) : MeteroidScreen(route) {
+    data object Purchase : Home(
+      "Purchase",
+      MeteroidIcons.TwoTone.WaterFull,
+      MeteroidIcons.Outlined.WaterFull,
+      "server/{server}/user/{user}/purchase"
+    ) {
+      fun build(server: ServerId, user: UserId) = route
+        .replace("{server}", server.value.toString())
+        .replace("{user}", user.value.toString())
+    }
+
+    data object Deposit : Home(
+      "Deposit",
+      Icons.TwoTone.LocalAtm,
+      Icons.Outlined.LocalAtm,
+      "server/{server}/user/{user}/deposit"
+    ) {
+      fun build(server: ServerId, user: UserId) = route
+        .replace("{server}", server.value.toString())
+        .replace("{user}", user.value.toString())
+    }
+
+    data object History : Home(
+      "History",
+      Icons.TwoTone.History,
+      Icons.Outlined.History,
+      "server/{server}/user/{user}/history"
+    ) {
+      fun build(server: ServerId, user: UserId) = route
+        .replace("{server}", server.value.toString())
+        .replace("{user}", user.value.toString())
+    }
+
+    data object Wrapped : Home(
+      "Wrapped",
+      Icons.TwoTone.Celebration,
+      Icons.Outlined.Celebration,
+      "server/{server}/user/{user}/wrapped"
+    ) {
+      fun build(server: ServerId, user: UserId) = route
+        .replace("{server}", server.value.toString())
+        .replace("{user}", user.value.toString())
+    }
+  }
+
+  data object UserList : MeteroidScreen("server/{server}/userList") {
+    fun build(server: ServerId) = route
+      .replace("{server}", server.value.toString())
+  }
+
+  data object AddServer : MeteroidScreen("addServer") {
+    fun build() = route
+  }
+
+  data object Settings : MeteroidScreen("settings") {
+    fun build() = route
+  }
+
+  companion object {
+    fun byRoute(route: String?) = when (route) {
+      Home.Purchase.route -> Home.Purchase
+      Home.Deposit.route -> Home.Deposit
+      Home.History.route -> Home.History
+      Home.Wrapped.route -> Home.Wrapped
+      UserList.route -> UserList
+      AddServer.route -> AddServer
+      Settings.route -> Settings
+      else -> null
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt
deleted file mode 100644
index 29cd7a455b123957573b870498cac7cea25aed19..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt
+++ /dev/null
@@ -1,97 +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.ui
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.PinnedUserRepository
-import de.chaosdorf.meteroid.model.ServerId
-import de.chaosdorf.meteroid.model.ServerRepository
-import de.chaosdorf.meteroid.model.UserRepository
-import de.chaosdorf.meteroid.sync.AccountProvider
-import de.chaosdorf.meteroid.sync.SyncManager
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class NavigationViewModel @Inject constructor(
-  syncManager: SyncManager,
-  serverRepository: ServerRepository,
-  userRepository: UserRepository,
-  pinnedUserRepository: PinnedUserRepository,
-  private val accountProvider: AccountProvider
-) : ViewModel() {
-  val serverId = MutableStateFlow<ServerId?>(null)
-  val userId = MutableStateFlow<UserId?>(null)
-
-  val server = serverId.flatMapLatest {
-    if (it == null) flowOf(null)
-    else serverRepository.getFlow(it)
-  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
-
-  val user = combine(serverId, userId) { serverId, userId ->
-    if (serverId == null || userId == null) null
-    else Pair(serverId, userId)
-  }.flatMapLatest {
-    if (it == null) flowOf(null)
-    else userRepository.getFlow(it.first, it.second)
-  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
-
-  val pinned = combine(serverId, userId) { serverId, userId ->
-    if (serverId == null || userId == null) null
-    else Pair(serverId, userId)
-  }.flatMapLatest {
-    if (it == null) flowOf(null)
-    else pinnedUserRepository.isPinnedFlow(it.first, it.second)
-  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
-
-  init {
-    viewModelScope.launch {
-      combine(server, user) { server, user ->
-        server?.let { Pair(server, user) }
-      }.distinctUntilChanged().collectLatest { account ->
-        account?.let { (server, user) ->
-          syncManager.sync(server, user, incremental = true)
-        }
-      }
-    }
-  }
-
-  fun togglePin(serverId: ServerId, userId: UserId) {
-    viewModelScope.launch {
-      accountProvider.togglePin(serverId, userId)
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt
new file mode 100644
index 0000000000000000000000000000000000000000..789047bc7d895b5c7403e9d3082477007ba0d75c
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt
@@ -0,0 +1,130 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.home
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.navigation.NavController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import de.chaosdorf.meteroid.ui.MeteroidScreen
+import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel
+import de.chaosdorf.meteroid.util.findStartDestination
+import kotlinx.datetime.Clock
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+
+@Composable
+fun MeteroidBottomBar(navController: NavController, viewModel: NavigationViewModel) {
+  val backStackEntry by navController.currentBackStackEntryAsState()
+  val activeRoute = MeteroidScreen.byRoute(backStackEntry?.destination?.route)
+
+  val account by viewModel.account.collectAsState()
+  val server = account?.server
+  val user = account?.user
+  val historyDisabled by viewModel.historyDisabled.collectAsState()
+  val wrappedEnabled = Clock.System.now()
+    .toLocalDateTime(TimeZone.currentSystemDefault())
+    .month.let { it == java.time.Month.DECEMBER }
+
+  AnimatedVisibility(
+    activeRoute is MeteroidScreen.Home && server != null && user != null,
+    enter = slideInVertically(initialOffsetY = { it }),
+    exit = slideOutVertically(targetOffsetY = { it })
+  ) {
+    NavigationBar(
+      contentColor = MaterialTheme.colorScheme.primary
+    ) {
+      MeteroidBottomBarItem(
+        MeteroidScreen.Home.Purchase,
+        activeRoute == MeteroidScreen.Home.Purchase,
+      ) {
+        if (server != null && user != null) {
+          navController.navigate(MeteroidScreen.Home.Purchase.build(server, user)) {
+            launchSingleTop = true
+            restoreState = false
+            popUpTo(findStartDestination(navController.graph).id) {
+              saveState = false
+            }
+          }
+        }
+      }
+
+      MeteroidBottomBarItem(
+        MeteroidScreen.Home.Deposit,
+        activeRoute == MeteroidScreen.Home.Deposit,
+      ) {
+        if (server != null && user != null) {
+          navController.navigate(MeteroidScreen.Home.Deposit.build(server, user)) {
+            launchSingleTop = true
+            restoreState = false
+            popUpTo(findStartDestination(navController.graph).id) {
+              saveState = false
+            }
+          }
+        }
+      }
+
+      if (!historyDisabled) {
+        MeteroidBottomBarItem(
+          MeteroidScreen.Home.History,
+          activeRoute == MeteroidScreen.Home.History,
+        ) {
+          if (server != null && user != null) {
+            navController.navigate(MeteroidScreen.Home.History.build(server, user)) {
+              launchSingleTop = true
+              restoreState = false
+              popUpTo(findStartDestination(navController.graph).id) {
+                saveState = false
+              }
+            }
+          }
+        }
+
+        if (wrappedEnabled) {
+          MeteroidBottomBarItem(
+            MeteroidScreen.Home.Wrapped,
+            activeRoute == MeteroidScreen.Home.Wrapped,
+          ) {
+            if (server != null && user != null) {
+              navController.navigate(MeteroidScreen.Home.Wrapped.build(server, user)) {
+                launchSingleTop = true
+                restoreState = false
+                popUpTo(findStartDestination(navController.graph).id) {
+                  saveState = false
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..052ee599f2f4806582776a3480ed278fe52b049a
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt
@@ -0,0 +1,54 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.home
+
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import de.chaosdorf.meteroid.theme.onPrimaryContainerTinted
+import de.chaosdorf.meteroid.ui.MeteroidScreen
+
+@Composable
+fun RowScope.MeteroidBottomBarItem(
+  route: MeteroidScreen.Home,
+  active: Boolean,
+  onClick: () -> Unit,
+) {
+  NavigationBarItem(
+    icon = {
+      Icon(
+        if (active) route.activeIcon else route.inactiveIcon,
+        contentDescription = null,
+        tint = MaterialTheme.colorScheme.onPrimaryContainerTinted
+      )
+    },
+    label = { Text(route.label, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) },
+    selected = active,
+    onClick = onClick,
+    colors = NavigationBarItemDefaults.colors(
+      indicatorColor = MaterialTheme.colorScheme.primaryContainer
+    )
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt
similarity index 97%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt
index 2231c375270f284fcd144aa8f4cf47e4e33b494c..8a90ba385131642c3f0323287f0be815d4ebffd6 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui
+package de.chaosdorf.meteroid.ui.home
 
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.Badge
@@ -33,7 +33,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import java.math.BigDecimal
 
 @Composable
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt
similarity index 85%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt
index c503e5e28915afdf07517fab17c1224c814eaa74..1e83c4f935582132652c5ee88f214fa9abc81047 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt
@@ -22,15 +22,11 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.money
+package de.chaosdorf.meteroid.ui.home.deposit
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
@@ -39,10 +35,10 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
-import de.chaosdorf.meteroid.ui.PriceBadge
+import de.chaosdorf.meteroid.ui.home.PriceBadge
 
 @Composable
-fun MoneyTile(
+fun DepositMoneyItem(
   item: MonetaryAmount,
   modifier: Modifier = Modifier,
   onDeposit: (MonetaryAmount) -> Unit = {}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt
similarity index 91%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt
index ee78739b0423b409ab25851b39a1e58b63fd8fdc..339520e5d35c339c0fd71585893adf9e37f1d861 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.money
+package de.chaosdorf.meteroid.ui.home.deposit
 
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
@@ -35,9 +35,9 @@ import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
 
 @Composable
-fun MoneyListScreen(
+fun DepositScreen(
   navController: NavController,
-  viewModel: MoneyListViewModel,
+  viewModel: DepositViewModel,
   contentPadding: PaddingValues = PaddingValues(),
 ) {
   LazyVerticalGrid(
@@ -47,9 +47,9 @@ fun MoneyListScreen(
   ) {
     items(
       viewModel.money,
-      key = { "${it.ordinal}" },
+      key = { "deposit-${it.ordinal}" },
     ) { monetaryAmount ->
-      MoneyTile(monetaryAmount) {
+      DepositMoneyItem(monetaryAmount) {
         viewModel.deposit(it, navController::navigateUp)
       }
     }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt
similarity index 75%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt
index 105d5dba05df8f6f5dbe4b0b3da21a5e8481fd86..82ad5ee33f2b712ac83e63d2336e30d70b6dcb92 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt
@@ -22,34 +22,21 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.money
+package de.chaosdorf.meteroid.ui.home.deposit
 
-import androidx.annotation.DrawableRes
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.R
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.SyncManager
 import kotlinx.coroutines.launch
-import java.math.BigDecimal
 import javax.inject.Inject
 
-enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) {
-  MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50),
-  MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100),
-  MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200),
-  MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500),
-  MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000),
-  MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000),
-  MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000),
-}
-
 @HiltViewModel
-class MoneyListViewModel @Inject constructor(
+class DepositViewModel @Inject constructor(
   savedStateHandle: SavedStateHandle,
   private val accountProvider: AccountProvider,
   private val syncManager: SyncManager,
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a38f63060a6a2390be36737fe162872582545149
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt
@@ -0,0 +1,39 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.home.deposit
+
+import androidx.annotation.DrawableRes
+import de.chaosdorf.meteroid.R
+import java.math.BigDecimal
+
+enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) {
+  MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50),
+  MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100),
+  MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200),
+  MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500),
+  MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000),
+  MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000),
+  MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000),
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt
similarity index 74%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt
index 9a6d9727b187f57643d370aa0b431ab0fa628918..274c9b1e58c599beabdee793a3f23f1f43346989 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt
@@ -22,26 +22,17 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.drinks
+package de.chaosdorf.meteroid.ui.home.purchase
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.*
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
@@ -49,23 +40,30 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.sample.SampleDrinkProvider
-import de.chaosdorf.meteroid.ui.PriceBadge
-import de.chaosdorf.meteroid.ui.theme.secondaryGradient
+import de.chaosdorf.meteroid.theme.secondaryGradient
+import de.chaosdorf.meteroid.ui.home.PriceBadge
+import kotlinx.coroutines.delay
 import java.math.BigDecimal
 
-@Preview(widthDp = 120, showBackground = true)
 @Composable
-fun DrinkTile(
-  @PreviewParameter(SampleDrinkProvider::class) item: Drink,
+fun PurchaseDrinkTile(
+  item: Drink,
   modifier: Modifier = Modifier,
-  onPurchase: (Drink) -> Unit = {}
+  onPurchase: (Drink, Int) -> Unit = { _, _ -> }
 ) {
+  var purchaseCount by remember { mutableStateOf(0) }
+  val pendingPurchases = purchaseCount != 0
+
+  LaunchedEffect(purchaseCount) {
+    delay(2000L)
+    onPurchase(item, purchaseCount)
+    purchaseCount = 0
+  }
+
   val thumbPainter = rememberAsyncImagePainter(
     item.logoUrl
   )
@@ -79,25 +77,36 @@ fun DrinkTile(
       .height(IntrinsicSize.Max)
       .alpha(if (item.active) 1.0f else 0.67f)
       .clip(RoundedCornerShape(8.dp))
-      .clickable { onPurchase(item) }
+      .clickable { purchaseCount += 1 }
       .padding(8.dp)
   ) {
-    Box {
+    Box(
+      Modifier.aspectRatio(1.0f)
+        .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient(), CircleShape),
+      contentAlignment = Alignment.Center
+    ) {
       Image(
         drinkPainter,
         contentDescription = null,
         contentScale = ContentScale.Fit,
-        modifier = Modifier
-          .aspectRatio(1.0f)
+        modifier = Modifier.alpha(if (pendingPurchases) 0.0f else 1.0f)
           .clip(CircleShape)
-          .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient())
       )
       PriceBadge(
         item.price,
         modifier = Modifier
+          .alpha(if (pendingPurchases) 0.0f else 1.0f)
           .align(Alignment.BottomEnd)
           .paddingFromBaseline(bottom = 12.dp)
       )
+      Text(
+        "×$purchaseCount",
+        fontSize = 36.sp,
+        fontWeight = FontWeight.Light,
+        color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f),
+        textAlign = TextAlign.Center,
+        modifier = Modifier.alpha(if (pendingPurchases) 1.0f else 0.0f)
+      )
     }
     Spacer(Modifier.height(4.dp))
     Text(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt
similarity index 87%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt
index cba1aa89f3f8cf26802abe66535fd6a60c5c6a5a..807b2eca4e238fd1b3feb725f2694e6358ac9f1f 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt
@@ -22,22 +22,18 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.drinks
+package de.chaosdorf.meteroid.ui.home.purchase
 
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.FilterChip
-import androidx.compose.material3.FilterChipDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import androidx.compose.material3.*
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 
 @Composable
-fun DrinkListFilterChip(
+fun PurchaseFilterChip(
   label: String,
   selected: Boolean,
   onClick: () -> Unit,
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt
similarity index 72%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt
index 3e7db6e9a87d2a7ef2ae5fd0e0fcaaebae9f72ef..74f49ebbd24c61947207cae0fd3e3814200b7738 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt
@@ -22,13 +22,9 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.drinks
+package de.chaosdorf.meteroid.ui.home.purchase
 
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
@@ -42,9 +38,9 @@ import androidx.navigation.NavController
 
 @OptIn(ExperimentalLayoutApi::class)
 @Composable
-fun DrinkListScreen(
+fun PurchaseScreen(
   navController: NavController,
-  viewModel: DrinkListViewModel,
+  viewModel: PurchaseViewModel,
   contentPadding: PaddingValues = PaddingValues(),
 ) {
   val drinks by viewModel.drinks.collectAsState()
@@ -60,25 +56,25 @@ fun DrinkListScreen(
         modifier = Modifier.padding(horizontal = 16.dp),
         horizontalArrangement = Arrangement.spacedBy(8.dp)
       ) {
-        DrinkListFilterChip(
+        PurchaseFilterChip(
           label = "Active",
-          selected = filters.contains(DrinkListViewModel.Filter.Active),
-          onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.Active) }
+          selected = filters.contains(PurchaseViewModel.Filter.Active),
+          onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.Active) }
         )
-        DrinkListFilterChip(
+        PurchaseFilterChip(
           label = "Coffeine Free",
-          selected = filters.contains(DrinkListViewModel.Filter.CaffeineFree),
-          onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.CaffeineFree) }
+          selected = filters.contains(PurchaseViewModel.Filter.CaffeineFree),
+          onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.CaffeineFree) }
         )
       }
     }
 
     items(
       drinks,
-      key = { "${it.serverId}-${it.drinkId}" },
+      key = { "drink-${it.serverId}-${it.drinkId}" },
     ) { drink ->
-      DrinkTile(drink) {
-        viewModel.purchase(it, navController::navigateUp)
+      PurchaseDrinkTile(drink) { item, count ->
+        viewModel.purchase(item, count, navController::navigateUp)
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt
similarity index 89%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt
index 59f2d5328a027b50372397e19e50a6ee3cd795eb..16302040e89779d76082a852e612071d6bc435f9 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt
@@ -22,16 +22,16 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.drinks
+package de.chaosdorf.meteroid.ui.home.purchase
 
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.SyncManager
 import de.chaosdorf.meteroid.util.update
@@ -43,14 +43,14 @@ import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 @HiltViewModel
-class DrinkListViewModel @Inject constructor(
+class PurchaseViewModel @Inject constructor(
   private val savedStateHandle: SavedStateHandle,
   private val accountProvider: AccountProvider,
   private val syncManager: SyncManager,
   drinkRepository: DrinkRepository,
 ) : ViewModel() {
-  private val serverId = ServerId(checkNotNull(savedStateHandle["server"]))
-  private val userId = UserId(checkNotNull(savedStateHandle["user"]))
+  val serverId = ServerId(checkNotNull(savedStateHandle["server"]))
+  val userId = UserId(checkNotNull(savedStateHandle["user"]))
 
   val filters: StateFlow<Set<Filter>> =
     savedStateHandle.getStateFlow("filters", setOf(Filter.Active))
@@ -71,10 +71,10 @@ class DrinkListViewModel @Inject constructor(
     }
   }
 
-  fun purchase(item: Drink, onBack: () -> Unit) {
+  fun purchase(item: Drink, count: Int, onBack: () -> Unit) {
     viewModelScope.launch {
       accountProvider.account(serverId, userId)?.let { account ->
-        syncManager.purchase(account, item)
+        syncManager.purchase(account, item, count)
         if (!account.pinned) {
           onBack()
         }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt
similarity index 90%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt
index 7b5eaad3edd421127811b266eb2529e41d873ed6..71d5353aa47fab38afa041d787c7bd9c6a44cb4c 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt
@@ -22,15 +22,11 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.transactions
+package de.chaosdorf.meteroid.ui.home.transactionhistory
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AttachMoney
@@ -48,8 +44,8 @@ import androidx.compose.ui.unit.dp
 import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.Transaction
-import de.chaosdorf.meteroid.ui.PriceBadge
-import de.chaosdorf.meteroid.ui.theme.secondaryGradient
+import de.chaosdorf.meteroid.theme.secondaryGradient
+import de.chaosdorf.meteroid.ui.home.PriceBadge
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toJavaLocalDateTime
 import kotlinx.datetime.toLocalDateTime
@@ -58,7 +54,7 @@ import java.time.format.DateTimeFormatter
 import java.time.format.FormatStyle
 
 @Composable
-fun TransactionListItem(
+fun TransactionHistoryItem(
   transaction: Transaction,
   drink: Drink?,
   modifier: Modifier = Modifier
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt
similarity index 86%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt
index 7d208d9f3a1cf1f072297149ee1fb9f39fcc1508..f6cbec97ea4118616f90cdf85328676da637fbcb 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.transactions
+package de.chaosdorf.meteroid.ui.home.transactionhistory
 
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.lazy.LazyColumn
@@ -32,8 +32,8 @@ import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 
 @Composable
-fun TransactionListScreen(
-  viewModel: TransactionViewModel,
+fun TransactionHistoryScreen(
+  viewModel: TransactionHistoryViewModel,
   contentPadding: PaddingValues = PaddingValues(),
 ) {
   val transactions by viewModel.transactions.collectAsState()
@@ -41,9 +41,9 @@ fun TransactionListScreen(
   LazyColumn(contentPadding = contentPadding) {
     items(
       transactions,
-      key = { "${it.transaction.serverId}-${it.transaction.transactionId}" },
+      key = { "transaction-${it.transaction.serverId}-${it.transaction.transactionId}" },
     ) { (transaction, drink) ->
-      TransactionListItem(transaction, drink)
+      TransactionHistoryItem(transaction, drink)
     }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt
similarity index 91%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt
index 493608633005e53b33f297aeac661d5a46a696ca..0ad8aacac4d956c84fd73bac2d1143fe576b6311 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt
@@ -22,30 +22,26 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.transactions
+package de.chaosdorf.meteroid.ui.home.transactionhistory
 
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.SyncManager
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.launch
 import java.math.BigDecimal
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.minutes
 
 @HiltViewModel
-class TransactionViewModel @Inject constructor(
+class TransactionHistoryViewModel @Inject constructor(
   savedStateHandle: SavedStateHandle,
   private val accountProvider: AccountProvider,
   private val syncManager: SyncManager,
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt
similarity index 92%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt
index c4654ce50a48a99930da29c83ba6431a7c78d504..15e2038601aca582d78b1afe9f3e58be3254907d 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt
@@ -22,12 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.transactions
+package de.chaosdorf.meteroid.ui.home.transactionhistory
 
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.Transaction
 
 data class TransactionInfo(
-    val transaction: Transaction,
-    val drink: Drink?
+  val transaction: Transaction,
+  val drink: Drink?
 )
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt
similarity index 93%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt
index 6d8e2ccca3787858b80a7074f916732c0e986887..c08ee4d6400f728fa78542784add5c94d73e0057 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt
@@ -22,8 +22,10 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.wrapped
+package de.chaosdorf.meteroid.ui.home.wrapped
 
+import android.os.Build.VERSION
+import android.os.Build.VERSION_CODES
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.size
@@ -49,11 +51,16 @@ fun WrappedScreen(
   contentPadding: PaddingValues = PaddingValues(),
 ) {
   val slides by viewModel.slides.collectAsState()
+  @Suppress("DEPRECATION")
+  val locale = LocalConfiguration.current.let {
+    if (VERSION.SDK_INT >= VERSION_CODES.N) it.locales.get(0)
+    else it.locale
+  }
 
   LazyColumn(contentPadding = contentPadding) {
     items(
       slides,
-      key = { "${it::class.qualifiedName}" },
+      key = { "wrapped-${it::class.simpleName}" },
     ) { slide ->
       when (slide) {
         is WrappedSlide.MostBoughtDrink ->
@@ -124,10 +131,7 @@ fun WrappedScreen(
             headlineContent = {
               Text(
                 "You were most active on ${
-                  slide.weekday.getDisplayName(
-                    TextStyle.FULL,
-                    LocalConfiguration.current.locale
-                  )
+                  slide.weekday.getDisplayName(TextStyle.FULL, locale)
                 }s at ${slide.hour} o'clock."
               )
             },
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt
similarity index 89%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt
index c326c1ab3bf52635be509e7966c161e060ee313e..ec264b6d77d268b361ef974abb7aff4af74c22a9 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.wrapped
+package de.chaosdorf.meteroid.ui.home.wrapped
 
 import de.chaosdorf.mete.model.DrinkId
 import de.chaosdorf.meteroid.model.Drink
@@ -34,19 +34,19 @@ import kotlinx.datetime.toLocalDateTime
 sealed class WrappedSlide {
   interface Factory {
     fun create(
-        transactions: List<Transaction>,
-        drinks: Map<DrinkId, Drink>
+      transactions: List<Transaction>,
+      drinks: Map<DrinkId, Drink>
     ): WrappedSlide?
   }
 
   data class MostBoughtDrink(
-      val drink: Drink,
-      val count: Int,
+    val drink: Drink,
+    val count: Int,
   ) : WrappedSlide() {
     companion object : Factory {
       override fun create(
-          transactions: List<Transaction>,
-          drinks: Map<DrinkId, Drink>
+        transactions: List<Transaction>,
+        drinks: Map<DrinkId, Drink>
       ): WrappedSlide? = transactions
         .mapNotNull { drinks[it.drinkId] }
         .groupingBy { it }
@@ -84,8 +84,8 @@ sealed class WrappedSlide {
 
     companion object : Factory {
       override fun create(
-          transactions: List<Transaction>,
-          drinks: Map<DrinkId, Drink>
+        transactions: List<Transaction>,
+        drinks: Map<DrinkId, Drink>
       ): WrappedSlide = transactions
         .mapNotNull { drinks[it.drinkId] }
         .mapNotNull { drink -> drink.caffeine?.let { it * drink.volume.toDouble() * 10 } }
@@ -93,7 +93,7 @@ sealed class WrappedSlide {
         .let { dosage ->
           Caffeine(
             dosage,
-            Animal.values()
+            Animal.entries
               .sortedBy(Animal::lethalDosage)
               .lastOrNull { it.lethalDosage < dosage }
           )
@@ -102,13 +102,13 @@ sealed class WrappedSlide {
   }
 
   data class MostActive(
-      val weekday: DayOfWeek,
-      val hour: Int,
+    val weekday: DayOfWeek,
+    val hour: Int,
   ) : WrappedSlide() {
     companion object : Factory {
       override fun create(
-          transactions: List<Transaction>,
-          drinks: Map<DrinkId, Drink>
+        transactions: List<Transaction>,
+        drinks: Map<DrinkId, Drink>
       ): WrappedSlide? = transactions
         .map { it.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) }
         .groupingBy { Pair(it.dayOfWeek, it.hour) }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt
similarity index 91%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt
index a3fceec55d9ea40e15dd6c7ec9435e1d5c13589b..91e1f7d4666784c7874a3100c5ddfb4e82634769 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt
@@ -22,29 +22,24 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.wrapped
+package de.chaosdorf.meteroid.ui.home.wrapped
 
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.Transaction
 import de.chaosdorf.meteroid.model.TransactionRepository
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.datetime.Clock
-import kotlinx.datetime.LocalDateTime
-import kotlinx.datetime.Month
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.toInstant
-import kotlinx.datetime.toLocalDateTime
+import kotlinx.datetime.*
 import javax.inject.Inject
 
 @HiltViewModel
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt
deleted file mode 100644
index a8c0369fbc83085b632dc954a0ea3532844ad38e..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt
+++ /dev/null
@@ -1,101 +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.ui.navigation
-
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarItem
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.navigation.NavController
-import androidx.navigation.compose.currentBackStackEntryAsState
-import de.chaosdorf.meteroid.ui.NavigationViewModel
-import de.chaosdorf.meteroid.ui.theme.onPrimaryContainerTinted
-import de.chaosdorf.meteroid.util.popUpToRoot
-import kotlinx.datetime.Clock
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.toLocalDateTime
-import java.time.Month
-
-@Composable
-fun MeteroidBottomBar(
-  navController: NavController,
-  viewModel: NavigationViewModel
-) {
-  val user by viewModel.user.collectAsState()
-  val historyEnabled = user?.audit == true
-  val wrappedEnabled = Clock.System.now()
-    .toLocalDateTime(TimeZone.currentSystemDefault())
-    .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }
-  val navBackStackEntry by navController.currentBackStackEntryAsState()
-  val currentDestination = navBackStackEntry?.destination
-  val activeRoute = if (user == null) null
-  else HomeSections.entries.find {
-    it.route == currentDestination?.route
-  }
-
-  if (activeRoute != null) {
-    NavigationBar(
-      contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted
-    ) {
-      for (route in HomeSections.entries) {
-        if (wrappedEnabled || route != HomeSections.WRAPPED) {
-          NavigationBarItem(
-            icon = {
-              Icon(
-                if (route == activeRoute) route.iconActive else route.icon,
-                contentDescription = route.title,
-                tint = MaterialTheme.colorScheme.onPrimaryContainerTinted
-              )
-            },
-            label = { Text(route.title, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) },
-            selected = route == activeRoute,
-            onClick = {
-              navController.popUpToRoot()
-              navController.navigate(Routes.Servers.List)
-              navController.navigate(Routes.Users.list(user!!.serverId))
-              navController.navigate(
-                route.withArguments(
-                  "server" to user!!.serverId.value.toString(),
-                  "user" to user!!.userId.value.toString()
-                )
-              )
-            },
-            enabled = when (route) {
-              HomeSections.PURCHASE,
-              HomeSections.DEPOSIT -> true
-
-              HomeSections.HISTORY,
-              HomeSections.WRAPPED -> historyEnabled
-            }
-          )
-        }
-      }
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3b5cab63ecc5fc144db55d5569abc5816817b790
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt
@@ -0,0 +1,171 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import de.chaosdorf.mete.model.ServerId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.storage.AccountPreferences
+import de.chaosdorf.meteroid.sync.base.SyncHandler
+import de.chaosdorf.meteroid.ui.MeteroidScreen
+import de.chaosdorf.meteroid.util.findStartDestination
+
+@Composable
+fun MeteroidNavigation(navController: NavController, viewModel: NavigationViewModel) {
+  val backStackEntry by navController.currentBackStackEntryAsState()
+  val activeRoute = MeteroidScreen.byRoute(backStackEntry?.destination?.route)
+
+  val expanded by viewModel.expanded.collectAsState()
+  val account by viewModel.account.collectAsState()
+  val entries by viewModel.entries.collectAsState()
+  val syncState by viewModel.syncState.collectAsState()
+
+  LaunchedEffect(navController) {
+    navController.addOnDestinationChangedListener { _, _, arguments ->
+      val serverId = arguments?.getLong("server")?.let(::ServerId)
+      val userId = arguments?.getLong("user")?.let(::UserId)
+
+      viewModel.account.value = AccountPreferences.State(serverId, userId)
+      viewModel.expanded.value = false
+    }
+  }
+
+  BackHandler(expanded) {
+    viewModel.expanded.value = false
+  }
+
+  val verticalContentPadding: Dp by animateDpAsState(if (expanded) 4.dp else 0.dp, label = "verticalContentPadding")
+  val shadowElevation: Dp by animateDpAsState(if (expanded) 16.dp else 4.dp, label = "shadowElevation")
+
+  Surface(
+    Modifier.fillMaxWidth().animateContentSize()
+      .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 128.dp),
+    shape = RoundedCornerShape(32.dp),
+    shadowElevation = shadowElevation,
+    tonalElevation = 8.dp
+  ) {
+    Box {
+      LazyColumn(contentPadding = PaddingValues(vertical = verticalContentPadding)) {
+        items(entries, key = NavigationElement::key) { entry ->
+          val isCurrent = entry.isCurrent(activeRoute, account?.server, account?.user)
+
+          NavigationAnimationContainer(
+            expanded || isCurrent
+          ) {
+            when (entry) {
+              is NavigationElement.ServerElement -> NavigationServerItem(expanded, entry.server) {
+                viewModel.expanded.value = true
+              }
+
+              is NavigationElement.UserElement -> NavigationUserItem(expanded,
+                entry.user,
+                entry.pinned,
+                viewModel::togglePin,
+                onExpand = { viewModel.expanded.value = true }
+              ) {
+                if (isCurrent) {
+                  viewModel.expanded.value = false
+                } else {
+                  navController.navigate(
+                    MeteroidScreen.Home.Purchase.build(
+                      entry.user.serverId, entry.user.userId
+                    )
+                  ) {
+                    launchSingleTop = true
+                    restoreState = false
+                    popUpTo(findStartDestination(navController.graph).id) {
+                      saveState = false
+                    }
+                  }
+                }
+              }
+
+              is NavigationElement.UserListElement -> NavigationUserListItem {
+                if (NavigationElement.ServerElement(entry.server)
+                    .isCurrent(activeRoute, account?.server, account?.user)
+                ) {
+                  viewModel.expanded.value = false
+                } else {
+                  navController.navigate(MeteroidScreen.UserList.build(entry.server.serverId)) {
+                    launchSingleTop = true
+                    restoreState = false
+                    popUpTo(findStartDestination(navController.graph).id) {
+                      saveState = false
+                    }
+                  }
+                }
+              }
+
+              NavigationElement.AddServerElement -> NavigationAddServerItem(expanded,
+                onExpand = { viewModel.expanded.value = true }
+              ) {
+                if (isCurrent) {
+                  viewModel.expanded.value = false
+                } else {
+                  navController.navigate(MeteroidScreen.AddServer.build())
+                }
+              }
+
+              NavigationElement.SettingsElement -> NavigationSettingsItem(
+                expanded,
+                onExpand = { viewModel.expanded.value = true }
+              ) {
+                if (isCurrent) {
+                  viewModel.expanded.value = false
+                } else {
+                  navController.navigate(MeteroidScreen.Settings.build())
+                }
+              }
+            }
+          }
+        }
+      }
+      AnimatedVisibility(syncState == SyncHandler.State.Loading, enter = fadeIn(), exit = fadeOut()) {
+        LinearProgressIndicator(Modifier.align(Alignment.TopCenter).requiredHeight(2.dp).fillMaxWidth())
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
deleted file mode 100644
index a653449f0613b4fc79804eed47ca511c24fb466d..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
+++ /dev/null
@@ -1,220 +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.ui.navigation
-
-import android.util.Log
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.PushPin
-import androidx.compose.material.icons.outlined.PushPin
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
-import de.chaosdorf.meteroid.R
-import de.chaosdorf.meteroid.ui.NavigationViewModel
-import de.chaosdorf.meteroid.ui.PriceBadge
-import de.chaosdorf.meteroid.util.rememberAvatarPainter
-import okhttp3.HttpUrl.Companion.toHttpUrl
-
-@Composable
-fun MeteroidTopBar(
-  navController: NavController,
-  viewModel: NavigationViewModel,
-  modifier: Modifier = Modifier
-) {
-  val server by viewModel.server.collectAsState()
-  val user by viewModel.user.collectAsState()
-  val pinned by viewModel.pinned.collectAsState()
-
-  val backstack by navController.currentBackStack.collectAsState()
-  val canNavigateUp = backstack.size > 2
-  LaunchedEffect(backstack) {
-    val backstackEntries = backstack.map {
-      it.destination.route
-        ?.replace("{server}", it.arguments?.getLong("server")?.toString() ?: "{server}")
-        ?.replace("{user}", it.arguments?.getLong("user")?.toString() ?: "{user}")
-    }
-    Log.i("Navigation", "BACKSTACK: [${backstackEntries.joinToString(" › ")}]")
-  }
-
-  val avatarPainter = rememberAvatarPainter(
-    user?.gravatarUrl,
-    32.dp, 32.dp,
-    MaterialTheme.colorScheme.primary
-  )
-
-  val iconPainter = painterResource(R.drawable.ic_launcher)
-
-  Surface(
-    modifier = modifier
-      .padding(8.dp)
-      .height(64.dp),
-    color = MaterialTheme.colorScheme.surface,
-    shadowElevation = 6.dp,
-    tonalElevation = 6.dp,
-    shape = RoundedCornerShape(32.dp),
-    onClick = { navController.navigateUp() }
-  ) {
-    Row(modifier = Modifier.padding(8.dp)) {
-      if (user != null) {
-        Box(
-          modifier = Modifier
-            .size(48.dp)
-            .clip(CircleShape)
-            .background(MaterialTheme.colorScheme.primaryContainer)
-        ) {
-          Image(
-            avatarPainter,
-            contentDescription = null,
-            contentScale = ContentScale.Crop,
-            modifier = Modifier.align(Alignment.Center)
-          )
-        }
-      } else if (canNavigateUp) {
-        Icon(
-          Icons.AutoMirrored.Default.ArrowBack,
-          contentDescription = null,
-          tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
-          modifier = Modifier
-            .padding(8.dp)
-            .size(32.dp)
-        )
-      } else {
-        Image(
-          iconPainter,
-          contentDescription = null,
-          contentScale = ContentScale.Crop,
-          modifier = Modifier
-            .size(48.dp)
-            .clip(CircleShape)
-        )
-      }
-      Spacer(Modifier.width(16.dp))
-      Column(
-        modifier = Modifier
-          .align(Alignment.CenterVertically)
-          .weight(1.0f, fill = true)
-      ) {
-        when {
-          user != null && server != null -> {
-            Text(
-              user!!.name,
-              fontWeight = FontWeight.SemiBold,
-              overflow = TextOverflow.Ellipsis,
-              softWrap = false
-            )
-            Text(
-              server!!.url.toHttpUrl().host,
-              color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
-              fontWeight = FontWeight.Normal,
-              overflow = TextOverflow.Ellipsis,
-              softWrap = false
-            )
-          }
-
-          server != null && server!!.name != null -> {
-            Text(
-              server!!.name!!,
-              fontWeight = FontWeight.SemiBold,
-              overflow = TextOverflow.Ellipsis,
-              softWrap = false
-            )
-            Text(
-              server!!.url.toHttpUrl().host,
-              color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
-              fontWeight = FontWeight.Normal,
-              overflow = TextOverflow.Ellipsis,
-              softWrap = false
-            )
-          }
-
-          server != null -> {
-            Text(
-              server!!.url.toHttpUrl().host,
-              fontWeight = FontWeight.SemiBold,
-              overflow = TextOverflow.Ellipsis,
-              softWrap = false
-            )
-          }
-
-          else -> {
-            Text(
-              "Meteroid",
-              fontWeight = FontWeight.SemiBold,
-              overflow = TextOverflow.Ellipsis,
-              softWrap = false
-            )
-          }
-        }
-      }
-      Spacer(Modifier.width(16.dp))
-      user?.let { user ->
-        IconButton(onClick = {
-          viewModel.togglePin(user.serverId, user.userId)
-        }) {
-          Icon(
-            if (pinned == true) Icons.Filled.PushPin
-            else Icons.Outlined.PushPin,
-            contentDescription = null
-          )
-        }
-        Spacer(Modifier.width(8.dp))
-        PriceBadge(
-          user.balance,
-          modifier = Modifier
-            .align(Alignment.CenterVertically)
-            .padding(end = 12.dp)
-        )
-      }
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt
similarity index 52%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt
index 2ff97899dbaafaa180550bc51def84576323821e..9bc43f5e0994141974f668904b40c6b8c989b2b3 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt
@@ -24,26 +24,33 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Celebration
-import androidx.compose.material.icons.outlined.History
-import androidx.compose.material.icons.outlined.LocalAtm
-import androidx.compose.material.icons.twotone.Celebration
-import androidx.compose.material.icons.twotone.History
-import androidx.compose.material.icons.twotone.LocalAtm
-import androidx.compose.ui.graphics.vector.ImageVector
-import de.chaosdorf.meteroid.icons.MeteroidIcons
-import de.chaosdorf.meteroid.icons.outlined.WaterFull
-import de.chaosdorf.meteroid.icons.twotone.WaterFull
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.ui.AvatarLayout
 
-enum class HomeSections(
-  override val title: String,
-  override val icon: ImageVector,
-  override val iconActive: ImageVector,
-  override val route: String
-) : MeteroidNavSection {
-  PURCHASE("Purchase", MeteroidIcons.Outlined.WaterFull, MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase),
-  DEPOSIT("Deposit", Icons.Outlined.LocalAtm, Icons.TwoTone.LocalAtm, Routes.Home.Deposit),
-  HISTORY("History", Icons.Outlined.History, Icons.TwoTone.History, Routes.Home.History),
-  WRAPPED("Wrapped", Icons.Outlined.Celebration, Icons.TwoTone.Celebration, Routes.Home.Wrapped);
+@Composable
+fun NavigationAddServerItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) {
+  val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height")
+
+  ListItem(
+    headlineContent = { Text("Add Server") },
+    leadingContent = {
+      AvatarLayout {
+        Icon(Icons.Default.Add, contentDescription = null)
+      }
+    },
+    modifier = Modifier.requiredHeight(height)
+      .clickable(onClick = if (expanded) onClick else onExpand)
+  )
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt
similarity index 75%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt
index 218a3d82fb12f791ec61fc8eff1cb7c9ffa1c4b8..d05bf86d1823651e98768f108fa1f41151feb027 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt
@@ -24,19 +24,16 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
-import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.runtime.Composable
 
-interface MeteroidNavSection {
-  val title: String
-  val icon: ImageVector
-  val iconActive: ImageVector
-  val route: String
-
-  fun withArguments(vararg args: Pair<String, String>): String {
-    var result = route
-    for ((parameter, value) in args) {
-      result = result.replace("{$parameter}", value)
-    }
-    return result
+@Composable
+fun NavigationAnimationContainer(
+  visible: Boolean, content: @Composable () -> Unit
+) {
+  AnimatedVisibility(visible, enter = expandVertically(), exit = shrinkVertically()) {
+    content()
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt
new file mode 100644
index 0000000000000000000000000000000000000000..706d29924f2debd4e8084e0e7b472e443862a519
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt
@@ -0,0 +1,73 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import de.chaosdorf.mete.model.ServerId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.ui.MeteroidScreen
+
+sealed class NavigationElement(val key: String) {
+  abstract fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean
+
+  data class ServerElement(
+    val server: Server,
+  ) : NavigationElement("navigation-${server.serverId}") {
+
+    override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean {
+      return route == MeteroidScreen.UserList && serverId == server.serverId
+    }
+  }
+
+  data class UserElement(
+    val user: User,
+    val pinned: Boolean,
+  ) : NavigationElement("navigation-${user.serverId}-${user.userId}-${if (pinned) "pinned" else "current"}") {
+    override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean {
+      return route is MeteroidScreen.Home && serverId == user.serverId && userId == user.userId
+    }
+  }
+
+  data class UserListElement(
+    val server: Server,
+  ) : NavigationElement("navigation-${server.serverId}-userList") {
+    override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean = false
+  }
+
+  data object AddServerElement : NavigationElement("navigation-addServer") {
+
+    override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean {
+      return route == MeteroidScreen.AddServer
+    }
+  }
+
+  data object SettingsElement : NavigationElement("navigation-settings") {
+
+    override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean {
+      return route == MeteroidScreen.Settings
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt
new file mode 100644
index 0000000000000000000000000000000000000000..265d989aadbda4586a144df70bfbcf1de0933a73
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt
@@ -0,0 +1,51 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+
+@Composable
+fun NavigationScrim(viewModel: NavigationViewModel) {
+  val expanded by viewModel.expanded.collectAsState()
+
+  AnimatedVisibility(expanded, enter = fadeIn(), exit = fadeOut()) {
+    Surface(
+      color = MaterialTheme.colorScheme.scrim.copy(alpha = 0.2f),
+      modifier = Modifier.fillMaxSize().clickable {
+        viewModel.expanded.value = false
+      }
+    ) {}
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2092a71aa639e57025d30dc9c5293e6cc2c1709a
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt
@@ -0,0 +1,55 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.ui.ServerAvatar
+import de.chaosdorf.meteroid.util.humanReadableHost
+import okhttp3.HttpUrl.Companion.toHttpUrl
+
+@Composable
+fun NavigationServerItem(expanded: Boolean, server: Server, onExpand: () -> Unit) {
+  val host = humanReadableHost(server.url.toHttpUrl())
+
+  ListItem(
+    headlineContent = { Text(server.name ?: host, maxLines = 1, overflow = TextOverflow.Ellipsis) },
+    supportingContent = { if (server.name != null) Text(host, maxLines = 1, overflow = TextOverflow.Ellipsis) },
+    leadingContent = {
+      ServerAvatar(server.logoUrl)
+    },
+    modifier = Modifier.requiredHeight(64.dp).let {
+      if (expanded) it
+      else it.clickable(onClick = onExpand)
+    }
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..20344554c820e7ab22adae06940ed3e34b572d2b
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt
@@ -0,0 +1,56 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.ui.AvatarLayout
+
+@Composable
+fun NavigationSettingsItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) {
+  val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height")
+
+  ListItem(
+    headlineContent = { Text("Settings") },
+    leadingContent = {
+      AvatarLayout {
+        Icon(Icons.Filled.Settings, contentDescription = null)
+      }
+    },
+    modifier = Modifier.requiredHeight(height)
+      .clickable(onClick = if (expanded) onClick else onExpand)
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b42ba6160e862de008cb734b93d10334d5e57c0a
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt
@@ -0,0 +1,80 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.material.icons.filled.StarOutline
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.mete.model.ServerId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.ui.UserAvatar
+import de.chaosdorf.meteroid.ui.home.PriceBadge
+
+@Composable
+fun NavigationUserItem(
+  expanded: Boolean,
+  user: User,
+  pinned: Boolean,
+  onTogglePin: (ServerId, UserId) -> Unit,
+  onExpand: () -> Unit,
+  onClick: () -> Unit,
+) {
+  ListItem(
+    headlineContent = { Text(user.name, maxLines = 1, overflow = TextOverflow.Ellipsis) },
+    supportingContent = { if (user.email != null) Text(user.email!!, maxLines = 1, overflow = TextOverflow.Ellipsis) },
+    leadingContent = { UserAvatar(user.gravatarUrl) },
+    trailingContent = {
+      Row(verticalAlignment = Alignment.CenterVertically) {
+        AnimatedVisibility(!expanded, enter = fadeIn(), exit = fadeOut()) {
+          IconButton(onClick = { onTogglePin(user.serverId, user.userId) }) {
+            Icon(
+              if (pinned) Icons.Default.Star else Icons.Default.StarOutline,
+              contentDescription = null
+            )
+          }
+        }
+        PriceBadge(user.balance)
+      }
+    },
+    modifier = Modifier.requiredHeight(64.dp)
+      .clickable(onClick = if (expanded) onClick else onExpand)
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9a89652dd3453ed5fdf2916c0568214cdbf12494
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt
@@ -0,0 +1,55 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Group
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.ui.AvatarLayout
+
+@Composable
+fun NavigationUserListItem(onClick: () -> Unit) {
+  Column {
+    ListItem(
+      headlineContent = { Text("All Users") },
+      leadingContent = {
+        AvatarLayout {
+          Icon(Icons.Default.Group, contentDescription = null)
+        }
+      },
+      modifier = Modifier.requiredHeight(48.dp).clickable(onClick = onClick)
+    )
+    HorizontalDivider()
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..45d298cc292d10a97fce809864d002d320403d0b
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt
@@ -0,0 +1,120 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.navigation
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import de.chaosdorf.mete.model.ServerId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.model.*
+import de.chaosdorf.meteroid.storage.AccountPreferences
+import de.chaosdorf.meteroid.sync.AccountProvider
+import de.chaosdorf.meteroid.sync.SyncManager
+import de.chaosdorf.meteroid.sync.base.SyncHandler
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class NavigationViewModel @Inject constructor(
+  serverRepository: ServerRepository,
+  userRepository: UserRepository,
+  pinnedUserRepository: PinnedUserRepository,
+  syncManager: SyncManager,
+  private val accountProvider: AccountProvider
+) : ViewModel() {
+  val expanded = MutableStateFlow(false)
+  val account = MutableStateFlow<AccountPreferences.State?>(null)
+
+  val syncState = syncManager.syncState
+    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SyncHandler.State.Idle)
+
+  private val servers: StateFlow<List<Server>> = serverRepository.getAllFlow()
+    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+
+  private val pinnedUsers: StateFlow<List<User>> = pinnedUserRepository.getPinnedUsersFlow()
+    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+
+  private val currentUser: StateFlow<User?> = account.flatMapLatest { account ->
+    if (account?.server == null || account.user == null) flowOf(null)
+    else userRepository.getFlow(account.server, account.user)
+  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
+
+  val historyDisabled = currentUser.map {
+    it?.audit == false
+  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
+
+  val entries: StateFlow<List<NavigationElement>> = combine(
+    servers,
+    pinnedUsers,
+    currentUser,
+  ) { servers, pinnedUsers, currentUser ->
+    val entries = mutableListOf<NavigationElement>()
+    val userInList = currentUser != null && pinnedUsers.any {
+      it.serverId == currentUser.serverId && it.userId == currentUser.userId
+    }
+
+    for (server in servers) {
+      entries.add(NavigationElement.ServerElement(server))
+      if (currentUser != null && currentUser.serverId == server.serverId && !userInList) {
+        entries.add(NavigationElement.UserElement(currentUser, pinned = false))
+      }
+      for (user in pinnedUsers) {
+        if (user.serverId == server.serverId) {
+          entries.add(NavigationElement.UserElement(user, pinned = true))
+        }
+      }
+      entries.add(NavigationElement.UserListElement(server))
+    }
+
+    entries.add(NavigationElement.AddServerElement)
+    entries.add(NavigationElement.SettingsElement)
+
+    entries
+  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+
+  init {
+    viewModelScope.launch {
+      account.collectLatest { account ->
+        val serverId = account?.server
+        val userId = account?.user
+        if (serverId != null && userId != null) {
+          val server = serverRepository.get(serverId)
+          val user = userRepository.get(serverId, userId)
+          if (server != null && user != null) {
+            syncManager.sync(server, user, incremental = true)
+          }
+        }
+      }
+    }
+  }
+
+  fun togglePin(serverId: ServerId, userId: UserId) {
+    viewModelScope.launch {
+      accountProvider.togglePin(serverId, userId)
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt
deleted file mode 100644
index 2d7fe45a225b4842224057e9b88801f52a4f57c2..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt
+++ /dev/null
@@ -1,58 +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.ui.navigation
-
-import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.ServerId
-
-object Routes {
-  object Servers {
-    const val List = "server"
-    const val Add = "server/create"
-  }
-  object Users {
-    const val List = "server/{server}"
-    fun list(server: ServerId) = List
-      .replace("{server}", server.value.toString())
-  }
-  object Home {
-    const val Purchase = "server/{server}/user/{user}/purchase"
-    const val Deposit = "server/{server}/user/{user}/deposit"
-    const val History = "server/{server}/user/{user}/history"
-    const val Wrapped = "server/{server}/user/{user}/wrapped"
-    fun purchase(server: ServerId, user: UserId) = Purchase
-      .replace("{server}", server.value.toString())
-      .replace("{user}", user.value.toString())
-    fun deposit(server: ServerId, user: UserId) = Deposit
-      .replace("{server}", server.value.toString())
-      .replace("{user}", user.value.toString())
-    fun history(server: ServerId, user: UserId) = History
-      .replace("{server}", server.value.toString())
-      .replace("{user}", user.value.toString())
-    fun wrapped(server: ServerId, user: UserId) = Wrapped
-      .replace("{server}", server.value.toString())
-      .replace("{user}", user.value.toString())
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt
index f5ea03948d6339d917cf91d09153950d35309447..a629d0b6c16e2394e0ee68cdd99531c7d64d47de 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt
@@ -24,22 +24,10 @@
 
 package de.chaosdorf.meteroid.ui.servers
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.*
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
-import androidx.compose.material3.Card
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
+import androidx.compose.material3.*
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -51,7 +39,6 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
 import coil.compose.AsyncImage
-import de.chaosdorf.meteroid.ui.navigation.Routes
 import kotlinx.coroutines.launch
 import okhttp3.HttpUrl.Companion.toHttpUrl
 
@@ -112,7 +99,7 @@ fun AddServerScreen(
           IconButton(onClick = {
             scope.launch {
               viewModel.addServer()
-              navController.navigate(Routes.Servers.List)
+              navController.navigateUp()
             }
           }) {
             Icon(Icons.Default.Add, contentDescription = "Add Server")
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
index 26100e67525c906f7b864242eaa37678bf7a2f0c..8c1ee10134223eea50d93c2b5c25433bb857215a 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
@@ -28,16 +28,11 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
 import de.chaosdorf.meteroid.util.newServer
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.*
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt
deleted file mode 100644
index 19242ea4eb789e304677a7ddf7c46e2c31da48f0..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt
+++ /dev/null
@@ -1,81 +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.ui.servers
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
-import coil.compose.AsyncImage
-import de.chaosdorf.meteroid.ui.navigation.Routes
-import okhttp3.HttpUrl.Companion.toHttpUrl
-
-@Composable
-fun ServerListScreen(
-  navController: NavController,
-  viewModel: ServerListViewModel,
-  contentPadding: PaddingValues = PaddingValues(),
-) {
-  val servers by viewModel.servers.collectAsState()
-
-  LazyColumn(contentPadding = contentPadding) {
-    items(servers) { server ->
-      ListItem(
-        headlineContent = { Text(server.name ?: server.url) },
-        supportingContent = { if (server.name != null) Text(server.url.toHttpUrl().host) },
-        leadingContent = {
-          AsyncImage(
-            server.logoUrl,
-            contentDescription = null,
-            contentScale = ContentScale.Crop,
-            modifier = Modifier.size(48.dp)
-          )
-        },
-        modifier = Modifier.clickable {
-          navController.navigate(Routes.Users.list(server.serverId))
-          viewModel.selectServer(server.serverId)
-        }
-      )
-    }
-    item {
-      ListItem(
-        headlineContent = { Text("Add Server") },
-        modifier = Modifier.clickable {
-          navController.navigate(Routes.Servers.Add)
-        }
-      )
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt
new file mode 100644
index 0000000000000000000000000000000000000000..898ff670076a1d846e7f631e71414820498bc5d8
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt
@@ -0,0 +1,45 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+
+@Composable
+fun SettingsScreen(navController: NavController, viewModel: SettingsViewModel, contentPadding: PaddingValues) {
+  Column(
+    Modifier
+      .padding(contentPadding)
+      .padding(16.dp, 8.dp)
+  ) {
+    Text("Settings")
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt
similarity index 92%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt
index 5ae0c58b4528f76c57d8fad51672d29a0ac33e49..35aa5a251c13ed8f7f1a279f723f1e305c10fa59 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt
@@ -22,14 +22,11 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.sync
+package de.chaosdorf.meteroid.ui.settings
 
 import androidx.lifecycle.ViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
 
 @HiltViewModel
-class SyncViewModel @Inject constructor(
-) : ViewModel() {
-
-}
+class SettingsViewModel @Inject constructor() : ViewModel()
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt
similarity index 97%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt
index ba11f9170cda673579a9a1a5a74d3be83d9e4edd..f903e304e573d6dcc7efd3d66ecdc569b96b7e9d 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.users
+package de.chaosdorf.meteroid.ui.userlist
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
@@ -39,8 +39,8 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.dp
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.util.rememberAvatarPainter
 
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt
similarity index 79%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt
index ef1c0172ee90ecc74ecba896e1bc4de5cab182be..a7e6156d8a2b3f83c85579e5dd6530617c19449d 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.users
+package de.chaosdorf.meteroid.ui.userlist
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.PaddingValues
@@ -38,7 +38,8 @@ import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
-import de.chaosdorf.meteroid.ui.navigation.Routes
+import de.chaosdorf.meteroid.ui.MeteroidScreen
+import de.chaosdorf.meteroid.util.findStartDestination
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
@@ -64,10 +65,16 @@ fun UserListScreen(
 
       items(
         pinnedUsers,
-        key = { "pinned-${it.userId}" },
+        key = { "pinned-${it.serverId}-${it.userId}" },
       ) { user ->
         UserListItem(user) { serverId, userId ->
-          navController.navigate(Routes.Home.purchase(serverId, userId))
+          navController.navigate(MeteroidScreen.Home.Purchase.build(serverId, userId)) {
+            launchSingleTop = true
+            restoreState = false
+            popUpTo(findStartDestination(navController.graph).id) {
+              saveState = false
+            }
+          }
           viewModel.selectUser(serverId, userId)
         }
       }
@@ -88,10 +95,16 @@ fun UserListScreen(
 
         items(
           group,
-          key = { "${it.serverId}-${it.userId}" },
+          key = { "user-${it.serverId}-${it.userId}" },
         ) { user ->
           UserListItem(user) { serverId, userId ->
-            navController.navigate(Routes.Home.purchase(serverId, userId))
+            navController.navigate(MeteroidScreen.Home.Purchase.build(serverId, userId)) {
+              launchSingleTop = true
+              restoreState = false
+              popUpTo(findStartDestination(navController.graph).id) {
+                saveState = false
+              }
+            }
             viewModel.selectUser(serverId, userId)
           }
         }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt
similarity index 97%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt
index 0823feb08f05dac58a8baedfadbfcd94a2b7559e..22a6a097db053957c3df05fd2b0f2d0d8f3bf982 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt
@@ -22,16 +22,16 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.users
+package de.chaosdorf.meteroid.ui.userlist
 
 import android.util.Log
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.PinnedUserRepository
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.model.UserRepository
 import de.chaosdorf.meteroid.storage.AccountPreferences
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt
deleted file mode 100644
index 16a5f7ca550009d8661bf584636f5532c362da8f..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt
+++ /dev/null
@@ -1,102 +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.ui.users
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
-import coil.compose.rememberAsyncImagePainter
-import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.User
-import de.chaosdorf.meteroid.sample.SampleDrinkProvider
-import de.chaosdorf.meteroid.ui.PriceBadge
-
-@Composable
-fun UserTile(
-  item: User,
-  onSelect: (UserId) -> Unit = {}
-) {
-  val avatarPainter = rememberAsyncImagePainter(
-    item.gravatarUrl
-  )
-
-  Column(
-    modifier = Modifier
-      .height(IntrinsicSize.Max)
-      .alpha(if (item.active) 1.0f else 0.67f)
-      .clip(RoundedCornerShape(8.dp))
-      .clickable { onSelect(item.userId) }
-      .padding(8.dp)
-  ) {
-    Box {
-      Image(
-        avatarPainter,
-        contentDescription = null,
-        contentScale = ContentScale.Fit,
-        modifier = Modifier
-          .aspectRatio(1.0f)
-          .clip(CircleShape)
-          .background(MaterialTheme.colorScheme.primaryContainer)
-      )
-    }
-    Spacer(Modifier.height(4.dp))
-    Text(
-      item.name,
-      modifier = Modifier
-        .fillMaxWidth()
-        .padding(horizontal = 8.dp),
-      textAlign = TextAlign.Center,
-      fontWeight = FontWeight.SemiBold,
-      style = MaterialTheme.typography.labelLarge,
-    )
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/BundleExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/BundleExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..902e0c569706e9e1bec4f3218ea43fc5db4f9a22
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/BundleExtensions.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.util
+
+import android.os.Bundle
+
+internal fun Bundle.toMap(): Map<String, Any?> =
+  keySet().associate { @Suppress("DEPRECATION") Pair(it, get(it)) }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/HumanReadableHost.kt
similarity index 54%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/util/HumanReadableHost.kt
index 690b73c4ae6e870f337944de0061df471391c58c..6379d5611a00c656213c79cf56df379d641b7373 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/HumanReadableHost.kt
@@ -22,34 +22,14 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.sample
+package de.chaosdorf.meteroid.util
 
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import de.chaosdorf.mete.model.DrinkId
-import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.ServerId
+import okhttp3.HttpUrl
 
-class SampleDrinkProvider : PreviewParameterProvider<Drink> {
-  override val values = sequenceOf(
-    Drink(
-      serverId = ServerId(-1),
-      drinkId = DrinkId(27),
-      active = true,
-      name = "Club Mate",
-      volume = 0.5.toBigDecimal(),
-      caffeine = null,
-      price = 1.5.toBigDecimal(),
-      logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png",
-    ),
-    Drink(
-      serverId = ServerId(-1),
-      drinkId = DrinkId(15),
-      active = false,
-      name = "Paulaner Spezi",
-      volume = 0.5.toBigDecimal(),
-      caffeine = null,
-      price = 1.5.toBigDecimal(),
-      logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png",
-    )
-  )
+fun humanReadableHost(url: HttpUrl): String {
+  val actualPort = url.port
+  val defaultPort = HttpUrl.defaultPort(url.scheme)
+  val actualHost = url.host
+  if (actualPort == defaultPort) return actualHost
+  return "$actualHost:$actualPort"
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt
index a9b65cfa0cfec0d03e965d35c78c7398b3d7a32c..e8371e2e2fc77e5ba6a5d36ab288eebaa6d83e2e 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt
@@ -25,8 +25,8 @@
 package de.chaosdorf.meteroid.util
 
 import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.ServerId
 import java.net.URI
 
 suspend fun MeteApiFactory.newServer(serverId: ServerId, baseUrl: String): Server? = try {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackEntryExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackEntryExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..64ac7664c5c93b35f14a817008af4192212c048e
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackEntryExtensions.kt
@@ -0,0 +1,42 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.navigation.NavBackStackEntry
+
+internal fun NavBackStackEntry.toFancyString(): String {
+  val arguments = this.arguments?.toMap().orEmpty().toList()
+    .filter { (key, _) -> key == "server" || key == "user" }
+    .joinToString(", ", prefix = "(", postfix = ")") { (key, value) -> "$key=$value" }
+
+  return "${destination.route}$arguments"
+}
+
+internal fun Iterable<NavBackStackEntry>.toFancyString(): String = joinToString(
+  separator = " › ",
+  prefix = "[",
+  postfix = "]",
+  transform = NavBackStackEntry::toFancyString
+)
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavGraphExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavGraphExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bac65daa64fd7975da11459fc5f45400fbe2b7fe
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavGraphExtensions.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.util
+
+import androidx.navigation.NavDestination
+import androidx.navigation.NavGraph
+
+private val NavGraph.startDestination: NavDestination?
+  get() = findNode(startDestinationId)
+
+tailrec fun findStartDestination(graph: NavDestination): NavDestination {
+  return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt
index d1f83fa61e4593a93149000addb76d085a0a6c38..463fdf4b7f7f15b690ce3a90a7565054eb3b7cf3 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt
@@ -37,15 +37,15 @@ import coil.compose.rememberAsyncImagePainter
 @Composable
 fun rememberAvatarPainter(url: String?, iconWidth: Dp, iconHeight: Dp, iconTint: Color): AsyncImagePainter {
   val personPainter = rememberVectorPainter(
-      defaultHeight = iconHeight,
-      defaultWidth = iconWidth,
-      viewportWidth = Icons.Filled.Person.viewportWidth,
-      viewportHeight = Icons.Filled.Person.viewportHeight,
-      name = Icons.Filled.Person.name,
-      tintColor = iconTint,
-      tintBlendMode = Icons.Filled.Person.tintBlendMode,
-      autoMirror = Icons.Filled.Person.autoMirror,
-      content = { _, _ -> RenderVectorGroup(group = Icons.Filled.Person.root) }
+    defaultHeight = iconHeight,
+    defaultWidth = iconWidth,
+    viewportWidth = Icons.Filled.Person.viewportWidth,
+    viewportHeight = Icons.Filled.Person.viewportHeight,
+    name = Icons.Filled.Person.name,
+    tintColor = iconTint,
+    tintBlendMode = Icons.Filled.Person.tintBlendMode,
+    autoMirror = Icons.Filled.Person.autoMirror,
+    content = { _, _ -> RenderVectorGroup(group = Icons.Filled.Person.root) }
   )
   return rememberAsyncImagePainter(url, fallback = personPainter)
 }
diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml
index 2e4644c5588f9df49813fa88187dcd2d20466d8f..b45acc3994db58b7f4f90961613e2a2988598384 100644
--- a/app/src/main/res/drawable/ic_launcher.xml
+++ b/app/src/main/res/drawable/ic_launcher.xml
@@ -23,20 +23,21 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-  xmlns:aapt="http://schemas.android.com/aapt"
-  xmlns:tools="http://schemas.android.com/tools"
-  android:width="72dp"
-  android:height="72dp"
-  android:viewportWidth="72"
-  android:viewportHeight="72">
+        xmlns:aapt="http://schemas.android.com/aapt"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:width="72dp"
+        android:height="72dp"
+        android:viewportWidth="72"
+        android:viewportHeight="72">
   <path
     android:fillColor="#003984"
-    android:pathData="m11.5 0c-6.35 0-11.5 5.15-11.5 11.5v49c0 6.35 5.15 11.5 11.5 11.5h49c6.35 0 11.5-5.15 11.5-11.5v-49c0-6.35-5.15-11.5-11.5-11.5z" />
+    android:pathData="m11.5 0c-6.35 0-11.5 5.15-11.5 11.5v49c0 6.35 5.15 11.5 11.5 11.5h49c6.35 0 11.5-5.15 11.5-11.5v-49c0-6.35-5.15-11.5-11.5-11.5z"/>
   <path
     android:fillAlpha="0.5"
     android:fillColor="#000000"
-    android:pathData="m21.7 60.9v11.1h38.8c0 0 0.256-0 0-0l-10.6-11.1z" />
-  <path android:pathData="m53.5 23.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z">
+    android:pathData="m21.7 60.9v11.1h38.8c0 0 0.256-0 0-0l-10.6-11.1z"/>
+  <path
+    android:pathData="m53.5 23.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z">
     <aapt:attr name="android:fillColor">
       <gradient
         android:endX="0"
@@ -46,22 +47,22 @@
         android:type="linear">
         <item
           android:color="#ff9800"
-          android:offset="0.6" />
+          android:offset="0.6"/>
         <item
           android:color="#f9c579"
-          android:offset="1.0" />
+          android:offset="1.0"/>
       </gradient>
     </aapt:attr>
   </path>
   <path
     android:fillColor="#bad9ff"
     android:pathData="m36 15.4c-5.15 0-9.85 0.237-13.3 0.625-1.71 0.194-3.09 0.422-4.09 0.69-0.5 0.135-0.9 0.273-1.24 0.469-0.17 0.098-0.329 0.21-0.474 0.39-0.144 0.18-0.268 0.475-0.232 0.77l5 42.2c0.072 0.61 0.53 1.08 1.12 1.4 0.59 0.314 1.38 0.545 2.46 0.745 2.17 0.398 5.55 0.63 10.7 0.63s8.55-0.233 10.7-0.63c1.08-0.199 1.87-0.432 2.46-0.745 0.59-0.314 1.05-0.785 1.12-1.39l5-42.2c0.0353-0.298-0.088-0.59-0.232-0.77s-0.305-0.292-0.474-0.39c-0.339-0.196-0.74-0.334-1.24-0.469-1-0.27-2.38-0.499-4.09-0.69-3.42-0.387-8.1-0.625-13.3-0.625zm0 1.5c5.1 0 9.8 0.238 13.1 0.615 1.66 0.188 3 0.415 3.87 0.65 0.398 0.108 0.665 0.218 0.815 0.297l-4.98 42c0.0034-0.0282 0.021 0.0545-0.337 0.245-0.358 0.19-1.02 0.412-2.02 0.595-2 0.366-5.35 0.605-10.4 0.605s-8.45-0.24-10.4-0.605c-1-0.183-1.66-0.404-2.02-0.595-0.358-0.19-0.34-0.274-0.337-0.245l-4.98-42c0.15-0.0785 0.416-0.189 0.815-0.297 0.865-0.234 2.2-0.46 3.87-0.65 3.33-0.377 8-0.615 13.1-0.615z"
-    tools:ignore="VectorPath" />
+    tools:ignore="VectorPath"/>
   <path
     android:fillColor="#ffe2b7"
     android:pathData="m27 26.4a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm18 4.49a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm-19.4 6.2a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm3.92 3.85a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 0 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm12.2 0.725a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-4.46 4.02a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-6.35 1.4a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm11.6 1.92a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-5.15 1.54a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.48a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm10.4 3.06a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.132a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm3.72 1.64a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm2.63 2.34a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845z"
-    tools:ignore="VectorPath" />
+    tools:ignore="VectorPath"/>
   <path
     android:fillColor="#d91616"
-    android:pathData="m36 26.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z" />
+    android:pathData="m36 26.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z"/>
 </vector>
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index b663e5e94bc2ea41f13ef9397ce180df720fc2b1..df5ce8be4c56aab013bff0a4bcaeb59d39bd8993 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -23,17 +23,18 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-  xmlns:aapt="http://schemas.android.com/aapt"
-  xmlns:tools="http://schemas.android.com/tools"
-  android:width="108dp"
-  android:height="108dp"
-  android:viewportWidth="108"
-  android:viewportHeight="108">
+        xmlns:aapt="http://schemas.android.com/aapt"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:width="108dp"
+        android:height="108dp"
+        android:viewportWidth="108"
+        android:viewportHeight="108">
   <path
     android:fillAlpha="0.5"
     android:fillColor="#000000"
-    android:pathData="m39.7 78.9h28.6l28 29.1h-56.5z" />
-  <path android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z">
+    android:pathData="m39.7 78.9h28.6l28 29.1h-56.5z"/>
+  <path
+    android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z">
     <aapt:attr name="android:fillColor">
       <gradient
         android:endX="0"
@@ -43,22 +44,22 @@
         android:type="linear">
         <item
           android:color="#ff9800"
-          android:offset="0.6" />
+          android:offset="0.6"/>
         <item
           android:color="#f9c579"
-          android:offset="1.0" />
+          android:offset="1.0"/>
       </gradient>
     </aapt:attr>
   </path>
   <path
     android:fillColor="#bad9ff"
     android:pathData="m54 33.4c-5.15 0-9.85 0.237-13.3 0.625-1.71 0.194-3.09 0.422-4.09 0.69-0.5 0.135-0.9 0.273-1.24 0.469-0.17 0.098-0.329 0.21-0.474 0.39-0.144 0.18-0.268 0.475-0.232 0.77l5 42.2c0.072 0.61 0.53 1.08 1.12 1.4 0.59 0.314 1.38 0.545 2.46 0.745 2.17 0.398 5.55 0.63 10.7 0.63s8.55-0.233 10.7-0.63c1.08-0.199 1.87-0.432 2.46-0.745 0.59-0.314 1.05-0.785 1.12-1.39l5-42.2c0.0353-0.298-0.088-0.59-0.232-0.77s-0.305-0.292-0.474-0.39c-0.339-0.196-0.74-0.334-1.24-0.469-1-0.27-2.38-0.499-4.09-0.69-3.42-0.387-8.1-0.625-13.3-0.625zm0 1.5c5.1 0 9.8 0.238 13.1 0.615 1.66 0.188 3 0.415 3.87 0.65 0.398 0.108 0.665 0.218 0.815 0.297l-4.98 42c0.0034-0.0282 0.021 0.0545-0.337 0.245-0.358 0.19-1.02 0.412-2.02 0.595-2 0.366-5.35 0.605-10.4 0.605s-8.45-0.24-10.4-0.605c-1-0.183-1.66-0.404-2.02-0.595-0.358-0.19-0.34-0.274-0.337-0.245l-4.98-42c0.15-0.0785 0.416-0.189 0.815-0.297 0.865-0.234 2.2-0.46 3.87-0.65 3.33-0.377 8-0.615 13.1-0.615z"
-    tools:ignore="VectorPath" />
+    tools:ignore="VectorPath"/>
   <path
     android:fillColor="#ffe2b7"
     android:pathData="m45 44.4a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm18 4.49a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm-19.4 6.2a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm3.92 3.85a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 0 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm12.2 0.725a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-4.46 4.02a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-6.35 1.4a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm11.6 1.92a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-5.15 1.54a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.48a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm10.4 3.06a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.132a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm3.72 1.64a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm2.63 2.34a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845z"
-    tools:ignore="VectorPath" />
+    tools:ignore="VectorPath"/>
   <path
     android:fillColor="#d91616"
-    android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z" />
+    android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z"/>
 </vector>
diff --git a/app/src/main/res/drawable/ic_splash.xml b/app/src/main/res/drawable/ic_splash.xml
index 17bb99f97c7e6ec19faa44db4ad0203522c8eff9..ea9f9bc37d5d0e86087ae7740d7af3aef84fd850 100644
--- a/app/src/main/res/drawable/ic_splash.xml
+++ b/app/src/main/res/drawable/ic_splash.xml
@@ -23,13 +23,14 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-  xmlns:aapt="http://schemas.android.com/aapt"
-  xmlns:tools="http://schemas.android.com/tools"
-  android:width="108dp"
-  android:height="108dp"
-  android:viewportWidth="108"
-  android:viewportHeight="108">
-  <path android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z">
+        xmlns:aapt="http://schemas.android.com/aapt"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:width="108dp"
+        android:height="108dp"
+        android:viewportWidth="108"
+        android:viewportHeight="108">
+  <path
+    android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z">
     <aapt:attr name="android:fillColor">
       <gradient
         android:endX="0"
@@ -39,22 +40,22 @@
         android:type="linear">
         <item
           android:color="#ff9800"
-          android:offset="0.6" />
+          android:offset="0.6"/>
         <item
           android:color="#f9c579"
-          android:offset="1.0" />
+          android:offset="1.0"/>
       </gradient>
     </aapt:attr>
   </path>
   <path
     android:fillColor="#bad9ff"
     android:pathData="m54 33.4c-5.15 0-9.85 0.237-13.3 0.625-1.71 0.194-3.09 0.422-4.09 0.69-0.5 0.135-0.9 0.273-1.24 0.469-0.17 0.098-0.329 0.21-0.474 0.39-0.144 0.18-0.268 0.475-0.232 0.77l5 42.2c0.072 0.61 0.53 1.08 1.12 1.4 0.59 0.314 1.38 0.545 2.46 0.745 2.17 0.398 5.55 0.63 10.7 0.63s8.55-0.233 10.7-0.63c1.08-0.199 1.87-0.432 2.46-0.745 0.59-0.314 1.05-0.785 1.12-1.39l5-42.2c0.0353-0.298-0.088-0.59-0.232-0.77s-0.305-0.292-0.474-0.39c-0.339-0.196-0.74-0.334-1.24-0.469-1-0.27-2.38-0.499-4.09-0.69-3.42-0.387-8.1-0.625-13.3-0.625zm0 1.5c5.1 0 9.8 0.238 13.1 0.615 1.66 0.188 3 0.415 3.87 0.65 0.398 0.108 0.665 0.218 0.815 0.297l-4.98 42c0.0034-0.0282 0.021 0.0545-0.337 0.245-0.358 0.19-1.02 0.412-2.02 0.595-2 0.366-5.35 0.605-10.4 0.605s-8.45-0.24-10.4-0.605c-1-0.183-1.66-0.404-2.02-0.595-0.358-0.19-0.34-0.274-0.337-0.245l-4.98-42c0.15-0.0785 0.416-0.189 0.815-0.297 0.865-0.234 2.2-0.46 3.87-0.65 3.33-0.377 8-0.615 13.1-0.615z"
-    tools:ignore="VectorPath" />
+    tools:ignore="VectorPath"/>
   <path
     android:fillColor="#ffe2b7"
     android:pathData="m45 44.4a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm18 4.49a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm-19.4 6.2a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm3.92 3.85a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 0 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm12.2 0.725a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-4.46 4.02a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-6.35 1.4a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm11.6 1.92a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-5.15 1.54a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.48a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm10.4 3.06a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.132a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm3.72 1.64a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm2.63 2.34a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845z"
-    tools:ignore="VectorPath" />
+    tools:ignore="VectorPath"/>
   <path
     android:fillColor="#d91616"
-    android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z" />
+    android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z"/>
 </vector>
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 7353dbd1fd82487df2d06121f85f7994728f1070..5f349f7f4707960b76e4f706aa5f8554acb63002 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/ic_launcher_background"/>
-    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
-</adaptive-icon>
\ No newline at end of file
+  <background android:drawable="@color/ic_launcher_background"/>
+  <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 7353dbd1fd82487df2d06121f85f7994728f1070..5f349f7f4707960b76e4f706aa5f8554acb63002 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/ic_launcher_background"/>
-    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
-</adaptive-icon>
\ No newline at end of file
+  <background android:drawable="@color/ic_launcher_background"/>
+  <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
index 4e4cd80f69516eb0a9951d00a0c957be874afadf..c47f2aa0c5e494cf3bb3d4cd48ba96043840e2aa 100644
--- a/app/src/main/res/values/ic_launcher_background.xml
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <color name="ic_launcher_background">#003984</color>
-</resources>
\ No newline at end of file
+  <color name="ic_launcher_background">#003984</color>
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index be7b46ab25ee168fcf368cf7c7daf2ada7607b9b..4e1d48592089b3f250762f4f2ebe5bdd53f6c6a7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -23,5 +23,5 @@
   -->
 
 <resources>
-    <string name="application_name">Meteroid</string>
+  <string name="application_name">Meteroid</string>
 </resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 37bf28c89a74f94745d6c5dffff7fe11d9429a36..5cb87d3878fef7803f58127e1ee5439e2288d689 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -9,7 +9,7 @@
     <item name="android:dialogTheme">@style/Theme.DialogFullScreen</item>
   </style>
 
-  <style name="Theme.Material.DayNight.NoActionBar" parent="@android:style/Theme.Material.Light.NoActionBar" />
+  <style name="Theme.Material.DayNight.NoActionBar" parent="@android:style/Theme.Material.Light.NoActionBar"/>
 
   <style name="Theme.DialogFullScreen" parent="Theme.Material.DayNight.NoActionBar">
     <item name="android:windowMinWidthMajor">100%</item>
diff --git a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
index a6a58c475dcc2b736963bf649aad2bbd940a4f26..17c95bdce68c7c7ad353a9863b19012408e8fbcd 100644
--- a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
+++ b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
@@ -5,7 +5,7 @@ import org.gradle.api.Project
 import org.gradle.kotlin.dsl.configure
 import util.cmd
 import util.properties
-import java.util.Locale
+import java.util.*
 
 class AndroidApplicationConvention : Plugin<Project> {
   override fun apply(target: Project) {
diff --git a/gradle/convention/src/main/kotlin/KotlinConvention.kt b/gradle/convention/src/main/kotlin/KotlinConvention.kt
index d99f88171db54cdf468239e7c182cf8221b3d722..ce81fe9eb85e65db3651620ce81591d24ecda418 100644
--- a/gradle/convention/src/main/kotlin/KotlinConvention.kt
+++ b/gradle/convention/src/main/kotlin/KotlinConvention.kt
@@ -1,4 +1,3 @@
-import org.gradle.api.JavaVersion
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.plugins.JavaPluginExtension
@@ -7,9 +6,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
 import org.gradle.kotlin.dsl.configure
 import org.gradle.kotlin.dsl.withType
 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
 import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
-import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
 
 class KotlinConvention : Plugin<Project> {
   override fun apply(target: Project) {
diff --git a/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt b/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt
index 9ab40febbebc72d5631e5771626939ccef16e4a9..d1d4eb7c97f860d3191b739e454e88ed534cade5 100644
--- a/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt
+++ b/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt
@@ -2,7 +2,7 @@ package util
 
 import org.gradle.api.Project
 import java.io.ByteArrayOutputStream
-import java.util.Properties
+import java.util.*
 
 fun Project.cmd(vararg command: String) = try {
   val stdOut = ByteArrayOutputStream()
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index da771205e5262804226dfbf17796ae06dafa126f..b20ab82db420a8686c8409baae7b6afc22e5e301 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,12 +2,10 @@
 androidGradlePlugin = "8.1.4"
 androidx-activity = "1.8.1"
 androidx-appcompat = "1.6.1"
-androidx-compose-bom = "2023.10.01"
+androidx-compose = "1.6.0-beta02"
 androidx-compose-compiler = "1.5.5"
-androidx-compose-material = "1.5.0-alpha04"
 androidx-compose-material3 = "1.2.0-alpha12"
 androidx-compose-runtimetracing = "1.0.0-beta01"
-androidx-compose-tooling = "1.6.0-beta02"
 androidx-datastore = "1.0.0"
 androidx-hilt = "1.1.0"
 androidx-navigation = "2.7.5"
@@ -30,21 +28,20 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a
 androidx-appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "androidx-appcompat" }
 
 androidx-compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "androidx-compose-compiler" }
-androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" }
-androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" }
-androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
-androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
-androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.6.0-beta01" }
+androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "androidx-compose" }
+androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" }
+androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "androidx-compose" }
+androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "androidx-compose" }
 androidx-compose-material = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
 androidx-compose-material-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "androidx-compose-material3" }
-androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
-androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
+androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidx-compose" }
+androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "androidx-compose" }
 androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidx-compose-runtimetracing" }
-androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test" }
-androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
-androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose-tooling" }
-androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose-tooling" }
-androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
+androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test", version.ref = "androidx-compose" }
+androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidx-compose" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose" }
+androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose" }
+androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "androidx-compose" }
 
 androidx-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" }
 
@@ -91,4 +88,4 @@ android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
 dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger-hilt" }
 kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
-kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin.ksp" }
+kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" }
diff --git a/lint.xml b/lint.xml
index eab67bd5e6f29d10e42082945d8a9195c76e4777..cbbcf6ba3fdaeba8e6833b9fc78311ac025f2041 100644
--- a/lint.xml
+++ b/lint.xml
@@ -18,36 +18,36 @@
   -->
 
 <lint>
-  <issue id="NewerVersionAvailable" severity="error" />
+  <issue id="NewerVersionAvailable" severity="error"/>
 
   <!-- Because of course paging and room have incompatible versions -->
-  <issue id="GradleCompatible" severity="ignore" />
+  <issue id="GradleCompatible" severity="ignore"/>
 
   <!-- Because these are entirely broken -->
-  <issue id="ResourceType" severity="ignore" />
-  <issue id="UnusedResources" severity="ignore" />
-  <issue id="ObsoleteLintCustomCheck" severity="ignore" />
-  <issue id="UnusedAttribute" severity="informational" />
+  <issue id="ResourceType" severity="ignore"/>
+  <issue id="UnusedResources" severity="ignore"/>
+  <issue id="ObsoleteLintCustomCheck" severity="ignore"/>
+  <issue id="UnusedAttribute" severity="informational"/>
 
   <!-- Because this doesn’t work when using splash themes -->
-  <issue id="Overdraw" severity="ignore" />
+  <issue id="Overdraw" severity="ignore"/>
 
   <!-- Can’t request a translation without a release, can’t release without translation -->
-  <issue id="MissingTranslation" severity="informational" />
+  <issue id="MissingTranslation" severity="informational"/>
   <!-- Because we don’t use app bundles and never will use them -->
-  <issue id="AppBundleLocaleChanges" severity="informational" />
+  <issue id="AppBundleLocaleChanges" severity="informational"/>
   <!-- Because this tries to apply english orthography to other locales -->
-  <issue id="Typos" severity="ignore" />
+  <issue id="Typos" severity="ignore"/>
 
   <!-- Because Autofill isn’t a priority at the moment -->
-  <issue id="Autofill" severity="informational" />
+  <issue id="Autofill" severity="informational"/>
 
   <!-- We’re not using AGP 5 yet, and once we are, we’ll use compose anyway -->
-  <issue id="NonConstantResourceId" severity="informational" />
+  <issue id="NonConstantResourceId" severity="informational"/>
 
   <!-- It’s only used for testing -->
-  <issue id="TrustAllX509TrustManager" severity="informational" />
+  <issue id="TrustAllX509TrustManager" severity="informational"/>
 
   <!-- TODO for the future -->
-  <issue id="DataExtractionRules" severity="informational" />
+  <issue id="DataExtractionRules" severity="informational"/>
 </lint>
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
index 63251aedabdef9726f662283626f9524c46fd7a0..b82df8560055f323059feae97ad8fb2c8413e972 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
@@ -24,20 +24,10 @@
 
 package de.chaosdorf.meteroid
 
-import androidx.room.AutoMigration
 import androidx.room.Database
 import androidx.room.RoomDatabase
 import androidx.room.TypeConverters
-import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.PinnedUser
-import de.chaosdorf.meteroid.model.PinnedUserRepository
-import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.ServerRepository
-import de.chaosdorf.meteroid.model.Transaction
-import de.chaosdorf.meteroid.model.TransactionRepository
-import de.chaosdorf.meteroid.model.User
-import de.chaosdorf.meteroid.model.UserRepository
+import de.chaosdorf.meteroid.model.*
 import de.chaosdorf.meteroid.util.BigDecimalTypeConverter
 import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter
 
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 18d5a79f78d29eb6b0b19fb5bec93f657aeb6244..97593d1a4582920e596ac9f786ae611528765026 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
@@ -24,14 +24,10 @@
 
 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 androidx.room.*
 import de.chaosdorf.mete.model.DrinkId
 import de.chaosdorf.mete.model.DrinkModel
+import de.chaosdorf.mete.model.ServerId
 import kotlinx.coroutines.flow.Flow
 import java.math.BigDecimal
 import java.net.URI
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt
index 455a90c60905ed9b4ae50a176205c65ae2a7db7e..8478850af0995ae3f46ec71d90406bb0f35d1bc4 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt
@@ -24,12 +24,8 @@
 
 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 androidx.room.*
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
 import kotlinx.coroutines.flow.Flow
 
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 0e94a9b1620592af7dae9064b7bb02c6d60f6b70..5537758ac79cef1e724c88ad1a78149955d050c0 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt
@@ -24,17 +24,10 @@
 
 package de.chaosdorf.meteroid.model
 
-import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.PrimaryKey
-import androidx.room.Query
+import androidx.room.*
+import de.chaosdorf.mete.model.ServerId
 import kotlinx.coroutines.flow.Flow
 
-@JvmInline
-value class ServerId(val value: Long)
-
 @Entity
 data class Server(
   @PrimaryKey
@@ -51,6 +44,7 @@ interface ServerRepository {
 
   @Query("SELECT * FROM Server WHERE serverId = :id LIMIT 1")
   fun getFlow(id: ServerId): Flow<Server?>
+
   @Query("SELECT count(*) FROM Server")
   fun countFlow(): Flow<Int>
 
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
index f3206899c63ef84c68c0f5a77dcafb8eb80aa5f2..66da9650e25aba981fbc340bacc220e2846caea0 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
@@ -24,17 +24,8 @@
 
 package de.chaosdorf.meteroid.model
 
-import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.Index
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import de.chaosdorf.mete.model.DrinkId
-import de.chaosdorf.mete.model.TransactionId
-import de.chaosdorf.mete.model.TransactionModel
-import de.chaosdorf.mete.model.UserId
+import androidx.room.*
+import de.chaosdorf.mete.model.*
 import kotlinx.coroutines.flow.Flow
 import kotlinx.datetime.Instant
 import java.math.BigDecimal
@@ -86,6 +77,7 @@ interface TransactionRepository {
 
   @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC")
   fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Transaction>>
+
   @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC LIMIT 1")
   suspend fun getLatest(serverId: ServerId, userId: UserId): Transaction?
 
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
index 8052cde69ea1e8741709c690ae36726743b5c3fa..dd0c94f575f61fe6a02396b3580d701054c8df08 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
@@ -24,18 +24,13 @@
 
 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 androidx.room.*
+import de.chaosdorf.mete.model.ServerId
 import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.mete.model.UserModel
+import de.chaosdorf.meteroid.util.gravatarUrl
 import kotlinx.coroutines.flow.Flow
 import java.math.BigDecimal
-import java.security.MessageDigest
-import java.util.Locale
 
 @Entity(
   primaryKeys = ["serverId", "userId"],
@@ -53,16 +48,7 @@ data class User(
   val audit: Boolean,
   val redirect: Boolean,
 ) {
-  @OptIn(ExperimentalStdlibApi::class)
-  val gravatarUrl: String? by lazy {
-    email?.let {
-      val normalised: String = it.lowercase(Locale.ROOT)
-      val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8)
-      val binaryHash: ByteArray = MessageDigest.getInstance("MD5").digest(binaryData)
-      val hash: String = binaryHash.toHexString()
-      "https://www.gravatar.com/avatar/$hash?d=404&s=640"
-    }
-  }
+  val gravatarUrl: String? by lazy { email?.let(::gravatarUrl) }
 
   companion object {
     fun fromModel(server: Server, value: UserModel) = User(
@@ -86,6 +72,12 @@ interface UserRepository {
   @Query("SELECT * FROM User WHERE serverId = :serverId AND userId = :userId LIMIT 1")
   fun getFlow(serverId: ServerId, userId: UserId): Flow<User?>
 
+  @Query("SELECT * FROM User ORDER BY NAME ASC")
+  suspend fun getAll(): List<User>
+
+  @Query("SELECT * FROM User ORDER BY NAME ASC")
+  fun getAllFlow(): Flow<List<User>>
+
   @Query("SELECT * FROM User WHERE serverId = :serverId ORDER BY NAME ASC")
   suspend fun getAll(serverId: ServerId): List<User>
 
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/GravatarUrl.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/GravatarUrl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..05700b469503f7f2b2f192eb06c3487d3b112f0b
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/GravatarUrl.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.util
+
+import java.security.MessageDigest
+import java.util.*
+
+enum class GravatarFallbackType(val tag: String) {
+  FileNotFound("404"),
+  MysteryPerson("mp"),
+  Identicon("identicon"),
+  MonsterId("monsterid"),
+  Wavatar("wavatar"),
+  Retro("retro"),
+  Robohash("robohash"),
+  Blank("blank")
+}
+
+enum class GravatarRating(val tag: String) {
+  General("g"),
+  ParentalGuidance("pg"),
+  Restricted("r"),
+  Adult("x")
+}
+
+@OptIn(ExperimentalStdlibApi::class)
+fun gravatarUrl(
+  email: String,
+  size: Int = 640,
+  fallback: GravatarFallbackType = GravatarFallbackType.FileNotFound,
+  rating: GravatarRating = GravatarRating.General,
+): String {
+  val normalised: String = email.trim().lowercase(Locale.ROOT)
+  val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8)
+  val binaryHash: ByteArray = MessageDigest.getInstance("SHA-256").digest(binaryData)
+  val hash: String = binaryHash.toHexString()
+  return "https://gravatar.com/avatar/$hash?default=${fallback.tag}&size=$size&rating=${rating.tag}"
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt
index 289ea21afd72e5fce225fbf92445cf4930f55241..b39982e33824dabfb16969eee3bd89280206ace0 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt
@@ -30,6 +30,7 @@ import kotlinx.datetime.Instant
 class KotlinDatetimeTypeConverter {
   @TypeConverter
   fun load(value: Long): Instant = Instant.fromEpochMilliseconds(value)
+
   @TypeConverter
   fun store(value: Instant): Long = value.toEpochMilliseconds()
 }