moved javascript to global file, added js minifier and html formatter

This commit is contained in:
2025-07-05 00:54:17 +02:00
parent 9b7c3dc697
commit 3f427dfa32
10 changed files with 462 additions and 351 deletions

View File

@@ -1 +1 @@
2.8.2 2.8.3

View File

@@ -11,6 +11,7 @@ from pathlib import Path
from tqdm.auto import tqdm from tqdm.auto import tqdm
from PIL import Image, ImageOps from PIL import Image, ImageOps
from jsmin import jsmin
from modules.argumentparser import parse_arguments, Args from modules.argumentparser import parse_arguments, Args
@@ -123,6 +124,10 @@ def copy_static_files(_args: Args) -> None:
with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f: with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f:
logger.info("writing theme file") logger.info("writing theme file")
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail) f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
logger.info("minifying javascript")
with open(os.path.join(SCRIPTDIR, "templates", "functionality.js"), "r", encoding="utf-8") as js_file:
with open(os.path.join(static_dir, "functionality.min.js"), "w+", encoding="utf-8") as min_file:
min_file.write(jsmin(js_file.read()))
def generate_thumbnail(arguments: tuple[str, str, str]) -> None: def generate_thumbnail(arguments: tuple[str, str, str]) -> None:

View File

@@ -208,7 +208,7 @@ input {
} }
.tooltiptext.tagdropdown.show { .tooltiptext.tagdropdown.show {
max-height: 286px; max-height: 80vh;
overflow-y: scroll; overflow-y: scroll;
opacity: 1; opacity: 1;
} }
@@ -232,7 +232,7 @@ input {
} }
.tagentryparent.show { .tagentryparent.show {
max-height: 286px; max-height: 80vh;
opacity: 1; opacity: 1;
overflow-y: scroll; overflow-y: scroll;
} }

View File

