#include "Web_Server.h" #include #include #include "SD_Card.h" #include "Display_PNG.h" #include "Audio_Player.h" WebServer server(80); File uploadFile; bool uploadSuccess = false; String uploadedFilename = ""; volatile int g_webDisplayRequest = -1; volatile bool g_webDisplayDirty = false; static String buildPage(const String& status, const String& file); // ── Helpers ─────────────────────────────────────────────────────────────────── static bool allowedPhoto(const String& name) { String l = name; l.toLowerCase(); return l.endsWith(".png") || l.endsWith(".jpg") || l.endsWith(".jpeg"); } static bool allowedAudio(const String& name) { String l = name; l.toLowerCase(); return l.endsWith(".mp3") || l.endsWith(".wav"); } static String memoriesJson() { String j = "["; for (int i = 0; i < g_memoryCount; i++) { if (i > 0) j += ","; j += "{\"id\":"; j += g_memories[i].id; j += ",\"name\":\""; j += g_memories[i].name; j += "\",\"photo\":\""; j += g_memories[i].photo; j += "\",\"audio\":\""; j += g_memories[i].audio; j += "\"}"; } j += "]"; return j; } // ── Single-page app HTML ────────────────────────────────────────────────────── static const char APP_HTML[] PROGMEM = R"HTML( Sunflower Keepsake Frame

🌻 Add Keepsake

Create a beautiful photo-sound memorial

Step 1 of 3 — Choose a Photo
📸

Tap to select any photo
from your library or camera

preview
Preparing photo...
Step 2 of 3 — Choose or Record a Soundbite (Optional)
🎙️

Tap to **Record Voice Note** directly from mic

📁

Tap to upload an existing sound clip
.mp3 or .wav file

Uploading audio...
Step 3 of 3 — Name This Memory

🌻 Settings

Configure frame preferences

