Lyrics: Schalter "Mitlaufen" im Lyrics-Reiter, Vorgabe AUS. Hervorgehoben wird die
laufende Zeile immer, gescrollt nur bei eingeschaltetem Mitlaufen. Nur bei synchronen
Texten sichtbar. Android speichert den Schluessel einzeln, damit das Speichern im
Einstellungsbildschirm ihn nicht auf die Vorgabe zuruecksetzt.
Mischen (Web): sort(() => Math.random() - 0.5) ist keine Mischung. Gemessen blieb der
erste Titel drei- bis fuenfmal zu oft vorn (18,8 % statt 10 % bei n=10, 0,23 % statt
0,05 % bei n=2000). Ersetzt durch Fisher-Yates in queueMath.mischeListe.
Queue-Ende (Web): ein beendetes <audio> meldet sich als pausiert, die Wiederaufnahme
nach unfreiwilligem Hintergrund-Stop rief play() und spielte den letzten Titel erneut.
Jetzt Abbruch bei audio.ended.
Shuffle (Android): das Mischen macht jetzt der Player (playSongs(liste, -1)), damit die
ungemischte Liste erhalten bleibt — vorher leuchtete das Symbol nicht und "Shuffle aus"
konnte die Albumreihenfolge nicht wiederherstellen.
Radio-Modus (beide): der laufende Titel ist aus der letzten Rueckfallebene ausgenommen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vorher landeten alle gefolgten Künstler in der waagerechten Reihe der
Favoriten — bei 200 Gefolgten 200 Karten samt Bildern, von denen man vier
sieht. Am Ende steht jetzt "Alle anzeigen (+n weitere)" und springt in die
Künstlerliste mit aktivem Gefolgt-Filter.
Im Web richtet sich die Anzahl nach der Fensterbreite statt nach einer
festen Zahl (passende Karten minus eine, damit der Knopf nicht
angeschnitten klebt; Untergrenze zwei), und bei Größenänderung wird neu
aufgeteilt. Android zeigt zehn; die LazyRow baut ohnehin nur Sichtbares,
dort ging es allein um Durchsehbarkeit.
LibraryIntent reicht die Absicht einmalig durch, weil das
LibraryViewModel am Navigationseintrag hängt und einen Zielwechsel nicht
überlebt.
Dazu: die Zeitfenster-Chips der Android-Zeitleiste beginnen mit 90 Tagen,
der Vorgabe — Web war schon so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Filter statt eigenem Reiter: die Bibliotheksleiste trägt schon Playlists,
Alben, Künstler und Offline. Die markierten IDs kommen bei jedem
Einschalten frisch, damit ein eben entfolgter Künstler nicht dasteht.
Die Zeitleiste ist über "✨ Neu" in den Favoriten erreichbar — dort stehen
die gefolgten Künstler, also liegt die Frage nach Neuem dort nahe.
Gleiche Darstellung wie im Web: nach Monat gruppiert, neueste zuerst,
Vorhandenes mit Haken statt Download-Knopf, Künstlername anklickbar.
Download läuft über den bekannten Weg samt Fortschrittsmeldung.
Neu: api/ReleasesClient.kt, api/model/ReleaseModels.kt,
viewmodel/ArtistNewsViewModel.kt, ui/screen/ArtistNewsScreen.kt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Herz sitzt jetzt im Banner neben dem Pin-Zahnrad statt in der
Aktionsleiste: die Leiste ist "was ich jetzt tue" (abspielen, mischen,
Konzerte), Folgen ist ein Zustand und gehört an den Künstler. Auf beiden
Plattformen gleich. Beim Album bleibt das Herz in der Aktionsleiste, dort
gibt es kein Banner.
Dazu die Übersicht, wem man folgt: die Favoritenseite bekommt Abschnitte
Künstler / Alben / Songs. Ohne zusätzlichen Aufruf — getStarred2 liefert
Künstler und Alben im selben Paket wie die Songs, wir haben sie bisher
verworfen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Folgen ist kein eigener Speicher, sondern Navidromes Stern auf dem
Künstler (star&id=<ndArtistId>) — Navidrome wirft id, albumId und
artistId beim Markieren in eine Liste und behandelt sie gleich, der
vorhandene Aufruf trug also ohne Änderung. Zustand aus `starred` in
getArtist/getAlbum, beide Plattformen schalten optimistisch um.
Android nutzt den vorhandenen StarState, der ohnehin über IDs arbeitet.
Dazu: getTopSongs bekommt seit Navidrome 0.64 zusätzlich `id`. Der
Namensweg sucht mit LIKE und Max:1, bei gleichnamigen Künstlern gewann
der erste Treffer. Jetzt id + artist zusammen; in Erkunden bleibt es beim
Namen, weil die artistId dort Deezers ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Löschen ließ Dateien zurück: Cover wurden nie entfernt (nur die
Audiodatei), unfertige Titel behielten ihre Datei, weil der Pfad aus der
Karte der FERTIGEN Titel kam, und laufende Downloadarbeit lief nach dem
Löschen noch minutenlang ins Leere. Jetzt kommt der Pfad aus der
Datenbankzeile, .tmp wird mitgenommen, Cover gehen nur weg wenn kein
anderer Titel sie noch benutzt, und die Arbeit der Gruppe wird
abgebrochen.
Dazu: ein zu hoch gezählter Fortschritt aus der Zeit vor 1.10 konnte sich
nie mehr korrigieren — die Gruppe galt als vollständig, der Knopf war
unantastbar und das Fortsetzen sucht nur Gruppen unter dem Sollwert. Beim
App-Start wird der Fortschritt aller Gruppen einmal aus dem echten
Zustand neu berechnet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Downloadarbeit lief mit NetworkType.CONNECTED, also über jede
Verbindung. Das war selbstbegrenzend, solange nach zehn Minuten Schluss
war — mit der automatischen Wiederholung aus 1.9 hätte sich eine große
Playlist unterwegs ununterbrochen weitergeladen (1900 Titel ≈ 9-10 GB).
Neuer Schalter unter "Offline-Downloads", standardmäßig an: dann gilt
UNMETERED, der Download pausiert ohne WLAN und läuft von selbst weiter.
Abschalten nur über eine Rückfrage, die das Datenvolumen benennt.
Beim Speichern werden laufende Gruppen mit REPLACE neu eingereiht —
WorkManager ändert die Bedingungen eingereihter Arbeit nicht mehr, der
Schalter hätte sonst erst beim nächsten App-Start gewirkt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Worker startete done bei group.downloadedTracks und zählte dieselben
fertigen Titel beim Überspringen nochmal: 412 fertige wurden zu 824.
Übersteigt der Wert totalTracks, gilt die Gruppe als vollständig, der
Fortschrittsbalken verschwindet und der Herunterladen-Knopf wird
unantastbar, obwohl Titel fehlen.
Der Fehler ist älter, fiel aber kaum auf, solange Fortsetzen nur von Hand
passierte — mit der automatischen Wiederholung aus 1.9 hätte er bei jedem
Lauf zugeschlagen. Zähler beginnt jetzt bei 0.
Dazu: beim Überspringen wurde der unveränderte Fortschritt geschrieben,
1900 überflüssige DB-Writes je Lauf. Jetzt nur nach echtem Versuch und
einmal beim Abbruch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WorkManager stoppt einen Hintergrund-Worker nach ~10 Minuten (ein
Vordergrunddienst scheidet aus, FOREGROUND_SERVICE_TYPE_DATA_SYNC stürzt
unter Android 15 ab). Der Worker prüfte nie isStopped, lief blind weiter
und meldete Result.failure() ohne Backoff — WorkManager startete nichts
neu, bei 1900 Titeln musste man wiederholt von Hand antippen.
Jetzt: sauberer Ausstieg bei isStopped mit Result.retry() und linearem
Backoff, Selbst-Fortsetzung solange Titel offen sind (max. 20 Versuche,
damit dauerhaft kaputte Titel keine Endlosschleife auslösen), Retry statt
Failure im äußeren catch, und beim App-Start werden unvollständige
Gruppen weitergeführt.
Dazu: die Zahl offener Titel kam aus einer Abfrage pro Titel am Ende
jedes Laufs — jetzt ein einzelnes COUNT mit JOIN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Warteschlange wurde aus allen Titeln einer Gruppe gebaut, ohne den
Download-Zustand zu prüfen. Die Map für lokale Dateien kennt aber nur
state = 'DONE' — für wartende oder fehlgeschlagene Titel lieferte
streamUrl() die Netz-Adresse, die offline nichts tut. Bei 100 Titeln ist
der Download durch, bei 1900 nicht; steht so ein Titel vorn, scheitert
die Wiedergabe sofort.
Play und Shuffle nehmen jetzt nur fertige Titel, ein Toast nennt die Zahl
der übersprungenen. Die Liste zeigt weiter alle, damit sichtbar bleibt,
was fehlt; Tippen auf einen unfertigen sagt es ebenfalls.
Nicht die Ursache: Media3 reicht große Listen als BundleListRetriever
durch, die 1-MB-Binder-Grenze greift dort nicht.
Dazu Backlog-Notiz: Künstler/Alben liken als Basis für "Künstler folgen".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
navigate() ließ den Stack-Eintrag weg, wenn sich die Ansicht nicht
geändert hatte, rief history.pushState aber trotzdem. Zweimal auf
denselben Tab geklickt = zwei Browser-Einträge auf dieselbe Ansicht,
jeder davon ein Zurück-Druck ohne sichtbare Wirkung. Im Test: 3x "Alben"
-> 3 Browser-Einträge bei 1 Stack-Eintrag, danach bewegten 4 Zurück-
Drücke die Ansicht einmal. Jetzt pusht nur eine wirklich neue Ansicht,
sonst replaceState. Vorwärts mitgetestet, unverändert.
Playlists: Navidrome liefert owner mit, das Web nutzte es nie. Übersicht
und Detail zeigen "· von <Name>", nur bei fremden Listen. Android zeigte
ihn bisher bei jeder Liste und unbeschriftet; beide Plattformen jetzt
gleich, Bibliotheks-Übersicht ebenfalls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Künstlername unter einem Deezer-Album war reiner Text — von dort zum
Künstler ging nur über Zurück und neu suchen. Er ist jetzt anklickbar und
öffnet dieselbe Deezer-Künstlerseite wie eine Künstler-Kachel.
Kostet keinen zusätzlichen API-Aufruf: Deezer legt /search/album und
/album/{id} das artist-Objekt samt id bei (120/120 Treffer über zwölf
Suchen geprüft), beide Mappings übernahmen die id längst.
/artist/{id}/albums lässt das artist-Objekt weg — Alben von dort bleiben
bewusst reiner Text, statt ins Leere zu zeigen.
Android zeigte in der Album-Detailansicht bisher gar keinen Künstler und
lädt Bild + Fanzahl nach, wenn der Künstler aus einem Album stammt.
.album-artist-name trug cursor:pointer auch ohne Ziel — jetzt nur noch,
wo wirklich etwas dahintersteht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Im cacheDir liegen Coils Bildspeicher und heruntergeladene APK-Updates, beides
jederzeit neu beschaffbar. Blieb dort nach einer neuen Version etwas Altes
liegen, war "Cache leeren" von Hand in den Systemeinstellungen die einzige
Abhilfe — nichts, das sich einem normalen Nutzer erklaeren laesst.
Geleert wird jetzt beim ersten Start nach einem Versionswechsel, verglichen wird
der gespeicherte versionCode. Im laufenden Betrieb passiert nichts, sonst waeren
die Cover bei jedem Start neu zu laden. Beim allerersten Start nach der
Installation ebenfalls nicht, da gibt es nichts aufzuraeumen.
Offline-Downloads bleiben unberuehrt: die liegen unter filesDir/offline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Android speicherte nur die Song-Ids und lud beim Start jeden Titel EINZELN und
nacheinander ueber das Netz nach. Das dauerte Sekunden, war unzuverlaessig und
ohne Verbindung unmoeglich. Gespeichert werden jetzt die vollstaendigen Songs als
JSON — Wiederherstellen ist ein lokaler Lesevorgang, wie im Web. Der Preis ist
ein veralteter Titel oder Cover bis zum naechsten Abspielen, was die deutlich
kleinere Unschoenheit ist.
Dazu sichert MusicService.onTaskRemoved die Warteschlange beim Wegwischen der
App: dort feuert onIsPlayingChanged nicht zuverlaessig, genau dieser Fall ging
vorher verloren.
Service Worker: die Cache-Version stand handgepflegt als ns-v78 in sw.js und
wurde beim Ausliefern vergessen — die Aktivierung loescht nur Caches mit
abweichendem Namen, also blieb der alte bestehen und Nutzer mussten den
Browsercache von Hand leeren. Ein Vite-Plugin setzt sie jetzt aus der Bauzeit,
die Quelldatei bleibt unveraendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Wiederherstellen schlug still fehl. Mehrere runCatching verschluckten jeden
Grund, sodass von aussen nur "passiert nichts" zu sehen war.
Zwei wahrscheinliche Ursachen adressiert:
- connect() laeuft in Application.onCreate(), wo der DataStore mit den
Navidrome-Zugangsdaten unter Umstaenden noch nicht gelesen ist. Dann scheitert
das Aufloesen ALLER Song-Ids und die Warteschlange bleibt leer. Es wird jetzt
bis zu fuenfmal mit wachsendem Abstand versucht.
- Wischt man die App aus den letzten Apps, wurde nichts gesichert:
onIsPlayingChanged feuert dabei nicht zuverlaessig. MusicService.onTaskRemoved
sichert jetzt vor dem Beenden.
Dazu Protokollausgaben unter dem Tag NsQueue an jeder Stufe (gesichert,
gefunden, Id nicht aufloesbar, wiederhergestellt, aufgegeben), damit ein
weiterer Fehlschlag per logcat zu klaeren ist statt per Vermutung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Erste Android-Version mit Warteschlangen-Wiederherstellung und Streitpunkten.
Das zuvor hochgeladene 1.3 enthielt restoreQueue noch nicht — daher blieb die
Now-Playing-Leiste nach dem Oeffnen leer. Release-Beschreibung mit angehoben,
sonst meldet der In-App-Updater kein Update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Mitschreiben der Abspielposition war zu viel des Guten: es bedeutete
staendiges Schreiben waehrend der Wiedergabe fuer einen Gewinn, den man kaum
bemerkt. Gesichert werden jetzt nur Warteschlange und aktueller Titel, der dann
von vorn beginnt.
Der eigentliche Gewinn bleibt und war der Ausloeser: unter Android war die
Now-Playing-Leiste nach dem Oeffnen leer, bis man einmal auf Play drueckte —
gesichert war nur der letzte Song, nicht die Warteschlange. Das Wiederherstellen
fuellt den Zustand jetzt ueber syncState(), also ohne Abspielen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wiedergabe fortsetzen:
Im Web ueberlebten Warteschlange und letzter Titel bereits, die POSITION im
Titel aber nicht — beim Neuladen begann der Song von vorn. Android sicherte nur
den letzten Song ohne Warteschlange. Beide merken sich jetzt Warteschlange,
Listenposition und Sekunde.
Web schreibt gedrosselt (alle 5 s) waehrend der Wiedergabe und sicher beim
Pausieren, Wegwischen und Tabwechsel — pagehide/visibilitychange statt
beforeunload, das am Handy unzuverlaessig feuert. Android sichert bei jedem
Titelwechsel und beim Pausieren und stellt nach dem Verbinden wieder her:
vorbereitet, aber NICHT abgespielt, wer die App oeffnet will nicht ueberfallen
werden. Laeuft schon etwas (Android Auto), passiert nichts.
Zwei Feinheiten: lag die Position im letzten Rest des Titels, faengt er von vorn
an, statt zwei Sekunden Abspann zu spielen; und gespeichert werden nur Song-Ids,
weil Titel und Cover sich durch Metadaten-Bearbeitung aendern koennen —
verschwundene Titel werden beim Wiederherstellen uebersprungen.
Streitpunkte im Duell:
Gemeinsame Kuenstler nach groesstem Ungleichgewicht statt nach Summe, mit
Kraefteverhaeltnis-Balken. Mindestens vier gemeinsame Plays und 50 % Unwucht,
sonst gilt ein einzelner Play gegen null schon als Streit. Web und Android mit
identischen Schwellen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ergaenzt Backend und Scrobble um die Navidrome-Adresse: alle drei kommen als
buildConfigField aus dem Build und greifen, wenn im Geraetespeicher nichts steht.
Der Anmeldebildschirm ist damit vorbelegt — beim ersten Start bleiben nur noch
Benutzer und Passwort. Die Felder in den Einstellungen bleiben sichtbar und
ueberschreibbar, ein Umzug ist also ohne neues APK zu korrigieren.
Zugangsdaten bleiben aussen vor. Das Publish-Skript reicht die Werte aus
scripts/.env.release durch, damit kuenftige Builds sie nicht verlieren.
Am erzeugten BuildConfig geprueft, alle vier Werte stehen darin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beide Adressen stehen ohnehin in der docker-compose.yml — sie zusaetzlich auf
jedem Geraet einzutippen ist Arbeit ohne Nutzen. Sie kommen jetzt als
buildConfigField aus -PnovasonicBackend= / -PnovasonicScrobble= und greifen,
wenn im Geraetespeicher nichts steht.
Die Felder in den Einstellungen bleiben sichtbar und ueberschreibbar: zieht ein
Dienst um oder aendert sich ein Port, laesst sich das am Geraet korrigieren,
statt ein APK bauen, hochladen und installieren zu muessen. Ohne gesetzte
Eigenschaften verhaelt sich die App exakt wie bisher.
Zugangsdaten bleiben bewusst aussen vor — die gehoeren pro Person, nicht in ein
APK, das im Release liegt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Host stand ohnehin schon beim Bauen fest, weil die App-Links ihn im Manifest
brauchen. Ihn zusaetzlich als Einstellung zu fuehren hiess: eine vierte Adresse
pflegen, die vom Manifest abweichen kann, ohne dass es jemand merkt — und der
Teilen-Eintrag versteckte sich, solange sie leer war, was beim Suchen schlicht
wie ein fehlendes Feature aussah.
Jetzt kommt beides aus -PnovasonicHost=…: das Manifest fuer eingehende Links,
BuildConfig.SHARE_HOST fuer ausgehende. Die Einstellung samt Speicherfeld ist
entfernt, Teilen ist immer sichtbar. versionCode 5 / 1.2, APK hochgeladen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der In-App-Updater vergleicht versionCode aus der Gitea-Release-Beschreibung mit
BuildConfig.VERSION_CODE. Beide standen auf 3, das hochgeladene APK meldete sich
also korrekt als "kein Update" — trotz Teilen-per-Link, Songloeschen,
Playlist-aus-Import und Duell-Anzeigetafel. Version angehoben und die
Release-Beschreibung auf versionCode=4 gesetzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Statt eines Prozentrings ("wie aehnlich seid ihr") zeigt die Ansicht jetzt, wer
worin gewinnt: fuenf Kategorien mit je einem Sieger und ein Gesamtstand.
Hoerzeit, Vielfalt, Tiefe (Plays je Kuenstler), Nachteule (Anteil vor 5 Uhr) und
Entdeckerdrang (Anteil exklusiver Kuenstler) — bewusst Kennzahlen zur Hoerweise
statt zur Menge, sonst gewinnt immer, wer laenger dabei ist.
Am laufenden Dienst mit zwei gegensaetzlichen Nutzern geprueft: 60 Plays a 300 s
ergeben 5,0 h und Tiefe 12,0, jeder dritte Play nachts ergibt 33 %, 60 von 62
exklusiven Kuenstlern ergeben 97 % — Endstand 3:2. Das JSON-Feldschema wurde
gegen das Kotlin-Modell abgeglichen.
Dazu drei Fixes im selben Bereich:
- Die Web-Ansicht lebte nur von Inline-Stilen mit festen Flex-Verhaeltnissen und
zerfiel am Desktop. Jetzt ein Raster mit eigenen Klassen, eine Spalte am Handy,
zwei ab 780 px; das Fenster musste dafuer von 720 auf 940 px, sonst haette die
zweite Spalte nie ausgeloest.
- Gemeinsame Songs wurden ueber song_id abgeglichen und fielen auseinander,
sobald eine Datei umbenannt wurde. Jetzt normalisierter Titel+Kuenstler.
- Media-Session: die Metadaten wurden feldweise mutiert (gegen Kachel-Flackern
unter Windows), ueber MPRIS kam davon nichts an — Plasma-Widgets zeigten
dauerhaft den zuerst gespielten Titel. Bei echtem Titelwechsel jetzt ein neues
MediaMetadata, sonst weiter in-place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Host eingehender Links steht fest im Manifest, ein Domainwechsel brauchte
also zwingend ein neues APK und liess alte Links sofort ins Leere laufen. Mit
-PnovasonicHost2= laesst sich waehrend des Umzugs eine zweite Adresse angeben;
ohne Angabe wird der erste Host wiederholt. Bewusst kein Platzhalter-Host als
Vorgabe: auf Android 11 und aelter verifiziert das System nur, wenn ALLE Hosts
eine gueltige assetlinks.json liefern — ein toter Eintrag wuerde alle blockieren.
Am zusammengefuehrten Manifest geprueft, beide Hosts landen darin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Song oder Album laesst sich per https://<host>/?song=<id> an jemanden auf
derselben Instanz schicken. Der Link traegt nur die Navidrome-Id, keine
Zugangsdaten — der Empfaenger sieht den Inhalt mit seinem eigenen Konto.
Web: Teilen ueber navigator.share() mit Rueckfall auf die Zwischenablage. Beim
Start wird ?song= / ?album= aufgeloest (nach der Anmeldung, vorher fehlen die
Zugangsdaten), das Album geoeffnet, der Song gestartet und der Parameter aus der
Adresszeile entfernt, damit ein Neuladen nicht erneut losspielt. Das Manifest
bekommt launch_handler=navigate-existing, damit der Link in einem bereits
offenen PWA-Fenster landet statt in einem neuen.
Android: Teilen ueber ACTION_SEND, Empfang ueber einen App-Link-Intent-Filter.
Der Host ist eine Build-Eigenschaft (-PnovasonicHost=…, verifiziert am
zusammengefuehrten Manifest), die Web-Adresse fuer ausgehende Links eine neue
Einstellung. Weil der NavController in der Compose-Hierarchie lebt und die
Activity nicht selbst navigieren kann, reicht DeepLinkState den Link durch;
onNewIntent ist mitbedacht, sonst passierte beim zweiten Link nichts.
assetlinks.json liegt als Vorlage in public/ und wird vom Frontend-Image
ausgeliefert (im Build verifiziert); ohne den Release-Fingerabdruck oeffnet der
Link die Web-App im Browser statt die App. Anleitung: docs/share-links.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Findet das Backend die von Navidrome gemeldete Datei nicht am angegebenen Pfad
(Subsonic liefert oft einen aus Tags gebauten virtuellen Pfad), sucht es ueber
Dateiname bzw. Kuenstler+Titel weiter. Liegt derselbe Song mehrfach auf der
Platte, konnte das die falsche Kopie treffen: die wurde getaggt, der bearbeitete
Song blieb unveraendert — die Aenderung "sprang zurueck". Das erklaert, warum es
bei Songs ohne Dublette immer ging und bei anderen nie.
Der ueber Aehnlichkeit gefundene Kandidat wird jetzt per ffprobe gegen die Tags
geprueft, die Navidrome fuer diesen Song meldet, und bei Widerspruch mit 409 und
erklaerender Meldung abgebrochen statt geraten. Zusaetzlich melden uebersprungene
Umbenennungen und Verschiebungen sich als Warnung, statt stumm zu bleiben; die
Antwort nennt den tatsaechlich bearbeiteten Pfad, der Editor zeigt beides an.
Ausserdem: -dontwarn org.slf4j.** — der Release-APK-Build brach seit laengerem an
R8 ab (Ktors Logging referenziert den slf4j-1.x-Binder, den es auf Android nicht
gibt). Vorbestehend, mit HEAD~1 verifiziert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Android holt zwei Sachen nach, die es im Web laengst gibt:
- Song von der Festplatte loeschen, im Kontextmenue unten abgesetzt und rot, mit
derselben Sicherheitsabfrage wie im Web. Laeuft ueber /api/delete-song und
erscheint nur mit konfiguriertem Backend — Navidrome allein loescht nichts.
- Playlist direkt aus dem Import anlegen: Haekchen plus Namensfeld ab zwei
Titeln, danach Scan anstossen, Titel suchen, Playlist aus den gefundenen.
Nicht gefundene werden gezaehlt statt die Erstellung scheitern zu lassen.
Dazu der Web-Fix zur Album-Position: die gespeicherte Scrollposition wurde in
genau einem requestAnimationFrame gesetzt. Solange Cover und Eintraege noch
nachluden, war scrollHeight kleiner als das Ziel und der Browser kappte den Wert
stillschweigend — man landete wieder oben. Jetzt wird er ueber bis zu ~0,5 s
erneut gesetzt, bis er sitzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
APK mit dem Debug-Key vom alten Rechner gebaut (Signatur 1039a00e…, passend zur
installierten App) und als Asset im Release apk-latest ersetzt, Body auf
versionCode=3. Dazu das ausfuehrbare Bit auf gradlew, das im Clone fehlte, und
.idea/ ignoriert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LaunchedEffect(state.albumSort) { scrollToItem(0) } feuerte nicht nur beim
Sortierwechsel, sondern auch beim ersten Compose nach der Rueckkehr aus einem
Album — die im ViewModel gehaltene Position wurde also sofort ueberschrieben.
Das Hochscrollen sitzt jetzt in setAlbumSort() und laeuft nur bei echtem
Wechsel; dazu zwei ungenutzte remember*State-Importe entfernt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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>
The app checks {backend}/api/apk/info on launch and, if a newer versionCode
than BuildConfig.VERSION_CODE is offered, shows an update dialog that
downloads {backend}/api/apk into a single wiped-each-time cache file and
hands it to the system installer (FileProvider + REQUEST_INSTALL_PACKAGES).
No APK hoarding; in-place install preserves data (stable signing key needed).
- backend /api/apk/info now returns versionCode (parsed from the release body).
- publish-apk.mjs auto-increments versionCode, sets versionName, and writes
versionCode/versionName into the Gitea release body.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Going into an album/artist and back now restores the scroll position and
keeps the typed query, instead of resetting to the top.
- Web: navigation.js snapshots each history entry's #main-content scroll and
any open local (header) search, restoring them on Back/Forward; a genuine
new navigation still starts at the top.
- Android: the Alben/Künstler lists (LibraryViewModel) and the global/local
search-results lists (ExploreViewModel/SearchViewModel) hoist their
LazyListState/LazyGridState, so scroll survives a drill-in/back for the
tab's lifetime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
You can still enjoy an incomplete album: the downloaded-tracks section now has
Abspielen + Zufall buttons (play/shuffle the downloaded songs) and each
downloaded row gets the 3-dot context menu, threaded from both artist screens.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The album-name normaliser stripped everything but a-z0-9, so any JP/CJK
title collapsed to "". All of an artist's Japanese singles then normalised
identically, de-duplicated into one entry and "matched" each other — e.g.
inabakumori (稲葉曇) showed only 2 of 9 releases. Normalisers now keep
Unicode letters/numbers (\p{L}\p{N}) in:
- web _normAlbum (detail.js)
- android normAlbum (categorizeAlbums), normTrack (partial-album split),
the enrichPartialCounts helpers, and the Deezer artist-name match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Downloads started from global search / Bibliothek now poll via the
app-lifetime NotificationStore, so they show success/failure like the
importer and artist page did (was a silent toast only).
- Swipe between tabs, synced with the tab bar, matching Erkunden:
* Now Playing: Player <-> Lyrics <-> Queue (cover's skip-track swipe
still wins as a child gesture).
* Bibliothek: Playlists <-> Alben <-> Künstler <-> Offline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Importer: each resolved track gets a play/stop button that plays its
Deezer/Spotify preview via the app-level preview bar (pauses library
playback, highlights the row). Parity with the web importer.
- Artist "Unvollständig" now works: Deezer's /artist/{id}/albums omits
nb_tracks, so owned albums never registered as partial. The artist VM
now resolves the real total for matched albums via a per-album lookup
(like web), so the partial section + downloaded/missing split populate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>