Cleanups. Delete+Move works now.

This commit is contained in:
Stefan Ostermann 2025-07-27 22:16:35 +02:00
parent 9361bd333f
commit ce4ef44dcd
10 changed files with 118 additions and 439 deletions

View File

@ -1,4 +1,5 @@
#include "DirectoryNode.h"
#include "globals.h"
DirectoryNode::DirectoryNode(const String &nodeName)
: name(nodeName), currentPlaying(nullptr)
@ -465,7 +466,7 @@ String DirectoryNode::getCurrentPlayingFilePath() const
{
if (currentPlaying != nullptr)
{
String filePath = "/" + name;
String filePath = name;
if (!filePath.endsWith("/"))
{
filePath += "/";

View File

@ -5,7 +5,7 @@
#include <vector>
const String sys_dir = "system";

View File

@ -1,68 +0,0 @@
// HTML web page
const char index_html[] PROGMEM = R"rawliteral(
<!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>
<h1>🎵 HannaBox 🎵</h1>
<span id="state"></span><br/><br/>
<span id="voltage"></span><br/>
<span id="uid"></span><br/>
<span id="heap"></span><br/>
<div>
<button class="prev-button" onclick="simpleGetCall('previous');""></button>
<button class="play-button" onclick="simpleGetCall('toggleplaypause');"></button>
<button class="next-button" onclick="simpleGetCall('next');"></button><br/><br/>
</div>
<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;"
>
<label for="progress" id="progressLabel"></label>
</div>
<div class="slidecontainer">
<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()"
>
<label for="volume">Vol</label>
</div>
<!--
<button onmouseup="simpleGetCall('stop');" ontouchend="simpleGetCall('stop');">Stop</button>&nbsp;
-->
<p class="playlist-container">
<h2>🎶 Playlist 🎶</h2>
%DIRECTORY%
</p>
<form id="uploadForm" 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"/>
<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>
<h2>Edit RFID Mapping</h2>
<form id="editMappingForm">
<label for="rfid">RFID:</label>
<input type="text" id="rfid" name="rfid" required><br>
<label for="song">Song:</label>
<input type="text" id="song" name="song" required><br>
<button type="button" onclick="editMapping()">Update Mapping</button>
</form>
<script src="/script.js"></script>
</body>
</html>)rawliteral";

View File

@ -1,3 +0,0 @@
// CSS content moved to SD card at /system/style.css
// This saves approximately 4KB of flash memory
const char css[] PROGMEM = "";

View File

@ -1,93 +1,23 @@
#ifndef GLOBALS_H_
#define GLOBALS_H_
void stop();
void start();
bool playFile(const char* filename, uint32_t resumeFilePos = 0);
void loop2(void* parameter);
void id_song_action(AsyncWebServerRequest *request);
void progress_action(AsyncWebServerRequest *request);
void volume_action(AsyncWebServerRequest *request);
boolean buttonPressed(const uint8_t pin);
/**
* Helper routine to dump a byte array as hex values to Serial.
*/
void dump_byte_array(byte *buffer, byte bufferSize)
{
for (byte i = 0; i < bufferSize; i++)
{
Serial.print(buffer[i] < 0x10 ? " 0" : " ");
Serial.print(buffer[i], HEX);
}
}
String getRFIDString(byte uidByte[10])
{
String uidString = String(uidByte[0]) + " " + String(uidByte[1]) + " " +
String(uidByte[2]) + " " + String(uidByte[3]);
return uidString;
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
unsigned long lastStart = 0;
unsigned long lastInteraction = 0;
boolean sleepSoundPlayed = false;
boolean startupSoundPlayed = false;
boolean continuousMode = false;
uint8_t buttontoignore = 0;
uint32_t lastVoltage = 0;
uint loopCounter = 0;
String lastUid = "";
uint16_t currentSongId = 0;
uint32_t currentSongSeconds = 0;
boolean continuePlaying = false;
boolean prepareSleepMode = false;
const String sys_dir = "system";
const String sleep_sound = "sleep.mp3";
const String startup_sound = "start.mp3";
const String mapping_file = "/mapping.txt";
const String mapping_file = "mapping.txt";
const String progress_file = "progress.txt";
std::map<String, String> rfid_map;
/*
const long sleepMessageDelay = 28000;

View File

@ -47,14 +47,9 @@
#define MAX_VOL 15
#include "globals.h"
#include "WebContent.h"
#include "main.h"
#include "DirectoryNode.h"
#define SOUND_STARTUP "start.mp3"
#define SOUND_SLEEP "sleep.mp3"
File root;
File mp3File;
@ -280,7 +275,7 @@ void handleDeleteFile(AsyncWebServerRequest *request) {
String filename = request->arg("filename");
if (SD.exists(filename)) {
SD.remove(filename.c_str());
SD.remove(filename);
Serial.println("Deleted file: " + filename);
request->send(200, "text/plain", "File deleted successfully.");
} else {
@ -492,7 +487,7 @@ String getState()
}
// Function to save the rfid_map to the mapping file
void saveMappingToFile(const char *filename) {
void saveMappingToFile(const String filename) {
File file = SD.open(filename, FILE_WRITE);
if (file) {
for (const auto &pair : rfid_map) {
@ -511,7 +506,7 @@ void editMapping(AsyncWebServerRequest *request) {
String rfid = request->getParam("rfid", true)->value();
String song = request->getParam("song", true)->value();
rfid_map[rfid] = song;
saveMappingToFile(mapping_file.c_str());
saveMappingToFile(getSysDir(mapping_file));
request->send(200, "text/plain", "Mapping updated");
} else {
request->send(400, "text/plain", "Invalid parameters");
@ -752,7 +747,7 @@ void setup()
String progressPath = "/"+sys_dir+"/"+progress_file;
String progressPath = getSysDir(progress_file);
continuePlaying = readSongProgress(progressPath.c_str());
@ -824,14 +819,14 @@ void setup()
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
webrequestActive = true;
String htmlPath = "/system/index.html";
String htmlPath = getSysDir("index.html");
if (SD.exists(htmlPath)) {
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html", false, processor);
response->addHeader("Content-Type", "text/html; charset=UTF-8");
request->send(response);
} else {
// Fallback: serve minimal error if file not found
request->send(200, "text/plain", "ERROR: /system/index.html on SD Card not found!");
request->send(404, "text/plain", "ERROR: /system/index.html on SD Card not found!");
}
webrequestActive = false;
@ -840,12 +835,12 @@ void setup()
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
{
webrequestActive = true;
String cssPath = "/system/style.css";
String cssPath = getSysDir("style.css");
if (SD.exists(cssPath)) {
request->send(SD, cssPath, "text/css");
} else {
// Fallback: serve minimal CSS if file not found
request->send(200, "text/css", "body{font-family:Arial;text-align:center;padding:20px;}");
request->send(404, "text/plain", "ERROR: /system/style.css on SD Card not found!");
}
webrequestActive = false;
@ -856,12 +851,12 @@ void setup()
webrequestActive = true;
deactivateRFID();
activateSD();
String jsPath = "/system/script.js";
String jsPath = getSysDir("script.js");
if (SD.exists(jsPath)) {
request->send(SD, jsPath, "application/javascript");
} else {
// Fallback: serve minimal JS if file not found
request->send(200, "application/javascript", "console.log('JavaScript file not found on SD card');");
request->send(404, "text/plain", "ERROR: /system/script.js on SD Card not found!");
}
webrequestActive = false;
@ -990,9 +985,9 @@ void volume_action(AsyncWebServerRequest *request)
request->send_P(200, "text/plain", "ok");
}
String getStartupSoundDir() {
static String tempPath = "/"+sys_dir+"/"+startup_sound;
return tempPath;
const String getSysDir(const String filename) {
return "/"+sys_dir+"/"+filename;
}
void loop()
@ -1022,7 +1017,7 @@ void loop()
playSongById(currentSongId,currentSongSeconds);
} else if (!startupSoundPlayed) {
startupSoundPlayed = true;
playSongByPath(getStartupSoundDir().c_str());
playSongByPath(getSysDir(startup_sound));
}
@ -1034,12 +1029,12 @@ void loop()
sleepSoundPlayed = true;
prepareSleepMode = true;
if (currentNode != NULL) {
String progressPath = "/"+sys_dir+"/"+progress_file;
String progressPath = getSysDir(progress_file);
writeSongProgress(progressPath.c_str(),currentNode->getCurrentPlayingId(),currentNode->getSecondsPlayed());
}
String tempPath = "/"+sys_dir+"/"+sleep_sound;
String tempPath = getSysDir(sleep_sound);
playSongByPath(tempPath.c_str());
}
@ -1067,7 +1062,7 @@ void loop()
vol++;
}
audio.setVolume(vol);
playSongByPath(getStartupSoundDir().c_str());
playSongByPath(getSysDir(startup_sound));
}
@ -1083,7 +1078,7 @@ void loop()
vol--;
}
audio.setVolume(vol);
playSongByPath(getStartupSoundDir().c_str());
playSongByPath(getSysDir(startup_sound));
}
}

85
src/main.h Normal file
View File

@ -0,0 +1,85 @@
#ifndef MAIN_H_
#define MAIN_H_
void stop();
void start();
bool playFile(const char* filename, uint32_t resumeFilePos = 0);
void loop2(void* parameter);
void id_song_action(AsyncWebServerRequest *request);
void progress_action(AsyncWebServerRequest *request);
void volume_action(AsyncWebServerRequest *request);
boolean buttonPressed(const uint8_t pin);
const String getSysDir(const String filename);
/**
* Helper routine to dump a byte array as hex values to Serial.
*/
void dump_byte_array(byte *buffer, byte bufferSize)
{
for (byte i = 0; i < bufferSize; i++)
{
Serial.print(buffer[i] < 0x10 ? " 0" : " ");
Serial.print(buffer[i], HEX);
}
}
String getRFIDString(byte uidByte[10])
{
String uidString = String(uidByte[0]) + " " + String(uidByte[1]) + " " +
String(uidByte[2]) + " " + String(uidByte[3]);
return uidString;
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
unsigned long lastStart = 0;
unsigned long lastInteraction = 0;
boolean sleepSoundPlayed = false;
boolean startupSoundPlayed = false;
boolean continuousMode = false;
uint8_t buttontoignore = 0;
uint32_t lastVoltage = 0;
uint loopCounter = 0;
String lastUid = "";
uint16_t currentSongId = 0;
uint32_t currentSongSeconds = 0;
boolean continuePlaying = false;
boolean prepareSleepMode = false;
std::map<String, String> rfid_map;
#endif

View File

@ -75,13 +75,13 @@
<input type="text" id="moveFrom" placeholder="/oldname.mp3"/>
<label for="moveTo">To:</label>
<input type="text" id="moveTo" placeholder="/newname.mp3"/>
<button class="action-btn" onclick="moveFile()">Move/Rename</button>
<button type="button" class="action-btn" onclick="moveFile()">Move/Rename</button>
</form>
<form class="form">
<h3>Delete File</h3>
<label for="deleteFile">Filename:</label>
<input type="text" id="deleteFile" placeholder="/song.mp3"/>
<button class="action-btn" onclick="deleteFile()">Delete</button>
<label for="deleteFileName">Filename:</label>
<input type="text" id="deleteFileName" placeholder="/song.mp3"/>
<button type="button" class="action-btn" onclick="deleteFileOnServer()">Delete</button>
</form>
</div>

View File

@ -86,9 +86,10 @@ function displayState(state) {
document.getElementById("uid").innerHTML = 'Last NFC ID: '+state['uid'];
/* ==== Autofill convenience fields ==== */
if (state['filepath']) {
var fm = document.getElementById('fileManager');
if (state['filepath'] && fm.style.display == 'none') {
document.getElementById('moveFrom').value = state['filepath'];
document.getElementById('deleteFile').value = state['filepath'];
document.getElementById('deleteFileName').value = state['filepath'];
document.getElementById('song').value = state['filepath'];
}
if (state['uid']) {
@ -304,8 +305,8 @@ function moveFile() {
xhr.send();
}
function deleteFile() {
var filename = document.getElementById('deleteFile').value.trim();
function deleteFileOnServer() {
var filename = document.getElementById('deleteFileName').value.trim();
if (!filename) {
alert('Please provide filename to delete.');
return;

View File

@ -1,262 +0,0 @@
setInterval(getState, 4000);
setInterval(updateProgress, 500); // Update progress every second
// Get the <li> elements
var liElements = document.querySelectorAll('ul li');
var lastChange = 0;
var lastStateUpdateTime = Date.now();
var songStartTime = 0;
var currentSongLength = 0;
var isPlaying = false;
var userIsInteracting = false; // Flag to track user interaction with the slider
// Add click event listener to each <li> element
liElements.forEach(function(li) {
li.addEventListener('click', function() {
//var liText = this.innerText;
var id = this.dataset.id;
playSongById(id);
});
});
function simpleGetCall(endpoint) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/" + endpoint, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
getState(); // Fetch the latest state right after the button action
}
};
xhr.send();
}
function postValue(endpoint,value) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/" + endpoint, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.send("value="+encodeURIComponent(value));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
getState(); // Fetch the latest state right after the button action
}
};
}
function getState() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var state = JSON.parse(xhr.response);
isPlaying = state['playing'];
if (isPlaying) {
songStartTime = Date.now() - state['time'] * 1000;
currentSongLength = state['length'] * 1000;
}
lastStateUpdateTime = Date.now();
displayState(state);
}
}
xhr.open("GET","/state", true);
xhr.send();
}
function updateProgress() {
if (isPlaying && !userIsInteracting) { // Check if user is not interacting
var elapsedTime = Date.now() - songStartTime;
if (elapsedTime >= currentSongLength) {
elapsedTime = currentSongLength;
isPlaying = false; // Stop updating if the song has ended
}
var progressElement = document.getElementById('progressSlider');
progressElement.value = elapsedTime / 1000; // Convert to seconds
document.getElementById("progressLabel").innerHTML = Math.floor(elapsedTime / 1000);
}
}
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 elements = document.getElementsByClassName('play-button');
var btn = elements[0];
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'];
var volume = document.getElementById('volumeSlider');
volume.value = state['volume'];
}
updateProgress();
}
function playSongById(id) {
var url = "/playbyid";
var params = "id="+id;
var http = new XMLHttpRequest();
http.open("GET", url+"?"+params, true);
http.onreadystatechange = function()
{
if(http.readyState == 4 && http.status == 200) {
getState();
}
}
http.send(null);
}
function playNamedSong(song) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/playnamed");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
//application/x-www-form-urlencoded
var body = song;
xhr.send("title="+encodeURIComponent(body));
}
function editMapping() {
var rfid = document.getElementById('rfid').value;
var song = document.getElementById('song').value;
var xhr = new XMLHttpRequest();
xhr.open("POST", "/edit_mapping", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.send("rfid=" + encodeURIComponent(rfid) + "&song=" + encodeURIComponent(song));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
alert("Mapping updated successfully!");
}
};
}
// Validate file before upload
function validateFile(file) {
var maxSize = 50 * 1024 * 1024; // 50MB limit
var allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/flac', 'audio/mp4', 'audio/ogg'];
var allowedExtensions = ['.mp3', '.wav', '.flac', '.m4a', '.ogg'];
if (file.size > maxSize) {
return 'File too large. Maximum size is 50MB.';
}
var fileName = file.name.toLowerCase();
var hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!hasValidExtension) {
return 'Invalid file type. Only audio files (.mp3, .wav, .flac, .m4a, .ogg) are allowed.';
}
return null; // No error
}
// Handle form submission with AJAX to show upload status
document.getElementById('uploadForm').addEventListener('submit', function(event) {
event.preventDefault(); // Prevent the default form submit
var fileInput = document.getElementById('uploadFile');
var file = fileInput.files[0];
if (!file) {
alert('Please select a file to upload.');
return;
}
var validationError = validateFile(file);
if (validationError) {
alert(validationError);
return;
}
var form = event.target;
var formData = new FormData(form);
var uploadButton = document.getElementById('uploadButton');
var uploadStatus = document.getElementById('uploadStatus');
var uploadProgress = document.getElementById('uploadProgress');
var progressFill = document.getElementById('progressFill');
var progressText = document.getElementById('progressText');
// Disable upload button and show progress
uploadButton.disabled = true;
uploadButton.value = 'Uploading...';
uploadProgress.style.display = 'block';
uploadStatus.innerHTML = 'Preparing upload...';
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);
xhr.upload.onloadstart = function() {
uploadStatus.innerHTML = 'Upload started...';
};
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
var percentComplete = Math.round((event.loaded / event.total) * 100);
progressFill.style.width = percentComplete + '%';
progressText.innerHTML = percentComplete + '%';
uploadStatus.innerHTML = 'Uploading: ' + percentComplete + '% (' +
Math.round(event.loaded / 1024) + 'KB / ' +
Math.round(event.total / 1024) + 'KB)';
}
};
xhr.upload.onerror = function() {
uploadStatus.innerHTML = 'Upload failed due to network error.';
resetUploadForm();
};
xhr.upload.onabort = function() {
uploadStatus.innerHTML = 'Upload was cancelled.';
resetUploadForm();
};
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // Request is done
if (xhr.status >= 200 && xhr.status < 300) { // Success status code range
uploadStatus.innerHTML = 'Upload completed successfully!';
progressFill.style.width = '100%';
progressText.innerHTML = '100%';
setTimeout(function() {
alert('File uploaded successfully!');
location.reload(); // Reload to get updated playlist
}, 1000);
} else {
var errorMsg = xhr.responseText || 'Unknown error occurred';
uploadStatus.innerHTML = 'Upload failed: ' + errorMsg;
alert('Upload failed: ' + errorMsg);
resetUploadForm();
}
}
};
xhr.send(formData); // Send the form data using XMLHttpRequest
});
function resetUploadForm() {
var uploadButton = document.getElementById('uploadButton');
var uploadProgress = document.getElementById('uploadProgress');
var progressFill = document.getElementById('progressFill');
var progressText = document.getElementById('progressText');
uploadButton.disabled = false;
uploadButton.value = 'Upload';
uploadProgress.style.display = 'none';
progressFill.style.width = '0%';
progressText.innerHTML = '0%';
// Clear file input
document.getElementById('uploadFile').value = '';
}