@@ -3,6 +3,7 @@ import re
import urllib.parse import urllib.parse
import fnmatch import fnmatch
import json import json
import html
from typing import Any from typing import Any
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
@@ -11,6 +12,7 @@ from tqdm.auto import tqdm
from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from defusedxml import ElementTree from defusedxml import ElementTree
from bs4 import BeautifulSoup
from modules.logger import logger from modules.logger import logger
from modules import cclicense from modules import cclicense
@@ -516,17 +518,18 @@ def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dic
def process_license(folder: str, item: str) -> None: def process_license(folder: str, item: str) -> None:
""" """
Processes a LICENSE file. Processes a LICENSE file, preserving formatting in HTML.
Args: Args:
folder (str): The folder containing the info file. folder (str): The folder containing the LICENSE file.
item (str): The licenses file name. item (str): The LICENSE file name.
""" """
with open(os.path.join(folder, item), encoding="utf-8") as f: path = os.path.join(folder, item)
logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)}) with open(path, encoding="utf-8") as f:
licens[urllib.parse.quote(folder)] = ( logger.info("processing LICENSE", extra={"path": path})
f.read().replace("\n", "</br>\n").replace(" ", "&emsp;").replace(" ", "&ensp;").replace("sp; ", "sp;&ensp;").replace("&ensp;&ensp;", "&emsp;") raw_text = f.read()
) escaped_text = html.escape(raw_text)
licens[urllib.parse.quote(folder)] = f"<pre>{escaped_text}</pre>"
def process_info_file(folder: str, item: str) -> None: def process_info_file(folder: str, item: str) -> None:
@@ -556,6 +559,11 @@ def should_generate_html(images: list[dict[str, Any]], contains_files, _args: Ar
return images or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files) return images or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files)
def format_html(html: str) -> str:
soup = BeautifulSoup(html, "html5lib")
return soup.prettify()
def create_html_file( def create_html_file(
folder: str, title: str, foldername: str, images: list[dict[str, Any]], subfolders: list[dict[str, str]], _args: Args, version: str, logo: str, subfoldertags: list[str] folder: str, title: str, foldername: str, images: list[dict[str, Any]], subfolders: list[dict[str, str]], _args: Args, version: str, logo: str, subfoldertags: list[str]
) -> list[str]: ) -> list[str]:
@@ -624,7 +632,7 @@ def create_html_file(
logo=logo, logo=logo,
licensefile=folder_license, licensefile=folder_license,
) )
f.write(content) f.write(format_html(content))
html = env.get_template("index.html.j2") html = env.get_template("index.html.j2")
content = html.render( content = html.render(
@@ -646,8 +654,8 @@ def create_html_file(
) )
with open(html_file, "w", encoding="utf-8") as f: with open(html_file, "w", encoding="utf-8") as f:
logger.info("writing html file", extra={"path": html_file}) logger.info("writing formatted html file", extra={"path": html_file})
f.write(content) f.write(format_html(content))
return sorted(alltags) return sorted(alltags)

View File

@@ -146,7 +146,7 @@ def render_manifest_json(_args: Args, icon_list: list[Icon], colors: dict[str, s
colors : dict[str, str] colors : dict[str, str]
dictionary containing color scheme and theme color. dictionary containing color scheme and theme color.
""" """
manifest = env.get_template("manifest.json.j2") manifest = env.get_template("manifest.webmanifest.j2")
content = manifest.render( content = manifest.render(
name=_args.web_root_url.replace("https://", "").replace("http://", "").replace("/", ""), name=_args.web_root_url.replace("https://", "").replace("http://", "").replace("/", ""),
short_name=_args.site_title, short_name=_args.site_title,
@@ -154,8 +154,8 @@ def render_manifest_json(_args: Args, icon_list: list[Icon], colors: dict[str, s
background_color=colors["bcolor1"], background_color=colors["bcolor1"],
theme_color=colors["theme_color"], theme_color=colors["theme_color"],
) )
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f: with open(os.path.join(_args.root_directory, ".static", "manifest.webmanifest"), "w", encoding="utf-8") as f:
logger.info("rendering manifest.json", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.json")}) logger.info("rendering manifest.webmanifest", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.webmanifest")})
f.write(content) f.write(content)

View File

@@ -1,6 +1,8 @@
CairoSVG==2.7.1 CairoSVG==2.7.1
defusedxml==0.7.1 defusedxml==0.7.1
html5lib==1.1
Jinja2==3.1.5 Jinja2==3.1.5
jsmin==3.0.1
Pillow==11.1.0 Pillow==11.1.0
pyinstaller==6.11.1 pyinstaller==6.11.1
python_json_logger==2.0.7 python_json_logger==2.0.7

410
templates/functionality.js Normal file
View File

@@ -0,0 +1,410 @@
class PhotoGallery {
constructor() {
this.pswpElement = document.querySelector(".pswp");
this.re = /pid=(\d+)/;
this.filterRe = /#(.*)/;
this.recursiveRe = /\?recursive/;
this.items = [];
this.shown = [];
this.subfolders = [];
this.controllers = {};
this.tagDropdownShown = false;
this.debounce = this.debounce.bind(this);
this.openSwipe = this.openSwipe.bind(this);
this.prefetch = this.prefetch.bind(this);
this.cancel = this.cancel.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.updateImageList = this.updateImageList.bind(this);
this.setFilter = this.setFilter.bind(this);
this.toggleTag = this.toggleTag.bind(this);
this.setupDropdownToggle = this.setupDropdownToggle.bind(this);
this.setupTagHandlers = this.setupTagHandlers.bind(this);
this.setupClickHandlers = this.setupClickHandlers.bind(this);
this.scrollFunction = this.scrollFunction.bind(this);
this.topFunction = this.topFunction.bind(this);
this.onLoad = this.onLoad.bind(this);
this.init();
}
debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
openSwipe(imgIndex) {
const options = { index: imgIndex };
const gallery = new PhotoSwipe(
this.pswpElement,
PhotoSwipeUI_Default,
this.shown,
options
);
gallery.init();
}
prefetch(imgIndex) {
if (this.controllers[imgIndex]) {
this.cancel(imgIndex);
}
const controller = new AbortController();
const signal = controller.signal;
this.controllers[imgIndex] = controller;
const urlToFetch = this.items[imgIndex]?.src;
if (urlToFetch) {
fetch(urlToFetch, { method: "GET", signal }).catch(() => {});
}
}
cancel(imgIndex) {
if (this.controllers[imgIndex]) {
this.controllers[imgIndex].abort();
delete this.controllers[imgIndex];
}
}
reset() {
const curr = window.location.href.split("#");
const content = document.documentElement.innerHTML;
const title = document.title;
const folders = document.querySelector(".folders");
if (folders) folders.style.display = "";
document
.querySelectorAll("#tagdropdown input.tagcheckbox:checked")
.forEach((checkbox) => (checkbox.checked = false));
window.history.replaceState(
{ html: content, pageTitle: title },
"",
curr[0].split("?")[0] + "#"
);
this.requestMetadata();
}
async recursive() {
const curr = window.location.href.split("#");
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 = "";
window.history.replaceState(
{ html: content, pageTitle: title },
"",
curr[0].split("?")[0] + "#" + (curr[1] || "")
);
this.requestMetadata();
return;
}
if (folders) folders.style.display = "none";
window.history.replaceState(
{ html: content, pageTitle: title },
"",
curr[0].split("?")[0] + "?recursive#" + (curr[1] || "")
);
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() {
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 (this.filterRe.test(window.location.href)) {
const selected = window.location.href
.match(this.filterRe)[1]
.split(",");
this.setFilter(selected);
}
if (this.recursiveRe.test(window.location.href)) {
const recChk = document.getElementById("recursive");
if (recChk) recChk.checked = true;
this.recursive();
} else {
this.filter();
}
if (this.re.test(window.location.href)) {
const pid = window.location.href.match(this.re)[1];
this.openSwipe(parseInt(pid));
}
})
.catch(() => {});
}
filter() {
this.shown = [];
const curr = window.location.href.split("#")[0] + "#";
const path = decodeURIComponent(
window.location.href.split("#")[0].replace("index.html", "")
);
const selectedTags = [];
document
.querySelectorAll("#tagdropdown input.tagcheckbox:checked")
.forEach((checkbox) => {
let tag = checkbox.parentElement.id.trim().substring(1);
if (checkbox.parentElement.parentElement.children.length > 1)
tag += "|";
selectedTags.push(tag);
});
const urltags = selectedTags.join(",");
let isRecursiveChecked = false;
try {
isRecursiveChecked =
document.getElementById("recursive")?.checked || false;
} catch {}
for (const item of this.items) {
const tags = item.tags || [];
const include = selectedTags.every((selected) => {
const isParent = selected.endsWith("|");
return isParent
? tags.some((t) => t.startsWith(selected))
: tags.includes(selected);
});
if (include || selectedTags.length === 0) {
if (!isRecursiveChecked) {
if (decodeURIComponent(item.src.replace(item.name, "")) === path) {
this.shown.push(item);
}
} else {
this.shown.push(item);
}
}
}
this.updateImageList();
window.location.href = curr + urltags;
}
updateImageList() {
const imagelist = document.getElementById("imagelist");
if (!imagelist) return;
let str = "";
this.shown.forEach((item, index) => {
str += `<div class="column"><figure><img src="${item.msrc}" data-index="${index}" /><figcaption class="caption">${item.name}`;
if (item.tiff) str += ` <a href="${item.tiff}">TIFF</a>`;
if (item.raw) str += ` <a href="${item.raw}">RAW</a>`;
str += "</figcaption></figure></div>";
});
imagelist.innerHTML = str;
}
setFilter(selected) {
document
.querySelectorAll("#tagdropdown input.tagcheckbox")
.forEach((checkbox) => {
selected.forEach((tag) => {
if (
checkbox.parentElement.id
.trim()
.substring(1)
.replace(" ", "%20") === tag
) {
checkbox.checked = true;
}
});
});
}
toggleTag(tagid) {
const tag = document.getElementById(tagid);
const ol = tag?.closest(".tagentry")?.querySelector(".tagentryparent");
const svg = tag?.parentElement.querySelector(".tagtoggle svg");
if (!ol || !svg) return;
ol.classList.toggle("show");
svg.style.transform = ol.classList.contains("show")
? "rotate(180deg)"
: "rotate(0deg)";
}
setupDropdownToggle() {
const toggleLink = document.getElementById("tagtogglelink");
const dropdown = document.getElementById("tagdropdown");
if (!toggleLink) return;
toggleLink.addEventListener("click", (event) => {
event.stopPropagation();
const svg = toggleLink.querySelector("svg");
dropdown.classList.toggle("show");
if (svg)
svg.style.transform = dropdown.classList.contains("show")
? "rotate(180deg)"
: "rotate(0deg)";
this.tagDropdownShown = dropdown.classList.contains("show");
});
document.addEventListener("click", (event) => {
if (
!dropdown.contains(event.target) &&
!toggleLink.contains(event.target)
) {
dropdown.classList.remove("show");
this.tagDropdownShown = false;
const svg = toggleLink.querySelector("svg");
if (svg) svg.style.transform = "rotate(0deg)";
}
});
}
setupTagHandlers() {
const tagContainer = document.getElementById("tagdropdown");
if (!tagContainer) return;
const debouncedFilter = this.debounce(this.filter, 150);
tagContainer.addEventListener("change", debouncedFilter);
tagContainer.addEventListener("click", (event) => {
const toggle = event.target.closest(".tagtoggle");
if (toggle) {
event.stopPropagation();
const tagid = toggle.dataset.toggleid;
this.toggleTag(tagid);
}
});
}
setupClickHandlers() {
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 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("mouseover", (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);
});
imagelist.addEventListener("mouseleave", (event) => {
const img = event.target.closest("img");
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.cancel(index);
});
}
}
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";
}
}
topFunction() {
window.scrollTo({ top: 0, behavior: "smooth" });
}
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();
window.addEventListener("scroll", this.scrollFunction);
}
init() {
if (window.addEventListener) {
window.addEventListener("load", this.onLoad, false);
} else if (window.attachEvent) {
window.attachEvent("onload", this.onLoad);
}
}
}

