diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3b36a..1f9784a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,7 +80,7 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d ## [3.1] — 2026-09-22 -**Android:** 1.15 (versionCode 18) → 1.23 (versionCode 26). 1.16 brachte den Rollbalken, 1.17 bis 1.21 machten ihn bedienbar, 1.19 das Bearbeiten der Warteschlange, 1.22 die Wischgeste, den Mischen-Fehler und die zwei neuen Designs, 1.23 den Honigtopf. +**Android:** 1.15 (versionCode 18) → 1.23 (versionCode 26). 1.16 brachte den Rollbalken, 1.17 bis 1.21 machten ihn bedienbar, 1.19 das Bearbeiten der Warteschlange, 1.22 die Wischgeste, den Mischen-Fehler und die zwei neuen Designs, 1.23 den Honigtopf und die Gesamtdauer. ### Added - **Zwei neue Designs: Violet und Crimson (Web + Android)** — ein helles Lila auf tiefem @@ -126,6 +126,16 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d ### Changed +- **Gesamtdauer mit Stunden (Web + Android)** — im Web stand bei Playlists und Alben nur eine + Minutenzahl; „134 Min." muss man selbst umrechnen, und bei einer Playlist mit 2000 Titeln steht + dort eine vierstellige Zahl. Ab einer Stunde heißt es jetzt „2 Std. 14 Min.", volle Stunden ohne + Anhängsel („2 Std."). **In Android fehlte die Dauer ganz** — Playlist und Album zeigen sie jetzt + ebenfalls. + - Kostet keinen zusätzlichen Aufruf: Subsonic liefert `duration` beim Playlist- und Album-Objekt + mit, in Android wird genau die genommen; im Web wird weiterhin über die geladenen Titel + summiert, weil die dort ohnehin schon vorliegen. + - Dieselbe Regel und dieselbe Schreibweise auf beiden Plattformen, damit dieselbe Playlist nicht + zwei verschiedene Angaben trägt. Neun Fälle geprüft (0, 59 Min., genau 1 Std., 100 Std., leer). - **Benachrichtigungen tragen einen Honigtopf mit Brief statt der Glocke (Web + Android)** — reine Strichzeichnung wie die übrigen Symbole, damit sie die Akzentfarbe annimmt, wenn etwas Ungelesenes anliegt. Im Web als Inline-SVG, in Android als Vektor-Drawable mit denselben diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/Dauer.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/Dauer.kt new file mode 100644 index 0000000..cfe2b09 --- /dev/null +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/Dauer.kt @@ -0,0 +1,16 @@ +package com.novasonic.app.ui + +/** + * Gesamtdauer einer Playlist oder eines Albums, in Sekunden hereingereicht. + * + * Ab einer Stunde werden Stunden ausgewiesen: "134 Min." muss man sonst selbst umrechnen, und bei + * einer Playlist mit 2000 Titeln staende dort eine vierstellige Minutenzahl. Gleiche Regel und + * gleiche Schreibweise wie `formatGesamtdauer` im Web (src/utils.js). + */ +fun formatGesamtdauer(sekunden: Int): String { + val min = Math.round((sekunden.coerceAtLeast(0)) / 60.0).toInt() + if (min < 60) return "$min Min." + val std = min / 60 + val rest = min % 60 + return if (rest > 0) "$std Std. $rest Min." else "$std Std." +} 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 a5f4c12..5752776 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 @@ -39,6 +39,7 @@ import com.novasonic.app.ui.component.SongRow import com.novasonic.app.ui.theme.* import com.novasonic.app.viewmodel.AlbumViewModel import com.novasonic.app.ui.component.Rollbalken +import com.novasonic.app.ui.formatGesamtdauer @Composable fun AlbumScreen( @@ -148,6 +149,7 @@ fun AlbumScreen( album.year?.toString(), album.genre?.takeIf { it.isNotBlank() }, if (album.songCount > 0) "${album.songCount} Songs" else null, + if (album.duration > 0) formatGesamtdauer(album.duration) else null, ).joinToString(" · ") if (meta.isNotBlank()) { Spacer(Modifier.height(3.dp)) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/PlaylistDetailScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/PlaylistDetailScreen.kt index ae82dbb..9a090f2 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/PlaylistDetailScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/PlaylistDetailScreen.kt @@ -41,6 +41,7 @@ import com.novasonic.app.ui.component.SongRow import com.novasonic.app.ui.theme.* import com.novasonic.app.viewmodel.PlaylistViewModel import com.novasonic.app.ui.component.Rollbalken +import com.novasonic.app.ui.formatGesamtdauer @Composable fun PlaylistDetailScreen( @@ -182,6 +183,9 @@ fun PlaylistDetailScreen( // Beschriftung — bei einer geteilten Liste ist genau das die interessante Angabe. val meta = listOfNotNull( if (pl.songCount > 0) "${pl.songCount} Songs" else null, + // Gesamtdauer: Subsonic liefert sie beim Playlist-Objekt mit, es kostet + // also keinen zusaetzlichen Aufruf. + if (pl.duration > 0) formatGesamtdauer(pl.duration) else null, pl.owner?.takeIf { it.isNotBlank() && it != state.currentUser }?.let { "von $it" }, ).joinToString(" · ") if (meta.isNotBlank()) { diff --git a/src/utils.js b/src/utils.js index 139538c..9a50de8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -47,6 +47,17 @@ export function formatDur(s) { return `${m}:${sec.toString().padStart(2,'0')}`; } +// Gesamtdauer einer Playlist oder eines Albums. Ab einer Stunde werden Stunden ausgewiesen — +// "134 Min." muss man sonst selbst umrechnen, und bei einer Playlist mit 2000 Titeln steht dort +// sonst eine vierstellige Minutenzahl. +export function formatGesamtdauer(sekunden) { + const min = Math.round((sekunden || 0) / 60); + if (min < 60) return `${min} Min.`; + const std = Math.floor(min / 60); + const rest = min % 60; + return rest ? `${std} Std. ${rest} Min.` : `${std} Std.`; +} + // Fan/count formatting in German: full grouped number up to a million (559534 → "559.542"), // then compact millions (1200000 → "1,2 Mio."). export function fmtCount(n) { diff --git a/src/views/detail.js b/src/views/detail.js index 4d6d511..f5aa74b 100644 --- a/src/views/detail.js +++ b/src/views/detail.js @@ -1,5 +1,5 @@ import { S } from '../state.js'; -import { esc, toast, fmtCount } from '../utils.js'; +import { esc, toast, fmtCount, formatGesamtdauer } from '../utils.js'; import { api, invalidateApiCache, coverUrl, deezerSearch, deezerLookupAlbum, deezerLookupArtistSongs, deezerLookupArtistAlbums, deezerArtistImage, deezerRelatedArtists, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap, getArtistOverride } from '../api.js'; import { openArtistPin } from '../ui/artistPin.js'; import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js'; @@ -671,8 +671,8 @@ export async function loadAlbumDetail(album) { try { const r = await api('getAlbum', `&id=${album.id}`); const songs = r.album?.song || []; - const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60); - document.getElementById('album-detail-stats').textContent = `${songs.length} Titel · ${r.album.year||''} · ${totalMin} Min.`; + const gesamt = songs.reduce((s,t) => s+(t.duration||0), 0); + document.getElementById('album-detail-stats').textContent = `${songs.length} Titel · ${r.album.year||''} · ${formatGesamtdauer(gesamt)}`; _verdrahteMerken( document.getElementById('btn-star-album'), album.id, r.album?.starred, 'Gemerkt', 'Merken', '❤ Album gemerkt', 'Aus den Favoriten entfernt', diff --git a/src/views/playlists.js b/src/views/playlists.js index 93ddcd5..a64984a 100644 --- a/src/views/playlists.js +++ b/src/views/playlists.js @@ -1,5 +1,5 @@ import { S } from '../state.js'; -import { esc, toast } from '../utils.js'; +import { esc, toast, formatGesamtdauer } from '../utils.js'; import { api, apiCached, apiIsCached, coverUrl, romajiLookup, dispArtistById, getArtistOverride } from '../api.js'; import { makeCard, makeAlbumCard, makeSongItem, mkSection } from '../ui/components.js'; import { playQueue } from '../player/player.js'; @@ -62,10 +62,10 @@ export async function loadPlaylistDetail(pl) { const full = r.playlist || {}; S._currentPlaylist = {...pl, ...full, id: pl.id}; const songs = full.entry || []; - const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60); + const gesamt = songs.reduce((s,t) => s+(t.duration||0), 0); const von = _besitzer({ ...pl, ...full }); document.getElementById('playlist-detail-stats').textContent = - `${songs.length} Titel · ${totalMin} Min.` + (von ? ` · von ${von}` : ''); + `${songs.length} Titel · ${formatGesamtdauer(gesamt)}` + (von ? ` · von ${von}` : ''); const overviewCard = document.querySelector(`#playlists-grid [data-pl-id="${pl.id}"]`); if (overviewCard) overviewCard.querySelector('.card-sub').textContent = songs.length + ' Titel' + (von ? ' · von ' + von : '');