-
-
+
-
Edit RFID Mapping
- Hint: Use a folder or filename, not the absolute file path!
-
%MAPPING%
-
+
+
+
+
+
+
- Move / Rename File
-
-
- Delete File
-
-
+
+
—
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/script.js b/web/script.js
index 5d19eb5..10d7fa8 100644
--- a/web/script.js
+++ b/web/script.js
@@ -79,38 +79,60 @@ function updateProgress() {
}
function displayState(state) {
- document.getElementById("state").innerHTML = state['title'];
- document.getElementById("progressLabel").innerHTML = state['time'];
- document.getElementById("voltage").innerHTML = state['voltage']+' mV';
- document.getElementById("heap").innerHTML = state['heap']+' bytes free heap';
- document.getElementById("uid").innerHTML = 'Last NFC ID: '+state['uid'];
+ var title = state['title'] || '—';
+ var titleEl = document.getElementById("state");
+ if (titleEl) titleEl.innerHTML = title;
+ var bigTitleEl = document.getElementById("stateTitle");
+ if (bigTitleEl) bigTitleEl.innerText = title;
+
+ var progressLabel = document.getElementById("progressLabel");
+ if (progressLabel) progressLabel.innerHTML = state['time'];
+
+ var progressMax = document.getElementById("progressMax");
+ if (progressMax) progressMax.innerHTML = state['length'] || 0;
+
+ var voltageEl = document.getElementById("voltage");
+ if (voltageEl) voltageEl.innerHTML = (state['voltage'] || '') + ' mV';
+
+ var heapEl = document.getElementById("heap");
+ if (heapEl) heapEl.innerHTML = (state['heap'] || '') + ' bytes free heap';
+
+ var uidEl = document.getElementById("uid");
+ if (uidEl) uidEl.innerHTML = 'Last NFC ID: ' + (state['uid'] || '');
/* ==== Autofill convenience fields ==== */
var fm = document.getElementById('fileManager');
- if (state['filepath'] && fm.style.display == 'none') {
- document.getElementById('moveFrom').value = state['filepath'];
- document.getElementById('deleteFileName').value = state['filepath'];
- document.getElementById('song').value = state['filepath'];
+ if (state['filepath'] && fm && fm.style.display == 'none') {
+ var moveFrom = document.getElementById('moveFrom');
+ var deleteFileName = document.getElementById('deleteFileName');
+ var song = document.getElementById('song');
+ if (moveFrom) moveFrom.value = state['filepath'];
+ if (deleteFileName) deleteFileName.value = state['filepath'];
+ if (song) song.value = state['filepath'];
}
if (state['uid']) {
- document.getElementById('rfid').value = state['uid'];
+ var rfidEl = document.getElementById('rfid');
+ if (rfidEl) rfidEl.value = state['uid'];
}
- var elements = document.getElementsByClassName('play-button');
- var btn = elements[0];
- if (state['playing']) {
- btn.classList.add('paused');
- } else {
- btn.classList.remove('paused');
+ var btn = document.querySelector('.play-button');
+ if (btn) {
+ if (state['playing']) {
+ btn.classList.add('paused');
+ } else {
+ btn.classList.remove('paused');
+ }
}
if (Date.now()-lastChange>1200) {
var progress = document.getElementById('progressSlider');
- progress.value = state['time'];
- progress.max = state['length'];
+ if (progress) {
+ progress.value = state['time'];
+ progress.max = state['length'];
+ }
var volume = document.getElementById('volumeSlider');
- volume.value = state['volume'];
+ if (volume) volume.value = state['volume'];
}
updateProgress();
}
diff --git a/web/style.css b/web/style.css
index 485eb80..96df542 100644
--- a/web/style.css
+++ b/web/style.css
@@ -1,211 +1,495 @@
-body {
- font-family: Arial, sans-serif;
- margin: 0 auto;
- padding: 20px;
- text-align: center; /* Center align elements */
- background-color: #f4f4f4; /* Light background */
+/* Theme variables */
+:root{
+ --bg: #f4f6fb;
+ --card: #ffffff;
+ --muted: #6b7280;
+ --accent: #2563eb;
+ --accent-dark: #1649b0;
+ --glass: rgba(255,255,255,0.6);
+ --radius: 12px;
+ --shadow: 0 6px 18px rgba(18, 38, 63, 0.08);
+ --max-width: 1100px;
}
-.playlist-container {
- max-height: 300px;
- overflow-y: auto;
- border: 1px solid #ccc;
- padding: 10px;
- margin-top: 20px;
- text-align: left; /* Align playlist text to the left */
+/* Page basics */
+* { box-sizing: border-box; }
+html,body {
+ height: 100%;
+ margin: 0;
+ font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ background: linear-gradient(180deg, var(--bg), #eef3fb 120%);
+ color: #1f2937;
+ -webkit-font-smoothing:antialiased;
+ -moz-osx-font-smoothing:grayscale;
}
-li {
- cursor: pointer;
- list-style-type: none;
+a { color: var(--accent); text-decoration: none; }
+
+.topbar {
+ max-width: var(--max-width);
+ margin: 18px auto 0 auto;
+ padding: 12px 18px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
}
-.play-button, .next-button, .prev-button {
- border: 0;
- background: transparent;
- cursor: pointer;
- transition: background-color 0.3s ease; /* Smooth transition for hover */
+.brand {
+ display: flex;
+ gap: 12px;
+ align-items: center;
}
+.logo {
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+ filter: drop-shadow(0 4px 10px rgba(37,99,235,0.12));
+ border-radius: 8px;
+}
+
+.topbar h1 {
+ margin: 0;
+ font-size: 1.125rem;
+ letter-spacing: 0.2px;
+}
+
+.topbar .sub {
+ font-size: 0.8rem;
+ color: var(--muted);
+ margin-top: 2px;
+}
+
+/* Status (current song) */
+.status {
+ color: var(--muted);
+ font-weight: 600;
+ font-size: 0.95rem;
+ text-align: right;
+ min-width: 220px;
+}
+
+/* Main layout */
+.container {
+ max-width: var(--max-width);
+ margin: 18px auto;
+ display: grid;
+ grid-template-columns: 1fr; /* single column layout: player spans full width, playlist below */
+ gap: 20px;
+ padding: 12px;
+ align-items: start;
+}
+
+/* Player card - grid with fixed artwork column and a fluid content column */
+.player-card {
+ background: linear-gradient(180deg, rgba(255,255,255,0.9), var(--card));
+ border-radius: var(--radius);
+ padding: 18px;
+ display: grid;
+ grid-template-columns: 180px 1fr;
+ grid-auto-rows: min-content;
+ gap: 20px;
+ align-items: start;
+ box-shadow: var(--shadow);
+ min-height: 150px;
+ align-items: center;
+}
+
+/* Artwork */
+.artwork {
+ width: 160px;
+ height: 160px;
+ border-radius: 10px;
+ background: linear-gradient(180deg,#e9f3ff,#dbefff);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ box-shadow: 0 6px 18px rgba(37,99,235,0.06);
+ grid-column: 1 / 2;
+ grid-row: 1 / 3; /* span both rows so artwork stays aligned to left */
+}
+
+.art-svg { width: 100%; height: 100%; }
+
+/* Controls column - use a predictable flex column so controls center nicely */
+.controls {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ align-items: center;
+ justify-content: center;
+ grid-column: 2 / 3;
+ grid-row: 1 / 3;
+ padding-right: 6px;
+}
+
+.big-title {
+ font-size: 1.125rem;
+ font-weight: 700;
+ color: #0f172a;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Buttons row - span full width of controls grid */
+.control-row {
+ grid-column: 1 / -1;
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Icon button */
+.icon-btn {
+ width: 64px;
+ height: 64px;
+ border-radius: 14px;
+ border: none;
+ background: linear-gradient(180deg, #ffffff, #f6fbff);
+ display:flex;
+ align-items:center;
+ justify-content:center;
+ cursor: pointer;
+ transition: transform .14s ease, box-shadow .14s ease;
+ box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
+ color: var(--accent);
+}
+
+.icon-btn:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 10px 26px rgba(15, 23, 42, 0.08);
+}
+
+/* slightly different styling for play (primary) */
.play-button {
- box-sizing: border-box;
- margin: 5% auto;
- height: 50px; /* Consistent size for play button */
- border-color: transparent transparent transparent #007bff;
- border-style: solid;
- border-width: 25px 0 25px 40px; /* Adjusted size */
+ width: 86px;
+ height: 86px;
+ border-radius: 18px;
+ background: linear-gradient(180deg, var(--accent), var(--accent-dark));
+ color: white;
+ box-shadow: 0 10px 30px rgba(37,99,235,0.18);
+ display:flex;
+ align-items:center;
+ justify-content:center;
+ padding: 6px;
}
+/* Icon colors inside play */
+.play-button .play-icon,
+.play-button .pause-icon {
+ fill: currentColor;
+ width: 34px;
+ height: 34px;
+}
+
+/* Play/pause toggle class (keeps JS compatibility) */
.play-button.paused {
- border-style: double;
- border-width: 25px 0 25px 40px; /* Same size for pause button */
- height: 50px; /* Consistent height */
+ background: linear-gradient(180deg, #f3f4f6, #e5e7eb);
+ color: #111827;
+ box-shadow: 0 4px 10px rgba(2,6,23,0.06);
}
-.play-button:hover {
- border-color: transparent transparent transparent #0056b3; /* Darker blue on hover */
+/* Prev / Next buttons - larger and visually consistent with play button */
+.prev-button, .next-button {
+ width: 64px;
+ height: 64px;
+ border-radius: 14px;
+ color: var(--accent);
+ background: linear-gradient(180deg,#ffffff,#f6fbff);
+ box-shadow: 0 8px 20px rgba(37,99,235,0.06);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform .14s ease, box-shadow .14s ease;
}
-.next-button, .prev-button {
- padding: 0;
- margin: 10px;
- border-color: transparent #007bff transparent #007bff;
- border-style: solid;
+/* Slider row - kept simple; placement is handled by the .controls grid */
+.slider-row {
+ display:block;
+ margin: 0;
}
-.next-button {
- border-width: 15px 0 15px 25px;
- box-shadow: 8px 0 0 0 #007bff;
+/* Desktop: keep internal grid placement but ensure small screens revert */
+@media (min-width: 921px) {
+ .slider-row {
+ display:block;
+ }
}
-.next-button:hover {
- border-color: transparent #0056b3 transparent #0056b3;
- box-shadow: 8px 0 0 0 #0056b3;
+/* Small screens: fallback to stacked layout */
+@media (max-width: 920px) {
+ .controls {
+ display: flex;
+ flex-direction: column;
+ }
}
-.prev-button {
- border-width: 15px 25px 15px 0;
- box-shadow: -8px 0 0 0 #007bff;
-}
-
-.prev-button:hover {
- border-color: transparent #0056b3 transparent #0056b3;
- box-shadow: -8px 0 0 0 #0056b3;
+/* Progress slider - ensure full width inside controls */
+.slidecontainer {
+ width: 100%;
+ max-width: 820px;
+ min-width: 220px;
}
.slider {
- width: 90%; /* Make slider wider for easier interaction */
- margin: 10px auto; /* Center align the slider */
+ width: 100%;
+ appearance: none;
+ height: 8px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, rgba(37,99,235,0.2), rgba(37,99,235,0.1));
+ outline: none;
}
-.slidecontainer {
- margin: 20px 0; /* Space out elements */
+.slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: white;
+ border: 4px solid var(--accent);
+ box-shadow: 0 4px 14px rgba(37,99,235,0.2);
+ cursor: pointer;
}
-/* Upload progress bar styles */
+.slider::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: white;
+ border: 4px solid var(--accent);
+}
+
+/* time row */
+.time-row {
+ display:flex;
+ gap: 8px;
+ font-weight: 600;
+ color: var(--muted);
+ padding-top: 8px;
+}
+
+/* Volume container - on desktop keep it compact and aligned under the progress; on small screens it expands */
+.volumecontainer {
+ width: 70%;
+ max-width: 420px;
+ min-width: 140px;
+ margin-top: 6px;
+ padding-left: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+/* Ensure the volume label sits above the slider and is aligned */
+.volumecontainer label {
+ display: block;
+ margin-bottom: 6px;
+ font-weight: 600;
+ color: var(--muted);
+}
+
+/* Small screens: make volume full width under the progress slider */
+@media (max-width: 920px) {
+ .volumecontainer {
+ width: 100%;
+ max-width: 100%;
+ align-items: stretch;
+ }
+}
+
+/* On wide screens keep a compact volume control to the right */
+@media (min-width: 921px) {
+ .volumecontainer {
+ width: 180px;
+ min-width: 120px;
+ }
+}
+
+/* Playlist aside */
+.playlist {
+ display:flex;
+ flex-direction:column;
+ gap: 12px;
+ align-self: start; /* ensure sidebar aligns to top of the player card on wide screens */
+}
+
+.playlist-header {
+ display:flex;
+ align-items:center;
+ justify-content:space-between;
+ gap:8px;
+}
+
+.playlist-container {
+ background: var(--card);
+ padding: 12px;
+ border-radius: 12px;
+ box-shadow: var(--shadow);
+ max-height: 420px;
+ overflow-y: auto;
+ border: 1px solid rgba(15,23,42,0.04);
+ position: relative; /* keep content contained */
+ z-index: 0;
+}
+
+/* Assuming server injects
- Song
*/
+.playlist-container ul { margin: 0; padding: 0; list-style: none; }
+.playlist-container li {
+ padding: 10px 12px;
+ border-radius: 8px;
+ margin-bottom: 6px;
+ display:flex;
+ align-items:center;
+ gap:10px;
+ cursor: pointer;
+ transition: background .12s ease, transform .08s ease;
+ background: transparent;
+ z-index: 1;
+}
+.playlist-container li:hover {
+ background: linear-gradient(90deg, rgba(37,99,235,0.04), rgba(37,99,235,0.02));
+ transform: translateY(-2px);
+}
+.playlist-container li .title { flex:1; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+.playlist-container li .meta { color: var(--muted); font-size: 0.85rem; }
+
+/* Manager toggle & file manager */
+.manager-toggle { margin-top: 8px; text-align: center; }
+.file-manager {
+ margin-top: 12px;
+ background: var(--card);
+ padding: 12px;
+ border-radius: 12px;
+ box-shadow: var(--shadow);
+ border: 1px solid rgba(15,23,42,0.04);
+}
+
+/* A dedicated mapping list container so injected HTML doesn't inherit .form styling */
+.mapping-list {
+ background: #fbfdff;
+ padding: 10px;
+ border-radius: 8px;
+ border: 1px solid rgba(37,99,235,0.06);
+ margin-bottom: 8px;
+ color: #0f172a;
+ font-size: 0.95rem;
+ line-height: 1.35;
+}
+
+/* Align info row */
+.info-row { display:flex; gap:8px; font-weight:600; color:var(--muted); margin-bottom:8px; flex-wrap:wrap; }
+
+/* Forms & progress bar (preserve earlier styles) */
+.form {
+ background-color: white;
+ padding: 12px;
+ border-radius: 8px;
+ box-shadow: 0 2px 6px rgba(2,6,23,0.04);
+ margin: 10px 0;
+ text-align: left;
+}
+
+/* Grid helper for multi-field forms (move/rename/delete) */
+.form-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ align-items: center;
+}
+
+/* Make .form-grid single-column on small screens */
+@media (max-width: 520px) {
+ .form-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.form-grid label {
+ margin: 0;
+ font-weight:600;
+ color: var(--muted);
+ font-size: 0.95rem;
+}
+
+.form-grid input[type="text"] {
+ width: 100%;
+ padding: 8px;
+ border-radius: 6px;
+ border: 1px solid #eef4ff;
+}
+
+/* Ensure labels are block and inputs full width across all forms */
+.form label { display:block; margin: 8px 0 6px 0; font-weight:600; color: #334155; }
+.form input[type="text"],
+.form input[type="file"],
+.form input[type="range"] { width: 100%; padding: 8px; border: 1px solid #e6eefc; border-radius: 6px; }
+
+/* Upload progress bar */
.progress-bar {
width: 100%;
- height: 20px;
- background-color: #e0e0e0;
- border-radius: 10px;
+ height: 12px;
+ background-color: #eef3ff;
+ border-radius: 8px;
overflow: hidden;
- margin: 10px 0;
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
+ margin: 8px 0;
}
.progress-fill {
height: 100%;
- background: linear-gradient(90deg, #007bff, #0056b3);
+ background: linear-gradient(90deg, var(--accent), var(--accent-dark));
width: 0%;
transition: width 0.3s ease;
- border-radius: 10px;
-}
-
-#uploadProgress {
- margin: 15px 0;
- text-align: center;
-}
-
-#progressText {
- font-weight: bold;
- color: #007bff;
- margin-left: 10px;
-}
-
-#uploadStatus {
- margin: 10px 0;
- padding: 8px;
- border-radius: 4px;
- font-weight: bold;
-}
-
-#uploadStatus:not(:empty) {
- background-color: #e7f3ff;
- border: 1px solid #007bff;
- color: #0056b3;
-}
-
-/* Form styling improvements */
-.form {
- background-color: white;
- padding: 20px;
border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- margin: 20px 0;
- text-align: left;
-}
-
-#uploadFile {
- padding: 8px;
- border: 1px solid #ccc;
- border-radius: 4px;
- margin-right: 10px;
-}
-
-
-
-/* ====== General mobile-friendly input & button styling ====== */
-input[type="text"],
-input[type="file"],
-input[type="range"] {
- width: 100%;
- padding: 10px;
- border: 1px solid #ccc;
- border-radius: 4px;
- margin: 8px 0;
- box-sizing: border-box;
- font-size: 16px;
-}
-
-label {
- display: block;
- margin: 10px 0 5px 0;
- font-weight: bold;
- color: #333;
}
+/* Buttons (action-btn) */
.action-btn {
- background-color: #007bff;
+ background-color: var(--accent);
color: #fff;
border: none;
- padding: 10px 20px;
- border-radius: 4px;
+ padding: 8px 12px;
+ border-radius: 8px;
cursor: pointer;
- font-size: 16px;
- transition: background-color 0.3s ease;
- margin: 5px 0;
+ font-size: 14px;
+ transition: background-color 0.18s ease, transform 0.12s ease;
}
-.action-btn:hover:not(:disabled) {
- background-color: #0056b3;
+.action-btn.small { padding:6px 10px; font-size:13px; }
+.action-btn:hover:not(:disabled) { background-color: var(--accent-dark); transform: translateY(-2px); }
+.action-btn:disabled { background-color: #9aa4b2; cursor:not-allowed; }
+
+/* Footer */
+.footer {
+ max-width: var(--max-width);
+ margin: 18px auto 36px auto;
+ text-align: center;
+ color: var(--muted);
+ font-size: 0.9rem;
}
-.action-btn:disabled {
- background-color: #6c757d;
- cursor: not-allowed;
+/* Responsive tweaks */
+@media (max-width: 920px) {
+ .container {
+ grid-template-columns: 1fr;
+ padding: 8px;
+ }
+ .player-card { flex-direction: row; gap: 14px; padding: 14px; }
+ .artwork { width: 120px; height: 120px; }
+ .play-button { width: 72px; height: 72px; }
+ .playlist { order: 2; }
+ .playlist-container { max-height: 320px; }
}
-/* Responsive design improvements */
-@media (max-width: 600px) {
- body {
- padding: 10px;
- }
-
- .slider {
- width: 95%;
- }
-
- #uploadForm, #editMappingForm {
- padding: 15px;
- }
-
- #uploadButton, #editMappingForm button {
- width: 100%;
- margin: 10px 0;
- }
-
- #uploadFile {
- width: 100%;
- margin: 10px 0;
- }
+@media (max-width: 520px) {
+ .topbar { padding: 10px; }
+ .brand h1 { font-size: 1rem; }
+ .artwork { width: 96px; height: 96px; }
+ .play-button { width: 64px; height: 64px; }
+ .control-row { gap: 8px; }
+ .time-row { font-size: 0.9rem; }
+ .volumecontainer { width: 100%; }
+ .slider-row { flex-direction: column; align-items: stretch; gap: 8px; }
}