Upload files to "/"

This commit is contained in:
simonpipe 2025-09-22 16:53:26 +02:00
parent 31e045c756
commit 49ff62adda
3 changed files with 540 additions and 0 deletions

85
index.html Normal file
View file

@ -0,0 +1,85 @@
<!-- Public Domain -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Radioplayer mit Presets</title>
<link rel="stylesheet" href="style.css">
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.14.0/Sortable.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.min.js"></script>
<script defer src="script.js"></script>
</head>
<body>
<div class="header" role="banner">
<div class="titel">
<img src="/img/icon.png" alt="Logo des Radioplayers, das einem alten Kofferradio nachempfunden ist" height="35" width="35">
<h1>Radioplayer <button class="stop-button hidden" aria-label="Wiedergabe stoppen"><i class="fas fa-stop"></i></button></h1>
</div>
<div class="current-station" aria-live="polite">
<span id="aktueller-sender"><span id="current-station-name">Wähle einen Sender aus</span></span>
</div>
</div>
<div class="preset-container" id="preset-container" role="list"></div>
<div class="button-container">
<button id="open-sleep-timer-modal">Sleeptimer</button>
<button id="edit-presets-button" class="edit-presets-button" aria-label="Sender bearbeiten" aria-pressed="false">Presets bearbeiten</button>
</div>
<footer>
<div class="footer" role="contentinfo">
&copy; 2024-<span id="current-year"></span>&nbsp;<img src="/img/favicon-16x16.png" alt="Radioplayer" height="14" width="14">&nbsp;<span id="server-name"></span>&nbsp;//&nbsp;View on&nbsp;<a target="_blank" href="https://codeberg.org/simonpipe/Radioplayer-with-Presets" aria-label="Codeberg-Seite öffnen">&nbsp;<img src="/img/codeberg_16px.ico" alt="Codeberg" height="12" width="12"> Codeberg</a>
</div>
</footer>
<!-- Modal für Sleep Timer -->
<div id="sleep-timer-modal" class="modal hidden">
<div class="modal-content">
<h2>Sleeptimer einstellen</h2>
<div class="button-container">
<button class="time-option" data-time="600000">10 Minuten</button>
<button class="time-option" data-time="900000">15 Minuten</button>
<button class="time-option" data-time="1200000">20 Minuten</button>
<button class="time-option" data-time="1800000">30 Minuten</button>
<button class="time-option" data-time="3600000">60 Minuten</button>
</div>
<div class="button-container">
<button id="off-sleep-timer-modal">Timer aus</button>
<button id="close-sleep-timer-modal">Abbrechen</button>
</div>
</div>
</div>
<!-- Modal für die Bearbeitung der Presets -->
<div id="preset-modal" class="modal hidden">
<div class="modal-content">
<h2>Sender bearbeiten</h2>
<table id="preset-table">
<thead>
<tr>
<th>Sortieren</th>
<th>Name</th>
<th>URL</th>
<th>Löschen</th>
</tr>
</thead>
<tbody id="sortable-tbody">
<!-- Presets werden hier dynamisch eingefügt -->
</tbody>
</table>
<div class="button-container">
<button onclick="addPreset()">Neuer Sender hinzufügen</button>
<button onclick="exportPreset()">Presets exportieren</button>
<button onclick="importPreset()">Presets importieren</button>
</div>
<div class="button-container">
<button onclick="closeModal()">Abbrechen</button>
<button onclick="saveAndClose()">Speichern</button>
</div>
</div>
</div>
</body>
</html>

271
script.js Normal file
View file

