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>
173 lines
6.5 KiB
JavaScript
173 lines
6.5 KiB
JavaScript
'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">★ 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;
|
||
}
|
||
});
|
||
}
|