185 lines
7.6 KiB
HTML
185 lines
7.6 KiB
HTML
<!DOCTYPE HTML>
|
|
<html>
|
|
<head>
|
|
<title>HannaBox</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta charset="UTF-8">
|
|
<link rel="stylesheet" href="style.css" type="text/css" media="all" />
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="brand">
|
|
<div>
|
|
<h1>HannaBox</h1>
|
|
</div>
|
|
</div>
|
|
<div class="status" id="state">—</div>
|
|
</header>
|
|
|
|
<main class="container">
|
|
<section class="player-card">
|
|
<div class="artwork" id="artwork">
|
|
<!-- Placeholder artwork -->
|
|
<svg viewBox="0 0 100 100" class="art-svg" aria-hidden="true">
|
|
<rect x="0" y="0" width="100" height="100" fill="#e9f0ff"></rect>
|
|
<circle cx="50" cy="40" r="18" fill="#cfe0ff"></circle>
|
|
<rect x="20" y="65" width="60" height="8" rx="2" fill="#cfe0ff"></rect>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div class="big-title" id="stateTitle">—</div>
|
|
|
|
<div class="control-row">
|
|
<button class="icon-btn prev-button" title="Previous" onclick="simpleGetCall('previous');">
|
|
<svg width="36" height="36" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
<path d="M11 19V5L4 12l7 7zM20 5v14h-2V5h2z"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<button class="icon-btn play-button" title="Play / Pause" onclick="simpleGetCall('toggleplaypause');">
|
|
<svg class="play-icon" width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M5 3v18l15-9L5 3z"/></svg>
|
|
<svg class="pause-icon" width="40" height="40" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
|
</button>
|
|
|
|
<button class="icon-btn next-button" title="Next" onclick="simpleGetCall('next');">
|
|
<svg width="36" height="36" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
<path d="M13 5v14l7-7-7-7zM4 5v14h2V5H4z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="slider-row">
|
|
<div class="slidecontainer">
|
|
<input name="progress" type="range" min="0" max="100" value="0" class="slider" id="progressSlider"
|
|
onchange="postValue('progress',document.getElementById('progressSlider').value);lastChange = Date.now();userIsInteracting = false;"
|
|
oninput="userIsInteracting = true;">
|
|
<div class="time-row">
|
|
<span id="progressLabel">0</span>
|
|
<span class="time-sep">/</span>
|
|
<span id="progressMax">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="volumecontainer">
|
|
<label for="volumeSlider">Vol</label>
|
|
<input name="volume" type="range" min="0" max="15" value="7" class="slider" id="volumeSlider"
|
|
onchange="postValue('volume',document.getElementById('volumeSlider').value);lastChange = Date.now()">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<aside class="playlist">
|
|
<div class="playlist-header">
|
|
<h2>Playlist</h2>
|
|
<button class="action-btn small" onclick="location.reload()">Refresh</button>
|
|
</div>
|
|
<div class="playlist-container" id="playlistContainer"></div>
|
|
|
|
<div class="manager-toggle">
|
|
<button id="toggleFileManagerButton" class="action-btn" onclick="toggleFileManager()">Toggle Manager</button>
|
|
</div>
|
|
|
|
<div id="fileManager" class="file-manager" style="display:none;">
|
|
<h3>Manager</h3>
|
|
<div class="info-row">
|
|
<div id="voltage"></div>
|
|
<div id="uid"></div>
|
|
<div id="heap"></div>
|
|
</div>
|
|
|
|
<h4>Upload File</h4>
|
|
<form id="uploadForm" class="form" method="POST" action="/upload" enctype="multipart/form-data">
|
|
<input type="file" name="data" id="uploadFile" accept=".mp3,.wav,.flac,.m4a,.ogg"/>
|
|
<input type="submit" name="upload" value="Upload" title="Upload Audio File" id="uploadButton" class="action-btn"/>
|
|
<div id="uploadStatus"></div>
|
|
<div id="uploadProgress" style="display: none;">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressFill"></div>
|
|
</div>
|
|
<span id="progressText">0</span>
|
|
</div>
|
|
</form>
|
|
|
|
<h4>Edit RFID Mapping</h4>
|
|
<p class="hint">Hint: Use a folder or filename, not the absolute file path!</p>
|
|
<div class="mapping-list" id="mappingList"></div>
|
|
|
|
<form id="editMappingForm" class="form form-grid">
|
|
<div>
|
|
<label for="rfid">RFID:</label>
|
|
<input type="text" id="rfid" name="rfid" required>
|
|
</div>
|
|
<div>
|
|
<label for="song">Song:</label>
|
|
<input type="text" id="song" name="song" required>
|
|
</div>
|
|
<div>
|
|
<label for="mode">Mode:</label>
|
|
<select id="mode" name="mode">
|
|
<option value="s">Single (play selected song / file)</option>
|
|
<option value="f">Folder (play selected folder, then stop)</option>
|
|
<option value="c">Continuous (continuous playback / loop folder)</option>
|
|
</select>
|
|
</div>
|
|
<button type="button" class="action-btn" style="grid-column: 1 / -1;" onclick="editMapping()">Update Mapping</button>
|
|
</form>
|
|
|
|
<h4>Move / Rename File</h4>
|
|
<form class="form form-grid">
|
|
<div>
|
|
<label for="moveFrom">From:</label>
|
|
<input type="text" id="moveFrom" placeholder="/oldname.mp3"/>
|
|
</div>
|
|
<div>
|
|
<label for="moveTo">To:</label>
|
|
<input type="text" id="moveTo" placeholder="/newname.mp3"/>
|
|
</div>
|
|
<button type="button" class="action-btn" style="grid-column: 1 / -1;" onclick="moveFile()">Move/Rename</button>
|
|
</form>
|
|
|
|
<h4>Delete File</h4>
|
|
<form class="form form-grid">
|
|
<div>
|
|
<label for="deleteFileName">Filename:</label>
|
|
<input type="text" id="deleteFileName" placeholder="/song.mp3"/>
|
|
</div>
|
|
<button type="button" class="action-btn" style="grid-column: 1 / -1;" onclick="deleteFileOnServer()">Delete</button>
|
|
</form>
|
|
</div>
|
|
</aside>
|
|
</main>
|
|
|
|
<footer class="footer">
|
|
<div>Built on ESP32 • hannabox</div>
|
|
</footer>
|
|
|
|
<script src="script.js"></script>
|
|
<script>
|
|
// Keep play/pause icon in sync with the existing class-based logic
|
|
// Toggle visibility of play/pause SVGs based on .paused class from displayState()
|
|
function syncPlayIcon() {
|
|
var btn = document.querySelector('.play-button');
|
|
if (!btn) return;
|
|
var playIcon = btn.querySelector('.play-icon');
|
|
var pauseIcon = btn.querySelector('.pause-icon');
|
|
if (btn.classList.contains('paused')) {
|
|
playIcon.style.display = 'none';
|
|
pauseIcon.style.display = '';
|
|
} else {
|
|
playIcon.style.display = '';
|
|
pauseIcon.style.display = 'none';
|
|
}
|
|
}
|
|
// Observe mutations to class attribute on play-button to update icons
|
|
var observer = new MutationObserver(syncPlayIcon);
|
|
var playBtn = document.querySelector('.play-button');
|
|
if (playBtn) observer.observe(playBtn, { attributes: true, attributeFilter: ['class'] });
|
|
|
|
// Also call regularly to catch updates
|
|
setInterval(syncPlayIcon, 800);
|
|
</script>
|
|
</body>
|
|
</html>
|