diff --git a/.version b/.version index cae9add..642c63c 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.8.2 \ No newline at end of file +2.8.3 \ No newline at end of file diff --git a/builder.py b/builder.py index 2d6b992..5ccd448 100755 --- a/builder.py +++ b/builder.py @@ -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: diff --git a/files/global.css b/files/global.css index 2cb0e17..dfe59f9 100644 --- a/files/global.css +++ b/files/global.css @@ -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; } diff --git a/modules/generate_html.py b/modules/generate_html.py index 201583d..6fe4cd1 100644 --- a/modules/generate_html.py +++ b/modules/generate_html.py @@ -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", "\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"
{escaped_text}"
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]:
@@ -624,7 +632,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 +654,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)
diff --git a/modules/svg_handling.py b/modules/svg_handling.py
index 27729aa..f18e30d 100644
--- a/modules/svg_handling.py
+++ b/modules/svg_handling.py
@@ -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)
diff --git a/requirements.txt b/requirements.txt
index 01e43a1..55c60e2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,8 @@
CairoSVG==2.7.1
defusedxml==0.7.1
+html5lib==1.1
Jinja2==3.1.5
+jsmin==3.0.1
Pillow==11.1.0
pyinstaller==6.11.1
python_json_logger==2.0.7
diff --git a/templates/functionality.js b/templates/functionality.js
new file mode 100644
index 0000000..61d4a85
--- /dev/null
+++ b/templates/functionality.js
@@ -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 += `";
+ });
+ 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);
+ }
+ }
+}
diff --git a/templates/index.html.j2 b/templates/index.html.j2
index eef003b..7a8f1f6 100644
--- a/templates/index.html.j2
+++ b/templates/index.html.j2
@@ -28,7 +28,7 @@