damongolding / immich-kiosk

Immich Kiosk is a lightweight slideshow for running on kiosk devices and browsers that uses Immich as a data source.
GNU Affero General Public License v3.0
532 stars 19 forks source link

Remove the use of chaining in the kiosk.js file to support older browsers #70

Closed natloh closed 2 months ago

natloh commented 2 months ago

Describe the bug using the (?.) chaining syntax in the kiosk.js file prevents a few older browsers, notably the ipad air 1 running iOS 12.5.7, from working.

To Reproduce Steps to reproduce the behavior:

  1. Open kiosk page on an ipad running iOS 12.5.7
  2. Notice images do not rotate

Expected behavior Images should rotate

Your Kiosk version 0.8.0

Your Kiosk installation

The URL used to access Kiosk

Desktop:

Smartphone:

Additional context I made the edits to the JS that can be seen below and have redeployed the docker image under my own fork. Looks like it works as expected. Here is the updated kiosk.js code


"use strict";

/**
 * Immediately Invoked Function Expression (IIFE) to encapsulate the kiosk functionality
 * and avoid polluting the global scope.
 */
(() => {
  // Parse kiosk data from the HTML element
  const kioskData = JSON.parse(
    document.getElementById("kiosk-data").textContent,
  );
  // Set polling interval based on the refresh rate in kiosk data
  const pollInterval = htmx.parseInterval(`${kioskData.refresh}s`);
  let pollingInterval;

  let lastUpdateTime = 0;
  let animationFrameId;
  let progressBarElement;

  let isPaused = false;
  let isFullscreen = false;
  let triggerSent = false;

  // Cache DOM elements for better performance
  const documentBody = document.body;
  const progressBar = htmx.find(".progress--bar");
  const fullscreenButton = htmx.find(".navigation--fullscreen");
  const kiosk = htmx.find("#kiosk");
  const menu = htmx.find(".navigation");
  const menuPausePlayButton = htmx.find(".navigation--control");

  // Get the appropriate fullscreen API for the current browser
  const fullscreenAPI = getFullscreenAPI();

  /**
   * Initialize Kiosk functionality
   */
  function init() {
    if (!fullscreenAPI.requestFullscreen) {
      htmx.remove(fullscreenButton);
    }

    if (!isPaused) startPolling();

    addEventListeners();
  }

  /**
   * Updates the kiosk display and progress bar
   * @param {number} timestamp - The current timestamp from requestAnimationFrame
   */
  function updateKiosk(timestamp) {
    // Initialize lastUpdateTime if it's the first update
    if (!lastUpdateTime) lastUpdateTime = timestamp;

    // Calculate elapsed time and progress
    const elapsed = timestamp - lastUpdateTime;
    const triggerOffset = 500; // 0.5 second offset
    const progress = Math.min(elapsed / pollInterval, 1);

    // Update progress bar width
    if (progressBarElement) {
      progressBarElement.style.width = `${progress * 100}%`;
    }

    // Trigger new image 1 second before the interval has passed
    if (elapsed >= pollInterval - triggerOffset && !triggerSent) {
      console.log("Trigger new image");
      htmx.trigger(kiosk, "kiosk-new-image");
      triggerSent = true;
    }

    // Reset progress bar and lastUpdateTime when the full interval has passed
    if (elapsed >= pollInterval) {
      if (progressBarElement) {
        progressBarElement.style.width = "0%";
      }
      lastUpdateTime = timestamp;
      triggerSent = false;
    }

    // Schedule the next update
    animationFrameId = requestAnimationFrame(updateKiosk);
  }

  /**
   * Determine the correct fullscreen API methods for the current browser
   * @returns {Object} An object containing the appropriate fullscreen methods
   */
  function getFullscreenAPI() {
    const apis = [
      [
        "requestFullscreen",
        "exitFullscreen",
        "fullscreenElement",
        "fullscreenEnabled",
      ],
      [
        "mozRequestFullScreen",
        "mozCancelFullScreen",
        "mozFullScreenElement",
        "mozFullScreenEnabled",
      ],
      [
        "webkitRequestFullscreen",
        "webkitExitFullscreen",
        "webkitFullscreenElement",
        "webkitFullscreenEnabled",
      ],
      [
        "msRequestFullscreen",
        "msExitFullscreen",
        "msFullscreenElement",
        "msFullscreenEnabled",
      ],
    ];

    for (const [request, exit, element, enabled] of apis) {
      if (request in document.documentElement) {
        return {
          requestFullscreen: request,
          exitFullscreen: exit,
          fullscreenElement: element,
          fullscreenEnabled: enabled,
        };
      }
    }

    return {
      requestFullscreen: null,
      exitFullscreen: null,
      fullscreenElement: null,
      fullscreenEnabled: null,
    };
  }

  /**
   * Toggle fullscreen mode
   */
  function toggleFullscreen() {
    if (isFullscreen) {
      document[fullscreenAPI.exitFullscreen]();
    } else {
      documentBody[fullscreenAPI.requestFullscreen]();
    }

    isFullscreen = !isFullscreen;

    if (fullscreenButton) {
      fullscreenButton.classList.toggle("navigation--fullscreen-enabled");
    }
  }

  /**
   * Start the polling process to fetch new images
   */
  function startPolling() {
    progressBarElement = htmx.find(".progress--bar");
    if (progressBarElement) {
      progressBarElement.classList.remove("progress--bar-paused");
    }

    if (menuPausePlayButton) {
      menuPausePlayButton.classList.remove("navigation--control--paused");
    }

    lastUpdateTime = 0;
    animationFrameId = requestAnimationFrame(updateKiosk);
  }

  /**
   * Stop the polling process
   */
  function stopPolling() {
    cancelAnimationFrame(animationFrameId);

    if (progressBarElement) {
      progressBarElement.classList.add("progress--bar-paused");
    }

    if (menuPausePlayButton) {
      menuPausePlayButton.classList.add("navigation--control--paused");
    }
  }

  /**
   * Toggle the polling state (pause/restart)
   */
  function togglePolling() {
    isPaused ? startPolling() : stopPolling();

    if (menu) {
      menu.classList.toggle("navigation-hidden");
    }

    isPaused = !isPaused;
  }

  /**
   * Add event listeners to Kiosk elements
   */
  function addEventListeners() {
    // Pause and show menu
    if (kiosk) {
      kiosk.addEventListener("click", togglePolling);
    }

    if (menuPausePlayButton) {
      menuPausePlayButton.addEventListener("click", togglePolling);
    }

    if (fullscreenButton) {
      fullscreenButton.addEventListener("click", toggleFullscreen);
    }

    document.addEventListener("fullscreenchange", () => {
      isFullscreen = !!document[fullscreenAPI.fullscreenElement];

      if (fullscreenButton) {
        fullscreenButton.classList.toggle(
          "navigation--fullscreen-enabled",
          isFullscreen,
        );
      }
    });
  }

  // Initialize Kiosk when the DOM is fully loaded
  document.addEventListener("DOMContentLoaded", init);
})();
damongolding commented 2 months ago

