feat(web+android): Gesamtdauer mit Stunden, in Android neu
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7fed82736
commit
37f8f8acac
7 files changed
+50
-7
No files matched your search
+11
-1
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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',
|
||||
|
||||
@@ -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 : '');
|
||||
|
||||
Reference in new issue
Block a user