refactor(server): remove bootstrap admin path #34
@@ -4,15 +4,34 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.fabledsword.minstrel.library.ui.LibraryScreen
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.composables.icons.lucide.House
|
||||||
|
import com.composables.icons.lucide.LibraryBig
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composables.icons.lucide.Search
|
||||||
|
import com.composables.icons.lucide.Settings
|
||||||
|
import com.fabledsword.minstrel.nav.Home
|
||||||
|
import com.fabledsword.minstrel.nav.Library
|
||||||
|
import com.fabledsword.minstrel.nav.MinstrelNavGraph
|
||||||
|
import com.fabledsword.minstrel.nav.Search as SearchRoute
|
||||||
|
import com.fabledsword.minstrel.nav.Settings as SettingsRoute
|
||||||
import com.fabledsword.minstrel.theme.MinstrelTheme
|
import com.fabledsword.minstrel.theme.MinstrelTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -27,12 +46,44 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun App() {
|
private fun App() {
|
||||||
// Bottom-bar nav-graph wiring lands in Phase 5.5. For now render
|
val navController = rememberNavController()
|
||||||
// LibraryScreen directly so the new UI is reachable end-to-end.
|
Scaffold(
|
||||||
// onArtistClick / onAlbumClick are no-op until the nav graph lands.
|
modifier = Modifier.fillMaxSize(),
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { inner ->
|
bottomBar = { MinstrelBottomBar(navController) },
|
||||||
Box(modifier = Modifier.padding(inner)) {
|
) { inner ->
|
||||||
LibraryScreen()
|
MinstrelNavGraph(
|
||||||
|
navController = navController,
|
||||||
|
modifier = Modifier.padding(inner),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class BottomBarTab(
|
||||||
|
val label: String,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val routeClass: KClass<*>,
|
||||||
|
val navigate: NavHostController.() -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MinstrelBottomBar(navController: NavHostController) {
|
||||||
|
val tabs = listOf(
|
||||||
|
BottomBarTab("Home", Lucide.House, Home::class) { navigate(Home) },
|
||||||
|
BottomBarTab("Library", Lucide.LibraryBig, Library::class) { navigate(Library) },
|
||||||
|
BottomBarTab("Search", Lucide.Search, SearchRoute::class) { navigate(SearchRoute) },
|
||||||
|
BottomBarTab("Settings", Lucide.Settings, SettingsRoute::class) { navigate(SettingsRoute) },
|
||||||
|
)
|
||||||
|
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentDestination = backStackEntry?.destination
|
||||||
|
NavigationBar {
|
||||||
|
tabs.forEach { tab ->
|
||||||
|
val selected = currentDestination?.hasRoute(tab.routeClass) == true
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = { tab.navigate(navController) },
|
||||||
|
icon = { Icon(tab.icon, contentDescription = tab.label) },
|
||||||
|
label = { Text(tab.label) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.fabledsword.minstrel.nav
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import com.fabledsword.minstrel.library.ui.LibraryScreen
|
||||||
|
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single NavHost for the whole app. Top-level tabs (Home / Library /
|
||||||
|
* Search / Settings) are direct destinations; detail screens push on
|
||||||
|
* top with their argument types passed via the @Serializable route
|
||||||
|
* classes' fields.
|
||||||
|
*
|
||||||
|
* Stub composables call EmptyState — the real screens land in their
|
||||||
|
* respective feature phases.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MinstrelNavGraph(
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = Library,
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
// ── Top-level tabs ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
composable<Home> { ComingSoon(title = "Home", body = "Lands in Phase 6.") }
|
||||||
|
|
||||||
|
composable<Library> {
|
||||||
|
LibraryScreen(
|
||||||
|
onArtistClick = { artist -> navController.navigate(ArtistDetail(artist.id)) },
|
||||||
|
onAlbumClick = { album -> navController.navigate(AlbumDetail(album.id)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Search> { ComingSoon(title = "Search", body = "Lands in a later phase.") }
|
||||||
|
|
||||||
|
composable<Settings> { ComingSoon(title = "Settings", body = "Lands in Phase 11.") }
|
||||||
|
|
||||||
|
// ── Detail / overlay screens ──────────────────────────────────────
|
||||||
|
|
||||||
|
composable<AlbumDetail> { backStackEntry ->
|
||||||
|
val route: AlbumDetail = backStackEntry.toRoute()
|
||||||
|
ComingSoon(
|
||||||
|
title = "Album",
|
||||||
|
body = "Album detail for ${route.id} — lands in a later 5.x slice.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<ArtistDetail> { backStackEntry ->
|
||||||
|
val route: ArtistDetail = backStackEntry.toRoute()
|
||||||
|
ComingSoon(
|
||||||
|
title = "Artist",
|
||||||
|
body = "Artist detail for ${route.id} — lands in a later 5.x slice.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<PlaylistDetail> { backStackEntry ->
|
||||||
|
val route: PlaylistDetail = backStackEntry.toRoute()
|
||||||
|
ComingSoon(
|
||||||
|
title = "Playlist",
|
||||||
|
body = "Playlist detail for ${route.id} — lands in Phase 7.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<NowPlaying> { ComingSoon(title = "Now playing", body = "Lands in Phase 6.4.") }
|
||||||
|
composable<Queue> { ComingSoon(title = "Queue", body = "Lands in Phase 6.") }
|
||||||
|
composable<Login> { ComingSoon(title = "Sign in", body = "Lands in a later phase.") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ComingSoon(title: String, body: String) {
|
||||||
|
// Reuse EmptyState as a generic placeholder for unbuilt screens —
|
||||||
|
// keeps the design system intact rather than ad-hoc Text.
|
||||||
|
EmptyState(
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.fabledsword.minstrel.nav
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
// Top-level destinations (bottom-bar tabs).
|
||||||
|
|
||||||
|
@Serializable data object Home
|
||||||
|
@Serializable data object Library
|
||||||
|
@Serializable data object Search
|
||||||
|
@Serializable data object Settings
|
||||||
|
|
||||||
|
// Push-on-top destinations (no bottom-bar entry).
|
||||||
|
|
||||||
|
@Serializable data class AlbumDetail(val id: String)
|
||||||
|
@Serializable data class ArtistDetail(val id: String)
|
||||||
|
@Serializable data class PlaylistDetail(val id: String)
|
||||||
|
|
||||||
|
@Serializable data object NowPlaying
|
||||||
|
@Serializable data object Queue
|
||||||
|
@Serializable data object Login
|
||||||
Reference in New Issue
Block a user