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 = ` +