fix: ABS skip library list + nav label overflow + NowPlaying top bar + dominant color bg
- ABS: new AbsBooksScreen loads books directly on tab open; library pills shown only when >1 library — no more two-step library→books navigation - Nav: shorten "Hörbücher"→"Bücher", "Einstellungen"→"Einst.", reduce label font to 9sp with maxLines=1 to prevent label wrapping - NowPlaying: increase top padding so the tab bar sits lower on screen (statusBarTop + 16dp, min 52dp) - NowPlaying: replace blurred-cover background with dominant-color gradient extracted via Palette from cover art; animates on track change; falls back to dark color Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
820a75f40b
commit
72feac41bb
4 files changed
+145
-32
No files matched your search
@@ -93,6 +93,9 @@ dependencies {
|
||||
// Coil
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
// Palette (dominant cover color extraction in NowPlaying)
|
||||
implementation("androidx.palette:palette-ktx:1.0.0")
|
||||
|
||||
// Google Fonts (Outfit)
|
||||
implementation(libs.compose.ui.google.fonts)
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.*
|
||||
import androidx.navigation.compose.*
|
||||
import com.novasonic.app.api.AbsClient
|
||||
import com.novasonic.app.api.AbsLibrary
|
||||
import com.novasonic.app.api.AbsLibraryItem
|
||||
import com.novasonic.app.api.model.Song
|
||||
import com.novasonic.app.player.AbsPlayerController
|
||||
@@ -50,8 +49,8 @@ private enum class TopLevel(val route: String, val icon: ImageVector, val label:
|
||||
LIBRARY("library", Icons.Default.LibraryMusic, "Bibliothek"),
|
||||
ERKUNDEN("erkunden", Icons.Default.Explore, "Erkunden"),
|
||||
STARRED("starred", Icons.Default.Favorite, "Favoriten"),
|
||||
ABS("abs", Icons.Default.Book, "Hörbücher"),
|
||||
SETTINGS("settings", Icons.Default.Settings, "Einstellungen"),
|
||||
ABS("abs", Icons.Default.Book, "Bücher"),
|
||||
SETTINGS("settings", Icons.Default.Settings, "Einst."),
|
||||
}
|
||||
|
||||
private val NavBg = Color(0xF7080814)
|
||||
@@ -96,7 +95,6 @@ fun AppNavigation() {
|
||||
var playlistPickerSong by remember { mutableStateOf<Song?>(null) }
|
||||
var infoSheetSong by remember { mutableStateOf<Song?>(null) }
|
||||
var showNotifications by remember { mutableStateOf(false) }
|
||||
var selectedAbsLibrary by remember { mutableStateOf<AbsLibrary?>(null) }
|
||||
var selectedAbsItem by remember { mutableStateOf<AbsLibraryItem?>(null) }
|
||||
var showAbsPlayer by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -125,14 +123,6 @@ fun AppNavigation() {
|
||||
backDispatcher.addCallback(cb)
|
||||
onDispose { cb.remove() }
|
||||
}
|
||||
DisposableEffect(selectedAbsLibrary, backDispatcher) {
|
||||
if (selectedAbsLibrary == null || backDispatcher == null) return@DisposableEffect onDispose {}
|
||||
val cb = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() { selectedAbsLibrary = null }
|
||||
}
|
||||
backDispatcher.addCallback(cb)
|
||||
onDispose { cb.remove() }
|
||||
}
|
||||
|
||||
fun coverUrl(coverId: String?) = repo.coverArtUrl(coverId, 600)
|
||||
fun heroCoverUrl(coverId: String?) = repo.coverArtUrl(coverId, 1000)
|
||||
@@ -210,7 +200,7 @@ fun AppNavigation() {
|
||||
}
|
||||
},
|
||||
icon = { Icon(dest.icon, contentDescription = dest.label) },
|
||||
label = { Text(dest.label, style = TextStyle(fontFamily = Outfit, fontWeight = FontWeight.SemiBold, fontSize = 10.sp)) },
|
||||
label = { Text(dest.label, style = TextStyle(fontFamily = Outfit, fontWeight = FontWeight.SemiBold, fontSize = 9.sp), maxLines = 1) },
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = NsAccent,
|
||||
selectedTextColor = NsAccent,
|
||||
@@ -288,27 +278,23 @@ fun AppNavigation() {
|
||||
}
|
||||
composable(TopLevel.ABS.route) {
|
||||
val client = absClient
|
||||
when {
|
||||
client == null -> {}
|
||||
selectedAbsItem != null -> AbsBookDetailScreen(
|
||||
if (client != null) {
|
||||
if (selectedAbsItem != null) {
|
||||
AbsBookDetailScreen(
|
||||
absClient = client,
|
||||
libraryItem = selectedAbsItem!!,
|
||||
absPlayerController = absPlayerController,
|
||||
onPlay = { showAbsPlayer = true },
|
||||
onBack = { selectedAbsItem = null },
|
||||
)
|
||||
selectedAbsLibrary != null -> AbsLibraryScreen(
|
||||
} else {
|
||||
AbsBooksScreen(
|
||||
absClient = client,
|
||||
library = selectedAbsLibrary!!,
|
||||
onItemClick = { selectedAbsItem = it },
|
||||
onBack = { selectedAbsLibrary = null },
|
||||
)
|
||||
else -> AbsScreen(
|
||||
absClient = client,
|
||||
onLibraryClick = { selectedAbsLibrary = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
composable(TopLevel.SETTINGS.route) { SettingsScreen() }
|
||||
|
||||
composable(
|
||||
|
||||
@@ -9,11 +9,13 @@ import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -92,6 +94,97 @@ fun AbsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Combined books screen (skips library list, auto-loads first library) ─────
|
||||
|
||||
@Composable
|
||||
fun AbsBooksScreen(
|
||||
absClient: AbsClient,
|
||||
onItemClick: (AbsLibraryItem) -> Unit,
|
||||
) {
|
||||
var libraries by remember { mutableStateOf<List<AbsLibrary>>(emptyList()) }
|
||||
var selectedLibIdx by remember { mutableIntStateOf(0) }
|
||||
var items by remember { mutableStateOf<List<AbsLibraryItem>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// Initial load: fetch libraries, then immediately load first library's items
|
||||
LaunchedEffect(Unit) {
|
||||
isLoading = true
|
||||
val libs = runCatching { absClient.getLibraries() }.getOrDefault(emptyList())
|
||||
libraries = libs
|
||||
val first = libs.firstOrNull()
|
||||
if (first != null) {
|
||||
val r = runCatching { absClient.getLibraryItems(first.id).results }
|
||||
items = r.getOrDefault(emptyList())
|
||||
error = r.exceptionOrNull()?.message
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
// Reload when user switches library tab (only after initial load)
|
||||
LaunchedEffect(selectedLibIdx) {
|
||||
val lib = libraries.getOrNull(selectedLibIdx) ?: return@LaunchedEffect
|
||||
isLoading = true
|
||||
error = null
|
||||
val r = runCatching { absClient.getLibraryItems(lib.id).results }
|
||||
items = r.getOrDefault(emptyList())
|
||||
error = r.exceptionOrNull()?.message
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 16.dp)) {
|
||||
Text(
|
||||
"Hörbücher",
|
||||
fontFamily = Outfit, fontWeight = FontWeight.Bold, fontSize = 24.sp, color = NsOnSurface,
|
||||
)
|
||||
|
||||
// Library switcher — only shown when more than one library exists
|
||||
if (libraries.size > 1) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
libraries.forEachIndexed { idx, lib ->
|
||||
val sel = idx == selectedLibIdx
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(if (sel) NsAccent else NsSurface)
|
||||
.clickable { selectedLibIdx = idx }
|
||||
.padding(horizontal = 14.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
lib.name, fontFamily = Outfit, fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp, color = if (sel) NsBackground else NsOnSurface2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
when {
|
||||
isLoading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = NsAccent)
|
||||
}
|
||||
error != null -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(error ?: "Fehler", fontFamily = Outfit, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
items.isEmpty() -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Keine Bücher gefunden", fontFamily = Outfit, color = NsOnSurface2)
|
||||
}
|
||||
else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(items) { item ->
|
||||
AbsItemRow(item = item, absClient = absClient, onClick = { onItemClick(item) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibraryRow(library: AbsLibrary, onClick: () -> Unit) {
|
||||
Row(
|
||||
|
||||
@@ -33,9 +33,15 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import androidx.palette.graphics.Palette
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -70,7 +76,7 @@ fun NowPlayingScreen(
|
||||
var tab by remember { mutableStateOf(NpTab.PLAYER) }
|
||||
|
||||
val statusBarTop: Dp = with(LocalDensity.current) { WindowInsets.safeDrawing.getTop(this).toDp() }
|
||||
val topPad = maxOf(statusBarTop, 36.dp)
|
||||
val topPad = maxOf(statusBarTop + 16.dp, 52.dp)
|
||||
val navBarBottom: Dp = with(LocalDensity.current) { WindowInsets.safeDrawing.getBottom(this).toDp() }
|
||||
|
||||
// Apple Music-style: cover scales up while playing
|
||||
@@ -83,20 +89,45 @@ fun NowPlayingScreen(
|
||||
label = "coverScale",
|
||||
)
|
||||
|
||||
// Extract dominant color from cover art for background tinting
|
||||
val context = LocalContext.current
|
||||
Box(Modifier.fillMaxSize().background(NsBackground)) {
|
||||
// Blurred album art full-screen background — crossfade prevents transparent flash on song change
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context).data(coverUrl).crossfade(500).build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize().blur(72.dp),
|
||||
var dominantColor by remember(coverUrl) { mutableStateOf<Color?>(null) }
|
||||
LaunchedEffect(coverUrl) {
|
||||
dominantColor = null
|
||||
if (coverUrl.isNullOrBlank()) return@LaunchedEffect
|
||||
try {
|
||||
val loader = ImageLoader(context)
|
||||
val req = ImageRequest.Builder(context).data(coverUrl).allowHardware(false).size(80).build()
|
||||
val result = loader.execute(req)
|
||||
if (result is SuccessResult) {
|
||||
val bitmap = (result.drawable as? BitmapDrawable)?.bitmap ?: return@LaunchedEffect
|
||||
val palette = withContext(Dispatchers.Default) { Palette.from(bitmap).generate() }
|
||||
val swatch = palette.darkMutedSwatch ?: palette.mutedSwatch ?: palette.dominantSwatch
|
||||
swatch?.rgb?.let { dominantColor = Color(it) }
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
val rawBgColor = dominantColor ?: Color(0xFF0D0D1A)
|
||||
val animatedBgColor by animateColorAsState(
|
||||
targetValue = rawBgColor,
|
||||
animationSpec = tween(600),
|
||||
label = "npBg",
|
||||
)
|
||||
|
||||
Box(Modifier.fillMaxSize().background(NsBackground)) {
|
||||
// Dominant-color gradient background
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(
|
||||
Brush.verticalGradient(0f to Color(0xCC000000), 1f to Color(0xF2000000))
|
||||
Brush.verticalGradient(
|
||||
0f to animatedBgColor,
|
||||
0.55f to animatedBgColor.copy(alpha = 0.65f),
|
||||
1f to NsBackground,
|
||||
)
|
||||
)
|
||||
)
|
||||
// Dark scrim for readability
|
||||
Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.28f)))
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
Reference in new issue
Block a user