Upload files to "js"

This commit is contained in:
simonpipe 2025-12-19 15:34:10 +01:00
parent 58dc15272a
commit 5f723ed1e8
5 changed files with 403 additions and 0 deletions

112
js/meta-handler.js Normal file
View file

@ -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('&nbsp;')[0].split(' <i class="fa-solid fa-music"')[0].trim();
// 2. Dubletten-Check & Fehler-Filter
const tempDiv = document.createElement("div");
tempDiv.innerHTML = stationPart;
const stationNameOnly = tempDiv.textContent.trim();
const cleanTitle = title.toLowerCase().trim();
const cleanStation = stationNameOnly.toLowerCase();
// --- NEU: Liste der unerwünschten Titel ---
const isErrorTitle = cleanTitle.includes('error code:') ||
cleanTitle.includes('internal server error') ||
cleanTitle.includes('500 error');
// Überprüfung auf Dubletten, Telefonnummern ODER Fehlermeldungen
if (cleanTitle === cleanStation ||
cleanTitle.includes(cleanStation) ||
title.includes('0848') ||
isErrorTitle) { // <--- Hier wird der Fehler-Filter angewendet
stationDisplay.innerHTML = stationPart; // Nur Sendername anzeigen
} else {
// 3. Anzeige neu zusammenbauen
stationDisplay.innerHTML = `${stationPart}&nbsp;&nbsp;<i class="fa-solid fa-music"></i> ${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();

97
js/preset-manager.js Normal file
View file

@ -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);
}
};

81
js/preset-ui.js Normal file
View file

@ -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 = `
<td class="drag-handle"></td>
<td><input type="text" value="${preset.name}" onchange="PresetUI.changeName(${index}, this.value)"></td>
<td><input type="text" value="${preset.url}" onchange="PresetUI.changeURL(${index}, this.value)"></td>
<td><button class="delete-btn" onclick="PresetUI.deletePreset(${index})"><i class="fas fa-trash-alt"></i></button></td>
`;
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&ouml;schen" zur Sicherheit
window.UI.showConfirm("L&ouml;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();

57
js/sleeptimer.js Normal file
View file

@ -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 = `<i class="fas fa-hourglass-half"></i> ${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';
}
}
};

56
js/ui-helper.js Normal file
View file

@ -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;
}
};