View File

@@ -28,7 +28,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title> <title>{{ title }}</title>
{%- if webmanifest %} {%- if webmanifest %}
<link rel="manifest" href="/.static/manifest.json"> <link rel="manifest" href="/.static/manifest.webmanifest">
{%- endif %} {%- endif %}
<link rel="preload" href="{{ stylesheet }}" as="style"> <link rel="preload" href="{{ stylesheet }}" as="style">
{%- if theme %} {%- if theme %}
@@ -43,10 +43,12 @@
<link rel="preload" href="{{ root }}.static/pswp/default-skin/default-skin.css" as="style"> <link rel="preload" href="{{ root }}.static/pswp/default-skin/default-skin.css" as="style">
<link rel="modulepreload" href="{{ root }}.static/pswp/photoswipe.min.js"> <link rel="modulepreload" href="{{ root }}.static/pswp/photoswipe.min.js">
<link rel="modulepreload" href="{{ root }}.static/pswp/photoswipe-ui-default.min.js"> <link rel="modulepreload" href="{{ root }}.static/pswp/photoswipe-ui-default.min.js">
<link rel="modulepreload" href="{{ root }}.static/functionality.min.js">
<link rel="stylesheet" href="{{ root }}.static/pswp/photoswipe.css"> <link rel="stylesheet" href="{{ root }}.static/pswp/photoswipe.css">
<link rel="stylesheet" href="{{ root }}.static/pswp/default-skin/default-skin.css"> <link rel="stylesheet" href="{{ root }}.static/pswp/default-skin/default-skin.css">
<script src="{{ root }}.static/pswp/photoswipe.min.js"></script> <script src="{{ root }}.static/pswp/photoswipe.min.js"></script>
<script src="{{ root }}.static/pswp/photoswipe-ui-default.min.js"></script> <script src="{{ root }}.static/pswp/photoswipe-ui-default.min.js"></script>
<script src="{{ root }}.static/functionality.min.js"></script>
</head> </head>
<body> <body>
@@ -78,8 +80,9 @@
</g> </g>
</svg></a> </svg></a>
<ol class="tooltiptext tagdropdown" id="tagdropdown"> <ol class="tooltiptext tagdropdown" id="tagdropdown">
<span class="tagentry" id="reset-filter"><label>reset filter</label></span>
<span class="tagentry"> <span class="tagentry">
<label onclick="recursive()"> <label>
<input type="checkbox" id="recursive" />recursive filter <input type="checkbox" id="recursive" />recursive filter
</label> </label>
</span> </span>
@@ -130,14 +133,14 @@
{%- endif %} {%- endif %}
<span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a <span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span> href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span>
<button type="button" onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button> <button type="button" id="totop" title="Back to Top">Back to Top</button>
</div> </div>
{%- endif %} {%- endif %}
{%- else %} {%- else %}
<div class="footer"> <div class="footer">
<span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a <span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder {{ version }}</a> by <a
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span> href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer">{{ logo }}</a>.</span>
<button type="button" onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button> <button type="button" id="totop" title="Back to Top">Back to Top</button>
</div> </div>
{%- endif %} {%- endif %}
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true"> <div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
@@ -176,329 +179,9 @@
</div> </div>
</div> </div>
</div> </div>
<script>
const pswpElement = document.querySelectorAll('.pswp')[0];
const re = /pid=(\d+)/;
const filterre = /#(.*)/;
const recursere = /\?recursive/;
let items = [];
let shown = [];
let subfolders = [];
let controllers = {};
let tagdropdownshown = false;
function requestMetadata() {
fetch(".metadata.json").then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
items = Object.values(data.images);
subfolders = data.subfolders;
if (filterre.test(window.location.href)) {
const selected = window.location.href.match(filterre)[1].split(",");
setFilter(selected);
}
if (recursere.test(window.location.href)) {
document.getElementById("recursive").checked = true;
recursive();
}
filter();
if (re.test(window.location.href)) {
const pid = window.location.href.match(re)[1];
openSwipe(parseInt(pid));
}
})
.catch(error => console.error('Failed to fetch data:', error));
}
function setupTagHandlers() {
const tagContainer = document.getElementById("tagdropdown");
if (tagContainer == null) {
return;
}
tagContainer.addEventListener("change", debounce(filter, 150));
tagContainer.addEventListener("click", function (event) {
const toggle = event.target.closest(".tagtoggle");
if (toggle) {
event.stopPropagation();
const tagid = toggle.dataset.toggleid;
toggleTag(tagid);
}
});
}
function toggleTag(tagid) {
const tag = document.getElementById(tagid);
const ol = tag?.closest(".tagentry")?.querySelector(".tagentryparent");
const svg = tag?.parentElement.querySelector(".tagtoggle svg");
if (!ol || !svg) return;
ol.classList.toggle("show");
svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
}
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
function openSwipe(img) {
const options = {
index: img
};
const gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, shown, options);
gallery.init();
}
async function recursive(sub = undefined) {
const curr = window.location.href.split("#");
const content = document.getRootNode().innerHTML;
const title = document.title;
const isChecked = document.getElementById("recursive").checked;
const folders = document.querySelector(".folders");
if (!isChecked) {
if (folders) folders.style.display = "";
window.history.replaceState({ html: content, pageTitle: title }, "", curr[0].split("?")[0] + "#" + curr[1]);
requestMetadata();
return;
}
if (folders) folders.style.display = "none";
window.history.replaceState({ html: content, pageTitle: title }, "", curr[0].split("?")[0] + "?recursive#" + curr[1]);
const visited = new Set();
const existingItems = new Set();
const newItems = [];
try {
const response = await fetch(".metadata.json");
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
const data = await response.json();
items = [];
subfolders = data.subfolders || [];
sub = subfolders;
for (const image of Object.values(data.images || {})) {
newItems.push(image);
existingItems.add(image.src);
}
} catch (error) {
console.error("Failed to fetch base .metadata.json:", error);
return;
}
async function fetchFoldersRecursively(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);
if (!folder.metadata) return;
try {
const response = await fetch(folder.metadata);
if (!response.ok) throw new Error(`Failed to fetch ${folder.metadata}`);
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 (error) {
console.error("Failed to fetch folder metadata:", error);
}
}));
if (nextLevel.length > 0) {
await fetchFoldersRecursively(nextLevel);
}
}
await fetchFoldersRecursively(sub);
items = [...newItems];
filter();
}
const totopbutton = document.getElementById("totop");
window.onscroll = function () { scrollFunction() };
function scrollFunction() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
totopbutton.style.display = "block";
} else {
totopbutton.style.display = "none";
}
}
function topFunction() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function updateImageList() {
let str = ""
let imagelist = document.getElementById("imagelist");
shown.forEach((item, index) => {
str += '<div class="column"><figure><img src="' + item.msrc + '" onclick="openSwipe(' + index + ')" onmouseover="prefetch(' + index + ')" onmouseleave="cancel(' + index + ')" /><figcaption class="caption">' + item.name;
if (item.tiff != "" & item.tiff != undefined) {
str += ' <a href="' + item.tiff + '">TIFF</a>';
}
if (item.raw != "" & item.raw != undefined) {
str += ' <a href="' + item.raw + '">RAW</a>';
}
str += '</figcaption></figure></div>';
});
imagelist.innerHTML = str;
}
function prefetch(img) {
const controller = new AbortController()
const signal = controller.signal
controllers[img] = controller;
let urlToFetch = items[img].src;
fetch(urlToFetch, {
method: 'get',
signal: signal,
}).catch(function (err) { });
}
function cancel(img) {
controllers[img].abort();
delete controllers[img];
}
function filter() {
shown = [];
let isRecursiveChecked = false;
const curr = window.location.href.split("#")[0] + "#";
const path = decodeURIComponent(window.location.href.split("#")[0].replace("index.html", ""))
const selected_tags = [];
const tagcheckboxes = document.querySelectorAll("#tagdropdown input[class='tagcheckbox']:checked");
tagcheckboxes.forEach((checkbox) => {
let tag = checkbox.parentElement.id.trim().substring(1);
if (checkbox.parentElement.parentElement.children.length > 1) {
tag += "|"
}
selected_tags.push(tag);
});
const urltags = selected_tags.join(",");
try {
isRecursiveChecked = document.getElementById("recursive").checked;
} catch { }
for (const item of items) {
const tags = item.tags || [];
const include = selected_tags.every(selected => {
const isParent = selected.endsWith('|');
if (isParent) {
return tags.some(t => t.startsWith(selected));
} else {
return tags.includes(selected);
}
});
if (include || selected_tags.length === 0) {
if (!isRecursiveChecked) {
if (decodeURIComponent(item.src.replace(item.name, "")) == path) {
shown.push(item);
}
} else {
shown.push(item);
}
}
}
updateImageList();
window.location.href = curr + urltags;
}
function setFilter(selected) {
const tagcheckboxes = document.querySelectorAll("#tagdropdown input[class='tagcheckbox']");
selected.forEach((tag) => {
tagcheckboxes.forEach((checkbox) => {
if (checkbox.parentElement.id.trim().substring(1).replace(" ", "%20") == tag) {
checkbox.checked = true;
}
});
});
}
function setupDropdownToggle() {
const toggleLink = document.getElementById("tagtogglelink");
const dropdown = document.getElementById("tagdropdown");
if (toggleLink == null) {
return;
}
toggleLink.addEventListener("click", function (event) {
event.stopPropagation();
const svg = this.querySelector("svg");
dropdown.classList.toggle("show");
if (svg) svg.style.transform = dropdown.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
tagdropdownshown = dropdown.classList.contains("show");
});
document.addEventListener("click", function (event) {
if (!dropdown.contains(event.target) && !toggleLink.contains(event.target)) {
dropdown.classList.remove("show");
tagdropdownshown = false;
const svg = toggleLink.querySelector("svg");
if (svg) svg.style.transform = "rotate(0deg)";
}
});
}
function onLoad() {
document.querySelectorAll('.tagtoggle').forEach(toggle => {
toggle.addEventListener('mouseup', function (event) {
event.stopPropagation();
const tagid = this.getAttribute('data-tagid');
toggleTag(tagid);
});
});
requestMetadata();
setupDropdownToggle();
setupTagHandlers();
const recurseEl = document.getElementById("recursive")
if (recurseEl != null) { recurseEl.addEventListener("change", debounce(recursive, 150)); }
}
window.addEventListener ?
window.addEventListener("load", onLoad, false) :
window.attachEvent && window.attachEvent("onload", onLoad);
</script>
</body> </body>
<script>
new PhotoGallery();
</script>
</html> </html>

View File

@@ -21,13 +21,16 @@
<body> <body>
<div class="header"> <div class="header">
<ul class="navbar"> <ol class="navbar">
<div class="navleft">
<li><a href="{{ root }}">Home</a></li> <li><a href="{{ root }}">Home</a></li>
{%- if parent %} {%- if parent %}
<li><a href="{{ parent }}">Parent Directory</a></li> <li><a href="{{ parent }}">Parent Directory</a></li>
{%- endif %} {%- endif %}
</div>
<div class="navcenter">
<li class="title"><span class="header">{{ header }}</span></li> <li class="title"><span class="header">{{ header }}</span></li>
</ul> </div>
</div> </div>
{%- if licensefile %} {%- if licensefile %}
<div class="licensefile"> <div class="licensefile">