diff --git a/CHANGELOG.md b/CHANGELOG.md
index e147188..664d325 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 7096766..ec72995 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -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.
diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt
index 6df54be..1c3bcdc 100644
--- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt
+++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt
@@ -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",
diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt
index 9a46627..731e7bd 100644
--- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt
+++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt
@@ -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,
diff --git a/src/views/detail.js b/src/views/detail.js
index ed23f6f..df1c45d 100644
--- a/src/views/detail.js
+++ b/src/views/detail.js
@@ -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 ? `` : '