diff --git a/CHANGELOG.md b/CHANGELOG.md index 4211fe5..8aef6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,7 +81,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 und die Gesamtdauer, 1.24 die Konzerte. +**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, 1.24 die Konzerte, 1.25 sie ohne Zeitfenster. ### Added - **Konzerte deiner Künstler (Web + Android)** — die Zeitleiste hat einen zweiten Reiter: neben @@ -92,8 +92,12 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d - **Bewusst nicht in eine gemeinsame Liste gemischt.** Veröffentlichungen liegen hinter dir, Konzerte vor dir. Chronologisch gemischt stünde „heute" mittendrin und man wüsste nie, ob man vorwärts oder rückwärts liest. Deshalb läuft die Konzertliste **nach vorn**: das nächste - Konzert zuerst. Das Zeitfenster (90/30/365) gilt je Reiter getrennt — bei den Konzerten zeigt - es nach vorn, Vorgabe ein Jahr. + Konzert zuerst, nach Monat gruppiert wie bei den Veröffentlichungen. + - **Kein Zeitfenster bei den Konzerten.** Die Chips 90/30/365 waren dort zuerst mit dabei und + ergaben keinen Sinn: Ticketmaster liefert ohnehin nur Anstehendes, und bei Terminen will man + alles Angekündigte sehen — eine Tagesgrenze schneidet nur willkürlich ab. Die Chip-Reihe + erscheint jetzt nur noch beim Reiter *Neu erschienen*; der Endpunkt beachtet ein + mitgeschicktes Fenster weiterhin, verlangt es aber nicht mehr. - **Gegenprobe gegen die Stichwortsuche.** Ticketmaster wird über den Namen abgefragt (`keyword=`), nicht über `attractionId` — dieselbe Unschärfe wie bei der Deezer-Zuordnung. Jeder Treffer wird deshalb gegengeprüft: der Künstlername muss im Veranstaltungs- **oder** diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8e37601..6e2c8b7 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 = 27 - versionName = "1.24" + versionCode = 28 + versionName = "1.25" // 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/api/ReleasesClient.kt b/android/app/src/main/kotlin/com/novasonic/app/api/ReleasesClient.kt index 9e0980e..e80b6c2 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/api/ReleasesClient.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/api/ReleasesClient.kt @@ -40,11 +40,12 @@ object ReleasesClient { } /** Anstehende Konzerte derselben Kuenstler — gleicher Weg, anderer Endpunkt. */ - suspend fun konzerte(backendUrl: String, artists: List, days: Int): ArtistKonzerteResponse { + suspend fun konzerte(backendUrl: String, artists: List): ArtistKonzerteResponse { if (backendUrl.isBlank() || artists.isEmpty()) return ArtistKonzerteResponse() + // Ohne `days`: das Backend liefert alles Angekuendigte. return http.post("${backendUrl.trimEnd('/')}/api/artist-concerts") { contentType(ContentType.Application.Json) - setBody(ArtistKonzerteRequest(artists, days)) + setBody(ArtistKonzerteRequest(artists)) }.body() } } diff --git a/android/app/src/main/kotlin/com/novasonic/app/api/model/ReleaseModels.kt b/android/app/src/main/kotlin/com/novasonic/app/api/model/ReleaseModels.kt index 4fd9e59..400bfe0 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/api/model/ReleaseModels.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/api/model/ReleaseModels.kt @@ -56,4 +56,4 @@ data class ArtistKonzerteResponse( ) @Serializable -data class ArtistKonzerteRequest(val artists: List, val days: Int) +data class ArtistKonzerteRequest(val artists: List) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistNewsScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistNewsScreen.kt index d7c843f..bbafd44 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistNewsScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistNewsScreen.kt @@ -91,24 +91,28 @@ fun ArtistNewsScreen( } } - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 10.dp), - ) { - // 90 Tage zuerst, weil es die Vorgabe ist — der aktive Chip steht dann vorn. - // Bei den Konzerten zeigt dasselbe Fenster nach VORN, dort sind 1 Jahr die Vorgabe. - val aktuell = if (state.reiter == NewsReiter.KONZERTE) state.tageKonzerte else state.tage - listOf(90 to "90 Tage", 30 to "30 Tage", 365 to "1 Jahr").forEach { (tage, label) -> - FilterChip( - selected = aktuell == tage, - onClick = { vm.setzeTage(tage) }, - label = { Text(label, fontSize = 12.sp) }, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = NsAccent.copy(alpha = 0.2f), - selectedLabelColor = NsAccent, - ), - ) + // Das Zeitfenster gilt nur fuer die Veroeffentlichungen. Konzerte sind ohnehin Zukunft, + // dort will man alles Angekuendigte sehen — eine Tagesgrenze schnitte nur willkuerlich ab. + if (state.reiter == NewsReiter.RELEASES) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 10.dp), + ) { + // 90 Tage zuerst, weil es die Vorgabe ist — der aktive Chip steht dann vorn. + listOf(90 to "90 Tage", 30 to "30 Tage", 365 to "1 Jahr").forEach { (tage, label) -> + FilterChip( + selected = state.tage == tage, + onClick = { vm.setzeTage(tage) }, + label = { Text(label, fontSize = 12.sp) }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = NsAccent.copy(alpha = 0.2f), + selectedLabelColor = NsAccent, + ), + ) + } } + } else { + Spacer(Modifier.height(10.dp)) } HorizontalDivider(color = MaterialTheme.colorScheme.outline) @@ -125,7 +129,7 @@ fun ArtistNewsScreen( state.reiter == NewsReiter.KONZERTE && state.keinSchluessel -> Hinweis("🎟", "Kein Ticketmaster-Schlüssel hinterlegt", "Im Backend TICKETMASTER_API_KEY setzen.") state.reiter == NewsReiter.KONZERTE && state.konzerte.isEmpty() -> Hinweis("🎪", - "Keine Termine in den nächsten ${state.tageKonzerte} Tagen", + "Keine anstehenden Termine", "Gesucht wird weltweit über den Künstlernamen.") state.reiter == NewsReiter.KONZERTE -> LazyColumn(contentPadding = PaddingValues(bottom = 90.dp)) { var letzterMonat = "" diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistNewsViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistNewsViewModel.kt index 2c0003a..d5e85e7 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistNewsViewModel.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistNewsViewModel.kt @@ -26,8 +26,6 @@ data class ArtistNewsUiState( val reiter: NewsReiter = NewsReiter.RELEASES, val releases: List = emptyList(), val konzerte: List = emptyList(), - /** Zeitfenster der Konzerte — nach VORN, deshalb ein eigener Wert. */ - val tageKonzerte: Int = 365, val keinSchluessel: Boolean = false, /** Schlüssel (Künstler + Album, normalisiert) alles dessen, was schon in der Bibliothek liegt. */ val vorhanden: Set = emptySet(), @@ -59,13 +57,13 @@ class ArtistNewsViewModel(app: Application) : AndroidViewModel(app) { if (r == NewsReiter.KONZERTE && _state.value.konzerte.isEmpty()) ladeKonzerte() } - /** Zeitfenster setzen — es gilt je Reiter, weil es in verschiedene Richtungen zeigt. */ - fun setzeTage(tage: Int) { - if (_state.value.reiter == NewsReiter.KONZERTE) ladeKonzerte(tage) else laden(tage) - } + /** Zeitfenster setzen — gibt es nur bei den Veroeffentlichungen. */ + fun setzeTage(tage: Int) = laden(tage) - fun ladeKonzerte(tage: Int = _state.value.tageKonzerte) = viewModelScope.launch { - _state.value = _state.value.copy(isLoading = true, tageKonzerte = tage, error = null, + // Kein Zeitfenster: Ticketmaster liefert ohnehin nur Anstehendes, und dort will man alles + // Angekuendigte sehen — eine Tagesgrenze schnitte nur willkuerlich ab. + fun ladeKonzerte() = viewModelScope.launch { + _state.value = _state.value.copy(isLoading = true, error = null, keineGefolgten = false, keinBackend = false, keinSchluessel = false) try { val backendUrl = runCatching { repo.getBackendUrl() }.getOrDefault("") @@ -78,7 +76,7 @@ class ArtistNewsViewModel(app: Application) : AndroidViewModel(app) { _state.value = _state.value.copy(isLoading = false, keineGefolgten = true) return@launch } - val antwort = ReleasesClient.konzerte(backendUrl, kuenstler, tage) + val antwort = ReleasesClient.konzerte(backendUrl, kuenstler) _state.value = _state.value.copy( konzerte = antwort.konzerte, keinSchluessel = antwort.keinSchluessel, diff --git a/backend/lib/konzerte.js b/backend/lib/konzerte.js index 64c3dd3..c65fb8e 100644 --- a/backend/lib/konzerte.js +++ b/backend/lib/konzerte.js @@ -102,11 +102,13 @@ async function inHaeppchen(items, groesse, fn) { /** * @param {Array<{id,name}>} kuenstler Navidrome-Künstler * @param {string} apiKey Ticketmaster-Schlüssel - * @param {number} tage Zeitfenster nach vorn + * @param {number} tage Zeitfenster nach vorn; 0 = kein Fenster (Vorgabe) * @returns {{konzerte:Array, ohneTermine:Array}} */ -async function kommendeKonzerte(kuenstler, apiKey, tage = 365) { - const grenze = Date.now() + tage * 24 * 60 * 60 * 1000; +async function kommendeKonzerte(kuenstler, apiKey, tage = 0) { + // Kein Fenster als Vorgabe: Ticketmaster liefert ohnehin nur Anstehendes, und bei Konzerten + // will man ALLES sehen, was angekuendigt ist — eine Tagesgrenze schneidet nur willkuerlich ab. + const grenze = tage > 0 ? Date.now() + tage * 24 * 60 * 60 * 1000 : Infinity; const ohneTermine = []; const proKuenstler = await inHaeppchen(kuenstler, GLEICHZEITIG, async (k) => { diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 9be548e..5768851 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -265,7 +265,10 @@ app.post('/api/artist-concerts', async (req, res) => { if (!key) return res.json({ konzerte: [], ohneTermine: [], keinSchluessel: true }); const artists = Array.isArray(req.body?.artists) ? req.body.artists : []; - const days = Math.min(Math.max(parseInt(req.body?.days, 10) || 365, 1), 365); + // Ohne `days` gibt es KEIN Fenster — Konzerte sind ohnehin nur Zukunft, und dort will man + // alles Angekuendigte sehen. Ein mitgeschicktes Fenster wird weiterhin beachtet. + const roh = parseInt(req.body?.days, 10); + const days = Number.isFinite(roh) && roh > 0 ? Math.min(roh, 3650) : 0; if (!artists.length) return res.json({ konzerte: [], ohneTermine: [] }); // Obergrenze, damit ein kaputter Client nicht hunderte Ticketmaster-Aufrufe ausloest. if (artists.length > 300) return res.status(400).json({ error: 'Zu viele Künstler (max. 300)' }); diff --git a/src/views/artistNews.js b/src/views/artistNews.js index 8bf4aec..c13a7ee 100644 --- a/src/views/artistNews.js +++ b/src/views/artistNews.js @@ -11,7 +11,6 @@ import { navigate, _navGoBack } from './navigation.js'; // Die Deezer-Auflösung macht das Backend, weil dort auch die Künstler-Pins liegen. let _tage = 90; -let _tageKonzerte = 365; // Konzerte schaut man in die Weite an, nicht auf 90 Tage let _reiter = 'releases'; let _laeuft = false; @@ -26,14 +25,18 @@ export function showArtistNewsTab(reiter) { ? 'Konzerte deiner Künstler' : 'Neu von deinen Künstlern'; } + // Das Zeitfenster gilt nur fuer die Veroeffentlichungen. Konzerte sind ohnehin Zukunft, und + // dort will man alles Angekuendigte sehen — eine Tagesgrenze schnitte nur willkuerlich ab. + const tageReihe = document.getElementById('artist-news-sort'); + if (tageReihe) tageReihe.style.display = _reiter === 'konzerte' ? 'none' : ''; if (_reiter === 'konzerte') loadKonzerte(); else loadArtistNews(); } -/** Zeitfenster setzen — es gilt je Reiter, weil es in verschiedene Richtungen zeigt. */ +/** Zeitfenster setzen — gibt es nur bei den Veroeffentlichungen. */ export function setArtistNewsDays(tage) { - if (_reiter === 'konzerte') { _tageKonzerte = tage; loadKonzerte(); } - else { _tage = tage; loadArtistNews(); } + _tage = tage; + loadArtistNews(); } function _markiereTage(tage) { @@ -44,6 +47,8 @@ function _markiereTage(tage) { export async function loadArtistNews(tage = _tage) { _tage = tage; _reiter = 'releases'; + const tageReihe = document.getElementById('artist-news-sort'); + if (tageReihe) tageReihe.style.display = ''; // Die Seite haengt an den Favoriten (Knopf "✨ Neu") und nicht mehr an der Seitenleiste, // deshalb braucht sie einen Weg zurueck wie jede andere Unterseite. const zurueck = document.getElementById('artist-news-back-btn'); @@ -196,12 +201,12 @@ function _zeile(rel, schonDa) { // Gegenstück zur Release-Zeitleiste. Die Liste läuft NACH VORN: das nächste Konzert zuerst. // Die Termine kommen über das Backend von Ticketmaster, gecacht für alle Geräte gemeinsam. -export async function loadKonzerte(tage = _tageKonzerte) { - _tageKonzerte = tage; +export async function loadKonzerte() { _reiter = 'konzerte'; const zurueck = document.getElementById('artist-news-back-btn'); if (zurueck) zurueck.onclick = () => _navGoBack(); - _markiereTage(tage); + const tageReihe = document.getElementById('artist-news-sort'); + if (tageReihe) tageReihe.style.display = 'none'; const el = document.getElementById('artist-news-list'); if (!el || _laeuft) return; @@ -226,7 +231,8 @@ export async function loadKonzerte(tage = _tageKonzerte) { const res = await fetch(`${S.cfg.backendUrl}/api/artist-concerts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ artists: kuenstler, days: tage }), + // Ohne `days`: das Backend liefert alles Angekuendigte. + body: JSON.stringify({ artists: kuenstler }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { konzerte = [], keinSchluessel } = await res.json(); @@ -240,7 +246,7 @@ export async function loadKonzerte(tage = _tageKonzerte) { if (!konzerte.length) { const leer = document.createElement('div'); leer.className = 'empty-state'; - leer.innerHTML = `
🎪

Keine Termine in den nächsten ${tage} Tagen

` + leer.innerHTML = '
🎪

Keine anstehenden Termine

' + '

Gesucht wird weltweit über den Künstlernamen.

'; el.appendChild(leer); return;