feat(android): Album-Knopfleiste wie im Playlist-Detail; Changelog-Backlog aufgeraeumt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
66fcdbba05
commit
eb72e5c27d
3 files changed
+39
-39
No files matched your search
+20
-18
@@ -79,8 +79,8 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d
|
|||||||
|
|
||||||
## [3.0] — 2026-09-19
|
## [3.0] — 2026-09-19
|
||||||
|
|
||||||
**Android:** 1.13 (versionCode 16) → 1.14 (versionCode 17). 1.13 brachte Folgen, Zeitleiste und
|
**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.
|
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,
|
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
|
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
|
- 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
|
allein um die Durchsehbarkeit. Weil das `LibraryViewModel` am Navigationseintrag hängt und einen
|
||||||
Zielwechsel nicht überlebt, reicht `LibraryIntent` die Absicht einmalig durch.
|
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
|
- **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.
|
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
|
- **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.
|
- **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.
|
- 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.
|
- 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:
|
- **Web + Android** — **Meldungen zu gefolgten Künstlern** — der Rest des Künstler-folgen-Vorhabens.
|
||||||
- A **follow list** of artists (stored per user, e.g. in app settings / a small backend table).
|
Folgen selbst, die Übersicht „wem folge ich" und die Zeitleiste neuer Veröffentlichungen sind
|
||||||
- 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.
|
**seit 3.0 gebaut** (siehe dort); offen sind nur noch die beiden Meldewege:
|
||||||
- **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).
|
- **Glocke für neue Veröffentlichungen:** die Zeitleiste muss man heute selbst aufschlagen. Die
|
||||||
- **Concert alerts:** reuse the existing **Ticketmaster** events integration to notify when a followed artist has nearby/upcoming shows.
|
Daten liegen schon vor (`POST /api/artist-releases`), es fehlt der Vergleich „was ist seit
|
||||||
- Ties into the importer: a "download this new release" action straight from the notification.
|
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** — Playlist drag-and-drop / sort order
|
||||||
- **Android** — Artist page iTunes fallback (for artists not in Navidrome)
|
- **Android** — Artist page iTunes fallback (for artists not in Navidrome)
|
||||||
- **Android** — Romaji / Japanese sort support
|
- **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
|
- **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
|
### Ideen / Backlog
|
||||||
- **Künstler und Alben liken, daraus „folgen" bauen** — anders als Playlists sind Künstler und Alben
|
- (Der Punkt „Künstler und Alben liken, daraus ‚folgen' bauen" ist mit 3.0 umgesetzt und dort
|
||||||
in der Subsonic-API wirklich markierbar. Navidrome wirft in `media_annotation.go` sogar alle drei
|
beschrieben.)
|
||||||
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.
|
|
||||||
|
|
||||||
### Known Bugs
|
### 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.
|
- **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.
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.novasonic.app"
|
applicationId = "com.novasonic.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 17
|
versionCode = 18
|
||||||
versionName = "1.14"
|
versionName = "1.15"
|
||||||
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
|
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
|
||||||
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
|
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
|
||||||
// Platzhalter stehen und der App-Link greift schlicht nicht.
|
// Platzhalter stehen und der App-Link greift schlicht nicht.
|
||||||
|
|||||||
@@ -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(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
|
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
@@ -165,20 +170,18 @@ fun AlbumScreen(
|
|||||||
modifier = Modifier.weight(1f).height(46.dp),
|
modifier = Modifier.weight(1f).height(46.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = NsAccent, contentColor = Color.White),
|
colors = ButtonDefaults.buttonColors(containerColor = NsAccent, contentColor = Color.White),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.PlayArrow, null, modifier = Modifier.size(20.dp))
|
Icon(Icons.Default.PlayArrow, "Abspielen", modifier = Modifier.size(22.dp))
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text("Play", fontFamily = Outfit, fontWeight = FontWeight.Bold, fontSize = 15.sp)
|
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { onPlay(album.song.shuffled(), 0) },
|
onClick = { onPlay(album.song.shuffled(), 0) },
|
||||||
modifier = Modifier.weight(1f).height(46.dp),
|
modifier = Modifier.weight(1f).height(46.dp),
|
||||||
border = ButtonDefaults.outlinedButtonBorder(enabled = true).copy(brush = SolidColor(NsAccent.copy(alpha = 0.55f))),
|
border = ButtonDefaults.outlinedButtonBorder(enabled = true).copy(brush = SolidColor(NsAccent.copy(alpha = 0.55f))),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Shuffle, null, modifier = Modifier.size(20.dp), tint = NsAccent)
|
Icon(Icons.Default.Shuffle, "Zufällig abspielen", modifier = Modifier.size(20.dp), tint = NsAccent)
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text("Shuffle", fontFamily = Outfit, fontWeight = FontWeight.Bold, fontSize = 15.sp, color = NsAccent)
|
|
||||||
}
|
}
|
||||||
// Album merken — derselbe Weg wie beim Song: Navidrome behandelt Song-, Album- und
|
// Album merken — derselbe Weg wie beim Song: Navidrome behandelt Song-, Album- und
|
||||||
// Kuenstler-IDs beim Markieren gleich.
|
// Kuenstler-IDs beim Markieren gleich.
|
||||||
@@ -186,9 +189,9 @@ fun AlbumScreen(
|
|||||||
val gemerkt = overrides[album.id] ?: (album.starred != null)
|
val gemerkt = overrides[album.id] ?: (album.starred != null)
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { vm.merkenUmschalten(album.id, album.starred != null) },
|
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(
|
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),
|
shape = RoundedCornerShape(14.dp),
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
@@ -196,7 +199,7 @@ fun AlbumScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
if (gemerkt) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
if (gemerkt) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||||
if (gemerkt) "Gemerkt" else "Album merken",
|
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 &&
|
val isFullyDownloaded = downloadGroup != null &&
|
||||||
@@ -205,22 +208,17 @@ fun AlbumScreen(
|
|||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
// Tappable unless fully done — resumes / re-downloads any missing songs.
|
// Tappable unless fully done — resumes / re-downloads any missing songs.
|
||||||
onClick = { if (!isFullyDownloaded) vm.downloadAlbum() },
|
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(
|
border = ButtonDefaults.outlinedButtonBorder(enabled = true).copy(
|
||||||
brush = SolidColor(
|
brush = SolidColor(if (isFullyDownloaded) NsAccent else NsOnSurface3)
|
||||||
when {
|
|
||||||
isFullyDownloaded -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
|
||||||
else -> NsAccent.copy(alpha = 0.55f)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
when {
|
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)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user