sort and bind

This commit is contained in:
2026-02-09 09:35:02 +01:00
committed by Florian Greistorfer
parent 45cc7c98f1
commit 61c7f0cd43

View File

@@ -6,30 +6,66 @@ class PhotoGallery {
this.subfolders = []; this.subfolders = [];
this.tagDropdownShown = false; this.tagDropdownShown = false;
this.darkMode = this.darkMode.bind(this);
this.darkModeToggle = this.darkModeToggle.bind(this);
this.debounce = this.debounce.bind(this); this.debounce = this.debounce.bind(this);
this.openSwipe = this.openSwipe.bind(this); this.detectDarkMode = this.detectDarkMode.bind(this);
this.prefetch = this.prefetch.bind(this);
this.reset = this.reset.bind(this);
this.recursive = this.recursive.bind(this);
this.requestMetadata = this.requestMetadata.bind(this);
this.filter = this.filter.bind(this); this.filter = this.filter.bind(this);
this.updateImageList = this.updateImageList.bind(this); this.finalize = this.finalize.bind(this);
this.insertPath = this.insertPath.bind(this);
this.lightMode = this.lightMode.bind(this);
this.onLoad = this.onLoad.bind(this);
this.openSwipe = this.openSwipe.bind(this);
this.parseHierarchicalTags = this.parseHierarchicalTags.bind(this);
this.prefetch = this.prefetch.bind(this);
this.recursive = this.recursive.bind(this);
this.renderTree = this.renderTree.bind(this);
this.requestMetadata = this.requestMetadata.bind(this);
this.reset = this.reset.bind(this);
this.scrollFunction = this.scrollFunction.bind(this);
this.setFilter = this.setFilter.bind(this); this.setFilter = this.setFilter.bind(this);
this.toggleTag = this.toggleTag.bind(this); this.setupClickHandlers = this.setupClickHandlers.bind(this);
this.setupDropdownToggle = this.setupDropdownToggle.bind(this); this.setupDropdownToggle = this.setupDropdownToggle.bind(this);
this.setupTagHandlers = this.setupTagHandlers.bind(this); this.setupTagHandlers = this.setupTagHandlers.bind(this);
this.setupClickHandlers = this.setupClickHandlers.bind(this); this.showLoader = this.showLoader.bind(this);
this.scrollFunction = this.scrollFunction.bind(this); this.toggleTag = this.toggleTag.bind(this);
this.topFunction = this.topFunction.bind(this); this.topFunction = this.topFunction.bind(this);
this.onLoad = this.onLoad.bind(this); this.updateImageList = this.updateImageList.bind(this);
this.darkMode = this.darkMode.bind(this);
this.lightMode = this.lightMode.bind(this);
this.darkModeToggle = this.darkModeToggle.bind(this);
this.detectDarkMode = this.detectDarkMode.bind(this);
this.init(); this.init();
} }
darkMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
localStorage.setItem("theme", "dark");
if (themeLink) themeLink.disabled = true;
if (darkThemeLink) darkThemeLink.disabled = false;
}
darkModeToggle(mode) {
const switchState = document.getElementById("dark-mode-switch-check");
if (mode == "dark") {
this.darkMode();
if (switchState) {
switchState.checked = true;
}
} else if (mode == "light") {
this.lightMode();
if (switchState) {
switchState.checked = false;
}
} else {
if (switchState.checked) {
switchState.checked = false;
this.lightMode();
} else {
switchState.checked = true;
this.darkMode();
}
}
}
debounce(fn, delay) { debounce(fn, delay) {
let timeoutId; let timeoutId;
return (...args) => { return (...args) => {
@@ -38,141 +74,27 @@ class PhotoGallery {
}; };
} }
openSwipe(imgIndex) { detectDarkMode() {
const options = { index: imgIndex }; if (document.getElementById("darktheme")) {
const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options); const switchState = document.getElementById("dark-mode-switch-check");
gallery.init(); const localStorageTheme = localStorage.getItem("theme");
} if (localStorageTheme === "dark") {
switchState.checked = true;
prefetch(imgIndex) { this.darkModeToggle("dark");
const prefetchDiv = document.getElementById("img-prefetch"); return;
if (!prefetchDiv) return; } else if (localStorageTheme === "light") {
switchState.checked = true;
const img = document.createElement("img"); this.darkModeToggle("light");
img.src = this.shown[imgIndex]?.src || ""; return;
prefetchDiv.removeChild(prefetchDiv.firstChild); }
prefetchDiv.appendChild(img); if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
} switchState.checked = true;
this.darkModeToggle("dark");
reset() { } else {
const content = document.documentElement.innerHTML; switchState.checked = false;
const title = document.title; this.darkModeToggle("light");
const folders = document.querySelector(".folders");
let path = window.location.origin + window.location.pathname;
if (path.startsWith("null")) {
path = window.location.protocol + "//" + path.substring(4);
}
if (folders) folders.style.display = "";
document.getElementById("recursive").checked = false;
document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false));
window.history.replaceState({ html: content, pageTitle: title }, "", path);
this.requestMetadata();
}
showLoader() {
const imagelist = document.getElementById("imagelist");
imagelist.innerHTML = '<span class="loader"></span>';
imagelist.classList.add("centerload");
imagelist.classList.remove("row");
}
async recursive() {
this.showLoader();
const loc = new URL(window.location.href);
const content = document.documentElement.innerHTML;
const title = document.title;
const isChecked = document.getElementById("recursive")?.checked;
const folders = document.querySelector(".folders");
if (!isChecked) {
if (folders) folders.style.display = "";
loc.searchParams.delete("recursive");
window.history.replaceState({ html: content, pageTitle: title }, "", loc);
this.requestMetadata();
return;
}
if (folders) folders.style.display = "none";
loc.searchParams.delete("recursive");
loc.searchParams.append("recursive", true);
window.history.replaceState({ html: content, pageTitle: title }, "", loc);
const visited = new Set();
const existingItems = new Set();
const newItems = [];
try {
const response = await fetch(".metadata.json");
if (!response.ok) throw new Error("Failed to fetch metadata");
const data = await response.json();
this.items = [];
this.subfolders = data.subfolders || [];
for (const image of Object.values(data.images || {})) {
newItems.push(image);
existingItems.add(image.src);
} }
} catch {
return;
} }
const fetchFoldersRecursively = async (folderList) => {
if (!Array.isArray(folderList)) return;
const nextLevel = [];
await Promise.all(
folderList.map(async (folder) => {
if (!folder || !folder.metadata || visited.has(folder.url)) return;
visited.add(folder.url);
try {
const response = await fetch(folder.metadata);
if (!response.ok) throw new Error();
const data = await response.json();
for (const image of Object.values(data.images || {})) {
if (!existingItems.has(image.src)) {
newItems.push(image);
existingItems.add(image.src);
}
}
if (Array.isArray(data.subfolders)) nextLevel.push(...data.subfolders);
} catch {}
}),
);
if (nextLevel.length > 0) await fetchFoldersRecursively(nextLevel);
};
await fetchFoldersRecursively(this.subfolders);
this.items = [...newItems];
this.filter();
}
requestMetadata() {
this.showLoader();
const hash = window.location.hash;
const searchParams = new URLSearchParams(window.location.search);
fetch(".metadata.json")
.then((response) => {
if (!response.ok) throw new Error("Failed to fetch metadata");
return response.json();
})
.then((data) => {
this.items = Object.values(data.images || {});
this.subfolders = data.subfolders || [];
if (hash != "") {
const selected = hash.replace("#", "").split(",");
this.setFilter(selected);
}
if (searchParams.get("recursive") != null) {
const recChk = document.getElementById("recursive");
if (recChk) recChk.checked = true;
this.recursive();
} else {
this.filter();
}
})
.catch(() => {});
} }
filter() { filter() {
@@ -224,23 +146,6 @@ class PhotoGallery {
} }
} }
insertPath(obj, path) {
let current = obj;
for (let i = 0; i < path.length; i++) {
const part = path[i];
if (i === path.length - 1) {
if (!current[part]) {
current[part] = null;
}
} else {
if (!current[part] || typeof current[part] !== "object") {
current[part] = {};
}
current = current[part];
}
}
}
finalize(obj) { finalize(obj) {
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) { if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
const result = {}; const result = {};
@@ -258,6 +163,55 @@ class PhotoGallery {
return obj || []; return obj || [];
} }
insertPath(obj, path) {
let current = obj;
for (let i = 0; i < path.length; i++) {
const part = path[i];
if (i === path.length - 1) {
if (!current[part]) {
current[part] = null;
}
} else {
if (!current[part] || typeof current[part] !== "object") {
current[part] = {};
}
current = current[part];
}
}
}
lightMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
localStorage.setItem("theme", "light");
if (themeLink) themeLink.disabled = false;
if (darkThemeLink) darkThemeLink.disabled = true;
}
onLoad() {
document.querySelectorAll(".tagtoggle").forEach((toggle) => {
toggle.addEventListener("mouseup", (event) => {
event.stopPropagation();
const tagid = toggle.getAttribute("data-tagid");
this.toggleTag(tagid);
});
});
this.requestMetadata();
this.setupDropdownToggle();
this.setupTagHandlers();
this.setupClickHandlers();
this.detectDarkMode();
window.addEventListener("scroll", this.scrollFunction);
}
openSwipe(imgIndex) {
const options = { index: imgIndex };
const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options);
gallery.init();
}
parseHierarchicalTags(tags, delimiter = "|") { parseHierarchicalTags(tags, delimiter = "|") {
const tree = {}; const tree = {};
for (const tag of tags) { for (const tag of tags) {
@@ -267,6 +221,88 @@ class PhotoGallery {
return this.finalize(tree); return this.finalize(tree);
} }
prefetch(imgIndex) {
const prefetchDiv = document.getElementById("img-prefetch");
if (!prefetchDiv) return;
const img = document.createElement("img");
img.src = this.shown[imgIndex]?.src || "";
prefetchDiv.removeChild(prefetchDiv.firstChild);
prefetchDiv.appendChild(img);
}
async recursive() {
this.showLoader();
const loc = new URL(window.location.href);
const content = document.documentElement.innerHTML;
const title = document.title;
const isChecked = document.getElementById("recursive")?.checked;
const folders = document.querySelector(".folders");
if (!isChecked) {
if (folders) folders.style.display = "";
loc.searchParams.delete("recursive");
window.history.replaceState({ html: content, pageTitle: title }, "", loc);
this.requestMetadata();
return;
}
this.showLoader();
if (folders) folders.style.display = "none";
loc.searchParams.delete("recursive");
loc.searchParams.append("recursive", true);
window.history.replaceState({ html: content, pageTitle: title }, "", loc);
const visited = new Set();
const existingItems = new Set();
const newItems = [];
try {
const response = await fetch(".metadata.json");
if (!response.ok) throw new Error("Failed to fetch metadata");
const data = await response.json();
this.items = [];
this.subfolders = data.subfolders || [];
for (const image of Object.values(data.images || {})) {
newItems.push(image);
existingItems.add(image.src);
}
} catch {
return;
}
const fetchFoldersRecursively = async (folderList) => {
if (!Array.isArray(folderList)) return;
const nextLevel = [];
await Promise.all(
folderList.map(async (folder) => {
if (!folder || !folder.metadata || visited.has(folder.url)) return;
visited.add(folder.url);
try {
const response = await fetch(folder.metadata);
if (!response.ok) throw new Error();
const data = await response.json();
for (const image of Object.values(data.images || {})) {
if (!existingItems.has(image.src)) {
newItems.push(image);
existingItems.add(image.src);
}
}
if (Array.isArray(data.subfolders)) nextLevel.push(...data.subfolders);
} catch {}
}),
);
if (nextLevel.length > 0) await fetchFoldersRecursively(nextLevel);
};
this.showLoader();
await fetchFoldersRecursively(this.subfolders);
this.items = [...newItems];
this.filter();
}
renderTree = (obj, depth = 0) => { renderTree = (obj, depth = 0) => {
let lines = []; let lines = [];
const indent = "&nbsp;&nbsp;".repeat(depth); const indent = "&nbsp;&nbsp;".repeat(depth);
@@ -283,24 +319,58 @@ class PhotoGallery {
return lines.join("\n"); return lines.join("\n");
}; };
updateImageList() { requestMetadata() {
this.showLoader(); this.showLoader();
const imagelist = document.getElementById("imagelist"); const hash = window.location.hash;
if (!imagelist) return; const searchParams = new URLSearchParams(window.location.search);
let str = ""; fetch(".metadata.json")
this.shown.sort((a, b) => a.src.replace(a.name, "").localeCompare(b.src.replace(b.name, ""))); .then((response) => {
this.shown.forEach((item, index) => { if (!response.ok) throw new Error("Failed to fetch metadata");
let tags = this.parseHierarchicalTags(item.tags || []); return response.json();
str += `<div class="column"><figure title="${this.renderTree(tags)}"><img src="${ })
item.msrc .then((data) => {
}" data-index="${index}" /><figcaption class="caption">${item.name}`; this.items = Object.values(data.images || {});
if (item.tiff) str += `&nbsp;<a href="${item.tiff}">TIFF</a>`; this.subfolders = data.subfolders || [];
if (item.raw) str += `&nbsp;<a href="${item.raw}">RAW</a>`;
str += "</figcaption></figure></div>"; if (hash != "") {
}); const selected = hash.replace("#", "").split(",");
imagelist.classList.add("row"); this.setFilter(selected);
imagelist.classList.remove("centerload"); }
imagelist.innerHTML = str; if (searchParams.get("recursive") != null) {
const recChk = document.getElementById("recursive");
if (recChk) recChk.checked = true;
this.recursive();
} else {
this.filter();
}
})
.catch(() => {});
}
reset() {
const content = document.documentElement.innerHTML;
const title = document.title;
const folders = document.querySelector(".folders");
let path = window.location.origin + window.location.pathname;
if (path.startsWith("null")) {
path = window.location.protocol + "//" + path.substring(4);
}
if (folders) folders.style.display = "";
document.getElementById("recursive").checked = false;
document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false));
window.history.replaceState({ html: content, pageTitle: title }, "", path);
this.requestMetadata();
}
scrollFunction() {
const totopbutton = document.getElementById("totop");
if (!totopbutton) return;
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
totopbutton.style.display = "block";
} else {
totopbutton.style.display = "none";
}
} }
setFilter(selected) { setFilter(selected) {
@@ -313,13 +383,35 @@ class PhotoGallery {
}); });
} }
toggleTag(tagid) { setupClickHandlers() {
const tag = document.getElementById(tagid); const resetEl = document.getElementById("reset-filter")?.querySelector("label");
const ol = tag?.closest(".tagentry")?.querySelector(".tagentryparent"); if (resetEl) resetEl.addEventListener("click", this.reset);
const svg = tag?.parentElement.querySelector(".tagtoggle svg");
if (!ol || !svg) return; const recurseEl = document.getElementById("recursive");
ol.classList.toggle("show"); if (recurseEl) recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
const totop = document.getElementById("totop");
if (totop) totop.addEventListener("click", this.topFunction);
const darkModeSwitch = document.getElementById("dark-mode-switch");
if (darkModeSwitch) darkModeSwitch.addEventListener("click", this.darkModeToggle);
const imagelist = document.getElementById("imagelist");
if (imagelist) {
imagelist.addEventListener("click", (event) => {
const img = event.target.closest("img");
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.openSwipe(index);
});
imagelist.addEventListener("mousemove", (event) => {
const img = event.target.closest("img");
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.prefetch(index);
});
}
} }
setupDropdownToggle() { setupDropdownToggle() {
@@ -362,129 +454,44 @@ class PhotoGallery {
}); });
} }
setupClickHandlers() { showLoader() {
const resetEl = document.getElementById("reset-filter")?.querySelector("label");
if (resetEl) resetEl.addEventListener("click", this.reset);
const recurseEl = document.getElementById("recursive");
if (recurseEl) recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
const totop = document.getElementById("totop");
if (totop) totop.addEventListener("click", this.topFunction);
const darkModeSwitch = document.getElementById("dark-mode-switch");
if (darkModeSwitch) darkModeSwitch.addEventListener("click", this.darkModeToggle);
const imagelist = document.getElementById("imagelist"); const imagelist = document.getElementById("imagelist");
if (imagelist) { imagelist.innerHTML = '<span class="loader"></span>';
imagelist.addEventListener("click", (event) => { imagelist.classList.add("centerload");
const img = event.target.closest("img"); imagelist.classList.remove("row");
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.openSwipe(index);
});
imagelist.addEventListener("mousemove", (event) => {
const img = event.target.closest("img");
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.prefetch(index);
});
}
} }
scrollFunction() { toggleTag(tagid) {
const totopbutton = document.getElementById("totop"); const tag = document.getElementById(tagid);
if (!totopbutton) return; const ol = tag?.closest(".tagentry")?.querySelector(".tagentryparent");
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) { const svg = tag?.parentElement.querySelector(".tagtoggle svg");
totopbutton.style.display = "block"; if (!ol || !svg) return;
} else { ol.classList.toggle("show");
totopbutton.style.display = "none"; svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
}
} }
topFunction() { topFunction() {
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
darkMode() { updateImageList() {
const themeLink = document.getElementById("theme"); this.showLoader();
const darkThemeLink = document.getElementById("darktheme"); const imagelist = document.getElementById("imagelist");
localStorage.setItem("theme", "dark"); if (!imagelist) return;
if (themeLink) themeLink.disabled = true; let str = "";
if (darkThemeLink) darkThemeLink.disabled = false; this.shown.sort((a, b) => a.src.replace(a.name, "").localeCompare(b.src.replace(b.name, "")));
} this.shown.forEach((item, index) => {
let tags = this.parseHierarchicalTags(item.tags || []);
lightMode() { str += `<div class="column"><figure title="${this.renderTree(tags)}"><img src="${
const themeLink = document.getElementById("theme"); item.msrc
const darkThemeLink = document.getElementById("darktheme"); }" data-index="${index}" /><figcaption class="caption">${item.name}`;
localStorage.setItem("theme", "light"); if (item.tiff) str += `&nbsp;<a href="${item.tiff}">TIFF</a>`;
if (themeLink) themeLink.disabled = false; if (item.raw) str += `&nbsp;<a href="${item.raw}">RAW</a>`;
if (darkThemeLink) darkThemeLink.disabled = true; str += "</figcaption></figure></div>";
}
darkModeToggle(mode) {
const switchState = document.getElementById("dark-mode-switch-check");
if (mode == "dark") {
this.darkMode();
if (switchState) {
switchState.checked = true;
}
} else if (mode == "light") {
this.lightMode();
if (switchState) {
switchState.checked = false;
}
} else {
if (switchState.checked) {
switchState.checked = false;
this.lightMode();
} else {
switchState.checked = true;
this.darkMode();
}
}
}
detectDarkMode() {
if (document.getElementById("darktheme")) {
const switchState = document.getElementById("dark-mode-switch-check");
const localStorageTheme = localStorage.getItem("theme");
if (localStorageTheme === "dark") {
switchState.checked = true;
this.darkModeToggle("dark");
return;
} else if (localStorageTheme === "light") {
switchState.checked = true;
this.darkModeToggle("light");
return;
}
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
switchState.checked = true;
this.darkModeToggle("dark");
} else {
switchState.checked = false;
this.darkModeToggle("light");
}
}
}
onLoad() {
document.querySelectorAll(".tagtoggle").forEach((toggle) => {
toggle.addEventListener("mouseup", (event) => {
event.stopPropagation();
const tagid = toggle.getAttribute("data-tagid");
this.toggleTag(tagid);
});
}); });
imagelist.classList.add("row");
this.requestMetadata(); imagelist.classList.remove("centerload");
this.setupDropdownToggle(); imagelist.innerHTML = str;
this.setupTagHandlers();
this.setupClickHandlers();
this.detectDarkMode();
window.addEventListener("scroll", this.scrollFunction);
} }
init() { init() {