diff --git a/CHANGELOG.md b/CHANGELOG.md index 619e0d2..49518ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,8 +79,8 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d ## [3.0] — 2026-09-19 -**Android:** 1.13 (versionCode 16) → 1.14 (versionCode 17). 1.13 brachte Folgen, Zeitleiste und -den Gefolgt-Filter, 1.14 die begrenzte Reihe in den Favoriten. +**Android:** 1.13 (versionCode 16) → 1.15 (versionCode 18). 1.13 brachte Folgen, Zeitleiste und +den Gefolgt-Filter, 1.14 die begrenzte Reihe in den Favoriten, 1.15 die Knopfleiste im Album. Erster Sprung auf eine neue Hauptversion seit 2.0: „Künstlern folgen" ist kein weiteres Fenster, sondern ein neuer Umgang mit der Bibliothek — die App erzählt jetzt, was **draußen** neu ist, statt @@ -124,6 +124,13 @@ nur zu zeigen, was drinsteht. - Android zeigt zehn und dann den Knopf; die `LazyRow` baut ohnehin nur Sichtbares, dort ging es allein um die Durchsehbarkeit. Weil das `LibraryViewModel` am Navigationseintrag hängt und einen Zielwechsel nicht überlebt, reicht `LibraryIntent` die Absicht einmalig durch. +- **Die Knopfleiste im Album sieht aus wie die der Playlist (Android)** — im Album trugen *Play* + und *Shuffle* noch Beschriftungen und teilten sich die Breite, während Herz und Download auf feste + 46 dp gesetzt waren. Vier Knöpfe in einer Reihe lassen dafür keinen Platz: die Schrift wurde + abgeschnitten, und die Reihe war ungleich geteilt. Jetzt vier gleich breite reine Symbolknöpfe wie + im Playlist-Detail — Play gefüllt als Hauptaktion, der Rest umrandet und erst accent gefärbt, wenn + er aktiv ist (gemerkt, vollständig geladen). Die Symbole tragen jetzt Beschriftungen für die + Vorlesefunktion, vorher stand dort bei Play und Shuffle `null`. - **Zeitfenster-Chips der Zeitleiste beginnen mit 90 Tagen (Android)** — das ist die Vorgabe, also steht der aktive Chip jetzt vorn statt in der Mitte. Web war schon so. - **Filter „❤ Gefolgt" im Künstler-Reiter (Android)** — zwei Chips über der Liste, gleiche Stelle @@ -1030,12 +1037,15 @@ Polish release: Android navigation and offline resilience, download accuracy for - **Keep what you like:** per-track (or whole-mix) option to **promote into the permanent library** instead of letting it expire. - Goal: completely random/unfamiliar music you don't already know — like Spotify's curated/discovery playlists, but self-hosted. - Open questions: where downloaded-but-temporary tracks live vs. the main library (separate folder + retention job), how Navidrome should/shouldn't index them, and dedupe against tracks you already own. -- **Web + Android** — **Artist following + new-release & concert notifications** — follow artists and get notified when they drop something new or play nearby. Idea: - - A **follow list** of artists (stored per user, e.g. in app settings / a small backend table). - - A **"Following" view** listing all artists you follow (grid of artist photos), as an entry point to their pages — a dedicated overview so you can see and manage who you follow, plus a follow/unfollow toggle on the artist page. - - **New-release alerts:** periodic check for new albums/singles by followed artists (sources: iTunes lookup / MusicBrainz / Last.fm), surfaced in the existing in-app notification bell (and optionally push). - - **Concert alerts:** reuse the existing **Ticketmaster** events integration to notify when a followed artist has nearby/upcoming shows. - - Ties into the importer: a "download this new release" action straight from the notification. +- **Web + Android** — **Meldungen zu gefolgten Künstlern** — der Rest des Künstler-folgen-Vorhabens. + Folgen selbst, die Übersicht „wem folge ich" und die Zeitleiste neuer Veröffentlichungen sind + **seit 3.0 gebaut** (siehe dort); offen sind nur noch die beiden Meldewege: + - **Glocke für neue Veröffentlichungen:** die Zeitleiste muss man heute selbst aufschlagen. Die + Daten liegen schon vor (`POST /api/artist-releases`), es fehlt der Vergleich „was ist seit + deinem letzten Besuch dazugekommen" und der Eintrag in der vorhandenen Benachrichtigungsglocke + (wahlweise Push). + - **Konzert-Meldungen:** die vorhandene **Ticketmaster**-Anbindung wiederverwenden und melden, + wenn ein gefolgter Künstler in der Nähe spielt. Bewusst zurückgestellt — erst die Neuheiten. - **Android** — Playlist drag-and-drop / sort order - **Android** — Artist page iTunes fallback (for artists not in Navidrome) - **Android** — Romaji / Japanese sort support @@ -1044,16 +1054,8 @@ Polish release: Android navigation and offline resilience, download accuracy for - **Android** - Andorid Auto Sync last played and queue too with Phone then, rn its not really syncing or safin what last played on Android Auto, also check if selecting a song to play on Android Auto works that song is getting played and in a playlist it will play the queue a this point then like it works already in Android and Web ### Ideen / Backlog -- **Künstler und Alben liken, daraus „folgen" bauen** — anders als Playlists sind Künstler und Alben - in der Subsonic-API wirklich markierbar. Navidrome wirft in `media_annotation.go` sogar alle drei - Parameter in eine Liste (`id`, `albumId`, `artistId`) und behandelt sie gleich, unser bestehender - Aufruf `api('star', '&id=' + id)` würde für Künstler also **ohne Änderung** funktionieren; den - Zustand liefert `getStarred2` mit Künstlern und Alben zurück. Es fehlt nur die Oberfläche: ein Herz - auf der Künstler- und Albumseite. Darauf ließe sich das länger angedachte *Künstler folgen* setzen - — neue Veröffentlichungen markierter Künstler über Deezer erkennen und melden, ohne dafür eine - eigene Liste pflegen zu müssen. Playlists gehen dafür ausdrücklich **nicht**: `getStarred`/ - `getStarred2` geben nur Künstler, Alben und Songs zurück, ein in Navidromes Oberfläche geliktes - Playlist ist über die API unsichtbar. +- (Der Punkt „Künstler und Alben liken, daraus ‚folgen' bauen" ist mit 3.0 umgesetzt und dort + beschrieben.) ### Known Bugs - **Android (lyrics)** — Synced-lyrics auto-scroll is **not smooth**: it follows playback and highlights the right line, but the list jumps to it rather than gliding. Web autoscroll is smooth. Not yet fixed. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d7830cb..fdd6035 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.novasonic.app" minSdk = 26 targetSdk = 35 - versionCode = 17 - versionName = "1.14" + versionCode = 18 + versionName = "1.15" // Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in // gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der // Platzhalter stehen und der App-Link greift schlicht nicht. diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/AlbumScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/AlbumScreen.kt index dcd0dd5..f2be1f7 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/AlbumScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/AlbumScreen.kt @@ -155,7 +155,12 @@ fun AlbumScreen( } } - // ── Play / Shuffle row ──────────────────────────────────────────── + // ── Play / Shuffle / Merken / Download ──────────────────────────── + // Gleiche Leiste wie beim Playlist-Detail: vier gleich breite, reine Symbolknoepfe. + // Play und Shuffle trugen hier noch Beschriftungen, waehrend Herz und Download fest + // 46 dp breit waren — die Reihe war dadurch ungleich geteilt und die Schrift wurde auf + // schmalen Geraeten abgeschnitten. Play bleibt gefuellt als Hauptaktion; die uebrigen + // sind umrandet und faerben sich erst accent, wenn sie aktiv sind. Row( Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -165,20 +170,18 @@ fun AlbumScreen( modifier = Modifier.weight(1f).height(46.dp), colors = ButtonDefaults.buttonColors(containerColor = NsAccent, contentColor = Color.White), shape = RoundedCornerShape(14.dp), + contentPadding = PaddingValues(0.dp), ) { - Icon(Icons.Default.PlayArrow, null, modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(6.dp)) - Text("Play", fontFamily = Outfit, fontWeight = FontWeight.Bold, fontSize = 15.sp) + Icon(Icons.Default.PlayArrow, "Abspielen", modifier = Modifier.size(22.dp)) } OutlinedButton( onClick = { onPlay(album.song.shuffled(), 0) }, modifier = Modifier.weight(1f).height(46.dp), border = ButtonDefaults.outlinedButtonBorder(enabled = true).copy(brush = SolidColor(NsAccent.copy(alpha = 0.55f))), shape = RoundedCornerShape(14.dp), + contentPadding = PaddingValues(0.dp), ) { - Icon(Icons.Default.Shuffle, null, modifier = Modifier.size(20.dp), tint = NsAccent) - Spacer(Modifier.width(6.dp)) - Text("Shuffle", fontFamily = Outfit, fontWeight = FontWeight.Bold, fontSize = 15.sp, color = NsAccent) + Icon(Icons.Default.Shuffle, "Zufällig abspielen", modifier = Modifier.size(20.dp), tint = NsAccent) } // Album merken — derselbe Weg wie beim Song: Navidrome behandelt Song-, Album- und // Kuenstler-IDs beim Markieren gleich. @@ -186,9 +189,9 @@ fun AlbumScreen( val gemerkt = overrides[album.id] ?: (album.starred != null) OutlinedButton( onClick = { vm.merkenUmschalten(album.id, album.starred != null) }, - modifier = Modifier.height(46.dp).width(46.dp), + modifier = Modifier.weight(1f).height(46.dp), border = ButtonDefaults.outlinedButtonBorder(enabled = true).copy( - brush = SolidColor(if (gemerkt) NsAccent else NsAccent.copy(alpha = 0.55f)) + brush = SolidColor(if (gemerkt) NsAccent else NsOnSurface3) ), shape = RoundedCornerShape(14.dp), contentPadding = PaddingValues(0.dp), @@ -196,7 +199,7 @@ fun AlbumScreen( Icon( if (gemerkt) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, if (gemerkt) "Gemerkt" else "Album merken", - Modifier.size(20.dp), tint = NsAccent, + Modifier.size(20.dp), tint = if (gemerkt) NsAccent else NsOnSurface2, ) } val isFullyDownloaded = downloadGroup != null && @@ -205,22 +208,17 @@ fun AlbumScreen( OutlinedButton( // Tappable unless fully done — resumes / re-downloads any missing songs. onClick = { if (!isFullyDownloaded) vm.downloadAlbum() }, - modifier = Modifier.height(46.dp).width(46.dp), + modifier = Modifier.weight(1f).height(46.dp), border = ButtonDefaults.outlinedButtonBorder(enabled = true).copy( - brush = SolidColor( - when { - isFullyDownloaded -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) - else -> NsAccent.copy(alpha = 0.55f) - } - ) + brush = SolidColor(if (isFullyDownloaded) NsAccent else NsOnSurface3) ), shape = RoundedCornerShape(14.dp), contentPadding = PaddingValues(0.dp), ) { when { - isFullyDownloaded -> Icon(Icons.Default.CheckCircle, "Downloaded", Modifier.size(20.dp), tint = MaterialTheme.colorScheme.primary) + isFullyDownloaded -> Icon(Icons.Default.CheckCircle, "Offline verfügbar", Modifier.size(20.dp), tint = NsAccent) isDownloading -> CircularProgressIndicator(Modifier.size(18.dp), color = NsAccent, strokeWidth = 2.dp) - else -> Icon(Icons.Default.Download, "Herunterladen", Modifier.size(20.dp), tint = NsAccent) + else -> Icon(Icons.Default.Download, "Herunterladen", Modifier.size(20.dp), tint = NsOnSurface2) } } }