diora-web/static/js/app/16-stations.js
mrwnslz 656520fe8e
Some checks failed
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 8m23s
Test / e2e (push) Failing after 1m54s
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.

Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00

173 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
// ---------------------------------------------------------------------------
// Mood / genre tag filter
// ---------------------------------------------------------------------------
const MOOD_TAGS = [
{ label: 'Fokus', tag: 'ambient' },
{ label: 'Lo-fi', tag: 'lofi' },
{ label: 'Jazz', tag: 'jazz' },
{ label: 'Klassik', tag: 'classical' },
{ label: 'Ambient', tag: 'ambient' },
{ label: 'Metal', tag: 'metal' },
{ label: 'Elektronisch', tag: 'electronic' },
{ label: 'Wort', tag: 'talk' },
];
function initMoodChips() {
const container = $('mood-chips');
if (!container) return;
MOOD_TAGS.forEach(({ label, tag }) => {
const btn = document.createElement('button');
btn.className = 'mood-chip';
btn.textContent = label;
btn.onclick = () => {
const input = $('search-input');
if (input) input.value = tag;
doSearch();
};
container.appendChild(btn);
});
}
// ---------------------------------------------------------------------------
// Curated station lists
// ---------------------------------------------------------------------------
const CURATED_LISTS = [
{
id: 'focus',
label: 'Fokus',
stations: [
{ name: 'SomaFM Drone Zone', url: 'https://ice6.somafm.com/dronezone-256-mp3' },
{ name: 'SomaFM Groove Salad', url: 'https://ice5.somafm.com/groovesalad-128-aac' },
{ name: 'Nightride FM', url: 'https://stream.nightride.fm/nightride.mp3' },
{ name: 'Nightride FM Chillsynth', url: 'https://stream.nightride.fm/chillsynth.mp3' },
],
},
{
id: 'lofi',
label: 'Lo-fi / Chill',
stations: [
{ name: 'SomaFM Groove Salad Classic', url: 'https://ice6.somafm.com/gsclassic-128-mp3' },
{ name: 'SomaFM Secret Agent', url: 'https://ice4.somafm.com/secretagent-128-mp3' },
{ name: 'dublab DE', url: 'https://dublabde.out.airtime.pro/dublabde_a' },
],
},
{
id: 'dark',
label: 'Dark / Industrial',
stations: [
{ name: 'SomaFM Doomed', url: 'https://ice2.somafm.com/doomed-256-mp3' },
{ name: 'Nightride FM Darksynth', url: 'https://stream.nightride.fm/darksynth.mp3' },
{ name: 'Radio Caprice Industrial', url: 'http://79.120.39.202:9095/' },
],
},
{
id: 'classical',
label: 'Klassik',
stations: [
{ name: 'BR Klassik', url: 'https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high' },
{ name: 'SWR Kultur', url: 'https://f111.rndfnk.com/ard/swr/swr2/live/mp3/256/stream.mp3?aggregator=web' },
{ name: 'Deutschlandfunk Kultur', url: 'https://st02.sslstream.dlf.de/dlf/02/high/aac/stream.aac?aggregator=web' },
],
},
];
function initCuratedLists() {
const container = document.getElementById('curated-lists');
if (!container) return;
if (INITIAL_FEATURED && INITIAL_FEATURED.length) {
const section = document.createElement('div');
section.className = 'curated-section';
section.innerHTML = `<div class="curated-label">&#9733; Empfohlen</div>`;
const ul = document.createElement('ul');
ul.className = 'curated-stations';
INITIAL_FEATURED.forEach(s => {
const li = document.createElement('li');
li.innerHTML = `<button class="btn btn-sm" onclick="playStation('${escapeAttr(s.url)}', '${escapeAttr(s.name)}', null)">▶</button>
<span class="curated-name">${escapeHtml(s.name)}</span>
${s.description ? `<span class="muted" style="font-size:0.78rem">${escapeHtml(s.description)}</span>` : ''}`;
ul.appendChild(li);
});
section.appendChild(ul);
container.appendChild(section);
}
CURATED_LISTS.forEach(list => {
const section = document.createElement('div');
section.className = 'curated-section';
section.innerHTML = `<div class="curated-label">${list.label}</div>`;
const ul = document.createElement('ul');
ul.className = 'curated-stations';
list.stations.forEach(s => {
const li = document.createElement('li');
li.innerHTML = `<button class="btn btn-sm" onclick="playStation('${escapeAttr(s.url)}', '${escapeAttr(s.name)}', null)">▶</button>
<span class="curated-name">${escapeHtml(s.name)}</span>`;
ul.appendChild(li);
});
section.appendChild(ul);
container.appendChild(section);
});
}
// ---------------------------------------------------------------------------
// Donation hint
// ---------------------------------------------------------------------------
const DONATION_HINT_THRESHOLD = 10;
const DONATION_HINT_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
function maybeShowDonationHint(stationUrl, stationName) {
const station = INITIAL_SAVED.find(s => s.url === stationUrl);
if (!station || station.play_count < DONATION_HINT_THRESHOLD) return;
const key = `diora_donation_hint_${stationUrl}`;
const last = parseInt(localStorage.getItem(key) || '0', 10);
if (Date.now() - last < DONATION_HINT_COOLDOWN_MS) return;
// Past the threshold just means "this station is a regular" — from then on
// every single play would otherwise qualify, so only actually show the hint
// on roughly 1 in 10 of those plays instead of every time.
if (Math.random() >= 0.1) return;
const existing = document.getElementById('donation-hint');
if (existing) existing.remove();
const el = document.createElement('div');
el.id = 'donation-hint';
el.innerHTML = `
<span>Du hörst <strong>${escapeHtml(stationName)}</strong> oft – vielleicht magst du den Sender unterstützen ♥</span>
<button onclick="dismissDonationHint('${escapeAttr(stationUrl)}')" title="Ausblenden">✕</button>
`;
document.body.appendChild(el);
setTimeout(() => dismissDonationHint(stationUrl), 12000);
}
function dismissDonationHint(stationUrl) {
localStorage.setItem(`diora_donation_hint_${stationUrl}`, Date.now());
const el = document.getElementById('donation-hint');
if (el) { el.classList.add('hiding'); setTimeout(() => el.remove(), 400); }
}
// ---------------------------------------------------------------------------
// Station notes
// ---------------------------------------------------------------------------
async function editNotes(pk, current) {
const note = await customPrompt('Notiz zum Sender:', current || '');
if (note === null) return; // cancelled
fetch(`/radio/notes/${pk}/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() },
body: JSON.stringify({ notes: note }),
}).then(r => {
if (r.ok) {
const cell = document.querySelector(`#saved-row-${pk} .notes-cell`);
if (cell) cell.textContent = note;
}
});
}