Review comments

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
jpirnay
2026-05-02 17:22:10 +02:00
co-authored by Copilot
parent 89f381a925
commit a5469c0bb9
11 changed files with 374 additions and 322 deletions
+198
View File
@@ -12,17 +12,22 @@
#include <cstring>
#include "CrossPointSettings.h"
#include "FontInstaller.h"
#include "OpdsServerStore.h"
#include "SdCardFontGlobals.h"
#include "SdCardFontRegistry.h"
#include "SettingsList.h"
#include "SystemStatus.h"
#include "WebDAVHandler.h"
#include "WifiCredentialStore.h"
#include "html/FilesPageHtml.generated.h"
#include "html/FontsPageHtml.generated.h"
#include "html/HomePageHtml.generated.h"
#include "html/SettingsPageHtml.generated.h"
#include "html/WelcomePageHtml.generated.h"
#include "html/js/jszip_minJs.generated.h"
namespace {
// Folders/files to hide from the web interface file browser
// Note: Items starting with "." are automatically hidden
@@ -196,6 +201,12 @@ void CrossPointWebServer::begin() {
server->on("/api/settings", HTTP_GET, [this] { handleGetSettings(); });
server->on("/api/settings", HTTP_POST, [this] { handlePostSettings(); });
// Font management endpoints
server->on("/fonts", HTTP_GET, [this] { handleFontsPage(); });
server->on("/api/fonts", HTTP_GET, [this] { handleFontList(); });
server->on("/api/fonts/upload", HTTP_POST, [this] { handleFontUpload(); }, [this] { handleFontUploadData(); });
server->on("/api/fonts/delete", HTTP_POST, [this] { handleFontDelete(); });
// OPDS server endpoints
server->on("/api/opds", HTTP_GET, [this] { handleGetOpdsServers(); });
server->on("/api/opds", HTTP_POST, [this] { handlePostOpdsServer(); });
@@ -1399,6 +1410,193 @@ void CrossPointWebServer::handlePostSettings() {
server->send(200, "text/plain", String("Applied ") + String(applied) + " setting(s)");
}
// ---- Font Management API ----
void CrossPointWebServer::handleFontsPage() const {
sendHtmlContent(server.get(), FontsPageHtml, sizeof(FontsPageHtml));
LOG_DBG("WEB", "Served fonts page");
}
void CrossPointWebServer::handleFontList() {
FontInstaller installer(sdFontSystem.registry());
installer.refreshRegistry();
const auto& families = sdFontSystem.registry().getFamilies();
JsonDocument doc;
JsonArray arr = doc["families"].to<JsonArray>();
doc["maxFamilies"] = SdCardFontRegistry::MAX_SD_FAMILIES;
for (const auto& family : families) {
JsonObject fObj = arr.add<JsonObject>();
fObj["name"] = family.name;
JsonArray sizes = fObj["sizes"].to<JsonArray>();
for (uint8_t s : family.availableSizes()) {
sizes.add(s);
}
JsonArray files = fObj["files"].to<JsonArray>();
for (const auto& file : family.files) {
JsonObject fileObj = files.add<JsonObject>();
const char* name = strrchr(file.path.c_str(), '/');
fileObj["name"] = name ? name + 1 : file.path.c_str();
FsFile f;
if (Storage.openFileForRead("WEB", file.path.c_str(), f)) {
fileObj["size"] = static_cast<unsigned long>(f.size());
f.close();
} else {
fileObj["size"] = 0;
}
}
}
String json;
serializeJson(doc, json);
server->send(200, "application/json", json);
}
void CrossPointWebServer::handleFontUploadData() {
HTTPUpload& up = server->upload();
switch (up.status) {
case UPLOAD_FILE_START: {
esp_task_wdt_reset();
String family = server->arg("family");
fontUpload.valid = false;
fontUpload.magicChecked = false;
fontUpload.bytesWritten = 0;
fontUpload.bufferPos = 0;
if (!FontInstaller::isValidFamilyName(family.c_str())) {
LOG_ERR("WEB", "Invalid font family name: %s", family.c_str());
break;
}
String filename = up.filename;
if (!filename.endsWith(".cpfont")) {
LOG_ERR("WEB", "Not a .cpfont file: %s", filename.c_str());
break;
}
fontUpload.familyName = family.c_str();
FontInstaller installer(sdFontSystem.registry());
if (!installer.ensureFamilyDir(family.c_str())) {
LOG_ERR("WEB", "Failed to create font family dir");
break;
}
char path[128];
FontInstaller::buildFontPath(family.c_str(), filename.c_str(), path, sizeof(path));
fontUpload.filePath = path;
if (!Storage.openFileForWrite("WEB", path, fontUpload.file)) {
LOG_ERR("WEB", "Failed to open font file for write: %s", path);
break;
}
fontUpload.valid = true;
LOG_DBG("WEB", "Font upload started: %s -> %s", filename.c_str(), path);
break;
}
case UPLOAD_FILE_WRITE: {
if (!fontUpload.valid) break;
esp_task_wdt_reset();
if (!fontUpload.magicChecked && up.currentSize >= 8) {
if (memcmp(up.buf, "CPFONT\0\0", 8) != 0) {
LOG_ERR("WEB", "Invalid .cpfont magic bytes");
fontUpload.valid = false;
break;
}
fontUpload.magicChecked = true;
}
size_t remaining = up.currentSize;
const uint8_t* src = up.buf;
while (remaining > 0) {
size_t space = FontUploadState::BUFFER_SIZE - fontUpload.bufferPos;
size_t chunk = (remaining < space) ? remaining : space;
memcpy(fontUpload.buffer.data() + fontUpload.bufferPos, src, chunk);
fontUpload.bufferPos += chunk;
src += chunk;
remaining -= chunk;
if (fontUpload.bufferPos >= FontUploadState::BUFFER_SIZE) {
fontUpload.file.write(fontUpload.buffer.data(), fontUpload.bufferPos);
fontUpload.bytesWritten += fontUpload.bufferPos;
fontUpload.bufferPos = 0;
esp_task_wdt_reset();
}
}
break;
}
case UPLOAD_FILE_END: {
if (fontUpload.valid && fontUpload.bufferPos > 0) {
fontUpload.file.write(fontUpload.buffer.data(), fontUpload.bufferPos);
fontUpload.bytesWritten += fontUpload.bufferPos;
fontUpload.bufferPos = 0;
}
fontUpload.file.close();
if (!fontUpload.valid && !fontUpload.filePath.empty()) {
Storage.remove(fontUpload.filePath.c_str());
}
LOG_DBG("WEB", "Font upload end: valid=%d, %zu bytes", fontUpload.valid, fontUpload.bytesWritten);
break;
}
case UPLOAD_FILE_ABORTED: {
fontUpload.file.close();
if (!fontUpload.filePath.empty()) {
Storage.remove(fontUpload.filePath.c_str());
}
fontUpload.valid = false;
LOG_DBG("WEB", "Font upload aborted");
break;
}
}
}
void CrossPointWebServer::handleFontUpload() {
if (fontUpload.valid) {
FontInstaller installer(sdFontSystem.registry());
installer.refreshRegistry();
server->send(200, "application/json", "{\"ok\":true}");
LOG_DBG("WEB", "Font upload complete: %s", fontUpload.filePath.c_str());
} else {
server->send(400, "application/json", "{\"error\":\"Invalid .cpfont file\"}");
}
}
void CrossPointWebServer::handleFontDelete() {
String body = server->arg("plain");
JsonDocument doc;
DeserializationError err = deserializeJson(doc, body);
if (err || !doc["family"].is<const char*>()) {
server->send(400, "application/json", "{\"error\":\"Invalid request\"}");
return;
}
const char* familyName = doc["family"];
FontInstaller installer(sdFontSystem.registry());
auto result = installer.deleteFamily(familyName);
if (result == FontInstaller::Error::OK) {
installer.refreshRegistry();
server->send(200, "application/json", "{\"ok\":true}");
LOG_DBG("WEB", "Deleted font family: %s", familyName);
} else {
server->send(500, "application/json", "{\"error\":\"Delete failed\"}");
LOG_ERR("WEB", "Failed to delete font family: %s", familyName);
}
}
// ---- Wi-Fi Credentials API ----
void CrossPointWebServer::handleGetWifiNetworks() const {
+21
View File
@@ -110,6 +110,27 @@ class CrossPointWebServer {
void handleGetSettings() const;
void handlePostSettings();
// Font management handlers
void handleFontsPage() const;
void handleFontList();
void handleFontUpload();
void handleFontUploadData();
void handleFontDelete();
struct FontUploadState {
FsFile file;
std::string familyName;
std::string filePath;
bool valid = false;
bool magicChecked = false;
size_t bytesWritten = 0;
static constexpr size_t BUFFER_SIZE = 4096;
std::vector<uint8_t> buffer;
size_t bufferPos = 0;
FontUploadState() { buffer.resize(BUFFER_SIZE); }
} fontUpload;
// OPDS server handlers
void handleGetOpdsServers() const;
void handlePostOpdsServer();
+45 -14
View File
@@ -181,22 +181,44 @@
const res = await fetch('/api/fonts');
const data = await res.json();
const el = document.getElementById('families');
el.innerHTML = '';
if (!data.families || data.families.length === 0) {
el.innerHTML = '<p class="empty">No fonts installed</p>';
const p = document.createElement('p');
p.className = 'empty';
p.textContent = 'No fonts installed';
el.appendChild(p);
return;
}
el.innerHTML = data.families.map(f => `
<div class="family">
<div class="family-info">
<h3>${f.name}</h3>
<span class="family-meta">
${f.sizes.join(', ')}pt &middot;
${f.files.map(fi => formatSize(fi.size)).join(' + ')}
</span>
</div>
<button class="btn btn-danger" data-family="${f.name}">Delete</button>
</div>
`).join('');
data.families.forEach(f => {
const familyRow = document.createElement('div');
familyRow.className = 'family';
const info = document.createElement('div');
info.className = 'family-info';
const title = document.createElement('h3');
title.textContent = f.name;
const meta = document.createElement('span');
meta.className = 'family-meta';
const sizes = Array.isArray(f.sizes) ? f.sizes.join(', ') : '';
const fileSizes = Array.isArray(f.files) ? f.files.map(fi => formatSize(fi.size)).join(' + ') : '';
meta.textContent = sizes + 'pt - ' + fileSizes;
info.appendChild(title);
info.appendChild(meta);
const delBtn = document.createElement('button');
delBtn.className = 'btn btn-danger';
delBtn.dataset.family = f.name;
delBtn.textContent = 'Delete';
familyRow.appendChild(info);
familyRow.appendChild(delBtn);
el.appendChild(familyRow);
});
el.querySelectorAll('button[data-family]').forEach(btn => {
btn.addEventListener('click', () => deleteFamily(btn.dataset.family));
});
@@ -272,7 +294,16 @@
return;
}
const family = sanitizeFamily(familyFromFilename(files[0].name));
const families = files.map(f => sanitizeFamily(familyFromFilename(f.name)));
const uniqueFamilies = new Set(families);
if (uniqueFamilies.size !== 1) {
status.className = 'status-err';
status.style.display = 'block';
status.textContent = 'Selected files belong to multiple families.';
return;
}
const family = families[0];
status.className = '';
status.style.display = 'block';
+1
View File
@@ -107,6 +107,7 @@
<div class="nav-links">
<a href="/files">Open File Manager</a>
<a href="/settings">Settings</a>
<a href="/fonts">Font Manager</a>
<a href="/systeminfo">System Info</a>
</div>
<div class="footer">Fast start page designed for weak connections</div>