From 7efc3a32e8de1ab3cd276755cda5bf1825f0b228 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <mail@justjanne.de>
Date: Tue, 10 Jun 2025 03:42:59 +0200
Subject: [PATCH] refactor: navigation

---
 .../de/chaosdorf/meteroid/MainActivity.kt     |   4 +-
 .../meteroid/ui/common/PriceBadge.kt          |  18 +-
 .../de/chaosdorf/meteroid/ui/common/TopBar.kt | 315 ------------------
 .../ui/navigation/NavigationAddServerItem.kt  |  17 +-
 .../ui/navigation/NavigationServerItem.kt     |  25 +-
 .../ui/navigation/NavigationServerListItem.kt |  26 +-
 .../ui/navigation/NavigationSettingsItem.kt   |  17 +-
 .../ui/navigation/NavigationUserItem.kt       |  53 ++-
 .../ui/navigation/NavigationUserListItem.kt   |  18 +-
 .../ui/navigation/OverlayNavigation.kt        | 207 ++++++++++++
 .../ui/navigation/PersistentNavigation.kt     | 135 ++++++++
 .../meteroid/ui/navigation/TopNavigation.kt   |  70 ++++
 .../meteroid/ui/purchase/PurchaseDrinkTile.kt |  20 +-
 .../ui/purchase/PurchaseFilterChip.kt         |   2 +-
 .../meteroid/ui/purchase/PurchaseFilterRow.kt |   2 +-
 .../meteroid/ui/purchase/PurchaseRoute.kt     |   1 +
 .../ui/{purchase => wrapped}/WrappedBanner.kt |  12 +-
 .../meteroid/viewmodel/NavigationViewModel.kt |   4 +-
 18 files changed, 558 insertions(+), 388 deletions(-)
 delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/TopBar.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/OverlayNavigation.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/TopNavigation.kt
 rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{purchase => wrapped}/WrappedBanner.kt (89%)

diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
index a4ea473..15ee94a 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
@@ -45,7 +45,7 @@ import dagger.hilt.android.lifecycle.withCreationCallback
 import de.chaosdorf.meteroid.theme.MeteroidTheme
 import de.chaosdorf.meteroid.ui.*
 import de.chaosdorf.meteroid.ui.common.BottomBar
-import de.chaosdorf.meteroid.ui.common.TopBar
+import de.chaosdorf.meteroid.ui.navigation.TopNavigation
 import de.chaosdorf.meteroid.ui.purchase.PurchaseRoute
 import de.chaosdorf.meteroid.viewmodel.NavigationViewModel
 import de.chaosdorf.meteroid.viewmodel.*