I've gotten too used to writing modern JS. I was trying to avoid going down the Typescript route but I think it might be worth adding.

damongolding commented 2 months ago

@natloh Would you be open to testing out a fix for this using the development docker image?

If so, you can use damongolding/immich-kiosk-development:0.8.1 as your image in your compose file.

If you are able could you report any issues or fixes to this issue please šŸ™

natloh commented 2 months ago

Hey @damongolding

The small projects always get big so quickly haha! This is a great project by the way, solved a need in a much better way than I could do and one I was putting off for a while. Thank you for the work!

Just tested out the dev branch you referenced and it looks to be running smoothly for me on the iPad Air Gen 1.

For posterity, this is the compose file I am using, pause and full screen also look good!


services:
  immich_kiosk:
    image: damongolding/immich-kiosk-development:0.8.1
    container_name: immich_kiosk
    environment:
      TZ: "America/Los_Angeles"
      # Required settings
      KIOSK_IMMICH_API_KEY: "*************"
      KIOSK_IMMICH_URL: "***************"
      # Clock
      KIOSK_SHOW_TIME: FALSE
      KIOSK_TIME_FORMAT: 12
      KIOSK_SHOW_DATE: FALSE
      KIOSK_DATE_FORMAT: MMM D
      # Kiosk behaviour
      KIOSK_REFRESH: 7200
      KIOSK_DISABLE_SCREENSAVER: TRUE
      # Asset sources
      KIOSK_ALBUM: "*************"
      #KIOSK_PERSON: "PERSON_ID,PERSON_ID,PERSON_ID"
      # UI
      KIOSK_DISABLE_UI: FALSE
      KIOSK_HIDE_CURSOR: FALSE
      KIOSK_BACKGROUND_BLUR: FALSE
      KIOSK_TRANSITION: "cross-fade"
      # Image display settings
      KIOSK_SHOW_PROGRESS: FALSE
      KIOSK_IMAGE_FIT: COVER
      # Image metadata
      KIOSK_SHOW_IMAGE_TIME: FALSE
      KIOSK_IMAGE_TIME_FORMAT: 12
      KIOSK_SHOW_IMAGE_DATE: FALSE
      KIOSK_IMAGE_DATE_FORMAT: MM-DD-YYY
      KIOSK_SHOW_IMAGE_EXIF: FALSE
      KIOSK_SHOW_IMAGE_LOCATION: FALSE
      # Kiosk settings
      KIOSK_CACHE: TRUE
    ports:
      - 3000:3000
    restart: on-failure
damongolding commented 2 months ago

The small projects always get big so quickly haha!

Don't they just! I now have a typescript and bundler build step šŸ˜…

I'm happy you're getting some use out of Kiosk :)