chore: stage previously uncommitted screen changes (ArtistDetail, ItunesArtist, NowPlaying)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
85252e24df
commit
925c173c15
3 files changed
+258
-15
No files matched your search
@@ -10,16 +10,23 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.LazyRow
|
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.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items as gridItems
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import androidx.compose.material.icons.filled.Event
|
import androidx.compose.material.icons.filled.Event
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Shuffle
|
import androidx.compose.material.icons.filled.Shuffle
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -61,6 +68,13 @@ fun ArtistDetailScreen(
|
|||||||
BackHandler(enabled = state.partialAlbum != null) { vm.dismissPartialAlbum() }
|
BackHandler(enabled = state.partialAlbum != null) { vm.dismissPartialAlbum() }
|
||||||
BackHandler(enabled = state.subAlbumItem != null && state.partialAlbum == null) { vm.dismissSubAlbum() }
|
BackHandler(enabled = state.subAlbumItem != null && state.partialAlbum == null) { vm.dismissSubAlbum() }
|
||||||
|
|
||||||
|
var showAllAlbums by remember { mutableStateOf(false) }
|
||||||
|
var allAlbumsTitle by remember { mutableStateOf("") }
|
||||||
|
var allAlbumEntries by remember { mutableStateOf<List<AllAlbumEntry>>(emptyList()) }
|
||||||
|
BackHandler(enabled = showAllAlbums && state.partialAlbum == null && state.subAlbumItem == null) {
|
||||||
|
showAllAlbums = false
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.pendingShufflePlay) {
|
LaunchedEffect(state.pendingShufflePlay) {
|
||||||
state.pendingShufflePlay?.let { songs ->
|
state.pendingShufflePlay?.let { songs ->
|
||||||
onPlay(songs, 0)
|
onPlay(songs, 0)
|
||||||
@@ -149,6 +163,15 @@ fun ArtistDetailScreen(
|
|||||||
// Album categories (computed once iTunes data arrives)
|
// Album categories (computed once iTunes data arrives)
|
||||||
val cats = if (state.iAlbums.isNotEmpty()) categorizeAlbums(artist.album, state.iAlbums) else null
|
val cats = if (state.iAlbums.isNotEmpty()) categorizeAlbums(artist.album, state.iAlbums) else null
|
||||||
|
|
||||||
|
if (showAllAlbums) {
|
||||||
|
AllAlbumsOverlay(
|
||||||
|
title = allAlbumsTitle,
|
||||||
|
entries = allAlbumEntries,
|
||||||
|
onBack = { showAllAlbums = false },
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
// Back link
|
// Back link
|
||||||
item {
|
item {
|
||||||
@@ -280,7 +303,7 @@ fun ArtistDetailScreen(
|
|||||||
// Navidrome top songs
|
// Navidrome top songs
|
||||||
if (state.topSongs.isNotEmpty()) {
|
if (state.topSongs.isNotEmpty()) {
|
||||||
item { SectionLabel("Top Songs") }
|
item { SectionLabel("Top Songs") }
|
||||||
itemsIndexed(state.topSongs, key = { _, s -> "top_${s.id}" }) { idx, song ->
|
itemsIndexed(state.topSongs, key = { idx, s -> "top_${idx}_${s.id}" }) { idx, song ->
|
||||||
SongRow(
|
SongRow(
|
||||||
song = song,
|
song = song,
|
||||||
coverUrl = coverUrl(song.coverArt),
|
coverUrl = coverUrl(song.coverArt),
|
||||||
@@ -297,7 +320,7 @@ fun ArtistDetailScreen(
|
|||||||
// ── iTunes: Weltweit populär ──────────────────────────────────────────
|
// ── iTunes: Weltweit populär ──────────────────────────────────────────
|
||||||
if (state.iTracks.isNotEmpty()) {
|
if (state.iTracks.isNotEmpty()) {
|
||||||
item { ArtistSectionHeader("Weltweit populär") }
|
item { ArtistSectionHeader("Weltweit populär") }
|
||||||
itemsIndexed(state.iTracks, key = { _, t -> t.trackId ?: 0L }) { _, track ->
|
itemsIndexed(state.iTracks, key = { idx, t -> "itrack_${idx}_${t.trackId ?: 0L}" }) { _, track ->
|
||||||
ItunesArtistTrackRow(
|
ItunesArtistTrackRow(
|
||||||
track = track,
|
track = track,
|
||||||
isPreviewing = state.previewingId == track.trackId,
|
isPreviewing = state.previewingId == track.trackId,
|
||||||
@@ -324,7 +347,17 @@ fun ArtistDetailScreen(
|
|||||||
// ── Album sections (categorized once iTunes data is ready) ────────────
|
// ── Album sections (categorized once iTunes data is ready) ────────────
|
||||||
if (cats != null) {
|
if (cats != null) {
|
||||||
if (cats.downloaded.isNotEmpty()) {
|
if (cats.downloaded.isNotEmpty()) {
|
||||||
item { ArtistSectionHeader("In Library", cats.downloaded.size) }
|
item {
|
||||||
|
ArtistSectionHeader("In Library", cats.downloaded.size,
|
||||||
|
onShowAll = if (cats.downloaded.size > 4) ({
|
||||||
|
allAlbumsTitle = "In Library"
|
||||||
|
allAlbumEntries = cats.downloaded.map { (nd, _) ->
|
||||||
|
AllAlbumEntry(name = nd.name, year = nd.year?.toString(), artUrl = coverUrl(nd.coverArt), onClick = { onAlbumClick(nd) })
|
||||||
|
}
|
||||||
|
showAllAlbums = true
|
||||||
|
}) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
@@ -344,7 +377,24 @@ fun ArtistDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cats.partial.isNotEmpty()) {
|
if (cats.partial.isNotEmpty()) {
|
||||||
item { ArtistSectionHeader("Unvollständig", cats.partial.size) }
|
item {
|
||||||
|
ArtistSectionHeader("Unvollständig", cats.partial.size,
|
||||||
|
onShowAll = if (cats.partial.size > 4) ({
|
||||||
|
allAlbumsTitle = "Unvollständig"
|
||||||
|
allAlbumEntries = cats.partial.map { (nd, ia, counts) ->
|
||||||
|
AllAlbumEntry(
|
||||||
|
name = nd.name,
|
||||||
|
year = nd.year?.toString(),
|
||||||
|
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb") ?: coverUrl(nd.coverArt),
|
||||||
|
badge = "${counts.first}/${counts.second}",
|
||||||
|
onClick = { vm.loadPartialAlbum(nd, ia) },
|
||||||
|
onDownload = { vm.showDownloadSheet(ia) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showAllAlbums = true
|
||||||
|
}) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
@@ -367,7 +417,24 @@ fun ArtistDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cats.notDownloaded.isNotEmpty()) {
|
if (cats.notDownloaded.isNotEmpty()) {
|
||||||
item { ArtistSectionHeader("Nicht heruntergeladen", cats.notDownloaded.size) }
|
item {
|
||||||
|
ArtistSectionHeader("Nicht heruntergeladen", cats.notDownloaded.size,
|
||||||
|
onShowAll = if (cats.notDownloaded.size > 4) ({
|
||||||
|
allAlbumsTitle = "Nicht heruntergeladen"
|
||||||
|
allAlbumEntries = cats.notDownloaded.map { ia ->
|
||||||
|
AllAlbumEntry(
|
||||||
|
name = ia.collectionName ?: "—",
|
||||||
|
year = ia.releaseDate?.take(4),
|
||||||
|
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb"),
|
||||||
|
dimmed = true,
|
||||||
|
onClick = { vm.loadSubAlbum(ia) },
|
||||||
|
onDownload = { vm.showDownloadSheet(ia) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showAllAlbums = true
|
||||||
|
}) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
@@ -460,3 +527,102 @@ private fun SectionLabel(title: String) {
|
|||||||
modifier = Modifier.padding(start = 20.dp, top = 4.dp, bottom = 12.dp),
|
modifier = Modifier.padding(start = 20.dp, top = 4.dp, bottom = 12.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal data class AllAlbumEntry(
|
||||||
|
val name: String,
|
||||||
|
val year: String?,
|
||||||
|
val artUrl: String?,
|
||||||
|
val badge: String? = null,
|
||||||
|
val dimmed: Boolean = false,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
val onDownload: (() -> Unit)? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun AllAlbumsOverlay(
|
||||||
|
title: String,
|
||||||
|
entries: List<AllAlbumEntry>,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
val filtered = remember(query, entries) {
|
||||||
|
if (query.isBlank()) entries
|
||||||
|
else entries.filter { it.name.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(NsBackground),
|
||||||
|
) {
|
||||||
|
// Top bar
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(start = 4.dp, end = 12.dp, top = 12.dp, bottom = 4.dp),
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Zurück", tint = NsOnSurface2)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
fontFamily = Outfit, fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp, color = NsOnSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${filtered.size}",
|
||||||
|
fontFamily = Outfit, fontSize = 12.sp,
|
||||||
|
color = NsOnSurface2.copy(0.6f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search bar
|
||||||
|
OutlinedTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = { query = it },
|
||||||
|
placeholder = { Text("Suchen…", color = NsOnSurface2) },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Search, null, tint = NsOnSurface2) },
|
||||||
|
trailingIcon = if (query.isNotEmpty()) { {
|
||||||
|
IconButton(onClick = { query = "" }) {
|
||||||
|
Icon(Icons.Default.Clear, null, tint = NsOnSurface2)
|
||||||
|
}
|
||||||
|
} } else null,
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = NsAccent,
|
||||||
|
unfocusedBorderColor = NsOnSurface3,
|
||||||
|
cursorColor = NsAccent,
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
items(filtered.size, key = { idx -> "${idx}_${filtered[idx].name}${filtered[idx].year ?: ""}" }) { idx ->
|
||||||
|
val entry = filtered[idx]
|
||||||
|
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
|
AlbumThumb(
|
||||||
|
name = entry.name,
|
||||||
|
year = entry.year,
|
||||||
|
artUrl = entry.artUrl,
|
||||||
|
badge = entry.badge,
|
||||||
|
dimmed = entry.dimmed,
|
||||||
|
onClick = entry.onClick,
|
||||||
|
onDownload = entry.onDownload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,18 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.LazyRow
|
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.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items as gridItems
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.blur
|
import androidx.compose.ui.draw.blur
|
||||||
@@ -97,6 +102,19 @@ fun ItunesArtistScreen(
|
|||||||
categorizeAlbums(artistState.ndAlbums, artistState.iAlbums)
|
categorizeAlbums(artistState.ndAlbums, artistState.iAlbums)
|
||||||
else null
|
else null
|
||||||
|
|
||||||
|
var showAllAlbums by remember { mutableStateOf(false) }
|
||||||
|
var allAlbumsTitle by remember { mutableStateOf("") }
|
||||||
|
var allAlbumEntries by remember { mutableStateOf<List<AllAlbumEntry>>(emptyList()) }
|
||||||
|
|
||||||
|
if (showAllAlbums) {
|
||||||
|
AllAlbumsOverlay(
|
||||||
|
title = allAlbumsTitle,
|
||||||
|
entries = allAlbumEntries,
|
||||||
|
onBack = { showAllAlbums = false },
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Pick backdrop: first Navidrome album cover, else first iTunes album art
|
// Pick backdrop: first Navidrome album cover, else first iTunes album art
|
||||||
val backdropUrl = artistState.ndAlbums.firstOrNull()?.let { coverUrl(it.coverArt) }
|
val backdropUrl = artistState.ndAlbums.firstOrNull()?.let { coverUrl(it.coverArt) }
|
||||||
?: artistState.iAlbums.firstOrNull()?.artworkUrl100?.replace("100x100bb", "300x300bb")
|
?: artistState.iAlbums.firstOrNull()?.artworkUrl100?.replace("100x100bb", "300x300bb")
|
||||||
@@ -204,7 +222,17 @@ fun ItunesArtistScreen(
|
|||||||
// ── Downloaded albums ─────────────────────────────────────────────
|
// ── Downloaded albums ─────────────────────────────────────────────
|
||||||
cats?.let { c ->
|
cats?.let { c ->
|
||||||
if (c.downloaded.isNotEmpty()) {
|
if (c.downloaded.isNotEmpty()) {
|
||||||
item { ArtistSectionHeader("In Library", c.downloaded.size) }
|
item {
|
||||||
|
ArtistSectionHeader("In Library", c.downloaded.size,
|
||||||
|
onShowAll = if (c.downloaded.size > 4) ({
|
||||||
|
allAlbumsTitle = "In Library"
|
||||||
|
allAlbumEntries = c.downloaded.map { (nd, _) ->
|
||||||
|
AllAlbumEntry(name = nd.name, year = nd.year?.toString(), artUrl = coverUrl(nd.coverArt), onClick = { onAlbumClick(nd) })
|
||||||
|
}
|
||||||
|
showAllAlbums = true
|
||||||
|
}) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
@@ -225,7 +253,24 @@ fun ItunesArtistScreen(
|
|||||||
|
|
||||||
// ── Partial albums ────────────────────────────────────────────
|
// ── Partial albums ────────────────────────────────────────────
|
||||||
if (c.partial.isNotEmpty()) {
|
if (c.partial.isNotEmpty()) {
|
||||||
item { ArtistSectionHeader("Unvollständig", c.partial.size) }
|
item {
|
||||||
|
ArtistSectionHeader("Unvollständig", c.partial.size,
|
||||||
|
onShowAll = if (c.partial.size > 4) ({
|
||||||
|
allAlbumsTitle = "Unvollständig"
|
||||||
|
allAlbumEntries = c.partial.map { (nd, ia, counts) ->
|
||||||
|
AllAlbumEntry(
|
||||||
|
name = nd.name,
|
||||||
|
year = nd.year?.toString(),
|
||||||
|
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb") ?: coverUrl(nd.coverArt),
|
||||||
|
badge = "${counts.first}/${counts.second}",
|
||||||
|
onClick = { onPartialAlbumOpen(nd, ia) },
|
||||||
|
onDownload = { onDownload(ia) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showAllAlbums = true
|
||||||
|
}) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
@@ -248,7 +293,24 @@ fun ItunesArtistScreen(
|
|||||||
|
|
||||||
// ── Not downloaded ────────────────────────────────────────────
|
// ── Not downloaded ────────────────────────────────────────────
|
||||||
if (c.notDownloaded.isNotEmpty()) {
|
if (c.notDownloaded.isNotEmpty()) {
|
||||||
item { ArtistSectionHeader("Nicht heruntergeladen", c.notDownloaded.size) }
|
item {
|
||||||
|
ArtistSectionHeader("Nicht heruntergeladen", c.notDownloaded.size,
|
||||||
|
onShowAll = if (c.notDownloaded.size > 4) ({
|
||||||
|
allAlbumsTitle = "Nicht heruntergeladen"
|
||||||
|
allAlbumEntries = c.notDownloaded.map { ia ->
|
||||||
|
AllAlbumEntry(
|
||||||
|
name = ia.collectionName ?: "—",
|
||||||
|
year = ia.releaseDate?.take(4),
|
||||||
|
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb"),
|
||||||
|
dimmed = true,
|
||||||
|
onClick = { onItunesAlbumOpen(ia) },
|
||||||
|
onDownload = { onDownload(ia) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showAllAlbums = true
|
||||||
|
}) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
@@ -384,7 +446,7 @@ internal fun ItunesArtistTrackRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ArtistSectionHeader(title: String, count: Int? = null) {
|
internal fun ArtistSectionHeader(title: String, count: Int? = null, onShowAll: (() -> Unit)? = null) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(start = 20.dp, top = 16.dp, bottom = 10.dp, end = 16.dp),
|
modifier = Modifier.padding(start = 20.dp, top = 16.dp, bottom = 10.dp, end = 16.dp),
|
||||||
@@ -399,5 +461,13 @@ internal fun ArtistSectionHeader(title: String, count: Int? = null) {
|
|||||||
.background(NsSurface2)
|
.background(NsSurface2)
|
||||||
.padding(horizontal = 7.dp, vertical = 2.dp))
|
.padding(horizontal = 7.dp, vertical = 2.dp))
|
||||||
}
|
}
|
||||||
|
if (onShowAll != null) {
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Text(
|
||||||
|
"Alle anzeigen",
|
||||||
|
fontFamily = Outfit, fontSize = 12.sp, color = NsAccent,
|
||||||
|
modifier = Modifier.clickable(onClick = onShowAll),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +75,12 @@ fun NowPlayingScreen(
|
|||||||
val song = state.currentSong
|
val song = state.currentSong
|
||||||
var tab by remember { mutableStateOf(NpTab.PLAYER) }
|
var tab by remember { mutableStateOf(NpTab.PLAYER) }
|
||||||
|
|
||||||
|
// Preload lyrics as soon as NowPlaying opens — so the Lyrics tab shows instantly
|
||||||
|
val lyricsVm: LyricsViewModel = viewModel()
|
||||||
|
LaunchedEffect(song?.id) {
|
||||||
|
if (song != null) lyricsVm.loadForSong(song)
|
||||||
|
}
|
||||||
|
|
||||||
val statusBarTop: Dp = with(LocalDensity.current) { WindowInsets.safeDrawing.getTop(this).toDp() }
|
val statusBarTop: Dp = with(LocalDensity.current) { WindowInsets.safeDrawing.getTop(this).toDp() }
|
||||||
val topPad = maxOf(statusBarTop + 16.dp, 52.dp)
|
val topPad = maxOf(statusBarTop + 16.dp, 52.dp)
|
||||||
val navBarBottom: Dp = with(LocalDensity.current) { WindowInsets.safeDrawing.getBottom(this).toDp() }
|
val navBarBottom: Dp = with(LocalDensity.current) { WindowInsets.safeDrawing.getBottom(this).toDp() }
|
||||||
@@ -392,7 +398,6 @@ fun NowPlayingScreen(
|
|||||||
|
|
||||||
// ── LYRICS TAB ─────────────────────────────────────────────────
|
// ── LYRICS TAB ─────────────────────────────────────────────────
|
||||||
if (tab == NpTab.LYRICS && song != null) {
|
if (tab == NpTab.LYRICS && song != null) {
|
||||||
val lyricsVm: LyricsViewModel = viewModel()
|
|
||||||
val lyricsState by lyricsVm.state.collectAsStateWithLifecycle()
|
val lyricsState by lyricsVm.state.collectAsStateWithLifecycle()
|
||||||
val lyricsScrollState = rememberScrollState()
|
val lyricsScrollState = rememberScrollState()
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -404,19 +409,21 @@ fun NowPlayingScreen(
|
|||||||
val lyricsPaddingPx = remember(density) { with(density) { 200.dp.toPx() } }
|
val lyricsPaddingPx = remember(density) { with(density) { 200.dp.toPx() } }
|
||||||
var lyricsViewH by remember { mutableIntStateOf(0) }
|
var lyricsViewH by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
LaunchedEffect(song.id) { lyricsVm.loadForSong(song) }
|
|
||||||
|
|
||||||
// 200ms lookahead — same as web app for better sync feel
|
// 200ms lookahead — same as web app for better sync feel
|
||||||
val activeIdx = if (lyricsState.lines.isNotEmpty())
|
val activeIdx = if (lyricsState.lines.isNotEmpty())
|
||||||
lyricsState.lines.indexOfLast { it.timeMs <= state.positionMs + 200L }.coerceAtLeast(0)
|
lyricsState.lines.indexOfLast { it.timeMs <= state.positionMs + 200L }.coerceAtLeast(0)
|
||||||
else -1
|
else -1
|
||||||
|
|
||||||
// Center the active line when it changes — matches web behavior (no drift)
|
// Center the active line when it changes
|
||||||
LaunchedEffect(activeIdx) {
|
LaunchedEffect(activeIdx) {
|
||||||
if (activeIdx < 0 || lyricsState.lines.isEmpty() || lyricsViewH == 0) return@LaunchedEffect
|
if (activeIdx < 0 || lyricsState.lines.isEmpty() || lyricsViewH == 0) return@LaunchedEffect
|
||||||
val target = (lyricsPaddingPx + activeIdx * lineHeightPx + lineHeightPx / 2f
|
val target = (lyricsPaddingPx + activeIdx * lineHeightPx + lineHeightPx / 2f
|
||||||
- lyricsViewH * 0.5f).coerceAtLeast(0f).toInt()
|
- lyricsViewH * 0.5f).coerceAtLeast(0f).toInt()
|
||||||
lyricsScrollState.animateScrollTo(target, tween(350, easing = FastOutSlowInEasing))
|
// Spring handles rapid line changes gracefully — starts from current scroll velocity
|
||||||
|
lyricsScrollState.animateScrollTo(
|
||||||
|
target,
|
||||||
|
spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
|||||||
Reference in new issue
Block a user