<!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
              >&nbsp;| Storage:&nbsp;<span id="fsUsed"></span>&nbsp;KB
              /&nbsp;<span id="fsTotal"></span>&nbsp;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>