mirror of
https://github.com/greflm13/StaticGalleryBuilder.git
synced 2026-02-05 11:09:26 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6076b5f6f2
|
|||
|
5036ff79f7
|
|||
|
50ca8ab5bf
|
|||
|
3f427dfa32
|
10
.hintrc
10
.hintrc
@@ -3,6 +3,14 @@
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"apple-touch-icons": "off"
|
||||
"apple-touch-icons": "off",
|
||||
"compat-api/css": [
|
||||
"default",
|
||||
{
|
||||
"ignore": [
|
||||
"-webkit-app-region"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(" ", " ").replace(" ", " ").replace("sp; ", "sp; ").replace("  ", " ")
|
||||
)
|
||||
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,6 +599,7 @@ def create_html_file(
|
||||
|
||||
alltags = set()
|
||||
for img in images:
|
||||
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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
411
templates/functionality.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -21,13 +21,16 @@
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<ul class="navbar">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{%- if licensefile %}
|
||||
<div class="licensefile">
|
||||
|
||||
Reference in New Issue
Block a user