Files
2026-04-26 10:39:49 +02:00

935 lines
31 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Router Music Player</title>
<style>
* {
box-sizing: border-box;
}
body {
background: #121212;
font-family: 'Segoe UI', 'Inter', system-ui, -apple-system, 'Roboto', monospace;
margin: 0;
padding: 20px;
color: #e0e0e0;
}
.player-container {
max-width: 1400px;
margin: 0 auto;
background: #1e1e2f;
border-radius: 2rem;
box-shadow: 0 20px 35px -12px rgba(0,0,0,0.5);
overflow: hidden;
border: 1px solid #2c2c3a;
}
.player-header {
background: #0f0f1a;
padding: 1.2rem 2rem;
border-bottom: 1px solid #2c2c3a;
}
.player-header h1 {
margin: 0 0 0.3rem 0;
font-size: 1.7rem;
font-weight: 600;
background: linear-gradient(135deg, #ffffff, #aaadff);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.sub {
font-size: 0.8rem;
color: #9a9ac0;
font-family: monospace;
}
.dashboard {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
padding: 1.8rem 2rem 2rem 2rem;
}
.folders-panel {
flex: 2;
min-width: 280px;
background: #151521;
border-radius: 1.5rem;
padding: 1rem;
border: 1px solid #2a2a36;
display: flex;
flex-direction: column;
}
.folders-panel h3 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
font-size: 1.2rem;
border-left: 4px solid #6c5ce7;
padding-left: 12px;
}
.breadcrumb {
font-size: 0.75rem;
padding: 8px 12px;
background: #0c0c16;
border-radius: 0.8rem;
margin-bottom: 12px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
font-family: monospace;
}
.breadcrumb a {
color: #6c5ce7;
cursor: pointer;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb span {
color: #888;
}
.folder-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 420px;
overflow-y: auto;
padding-right: 4px;
flex: 1;
}
.folder-item, .file-item {
background: #1e1e2c;
border-radius: 1rem;
padding: 10px 14px;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.2s ease;
border: 1px solid #2c2c3e;
}
.folder-item {
cursor: pointer;
}
.folder-item:hover {
background: #2a2a3a;
border-color: #6c5ce7;
transform: translateX(3px);
}
.file-item {
cursor: default;
opacity: 0.8;
}
.folder-name, .file-name {
font-weight: 500;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 10px;
font-family: 'Courier New', monospace;
word-break: break-all;
flex: 1;
}
.folder-name:before {
content: "📁";
font-size: 1.1rem;
}
.file-name:before {
content: "🎵";
font-size: 1.1rem;
}
.add-folder-btn {
background: #2c2c3f;
border: none;
color: white;
padding: 6px 12px;
border-radius: 30px;
font-size: 0.7rem;
cursor: pointer;
transition: 0.2s;
font-weight: 500;
}
.add-folder-btn:hover {
background: #6c5ce7;
transform: scale(0.98);
}
.queue-panel {
flex: 2.5;
min-width: 280px;
background: #151521;
border-radius: 1.5rem;
padding: 1rem;
border: 1px solid #2a2a36;
display: flex;
flex-direction: column;
}
.queue-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.queue-header h3 {
margin: 0;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.queue-actions button {
background: #2c2c3e;
border: none;
color: #ddd;
padding: 5px 12px;
border-radius: 40px;
font-size: 0.7rem;
margin-left: 8px;
cursor: pointer;
font-weight: 500;
}
.queue-actions button:hover {
background: #4a4a6a;
}
.queue-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
max-height: 320px;
background: #0e0e18;
border-radius: 1rem;
}
.queue-track {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
border-bottom: 1px solid #262633;
cursor: pointer;
transition: 0.1s;
font-size: 0.85rem;
}
.queue-track:hover {
background: #252538;
}
.queue-track.active-track {
background: #6c5ce7;
color: white;
border-radius: 0.6rem;
margin: 2px 5px;
padding: 8px 10px;
}
.track-info {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1;
}
.track-title {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track-folder {
font-size: 0.65rem;
opacity: 0.7;
font-family: monospace;
}
.remove-track {
background: none;
border: none;
color: #ff8a8a;
font-size: 1.2rem;
cursor: pointer;
opacity: 0.6;
margin-left: 8px;
}
.remove-track:hover {
opacity: 1;
}
.player-controls {
flex: 1.2;
min-width: 260px;
background: #151521;
border-radius: 1.5rem;
padding: 1.2rem;
display: flex;
flex-direction: column;
gap: 1rem;
border: 1px solid #2a2a36;
}
.now-playing {
background: #0c0c16;
border-radius: 1.2rem;
padding: 12px;
text-align: center;
}
.np-title {
font-weight: bold;
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.np-folder {
font-size: 0.7rem;
color: #aaa;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin: 10px 0;
}
.controls button {
background: #2d2d40;
border: none;
color: white;
font-size: 1.6rem;
width: 48px;
height: 48px;
border-radius: 60px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.2s;
}
.controls button:hover {
background: #6c5ce7;
transform: scale(1.03);
}
.progress-container {
display: flex;
align-items: center;
gap: 12px;
}
.progress-bar {
flex: 1;
height: 5px;
background: #2c2c3c;
border-radius: 10px;
cursor: pointer;
}
.progress-fill {
width: 0%;
height: 100%;
background: #6c5ce7;
border-radius: 10px;
}
.time {
font-size: 0.7rem;
font-family: monospace;
}
.volume-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 6px;
}
input[type="range"] {
background: #2c2c3a;
height: 3px;
border-radius: 5px;
-webkit-appearance: none;
}
input[type="range"]:focus {
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #b9b3ff;
cursor: pointer;
}
.footer-meta {
font-size: 0.7rem;
text-align: center;
padding: 1rem;
border-top: 1px solid #23232f;
color: #6f6f8f;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #1f1f2a;
}
::-webkit-scrollbar-thumb {
background: #4a4a6a;
border-radius: 6px;
}
.empty-message {
text-align: center;
padding: 26px;
color: #777;
font-style: italic;
}
.loading {
text-align: center;
padding: 20px;
color: #6c5ce7;
}
button {
background: none;
border: none;
}
.status-msg {
font-size: 0.7rem;
color: #6c5ce7;
margin-top: 8px;
text-align: center;
}
.back-button {
background: #2c2c3f;
border: none;
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.75rem;
cursor: pointer;
margin-right: 8px;
}
.back-button:hover {
background: #6c5ce7;
}
</style>
</head>
<body>
<div class="player-container">
<div class="player-header">
<h1>🎵 Router Music Player</h1>
<div class="sub">📂 Browse folders · Click (+) to add songs to queue</div>
</div>
<div class="dashboard">
<div class="folders-panel">
<h3>📀 File Browser</h3>
<div class="breadcrumb" id="breadcrumb">
<a id="rootLink">/card/</a>
</div>
<div class="folder-list" id="folderListContainer">
<div class="loading">Loading...</div>
</div>
<div class="status-msg" id="statusMsg"></div>
<div style="margin-top: 12px; font-size: 0.7rem; color:#8a8aae;">
💡 Click folder to navigate · Click to add all songs in that folder to queue
</div>
</div>
<div class="queue-panel">
<div class="queue-header">
<h3>🎧 Play Queue <span id="queueCount">(0)</span></h3>
<div class="queue-actions">
<button id="clearQueueBtn">🗑 Clear</button>
<button id="shuffleQueueBtn">🔀 Shuffle</button>
</div>
</div>
<ul class="queue-list" id="queueList">
<li class="empty-message">Queue empty. Browse and click on folders to add songs.</li>
</ul>
</div>
<div class="player-controls">
<div class="now-playing">
<div class="np-title" id="nowPlayingTitle"></div>
<div class="np-folder" id="nowPlayingFolder">Ready</div>
</div>
<div class="controls">
<button id="prevBtn"></button>
<button id="playPauseBtn"></button>
<button id="nextBtn"></button>
</div>
<div class="progress-container">
<span class="time" id="currentTime">0:00</span>
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<span class="time" id="durationTime">0:00</span>
</div>
<div class="volume-row">
<span>🔊</span>
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="0.7">
<span id="volPercent">70%</span>
</div>
<div style="font-size: 0.7rem; text-align: center; margin-top: 6px;">
⚡ Auto-advance · Queue loops · Android media controls supported
</div>
</div>
</div>
<div class="footer-meta">
🧩 Serving from /card/ · Supports MP3, WAV, OGG, M4A, FLAC, AAC
</div>
</div>
<script>
let musicQueue = [];
let currentTrackIndex = -1;
let audioElement = new Audio();
let isPlaying = false;
let currentPath = '/card/';
const folderListDiv = document.getElementById('folderListContainer');
const breadcrumbDiv = document.getElementById('breadcrumb');
const queueListEl = document.getElementById('queueList');
const queueCountSpan = document.getElementById('queueCount');
const playPauseBtn = document.getElementById('playPauseBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const clearQueueBtn = document.getElementById('clearQueueBtn');
const shuffleQueueBtn = document.getElementById('shuffleQueueBtn');
const nowPlayingTitle = document.getElementById('nowPlayingTitle');
const nowPlayingFolder = document.getElementById('nowPlayingFolder');
const progressFill = document.getElementById('progressFill');
const progressBar = document.getElementById('progressBar');
const currentTimeSpan = document.getElementById('currentTime');
const durationTimeSpan = document.getElementById('durationTime');
const volumeSlider = document.getElementById('volumeSlider');
const volPercentSpan = document.getElementById('volPercent');
const statusMsg = document.getElementById('statusMsg');
const rootLink = document.getElementById('rootLink');
const SUPPORTED_EXTS = ['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac', '.opus'];
function isAudioFile(filename) {
let ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
return SUPPORTED_EXTS.includes(ext);
}
function setupMediaSession() {
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', () => {
if (musicQueue.length === 0) return;
if (!audioElement.src || currentTrackIndex === -1) {
if (musicQueue.length) playTrackAtIndex(0);
return;
}
audioElement.play();
isPlaying = true;
playPauseBtn.textContent = '⏸';
updatePlaybackState();
});
navigator.mediaSession.setActionHandler('pause', () => {
audioElement.pause();
isPlaying = false;
playPauseBtn.textContent = '▶';
updatePlaybackState();
});
navigator.mediaSession.setActionHandler('previoustrack', () => prevTrack());
navigator.mediaSession.setActionHandler('nexttrack', () => nextTrack());
}
}
function updateMediaMetadata(track) {
if (!('mediaSession' in navigator)) return;
if (track) {
let title = track.name.replace(/\.[^/.]+$/, "");
navigator.mediaSession.metadata = new MediaMetadata({
title: title,
artist: "Router Music",
album: track.folder.replace(/\//g, ''),
artwork: []
});
}
updatePlaybackState();
}
function updatePlaybackState() {
if (!('mediaSession' in navigator)) return;
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
}
function parseDirectoryHTML(htmlText) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, 'text/html');
const links = doc.querySelectorAll('a');
const folders = [];
const files = [];
for (let link of links) {
let href = link.getAttribute('href');
if (href && href !== '../' && href !== '/') {
let decodedHref = decodeURIComponent(href);
if (decodedHref.endsWith('/')) {
folders.push(decodedHref);
} else if (isAudioFile(decodedHref)) {
files.push(decodedHref);
}
}
}
return { folders, files };
}
async function loadPath(path) {
folderListDiv.innerHTML = '<div class="loading">📡 Loading ' + path + '...</div>';
try {
const response = await fetch(path, { cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const htmlText = await response.text();
const { folders, files } = parseDirectoryHTML(htmlText);
updateBreadcrumb(path);
if (folders.length === 0 && files.length === 0) {
folderListDiv.innerHTML = '<div class="empty-message">📭 Empty folder</div>';
return;
}
folderListDiv.innerHTML = '';
for (let folder of folders) {
const folderDiv = document.createElement('div');
folderDiv.className = 'folder-item';
folderDiv.innerHTML = `
<div class="folder-name">${escapeHtml(folder)}</div>
<button class="add-folder-btn" data-folder="${folder}"> Add</button>
`;
const addBtn = folderDiv.querySelector('.add-folder-btn');
addBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await addFolderToQueue(path, folder);
});
folderDiv.addEventListener('click', () => {
navigateToFolder(path, folder);
});
folderListDiv.appendChild(folderDiv);
}
for (let file of files) {
const fileDiv = document.createElement('div');
fileDiv.className = 'file-item';
fileDiv.innerHTML = `
<div class="file-name">${escapeHtml(file)}</div>
<button class="add-folder-btn" style="opacity:0.5; cursor:default;" disabled>🎵</button>
`;
folderListDiv.appendChild(fileDiv);
}
} catch (err) {
console.error(err);
folderListDiv.innerHTML = '<div class="empty-message">❌ Error loading folder</div>';
}
}
function navigateToFolder(currentPath, folderName) {
let newPath = currentPath;
if (!newPath.endsWith('/')) newPath += '/';
newPath += folderName;
currentPath = newPath;
loadPath(currentPath);
}
function updateBreadcrumb(path) {
breadcrumbDiv.innerHTML = '';
const parts = path.replace(/^\/card\//, '').split('/').filter(p => p);
const homeLink = document.createElement('a');
homeLink.textContent = '/card/';
homeLink.style.cursor = 'pointer';
homeLink.onclick = () => {
currentPath = '/card/';
loadPath(currentPath);
};
breadcrumbDiv.appendChild(homeLink);
let accumulated = '/card/';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (!part) continue;
accumulated += part + '/';
breadcrumbDiv.appendChild(document.createTextNode(' / '));
const link = document.createElement('a');
link.textContent = part + '/';
link.style.cursor = 'pointer';
link.onclick = (function(pathToLoad) {
return function() {
currentPath = pathToLoad;
loadPath(currentPath);
};
})(accumulated);
breadcrumbDiv.appendChild(link);
}
}
async function addFolderToQueue(parentPath, folderName) {
let folderUrl = parentPath;
if (!folderUrl.endsWith('/')) folderUrl += '/';
folderUrl += folderName;
statusMsg.textContent = `Scanning ${folderName}...`;
try {
const response = await fetch(folderUrl, { cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const htmlText = await response.text();
const { files } = parseDirectoryHTML(htmlText);
const tracks = [];
for (let file of files) {
if (isAudioFile(file)) {
tracks.push({
name: file,
folder: folderUrl.replace('/card/', ''),
url: folderUrl + file
});
}
}
if (tracks.length === 0) {
statusMsg.textContent = `❌ No audio files in ${folderName}`;
setTimeout(() => { statusMsg.textContent = ''; }, 2000);
return;
}
const wasEmpty = musicQueue.length === 0;
musicQueue.push(...tracks);
renderQueue();
if (wasEmpty && musicQueue.length > 0 && currentTrackIndex === -1) {
currentTrackIndex = 0;
renderQueue();
}
statusMsg.textContent = `✅ Added ${tracks.length} songs from ${folderName}`;
setTimeout(() => { statusMsg.textContent = ''; }, 2000);
} catch (err) {
console.error(err);
statusMsg.textContent = `❌ Failed to scan ${folderName}`;
setTimeout(() => { statusMsg.textContent = ''; }, 2000);
}
}
function renderQueue() {
queueCountSpan.innerText = `(${musicQueue.length})`;
if (musicQueue.length === 0) {
queueListEl.innerHTML = '<li class="empty-message">🎼 Queue empty. Browse and click on folders to add songs.</li>';
return;
}
queueListEl.innerHTML = '';
musicQueue.forEach((track, idx) => {
const li = document.createElement('li');
li.className = 'queue-track';
if (idx === currentTrackIndex) li.classList.add('active-track');
li.innerHTML = `
<div class="track-info">
<div class="track-title">${escapeHtml(track.name)}</div>
<div class="track-folder">📁 ${escapeHtml(track.folder)}</div>
</div>
<button class="remove-track" data-index="${idx}">✖</button>
`;
const removeBtn = li.querySelector('.remove-track');
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
removeFromQueue(idx);
});
li.addEventListener('click', () => {
playTrackAtIndex(idx);
});
queueListEl.appendChild(li);
});
}
function removeFromQueue(index) {
if (index < 0 || index >= musicQueue.length) return;
const wasPlayingCurrent = (currentTrackIndex === index && isPlaying);
musicQueue.splice(index, 1);
if (musicQueue.length === 0) {
currentTrackIndex = -1;
audioElement.pause();
isPlaying = false;
playPauseBtn.textContent = '▶';
nowPlayingTitle.textContent = '—';
nowPlayingFolder.textContent = 'Ready';
updateMediaMetadata(null);
renderQueue();
return;
}
if (currentTrackIndex > index) {
currentTrackIndex--;
} else if (currentTrackIndex === index) {
if (currentTrackIndex < musicQueue.length) {
playTrackAtIndex(currentTrackIndex);
} else if (musicQueue.length > 0) {
playTrackAtIndex(musicQueue.length - 1);
}
}
renderQueue();
if (wasPlayingCurrent && currentTrackIndex !== -1 && musicQueue[currentTrackIndex]) {
playTrackAtIndex(currentTrackIndex);
}
}
function playTrackAtIndex(index) {
if (index < 0 || index >= musicQueue.length) return;
currentTrackIndex = index;
const track = musicQueue[currentTrackIndex];
if (!track) return;
audioElement.src = track.url;
audioElement.load();
audioElement.play().then(() => {
isPlaying = true;
playPauseBtn.textContent = '⏸';
nowPlayingTitle.textContent = track.name;
nowPlayingFolder.textContent = `from ${track.folder}`;
updateMediaMetadata(track);
updatePlaybackState();
}).catch(e => {
console.warn("Playback error:", e);
isPlaying = false;
playPauseBtn.textContent = '▶';
nowPlayingTitle.textContent = track.name;
nowPlayingFolder.textContent = `from ${track.folder} (tap play)`;
});
renderQueue();
}
function clearQueue() {
musicQueue = [];
currentTrackIndex = -1;
audioElement.pause();
isPlaying = false;
playPauseBtn.textContent = '▶';
audioElement.src = '';
nowPlayingTitle.textContent = '—';
nowPlayingFolder.textContent = 'Ready';
updateMediaMetadata(null);
renderQueue();
statusMsg.textContent = '🗑 Queue cleared';
setTimeout(() => { statusMsg.textContent = ''; }, 1500);
}
function shuffleQueue() {
if (musicQueue.length <= 1) return;
for (let i = musicQueue.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[musicQueue[i], musicQueue[j]] = [musicQueue[j], musicQueue[i]];
}
if (currentTrackIndex !== -1) {
const currentUrl = musicQueue[currentTrackIndex]?.url;
const newIdx = musicQueue.findIndex(t => t.url === currentUrl);
currentTrackIndex = newIdx !== -1 ? newIdx : 0;
} else if (musicQueue.length > 0) {
currentTrackIndex = 0;
}
renderQueue();
if (currentTrackIndex !== -1 && isPlaying) {
playTrackAtIndex(currentTrackIndex);
}
statusMsg.textContent = '🔀 Queue shuffled';
setTimeout(() => { statusMsg.textContent = ''; }, 1500);
}
function nextTrack() {
if (musicQueue.length === 0) return;
let nextIdx = currentTrackIndex + 1;
if (nextIdx >= musicQueue.length) nextIdx = 0;
playTrackAtIndex(nextIdx);
}
function prevTrack() {
if (musicQueue.length === 0) return;
let prevIdx = currentTrackIndex - 1;
if (prevIdx < 0) prevIdx = musicQueue.length - 1;
playTrackAtIndex(prevIdx);
}
function escapeHtml(str) {
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
function formatTime(secs) {
if (isNaN(secs) || !isFinite(secs)) return "0:00";
let mins = Math.floor(secs / 60);
let seconds = Math.floor(secs % 60);
return mins + ":" + (seconds < 10 ? '0' : '') + seconds;
}
audioElement.addEventListener('timeupdate', () => {
if (audioElement.duration && !isNaN(audioElement.duration)) {
const percent = (audioElement.currentTime / audioElement.duration) * 100;
progressFill.style.width = percent + '%';
currentTimeSpan.textContent = formatTime(audioElement.currentTime);
}
});
audioElement.addEventListener('loadedmetadata', () => {
durationTimeSpan.textContent = formatTime(audioElement.duration);
});
audioElement.addEventListener('ended', () => {
nextTrack();
});
audioElement.addEventListener('play', () => {
isPlaying = true;
playPauseBtn.textContent = '⏸';
updatePlaybackState();
});
audioElement.addEventListener('pause', () => {
isPlaying = false;
playPauseBtn.textContent = '▶';
updatePlaybackState();
});
progressBar.addEventListener('click', (e) => {
const rect = progressBar.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
if (audioElement.duration) {
audioElement.currentTime = percent * audioElement.duration;
}
});
volumeSlider.addEventListener('input', (e) => {
audioElement.volume = parseFloat(e.target.value);
volPercentSpan.innerText = Math.round(audioElement.volume * 100) + '%';
});
playPauseBtn.addEventListener('click', () => {
if (musicQueue.length === 0) return;
if (!audioElement.src || currentTrackIndex === -1) {
if (musicQueue.length) playTrackAtIndex(0);
return;
}
if (isPlaying) {
audioElement.pause();
} else {
audioElement.play();
}
});
nextBtn.addEventListener('click', nextTrack);
prevBtn.addEventListener('click', prevTrack);
clearQueueBtn.addEventListener('click', clearQueue);
shuffleQueueBtn.addEventListener('click', shuffleQueue);
rootLink.onclick = () => {
currentPath = '/card/';
loadPath(currentPath);
};
audioElement.volume = 0.7;
volumeSlider.value = 0.7;
volPercentSpan.innerText = '70%';
setupMediaSession();
loadPath('/card/');
</script>
</body>
</html>