4 Commits

12 changed files with 487 additions and 365 deletions

10
.hintrc
View File

@@ -3,6 +3,14 @@
"development"
],
"hints": {
"apple-touch-icons": "off"
"apple-touch-icons": "off",
"compat-api/css": [
"default",
{
"ignore": [
"-webkit-app-region"
]
}
]
}
}

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 PIL import Image, ImageOps
from jsmin import jsmin
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:
logger.info("writing theme file")
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:
@@ -169,7 +174,7 @@ def main(args) -> None:
try:
Path(lock_file).touch()
logger.info("starting builder", extra={"version": VERSION})
logger.info("starting builder", extra={"version": VERSION, "arguments": args})
logger.info("getting logo from sorogon.eu")
req = urllib.request.Request("https://files.sorogon.eu/logo.svg")

View File

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

View File

@@ -3,6 +3,7 @@ import re
import urllib.parse
import fnmatch
import json
import html
from typing import Any
from datetime import datetime
from collections import defaultdict
@@ -11,6 +12,7 @@ from tqdm.auto import tqdm
from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError
from jinja2 import Environment, FileSystemLoader
from defusedxml import ElementTree
from bs4 import BeautifulSoup
from modules.logger import logger
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:
"""
Processes a LICENSE file.
Processes a LICENSE file, preserving formatting in HTML.
Args:
folder (str): The folder containing the info file.
item (str): The licenses file name.
folder (str): The folder containing the LICENSE file.
item (str): The LICENSE file name.
"""
with open(os.path.join(folder, item), encoding="utf-8") as f:
logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)})
licens[urllib.parse.quote(folder)] = (
f.read().replace("\n", "</br>\n").replace(" ", "&emsp;").replace(" ", "&ensp;").replace("sp; ", "sp;&ensp;").replace("&ensp;&ensp;", "&emsp;")
)
path = os.path.join(folder, item)
with open(path, encoding="utf-8") as f:
logger.info("processing LICENSE", extra={"path": path})
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:
@@ -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)
def format_html(html: str) -> str:
soup = BeautifulSoup(html, "html5lib")
return soup.prettify()
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]
) -> list[str]:
@@ -591,7 +599,8 @@ def create_html_file(
alltags = set()
for img in images:
alltags.update(img["tags"])
if img["tags"]:
alltags.update(img["tags"])
alltags.update(set(subfoldertags))
@@ -624,7 +633,7 @@ def create_html_file(
logo=logo,
licensefile=folder_license,
)
f.write(content)
f.write(format_html(content))
html = env.get_template("index.html.j2")
content = html.render(
@@ -646,8 +655,8 @@ def create_html_file(
)
with open(html_file, "w", encoding="utf-8") as f:
logger.info("writing html file", extra={"path": html_file})
f.write(content)
logger.info("writing formatted html file", extra={"path": html_file})
f.write(format_html(content))
return sorted(alltags)

View File

@@ -95,7 +95,7 @@ def setup_logger(level=logging.INFO):
"""
_logger = logging.getLogger(name="defaultlogger")
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "process", "processName", "relativeCreated", "thread", "threadName"]
custom_format = " ".join(log_format(supported_keys))
formatter = jsonlogger.JsonFormatter(custom_format)
@@ -121,7 +121,7 @@ def setup_consolelogger(level=logging.INFO):
"""
_logger = logging.getLogger(name="consolelogger")
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "process", "processName", "relativeCreated", "thread", "threadName"]
custom_format = " ".join(log_format(supported_keys))
formatter = jsonlogger.JsonFormatter(custom_format)

View File

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

View File

@@ -1,9 +1,12 @@
CairoSVG==2.7.1
defusedxml==0.7.1
Jinja2==3.1.5
Pillow==11.1.0
pyinstaller==6.11.1
python_json_logger==2.0.7
rich_argparse==1.7.0
selenium==4.28.1
tqdm==4.66.4
beautifulsoup4~=4.13.4
CairoSVG~=2.7.1
defusedxml~=0.7.1
html5lib~=1.1
Jinja2~=3.1.6
jsmin~=3.0.1
Pillow~=11.3.0
pyinstaller~=6.11.1
python_json_logger~=2.0.7
rich_argparse~=1.7.1
selenium~=4.34.2
tqdm~=4.66.4

411
templates/functionality.js Normal file
View File

@@ -0,0 +1,411 @@
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.getElementById("recursive").checked = false;
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">
<title>{{ title }}</title>
{%- if webmanifest %}
<link rel="manifest" href="/.static/manifest.json">
<link rel="manifest" href="/.static/manifest.webmanifest">
{%- endif %}
<link rel="preload" href="{{ stylesheet }}" as="style">
{%- if theme %}
@@ -43,10 +43,12 @@
<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-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/default-skin/default-skin.css">
<script src="{{ root }}.static/pswp/photoswipe.min.js"></script>
<script src="{{ root }}.static/pswp/photoswipe-ui-default.min.js"></script>
<script src="{{ root }}.static/functionality.min.js"></script>
</head>
<body>
@@ -78,8 +80,9 @@
</g>
</svg></a>
<ol class="tooltiptext tagdropdown" id="tagdropdown">
<span class="tagentry" id="reset-filter"><label>reset filter</label></span>
<span class="tagentry">
<label onclick="recursive()">
<label>
<input type="checkbox" id="recursive" />recursive filter
</label>
</span>
@@ -130,14 +133,14 @@
{%- endif %}
<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>
<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>
{%- endif %}
{%- else %}
<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
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>
{%- endif %}
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
@@ -176,329 +179,9 @@
</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>
<script>
new PhotoGallery();
</script>
</html>

View File

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