Upload files to "js"
This commit is contained in:
parent
58dc15272a
commit
5f723ed1e8
5 changed files with 403 additions and 0 deletions
112
js/meta-handler.js
Normal file
112
js/meta-handler.js
Normal 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(' ')[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} <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
97
js/preset-manager.js
Normal 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
81
js/preset-ui.js
Normal 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ö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();
|
||||||
57
js/sleeptimer.js
Normal file
57
js/sleeptimer.js
Normal 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
56
js/ui-helper.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue