/*
Photo.js
Author: tobychui
This is a complete rewrite of the legacy Photo module for ArozOS
*/
// Number of photos to load per page (infinite scroll batch size)
const PAGE_SIZE = 40; //large enough to fill the whole page on load, but small enough to keep initial load fast and responsive
let photoList = [];
let prePhoto = "";
let nextPhoto = "";
let currentModel = "";
let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Check if image should use compression (only JPG/PNG)
function shouldUseCompression(filepath, filesize) {
const ext = filepath.split('.').pop().toLowerCase();
const isJpgOrPng = (ext === 'jpg' || ext === 'jpeg' || ext === 'png');
const COMPRESSION_THRESHOLD = 5 * 1024 * 1024; // 5MB
return isJpgOrPng && filesize && filesize > COMPRESSION_THRESHOLD;
}
// Get viewable image URL (handles RAW files)
function getViewableImageUrl(filepath, callback) {
// Both RAW and regular images now use backend rendering
const imageUrl = "../media?file=" + encodeURIComponent(filepath);
callback(imageUrl, true, false, isRawImage(filepath) ? 'backend_raw' : 'direct');
}
function getImageWidth(){
// Use the actual viewbox container width so the sidebar and scrollbar are
// already subtracted — this prevents gaps when the window is resized.
const container = document.getElementById('viewboxContainer');
const containerWidth = container ? container.clientWidth : (window.innerWidth - 210);
let boxCount;
if (containerWidth < 400) {
boxCount = 2;
} else if (containerWidth < 600) {
boxCount = 3;
} else if (containerWidth < 900) {
boxCount = 4;
} else if (containerWidth < 1100) {
boxCount = 5;
} else if (containerWidth < 1400) {
boxCount = 6;
} else {
boxCount = 8;
}
return Math.floor(containerWidth / boxCount);
}
function updateImageSizes(){
let newImageWidth = getImageWidth();
console.log(newImageWidth, $("#viewbox").width());
//Updates all the size of the images
$(".imagecard").css({
width: newImageWidth,
height: newImageWidth
});
}
function extractFolderName(folderpath){
return folderpath.split("/").pop();
}
function parseExifValue(value) {
if (typeof value === 'string' && value.includes('/')) {
let parts = value.split('/');
if (parts.length === 2) {
let num = parseFloat(parts[0]);
let den = parseFloat(parts[1]);
if (den !== 0) {
return num / den;
}
}
}
return parseFloat(value) || value;
}
function formatShutterSpeed(value) {
let num = parseExifValue(value);
if (num < 1) {
return "1/" + Math.round(1 / num);
} else {
return num ;
}
}
function photoListObject() {
return {
// data
pathWildcard: "user:/Photo/*",
currentPath: "user:/Photo",
renderSize: 200,
vroots: [],
allImages: [], // full list from server
images: [], // currently displayed slice
folders: [],
sortOrder: 'smart',
restored: false,
hasMoreImages: false,
isLoadingMore: false, // guard: blocks new batch until DOM has updated
sidebarOpen: !isMobile, // start hidden on mobile, visible on desktop
// init
init() {
this.getFolderInfo();
this.getRootInfo();
this.renderSize = getImageWidth();
updateImageSizes();
this.restored = false;
this.$nextTick(() => { this.setupInfiniteScroll(); });
const MOBILE_BP = 768;
let _prevMobile = window.innerWidth <= MOBILE_BP;
let _resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(_resizeTimer);
_resizeTimer = setTimeout(() => {
// Recalculate tile sizes
this.renderSize = getImageWidth();
updateImageSizes();
// Auto-manage sidebar visibility on breakpoint crossing
const nowMobile = window.innerWidth <= MOBILE_BP;
if (nowMobile && !_prevMobile) {
// Desktop → mobile: hide sidebar so it doesn't overlay content
this.sidebarOpen = false;
} else if (!nowMobile && _prevMobile) {
// Mobile → desktop: sidebar is back in normal flow, keep state clean
this.sidebarOpen = false;
}
_prevMobile = nowMobile;
}, 80);
});
},
updateRenderingPath(newPath, callback = null){
this.currentPath = JSON.parse(JSON.stringify(newPath));
this.pathWildcard = newPath + '/*';
this.restored = false;
if (isMobile) this.sidebarOpen = false;
this.getFolderInfo(callback);
},
// Returns path segments for the sidebar breadcrumb tree
getPathSegments() {
const parts = this.currentPath.split('/');
let segments = [];
let accumulated = '';
for (let i = 0; i < parts.length; i++) {
accumulated = i === 0 ? parts[0] : accumulated + '/' + parts[i];
segments.push({ name: parts[i], path: accumulated, depth: i, isDiskRoot: i === 0 });
}
return segments;
},
getFolderInfo(callback = null) {
fetch(ao_root + "system/ajgi/interface?script=Photo/backend/listFolder.js", {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"folder": this.pathWildcard,
"sort": this.sortOrder
})
}).then(resp => {
resp.json().then(data => {
console.log(data);
this.folders = data[0];
this.allImages = data[1];
this.images = this.allImages.slice(0, PAGE_SIZE);
this.hasMoreImages = this.allImages.length > PAGE_SIZE;
this.isLoadingMore = false;
if (this.allImages.length == 0){
$("#noimg").show();
}else{
$("#noimg").hide();
}
console.log(this.folders);
if (!this.restored) { restoreFromHash(); this.restored = true; }
if (callback) callback();
});
});
},
getRootInfo() {
fetch(ao_root + "system/ajgi/interface?script=Photo/backend/listRoots.js", {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
}).then(resp => {
resp.json().then(data => {
this.vroots = data;
this.$nextTick(() => {
$('.ui.dropdown').dropdown();
});
});
})
},
changeSort(newSort) {
this.sortOrder = newSort;
this.getFolderInfo();
},
// Load the next PAGE_SIZE images into the displayed list
loadMoreImages() {
if (this.isLoadingMore) return;
const current = this.images.length;
if (current >= this.allImages.length) return;
this.isLoadingMore = true;
const next = this.allImages.slice(current, current + PAGE_SIZE);
this.images = this.images.concat(next);
this.hasMoreImages = this.images.length < this.allImages.length;
// Release the guard only after Alpine has re-rendered and scrollHeight has grown
this.$nextTick(() => { this.isLoadingMore = false; });
},
// Attach a scroll listener to the viewbox container for infinite scroll
setupInfiniteScroll() {
const container = document.getElementById('viewboxContainer');
if (!container) return;
container.addEventListener('scroll', () => {
const { scrollTop, scrollHeight, clientHeight } = container;
// Trigger when within 300px of the bottom
if (scrollTop + clientHeight >= scrollHeight - 300) {
this.loadMoreImages();
}
});
}
}
}
function renderImageList(object){
var fd = $(object).attr("filedata");
fd = JSON.parse(decodeURIComponent(fd));
console.log(fd);
}
function ShowModal(){
$('#photo-viewer').show();
}
function closeViewer(){
$('#photo-viewer').hide();
window.location.hash = '';
ao_module_setWindowTitle("Photo");
setTimeout(function(){
$("#fullImage").attr("src","img/loading.png");
$("#compressedImage").attr("src","").hide().removeClass('hidden');
$("#bg-image").attr("src","");
$("#info-filename").text("");
$("#info-filepath").text("");
$("#info-dimensions").text("Loading...");
// Reset EXIF data display
$('#basic-info-section').hide();
$('#shooting-params-section').hide();
$('#tone-analysis-section').hide();
$('#device-info-section').hide();
$('#shooting-mode-section').hide();
$('#technical-params-section').hide();
$('#no-exif-message').hide();
$('.ui.divider').hide();
// Clear histogram canvas
const canvas = document.getElementById('histogram-canvas');
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}, 300);
}
let compressedImageLoaded = false;
let fullsizeImageLoaded = false;
function showImage(object){
// Reset zoom level when switching photos
if (typeof resetZoom === 'function') {
resetZoom();
}
if (!$(object).hasClass("imagecard")){
// Not an image card, do nothing
return;
}
// Reset loading flags
compressedImageLoaded = false;
fullsizeImageLoaded = false;
var fd = JSON.parse(decodeURIComponent($(object).attr("filedata")));
$("#info-dimensions").text("Calculating...");
// Check if we should use compression (only for JPG/PNG > 5MB)
const useCompression = shouldUseCompression(fd.filepath, fd.filesize);
// Set thumbnail as placeholder for full image
const thumbnailUrl = $(object).find('img').attr('src');
$("#fullImage").attr("src", thumbnailUrl);
$("#fullImage").hide();
$("#compressedImage").show();
$("#compressedImage").attr("src", thumbnailUrl);
$("#bg-image").attr("src", thumbnailUrl);
// Get image URL (backend handles RAW files automatically)
getViewableImageUrl(fd.filepath, (imageUrl, isSupported, isBlob, method) => {
$("#loading-progress").show();
const compressedImg = document.getElementById('compressedImage');
const fullImg = document.getElementById('fullImage');
const bgImg = document.getElementById('bg-image');
$("#loading-progress").html(` Loading`);
if (useCompression) {
// Use compressed version for large JPG/PNG files
console.log('Large JPG/PNG detected (' + (fd.filesize / 1024 / 1024).toFixed(2) + 'MB), loading compressed version first');
fetch(ao_root + "system/ajgi/interface?script=Photo/backend/getCompressedImg.js", {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"filepath": fd.filepath
})
}).then(resp => {
resp.text().then(dataURL => {
$("#loading-progress").html(` Optimizing Resolution`);
compressedImageLoaded = true;
// Only show compressed image if full-size hasn't loaded yet
if (!fullsizeImageLoaded) {
compressedImg.src = dataURL;
compressedImg.style.display = 'block';
bgImg.src = dataURL;
} else {
console.log('Full-size image already loaded, skipping compressed image display');
}
});
}).catch(error => {
console.error('Failed to load compressed image:', error);
// Fall back to full size image
fullImg.src = imageUrl;
bgImg.src = imageUrl;
});
// Start loading full-size image in background
loadFullSizeImageInBackground(imageUrl, fd);
} else {
$("#compressedImage").hide();
$("#fullImage").show();
$("#loading-progress").hide();
// Use full image URL directly for RAW, WEBP, or small JPG/PNG files
if (method === 'backend_raw') {
console.log('RAW file: Rendered by backend');
}
fullImg.src = imageUrl;
bgImg.src = imageUrl;
}
// Update image dimensions and generate histogram when full image loads
$("#fullImage").off("load").on('load', function() {
fullsizeImageLoaded = true;
let width = this.naturalWidth;
let height = this.naturalHeight;
$("#info-dimensions").text(width + ' × ' + height + "px");
// Hide the compressed image once full image is loaded
$("#compressedImage").hide();
$("#fullImage").show();
$("#loading-progress").hide();
const canvas = document.getElementById('histogram-canvas');
if (canvas) {
generateHistogram(this, canvas);
}
});
$("#info-filename").text(fd.filename);
$("#info-filepath").text(fd.filepath);
var nextCard = $(object).next();
var prevCard = $(object).prev();
if (nextCard.length > 0){
nextPhoto = nextCard[0];
}else{
nextPhoto = null;
}
if (prevCard.length > 0){
prePhoto = prevCard[0];
}else{
prePhoto = null;
}
// Update navigation buttons state
if (typeof updateNavigationButtons === 'function') {
updateNavigationButtons();
}
ao_module_setWindowTitle("Photo - " + fd.filename);
window.location.hash = encodeURIComponent(JSON.stringify({filename: fd.filename, filepath: fd.filepath}));
// Check for EXIF data
fetch(ao_root + "system/ajgi/interface?script=Photo/backend/getExif.js", {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"filepath": fd.filepath
})
}).then(resp => {
resp.json().then(data => {
formatExifData(data, fd);
})
}).catch(error => {
console.error('Failed to fetch EXIF data:', error);
formatExifData({}, fd); // Call with empty EXIF to show tone analysis
});
});
}
// Function to load full-size image in background with progress tracking
function loadFullSizeImageInBackground(fullSizeUrl, fileData) {
console.log('Starting background download of full-size image...');
const fullImage = document.getElementById('fullImage');
fullImage.src = fullSizeUrl;
}
$(document).on("keydown", function(e){
if (e.keyCode == 27){ // Escape
if ($('#photo-viewer').is(':visible')) {
closeViewer();
}
} else if (e.keyCode == 37){
//Left
if (prePhoto != null){
showImage(prePhoto);
}
}else if (e.keyCode == 39){
//Right
if (nextPhoto != null){
showImage(nextPhoto);
}
}
})
function generateToneAnalysis(imageElement) {
analysis_tone_types(imageElement, function(result) {
if (result) {
// Update tone type based on brightness, contrast, shadow and highlight ratios
let toneType = get_tone_type(result.brightness, result.contrast, result.shadowRatio, result.highlightRatio);
$('.tone-type-value').text(toneType);
$('.brightness-value').text(result.brightness);
$('.contrast-value').text(result.contrast);
$('.shadow-ratio-value').text(result.shadowRatio);
$('.highlight-ratio-value').text(result.highlightRatio);
} else {
$('.tone-type-value').text("N/A");
$('.brightness-value').text("N/A");
$('.contrast-value').text("N/A");
$('.shadow-ratio-value').text("N/A");
$('.highlight-ratio-value').text("N/A");
}
});
}
function formatExifData(exif, fileData) {
// Hide all sections initially
$('#basic-info-section').hide();
$('#shooting-params-section').hide();
$('#tone-analysis-section').hide();
$('#device-info-section').hide();
$('#shooting-mode-section').hide();
$('#technical-params-section').hide();
$('#no-exif-message').hide();
// Hide all dividers
$('.ui.divider').hide();
if (!exif || Object.keys(exif).length === 0) {
$('#no-exif-message').show();
//Generate histogram and tone analysis only
generateHistogram(document.getElementById('fullImage'), document.getElementById('histogram-canvas'));
generateToneAnalysis(document.getElementById('fullImage'));
$('#tone-analysis-section').show();
return;
}
let sectionsShown = 0;
// Section 1: Basic Information
let basicInfoShown = false;
if (fileData.filename) {
let ext = fileData.filename.split('.').pop().toUpperCase();
$('#format-value').text(ext);
$('#format-row').show();
basicInfoShown = true;
} else {
$('#format-row').hide();
}
if (exif.PixelXDimension && exif.PixelYDimension) {
$('#dimensions-value').text(`${exif.PixelXDimension} × ${exif.PixelYDimension}`);
$('#dimensions-row').show();
let pixels = (exif.PixelXDimension * exif.PixelYDimension / 1000000).toFixed(1);
$('#pixels-value').text(`${pixels} MP`);
$('#pixels-row').show();
basicInfoShown = true;
} else {
$('#dimensions-row').hide();
$('#pixels-row').hide();
}
if (exif.ColorSpace !== undefined) {
exif.ColorSpace = JSON.parse(exif.ColorSpace);
let colorSpace = exif.ColorSpace === 1 ? "sRGB" : exif.ColorSpace === 65535 ? "Uncalibrated" : "Unknown";
$('#color-space-value').text(colorSpace);
$('#color-space-row').show();
basicInfoShown = true;
} else {
$('#color-space-row').hide();
}
if (exif.DateTimeOriginal) {
exif.DateTimeOriginal = JSON.parse(exif.DateTimeOriginal);
$('#shooting-time-value').text(exif.DateTimeOriginal.replace(/:/g, '/').replace(' ', ' '));
$('#shooting-time-row').show();
basicInfoShown = true;
} else {
$('#shooting-time-row').hide();
}
if (exif.Software) {
exif.Software = JSON.parse(exif.Software);
$('#software-value').text(exif.Software);
$('#software-row').show();
basicInfoShown = true;
} else {
$('#software-row').hide();
}
if (basicInfoShown) {
$('#basic-info-section').show();
sectionsShown++;
if (sectionsShown > 1) $('#basic-info-divider').show();
}
// Section 2: Shooting Parameters
let shootingParamsShown = false;
if (exif.FocalLength) {
$('#focal-length-value').text(JSON.parse(exif.FocalLength));
$('#focal-length-row').show();
shootingParamsShown = true;
} else {
$('#focal-length-row').hide();
}
if (exif.FNumber) {
exif.FNumber = JSON.parse(exif.FNumber);
let aperture = parseExifValue(exif.FNumber);
let formattedAperture = aperture % 1 === 0 ? aperture.toString() : aperture.toFixed(1);
$('#aperture-value').text('f/' + formattedAperture);
$('#aperture-row').show();
shootingParamsShown = true;
} else {
$('#aperture-row').hide();
}
if (exif.ExposureTime) {
let exposureTime = JSON.parse(exif.ExposureTime);
let formattedExposure = formatShutterSpeed(exposureTime);
$('#shutter-speed-value').text(formattedExposure + 's');
$('#shutter-speed-row').show();
shootingParamsShown = true;
} else {
$('#shutter-speed-row').hide();
}
if (exif.ISOSpeedRatings) {
$('#iso-value').text(exif.ISOSpeedRatings);
$('#iso-row').show();
shootingParamsShown = true;
} else {
$('#iso-row').hide();
}
if (exif.ExposureBiasValue) {
$('#ev-value').text(JSON.parse(exif.ExposureBiasValue));
$('#ev-row').show();
shootingParamsShown = true;
} else {
$('#ev-row').hide();
}
if (shootingParamsShown) {
$('#shooting-params-section').show();
sectionsShown++;
if (sectionsShown > 1) $('#shooting-params-divider').show();
}
// Section 3: Tone Analysis
$('#tone-analysis-section').show();
sectionsShown++;
if (sectionsShown > 1) $('#tone-analysis-divider').show();
generateToneAnalysis(document.getElementById('fullImage'));
// Section 4: Device Information
let deviceInfoShown = false;
if (exif.Make && exif.Model) {
exif.Make = JSON.parse(exif.Make);
exif.Model = JSON.parse(exif.Model);
$('#camera-value').text(`${exif.Make} ${exif.Model}`);
$('#camera-row').show();
deviceInfoShown = true;
} else if (exif.Model) {
exif.Model = JSON.parse(exif.Model);
$('#camera-value').text(exif.Model);
$('#camera-row').show();
deviceInfoShown = true;
} else {
$('#camera-row').hide();
}
if (exif.LensModel) {
exif.LensModel = JSON.parse(exif.LensModel);
$('#lens-value').text(exif.LensModel);
$('#lens-row').show();
deviceInfoShown = true;
} else {
$('#lens-row').hide();
}
if (exif.FocalLength) {
exif.FocalLength = JSON.parse(exif.FocalLength);
$('#focal-length-device-value').text(`${exif.FocalLength}mm`);
$('#focal-length-device-row').show();
deviceInfoShown = true;
} else {
$('#focal-length-device-row').hide();
}
if (exif.MaxApertureValue) {
exif.MaxApertureValue = JSON.parse(exif.MaxApertureValue);
$('#max-aperture-value').text(exif.MaxApertureValue);
$('#max-aperture-row').show();
deviceInfoShown = true;
} else {
$('#max-aperture-row').hide();
}
if (deviceInfoShown) {
$('#device-info-section').show();
sectionsShown++;
if (sectionsShown > 1) $('#device-info-divider').show();
}
// Section 5: Shooting Mode
let shootingModeShown = false;
if (exif.ExposureProgram !== undefined) {
let programs = ["Not defined", "Manual", "Normal program", "Aperture priority", "Shutter priority", "Creative program", "Action program", "Portrait mode", "Landscape mode"];
let program = programs[exif.ExposureProgram] || "Unknown";
$('#exposure-program-value').text(program);
$('#exposure-program-row').show();
shootingModeShown = true;
} else {
$('#exposure-program-row').hide();
}
if (exif.ExposureMode !== undefined) {
let modes = ["Auto exposure", "Manual exposure", "Auto bracket"];
let mode = modes[exif.ExposureMode] || "Unknown";
$('#exposure-mode-value').text(mode);
$('#exposure-mode-row').show();
shootingModeShown = true;
} else {
$('#exposure-mode-row').hide();
}
if (exif.MeteringMode !== undefined) {
let metering = ["Unknown", "Average", "Center-weighted average", "Spot", "Multi-spot", "Pattern", "Partial"];
let meter = metering[exif.MeteringMode] || "Unknown";
$('#metering-mode-value').text(meter);
$('#metering-mode-row').show();
shootingModeShown = true;
} else {
$('#metering-mode-row').hide();
}
if (exif.WhiteBalance !== undefined) {
let wb = exif.WhiteBalance === 0 ? "Auto" : "Manual";
$('#white-balance-value').text(wb);
$('#white-balance-row').show();
shootingModeShown = true;
} else {
$('#white-balance-row').hide();
}
if (exif.Flash !== undefined) {
let flash = (exif.Flash & 1) ? "On" : "Off";
$('#flash-value').text(flash);
$('#flash-row').show();
shootingModeShown = true;
} else {
$('#flash-row').hide();
}
if (exif.SceneCaptureType !== undefined) {
let scenes = ["Standard", "Landscape", "Portrait", "Night scene"];
let scene = scenes[exif.SceneCaptureType] || "Unknown";
$('#scene-capture-value').text(scene);
$('#scene-capture-row').show();
shootingModeShown = true;
} else {
$('#scene-capture-row').hide();
}
if (shootingModeShown) {
$('#shooting-mode-section').show();
sectionsShown++;
if (sectionsShown > 1) $('#shooting-mode-divider').show();
}
// Section 6: Technical Parameters
let technicalParamsShown = false;
if (exif.ShutterSpeedValue) {
exif.ShutterSpeedValue = JSON.parse(exif.ShutterSpeedValue);
let apexValue = parseExifValue(exif.ShutterSpeedValue);
let shutterSpeedSeconds = Math.pow(2, -apexValue);
let shutterValue = formatShutterSpeed(shutterSpeedSeconds);
$('#shutter-speed-tech-value').text(shutterValue);
$('#shutter-speed-tech-row').show();
technicalParamsShown = true;
} else {
$('#shutter-speed-tech-row').hide();
}
if (exif.ApertureValue) {
exif.ApertureValue = JSON.parse(exif.ApertureValue);
let apexValue = parseExifValue(exif.ApertureValue);
let apertureValue = Math.pow(2, apexValue / 2);
$('#aperture-value-value').text(apertureValue.toFixed(1) + ' EV');
$('#aperture-value-row').show();
technicalParamsShown = true;
} else {
$('#aperture-value-row').hide();
}
if (exif.FocalPlaneXResolution && exif.FocalPlaneYResolution) {
exif.FocalPlaneXResolution = JSON.parse(exif.FocalPlaneXResolution);
exif.FocalPlaneYResolution = JSON.parse(exif.FocalPlaneYResolution);
let xRes = parseExifValue(exif.FocalPlaneXResolution);
let yRes = parseExifValue(exif.FocalPlaneYResolution);
$('#focal-plane-res-value').text(Math.round(xRes) + ' × ' + Math.round(yRes));
$('#focal-plane-res-row').show();
technicalParamsShown = true;
} else {
$('#focal-plane-res-row').hide();
}
if (technicalParamsShown) {
$('#technical-params-section').show();
sectionsShown++;
if (sectionsShown > 1) $('#technical-params-divider').show();
}
}
function restoreFromHash() {
if (window.location.hash) {
let hashData = decodeURIComponent(window.location.hash.substring(1));
try {
let data = JSON.parse(hashData);
// Find the element with matching filepath
let elements = document.querySelectorAll('[filedata]');
for (let el of elements) {
let fdStr = el.getAttribute('filedata');
if (fdStr) {
let fd = JSON.parse(decodeURIComponent(fdStr));
if (fd.filepath === data.filepath) {
showImage(el);
ShowModal();
break;
}
}
}
} catch (e) {
console.error('Invalid hash data', e);
}
}
}
// Modify the window onload event to ensure folder and thumbnails are loaded first
window.addEventListener('load', () => {
setTimeout(function(){
if (window.location.hash) {
const hashData = decodeURIComponent(window.location.hash.substring(1));
try {
const data = JSON.parse(hashData);
let filename = data.filename;
let filepath = data.filepath;
let dir = filepath.split("/").slice(0, -1).join("/");
// Access the Alpine data instance
const appElement = document.querySelector('[x-data*="photoListObject"]');
if (appElement) {
const app = appElement._x_dataStack[0];
if (app.currentPath !== dir) {
app.updateRenderingPath(dir, () => {
setTimeout(function(){
console.log("Test")
restoreFromHash();
}, 100);
});
} else {
// Folder is already loaded, try to restore immediately
restoreFromHash();
}
}
} catch (e) {
console.error('Invalid hash data', e);
}
}
}, 100);
});