@@ -84,7 +84,7 @@ class MainActivity : ComponentActivity() {
           val backStack by navigationViewModel.backStack.collectAsState()
 
           Scaffold(
-            topBar = { TopBar(navigationViewModel) },
+            topBar = { TopNavigation(navigationViewModel) },
             bottomBar = { BottomBar(navigationViewModel) }
           ) { paddingValues ->
             NavDisplay(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/PriceBadge.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/PriceBadge.kt
index 73317da..f78c93b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/PriceBadge.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/PriceBadge.kt
@@ -29,8 +29,8 @@ import androidx.compose.material3.Badge
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
 import java.math.BigDecimal
@@ -39,22 +39,24 @@ import java.math.BigDecimal
 fun PriceBadge(
   price: BigDecimal,
   modifier: Modifier = Modifier,
-  containerColor: Color =
-    if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.primary
-    else MaterialTheme.colorScheme.error,
-  textColor: Color =
-    if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary
-    else MaterialTheme.colorScheme.onError,
   textStyle: TextStyle = MaterialTheme.typography.labelLarge
 ) {
+  val positive = remember(price) { price >= BigDecimal.ZERO }
+
+  val containerColor = if (positive) MaterialTheme.colorScheme.primary
+  else MaterialTheme.colorScheme.error
+
+  val textColor = if (positive) MaterialTheme.colorScheme.onPrimary
+  else MaterialTheme.colorScheme.onError
+
   Badge(
     containerColor = containerColor,
+    contentColor = textColor,
     modifier = modifier
   ) {
     Text(
       "%.02f €".format(price),
       style = textStyle,
-      color = textColor,
       modifier = Modifier.padding(8.dp, 4.dp)
     )
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/TopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/TopBar.kt
deleted file mode 100644
index 0b8f26f..0000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/TopBar.kt
+++ /dev/null
@@ -1,315 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2013-2025 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.common
-
-import android.annotation.SuppressLint
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.*
-import androidx.compose.animation.core.animateDp
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Star
-import androidx.compose.material.icons.filled.StarOutline
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.TileMode
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupProperties
-import de.chaosdorf.meteroid.ui.MeteroidRoute
-import de.chaosdorf.meteroid.ui.MeteroidRoute.Purchase
-import de.chaosdorf.meteroid.ui.MeteroidRoute.UserList
-import de.chaosdorf.meteroid.ui.navigation.*
-import de.chaosdorf.meteroid.viewmodel.NavigationElement
-import de.chaosdorf.meteroid.viewmodel.NavigationViewModel
-import kotlinx.coroutines.flow.update
-
-@SuppressLint("UnusedTransitionTargetStateParameter")
-@OptIn(ExperimentalSharedTransitionApi::class)
-@Composable
-fun TopBar(
-  viewModel: NavigationViewModel,
-) {
-  val backStack = viewModel.backStack.collectAsState().value
-  val currentRoute = backStack.lastOrNull()
-
-  val entries by viewModel.entries.collectAsState()
-  val currentEntry = entries.firstOrNull { it.isCurrent(currentRoute) }
-  var open by remember { mutableStateOf(false) }
-  val transition = updateTransition(open, label = "transition")
-  val shadowElevation by transition.animateDp { if (it) 4.dp else 0.dp }
-
-  if (currentEntry != null) {
-    Box(
-      Modifier.background(
-        Brush.linearGradient(
-          listOf(
-            MaterialTheme.colorScheme.background,
-            MaterialTheme.colorScheme.background.copy(alpha = 0f),
-          ),
-          start = Offset(0.0f, 0.0f),
-          end = Offset(0.0f, Float.POSITIVE_INFINITY),
-          tileMode = TileMode.Clamp,
-        )
-      )
-    ) {
-      Surface(
-        modifier = Modifier
-          .windowInsetsPadding(WindowInsets.statusBars)
-          .padding(8.dp),
-        shape = RoundedCornerShape(26.dp),
-        shadowElevation = 4.dp - shadowElevation,
-        tonalElevation = 4.dp,
-      ) {
-        when (currentEntry) {
-          NavigationElement.ServerListElement ->
-            NavigationServerListItem(
-              modifier = Modifier
-                .requiredHeight(56.dp)
-                .clickable {
-                  open = true
-                },
-            )
-
-          is NavigationElement.ServerElement ->
-            NavigationServerItem(
-              currentEntry.server,
-              modifier = Modifier
-                .requiredHeight(56.dp)
-                .clickable {
-                  open = true
-                },
-            )
-
-          is NavigationElement.UserElement ->
-            NavigationUserItem(
-              currentEntry.user,
-              modifier = Modifier
-                .requiredHeight(56.dp)
-                .clickable {
-                  open = true
-                },
-            ) {
-              IconButton(onClick = { viewModel.togglePin(currentEntry.user.serverId, currentEntry.user.userId) }) {
-                Icon(
-                  if (currentEntry.pinned) Icons.Default.Star else Icons.Default.StarOutline,
-                  contentDescription = null
-                )
-              }
-            }
-
-          is NavigationElement.UserListElement ->
-            NavigationUserListItem(
-              modifier = Modifier
-                .requiredHeight(56.dp)
-                .clickable {
-                  open = true
-                },
-            )
-
-          NavigationElement.AddServerElement ->
-            NavigationAddServerItem(
-              modifier = Modifier
-                .requiredHeight(56.dp)
-                .clickable {
-                  open = true
-                },
-            )
-
-          NavigationElement.SettingsElement ->
-            NavigationSettingsItem(
-              modifier = Modifier
-                .requiredHeight(56.dp)
-                .clickable {
-                  open = true
-                },
-            )
-        }
-      }
-    }
-
-    if (open || transition.currentState || transition.isRunning) {
-      Popup(
-        offset = IntOffset(
-          x = WindowInsets.safeDrawing.getLeft(LocalDensity.current, LocalLayoutDirection.current),
-          y = WindowInsets.safeDrawing.getTop(LocalDensity.current),
-        ),
-        onDismissRequest = { open = false },
-        properties = PopupProperties(
-          usePlatformDefaultWidth = false,
-          dismissOnClickOutside = true,
-          dismissOnBackPress = true,
-          clippingEnabled = false,
-        ),
-      ) {
-        BackHandler {
-          open = false
-        }
-
-        val smallItemHeight by transition.animateDp { if (it) 48.dp else 56.dp }
-        val itemHeight by transition.animateDp { 56.dp }
-
-        Box(
-          modifier = Modifier
-            .fillMaxSize()
-            .clickable(interactionSource = null, indication = null) {
-              open = false
-            }
-        ) {
-          Surface(
-            modifier = Modifier.padding(8.dp),
-            shape = RoundedCornerShape(26.dp),
-            shadowElevation = shadowElevation,
-            tonalElevation = 4.dp,
-          ) {
-            Column {
-              for (entry in entries) {
-                key(entry.key) {
-                  transition.AnimatedVisibility(
-                    visible = { it || currentEntry == entry },
-                    enter = expandVertically() + fadeIn(),
-                    exit = shrinkVertically() + fadeOut(),
-                  ) {
-                    when (entry) {
-                      NavigationElement.ServerListElement ->
-                        NavigationServerListItem(
-                          modifier = Modifier
-                            .requiredHeight(itemHeight)
-                            .clickable {
-                              viewModel.backStack.update {
-                                listOf(MeteroidRoute.ServerList)
-                              }
-                              open = false
-                            },
-                        )
-
-                      is NavigationElement.ServerElement ->
-                        NavigationServerItem(
-                          server = entry.server,
-                          modifier = Modifier
-                            .requiredHeight(itemHeight)
-                            .clickable {
-                              viewModel.backStack.update {
-                                listOf(MeteroidRoute.ServerList, UserList(entry.server.serverId))
-                              }
-                              open = false
-                            },
-                        )
-
-                      is NavigationElement.UserElement ->
-                        NavigationUserItem(
-                          user = entry.user,
-                          modifier = Modifier
-                            .requiredHeight(itemHeight)
-                            .clickable {
-                              viewModel.backStack.update {
-                                listOf(
-                                  MeteroidRoute.ServerList,
-                                  UserList(entry.user.serverId),
-                                  Purchase(entry.user.serverId, entry.user.userId)
-                                )
-                              }
-                              open = false
-                            },
-                        ) {
-                          transition.AnimatedVisibility(
-                            visible = { !it && currentEntry == entry },
-                            enter = fadeIn(),
-                            exit = fadeOut(),
-                          ) {
-                            IconButton(onClick = { viewModel.togglePin(entry.user.serverId, entry.user.userId) }) {
-                              Icon(
-                                if (entry.pinned) Icons.Default.Star else Icons.Default.StarOutline,
-                                contentDescription = null
-                              )
-                            }
-                          }
-                        }
-
-                      is NavigationElement.UserListElement ->
-                        NavigationUserListItem(
-                          modifier = Modifier
-                            .requiredHeight(smallItemHeight)
-                            .clickable {
-                              viewModel.backStack.update {
-                                listOf(MeteroidRoute.ServerList, UserList(entry.server.serverId))
-                              }
-                              open = false
-                            },
-                        )
-
-                      NavigationElement.AddServerElement ->
-                        NavigationAddServerItem(
-                          modifier = Modifier
-                            .requiredHeight(smallItemHeight)
-                            .clickable {
-                              viewModel.backStack.update {
-                                it.plus(MeteroidRoute.Setup)
-                              }
-                              open = false
-                            },
-                        )
-
-                      NavigationElement.SettingsElement ->
-                        NavigationSettingsItem(
-                          modifier = Modifier
-                            .requiredHeight(smallItemHeight)
-                            .clickable {
-                              viewModel.backStack.update {
-                                it.plus(MeteroidRoute.Settings)
-                              }
-                              open = false
-                            },
-                        )
-                    }
-                  }
-                  if (entry is NavigationElement.UserListElement || entry is NavigationElement.ServerListElement) {
-                    transition.AnimatedVisibility(
-                      visible = { it },
-                      enter = expandVertically() + fadeIn(),
-                      exit = shrinkVertically() + fadeOut(),
-                    ) {
-                      HorizontalDivider()
-                    }
-                  }
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt
index 36679ef..13e3e58 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt
@@ -24,6 +24,8 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateDp
 import androidx.compose.foundation.layout.*
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
@@ -38,8 +40,19 @@ import androidx.compose.ui.unit.dp
 import de.chaosdorf.meteroid.ui.common.AvatarLayout
 
 @Composable
-fun NavigationAddServerItem(modifier: Modifier = Modifier) {
-  Row(modifier.padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+fun NavigationAddServerItem(
+  modifier: Modifier = Modifier,
+  active: Boolean = false,
+  transition: Transition<Boolean>? = null,
+) {
+  val itemHeight = transition?.animateDp { if (it && !active) 48.dp else 56.dp }?.value ?: 56.dp
+
+  Row(
+    modifier
+      .requiredHeight(itemHeight)
+      .padding(horizontal = 12.dp),
+    verticalAlignment = Alignment.CenterVertically,
+  ) {
     AvatarLayout {
       Icon(Icons.Default.Add, contentDescription = null)
     }
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
index fde8d7f..dcde41d 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt
@@ -24,16 +24,11 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.ListItem
+import androidx.compose.foundation.layout.*
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow
@@ -44,10 +39,20 @@ import de.chaosdorf.meteroid.util.humanReadableHost
 import okhttp3.HttpUrl.Companion.toHttpUrl
 
 @Composable
-fun NavigationServerItem(server: Server, modifier: Modifier = Modifier) {
-  val host = humanReadableHost(server.url.toHttpUrl())
+fun NavigationServerItem(
+  server: Server,
+  modifier: Modifier = Modifier,
+) {
+  val host = remember(server) {
+    humanReadableHost(server.url.toHttpUrl())
+  }
 
-  Row(modifier.padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+  Row(
+    modifier = modifier
+      .requiredHeight(56.dp)
+      .padding(horizontal = 12.dp),
+    verticalAlignment = Alignment.CenterVertically,
+  ) {
     ServerAvatar(server.logoUrl)
     Spacer(Modifier.width(16.dp))
     Column(Modifier.weight(1f, true)) {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerListItem.kt
index ee34bb8..5ceced3 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerListItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerListItem.kt
@@ -26,38 +26,36 @@ package de.chaosdorf.meteroid.ui.navigation
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.Icon
-import androidx.compose.material3.ListItem
 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.clip
-import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.imageResource
 import androidx.compose.ui.res.vectorResource
 import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
-import de.chaosdorf.meteroid.ui.common.ServerAvatar
 import de.chaosdorf.meteroid.R
 import de.chaosdorf.meteroid.ui.common.AvatarLayout
 
+@Preview(showBackground = true)
 @Composable
-fun NavigationServerListItem(modifier: Modifier = Modifier) {
+fun NavigationServerListItem(
+  modifier: Modifier = Modifier,
+) {
   val icon = ImageVector.vectorResource(R.drawable.ic_launcher)
 
-  Row(modifier.padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+  Row(
+    modifier = modifier
+      .requiredHeight(56.dp)
+      .padding(horizontal = 12.dp),
+    verticalAlignment = Alignment.CenterVertically,
+  ) {
     AvatarLayout(
       Modifier
         .clip(CircleShape)
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
index b7ba018..faff304 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt
@@ -24,6 +24,8 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateDp
 import androidx.compose.foundation.layout.*
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Settings
@@ -38,8 +40,19 @@ import androidx.compose.ui.unit.dp
 import de.chaosdorf.meteroid.ui.common.AvatarLayout
 
 @Composable
-fun NavigationSettingsItem(modifier: Modifier = Modifier) {
-  Row(modifier.padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+fun NavigationSettingsItem(
+  modifier: Modifier = Modifier,
+  active: Boolean = false,
+  transition: Transition<Boolean>? = null,
+) {
+  val itemHeight = transition?.animateDp { if (it && !active) 48.dp else 56.dp }?.value ?: 56.dp
+
+  Row(
+    modifier = modifier
+      .requiredHeight(itemHeight)
+      .padding(horizontal = 12.dp),
+    verticalAlignment = Alignment.CenterVertically,
+  ) {
     AvatarLayout {
       Icon(Icons.Default.Settings, contentDescription = null)
     }
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
index 684450e..4517b81 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt
@@ -24,13 +24,11 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
-import androidx.compose.foundation.layout.Column
-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.requiredHeight
-import androidx.compose.foundation.layout.width
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.*
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Star
 import androidx.compose.material.icons.filled.StarOutline
@@ -39,12 +37,11 @@ import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 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.common.PriceBadge
 import de.chaosdorf.meteroid.ui.common.UserAvatar
@@ -52,10 +49,18 @@ import de.chaosdorf.meteroid.ui.common.UserAvatar
 @Composable
 fun NavigationUserItem(
   user: User,
+  active: Boolean,
+  pinned: Boolean,
   modifier: Modifier = Modifier,
-  actions: @Composable () -> Unit = {},
+  transition: Transition<Boolean>? = null,
+  onPin: (User) -> Unit = {},
 ) {
-  Row(modifier.padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+  Row(
+    modifier = modifier
+      .requiredHeight(56.dp)
+      .padding(horizontal = 12.dp),
+    verticalAlignment = Alignment.CenterVertically,
+  ) {
     UserAvatar(user.gravatarUrl)
     Spacer(Modifier.width(16.dp))
     Column(Modifier.weight(1f, true)) {
@@ -77,7 +82,31 @@ fun NavigationUserItem(
       }
     }
     Spacer(Modifier.width(16.dp))
-    actions()
+    if (transition != null) {
+      transition.AnimatedVisibility(
+        visible = { !it && active },
+        enter = fadeIn(),
+        exit = fadeOut(),
+      ) {
+        PinButton(pinned) { onPin(user) }
+      }
+    } else {
+      PinButton(pinned) { onPin(user) }
+    }
     PriceBadge(user.balance)
   }
 }
+
+@Composable
+private fun PinButton(
+  pinned: Boolean,
+  onPin: () -> Unit,
+) {
+  val pinIcon = remember(pinned) {
+    if (pinned) Icons.Default.Star else Icons.Default.StarOutline
+  }
+
+  IconButton(onClick = onPin) {
+    Icon(pinIcon, contentDescription = "Pin")
+  }
+}
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
index da5f590..244d2a2 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt
@@ -24,14 +24,16 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateDp
 import androidx.compose.foundation.layout.*
 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.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow
@@ -39,8 +41,18 @@ import androidx.compose.ui.unit.dp
 import de.chaosdorf.meteroid.ui.common.AvatarLayout
 
 @Composable
-fun NavigationUserListItem(modifier: Modifier = Modifier) {
-  Row(modifier.padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+fun NavigationUserListItem(
+  modifier: Modifier = Modifier,
+  transition: Transition<Boolean>? = null,
+) {
+  val itemHeight = transition?.animateDp { if (it) 48.dp else 56.dp }?.value ?: 56.dp
+
+  Row(
+    modifier = modifier
+      .requiredHeight(itemHeight)
+      .padding(horizontal = 12.dp),
+    verticalAlignment = Alignment.CenterVertically,
+  ) {
     AvatarLayout {
       Icon(Icons.Default.Group, contentDescription = null)
     }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/OverlayNavigation.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/OverlayNavigation.kt
new file mode 100644
index 0000000..9c89600
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/OverlayNavigation.kt
@@ -0,0 +1,207 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 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.*
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateDp
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+import de.chaosdorf.meteroid.ui.MeteroidRoute
+import de.chaosdorf.meteroid.viewmodel.NavigationElement
+import de.chaosdorf.meteroid.viewmodel.NavigationViewModel
+import kotlinx.coroutines.flow.update
+
+@Composable
+fun OverlayNavigation(
+  viewModel: NavigationViewModel,
+  transition: Transition<Boolean>,
+  onClose: () -> Unit,
+) {
+  val backStack = viewModel.backStack.collectAsState()
+  val entries = viewModel.entries.collectAsState()
+  val currentEntry = remember {
+    derivedStateOf {
+      entries.value.firstOrNull { it.isCurrent(backStack.value.lastOrNull()) }
+    }
+  }
+
+  Popup(
+    onDismissRequest = onClose,
+    properties = PopupProperties(
+      usePlatformDefaultWidth = false,
+      dismissOnClickOutside = true,
+      dismissOnBackPress = true,
+    ),
+  ) {
+    val shadowElevation = transition.animateDp { if (it) 4.dp else 0.dp }
+
+    BackHandler(onBack = onClose)
+
+    Box(
+      modifier = Modifier
+        .fillMaxSize()
+        .clickable(interactionSource = null, indication = null, onClick = onClose)
+    ) {
+      Surface(
+        modifier = Modifier
+          .padding(8.dp)
+          .graphicsLayer {
+            this.shadowElevation = shadowElevation.value.toPx()
+            shape = NavigationShape
+            clip = false
+          },
+        shape = NavigationShape,
+        tonalElevation = 4.dp,
+      ) {
+        Column {
+          for (entry in entries.value) {
+            key(entry.key) {
+              val active = remember {
+                derivedStateOf {
+                  currentEntry.value == entry
+                }
+              }
+              transition.AnimatedVisibility(
+                visible = { it || currentEntry.value == entry },
+                enter = expandVertically() + fadeIn(),
+                exit = shrinkVertically() + fadeOut(),
+              ) {
+                when (entry) {
+                  NavigationElement.ServerListElement ->
+                    NavigationServerListItem(
+                      modifier = Modifier
+                        .clickable {
+                          viewModel.backStack.update {
+                            listOf(MeteroidRoute.ServerList)
+                          }
+                          onClose()
+                        },
+                    )
+
+                  is NavigationElement.ServerElement ->
+                    NavigationServerItem(
+                      server = entry.server,
+                      modifier = Modifier
+                        .clickable {
+                          viewModel.backStack.update {
+                            listOf(
+                              MeteroidRoute.ServerList,
+                              MeteroidRoute.UserList(entry.server.serverId)
+                            )
+                          }
+                          onClose()
+                        },
+                    )
+
+                  is NavigationElement.UserElement ->
+                    NavigationUserItem(
+                      user = entry.user,
+                      pinned = entry.pinned,
+                      active = active.value,
+                      transition = transition,
+                      modifier = Modifier.clickable {
+                        viewModel.backStack.update {
+                          listOf(
+                            MeteroidRoute.ServerList,
+                            MeteroidRoute.UserList(entry.user.serverId),
+                            MeteroidRoute.Purchase(entry.user.serverId, entry.user.userId)
+                          )
+                        }
+                        onClose()
+                      },
+                      onPin = {},
+                    )
+
+                  is NavigationElement.UserListElement ->
+                    NavigationUserListItem(
+                      transition = transition,
+                      modifier = Modifier
+                        .clickable {
+                          viewModel.backStack.update {
+                            listOf(
+                              MeteroidRoute.ServerList,
+                              MeteroidRoute.UserList(entry.server.serverId)
+                            )
+                          }
+                          onClose()
+                        },
+                    )
+
+                  NavigationElement.AddServerElement ->
+                    NavigationAddServerItem(
+                      transition = transition,
+                      active = active.value,
+                      modifier = Modifier
+                        .clickable {
+                          viewModel.backStack.update {
+                            it.plus(MeteroidRoute.Setup)
+                          }
+                          onClose()
+                        },
+                    )
+
+                  NavigationElement.SettingsElement ->
+                    NavigationSettingsItem(
+                      transition = transition,
+                      active = active.value,
+                      modifier = Modifier
+                        .clickable {
+                          viewModel.backStack.update {
+                            it.plus(MeteroidRoute.Settings)
+                          }
+                          onClose()
+                        },
+                    )
+                }
+              }
+              if (entry is NavigationElement.UserListElement || entry is NavigationElement.ServerListElement) {
+                transition.AnimatedVisibility(
+                  visible = { it },
+                  enter = expandVertically() + fadeIn(),
+                  exit = shrinkVertically() + fadeOut(),
+                ) {
+                  HorizontalDivider()
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt
new file mode 100644
index 0000000..78d873e
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt
@@ -0,0 +1,135 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 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.Transition
+import androidx.compose.animation.core.animateDp
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.TileMode
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.rememberGraphicsLayer
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.viewmodel.NavigationElement
+import de.chaosdorf.meteroid.viewmodel.NavigationViewModel
+
+val NavigationScrim: Brush
+  @Composable
+  @Stable
+  get() = Brush.linearGradient(
+    listOf(
+      MaterialTheme.colorScheme.background,
+      MaterialTheme.colorScheme.background.copy(alpha = 0f),
+    ),
+    start = Offset(0.0f, 0.0f),
+    end = Offset(0.0f, Float.POSITIVE_INFINITY),
+    tileMode = TileMode.Companion.Clamp,
+  )
+
+@Composable
+fun PersistentNavigation(
+  viewModel: NavigationViewModel,
+  transition: Transition<Boolean>,
+  onOpen: () -> Unit = {},
+) {
+  val backStack = viewModel.backStack.collectAsState()
+  val entries = viewModel.entries.collectAsState()
+  val currentEntry = remember {
+    derivedStateOf {
+      entries.value.firstOrNull { it.isCurrent(backStack.value.lastOrNull()) }
+    }
+  }
+
+  val shadowElevation = transition.animateDp { if (it) 4.dp else 0.dp }
+  val persistentShadowElevation = remember {
+    derivedStateOf {
+      4.dp - shadowElevation.value
+    }
+  }
+
+  val navigationScrim = NavigationScrim
+
+  Box(
+    Modifier
+      .drawWithCache {
+        onDrawBehind {
+          drawRect(navigationScrim)
+        }
+      }
+  ) {
+    Surface(
+      modifier = Modifier
+          .windowInsetsPadding(WindowInsets.Companion.statusBars)
+          .padding(8.dp)
+          .graphicsLayer {
+              this.shadowElevation = persistentShadowElevation.value.toPx()
+              shape = NavigationShape
+              clip = false
+          },
+      shape = NavigationShape,
+      tonalElevation = 4.dp,
+    ) {
+      when (val route = currentEntry.value) {
+        NavigationElement.ServerListElement -> NavigationServerListItem(
+          modifier = Modifier.clickable(onClick = onOpen),
+        )
+
+        is NavigationElement.ServerElement -> NavigationServerItem(
+          route.server,
+          modifier = Modifier.clickable(onClick = onOpen),
+        )
+
+        is NavigationElement.UserElement -> NavigationUserItem(
+          user = route.user,
+          active = true,
+          pinned = route.pinned,
+          modifier = Modifier.clickable(onClick = onOpen),
+          onPin = viewModel::togglePin,
+        )
+
+        is NavigationElement.UserListElement -> NavigationUserListItem(
+          modifier = Modifier.clickable(onClick = onOpen),
+        )
+
+        NavigationElement.AddServerElement -> NavigationAddServerItem(
+          modifier = Modifier.clickable(onClick = onOpen),
+        )
+
+        NavigationElement.SettingsElement -> NavigationSettingsItem(
+          modifier = Modifier.clickable(onClick = onOpen),
+        )
+
+        null -> Unit
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/TopNavigation.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/TopNavigation.kt
new file mode 100644
index 0000000..2983094
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/TopNavigation.kt
@@ -0,0 +1,70 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 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.annotation.SuppressLint
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.*
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.viewmodel.NavigationViewModel
+
+internal val NavigationShape = RoundedCornerShape(26.dp)
+
+@SuppressLint("UnusedTransitionTargetStateParameter")
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+fun TopNavigation(
+  viewModel: NavigationViewModel,
+) {
+  val backStack by viewModel.backStack.collectAsState()
+  val entries by viewModel.entries.collectAsState()
+  val currentEntry by remember {
+    derivedStateOf {
+      entries.firstOrNull { it.isCurrent(backStack.lastOrNull()) }
+    }
+  }
+
+  var open by remember { mutableStateOf(false) }
+  val transition = updateTransition(open, label = "transition")
+
+  if (currentEntry != null) {
+    PersistentNavigation(
+      viewModel = viewModel,
+      transition = transition,
+      onOpen = { open = true },
+    )
+
+    if (open || transition.currentState || transition.isRunning) {
+      OverlayNavigation(
+        viewModel = viewModel,
+        transition = transition,
+        onClose = { open = false },
+      )
+    }
+  }
+}
+
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt
index 00adcbd..2d0f9c7 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt
@@ -104,7 +104,7 @@ fun PurchaseDrinkTile(
             .padding(8.dp)
     ) {
         Box(
-            Modifier.Companion
+            Modifier
                 .aspectRatio(1.0f)
                 .background(
                     brush = MaterialTheme.colorScheme.secondaryGradient.verticalGradient(),
@@ -114,7 +114,7 @@ fun PurchaseDrinkTile(
             contentAlignment = Alignment.Companion.Center
         ) {
             Box(
-                modifier = Modifier.Companion
+                modifier = Modifier
                     .graphicsLayer {
                         clip = true
                         alpha = if (pendingPurchases) 0.0f else contentAlpha
@@ -124,12 +124,12 @@ fun PurchaseDrinkTile(
                     drinkPainter,
                     contentDescription = null,
                     contentScale = ContentScale.Companion.Fit,
-                    modifier = Modifier.Companion
+                    modifier = Modifier
                         .clip(CircleShape)
                 )
                 PriceBadge(
                     item.price,
-                    modifier = Modifier.Companion
+                    modifier = Modifier
                         .align(Alignment.Companion.BottomEnd)
                         .paddingFromBaseline(bottom = 12.dp)
                 )
@@ -140,17 +140,17 @@ fun PurchaseDrinkTile(
                 fontWeight = FontWeight.Companion.Light,
                 color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f),
                 textAlign = TextAlign.Companion.Center,
-                modifier = Modifier.Companion
+                modifier = Modifier
                     .graphicsLayer {
                         clip = true
                         alpha = if (pendingPurchases) contentAlpha else 0.0f
                     }
             )
         }
-        Spacer(Modifier.Companion.height(4.dp))
+        Spacer(Modifier.height(4.dp))
         Text(
             item.name,
-            modifier = Modifier.Companion
+            modifier = Modifier
                 .fillMaxWidth()
                 .padding(horizontal = 8.dp),
             textAlign = TextAlign.Companion.Center,
@@ -158,8 +158,8 @@ fun PurchaseDrinkTile(
             style = MaterialTheme.typography.labelLarge,
             color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha)
         )
-        Spacer(Modifier.Companion.height(4.dp))
-        Row(modifier = Modifier.Companion.align(Alignment.Companion.CenterHorizontally)) {
+        Spacer(Modifier.height(4.dp))
+        Row(modifier = Modifier.align(Alignment.Companion.CenterHorizontally)) {
             val label = remember(item) {
                 val unitPrice =
                     if (item.volume <= BigDecimal.ZERO) null
@@ -176,7 +176,7 @@ fun PurchaseDrinkTile(
 
             Text(
                 label,
-                modifier = Modifier.Companion
+                modifier = Modifier
                     .fillMaxWidth()
                     .padding(horizontal = 8.dp),
                 textAlign = TextAlign.Companion.Center,
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt
index 609d776..60feb16 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt
@@ -52,7 +52,7 @@ fun PurchaseFilterChip(
                 Icon(
                     Icons.Default.Check,
                     contentDescription = null,
-                    modifier = Modifier.Companion.size(18.dp)
+                    modifier = Modifier.size(18.dp)
                 )
             }
         },
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt
index 309572a..3a1a04b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt
@@ -40,7 +40,7 @@ fun PurchaseFilterRow(
     toggleFilter: (filter: PurchaseViewModel.Filter) -> Unit = {},
 ) {
     FlowRow(
-        modifier = Modifier.Companion.padding(horizontal = 12.dp),
+        modifier = Modifier.padding(horizontal = 12.dp),
         horizontalArrangement = Arrangement.spacedBy(8.dp)
     ) {
         PurchaseFilterChip(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt
index be3cb02..9fa20a8 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt
@@ -36,6 +36,7 @@ import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import de.chaosdorf.meteroid.ui.MeteroidRoute
+import de.chaosdorf.meteroid.ui.wrapped.WrappedBanner
 import de.chaosdorf.meteroid.viewmodel.Navigator
 import de.chaosdorf.meteroid.viewmodel.PurchaseViewModel
 import kotlinx.coroutines.flow.update
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedBanner.kt
similarity index 89%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedBanner.kt
index b4784d2..511ef23 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedBanner.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchase
+package de.chaosdorf.meteroid.ui.wrapped
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -54,14 +54,14 @@ fun WrappedBanner(
         color = MaterialTheme.colorScheme.primaryContainer,
         contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted,
         shape = RoundedCornerShape(8.dp),
-        modifier = Modifier.Companion.padding(vertical = 8.dp, horizontal = 12.dp)
+        modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp)
     ) {
         Row(
             horizontalArrangement = Arrangement.SpaceBetween,
             verticalAlignment = Alignment.Companion.CenterVertically,
-            modifier = Modifier.Companion.padding(vertical = 8.dp)
+            modifier = Modifier.padding(vertical = 8.dp)
         ) {
-            Spacer(Modifier.Companion.width(12.dp))
+            Spacer(Modifier.width(12.dp))
             Column(verticalArrangement = Arrangement.Center) {
                 val now = Clock.System.now().toLocalDateTime(TimeZone.Companion.UTC)
                 Text(
@@ -73,11 +73,11 @@ fun WrappedBanner(
                     style = MaterialTheme.typography.bodyMedium,
                 )
             }
-            Spacer(Modifier.Companion.width(8.dp))
+            Spacer(Modifier.width(8.dp))
             Button(onClick = onClick) {
                 Text("Let's go")
             }
-            Spacer(Modifier.Companion.width(4.dp))
+            Spacer(Modifier.width(4.dp))
         }
     }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt
index 2a4e7b3..d1184ad 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt
@@ -147,9 +147,9 @@ class NavigationViewModel @AssistedInject constructor(
     }
   }
 
-  fun togglePin(serverId: ServerId, userId: UserId) {
+  fun togglePin(user: User) {
     viewModelScope.launch {
-      accountProvider.togglePin(serverId, userId)
+      accountProvider.togglePin(user.serverId, user.userId)
     }
   }
 }
-- 
GitLab