feat(web+android): Musik-Match wird zum Duell mit Anzeigetafel
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>
This commit is contained in:
1 parent
5a6cff8105
commit
91d0d5fd0b
7 files changed
+218
-81
No files matched your search
+12
-9
@@ -1085,16 +1085,19 @@ export function updateMediaSession(s) {
|
||||
{ src: _cachedCoverUrl(s.coverArt, 512), sizes:'512x512', type:'image/jpeg' },
|
||||
{ src: _cachedCoverUrl(s.coverArt, 1024), sizes:'1024x1024', type:'image/jpeg' },
|
||||
] : [];
|
||||
// Mutate the existing metadata in place rather than assigning a new MediaMetadata —
|
||||
// replacing the object makes Windows tear down & rebuild the media tile (visible
|
||||
// flicker when skipping). Spotify-style: keep one object, just update its fields.
|
||||
if (ms.metadata) {
|
||||
ms.metadata.title = s.title;
|
||||
ms.metadata.artist = s.artist || '';
|
||||
ms.metadata.album = s.album || '';
|
||||
ms.metadata.artwork = artwork;
|
||||
} else {
|
||||
// Feldweise mutieren statt ein neues MediaMetadata zuzuweisen vermeidet unter Windows das
|
||||
// Neuaufbauen der Medienkachel (sichtbares Flackern beim Skippen). Auf Linux kam davon bei MPRIS
|
||||
// aber nichts an: Desktop-Widgets zeigten dauerhaft den ZUERST gespielten Titel, weil nur die
|
||||
// erste Zuweisung gemeldet wurde. Deshalb jetzt zweigeteilt — bei echtem Titelwechsel ein neues
|
||||
// Objekt (garantiert gemeldet), sonst weiter in-place (z. B. wenn nur das Cover nachgeladen wird).
|
||||
const wechsel = !ms.metadata
|
||||
|| ms.metadata.title !== s.title
|
||||
|| ms.metadata.artist !== (s.artist || '')
|
||||
|| ms.metadata.album !== (s.album || '');
|
||||
if (wechsel) {
|
||||
ms.metadata = new MediaMetadata({ title:s.title, artist:s.artist||'', album:s.album||'', artwork });
|
||||
} else {
|
||||
ms.metadata.artwork = artwork;
|
||||
}
|
||||
// Action handlers are stable — set them once, not on every track change.
|
||||
if (_msHandlersSet) return;
|
||||
|
||||
+39
-34
@@ -35,7 +35,7 @@ function _buildOverlay() {
|
||||
_overlay.id = 'compare-overlay';
|
||||
_overlay.addEventListener('click', e => { if (e.target === _overlay) closeCompare(); });
|
||||
_overlay.innerHTML = `
|
||||
<div class="modal-card" style="max-width:720px;width:calc(100vw - 32px);max-height:88vh;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--border2);border-radius:18px;box-shadow:0 40px 80px #0009;overflow:hidden">
|
||||
<div class="modal-card" style="max-width:940px;width:calc(100vw - 32px);max-height:88vh;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--border2);border-radius:18px;box-shadow:0 40px 80px #0009;overflow:hidden">
|
||||
<div class="modal-head" style="display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid var(--border)">
|
||||
<span style="font-size:18px">🎧</span>
|
||||
<div style="font-size:17px;font-weight:800;letter-spacing:-.3px;flex:1">Musik-Match</div>
|
||||
@@ -126,35 +126,40 @@ async function _loadCompare() {
|
||||
}
|
||||
|
||||
function _renderCompare(body, d) {
|
||||
const score = Math.max(0, Math.min(100, d.matchScore || 0));
|
||||
const verdict =
|
||||
score >= 80 ? 'Seelenverwandte 🎯' :
|
||||
score >= 60 ? 'Starkes Match 🔥' :
|
||||
score >= 40 ? 'Solide Überschneidung 👍' :
|
||||
score >= 20 ? 'Ein paar Gemeinsamkeiten 🙂' :
|
||||
'Ziemlich unterschiedlich 🌗';
|
||||
const b = d.battle;
|
||||
const urteil = !b ? '' :
|
||||
b.punkteA === b.punkteB ? 'Kopf-an-Kopf — kein Sieger.' :
|
||||
b.punkteA > b.punkteB ? `${d.userA} gewinnt ${b.punkteA}:${b.punkteB}.` :
|
||||
`${d.userB} gewinnt ${b.punkteB}:${b.punkteA}.`;
|
||||
|
||||
body.innerHTML = [
|
||||
_scoreRing(d, score, verdict),
|
||||
_totals(d),
|
||||
_sharedArtists(d.sharedArtists),
|
||||
_sharedTracks(d.sharedTracks),
|
||||
_uniqueCols(d),
|
||||
].join('');
|
||||
body.innerHTML = `<div class="cmp-body">
|
||||
<div class="cmp-wide">${_scoreboard(d, b, urteil)}</div>
|
||||
<div class="cmp-wide">${_totals(d)}</div>
|
||||
<div>${_sharedArtists(d.sharedArtists)}</div>
|
||||
<div>${_sharedTracks(d.sharedTracks)}</div>
|
||||
<div class="cmp-wide">${_uniqueCols(d)}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _scoreRing(d, score, verdict) {
|
||||
const deg = Math.round(score * 3.6);
|
||||
return `<div style="display:flex;flex-direction:column;align-items:center;gap:10px;margin:6px 0 22px">
|
||||
<div style="width:132px;height:132px;border-radius:50%;background:conic-gradient(var(--accent) ${deg}deg, var(--surface2) ${deg}deg);display:flex;align-items:center;justify-content:center">
|
||||
<div style="width:104px;height:104px;border-radius:50%;background:var(--surface);display:flex;flex-direction:column;align-items:center;justify-content:center">
|
||||
<div style="font-size:32px;font-weight:800;letter-spacing:-1px;line-height:1">${score}<span style="font-size:16px">%</span></div>
|
||||
<div style="font-size:11px;color:var(--text2);margin-top:2px">Match</div>
|
||||
</div>
|
||||
// Anzeigetafel: pro Kategorie ein Sieger, unten der Gesamtstand. Ersetzt den frueheren
|
||||
// Prozentring — nicht "wie aehnlich seid ihr", sondern "wer gewinnt worin".
|
||||
function _scoreboard(d, b, urteil) {
|
||||
if (!b?.kategorien?.length) return '';
|
||||
const zeilen = b.kategorien.map(k => {
|
||||
const gewinnA = k.sieger === 'A', gewinnB = k.sieger === 'B';
|
||||
return `<div class="cmp-cat ${gewinnA ? 'win-a' : gewinnB ? 'win-b' : ''}">
|
||||
<div class="cmp-val a ${gewinnA ? 'win' : gewinnB ? 'lose' : ''}">${esc(k.textA)}</div>
|
||||
<div class="cmp-cat-label">${esc(k.label)}</div>
|
||||
<div class="cmp-val b ${gewinnB ? 'win' : gewinnA ? 'lose' : ''}">${esc(k.textB)}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `<div class="cmp-head">
|
||||
<div class="cmp-name">${esc(d.userA)}</div>
|
||||
<div class="cmp-score">${b.punkteA} : ${b.punkteB}</div>
|
||||
<div class="cmp-name">${esc(d.userB)}</div>
|
||||
</div>
|
||||
<div style="font-size:15px;font-weight:700">${esc(verdict)}</div>
|
||||
<div style="font-size:13px;color:var(--text2)"><b>${esc(d.userA)}</b> ⟷ <b>${esc(d.userB)}</b></div>
|
||||
</div>`;
|
||||
<div class="cmp-verdict">${esc(urteil)}</div>
|
||||
${zeilen}`;
|
||||
}
|
||||
|
||||
function _totals(d) {
|
||||
@@ -175,17 +180,17 @@ function _h(t) {
|
||||
function _sharedArtists(items) {
|
||||
if (!items?.length) return `${_h('Gemeinsame Künstler')}${_info('Keine gemeinsamen Künstler in diesem Zeitraum.')}`;
|
||||
const max = Math.max(...items.map(a => Math.max(a.playsA, a.playsB)), 1);
|
||||
const rows = items.slice(0, 15).map(a => {
|
||||
const rows = items.slice(0, 12).map(a => {
|
||||
const nav = a.artistId ? `data-nav-type="artist" data-nav-id="${esc(a.artistId)}" data-nav-name="${esc(a.name)}" style="cursor:pointer"` : '';
|
||||
return `<div ${nav} style="display:flex;align-items:center;gap:10px;padding:7px 0">
|
||||
<div style="width:34px;text-align:right;font-size:12px;color:var(--text2);font-weight:700">${a.playsA}</div>
|
||||
<div style="flex:1;display:flex;justify-content:flex-end"><div style="height:6px;background:var(--accent);border-radius:3px;width:${Math.round((a.playsA / max) * 100)}%"></div></div>
|
||||
<div style="flex:1.6;text-align:center;font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0">${esc(a.name)}</div>
|
||||
<div style="flex:1;display:flex;justify-content:flex-start"><div style="height:6px;background:var(--accent-dark, var(--accent));opacity:.7;border-radius:3px;width:${Math.round((a.playsB / max) * 100)}%"></div></div>
|
||||
<div style="width:34px;text-align:left;font-size:12px;color:var(--text2);font-weight:700">${a.playsB}</div>
|
||||
return `<div ${nav} class="cmp-row">
|
||||
<div class="cmp-num" style="text-align:right">${a.playsA}</div>
|
||||
<div style="display:flex;justify-content:flex-end"><div class="cmp-bar" style="width:${Math.round((a.playsA / max) * 100)}%"></div></div>
|
||||
<div class="cmp-row-name">${esc(a.name)}</div>
|
||||
<div><div class="cmp-bar" style="width:${Math.round((a.playsB / max) * 100)}%;opacity:.65"></div></div>
|
||||
<div class="cmp-num">${a.playsB}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `${_h('Gemeinsame Künstler')}<div style="margin-bottom:22px">${rows}</div>`;
|
||||
return `${_h('Gemeinsame Künstler')}<div>${rows}</div>`;
|
||||
}
|
||||
|
||||
function _sharedTracks(items) {
|
||||
|
||||
Reference in new issue
Block a user