feat(web+android): vom Album in der globalen Suche zum Künstler springen (2.5 / Android 1.8)
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>
This commit is contained in:
1 parent
fca7cc79c1
commit
ec75fdef7b
7 files changed
+90
-10
No files matched your search
@@ -39,6 +39,7 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an
|
||||
| Musik-Duell (Anzeigetafel) | ✅ | ✅ |
|
||||
| Wiedergabe fortsetzen | ✅ | ✅ |
|
||||
| Teilen per Link | ✅ | ✅ |
|
||||
| Album → Künstler (Deezer-Suche) | ✅ | ✅ |
|
||||
| Playlist aus Import anlegen | ✅ | ✅ |
|
||||
| Song von Festplatte löschen | ✅ | ✅ |
|
||||
| Metadata editor (tags + cover) | ✅ | ❌ |
|
||||
@@ -69,6 +70,33 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an
|
||||
|
||||
---
|
||||
|
||||
## [2.5] — 2026-09-10
|
||||
|
||||
### Added
|
||||
- **Android 1.8 (versionCode 11)** — enthält den Sprung vom Album zum Künstler (siehe unten).
|
||||
- **Vom Album zum Künstler springen (Web + Android)** — in der globalen Suche führte ein Album
|
||||
bisher in eine Sackgasse: der Künstlername darunter war reiner Text. Wer von dort zum Künstler
|
||||
wollte, musste zurück und neu suchen. Der Name ist jetzt anklickbar und öffnet dieselbe
|
||||
Deezer-Künstlerseite wie eine Künstler-Kachel. Betrifft im Web die Inline-Albumansicht in der
|
||||
globalen Suche und die Vollbild-Albumseite, in Android die Album-Detailansicht in Erkunden —
|
||||
dort stand vorher **gar kein** Künstler, nur der Albumtitel.
|
||||
- **Kostet keinen zusätzlichen API-Aufruf.** Deezer legt `/search/album` und `/album/{id}` das
|
||||
vollständige `artist`-Objekt samt `id` bei (in 120 von 120 Treffern über zwölf Suchen geprüft);
|
||||
beide Mappings (`_dzAlbum` im Web, `DzAlbum.toItunesItem()` in Android) übernahmen die id schon,
|
||||
sie wurde nur nirgends benutzt.
|
||||
- **Ausnahme mit Absicht:** `/artist/{id}/albums` lässt das `artist`-Objekt weg. Alben aus dieser
|
||||
Liste haben keine id, dort bleibt der Name deshalb reiner Text statt ins Leere zu zeigen — man
|
||||
kommt ja gerade von der Künstlerseite. Ein Ersatz-Lookup wäre ein Aufruf für einen Sprung, den
|
||||
niemand macht.
|
||||
- Deezer nennt nur den **Haupt**künstler: bei „A feat. B" landet man auf A.
|
||||
- Android lädt Bild und Fanzahl nach, wenn der Künstler aus einem Album konstruiert wurde — sonst
|
||||
sähe die Kopfzeile anders aus als beim Weg über die Künstlersuche.
|
||||
- Nebenbei: `.album-artist-name` trug `cursor:pointer` unabhängig davon, ob der Name irgendwohin
|
||||
führte — der Zeiger versprach auf der Deezer- und der Teil-Album-Seite einen Klick, den es nicht
|
||||
gab. Jetzt nur noch dort, wo wirklich ein Ziel dahintersteht.
|
||||
|
||||
---
|
||||
|
||||
## [2.4] — 2026-09-06
|
||||
|
||||
### Added
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.novasonic.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 10
|
||||
versionName = "1.7"
|
||||
versionCode = 11
|
||||
versionName = "1.8"
|
||||
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
|
||||
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
|
||||
// Platzhalter stehen und der App-Link greift schlicht nicht.
|
||||
|
||||
@@ -126,6 +126,7 @@ fun ExploreScreen(
|
||||
previewingId = state.previewingId,
|
||||
onPreview = vm::togglePreview,
|
||||
onDownload = vm::showDownloadSheet,
|
||||
onArtistClick = vm::openArtistOfAlbum,
|
||||
onBack = vm::dismissAlbum,
|
||||
)
|
||||
}
|
||||
@@ -407,6 +408,7 @@ private fun GlobalExploreContent(
|
||||
previewingId = state.previewingId,
|
||||
onPreview = vm::togglePreview,
|
||||
onDownload = onDownload,
|
||||
onArtistClick = vm::openArtistOfAlbum,
|
||||
onBack = vm::dismissAlbum,
|
||||
)
|
||||
return
|
||||
@@ -524,6 +526,7 @@ internal fun ItunesAlbumDetail(
|
||||
previewingId: Long?,
|
||||
onPreview: (ItunesItem) -> Unit,
|
||||
onDownload: (ItunesItem) -> Unit = {},
|
||||
onArtistClick: ((ItunesItem) -> Unit)? = null,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
@@ -535,9 +538,25 @@ internal fun ItunesAlbumDetail(
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Zurück",
|
||||
tint = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
Text(title, fontFamily = Outfit, fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f))
|
||||
// Anklickbar nur, wenn Deezer eine Künstler-id mitgeliefert hat — /artist/{id}/albums
|
||||
// lässt das artist-Objekt weg, dort bleibt der Name einfach Text.
|
||||
val artistName = albumItem?.artistName
|
||||
val artistTippbar = onArtistClick != null && albumItem?.artistId != null &&
|
||||
!artistName.isNullOrBlank()
|
||||
Column(
|
||||
Modifier.weight(1f).then(
|
||||
if (artistTippbar) Modifier.clickable { onArtistClick!!(albumItem!!) } else Modifier
|
||||
)
|
||||
) {
|
||||
Text(title, fontFamily = Outfit, fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurface)
|
||||
if (!artistName.isNullOrBlank()) {
|
||||
Text(artistName, fontFamily = Outfit, fontSize = 12.sp,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = if (artistTippbar) NsAccent else NsOnSurface2)
|
||||
}
|
||||
}
|
||||
if (albumItem != null) {
|
||||
IconButton(onClick = { onDownload(albumItem) }) {
|
||||
Icon(Icons.Default.Download, "Album herunterladen",
|
||||
|
||||
@@ -180,6 +180,16 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) {
|
||||
_state.value = _state.value.copy(albumTracks = null, albumItem = null)
|
||||
}
|
||||
|
||||
/** Vom geöffneten Album zu dessen Künstler springen. Deezer legt das artist-Objekt samt id
|
||||
* jedem Albumtreffer bei, der Sprung kostet also keinen zusätzlichen Suchaufruf. Die
|
||||
* Albumansicht wird geschlossen, sonst läge sie weiter über der Künstlerseite. */
|
||||
fun openArtistOfAlbum(album: ItunesItem) {
|
||||
val id = album.artistId ?: return
|
||||
val name = album.artistName ?: return
|
||||
_state.value = _state.value.copy(albumTracks = null, albumItem = null, partialAlbum = null)
|
||||
openArtistDetail(ItunesItem(wrapperType = "artist", artistId = id, artistName = name))
|
||||
}
|
||||
|
||||
fun openArtistAllAlbums(title: String) { _state.value = _state.value.copy(artistAllAlbums = title) }
|
||||
fun closeArtistAllAlbums() { _state.value = _state.value.copy(artistAllAlbums = null) }
|
||||
|
||||
@@ -199,9 +209,14 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
val ndAlbums = ndAlbumsD.await()
|
||||
val iAlbums = iAlbumsD.await()
|
||||
// Aus einem Album konstruierte Künstler tragen weder Bild noch Fanzahl — einmal
|
||||
// nachschlagen, damit die Kopfzeile genauso aussieht wie aus der Künstlersuche.
|
||||
val voll = if (artistId != null && artist.artworkUrl100 == null && artist.fanCount == null)
|
||||
runCatching { ItunesClient.lookupArtist(artistId) }.getOrNull() ?: artist
|
||||
else artist
|
||||
_state.value = _state.value.copy(
|
||||
artistDetail = ItunesArtistState(
|
||||
artist = artist,
|
||||
artist = voll,
|
||||
isLoading = false,
|
||||
ndAlbums = ndAlbums,
|
||||
iAlbums = iAlbums,
|
||||
|
||||
+11
-1
@@ -482,7 +482,17 @@ export async function loadArtistAllSongs({ albums, artist }) {
|
||||
export async function loadDeezerAlbumDetail(album) {
|
||||
document.getElementById('deezer-album-back-btn').onclick = () => _navGoBack();
|
||||
document.getElementById('deezer-album-detail-name').textContent = album.collectionName || '—';
|
||||
document.getElementById('deezer-album-detail-artist').textContent = album.artistName || '';
|
||||
// Deezer liefert die Künstler-ID in jedem Albumtreffer gleich mit — ohne sie ist der Name nur
|
||||
// toter Text (z. B. bei Alben aus /artist/{id}/albums, wo Deezer das artist-Objekt weglässt).
|
||||
const dzArtistEl = document.getElementById('deezer-album-detail-artist');
|
||||
dzArtistEl.textContent = album.artistName || '';
|
||||
dzArtistEl.onclick = null;
|
||||
dzArtistEl.classList.toggle('is-link', !!(album.artistId && album.artistName));
|
||||
if (album.artistId && album.artistName) {
|
||||
dzArtistEl.onclick = () => navigate('artist-detail', {
|
||||
id: null, name: album.artistName, deezerArtistId: album.artistId, deezerFanCount: null,
|
||||
});
|
||||
}
|
||||
const cw = document.getElementById('deezer-album-cover-wrap');
|
||||
const art = (album.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600');
|
||||
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
|
||||
|
||||
+7
-1
@@ -206,7 +206,7 @@ export async function loadDeezerAlbum(album) {
|
||||
<button class="back-btn" style="margin:0" onclick="document.getElementById('global-search-input').dispatchEvent(new Event('input'))"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Zurück</button>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:15px;font-weight:700">${esc(album.collectionName)}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">${esc(album.artistName)}</div>
|
||||
<div style="font-size:12px;color:var(--text2)"${album.artistId ? ' class="song-link" id="dz-alb-artist"' : ''}>${esc(album.artistName)}</div>
|
||||
</div>
|
||||
<button class="btn-outline blue" style="font-size:12px;padding:7px 12px" onclick="openDlModal({artistName:'${esc(album.artistName)}',trackName:'${esc(album.collectionName)}',artworkUrl100:'${album.artworkUrl100||''}',collectionName:'${esc(album.collectionName)}',isAlbum:true,deezerData:${JSON.stringify(album).replace(/"/g,'"')}})">⬇ Album laden</button>
|
||||
</div>
|
||||
@@ -215,6 +215,12 @@ export async function loadDeezerAlbum(album) {
|
||||
tracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, tracks)));
|
||||
s.appendChild(list);
|
||||
el.innerHTML = ''; el.appendChild(s);
|
||||
// Deezer hängt an jeden Albumtreffer das Artist-Objekt mit id — ein Sprung zum Künstler
|
||||
// kostet also keinen zusätzlichen Aufruf.
|
||||
if (album.artistId) {
|
||||
s.querySelector('#dz-alb-artist')?.addEventListener('click', () =>
|
||||
loadDeezerArtistResults(album.artistId, album.artistName, null));
|
||||
}
|
||||
} catch(e) { toast('Fehler: ' + e.message); }
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -274,8 +274,10 @@ button:focus{outline:none}
|
||||
.album-cover .no-art{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:50px}
|
||||
.album-type{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--text2);margin-bottom:5px}
|
||||
.album-name{font-size:28px;font-weight:800;letter-spacing:-1px;margin-bottom:5px;line-height:1.1}
|
||||
.album-artist-name{font-size:14px;color:var(--text2);margin-bottom:10px;cursor:pointer}
|
||||
.album-artist-name:hover{color:var(--accent)}
|
||||
.album-artist-name{font-size:14px;color:var(--text2);margin-bottom:10px}
|
||||
/* Zeiger nur, wo der Name wirklich irgendwohin führt (lokal: .song-link-Spans, Deezer: .is-link). */
|
||||
.album-artist-name .song-link:hover,.album-artist-name.is-link:hover{color:var(--accent)}
|
||||
.album-artist-name.is-link{cursor:pointer;transition:color .15s}
|
||||
.album-stats{font-size:12px;color:var(--text3)}
|
||||
.playlist-detail-desc{font-size:13px;color:var(--text2);line-height:1.5;margin-top:6px;max-width:480px}
|
||||
.album-actions{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
|
||||
|
||||
Reference in new issue
Block a user