From 5f723ed1e86e0a03361369eaec18f6bc3bede9ed Mon Sep 17 00:00:00 2001 From: simonpipe Date: Fri, 19 Dec 2025 15:34:10 +0100 Subject: [PATCH] Upload files to "js" --- js/meta-handler.js | 112 +++++++++++++++++++++++++++++++++++++++++++ js/preset-manager.js | 97 +++++++++++++++++++++++++++++++++++++ js/preset-ui.js | 81 +++++++++++++++++++++++++++++++ js/sleeptimer.js | 57 ++++++++++++++++++++++ js/ui-helper.js | 56 ++++++++++++++++++++++ 5 files changed, 403 insertions(+) create mode 100644 js/meta-handler.js create mode 100644 js/preset-manager.js create mode 100644 js/preset-ui.js create mode 100644 js/sleeptimer.js create mode 100644 js/ui-helper.js diff --git a/js/meta-handler.js b/js/meta-handler.js new file mode 100644 index 0000000..3e57792 --- /dev/null +++ b/js/meta-handler.js @@ -0,0 +1,112 @@ +/** + * meta-handler.js + * Verwaltet das Abrufen und Anzeigen von Stream-Metadaten (Songtitel) + */ + +const MetaHandler = { + proxyPath: 'php/metadata.php', + updateInterval: 20000, + intervalId: null, + proxyActive: false, + + async init() { + try { + const response = await fetch(this.proxyPath, { method: 'HEAD' }); + this.proxyActive = response.ok; + } catch (e) { + this.proxyActive = false; + } + }, + + async fetchTitle(streamUrl) { + if (!this.proxyActive || !streamUrl) return; + try { + const res = await fetch(`${this.proxyPath}?url=${encodeURIComponent(streamUrl)}`); + const data = await res.json(); + if (data && data.title) { + this.updateUI(data.title); + } + } catch (e) { + console.error("Metadaten-Fehler:", e); + } + }, + + updateUI(title) { + const stationDisplay = document.getElementById('current-station-name'); + if (!stationDisplay) return; + + // 1. Alles vor dem Songtitel isolieren + let currentContent = stationDisplay.innerHTML; + let stationPart = currentContent.split(' ')[0].split(' ${title}`; + } + + // --- LAUFSCHRIFT-ÜBERPRÜFUNG --- + setTimeout(() => this.checkMarqueeOverflow(), 50); + }, + + checkMarqueeOverflow() { + const senderContainer = document.getElementById('aktueller-sender'); + const textElement = document.getElementById('current-station-name'); + + if (textElement && senderContainer) { + textElement.classList.remove('marquee-active'); + + // Wir messen die verfügbare Breite im Container abzüglich des Paddings + const availableWidth = senderContainer.clientWidth - 20; // 20px Abzug für das seitliche Padding (15px links/rechts) + + // Wenn der Text breiter ist als der verfügbare Platz innen + if (textElement.scrollWidth > availableWidth) { + textElement.classList.add('marquee-active'); + } + } + }, + + start(streamUrl) { + this.stop(); + this.fetchTitle(streamUrl); + + this.intervalId = setInterval(() => { + this.fetchTitle(streamUrl); + }, this.updateInterval); + }, + + stop() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + const stationDisplay = document.getElementById('current-station-name'); + if (stationDisplay) { + stationDisplay.classList.remove('marquee-active'); + // Hinweis: Der Inhalt wird beim Stoppen meist durch die main.js auf "Wähle einen Sender" zurückgesetzt + } + } +}; + +window.MetaHandler = MetaHandler; +MetaHandler.init(); \ No newline at end of file diff --git a/js/preset-manager.js b/js/preset-manager.js new file mode 100644 index 0000000..12cf775 --- /dev/null +++ b/js/preset-manager.js @@ -0,0 +1,97 @@ +// preset-manager.js - Verwaltung der Radio-Sender +window.PresetManager = { + presets: [], + + // Lädt Presets: URL-Hash hat Vorrang vor LocalStorage + async load(callback) { + // 1. Check: Ist ein Hash in der URL vorhanden? + const hash = window.location.hash.substring(1); + if (hash && hash.length > 30) { + const success = await this.importFromString(hash, false); // false = kein Reload + if (success) { + if (callback) callback(this.presets); + return; + } + } + + // 2. Check: LocalStorage laden + const stored = localStorage.getItem('presets'); + if (stored) { + try { + this.presets = JSON.parse(stored); + } catch (e) { + console.error("Fehler beim Parsen von LocalStorage", e); + this.presets = []; + } + } else { + // 3. Fallback: Leere Liste, wenn gar nichts gefunden wurde + this.presets = []; + } + + if (callback) callback(this.presets); + }, + + save() { + localStorage.setItem('presets', JSON.stringify(this.presets)); + }, + + // Generiert den Code und gibt ihn zurück + async generateExportCode(updateField = true) { + try { + if (!this.presets || this.presets.length === 0) return ""; + const compactData = this.presets.map(p => [p.name, p.url]); + const jsonString = JSON.stringify(compactData); + + const stream = new Blob([jsonString]).stream(); + const compressedStream = stream.pipeThrough(new CompressionStream('gzip')); + const response = new Response(compressedStream); + const buffer = await response.arrayBuffer(); + const base64String = btoa(String.fromCharCode(...new Uint8Array(buffer))); + + if (updateField) { + const exportField = document.getElementById('export-string-field'); + if (exportField) exportField.value = base64String; + } + return base64String; + } catch (e) { + console.error("Export-Fehler:", e); + return ""; + } + }, + + // Importiert von einem String + async importFromString(code, shouldReload = true) { + try { + const binary = atob(code); + const bytes = Uint8Array.from(binary, c => c.charCodeAt(0)); + const stream = new Blob([bytes]).stream(); + const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip')); + const response = new Response(decompressedStream); + const resultText = await response.text(); + + const importedArray = JSON.parse(resultText); + this.presets = importedArray.map(item => ({ name: item[0], url: item[1] })); + this.save(); + + if (shouldReload) { + // Seite ohne den langen Hash neu laden für eine saubere URL + location.href = window.location.origin + window.location.pathname; + } + return true; + } catch (e) { + console.error("Import-Fehler:", e); + return false; + } + }, + + exportToFile() { + const presetsJSON = JSON.stringify(this.presets, null, 2); + const blob = new Blob([presetsJSON], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'radio_presets.json'; + a.click(); + URL.revokeObjectURL(url); + } +}; \ No newline at end of file diff --git a/js/preset-ui.js b/js/preset-ui.js new file mode 100644 index 0000000..d49b954 --- /dev/null +++ b/js/preset-ui.js @@ -0,0 +1,81 @@ +// preset-ui.js +window.PresetUI = { + renderTable() { + const tableBody = document.querySelector('#sortable-tbody'); + if (!tableBody) return; + tableBody.innerHTML = ''; + + window.PresetManager.presets.forEach((preset, index) => { + const row = document.createElement('tr'); + row.setAttribute('data-index', index); + row.innerHTML = ` + ☰ + + + + `; + tableBody.appendChild(row); + }); + this.updateExportFields(); + }, + + async updateExportFields() { + const exportField = document.getElementById('export-string-field'); + if (!exportField || !window.PresetManager) return; + const dataString = await window.PresetManager.generateExportCode(false); + const baseUrl = window.location.origin + window.location.pathname; + exportField.value = baseUrl + '#' + dataString; + }, + + copyShareLink() { + const exportField = document.getElementById('export-string-field'); + if (!exportField || !exportField.value) return; + + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(exportField.value) + .catch(() => this.fallbackCopy(exportField)); + } else { + this.fallbackCopy(exportField); + } + }, + + fallbackCopy(field) { + field.select(); + field.setSelectionRange(0, 99999); + try { + document.execCommand('copy'); + } catch (err) { + window.UI.showAlert("Fehler", "Link bitte manuell kopieren."); + } + }, + + addPreset() { + // Hier Umlaute vermeiden oder Entities nutzen beim Default-Namen + window.PresetManager.presets.push({ name: 'Neuer Sender', url: '' }); + this.renderTable(); + }, + + deletePreset(index) { + // "Löschen" -> "Löschen" zur Sicherheit + window.UI.showConfirm("Löschen", "Sender wirklich entfernen?", (conf) => { + if (conf) { + window.PresetManager.presets.splice(index, 1); + this.renderTable(); + } + }); + }, + + changeName(i, v) { + window.PresetManager.presets[i].name = v; + this.updateExportFields(); + }, + + changeURL(i, v) { + window.PresetManager.presets[i].url = v; + this.updateExportFields(); + } +}; + +window.addPreset = () => window.PresetUI.addPreset(); +window.deletePreset = (i) => window.PresetUI.deletePreset(i); +window.copyShareLink = () => window.PresetUI.copyShareLink(); \ No newline at end of file diff --git a/js/sleeptimer.js b/js/sleeptimer.js new file mode 100644 index 0000000..5640cb1 --- /dev/null +++ b/js/sleeptimer.js @@ -0,0 +1,57 @@ +// sleeptimer.js - Verwaltet die automatische Abschaltung und Restzeitanzeige +window.SleepTimer = { + timeoutId: null, + endTime: null, + onExpiry: null, + + init(onExpiryCallback) { + this.onExpiry = onExpiryCallback; + }, + + setTimer(minutes) { + this.clear(); + const durationMs = minutes * 60000; + this.endTime = Date.now() + durationMs; + + this.timeoutId = setTimeout(() => { + if (this.onExpiry) this.onExpiry(); + this.clear(); + }, durationMs); + + this.updateButtonUI(); + }, + + clear() { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + this.endTime = null; + this.updateButtonUI(); + }, + + updateButtonUI() { + const btn = document.getElementById('open-sleep-timer-modal'); + if (!btn) return; + + if (this.endTime) { + const remaining = Math.max(0, Math.ceil((this.endTime - Date.now()) / 60000)); + // Anzeige mit Icon und Restzeit + btn.innerHTML = ` ${remaining} Min`; + + // Selbst-Aktualisierung alle 10 Sekunden (nur wenn Timer aktiv) + if (this.timeoutId) { + setTimeout(() => this.updateButtonUI(), 10000); + } + } else { + // Standard-Text wenn kein Timer läuft + btn.textContent = 'Sleeptimer'; + } + + // Den "Timer aus"-Button im Modal zeigen/verstecken + const offBtn = document.getElementById('off-sleep-timer-modal'); + if (offBtn) { + offBtn.style.display = this.timeoutId ? 'inline-block' : 'none'; + } + } +}; \ No newline at end of file diff --git a/js/ui-helper.js b/js/ui-helper.js new file mode 100644 index 0000000..be0ff81 --- /dev/null +++ b/js/ui-helper.js @@ -0,0 +1,56 @@ +// ui-helper.js - Zentrale Steuerung für Modals und allgemeine UI-Elemente +window.UI = { + openModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.classList.remove('hidden'); + modal.style.display = 'block'; + } + }, + + closeModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.classList.add('hidden'); + modal.style.display = 'none'; + } + }, + + showAlert(title, message) { + const modal = document.getElementById("custom-modal"); + document.getElementById("custom-modal-title").textContent = title; + document.getElementById("custom-modal-message").textContent = message; + document.getElementById("custom-modal-cancel").classList.add("hidden"); + modal.classList.remove("hidden"); + document.getElementById("custom-modal-ok").onclick = () => modal.classList.add("hidden"); + }, + + showConfirm(title, message, callback) { + const modal = document.getElementById("custom-modal"); + document.getElementById("custom-modal-title").textContent = title; + document.getElementById("custom-modal-message").textContent = message; + const cancelBtn = document.getElementById("custom-modal-cancel"); + cancelBtn.classList.remove("hidden"); + modal.classList.remove("hidden"); + + document.getElementById("custom-modal-ok").onclick = () => { + modal.classList.add("hidden"); + callback(true); + }; + cancelBtn.onclick = () => { + modal.classList.add("hidden"); + callback(false); + }; + }, + + updateDocumentTitle(stationName) { + document.title = stationName ? `${stationName} - Radioplayer` : "Radioplayer"; + }, + + initializeFooter() { + const yearEl = document.getElementById('current-year'); + const serverEl = document.getElementById('server-name'); + if (yearEl) yearEl.textContent = new Date().getFullYear(); + if (serverEl) serverEl.textContent = window.location.hostname; + } +}; \ No newline at end of file