@ -0,0 +1,271 @@
// Public Domain
document.addEventListener('DOMContentLoaded', function () {
const presetContainer = document.getElementById('preset-container');
const stopButton = document.querySelector('.stop-button');
const currentStationName = document.getElementById('current-station-name');
const presetModal = document.getElementById('preset-modal');
const tableBody = document.querySelector('#sortable-tbody');
const editPresetsButton = document.getElementById('edit-presets-button');
const openSleepTimerModalButton = document.getElementById('open-sleep-timer-modal');
const sleepTimerModal = document.getElementById('sleep-timer-modal');
const closeSleepTimerModalButton = document.getElementById('close-sleep-timer-modal');
const offSleepTimerModalButton = document.getElementById('off-sleep-timer-modal');
let currentSound = null;
let presets = [];
let sleepTimerTimeout = null;
let sleepTimerEndTime = null;
initializeFooter();
loadPresets();
initializeEventListeners();
function initializeFooter() {
const currentYear = new Date().getFullYear();
document.getElementById('current-year').textContent = currentYear;
document.getElementById('server-name').textContent = window.location.hostname;
}
function updateTitle(stationName) {
document.title = stationName ? `${stationName} - Radioplayer mit Presets` : "Radioplayer mit Presets";
}
function initializeEventListeners() {
stopButton.addEventListener('click', stopPlayback);
editPresetsButton.addEventListener('click', openModal);
}
async function loadPresets() {
presets = JSON.parse(localStorage.getItem('presets')) || [];
if (presets.length === 0) {
try {
const response = await fetch('webradio_presets.json');
if (!response.ok) throw new Error('Datei konnte nicht geladen werden');
presets = await response.json();
localStorage.setItem('presets', JSON.stringify(presets));
} catch (error) {
alert('Fehler beim Laden der Presets: ' + error.message);
}
}
displayPresets(presets);
renderPresets();
initSortable();
}
function displayPresets(presets) {
presetContainer.innerHTML = '';
presets.forEach(preset => addNewPreset(preset.name, preset.url));
}
function addNewPreset(name, url) {
const presetItem = document.createElement('div');
presetItem.classList.add('preset-item');
presetItem.dataset.streamUrl = url;
presetItem.innerHTML = `<h3>${name}</h3>`;
presetItem.addEventListener('click', () => playStream(url, presetItem));
presetContainer.appendChild(presetItem);
}
function renderPresets() {
tableBody.innerHTML = '';
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="changeName(${index}, this.value)"></td>
<td><input type="text" value="${preset.url}" onchange="changeURL(${index}, this.value)"></td>
<td><button class="delete-btn" onclick="deletePreset(${index})"><i class="fas fa-trash-alt" style="color:grey"></i></button></td>
`;
tableBody.appendChild(row);
});
}
openSleepTimerModalButton.addEventListener('click', function () {
sleepTimerModal.classList.remove('hidden');
sleepTimerModal.style.display = 'block';
updateOffButtonVisibility();
});
closeSleepTimerModalButton.addEventListener('click', function () {
sleepTimerModal.classList.add('hidden');
sleepTimerModal.style.display = 'none';
});
offSleepTimerModalButton.addEventListener('click', function () {
clearSleepTimer();
sleepTimerModal.classList.add('hidden');
sleepTimerModal.style.display = 'none';
});
document.querySelectorAll('.time-option').forEach(button => {
button.addEventListener('click', function () {
const timeInMilliseconds = button.getAttribute('data-time');
startSleepTimer(parseInt(timeInMilliseconds));
sleepTimerModal.classList.add('hidden');
sleepTimerModal.style.display = 'none';
});
});
function startSleepTimer(duration) {
if (sleepTimerTimeout) {
clearTimeout(sleepTimerTimeout);
}
sleepTimerEndTime = Date.now() + duration;
sleepTimerTimeout = setTimeout(() => {
stopPlayback();
updateSleepTimerButton();
updateOffButtonVisibility();
}, duration);
updateSleepTimerButton();
updateOffButtonVisibility();
}
function clearSleepTimer() {
if (sleepTimerTimeout) {
clearTimeout(sleepTimerTimeout);
sleepTimerTimeout = null;
sleepTimerEndTime = null;
openSleepTimerModalButton.textContent = 'Sleep Timer';
updateOffButtonVisibility();
}
}
function updateSleepTimerButton() {
if (sleepTimerEndTime) {
const remainingTime = Math.max(0, Math.ceil((sleepTimerEndTime - Date.now()) / 60000));
openSleepTimerModalButton.innerHTML = `<i class="fas fa-hourglass-half"></i> ${remainingTime} Minuten`;
if (remainingTime > 0) {
setTimeout(updateSleepTimerButton, 60000); // Update every minute
} else {
openSleepTimerModalButton.textContent = 'Sleep Timer';
sleepTimerEndTime = null;
updateOffButtonVisibility();
}
}
}
function updateOffButtonVisibility() {
if (sleepTimerTimeout) {
offSleepTimerModalButton.style.display = 'inline-block';
} else {
offSleepTimerModalButton.style.display = 'none';
}
}
function playStream(url, item) {
stopPlayback();
currentSound = new Howl({
src: [url],
html5: true,
volume: 0.5,
onplay: () => {
stopButton.classList.remove('hidden');
updateTitle(item.querySelector('h3').textContent);
currentStationName.textContent = 'Es läuft: ' + item.querySelector('h3').textContent;
},
onstop: () => {
stopButton.classList.add('hidden');
currentStationName.textContent = 'Wähle einen Sender aus';
updateTitle('');
}
});
currentSound.play();
}
function stopPlayback() {
if (currentSound) {
currentSound.stop();
currentSound = null;
}
}
function openModal() {
presetModal.classList.remove('hidden');
presetModal.style.display = 'block';
renderPresets();
}
window.closeModal = function() {
presetModal.classList.add('hidden');
presetModal.style.display = 'none';
}
window.addPreset = function() {
presets.push({ name: 'Neuer Sender', url: '' });
renderPresets();
}
window.changeName = function(index, newName) {
presets[index].name = newName;
}
window.changeURL = function(index, newURL) {
presets[index].url = newURL;
}
window.deletePreset = function(index) {
presets.splice(index, 1);
renderPresets();
}
window.saveAndClose = function() {
savePresets();
closeModal();
location.reload();
}
function savePresets() {
localStorage.setItem('presets', JSON.stringify(presets));
}
function initSortable() {
const sortable = new Sortable(tableBody, {
handle: '.drag-handle',
animation: 150,
onEnd: function (evt) {
const newOrder = Array.from(tableBody.children).map(row => parseInt(row.getAttribute('data-index')));
presets = newOrder.map(index => presets[index]);
}
});
}
window.exportPreset = function() {
const presetsJSON = JSON.stringify(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';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
window.importPreset = function() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.onchange = function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const importedPresets = JSON.parse(e.target.result);
presets = importedPresets;
localStorage.setItem('presets', JSON.stringify(presets));
renderPresets();
displayPresets(presets);
location.reload();
} catch (error) {
alert('Fehler beim Importieren der Presets: Ungültige Datei.');
}
};
reader.readAsText(file);
}
};
input.click();
}
});

184
style.css Normal file
View file

@ -0,0 +1,184 @@
/* Public Domain */
:root {
--primary-color: #ff9999;
--header-height: 120px;
--padding-top: calc(10px + var(--header-height));
--box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 20px;
padding-top: var(--padding-top);
}
.header {
background-color: #FFFFE3;
height: var(--header-height);
padding-bottom: 10px;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1000;
box-shadow: var(--box-shadow);
}
.titel {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.titel img {
height: 30px;
width: auto;
}
.hidden {
display: none;
}
.stop-button {
background-color: var(--primary-color);
color: #fff;
font-weight: bold;
border: none;
border-radius: 5px;
height: 30px;
padding: 4px 10px;
font-size: 18px;
margin-left: auto;
cursor: pointer;
transition: background-color 0.3s ease;
position: relative;
top: -2px;
}
.stop-button:focus {
outline: none;
box-shadow: 0 0 5px rgba(255, 77, 77, 0.75);
}
.current-station {
font-weight: bold;
font-size: 16px;
margin-bottom: 15px;
}
#aktueller-sender {
background-color: #d2f8d2;
border-radius: 5px;
padding: 10px 20px;
}
.preset-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 20px;
margin: 30px 0;
}
.preset-item {
background-color: #f1f1f1;
padding: 15px;
border-radius: 5px;
box-shadow: var(--box-shadow);
cursor: pointer;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
}
.preset-item h3 {
margin: 0;
flex: 1;
}
.preset-item:hover {
background-color: #e6e6e6;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.button-container {
display: flex;
justify-content: center;
gap: 10px;
}
.footer {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
font-size: 12px;
color: #666;
}
.footer a {
color: #666;
text-decoration: none;
}
.footer a:hover {
color: #333;
}
@media (max-width: 600px) {
.preset-container {
grid-template-columns: 1fr;
}
}
/* ===== Modal Styles ===== */
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
display: none;
}
.modal-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.modal-content table {
width: 100%;
border-collapse: collapse;
}
.modal-content th,
.modal-content td {
border: 1px solid #ddd;
padding: 8px;
}
.modal-content th {
background-color: #f2f2f2;
}
.modal-content .drag-handle {
cursor: grab;
text-align: center;
}
.modal-content input[type="text"] {
width: 100%;
box-sizing: border-box;
}