<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GIF Player</title> <style> :root { --s-thumb: #0006; --s-background: #0003; --overlay: rgba(0, 0, 0, 0.5); --background: #111; --text: #ddd; --gray-dark: #222; --gray-medium: #333; --gray-light: #eeeeee; --blue-dark: #284352; --blue-medium: #3b708b; --blue-light: #48a; --success-dark: #03440c; --success-medium: #548c2f; --success-light: #8ac926; --error-dark: #a80a0a; --error-medium: #c9262b; --error-light: #ff595e; --warning-dark: #dc2f02; --warning-medium: #e85d04; --warning-light: #f48c06; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--s-thumb); opacity: 0.2; border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background: var(--s-background); } ::selection { background: var(--blue-light); } * { font-family: Helvetica, Verdana, sans-serif; box-sizing: border-box; margin: 0; padding: 0; } body { margin: 0; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; background: var(--background); overflow: scroll; } footer { display: flex; justify-content: center; margin-block: 12px; padding-inline: 8px; font-size: 0.75rem; color: var(--text); font-weight: 600; } a { text-decoration: none; color: var(--blue-light); } a:is(:hover, :focus, :active) { color: var(--blue-medium); } .container { width: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; } .content { width: min(730px, calc(100% - 40px)); margin-inline: 20px; } .row { display: flex; position: relative; flex-wrap: nowrap; justify-content: center; align-items: center; } .row-dialog-button { display: flex; justify-content: center; margin-block: 10px; } .row-gif-control-buttons { display: flex; justify-content: center; flex-direction: row; flex-wrap: nowrap; gap: 10px; } .col { flex-basis: calc(50% - 10px); position: relative; padding-inline: 5px; } .col-full { flex-basis: 100%; position: relative; padding-inline: 5px; } .col-small { flex-basis: 25%; position: relative; padding-inline: 5px; } .col-medium { flex-basis: 60%; position: relative; padding-inline: 5px; } .col-dialog-button { min-width: 180px; display: flex; flex-direction: column; } .header { position: relative; display: flex; flex-direction: row; padding-block: 20px; justify-content: space-between; align-items: center; } .header .title { font-size: 2.5rem; line-height: 2.5rem; font-weight: 800; color: var(--gray-light); text-align: center; text-wrap: wrap; margin-inline: 20px; } .header .title .title-wled { image-rendering: pixelated; image-rendering: crisp-edges; height: 1.8rem; margin-inline: 15px; } .header .rainbow { background: linear-gradient( to right, #ef5350, #f48fb1, #7e57c2, #2196f3, #26c6da, #43a047, #eeff41, #f9a825, #ff5722 ); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 200% 200%; animation: rainbow 5s linear infinite; transition: background-position 0.5s ease; } .btn-icon { width: 40px; height: 40px; filter: invert(100%); transition: transform 0.1s; cursor: pointer; } .btn-icon.stop { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' viewBox='0 -960 960 960' width='40'%3E%3Cpath d='M360-326.67h240q14.17 0 23.75-9.58t9.58-23.75v-240q0-14.17-9.58-23.75T600-633.33H360q-14.17 0-23.75 9.58T326.67-600v240q0 14.17 9.58 23.75t23.75 9.58ZM480.18-80q-82.83 0-155.67-31.5-72.84-31.5-127.18-85.83Q143-251.67 111.5-324.56T80-480.33q0-82.88 31.5-155.78Q143-709 197.33-763q54.34-54 127.23-85.5T480.33-880q82.88 0 155.78 31.5Q709-817 763-763t85.5 127Q880-563 880-480.18q0 82.83-31.5 155.67Q817-251.67 763-197.46q-54 54.21-127 85.84Q563-80 480.18-80Z'/%3E%3C/svg%3E"); } .btn-icon.play { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' viewBox='0 -960 960 960' width='40'%3E%3Cpath d='M420.67-331.33 620.33-459q12-7.67 12-21t-12-21L420.67-628.67q-12.34-8.66-25.5-1.5Q382-623 382-607.67v255.34q0 15.33 13.17 22.5 13.16 7.16 25.5-1.5ZM480-80q-82.33 0-155.33-31.5-73-31.5-127.34-85.83Q143-251.67 111.5-324.67T80-480q0-83 31.5-156t85.83-127q54.34-54 127.34-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82.33-31.5 155.33-31.5 73-85.5 127.34Q709-143 636-111.5T480-80Zm0-66.67q139.33 0 236.33-97.33t97-236q0-139.33-97-236.33t-236.33-97q-138.67 0-236 97-97.33 97-97.33 236.33 0 138.67 97.33 236 97.33 97.33 236 97.33ZM480-480Z'/%3E%3C/svg%3E"); } .btn-icon.pause { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' viewBox='0 -960 960 960' width='40'%3E%3Cpath d='M400.12-320q14.21 0 23.71-9.58 9.5-9.59 9.5-23.75v-253.34q0-14.16-9.61-23.75-9.62-9.58-23.84-9.58-14.21 0-23.71 9.58-9.5 9.59-9.5 23.75v253.34q0 14.16 9.61 23.75 9.62 9.58 23.84 9.58Zm160 0q14.21 0 23.71-9.58 9.5-9.59 9.5-23.75v-253.34q0-14.16-9.61-23.75-9.62-9.58-23.84-9.58-14.21 0-23.71 9.58-9.5 9.59-9.5 23.75v253.34q0 14.16 9.61 23.75 9.62 9.58 23.84 9.58ZM480.18-80q-82.83 0-155.67-31.5-72.84-31.5-127.18-85.83Q143-251.67 111.5-324.56T80-480.33q0-82.88 31.5-155.78Q143-709 197.33-763q54.34-54 127.23-85.5T480.33-880q82.88 0 155.78 31.5Q709-817 763-763t85.5 127Q880-563 880-480.18q0 82.83-31.5 155.67Q817-251.67 763-197.46q-54 54.21-127 85.84Q563-80 480.18-80Z'/%3E%3C/svg%3E"); } .btn-icon.settings { width: 30px; height: 30px; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' viewBox='0 -960 960 960' width='40'%3E%3Cpath d='M425-80q-18.33 0-32.17-12-13.83-12-16.5-30l-13-84.67q-17-6.33-34.83-16.66-17.83-10.34-32.17-21.67l-78 35.33Q200.67-202 183-208q-17.67-6-27.67-22.33L101-327q-10-16.33-5.67-34.33 4.34-18 19.34-29.67l71-52.67q-1.67-8.33-2-18.16-.34-9.84-.34-18.17 0-8.33.34-18.17.33-9.83 2-18.16l-71-52.67q-15-11.67-19.34-29.67Q91-616.67 101-633l54.33-96.67Q165.33-746 183-752t35.33 1.67l78 35.33q14.34-11.33 32.34-21.67 18-10.33 34.66-16l13-85.33q2.67-18 16.5-30 13.84-12 32.17-12h110q18.33 0 32.17 12 13.83 12 16.5 30l13 84.67q17 6.33 35.16 16.33 18.17 10 31.84 22l78-35.33q17.66-7.67 35-1.67Q794-746 804-729.67L859-633q10 16.33 5.67 34.67Q860.33-580 845.33-569l-71 51.33q1.67 9 2 18.84.34 9.83.34 18.83 0 9-.34 18.5Q776-452 774-443l71 52q15 11 19.33 29.33 4.34 18.34-5.66 34.67L804-230.33Q794-214 776.33-208q-17.66 6-35.33-1.67L663.67-245q-14.34 11.33-32 22-17.67 10.67-35 16.33l-13 84.67q-2.67 18-16.5 30Q553.33-80 535-80H425Zm55.67-266.67q55.33 0 94.33-39T614-480q0-55.33-39-94.33t-94.33-39q-55.67 0-94.5 39-38.84 39-38.84 94.33t38.84 94.33q38.83 39 94.5 39Z'/%3E%3C/svg%3E"); } .btn-icon.back { width: 35px; height: 35px; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' viewBox='0 -960 960 960' width='40'%3E%3Cpath d='M287-446.67 503.67-230q10 10 9.83 23.33-.17 13.34-10.17 23.34-10 9.66-23.33 9.83-13.33.17-23.33-9.83L183.33-456.67q-5.33-5.33-7.5-11-2.16-5.66-2.16-12.33t2.16-12.33q2.17-5.67 7.5-11l273.34-273.34q9.66-9.66 23.16-9.66t23.5 9.66q10 10 10 23.5t-10 23.5L287-513.33h479.67q14.33 0 23.83 9.5 9.5 9.5 9.5 23.83 0 14.33-9.5 23.83-9.5 9.5-23.83 9.5H287Z'/%3E%3C/svg%3E"); } .text-info { font-size: 0.8rem; color: var(--text); } .image-list-info { margin-top: 5px; text-align: center; } label { margin-bottom: 5px; font-weight: bold; color: var(--text); text-align: center; } .margin-y { margin-top: 20px; } input[type="text"], input[type="number"], select, textarea { width: 100%; padding: 10px; border-radius: 50px; background-color: var(--gray-medium); border: 1px solid var(--gray-medium); outline: none; color: var(--gray-light); font-size: 0.875rem; text-align: center; } #hostname.error { color: var(--error-light); } .dropzone { border: 1px dashed var(--gray-light); background-color: var(--gray-dark); color: var(--gray-light); text-align: center; padding: 40px 10px; border-radius: 8px; transition: all 0.5s ease-in-out; margin-inline: 5px; } .dropzone:hover { cursor: pointer; color: var(--gray-dark); background-color: var(--gray-light); border-color: var(--gray-dark); } .dropzone.dragover { background-color: var(--gray-medium); } #toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 9999; } .toast { display: flex; align-items: center; width: auto; padding: 6px 12px; margin-top: 10px; border-radius: 8px; transform: translateY(30px); opacity: 0; visibility: hidden; } .toast .toast-body { padding-block: 8px; font-weight: 600; color: var(--text); letter-spacing: 0.5px; } .toast.success { background-color: var(--success-medium); } .toast.error { background-color: var(--error-light); } .toast.warning { background-color: var(--warning-light); } .toast-progress { position: absolute; left: 4px; bottom: 4px; width: calc(100% - 8px); height: 3px; transform: scaleX(0); transform-origin: left; border-radius: 8px; } .toast.success .toast-progress { background: linear-gradient( to right, var(--success-light), var(--success-medium) ); } .toast.error .toast-progress { background: linear-gradient( to right, var(--error-light), var(--error-medium) ); } .toast.warning .toast-progress { background: linear-gradient( to right, var(--warning-light), var(--warning-medium) ); } .carousel { display: flex; height: 100%; width: 100%; cursor: pointer; } button { width: 100%; border: 0; padding: 10px 18px; border-radius: 50px; color: var(--text); cursor: pointer; margin-bottom: 10px; background: var(--gray-medium); border: 1px solid var(--gray-dark); transition: all 0.5s ease-in-out; font-size: 0.875rem; font-weight: 600; text-wrap: nowrap; } button:hover { background: var(--gray-dark); border: 1px solid var(--gray-medium); } button:last-child { margin-bottom: 0; } #loading { content: ""; display: block; position: absolute; left: 50px; top: calc(50% - 14px); width: 28px; height: 28px; border: 4px solid var(--gray-light); border-top-color: var(--gray-dark); border-radius: 50%; animation: fadeIn 1s ease both, spin 1s linear infinite; } .show-when-connected { animation: fadeIn 1s ease both; } .image-list { display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; } .image-holder { display: flex; position: relative; border: 1px solid var(--gray-light); text-align: center; min-height: 72px; max-height: 72px; min-width: 72px; max-width: 72px; align-items: center; justify-content: center; background-position: center; background-size: cover; background-color: var(--gray-dark); image-rendering: pixelated; cursor: pointer; transition: transform 0.1s; } .image-holder.loading::after { content: ""; display: flex; position: absolute; width: 20px; height: 20px; border: 3px solid var(--gray-light); border-top-color: var(--gray-dark); border-radius: 50%; animation: spin 1s linear infinite; } .selected { transform: scale(93%); } .image-dialog-img { min-height: 180px; max-height: 180px; min-width: 180px; image-rendering: pixelated; } dialog { padding: 0; background: rgb(33 33 33 / 70%); color: var(--text); max-width: 90vw; border-radius: 10px; box-shadow: 0 5px 30px 0 rgb(0 0 0 / 10%); animation: fadeIn 0.3s ease both; text-align: center; margin: auto; align-self: center; border: none; max-inline-size: min(90vw, var(--size-content-3)); max-block-size: min(80vh, 100%); max-block-size: min(80dvb, 100%); overflow: hidden; } dialog::backdrop { animation: fadeIn 0.3s ease both; background: rgba(0, 0, 0, 0.137); z-index: 2; backdrop-filter: blur(20px); } .dialog { padding: 10px 30px; } h3 { color: var(--text); font-weight: bold; margin-block: 15px; } @media (max-width: 767px) { .col-small { flex-basis: 50%; } .image-list { justify-content: center; } .header { padding-block: 10px; } #loading { left: 5px; bottom: -35px; top: unset; } } @keyframes spin { to { transform: rotate(360deg); } } @keyframes progress { to { transform: scaleX(1); } } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes fadeInToast { 5% { opacity: 1; visibility: visible; transform: translateY(0); } 95% { opacity: 1; transform: translateY(0); } } @keyframes rainbow { 0% { background-position: 0% 0; } 100% { background-position: 200% 0; } } </style> </head> <body> <div class="container"> <div class="content"> <div class="header"> <div id="btnBack"> <div class="btn-icon back" id="btnBack" onclick="window.location.href = WLED_URL;" ></div> </div> <div class="" id="loading"></div> <span class="title" ><img alt="" class="title-wled" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAFCAYAAAC5Fuf5AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABbSURBVChTlY9bDoAwDMNW7n9nwCipytQN4Z8tbrTHmDmF4oPzyldwRqp1SSdnV/NuZuzqerAByxXznBw3igkeFEfXyUuhK/yFM0CxJfyqXZEOc6/Sr9/bf7uIC5Nwd7orMvAPAAAAAElFTkSuQmCC" /><span class="rainbow">GIF</span ><span class="title-player">PLAYER</span></span > <div id="btnSettings"> <div class="btn-icon settings"></div> </div> </div> <div id="contentConnected" class="show-when-connected" style="display: none" > <div class="row"> <div class="col-small" validate style="position: relative"> <div class="row-gif-control-buttons"> <div class="btn-icon pause" id="btnPlayPause"></div> <div class="btn-icon stop" id="btnStop"></div> </div> </div> </div> <div class="image-list margin-y" id="image-list"> <!-- dynamically populated --> </div> <div class="image-list-info"> <span class="text-info" >Right click/long press image for more options</span > </div> <form id="formUpload"> <div id="dropzone" class="dropzone margin-y" validate> <p id="dropzoneLabel"> Drag and drop a file here or click to select a local file </p> <input type="file" name="source" id="source" accept="image/gif, image/bmp, image/png" style="display: none" /> </div> </form> </div> <footer> <a href="https://github.com/Manut38/WLED-GifPlayer-html" target="_blank" > GitHub Source </a> <div id="storage" style="display: none"> <span > | Storage: <span id="fsUsed"></span> KB / <span id="fsTotal"></span> KB</span > </div> </footer> </div> </div> <dialog id="image-dialog"> <div class="dialog"> <h3 id="image-dialog-name"></h3> <img class="image-dialog-img" id="image-dialog-img" src="" /> <div class="row-dialog-button"> <div class="col-dialog-button"> <button type="button" class="button" id="btnShowImg" style="background-color: var(--success-medium)" > Show </button> </div> </div> <div class="row-dialog-button"> <div class="col-dialog-button"> <button type="button" class="button button-dialog" id="btnSavePreset" style="background-color: var(--blue-light)" > Save As Preset </button> </div> </div> <div class="row-dialog-button"> <div class="col-dialog-button"> <button type="button" class="button button-dialog" id="btnDeleteImg" style="background-color: var(--error-dark)" > Delete </button> </div> </div> <div class="row-dialog-button"> <div class="col-dialog-button"> <button type="button" class="button button-dialog" id="btnCloseImgDialog" style="background-color: var(--blue-dark)" onclick="imgDialog.close()" > Close </button> </div> </div> </div> </dialog> <dialog id="settings-dialog"> <div class="dialog"> <h3 id="">Settings</h3> <form id="formHostname" novalidate> <div class="row-dialog-button"> <div class="col-dialog-button" validate style="position: relative"> <label for="hostname">Hostname</label> <input type="text" name="hostname" id="hostname" placeholder="Hostname" required /> </div> </div> <div class="row-dialog-button"> <div class="col-dialog-button"> <button type="button" class="button" id="btnReloadImages" style="background-color: var(--blue-medium)" > Force refresh </button> </div> </div> </form> <div class="row-dialog-button"> <div class="col-dialog-button"> <button type="button" class="button button-dialog" id="btnCloseSettingsDialog" style="background-color: var(--blue-dark)" onclick="settingsDialog.close()" > Close </button> </div> </div> </div> </dialog> <div id="toast-container"></div> </body> <script> // define constants const FX_MODE_IMAGE = 53; // set effect mode "Image" const GIF_SPEED = 128; // set effect speed to 128, original gif speed const ALLOWED_EXT = ["gif", "bmp", "png"]; const urlEdit = "/edit"; const urlJsonState = "/json/state"; const urlJsonInfo = "/json/info"; const urlPresets = "/presets.json"; // dynamic variables const d = document; const hostname = gId("hostname"); const imgDialog = gId("image-dialog"); const settingsDialog = gId("settings-dialog"); const contentDivConnected = gId("content-connected"); let selectedImage; let presetsJson = {}; // get host address and build WLED_URL const params = new URLSearchParams(window.location.search); const host = params.get("hostname") ? params.get("hostname") : window.location.host ? window.location.host : "0.0.0.0"; const protocol = window.location.protocol === "file:" ? "http:" : window.location.protocol; let WLED_URL = `${protocol}//${host}`; // pre-fill hostname input hostname.value = host; checkBackBtn(); // load all info on page load loadInfo(); async function loadImages(images, forceReload = false) { const maxConcurrentRequests = 2; let currentIndex = 0; let numRequests = 0; let allImagesLoaded = false; async function loadImage(imageHolder, forceReload = false) { try { const options = forceReload ? { cache: "reload" } : { cache: "force-cache" }; const response = await fetch(imageHolder.dataset.src, options); imageHolder.style["background-image"] = `url(${URL.createObjectURL( await response.blob() )})`; imageHolder.classList.remove("loading"); } catch (error) { console.error(error); } finally { numRequests--; } } // continue with requests until all img are loaded while (!allImagesLoaded) { // watch the concurrent request limit while ( numRequests < maxConcurrentRequests && currentIndex < images.length ) { loadImage(images[currentIndex], forceReload); currentIndex++; numRequests++; } if (currentIndex >= images.length && numRequests === 0) { allImagesLoaded = true; } await new Promise((resolve) => setTimeout(resolve, 10)); // Add a small delay before checking conditions again } } async function images(forceReload = false) { const url = `${WLED_URL}${urlEdit}?list=/`; // clear image list intially gId("image-list").innerHTML = ""; // reset hostname input color gId("hostname").classList.remove("error"); showLoading(); hidePageContent(); try { const response = await fetch(url, { signal: AbortSignal.timeout(8000), }); const data = await response.json(); const images = data.filter((file) => { const { name } = file; file.name = name.replace("/", ""); const fileExt = name.split(".").pop().toLowerCase(); return ALLOWED_EXT.includes(fileExt); }); let imageList = gId("image-list"); let imageHolders = []; images.forEach((image) => { const imageHolder = d.createElement("div"); imageHolder.classList.add("image-holder"); imageHolder.classList.add("loading"); imageHolder.dataset.src = `${WLED_URL}/${image.name}`; imageHolder.dataset.name = image.name; imageHolder.dataset.path = `/${image.name}`; imageHolder.addEventListener( "contextmenu", (event) => { event.preventDefault(); selectedImage = imageHolder.dataset; gId("image-dialog-name").innerText = imageHolder.dataset.name; gId("image-dialog-img").src = ""; gId("image-dialog-img").src = imageHolder.dataset.src; imgDialog.showModal(); }, false ); ["mousedown", "touchstart"].forEach((evt) => imageHolder.addEventListener( evt, () => { imageHolder.classList.add("selected"); }, false ) ); ["mouseup", "touchend", "mouseleave"].forEach((evt) => imageHolder.addEventListener( evt, () => { imageHolder.classList.remove("selected"); }, false ) ); imageHolder.addEventListener( "click", (event) => { selectedImage = imageHolder.dataset; showImg(); }, false ); imageList.appendChild(imageHolder); imageHolders.push(imageHolder); }); // Load images asynchronously loadImages(imageHolders, forceReload); showPageContent(); return Promise.resolve("Image list received"); } catch (error) { console.error(error); toast("Failed to get image list, check hostname!", "error", 10000); gId("hostname").classList.add("error"); return Promise.reject("Failed to get image list"); } finally { hideLoading(); } } async function presets() { const url = `${WLED_URL}${urlPresets}`; try { const response = await fetch(url); presetsJson = await response.json(); } catch (error) { console.error(error); toast("Error loading presets. Malformed JSON? ", "error"); } } async function storage() { const url = `${WLED_URL}${urlJsonInfo}`; try { const response = await fetch(url, { signal: AbortSignal.timeout(10000), }); const info = await response.json(); gId("fsUsed").innerHTML = `${info.fs.u}`; gId("fsTotal").innerHTML = `${info.fs.t}`; gId("storage").style.display = "flex"; } catch (error) { console.error(error); } } async function deleteImage() { if (!confirm("Are you sure you want to delete this image?")) return; formData = new FormData(); formData.append("path", selectedImage.path); const url = `${WLED_URL}/edit`; const options = { method: "DELETE", body: formData, }; imgDialog.close(); try { const response = await fetch(url, options); if (!response.ok) { throw new Error(response.status); } toast("Image deleted (if not currently in use)"); loadInfo(); } catch (error) { const msg = `Error deleting image: ${error}`; console.error(msg); toast(msg, "error"); } } async function showImg() { let state = {}; let existingPreset = findPresetID(selectedImage.name); if (existingPreset) { Object.assign(state, { on: true, ps: existingPreset, }); } else { Object.assign(state, { on: true, seg: { id: 0, fx: FX_MODE_IMAGE, frz: false, sx: GIF_SPEED, n: selectedImage.name, }, }); } const url = `${WLED_URL}${urlJsonState}`; const options = { method: "POST", body: JSON.stringify(state), }; imgDialog.close(); try { const response = await fetch(url, options); const { success } = await response.json(); if (success) { if (existingPreset) { toast( `Preset '${presetsJson[existingPreset].n}' loaded`, "success", 1000 ); } else { toast("Image changed", "success", 1000); } } } catch (error) { console.error(error); toast(error, "error"); } } // save preset at new ID (highest preset ID+1), or overwrite existing preset async function savePreset() { await showImg(); imgDialog.close(); showLoading(); await new Promise((resolve) => setTimeout(resolve, 500)); await presets(); // find next lowest ID / ID of existing preset with segment name == image name let existingPreset = false; existingPreset = findPresetID(selectedImage.name); items = Object.keys(presetsJson); const id = existingPreset || parseInt(items.pop()) + 1; const pName = existingPreset ? presetsJson[id].n : selectedImage.name.substring(0, selectedImage.name.lastIndexOf(".")); if (existingPreset) { toast(`Preset '${pName}' already saved with ID ${id}`, "success"); hideLoading(); return; } let body = {}; Object.assign(body, { psave: parseInt(id), n: pName, }); try { const url = `${WLED_URL}/json`; const options = { method: "POST", body: JSON.stringify(body), }; const response = await fetch(url, options); const { success } = await response.json(); if (success) { toast( `${ existingPreset ? "Modified" : "Saved as" } preset '${pName}' with ID ${id}`, "success", 8000 ); window.parent.postMessage("loadPresets", WLED_URL); } } catch (error) { console.error(error); toast("Error saving preset", "error"); } finally { await new Promise((resolve) => setTimeout(resolve, 2000)); // Add a delay before refreshing presets await presets(); hideLoading(); } } async function upload() { const file = gId("source").files[0]; formData = new FormData(); formData.append("data", file, `/${file.name}`); const url = `${WLED_URL}${urlEdit}`; const options = { method: "POST", body: formData, }; showLoading(); try { const response = await fetch(url, options); if (!response.ok) { throw new Error(response.status); } toast(`${file.name} successfully uploaded`, "success"); } catch (error) { const msg = `Error uploading image: ${error}`; console.error(msg); toast(msg, "error"); } finally { await loadInfo(); hideLoading(); } } async function powerOff() { const url = `${WLED_URL}${urlJsonState}`; try { const options = { method: "POST", body: JSON.stringify({ on: false }), }; const response = await fetch(url, options); const { success } = await response.json(); if (success) { toast("Playback stopped", "success", 1000); } } catch (error) { console.error(error); } } async function playPause() { const url = `${WLED_URL}${urlJsonState}`; try { const options = { method: "POST", body: JSON.stringify({ seg: { frz: "t" } }), }; const response = await fetch(url, options); const { success } = await response.json(); if (success) { toast("Playback paused/resumed", "success", 1000); } } catch (error) { console.error(error); } } async function loadInfo() { // await loading of image list, actual images will load asynchronously if (await images()); { // await loading of presets.json await presets(); // get storage used/total count storage(); } } function findPresetID(segmentName) { items = Object.keys(presetsJson); return items.find( (key) => typeof presetsJson[key] === "object" && !presetsJson[key].playlist && presetsJson[key].seg && presetsJson[key].seg[0].fx === FX_MODE_IMAGE && presetsJson[key].seg[0].n === segmentName ); } // reload all info on change of hostname input hostname.addEventListener("blur", async () => { hostname.value = hostname.value.trim(); newWledUrl = `${protocol}//${hostname.value}`; if (WLED_URL == newWledUrl) { return; } WLED_URL = newWledUrl; // replace url in browser var url = window.location.protocol + "//" + window.location.host + window.location.pathname + `?hostname=${hostname.value}`; window.history.pushState({ path: url }, "", url); loadInfo(); }); gId("btnShowImg").addEventListener("click", showImg, false); gId("btnSavePreset").addEventListener("click", savePreset, false); gId("btnDeleteImg").addEventListener("click", deleteImage, false); gId("btnReloadImages").addEventListener( "click", async () => { settingsDialog.close(); await images(true); await presets(); storage(); }, false ); gId("dropzone").addEventListener("dragover", (e) => { e.preventDefault(); }); gId("dropzone").addEventListener("drop", (e) => { e.preventDefault(); if (!validateFile(e.dataTransfer.files[0])) { return; } source.files = e.dataTransfer.files; const { name } = source.files[0]; upload(); }); gId("dropzone").addEventListener("click", () => { source.click(); }); gId("source").addEventListener("change", (e) => { const dropzoneLabel = gId("dropzoneLabel"); const { value } = e.target; if (value) { if (!validateFile(e.target.files[0])) { return; } const { name } = e.target.files[0]; upload(); } }); // dismiss dialog when clicking somewhere other than its elements d.querySelectorAll("dialog").forEach((dialog) => dialog.addEventListener( "click", ({ target: dialog }) => { if (dialog.nodeName === "DIALOG") dialog.close(); }, false ) ); ["mousedown", "touchstart"].forEach((evt) => ["btnStop", "btnPlayPause", "btnBack", "btnSettings"].forEach((btn) => gId(btn).addEventListener( evt, () => { gId(btn).classList.add("selected"); }, false ) ) ); ["mouseup", "touchend", "mouseleave"].forEach((evt) => ["btnStop", "btnPlayPause", "btnBack", "btnSettings"].forEach((btn) => gId(btn).addEventListener( evt, () => { gId(btn).classList.remove("selected"); }, false ) ) ); gId("btnStop").addEventListener("click", powerOff, false); gId("btnPlayPause").addEventListener("click", playPause, false); gId("btnSettings").addEventListener( "click", () => settingsDialog.showModal(), false ); function toast(message, type = "success", duration = 5000) { const toast = d.createElement("div"); const wait = 100; toast.style.animation = "fadeInToast"; toast.style.animationDuration = `${duration + wait}ms`; toast.style.animationTimingFunction = "linear"; toast.classList.add("toast", type); const body = d.createElement("span"); body.classList.add("toast-body"); body.textContent = message; toast.appendChild(body); const progress = d.createElement("div"); progress.classList.add("toast-progress"); progress.style.animation = "progress"; progress.style.animationDuration = `${duration + wait}ms`; progress.style.animationDelay = "0s"; toast.appendChild(progress); gId("toast-container").appendChild(toast); setTimeout(() => { toast.style.opacity = 0; setTimeout(() => { toast.remove(); }, wait); }, duration); } function gId(id) { return d.getElementById(id); } function showLoading() { const loading = gId("loading"); loading.style.display = "block"; } function hideLoading() { const loading = gId("loading"); loading.style.display = "none"; } function showPageContent() { gId("contentConnected").style.display = "block"; checkBackBtn(); } function hidePageContent() { gId("contentConnected").style.display = "none"; checkBackBtn(); } // "Back" button will only be shown if served from webserver function checkBackBtn() { gId("btnBack").style.visibility = window.location.protocol === "http:" ? "visible" : "hidden"; } function validateFile(file) { var fileExt = file.name.split(".").pop().toLowerCase(); if (ALLOWED_EXT.includes(fileExt)) { return true; } else { toast("This file does not have a compatible image format", "error"); return false; } } </script> </html>