feat: artist-page scroll/search memory (android) + move APK button to bottom (web)
- Android: the artist page's main list and the "Alle anzeigen" album overlay (grid scroll + search query) are hoisted into the Artist/Explore ViewModels, so drilling into an album/artist and back keeps position + query. Covers the library artist and Deezer (Explore) artist screens. - Web: the "Android App" download section now sits at the very bottom of Settings, below the Navidrome-Server (account) section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
a780f6622b
commit
93ee958434
6 files changed
+41
-16
No files matched your search
@@ -215,11 +215,14 @@ fun ArtistDetailScreen(
|
||||
title = openAllAlbums ?: "",
|
||||
entries = allAlbumEntries,
|
||||
onBack = { openAllAlbums = null },
|
||||
gridState = vm.allAlbumsGrid,
|
||||
query = vm.allAlbumsQuery.value,
|
||||
onQueryChange = { vm.allAlbumsQuery.value = it },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
LazyColumn(state = vm.pageListState, modifier = Modifier.fillMaxSize()) {
|
||||
// Back link
|
||||
item {
|
||||
Row(
|
||||
@@ -624,8 +627,10 @@ internal fun AllAlbumsOverlay(
|
||||
title: String,
|
||||
entries: List<AllAlbumEntry>,
|
||||
onBack: () -> Unit,
|
||||
gridState: androidx.compose.foundation.lazy.grid.LazyGridState = androidx.compose.foundation.lazy.grid.rememberLazyGridState(),
|
||||
query: String = "",
|
||||
onQueryChange: (String) -> Unit = {},
|
||||
) {
|
||||
var query by remember { mutableStateOf("") }
|
||||
val filtered = remember(query, entries) {
|
||||
if (query.isBlank()) entries
|
||||
else entries.filter { it.name.contains(query, ignoreCase = true) }
|
||||
@@ -660,11 +665,11 @@ internal fun AllAlbumsOverlay(
|
||||
// Search bar
|
||||
OutlinedTextField(
|
||||
value = query,
|
||||
onValueChange = { query = it },
|
||||
onValueChange = onQueryChange,
|
||||
placeholder = { Text("Suchen…", color = NsOnSurface2) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, null, tint = NsOnSurface2) },
|
||||
trailingIcon = if (query.isNotEmpty()) { {
|
||||
IconButton(onClick = { query = "" }) {
|
||||
IconButton(onClick = { onQueryChange("") }) {
|
||||
Icon(Icons.Default.Clear, null, tint = NsOnSurface2)
|
||||
}
|
||||
} } else null,
|
||||
@@ -684,6 +689,7 @@ internal fun AllAlbumsOverlay(
|
||||
)
|
||||
|
||||
LazyVerticalGrid(
|
||||
state = gridState,
|
||||
columns = GridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
|
||||
@@ -145,6 +145,9 @@ fun ExploreScreen(
|
||||
allAlbumsOpen = state.artistAllAlbums,
|
||||
onOpenAllAlbums = vm::openArtistAllAlbums,
|
||||
onCloseAllAlbums = vm::closeArtistAllAlbums,
|
||||
allAlbumsGrid = vm.allAlbumsGrid,
|
||||
allAlbumsQuery = vm.allAlbumsQuery.value,
|
||||
onAllAlbumsQueryChange = { vm.allAlbumsQuery.value = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,9 @@ fun ItunesArtistScreen(
|
||||
allAlbumsOpen: String?,
|
||||
onOpenAllAlbums: (String) -> Unit,
|
||||
onCloseAllAlbums: () -> Unit,
|
||||
allAlbumsGrid: androidx.compose.foundation.lazy.grid.LazyGridState = androidx.compose.foundation.lazy.grid.rememberLazyGridState(),
|
||||
allAlbumsQuery: String = "",
|
||||
onAllAlbumsQueryChange: (String) -> Unit = {},
|
||||
) {
|
||||
val artist = artistState.artist
|
||||
val cats = if (!artistState.isLoading)
|
||||
@@ -144,6 +147,9 @@ fun ItunesArtistScreen(
|
||||
title = allAlbumsOpen,
|
||||
entries = allAlbumEntries,
|
||||
onBack = onCloseAllAlbums,
|
||||
gridState = allAlbumsGrid,
|
||||
query = allAlbumsQuery,
|
||||
onQueryChange = onAllAlbumsQueryChange,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,6 +70,12 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV
|
||||
private val _state = MutableStateFlow(ArtistUiState())
|
||||
val state: StateFlow<ArtistUiState> = _state
|
||||
|
||||
// Scroll of the artist page + the "alle anzeigen" overlay, hoisted so tapping an album/artist and
|
||||
// coming back returns you to the same spot (the artist route entry, hence this VM, is retained).
|
||||
val pageListState = androidx.compose.foundation.lazy.LazyListState()
|
||||
val allAlbumsGrid = androidx.compose.foundation.lazy.grid.LazyGridState()
|
||||
val allAlbumsQuery = androidx.compose.runtime.mutableStateOf("")
|
||||
|
||||
init {
|
||||
if (artistId.isNullOrBlank()) {
|
||||
_state.value = ArtistUiState(isLoading = false, error = "Keine Künstler-ID übergeben")
|
||||
|
||||
@@ -79,6 +79,10 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
// Global-search results scroll — hoisted so it survives an album/artist drill-in and back.
|
||||
val searchListState = LazyListState()
|
||||
// "Alle anzeigen" overlay (Deezer artist albums): scroll + search query, hoisted so an album
|
||||
// drill-in and back keeps both (the overlay itself leaves composition on drill-in).
|
||||
val allAlbumsGrid = androidx.compose.foundation.lazy.grid.LazyGridState()
|
||||
val allAlbumsQuery = androidx.compose.runtime.mutableStateOf("")
|
||||
|
||||
private var searchJob: Job? = null
|
||||
|
||||
|
||||
+12
-12
@@ -605,18 +605,6 @@
|
||||
<div id="s-cover-color-bg" class="toggle" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section" id="s-apk-section" style="display:none">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🤖</span>
|
||||
Android App
|
||||
</h3>
|
||||
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;line-height:1.6">Installiere die NovaSonic Android-App direkt aufs Handy. <span id="s-apk-meta" style="color:var(--text3)"></span></div>
|
||||
<a id="s-apk-btn" href="#" download style="display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border-radius:10px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;font-weight:700;font-size:13px;text-decoration:none;box-shadow:0 4px 12px var(--accent-glow)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
APK herunterladen
|
||||
</a>
|
||||
<div style="font-size:11px;color:var(--text3);margin-top:8px">Danach im Handy „Installation aus unbekannten Quellen" für den Browser erlauben.</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📊</span>
|
||||
@@ -709,6 +697,18 @@
|
||||
<div class="field"><label>Benutzername</label><input id="s-user" type="text" placeholder="admin"></div>
|
||||
<div class="field"><label>Passwort</label><input id="s-pass" type="password" placeholder="(unverändert lassen)"></div>
|
||||
</div>
|
||||
<div class="settings-section" id="s-apk-section" style="display:none">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🤖</span>
|
||||
Android App
|
||||
</h3>
|
||||
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;line-height:1.6">Installiere die NovaSonic Android-App direkt aufs Handy. <span id="s-apk-meta" style="color:var(--text3)"></span></div>
|
||||
<a id="s-apk-btn" href="#" download style="display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border-radius:10px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;font-weight:700;font-size:13px;text-decoration:none;box-shadow:0 4px 12px var(--accent-glow)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
APK herunterladen
|
||||
</a>
|
||||
<div style="font-size:11px;color:var(--text3);margin-top:8px">Danach im Handy „Installation aus unbekannten Quellen" für den Browser erlauben.</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sticky footer -->
|
||||
<div style="display:flex;flex-direction:column;gap:8px;padding:12px 24px 16px;border-top:1px solid var(--border);flex-shrink:0;background:var(--surface);border-radius:0 0 18px 18px">
|
||||
|
||||
Reference in new issue
Block a user