### Initial Device Setup and Polling Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Calls the initialization function and sets up interval polling for battery status updates every 60 seconds. ```javascript init(); // Battery polling every 60 seconds setInterval(updateBattery, 60000); ``` -------------------------------- ### Save Wi-Fi Setup Configuration Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Saves the current Wi-Fi setup configuration from the UI elements to the global configuration object. ```javascript async function saveSetupConfig() { const bootSoundToggle = document.getElementById("wifi-page-boot-sound-toggle"); const deviceNameInput = document.getElementById("device-name-input"); if (!bootSoundToggle || !deviceNameInput) return; globalConfig.boot_sound = !!bootSoundToggle.checked; globalConfig.device_name = syncDeviceNameInput(deviceNameInput); } ``` -------------------------------- ### Load Wi-Fi Setup Configuration Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Asynchronously loads the current Wi-Fi status and configuration, populating the UI elements. Handles potential API call errors. ```javascript async function loadSetupConfig() { const bootSoundToggle = document.getElementById("wifi-page-boot-sound-toggle"); const deviceNameInput = document.getElementById("device-name-input"); const ssidInput = document.getElementById("ssid-input"); const pwdInput = document.getElementById("pwd-input"); if (!bootSoundToggle || !deviceNameInput) return; try { const status = await apiCall("GET", "/api/wifi/status"); if (typeof status.boot_sound === "boolean") { globalConfig.boot_sound = status.boot_sound; } globalConfig.device_name = sanitizeDeviceNameInput(status.device_name || "papercolor") || "papercolor"; wifiOriginalSsid = status.ssid || ""; if (ssidInput && wifiOriginalSsid) { ssidInput.value = wifiOriginalSsid; } if (pwdInput) { pwdInput.value = ""; pwdInput.placeholder = wifiOriginalSsid ? "(unchanged)" : "Wi-Fi password"; } } catch(e) {} bootSoundToggle.checked = !!globalConfig.boot_sound; deviceNameInput.value = globalConfig.device_name || "papercolor"; syncDeviceNameInput(deviceNameInput); bindWifiSsidInput(); } ``` -------------------------------- ### Device Initialization Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Initializes the M5PaperColor device by loading Wi-Fi modes, setup configuration, and device status. Handles entering ready mode or switching to a landing page based on device readiness. ```javascript // ==================== Initialization ==================== async function init() { bindDeviceNameInput("device-name-input"); await Promise.all([loadWifiModes(), loadSetupConfig()]); updateBattery(); try { const [ready, status] = await Promise.all([ fetchDeviceReady(), refreshStatusBar() ]); if (status) updateStatusBar(status); if (ready.ui && ready.ui.can_enter_mode && ready.mode && ready.mode.current) { enterReadyMode(ready.mode.current); loadStorage(); requestAnimationFrame(() => { if (currentMode === "mode_1") applyModeConfig(); }); } else { switchPage("landing-page"); } } catch(e) { switchPage("landing-page"); showToast("Cannot reach server. Run: python server.py", "error"); } } ``` -------------------------------- ### Show Mode Switch Modal Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Fetches the list of available modes and displays them in a modal for user selection. If no modes are loaded, it first calls the API to get them. ```javascript async function showModeSwitch() { if (modeList.length === 0) { try { const d = await apiCall("GET", "/api/modes"); modeList = d.modes; } catch(e) { return; } } const list = document.getElementById("mode-modal-list"); list.innerHTML = modeList.map(m => `` ).join(""); document.getElementById("mode-modal").classList.remove("hidden"); } function hideModeSwitch(e) { if (e && e.target !== document.getElementById("mode-modal")) return; document.getElementById("mode-modal").classList.add("hidden"); } ``` -------------------------------- ### Show Mode Configuration Panel Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Hides all mode configuration panels and then shows the specific panel corresponding to the given mode ID. It also handles starting or stopping background polling based on the mode. ```javascript function showModeConfig(modeId) { document.querySelectorAll(".mode-config").forEach(el => { el.classList.add("hidden"); }); const panel = document.querySelector(".mode-config\[data-mode=" + modeId + "]"); if (panel) panel.classList.remove("hidden"); stopEzdataPolling(); if (modeId === "mode_1") { applyModeConfig(); loadModeConfig(); loadPhotoList(); loadStorage(); } if (modeId === "mode_2") { loadMode2Config(); startEzdataPolling(); } } ``` -------------------------------- ### Start Wi-Fi Status Polling Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Initiates a polling mechanism to check the device's readiness and Wi-Fi status. Resolves or rejects a promise based on the device state. Clears existing timers. ```javascript function startPolling(options) { if (pollingTimer) clearInterval(pollingTimer); const statusDiv = document.getElementById("conn-status"); const btn = document.getElementById("confirm-btn"); const targetMode = options?.targetMode || lockedWifiMode || selectedWifiMode; const successMessage = options?.successMessage || "Ready! Redirecting..."; const loadingMessage = options?.loadingMessage || "Connecting..."; return new Promise((resolve, reject) => { pollingTimer = setInterval(async () => { try { const ready = await fetchDeviceReady(); await refreshStatusBar(); if (ready.ui && ready.ui.can_enter_mode && ready.mode && ready.mode.current) { clearInterval(pollingTimer); pollingTimer = null; statusDiv.classList.remove("error"); statusDiv.classList.add("success"); statusDiv.innerHTML = successMessage; btn.disabled = false; btn.textContent = "Confirm"; setTimeout(() => { enterReadyMode(ready.mode.current || targetMode); statusDiv.classList.add("hidden"); statusDiv.classList.remove("success"); }, 1000); resolve(ready); return; } if (ready.wifi && ready.wifi.state === "failed") { clearInterval(pollingTimer); pollingTimer = null; statusDiv.classList.remove("success"); statusDiv.classList.add("error"); statusDiv.innerHTML = "Connection failed: " + errorMessage(ready.wifi.error); btn.disabled = false; btn.textContent = "Confirm"; showToast("Connection failed: " + errorMessage(ready.wifi ``` -------------------------------- ### Get Photos Per Page - JavaScript Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Determines the number of photos to display per page based on screen width. Desktop screens show fewer photos per page than smaller screens. ```javascript function getPhotoPerPage() { const isDesktop = window.innerWidth >= 1200; if (!isDesktop) return 16; return 12; } ``` -------------------------------- ### Highlight Wi-Fi Mode Button Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html A convenience function to select and highlight a Wi-Fi mode button, likely used for initial setup or default selection. ```javascript function highlightModeButton(modeId) { selectWifiMode(modeId); } ``` -------------------------------- ### Get EPD Color Pair for Two Colors Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Calculates the best matching pair of colors from the EPD_PALETTE for two input RGB color values. It minimizes the distance between the input colors and the chosen palette colors, considering both individual color distances and the distance between the pair. ```javascript function getEpdColorPair(r0, g0, b0, r1, g1, b1) { let minDistance = Infinity; let bestFirst = EPD_PALETTE[1]; let bestSecond = EPD_PALETTE[1]; const firstDiffs = EPD_PALETTE.map(color => { const dr = r0 - color[0]; const dg = g0 - color[1]; const db = b0 - color[2]; return { dr, dg, db, indiv: dr * dr + dg * dg + db * db }; }); const secondDiffs = EPD_PALETTE.map(color => { const dr = r1 - color[0]; const dg = g1 - color[1]; const db = b1 - color[2]; return { dr, dg, db, indiv: dr * dr + dg * dg + db * db }; }); for (let i = 0; i < EPD_PALETTE.length; i++) { for (let j = 0; j < EPD_PALETTE.length; j++) { const dr = firstDiffs[i].dr + secondDiffs[j].dr; const dg = firstDiffs[i].dg + secondDiffs[j].dg; const db = firstDiffs[i].db + secondDiffs[j].db; const distance = (dr * dr + dg * dg + db * db) + firstDiffs[i].indiv + secondDiffs[j].indiv; if (distance < minDistance) { minDistance = distance; bestFirst = EPD_PALETTE[i]; bestSecond = EPD_PALETTE[j]; } } } return [bestFirst, bestSecond]; } ``` -------------------------------- ### EZData Polling Control Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Starts or stops polling for EZData configuration. Uses setInterval for periodic updates. ```javascript function startEzdataPolling() { if (ezdataPollTimer) return; ezdataPollTimer = setInterval(loadMode2Config, 3000); } function stopEzdataPolling() { if (ezdataPollTimer) { clearInterval(ezdataPollTimer); ezdataPollTimer = null; } } ``` -------------------------------- ### Build Project Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/README.md Build the project using the idf.py tool. This command compiles the source code and prepares it for flashing. ```bash idf.py build ``` -------------------------------- ### Get Canvas Size Based on Orientation Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Returns the canvas dimensions based on the current orientation setting. Used for applying mode configurations. ```javascript function getCanvasSize() { return modeConfig.orientation === "portrait" ? [400, 600] : [600, 400]; } ``` -------------------------------- ### Flash Project Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/README.md Flash the built project onto the M5PaperColor device using the idf.py tool. Ensure the device is connected. ```bash idf.py flash ``` -------------------------------- ### Enter Ready Mode Function Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Transitions the device into a specific operational mode. It clears certain states and navigates the UI to the relevant mode configuration page. ```javascript function enterReadyMode(modeId) { if (!modeId) return; hideToast(); currentMode = modeId; lockedWifiMode = null; switchPage("mode-page"); showModeConfig(currentMode); } ``` -------------------------------- ### Initialize Photo Album Preview Upload Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Sets up event listeners for uploading images to a photo album via drag-and-drop or click. It also handles the visibility and functionality of a floating remove button for selected images. ```javascript (function initPreviewUpload() { const container = document.getElementById("preview-container"); const floatingRemoveButton = getFloatingRemoveButton(); if (floatingRemoveButton) { floatingRemoveButton.addEventListener("click", event => { event.stopPropagation(); const layer = getSelectedImageLayer(); if (layer) removeImageLayer(layer.id); }); } if (!container) return; container.addEventListener("click", e => { const clickedLayer = e.target.closest(".preview-layer"); const clickedRemoveBtn = e.target.closest(".layer-remove-btn"); if (clickedLayer || clickedRemoveBtn || hasCompositionImages()) return; document.getElementById("file-input").click(); }); container.addEventListener("dragover", e => { e.preventDefault(); container.style.borderColor = "#336699"; }); container.addEventListener("dragleave", () => { container.style.borderColor = ""; }); container.addEventLi }) ``` -------------------------------- ### Connect to Wi-Fi Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Initiates the Wi-Fi connection process using provided SSID, password, and mode. Handles both direct connection and mode entry scenarios. Displays connection status and manages UI states. ```javascript async function connectWiFi() { const ssid = document.getElementById("ssid-input").value.trim(); const password = document.getElementById("pwd-input").value; const mode = lockedWifiMode || selectedWifiMode; const deviceName = syncDeviceNameInput(document.getElementById("device-name-input")); if (!mode) { showToast("Please select a mode", "error"); return; } const statusDiv = document.getElementById("conn-status"); const btn = document.getElementById("confirm-btn"); if (!ssid) { if (mode !== "mode_1") { showToast("This mode requires a Wi-Fi connection", "error"); return; } statusDiv.classList.remove("hidden", "success", "error"); statusDiv.innerHTML = ' Entering...'; btn.disabled = true; btn.textContent = "Entering..."; try { await apiCall("POST", "/api/wifi/config", { mode, boot_sound: !!globalConfig.boot_sound, device_name: deviceName }); await startPolling({ targetMode: mode, successMessage: "Ready! Redirecting...", loadingMessage: "Entering..." }); } catch(e) { showToast(e.message, "error"); btn.disabled = false; btn.textContent = "Confirm"; statusDiv.classList.add("hidden"); } return; } statusDiv.classList.remove("hidden", "success", "error"); statusDiv.innerHTML = ' Connecting...'; btn.disabled = true; btn.textContent = "Connecting..."; try { await apiCall("POST", "/api/wifi/config", { ssid, password, mode, boot_sound: !!globalConfig.boot_sound, device_name: deviceName }); await startPolling({ targetMode: mode, successMessage: "Connected! Redirecting...", loadingMessage: "Connecting..." }); } catch(e) { showToast(e.message, "error"); btn.disabled = false; btn.textContent = "Confirm"; statusDiv.classList.add("hidden"); } } ``` -------------------------------- ### Show Settings Modal Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Displays the settings modal and pre-fills its fields with current configuration values. It also handles conditional visibility of Wi-Fi settings. ```javascript function showSettings() { const m = document.getElementById("settings-modal"); // Pre-fill Display updateOrientSeg(modeConfig.orientation); document.getElementById("auto-toggle").checked = modeConfig.auto_slideshow; document.getElementById("low-power-toggle").checked = !!modeConfig.low_power_mode; document.getElementById("interval-input").value = modeConfig.interval_minutes; document.getElementById("interval-group").classList.toggle("hidden", !modeConfig.auto_slideshow); // Pre-fill Wi-Fi & Device (async) loadSettingsDeviceInfo(); // Show/hide Wi-Fi section const wifiSection = document.getElementById("settings-wifi-section"); if (wifiSection) { wifiSection.classList.toggle("hidden", currentMode === "mode_1"); } // Reset Save button const saveBtn = document.getElementById("settings-save-btn"); if (saveBtn) saveBtn.classList.add("hidden"); document.body.style.overflow = "hidden"; m.classList.remove("hidden"); } ``` -------------------------------- ### Create Preview Image Layer Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Creates a new image layer for the preview composition. It appends the layer to the DOM and adds event listeners for selection and removal. ```javascript function createPreviewLayer(src, naturalWidth, naturalHeight) { const composition = getPreviewComposition(); const layerId = nextCompositionImageId++; const layerEl = document.createElement("div"); layerEl.className = "preview-layer"; layerEl.dataset.layerId = String(layerId); const img = document.createElement("img"); img.src = src; img.alt = "Preview layer"; const removeBtn = document.createElement("button"); removeBtn.className = "layer-remove-btn"; removeBtn.type = "button"; removeBtn.textContent = "×"; removeBtn.title = "Remove image"; removeBtn.addEventListener("click", event => { event.stopPropagation(); removeImageLayer(layerId); }); layerEl.appendChild(img); layerEl.appendChild(removeBtn); layerEl.addEventListener("pointerdown", () => selectImageLayer(layerId, true)); layerEl.addEventListener("mousedown", () => selectImageLayer(layerId, true)); layerEl.addEventListener("touchstart", () => selectImageLayer(layerId, true), { passive: true }); composition.appendChild(layerEl); const layer = { id: layerId, src, naturalWidth, naturalHeight, baseFitScale: 1, baseWidth: naturalWidth, baseHeight: naturalHeight, transform: { x: 0, y: 0, scale: 1, rotation: 0 }, el: layerEl, imgEl: img, }; compositionImages.push(layer); fitImageToContainer(layer, true); syncPlaceholderState(); selectImageLayer(layerId, true); } ``` -------------------------------- ### Save Device Name and Boot Sound Settings Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Handles changes to the device name and boot sound settings. It updates the global configuration and sends the changes to the API via a POST request. ```javascript function onSettingsDeviceNameChange() { const input = document.getElementById("settings-device-name-input"); const name = syncDeviceNameInput(input); globalConfig.device_name = name; apiCall("POST", "/api/wifi/config", { device_name: name }) .then(() => showToast("Saved", "success")) .catch(e => showToast(e.message, "error")); } ``` ```javascript function onSettingsBootSoundChange() { const on = document.getElementById("settings-boot-sound-toggle").checked; globalConfig.boot_sound = on; apiCall("POST", "/api/wifi/config", { boot_sound: on }) .then(() => showToast("Saved", "success")) .catch(e => showToast(e.message, "error")); } ``` -------------------------------- ### Load Storage Information Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Fetches and displays storage usage details, including percentage used, used/total MB, and free MB. It also applies visual warnings for low or nearly full storage. ```javascript async function loadStorage() { try { const info = await apiCall("GET", "/api/storage"); const pct = info.total > 0 ? (info.used / info.total * 100) : 0; const usedMB = (info.used / 1024 / 1024).toFixed(1); const totalMB = (info.total / 1024 / 1024).toFixed(1); const freeMB = (info.free / 1024 / 1024).toFixed(1); const bar = document.getElementById("storage-bar"); const detail = document.getElementById("storage-detail"); bar.style.width = pct.toFixed(1) + "%" bar.classList.remove("warning", "danger"); let status = ""; if (pct > 90) { bar.classList.add("danger"); status = "Nearly full"; } else if (pct > 70) { bar.classList.add("warning"); status = "Running low"; } detail.innerHTML = `${status} · Used ${usedMB} / ${totalMB} MB`; return info; } catch(e) { /* silent */ } return null; } ``` -------------------------------- ### Load Wi-Fi Modes Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Fetches available Wi-Fi modes from the API and initiates rendering of the mode selection buttons. Includes a basic error handling for the API call. ```javascript async function loadWifiModes() { try { const data = await apiCall("GET", "/api/modes"); modeList = data.modes; renderWifiModeButtons(); } catch(e) { /* retry later */ } } ``` -------------------------------- ### Fetch Device Ready Status Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Asynchronously fetches the current readiness status of the device, likely used to determine if the device is ready for mode switching or other operations. ```javascript async function fetchDeviceReady() { return apiCall("GET", "/api/device/ready"); } ``` -------------------------------- ### Load Wi-Fi and Device Info for Settings Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Loads the current Wi-Fi status and device name to pre-fill the settings form. It also sets up the password input and eye icon for toggling password visibility. ```javascript async function loadSettingsDeviceInfo() { try { const status = await apiCall("GET", "/api/wifi/status"); settingsOriginalSsid = status.ssid || ""; const ssidInput = document.getElementById("settings-ssid-input"); const pwdInput = document.getElementById("settings-pwd-input"); if (ssidInput) ssidInput.value = settingsOriginalSsid; if (pwdInput) { pwdInput.value = ""; pwdInput.type = "password"; pwdInput.placeholder = settingsOriginalSsid ? "(unchanged)" : "Wi-Fi password"; } const toggleBtn = document.getElementById("settings-toggle-pwd"); if (toggleBtn) toggleBtn.innerHTML = ICON_EYE_CLOSED; const dnInput = document.getElementById("settings-device-n ``` -------------------------------- ### Load Mode Configuration Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Asynchronously loads the mode configuration from the API. It merges fetched data with default settings and applies them. ```javascript async function loadModeConfig() { try { const data = await apiCall("GET", "/api/mode/mode_1/config"); modeConfig = { ...modeConfig, ...data, low_power_mode: !!data.low_power_mode }; applyModeConfig(); } catch(e) {} } ``` -------------------------------- ### Clone Submodules Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/README.md Initialize and update Git submodules recursively. This is a necessary step before building the project. ```bash git submodule update --init --recursive ``` -------------------------------- ### Load QR Code Generator Library Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Dynamically loads the QR code generator library from a CDN. Caches the promise to avoid multiple loads. ```javascript function loadEzdataQrLibrary() { if (window.qrcode) return Promise.resolve(window.qrcode); if (ezdataQrLibraryPromise) return ezdataQrLibraryPromise; ezdataQrLibraryPromise = new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = EZDATA_QR_LIBRARY_SRC; script.async = true; script.onload = () => resolve(window.qrcode); script.onerror = () => reject(new Error("Failed to load QR library")); document.head.appendChild(script); }).catch((error) => { ezdataQrLibraryPromise = null; throw error; }); return ezdataQrLibraryPromise; } ``` -------------------------------- ### Select Landing Page Mode Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Handles the selection of a mode from the landing page. It may involve fetching the list of available modes and initiating the process to enter the selected mode, such as 'Photo Slideshow'. ```javascript async function selectLandingMode(modeId) { if (modeList.length === 0) { try { const data = await apiCall("GET", "/api/modes"); modeList = data.modes; } catch (e) { showToast("Failed to load modes", "error"); return; } } if (modeId === "mode_1") { try { showToast("Entering...", ""); await apiCall("POST", "/api/mode/switch", { mode: "mode_1" }); await new Promise(resolve => setTimeout(resolve, 500)); const ready = await fetchDeviceReady(); await refreshStatusBar(); if (ready.ui && ready.ui.can_enter_mode && ready.mode && ready.mode.current) { enterReadyMode(ready.mode.current); return; } await startPolling({ targetMode: "mode_1", successMessage: "Ready! Redirecting...", loadingMessage: "Entering..." }); } catch (e) { showToast("Failed to enter Photo Slideshow", "error"); } } else if (modeId === "mode_2") { lockedWifiMode = "mode_2"; selectedWifiMod ``` -------------------------------- ### Upload Image Only Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Exports the current composition to a PNG blob and uploads it to the server. It checks for storage space, shows upload progress, and handles UI updates based on the upload response, including cache clearing for 'nearest' display mode. ```javascript async function uploadOnly() { const canvas = exportToCanvas(); if (!canvas) { showToast("Please select an image first", "error"); return; } try { const blob = await new Promise((resolve, reject) => { canvas.toBlob(b => { if (b) resolve(b); else reject(new Error("Export failed")); }, "image/png"); }); const storage = await loadStorage(); if (storage && storage.free < blob.size) { showToast("Storage full, please delete some photos", "error"); return; } const formData = new FormData(); formData.append("file", blob, "image_" + Date.now() + ".png"); formData.append("action", "upload_only"); formData.append("algorithm", displayMode); showToast("Uploading...", ""); const resp = await apiCall("POST", "/api/photos/upload", formData, true); showToast("Uploaded successfully", "success"); if (displayMode === "nearest" && resp && resp.name) { // Nearest photo inserts at front — all page boundaries shift, clear page cache photoPageCache = {}; const m = resp.name.match(/^imageN0*(\d+)/i); if (m) photoPage = Math.ceil(Number(m[1]) / getPhotoPerPage()); } else { photoPage = 9999; } await loadPhotoList(photoPage, /* forceRefresh */ true); loadStorage(); } catch(e) { showToast(e.message, "error"); } } ``` -------------------------------- ### Upload and Display Photo - JavaScript Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Handles uploading a photo blob to the server, updating the display, and managing storage. It includes error handling for storage full conditions and provides user feedback via toast messages. ```javascript async function uploadPhoto() { try { const blob = await takePhoto(); if (!blob) { showToast("Failed to take photo", "error"); return; } const resp = await apiCall("POST", "/api/photos/upload", blob, "image/png"); if (resp.error) { showToast(resp.error, "error"); return; } const storage = await loadStorage(); if (storage && storage.free < blob.size) { showToast("Storage full, please delete some photos", "error"); return; } const formData = new FormData(); formData.append("file", blob, "display_" + Date.now() + ".png"); formData.append("action", "upload_display"); formData.append("algorithm", displayMode); showToast("Uploading...", ""); const resp = await apiCall("POST", "/api/photos/upload", formData, true); showToast("Uploaded & display updated", "success"); // Compute which page the new photo landed on from its name if (displayMode === "nearest" && resp && resp.name) { // Nearest photo inserts at front — all pages shift, clear page cache photoPageCache = {}; const m = resp.name.match(/^imageN0*(\d+)/i); if (m) photoPage = Math.ceil(Number(m[1]) / getPhotoPerPage()); } else { photoPage = 9999; // Dither appends at end, only last page changes } await loadPhotoList(photoPage, /* forceRefresh */ true); loadStorage(); } catch(e) { showToast(e.message, "error"); } } ``` -------------------------------- ### Render QR Code as SVG Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Renders a QR code onto a host element using SVG. Includes background and foreground paths for modules. ```javascript function renderQrSvg(host, qr, size, quiet) { const modules = qr.getModuleCount(); const totalModules = modules + quiet * 2; const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("width", String(size)); svg.setAttribute("height", String(size)); svg.setAttribute("viewBox", `0 0 ${totalModules} ${totalModules}`); svg.setAttribute("role", "img"); svg.setAttribute("aria-label", "QR Code"); svg.style.display = "block"; const bg = document.createElementNS(svgNS, "path"); bg.setAttribute("fill", "#ffffff"); bg.setAttribute("d", `M0 0h${totalModules}v${totalModules}H0z`); bg.setAttribute("shape-rendering", "crispEdges"); const fg = document.createElementNS(svgNS, "path"); fg.setAttribute("fill", "#000000"); fg.setAttribute("d", buildQrSvgPath(qr)); fg.setAttribute("transform", `translate(${quiet} ${quiet})`); fg.setAttribute("shape-rendering", "crispEdges"); svg.appendChild(bg); svg.appendChild(fg); host.appendChild(svg); } ``` -------------------------------- ### Required CMakeLists.txt Boilerplate Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/CMakeLists.txt These lines must be included in your project's CMakeLists.txt in the specified order for the build system to function correctly. ```cmake cmake_minimum_required(VERSION 3.16) set(PROJECT_VER "1.0.1") include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(paper_color) ``` -------------------------------- ### Export Preview to EPD Palette Canvas Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Converts the exported canvas image to the EPD (Electronic Paper Display) palette using `quantizeToEpdPalette`. This is typically used for preparing images for display on E-Ink screens. Requires the `willReadFrequently` context option for performance. ```javascript function exportPreviewInkCanvas() { const source = exportToCanvas(); if (!source) return null; const [cw, ch] = getCanvasSize(); const canvas = document.createElement("canvas"); canvas.width = cw; canvas.height = ch; const ctx = canvas.getContext("2d", { willReadFrequently: true }); if (!ctx) return null; ctx.drawImage(source, 0, 0, cw, ch); const imageData = ctx.getImageData(0, 0, cw, ch); quantizeToEpdPalette(imageData); ctx.putImageData(imageData, 0, 0); return canvas; } ``` -------------------------------- ### Load EzData Mode 2 Configuration Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Fetches configuration for EzData Mode 2 from the API. Updates UI elements like connection status, token, and visit URL. ```javascript async function loadMode2Config() { try { const data = await apiCall("GET", "/api/mode/mode_2/config"); const dot = document.getElementById("ezdata-dot"); const text = document.getElementById("ezdata-status-text"); const token = document.getElementById("ezdata-token"); const qrCard = document.getElementById("ezdata-qrcode-card"); const activeToken = EZDATA_FORCE_TOKEN || data.device_token || ""; // 同步共享配置 modeConfig.orientation = data.orientation; modeConfig.auto_slideshow = data.auto_slideshow; modeConfig.low_power_mode = !!data.low_power_mode; modeConfig.interval_minutes = data.interval_minutes; if (token) token.value = activeToken; // Web visit URL const visitLink = document.getElementById("ezdata-visit-url"); if (visitLink && activeToken) { const webUrl = EZDATA_WEB_URL.replace("__TOKEN__", activeToken); visitLink.href = webUrl; visitLink.textContent = EZDATA_WEB_BASE; } if (data.connected) { dot.className = "ezdata-dot connected"; text.textContent = "Connected"; } else { dot.className = "ezdata-dot disconnected"; ``` -------------------------------- ### Build SVG Path for QR Code Modules Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Generates an SVG path string representing the dark modules of a QR code. ```javascript function buildQrSvgPath(qr) { const path = []; const modules = qr.getModuleCount(); for (let row = 0; row < modules; row++) { let start = null; for (let col = 0; col < modules; col++) { const dark = qr.isDark(row, col); if (dark && start === null) start = col; const atEnd = col === modules - 1; if ((!dark || atEnd) && start !== null) { const end = dark && atEnd ? col + 1 : col; path.push(`M${start} ${row}h${end - start}v1H${start}z`); start = null; } } } return path.join(""); } ``` -------------------------------- ### Select Display Mode Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Selects a new display mode ('dither' or 'nearest'), updates the UI label and selected option, hides the dropdown, and schedules a preview render. It also handles caching logic for 'nearest' mode. ```javascript function selectDisplayMode(val) { displayMode = val; document.getElementById("display-mode-label").textContent = val === "dither" ? "Dither" : "Nearest"; const opts = document.querySelectorAll("#display-mode-menu .display-mode-option"); opts.forEach(o => o.classList.toggle("selected", o.dataset.val === val)); document.getElementById("display-mode-menu").classList.add("hidden"); document.getElementById("display-mode-wrap").classList.remove("open"); schedulePreviewInkRender(true); } ``` -------------------------------- ### Image File Handling and Preview Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Handles file selection, validates image types, and displays a preview of the selected image. Uses FileReader API to read image data. ```javascript stener("drop", e => { e.preventDefault(); container.style.borderColor = ""; if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]); }); })(); function onFileSelected(e) { if (e.target.files.length > 0) { Array.from(e.target.files).forEach(handleFile); } e.target.value = ""; } function handleFile(file) { if (!file.type.startsWith("image/")) { showToast("Please select an image file", "error"); return; } const reader = new FileReader(); reader.onload = function(ev) { showPreview(ev.target.result); }; reader.readAsDataURL(file); } function showPreview(src) { const img = new Image(); img.onload = function() { createPreviewLayer(src, img.naturalWidth, img.naturalHeight); }; img.src = src; } ``` -------------------------------- ### Show Confirmation Dialog Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Displays a confirmation dialog with a message and a callback function to execute upon confirmation. Closes the dialog when the OK button is clicked. ```javascript function showConfirm(msg, cb) { document.getElementById("confirm-msg").textContent = msg; document.getElementById("confirm-dialog").classList.remove("hidden"); confirmCallback = cb; document.getElementById("confirm-ok-btn").onclick = function() { closeConfirm(); if (cb) cb(); }; } function closeConfirm() { document.getElementById("confirm-dialog").classList.add("hidden"); confirmCallback = null; } ``` -------------------------------- ### Quantize Image Data to EPD Palette with Dithering Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Iterates through image data, applying dithering and color quantization to map RGB values to the EPD palette. It uses a bias-based approach to distribute quantization errors. ```javascript function quantizeToEpdPalette(imageData) { const { data, width, height } = imageData; for (let y = 0; y < height; y++) { let biasBase = (y * EPD_Y_STEP) % EPD_STEP_VALUE; let r0 = 0; let g0 = 0; let b0 = 0; let prevIndex = -1; for (let x = 0; x <= width; x++) { let r = 128; let g = 128; let b = 128; let currentIndex = -1; if (x < width) { currentIndex = (y * width + x) * 4; r = data[currentIndex]; g = data[currentIndex + 1]; b = data[currentIndex + 2]; } biasBase -= EPD_X_STEP; if (biasBase < 0) biasBase += EPD_STEP_VALUE; let biasR = biasBase; let biasG = biasBase - EPD_STEP_DIFF; if (biasG < 0) biasG += EPD_STEP_VALUE; let biasB = biasG - EPD_STEP_DIFF; if (biasB < 0) biasB += EPD_STEP_VALUE; biasB = biasB * 2 - (EPD_STEP_VALUE - 1); biasG = biasG * 2 - (EPD_STEP_VALUE - 1); biasR = biasR * 2 - (EPD_STEP_VALUE - 1); let bias = biasR + biasG + biasB; bias = (bias * EPD_QUALITY_DITHER) >> 16; biasR = (biasR * EPD_QUALITY_DITHER) >> 16; biasG = (biasG * EPD_QUALITY_DITHER) >> 16; biasB = (biasB * EPD_QUALITY_DITHER) >> 16; ``` -------------------------------- ### Switch Application Mode Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Switches the application to a selected mode after verifying WiFi status. It updates the UI, status bar, and shows a toast notification. Handles cases where WiFi is not connected by prompting the user to select a mode. ```javascript async function switchMode(modeId) { const m = modeList.find(x => x.id === modeId); if (!m) return; document.getElementById("mode-modal").classList.add("hidden"); showToast("Switching mode...", ""); try { const status = await apiCall("GET", "/api/wifi/status"); if (status.connected) { try { await apiCall("POST", "/api/mode/switch", { mode: modeId }); } catch(e) {} currentMode = modeId; switchPage("mode-page"); showModeConfig(modeId); updateStatusBar({ connected: true, ip: status.ip, mode: modeId }); showToast("Switched to " + m.name, "success"); } else { lockedWifiMode = null; selectedWifiMode = modeId; resetWifiPageModeLock(); switchPage("wifi-page"); highlightModeButton(modeId); showToast("Select a mode", "success"); } } catch(e) { showToast("Cannot get connection status", "error"); } } ``` -------------------------------- ### Load Photo List - JavaScript Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Fetches a list of photos for a given page from the server. It utilizes caching to improve performance and handles pagination logic. If the current page becomes empty after deletion, it steps back to the previous page. ```javascript async function loadPhotoList(page, forceRefresh = false) { page = page || photoPage; const perPage = getPhotoPerPage(); // Serve from cache if this page was loaded before if (!forceRefresh && photoPageCache[page]) { photoPage = page; renderPhotoGridFromNames(photoPageCache[page]); renderPagination(); return; } photoPageLoading = true; try { const data = await apiCall("GET", `/api/photos/list?page=${page}&per_page=${perPage}`); if (data.page === page || page === photoPage) { // Store per-photo data globally const names = []; data.photos.forEach(p => { photoCache[p.name] = { name: p.name, url: p.url }; names.push(p.name); }); photoPageCache[data.page] = names; photoPage = data.page; photoTotalPages = data.total_pages; photoTotal = data.total; renderPhotoGridFromNames(names); renderPagination(); } } catch(e) { /* silent */ } photoPageLoading = false; } ``` -------------------------------- ### Window Resize Event Handler Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Applies mode configuration when the window is resized, specifically when the current mode is 'mode_1'. ```javascript window.addEventListener("resize", () => { if (currentMode === "mode_1") applyModeConfig(); }); ``` -------------------------------- ### Render EzData QR Code Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Generates and renders an EzData QR code for device binding. Loads the QR library if not already present. ```javascript async function renderEzdataQrCode(token) { const host = document.getElementById("ezdata-qrcode-canvas"); if (!host) return; host.innerHTML = ""; if (!token) return; const qrUrl = EZDATA_BIND_URL.replace("__TOKEN__", encodeURIComponent(token)); try { const qrcodeFactory = await loadEzdataQrLibrary(); const qr = qrcodeFactory(0, "M"); qr.addData(qrUrl, "Byte"); qr.make(); renderQrSvg(host, qr, 220, 4); } catch (error) { host.textContent = "QR unavailable"; } } ``` -------------------------------- ### Apply Mode Configuration to UI Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Applies the current mode configuration to the UI elements, adjusting container size, frame styles, and labels based on orientation and other settings. ```javascript function applyModeConfig() { const container = document.getElementById("preview-container"); const frame = document.getElementById("device-frame"); const label = document.getElementById("orientation-label"); const [w, h] = getCanvasSize(); const isPortrait = modeConfig.orientation === "portrait"; const shellWidth = isPortrait ? 430 : 760; const frameConfig = isPortrait ? { deviceWidth: 68.3, deviceHeight: 101.3, top: 5.95, right: 5.95, bottom: 10.75, left: 5.95, } : { deviceWidth: 101.3, deviceHeight: 68.3, top: 5.95, right: 5.95, bottom: 10.75, left: 10.75, }; frame.style.setProperty("--device-width", String(frameConfig.deviceWidth)); frame.style.setProperty("--device-height", String(frameConfig.deviceHeight)); frame.style.setProperty("--frame-top", String(frameConfig.top)); frame.style.setProperty("--frame-right", String(frameConfig.right)); frame.style.setProperty("--frame-bottom", String(frameConfig.bottom)); frame.style.setProperty("--frame-left", String(frameConfig.left)); frame.style.setProperty("--shell-display-width", `${shellWidth}px`); container.style.aspectRatio = w + "/" + h; container.style.width = ""; container.style.height = ""; label.textContent = w + "×" + h; requestAnimationFrame(() => { rerenderCompositionLayout(false); }); } ``` -------------------------------- ### Render Wi-Fi Mode Buttons Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Renders the buttons for selecting a Wi-Fi mode based on the fetched `modeList`. It updates the trigger button's text and populates the dropdown menu. ```javascript function renderWifiModeButtons() { const wrap = document.getElementById("wifi-mode-select-wrap"); const trigger = document.getElementById("wifi-mode-trigger"); const menu = document.getElementById("wifi-mode-menu"); if (!wrap || !trigger || !menu) return; if (!selectedWifiMode && modeList.length) { selectedWifiMode = modeList[0].id; } const selectedMode = modeList.find(m => m.id === selectedWifiMode); trigger.textContent = selectedMode ? selectedMode.name : "Select a mode"; trigger.classList.toggle("placeholder", !selectedMode); trigger.setAttribute("aria-expanded", wrap.classList.contains("open") ? "true" : "false"); menu.innerHTML = modeList.map(m => `` ).join(""); } ``` -------------------------------- ### Select Wi-Fi Mode Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Sets the selected Wi-Fi mode, re-renders the mode buttons to reflect the selection, and closes the dropdown. ```javascript function selectWifiMode(modeId) { selectedWifiMode = modeId; renderWifiModeButtons(); toggleWifiModeDropdown(false); } ``` -------------------------------- ### Render Photo Grid - JavaScript Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Renders the grid of photo thumbnails and their information. It handles the case where there are no photos to display and formats photo names and URLs safely for HTML. ```javascript function renderPhotoGridFromNames(names) { const grid = document.getElementById("photo-grid"); const count = document.getElementById("photo-count"); count.textContent = photoTotal > 0 ? `(${photoTotal})` : ""; if (names.length === 0) { grid.innerHTML = `
No photos
`; return; } grid.innerHTML = names.map(name => { const p = photoCache[name] || { name, url: "" }; return `
${escapeHtml(p.name)}
${escapeHtml(p.name)}
`; }).join(""); } ``` -------------------------------- ### Creating SPIFFS Image for Project Assets Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/CMakeLists.txt This command creates a SPIFFS image from specified project assets, useful for including files like images or configuration data within your project. ```cmake fatfs_create_spiflash_image(storage main/apps/local_photo_slideshow/images FLASH_IN_PROJECT) ``` -------------------------------- ### Format File Size Source: https://github.com/m5stack/m5papercolor-userdemo/blob/main/main/apps/app_server/index.html Converts a byte count into a human-readable string with appropriate units (B, KB, MB). ```javascript function formatSize(bytes) { if (bytes < 1024) return bytes + " B"; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB"; return (bytes / 1048576).toFixed(1) + " MB"; } ```