refactor(android): extract MinstrelTopAppBar (DRY 3b)

Ten screens duplicated the same TopAppBar + Text(title) +
optional-back-arrow + MainAppBarActions sandwich. Extract into
MinstrelTopAppBar(title, navController, currentRouteName, onBack,
actions) at shared/widgets/. Library's tab row still composes
above it inside a Column; Library's Shuffle button slots into the
extra actions trailing-lambda; drill-down screens pass onBack for
the back arrow; root tabs leave onBack null.

Net: -48 lines across 10 screens, +50 lines for the helper.

SearchScreen keeps its own TopAppBar (title is a SearchField
composable, not a string).

Per-screen TrackActionsViewModel snackbar wiring (Library/Search/
detail screens) was intentionally NOT consolidated here: each
hiltViewModel() returns a different VM instance than the shell-
scoped one already in ShellScaffold, so dedup belongs in 3c
(shell-scope the VM via CompositionLocal) rather than freezing the
current duplicate into a helper.
This commit is contained in:
2026-05-30 16:00:21 -04:00
parent 2b17a4ed69
commit 02a6c45958
11 changed files with 106 additions and 154 deletions
@@ -11,13 +11,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -29,7 +26,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Inbox import com.composables.icons.lucide.Inbox
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.TriangleAlert import com.composables.icons.lucide.TriangleAlert
@@ -44,7 +40,7 @@ import com.fabledsword.minstrel.nav.AdminRequests
import com.fabledsword.minstrel.nav.AdminUsers import com.fabledsword.minstrel.nav.AdminUsers
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -104,7 +100,6 @@ class AdminLandingViewModel @Inject constructor(
// ─── Screen ────────────────────────────────────────────────────────── // ─── Screen ──────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AdminLandingScreen( fun AdminLandingScreen(
navController: NavHostController, navController: NavHostController,
@@ -114,19 +109,11 @@ fun AdminLandingScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Admin") }, title = "Admin",
navigationIcon = { navController = navController,
IconButton(onClick = { navController.popBackStack() }) { currentRouteName = Admin::class.qualifiedName,
Icon(Lucide.ArrowLeft, contentDescription = "Back") onBack = { navController.popBackStack() },
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = Admin::class.qualifiedName,
)
},
) )
}, },
) { inner -> ) { inner ->
@@ -12,14 +12,11 @@ import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -29,13 +26,11 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Lucide
import com.fabledsword.minstrel.models.AdminQuarantineItemRef import com.fabledsword.minstrel.models.AdminQuarantineItemRef
import com.fabledsword.minstrel.nav.AdminQuarantine import com.fabledsword.minstrel.nav.AdminQuarantine
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -48,19 +43,11 @@ fun AdminQuarantineScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Admin · Quarantine") }, title = "Admin · Quarantine",
navigationIcon = { navController = navController,
IconButton(onClick = { navController.popBackStack() }) { currentRouteName = AdminQuarantine::class.qualifiedName,
Icon(Lucide.ArrowLeft, contentDescription = "Back") onBack = { navController.popBackStack() },
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = AdminQuarantine::class.qualifiedName,
)
},
) )
}, },
) { inner -> ) { inner ->
@@ -12,13 +12,10 @@ import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -28,13 +25,11 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Lucide
import com.fabledsword.minstrel.models.RequestRef import com.fabledsword.minstrel.models.RequestRef
import com.fabledsword.minstrel.nav.AdminRequests import com.fabledsword.minstrel.nav.AdminRequests
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -47,19 +42,11 @@ fun AdminRequestsScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Admin · Requests") }, title = "Admin · Requests",
navigationIcon = { navController = navController,
IconButton(onClick = { navController.popBackStack() }) { currentRouteName = AdminRequests::class.qualifiedName,
Icon(Lucide.ArrowLeft, contentDescription = "Back") onBack = { navController.popBackStack() },
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = AdminRequests::class.qualifiedName,
)
},
) )
}, },
) { inner -> ) { inner ->
@@ -14,12 +14,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Copy import com.composables.icons.lucide.Copy
import com.composables.icons.lucide.KeyRound import com.composables.icons.lucide.KeyRound
import com.composables.icons.lucide.Plus import com.composables.icons.lucide.Plus
import com.composables.icons.lucide.Trash2 import com.composables.icons.lucide.Trash2
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -30,7 +28,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import android.content.ClipData import android.content.ClipData
import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
@@ -52,11 +49,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.fabledsword.minstrel.models.AdminUserRef import com.fabledsword.minstrel.models.AdminUserRef
import com.fabledsword.minstrel.nav.AdminUsers import com.fabledsword.minstrel.nav.AdminUsers
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AdminUsersScreen( fun AdminUsersScreen(
navController: NavHostController, navController: NavHostController,
@@ -117,7 +113,6 @@ fun AdminUsersScreen(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun AdminUsersScaffold( private fun AdminUsersScaffold(
navController: NavHostController, navController: NavHostController,
@@ -134,19 +129,11 @@ private fun AdminUsersScaffold(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Admin · Users") }, title = "Admin · Users",
navigationIcon = { navController = navController,
IconButton(onClick = { navController.popBackStack() }) { currentRouteName = AdminUsers::class.qualifiedName,
Icon(Lucide.ArrowLeft, contentDescription = "Back") onBack = { navController.popBackStack() },
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = AdminUsers::class.qualifiedName,
)
},
) )
}, },
) { inner -> ) { inner ->
@@ -23,7 +23,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -35,7 +34,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Search as LucideSearch import com.composables.icons.lucide.Search as LucideSearch
import com.fabledsword.minstrel.discover.data.RequestOutcome import com.fabledsword.minstrel.discover.data.RequestOutcome
@@ -44,7 +42,7 @@ import com.fabledsword.minstrel.models.LidarrRequestKind
import com.fabledsword.minstrel.models.LidarrSearchResultRef import com.fabledsword.minstrel.models.LidarrSearchResultRef
import com.fabledsword.minstrel.nav.Discover import com.fabledsword.minstrel.nav.Discover
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -63,19 +61,11 @@ fun DiscoverScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Discover") }, title = "Discover",
navigationIcon = { navController = navController,
IconButton(onClick = { navController.popBackStack() }) { currentRouteName = Discover::class.qualifiedName,
Icon(Lucide.ArrowLeft, contentDescription = "Back") onBack = { navController.popBackStack() },
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = Discover::class.qualifiedName,
)
},
) )
}, },
snackbarHost = { SnackbarHost(snackbar) }, snackbarHost = { SnackbarHost(snackbar) },
@@ -21,14 +21,12 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -70,7 +68,7 @@ import com.fabledsword.minstrel.playlists.widgets.PlaylistPlaceholderCard
import com.fabledsword.minstrel.shared.UiState import com.fabledsword.minstrel.shared.UiState
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.SkeletonAlbumTile import com.fabledsword.minstrel.shared.widgets.SkeletonAlbumTile
import com.fabledsword.minstrel.shared.widgets.SkeletonArtistTile import com.fabledsword.minstrel.shared.widgets.SkeletonArtistTile
@@ -236,7 +234,6 @@ class HomeViewModel @Inject constructor(
// ─── Screen ────────────────────────────────────────────────────────── // ─── Screen ──────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeScreen( fun HomeScreen(
navController: NavHostController, navController: NavHostController,
@@ -249,14 +246,10 @@ fun HomeScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Minstrel") }, title = "Minstrel",
actions = { navController = navController,
MainAppBarActions( currentRouteName = Home::class.qualifiedName,
navController = navController,
currentRouteName = Home::class.qualifiedName,
)
},
) )
}, },
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
@@ -22,7 +22,6 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -49,7 +48,7 @@ import com.composables.icons.lucide.Shuffle
import com.fabledsword.minstrel.shared.UiState import com.fabledsword.minstrel.shared.UiState
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.ErrorRetry import com.fabledsword.minstrel.shared.widgets.ErrorRetry
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.SkeletonAlbumTile import com.fabledsword.minstrel.shared.widgets.SkeletonAlbumTile
import com.fabledsword.minstrel.shared.widgets.SkeletonArtistTile import com.fabledsword.minstrel.shared.widgets.SkeletonArtistTile
@@ -88,8 +87,10 @@ fun LibraryScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
Column { Column {
TopAppBar( MinstrelTopAppBar(
title = { Text("Library") }, title = "Library",
navController = navController,
currentRouteName = Library::class.qualifiedName,
actions = { actions = {
IconButton(onClick = viewModel::shuffleAll) { IconButton(onClick = viewModel::shuffleAll) {
Icon( Icon(
@@ -98,10 +99,6 @@ fun LibraryScreen(
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
MainAppBarActions(
navController = navController,
currentRouteName = Library::class.qualifiedName,
)
}, },
) )
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
@@ -9,11 +9,9 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -33,7 +31,7 @@ import com.fabledsword.minstrel.shared.UiState
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -95,7 +93,6 @@ class PlaylistsListViewModel @Inject constructor(
// ─── Screen ────────────────────────────────────────────────────────── // ─── Screen ──────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PlaylistsListScreen( fun PlaylistsListScreen(
navController: NavHostController, navController: NavHostController,
@@ -104,14 +101,10 @@ fun PlaylistsListScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Playlists") }, title = "Playlists",
actions = { navController = navController,
MainAppBarActions( currentRouteName = Playlists::class.qualifiedName,
navController = navController,
currentRouteName = Playlists::class.qualifiedName,
)
},
) )
}, },
) { inner -> ) { inner ->
@@ -14,16 +14,13 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -36,7 +33,6 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Disc3 import com.composables.icons.lucide.Disc3
import com.composables.icons.lucide.LibraryBig import com.composables.icons.lucide.LibraryBig
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
@@ -51,10 +47,9 @@ import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.Requests import com.fabledsword.minstrel.nav.Requests
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RequestsScreen( fun RequestsScreen(
navController: NavHostController, navController: NavHostController,
@@ -64,19 +59,11 @@ fun RequestsScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Your requests") }, title = "Your requests",
navigationIcon = { navController = navController,
IconButton(onClick = { navController.popBackStack() }) { currentRouteName = Requests::class.qualifiedName,
Icon(Lucide.ArrowLeft, contentDescription = "Back") onBack = { navController.popBackStack() },
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = Requests::class.qualifiedName,
)
},
) )
}, },
) { inner -> ) { inner ->
@@ -30,7 +30,6 @@ import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -55,11 +54,10 @@ import com.fabledsword.minstrel.nav.Admin
import com.fabledsword.minstrel.nav.Requests import com.fabledsword.minstrel.nav.Requests
import com.fabledsword.minstrel.nav.Settings as SettingsRoute import com.fabledsword.minstrel.nav.Settings as SettingsRoute
import com.fabledsword.minstrel.nav.ServerUrl import com.fabledsword.minstrel.nav.ServerUrl
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
import com.fabledsword.minstrel.theme.ThemeMode import com.fabledsword.minstrel.theme.ThemeMode
import com.fabledsword.minstrel.theme.ThemePreferenceViewModel import com.fabledsword.minstrel.theme.ThemePreferenceViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
navController: NavHostController, navController: NavHostController,
@@ -82,14 +80,10 @@ fun SettingsScreen(
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( MinstrelTopAppBar(
title = { Text("Settings") }, title = "Settings",
actions = { navController = navController,
MainAppBarActions( currentRouteName = SettingsRoute::class.qualifiedName,
navController = navController,
currentRouteName = SettingsRoute::class.qualifiedName,
)
},
) )
}, },
) { inner -> ) { inner ->
@@ -0,0 +1,50 @@
package com.fabledsword.minstrel.shared.widgets
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Lucide
/**
* The standard Minstrel top app bar — a `Text(title)` on the left, the
* universal [MainAppBarActions] row on the right, optionally preceded
* by extra screen-specific actions and a back arrow.
*
* Replaces the TopAppBar + title + navigationIcon + MainAppBarActions
* sandwich that was duplicated across ~10 screens. Library wraps this
* in a `Column` with its tab row; drill-down screens pass [onBack]
* for the back arrow; Library passes its Shuffle button via [actions].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MinstrelTopAppBar(
title: String,
navController: NavHostController,
currentRouteName: String?,
onBack: (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
) {
TopAppBar(
title = { Text(title) },
navigationIcon = {
if (onBack != null) {
IconButton(onClick = onBack) {
Icon(Lucide.ArrowLeft, contentDescription = "Back")
}
}
},
actions = {
actions()
MainAppBarActions(
navController = navController,
currentRouteName = currentRouteName,
)
},
)
}