Active Memory
-
Volume
Speaker volume for audio playback
12 / 21
🌻 Tribute Time Alarm
Select a daily time to play through all memories automatically
Enable Daily Tribute
Scheduled Time
)HTML"; static String pendingUploadPath = ""; static void handleUploadFile(const String& subdir, bool (*validator)(const String&)) { HTTPUpload& up = server.upload(); if (up.status == UPLOAD_FILE_START) { String fname = up.filename; uploadSuccess = false; uploadedFilename = ""; if (!validator(fname)) { printf("Upload: rejected %s\r\n", fname.c_str()); return; } pendingUploadPath = "/" + subdir + "/" + fname; uploadFile = SD_MMC.open(pendingUploadPath, "w"); uploadSuccess = (bool)uploadFile; if (uploadSuccess) uploadedFilename = fname; printf("Upload: writing to %s\r\n", pendingUploadPath.c_str()); } else if (up.status == UPLOAD_FILE_WRITE) { if (uploadFile) uploadFile.write(up.buf, up.currentSize); } else if (up.status == UPLOAD_FILE_END) { if (uploadFile) { uploadFile.close(); printf("Upload: done (%u bytes)\r\n", up.totalSize); } else { uploadSuccess = false; } } } void initWebServer() { WiFi.softAP("love", "love1234"); IPAddress IP = WiFi.softAPIP(); printf("WiFi AP Started. SSID: 'love' Password: 'love1234'\r\n"); printf("Web Server IP: %s\r\n", IP.toString().c_str()); server.on("/", HTTP_GET, []() { String status = server.hasArg("status") ? server.arg("status") : ""; String file = server.hasArg("file") ? server.arg("file") : ""; server.send(200, "text/html", buildPage(status, file)); }); server.on("/api/memories", HTTP_GET, []() { server.send(200, "application/json", memoriesJson()); }); server.on("/api/settings", HTTP_GET, []() { String name = ""; if (g_currentIndex >= 0 && g_currentIndex < g_memoryCount) name = String(g_memories[g_currentIndex].name); String j = "{\"volume\":"; j += g_volume; j += ",\"currentIndex\":"; j += g_currentIndex; j += ",\"currentName\":\""; j += name; j += "\",\"alarmEnabled\":"; j += g_tributeAlarmEnabled ? "true" : "false"; j += ",\"alarmHour\":"; j += g_tributeAlarmHour; j += ",\"alarmMinute\":"; j += g_tributeAlarmMinute; j += ",\"timeSynced\":"; j += g_timeSynchronized ? "true" : "false"; j += ",\"favoriteId\":"; j += g_favoriteMemoryId; j += "}"; server.send(200, "application/json", j); }); server.on("/api/settings", HTTP_POST, []() { String body = server.arg("plain"); // Volume int vi = body.indexOf("\"volume\":"); if (vi >= 0) { int v = body.substring(vi + 9).toInt(); if (v < 0) v = 0; if (v > 21) v = 21; setPlayerVolume((uint8_t)v); } // Alarm Enabled int ae = body.indexOf("\"alarmEnabled\":"); if (ae >= 0) { String sub = body.substring(ae + 15); g_tributeAlarmEnabled = sub.startsWith("true"); } // Alarm Hour int ah = body.indexOf("\"alarmHour\":"); if (ah >= 0) { g_tributeAlarmHour = body.substring(ah + 12).toInt(); } // Alarm Minute int am = body.indexOf("\"alarmMinute\":"); if (am >= 0) { g_tributeAlarmMinute = body.substring(am + 14).toInt(); } // Favorite Memory ID int fav = body.indexOf("\"favoriteId\":"); if (fav >= 0) { g_favoriteMemoryId = body.substring(fav + 13).toInt(); } Config_Save(); server.send(200, "application/json", "{\"ok\":true}"); }); server.on("/api/time", HTTP_POST, []() { String body = server.arg("plain"); int ep = body.indexOf("\"epoch\":"); int os = body.indexOf("\"offset\":"); if (ep >= 0 && os >= 0) { long long epoch = strtoll(body.substring(ep + 8).c_str(), NULL, 10); int offset = body.substring(os + 9).toInt(); #include struct timeval tv; tv.tv_sec = epoch; tv.tv_usec = 0; settimeofday(&tv, NULL); g_timezoneOffset = offset; g_timeSynchronized = true; printf("Time Synced: UTC Epoch %lld, Local offset %d sec\r\n", epoch, offset); Config_Save(); } server.send(200, "application/json", "{\"ok\":true}"); }); server.on("/api/upload/photo", HTTP_POST, []() { if (uploadSuccess) server.send(200, "application/json", "{\"ok\":true,\"filename\":\"" + uploadedFilename + "\"}"); else server.send(400, "application/json", "{\"ok\":false,\"error\":\"upload failed\"}"); }, []() { handleUploadFile("photos", allowedPhoto); } ); server.on("/api/upload/audio", HTTP_POST, []() { if (uploadSuccess) server.send(200, "application/json", "{\"ok\":true,\"filename\":\"" + uploadedFilename + "\"}"); else server.send(400, "application/json", "{\"ok\":false,\"error\":\"upload failed\"}"); }, []() { handleUploadFile("audio", allowedAudio); } ); server.on("/api/memory", HTTP_POST, []() { String body = server.arg("plain"); auto extract = [&](const char* key) -> String { String k = String("\"") + key + "\":\""; int p = body.indexOf(k); if (p < 0) return ""; int s = p + k.length(); int e = body.indexOf('"', s); if (e < 0) return ""; return body.substring(s, e); }; String name = extract("name"); String photo = extract("photo"); String audio = extract("audio"); if (name.isEmpty() || photo.isEmpty()) { server.send(400, "application/json", "{\"ok\":false,\"error\":\"missing name or photo\"}"); return; } int id = Config_Add_Memory(name.c_str(), photo.c_str(), audio.c_str()); if (id > 0) server.send(200, "application/json", "{\"ok\":true,\"id\":" + String(id) + "}"); else server.send(500, "application/json", "{\"ok\":false,\"error\":\"too many memories\"}"); }); server.on("/api/memory", HTTP_DELETE, []() { if (!server.hasArg("id")) { server.send(400, "application/json", "{\"ok\":false}"); return; } int id = server.arg("id").toInt(); bool ok = Config_Delete_Memory(id); server.send(200, "application/json", ok ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"not found\"}"); }); server.on("/api/display", HTTP_POST, []() { if (!server.hasArg("id")) { server.send(400, "application/json", "{\"ok\":false}"); return; } int id = server.arg("id").toInt(); for (int i = 0; i < g_memoryCount; i++) { if (g_memories[i].id == id) { g_currentIndex = i; g_webDisplayRequest = i; g_webDisplayDirty = true; Config_Save(); server.send(200, "application/json", "{\"ok\":true}"); return; } } server.send(404, "application/json", "{\"ok\":false,\"error\":\"memory not found\"}"); }); server.on("/photo", HTTP_GET, []() { String name = server.pathArg(0); if (name.isEmpty() && server.hasArg("f")) name = server.arg("f"); String uri = server.uri(); if (name.isEmpty()) { int slash = uri.lastIndexOf('/'); if (slash >= 0) name = uri.substring(slash + 1); } if (name.isEmpty()) { server.send(404, "text/plain", "Not found"); return; } String path = "/photos/" + name; File f = SD_MMC.open(path, "r"); if (!f) { server.send(404, "text/plain", "Not found"); return; } server.streamFile(f, "image/png"); f.close(); }); server.onNotFound([]() { String uri = server.uri(); if (uri.startsWith("/photo/")) { String name = uri.substring(7); String path = "/photos/" + name; File f = SD_MMC.open(path, "r"); if (f) { String ct = name.endsWith(".png") ? "image/png" : "image/jpeg"; server.streamFile(f, ct); f.close(); return; } } server.send(404, "text/plain", "Not found"); }); server.begin(); } static String buildPage(const String& status, const String& file) { String toast = ""; if (status == "ok") { String safeName = file; safeName.replace("<", "<"); safeName.replace(">", ">"); toast = "
✅ \"" + safeName + "\" uploaded successfully!
"; } else if (status == "fail") { toast = "
❌ Upload failed. Only .png, .mp3 and .wav files are accepted.
"; } String page = String(APP_HTML); page.replace("%TOAST%", toast); return page; } void handleWebServer() { server.handleClient(); }