26 Commits

Author SHA1 Message Date
ad6ef5fe01 like this? 2026-03-23 14:59:54 +01:00
a7a0fee815 add pageshow handler 2026-03-23 14:57:40 +01:00
cedb187c28 changed order so darkmode gets detected first 2026-03-23 14:49:23 +01:00
c316653b01 removed log for raw and tiff files found 2026-02-09 10:33:28 +01:00
c60645d019 fixed infotext still appearing when hovering over hidden element 2026-02-09 10:18:20 +01:00
f653f41b10 only prefetch after 500ms hover 2026-02-09 10:06:42 +01:00
61c7f0cd43 sort and bind 2026-02-09 09:35:02 +01:00
45cc7c98f1 showLoader some more 2026-02-09 09:11:58 +01:00
4f1c0388a5 (hopefully) consistent recurse tag filter sorting 2026-02-09 08:38:36 +01:00
8c60bd1eb1 removed console.log 2026-02-06 12:25:17 +01:00
4e5e6f2f91 this.cancel is undefined 2026-02-06 12:23:26 +01:00
9814f078eb ah geh, gehts jetzt? 2026-02-06 12:22:43 +01:00
6f7a3fe180 ugh 2026-02-06 12:19:15 +01:00
d88351d0ab fixed body 2026-02-06 12:03:46 +01:00
b37dbf4bf4 aria-hidden 2026-02-06 12:02:10 +01:00
ce6b5ebb39 different prefetch 2026-02-06 10:51:36 +01:00
104f0c18e9 yeah i guess 2026-02-06 09:26:14 +01:00
bc1da773c9 updated themes 2026-02-06 08:51:25 +01:00
10005d2bc0 removed class 2026-02-06 08:41:14 +01:00
8e1f9a738f fixed? 2026-02-06 08:40:59 +01:00
7d086a7a20 types 2026-02-06 08:00:53 +01:00
7d254f5a3e localStorage 2026-02-06 07:33:35 +01:00
895ac03590 fallback 2026-02-04 08:55:30 +01:00
cad6d88b22 import update 2026-02-04 08:54:19 +01:00
e06df9444d update theme color 2026-02-04 08:53:49 +01:00
9d5ce13e14 version bump 2026-02-04 08:47:29 +01:00
11 changed files with 399 additions and 359 deletions

View File

@@ -1 +1 @@
2.9.0 2.9.1

View File

@@ -129,8 +129,6 @@
"[python]": { "[python]": {
"editor.defaultFormatter": "charliermarsh.ruff" "editor.defaultFormatter": "charliermarsh.ruff"
}, },
"black-formatter.args": ["-l 260"],
"black-formatter.interpreter": ["/usr/bin/python3"],
"editor.formatOnSave": false, "editor.formatOnSave": false,
"emmet.includeLanguages": { "emmet.includeLanguages": {
"jinja-css": "css", "jinja-css": "css",
@@ -157,7 +155,9 @@
"json.schemaDownload.enable": true, "json.schemaDownload.enable": true,
"json.schemas": [ "json.schemas": [
{ {
"fileMatch": ["manifest.json.j2"], "fileMatch": [
"manifest.json.j2"
],
"url": "https://json.schemastore.org/web-manifest-combined.json" "url": "https://json.schemastore.org/web-manifest-combined.json"
} }
], ],
@@ -169,9 +169,10 @@
"packageManager": "ms-python.python:pip" "packageManager": "ms-python.python:pip"
} }
], ],
"python.analysis.inlayHints.callArgumentNames": "off", "python.analysis.inlayHints.callArgumentNames": "all",
"python.analysis.inlayHints.functionReturnTypes": false, "python.analysis.inlayHints.functionReturnTypes": true,
"python.analysis.inlayHints.variableTypes": false, "python.analysis.inlayHints.variableTypes": true,
"python.analysis.typeCheckingMode": "standard",
"yaml.schemas": { "yaml.schemas": {
"https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json": "file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/hl_config.yaml" "https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json": "file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/hl_config.yaml"
}, },
@@ -228,7 +229,9 @@
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
}, },
"dependsOn": ["Clean"] "dependsOn": [
"Clean"
]
}, },
{ {
"command": "rm -rf build dist", "command": "rm -rf build dist",
@@ -278,4 +281,4 @@
} }
] ]
} }
} }

View File

@@ -132,6 +132,8 @@ figure {
text-align: center; text-align: center;
padding: 14px 16px; padding: 14px 16px;
text-decoration: none; text-decoration: none;
height: 1.222em;
box-sizing: content-box;
} }
.navbar .navleft { .navbar .navleft {
@@ -200,6 +202,7 @@ input {
.tooltip .infotext { .tooltip .infotext {
padding: 12px; padding: 12px;
width: max-content; width: max-content;
pointer-events: none;
} }
.tooltiptext.tagdropdown { .tooltiptext.tagdropdown {
@@ -222,11 +225,13 @@ input {
.tooltip:hover .infotext { .tooltip:hover .infotext {
display: block; display: block;
opacity: 1; opacity: 1;
pointer-events: auto;
} }
.tooltip:active .infotext { .tooltip:active .infotext {
display: block; display: block;
opacity: 1; opacity: 1;
pointer-events: auto;
} }
.tagentryparent { .tagentryparent {
@@ -302,12 +307,7 @@ input {
border-style: none; border-style: none;
} }
.darkmodeswitch { .darkmodeswitch a {
font-size: smaller;
height: 100%;
}
#dark-mode-switch {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -316,7 +316,7 @@ input {
position: relative; position: relative;
} }
#dark-mode-switch .checkbox { .darkmodeswitch a input[type="checkbox"] {
position: absolute; position: absolute;
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -325,36 +325,44 @@ input {
z-index: 2; z-index: 2;
} }
#dark-mode-switch .knobs { .darkmodeswitch a .knobs {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
column-gap: 1em; column-gap: 1.25em;
position: relative; position: relative;
width: 100%; width: 100%;
} }
#dark-mode-switch .light, .darkmodeswitch a .light,
#dark-mode-switch .dark { .darkmodeswitch a .dark {
text-align: center; position: relative;
top: -0.111em;
} }
#dark-mode-switch .slider { .darkmodeswitch a .slider {
position: absolute; position: absolute;
width: calc(2em - 2px); width: calc(2em - 2px);
height: calc(2em - 2px); height: calc(2em - 2px);
border: 1px solid currentColor; border: 1px solid currentColor;
border-radius: 3px; border-radius: 3px;
top: 50%; top: calc(50% - 2px);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
transition: left 0.25s ease, transform 0.25s ease; transition: left 0.25s ease, transform 0.25s ease;
left: calc(50% - 1em + 1px); left: calc(50% - 1em + 1px);
} }
#dark-mode-switch .checkbox:checked+.knobs .slider { .darkmodeswitch a input[type="checkbox"]:checked+.knobs .slider {
left: calc(50% + 1em - 1px); left: calc(50% + 1em - 1px);
} }
.imgprefetch {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
.column { .column {
-ms-flex: 25%; -ms-flex: 25%;

View File

@@ -12,7 +12,7 @@ except ModuleNotFoundError:
RICH = False RICH = False
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
DEFAULT_THEME_PATH = os.path.join(SCRIPTDIR, "templates", "default.css") DEFAULT_THEME_PATH = os.path.join(SCRIPTDIR, "templates", "default.css")
DEFAULT_AUTHOR = "Author" DEFAULT_AUTHOR = "Author"
@@ -125,7 +125,7 @@ def parse_arguments(version: str) -> Args:
""" """
# fmt: off # fmt: off
if RICH: if RICH:
parser = configargparse.ArgumentParser(default_config_files=[CONFIGPATH], add_config_file_help=False, description="generate HTML files for a static image hosting website", formatter_class=RichHelpFormatter) parser = configargparse.ArgumentParser(default_config_files=[CONFIGPATH], add_config_file_help=False, description="generate HTML files for a static image hosting website", formatter_class=RichHelpFormatter) # pyright: ignore[reportPossiblyUnboundVariable]
else: else:
parser = configargparse.ArgumentParser(default_config_files=[CONFIGPATH], add_config_file_help=False, description="generate HTML files for a static image hosting website") parser = configargparse.ArgumentParser(default_config_files=[CONFIGPATH], add_config_file_help=False, description="generate HTML files for a static image hosting website")
parser.add_argument("-a", "--author-name", help="name of the author of the images", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR") parser.add_argument("-a", "--author-name", help="name of the author of the images", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
@@ -140,7 +140,7 @@ def parse_arguments(version: str) -> Args:
parser.add_argument("--exclude-folder", help="folders to exclude from processing, globs supported (can be specified multiple times)", action="append", dest="exclude_folders", metavar="FOLDER") parser.add_argument("--exclude-folder", help="folders to exclude from processing, globs supported (can be specified multiple times)", action="append", dest="exclude_folders", metavar="FOLDER")
parser.add_argument("--folderthumbnails", help="generate subfolder thumbnails (first image in folder will be shown)", action="store_true", default=False, dest="folder_thumbs") parser.add_argument("--folderthumbnails", help="generate subfolder thumbnails (first image in folder will be shown)", action="store_true", default=False, dest="folder_thumbs")
if RICH: if RICH:
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg") parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg") # pyright: ignore[reportPossiblyUnboundVariable]
parser.add_argument("--ignore-other-files", help="ignore files that do not match the specified extensions", action="store_true", default=False, dest="ignore_other_files") parser.add_argument("--ignore-other-files", help="ignore files that do not match the specified extensions", action="store_true", default=False, dest="ignore_other_files")
parser.add_argument("--ignore-extension", help="file extensions to ignore (can be specified multiple times)", action="append", default=[], dest="ignore_extensions", metavar="EXTENSION") parser.add_argument("--ignore-extension", help="file extensions to ignore (can be specified multiple times)", action="append", default=[], dest="ignore_extensions", metavar="EXTENSION")
parser.add_argument("--regenerate-thumbnails", help="regenerate thumbnails even if they already exist", action="store_true", default=False, dest="regenerate_thumbnails") parser.add_argument("--regenerate-thumbnails", help="regenerate thumbnails even if they already exist", action="store_true", default=False, dest="regenerate_thumbnails")

View File

@@ -128,4 +128,4 @@ def licensepicswitch(cclicense: str) -> list[str]:
], ],
} }
return switch.get(cclicense, "") return switch.get(cclicense, [])

View File

@@ -20,7 +20,7 @@ from modules.argumentparser import Args
from modules.datatypes.metadata import Metadata, ImageMetadata, SubfolderMetadata from modules.datatypes.metadata import Metadata, ImageMetadata, SubfolderMetadata
# Constants for file paths and exclusions # Constants for file paths and exclusions
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
FAVICON_PATH = ".static/favicon.ico" FAVICON_PATH = ".static/favicon.ico"
GLOBAL_CSS_PATH = ".static/global.css" GLOBAL_CSS_PATH = ".static/global.css"
EXCLUDES = ["index.html", "manifest.json", "robots.txt"] EXCLUDES = ["index.html", "manifest.json", "robots.txt"]
@@ -138,9 +138,14 @@ def update_metadata(metadata: Metadata, folder: str) -> None:
""" """
metadata_path = os.path.join(folder, ".metadata.json") metadata_path = os.path.join(folder, ".metadata.json")
if metadata: if metadata:
with open(metadata_path, "w", encoding="utf-8") as metadatafile: if os.path.exists(metadata_path):
logger.info("writing metadata file", extra={"file": metadata_path}) logger.info("updating metadata file", extra={"file": metadata_path})
metadatafile.write(json.dumps(metadata.to_dict(), indent=4)) with open(metadata_path, "w", encoding="utf-8") as metadatafile:
metadatafile.write(json.dumps(metadata.to_dict(), indent=4))
else:
logger.info("creating metadata file", extra={"file": metadata_path})
with open(metadata_path, "x", encoding="utf-8") as metadatafile:
metadatafile.write(json.dumps(metadata.to_dict(), indent=4))
else: else:
if os.path.exists(metadata_path): if os.path.exists(metadata_path):
logger.info("deleting empty metadata file", extra={"file": metadata_path}) logger.info("deleting empty metadata file", extra={"file": metadata_path})
@@ -257,11 +262,11 @@ def get_image_info(item: str, folder: str) -> ImageMetadata:
return ImageMetadata(w=width, h=height, tags=tags, exifdata=exifdata, xmp=xmp, src="", msrc="", name="", title="") return ImageMetadata(w=width, h=height, tags=tags, exifdata=exifdata, xmp=xmp, src="", msrc="", name="", title="")
def nested_dict(): def nested_dict() -> defaultdict[Any, Any]:
return defaultdict(nested_dict) return defaultdict(nested_dict)
def insert_path(d, path): def insert_path(d, path) -> None:
for part in path[:-1]: for part in path[:-1]:
d = d[part] d = d[part]
last = path[-1] last = path[-1]
@@ -378,10 +383,8 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: M
if os.path.exists(file): if os.path.exists(file):
url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}" url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}"
if _raw in (".tif", ".tiff"): if _raw in (".tif", ".tiff"):
logger.info("tiff file found", extra={"file": file})
image.tiff = url image.tiff = url
else: else:
logger.info("raw file found", extra={"file": file, "extension": _raw})
image.raw = url image.raw = url
metadata.images[item] = image metadata.images[item] = image

View File

@@ -21,7 +21,7 @@ from datetime import datetime
from pythonjsonlogger import jsonlogger from pythonjsonlogger import jsonlogger
# Constants for file paths and exclusions # Constants for file paths and exclusions
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
LOG_DIR = os.path.join(SCRIPTDIR, "logs") LOG_DIR = os.path.join(SCRIPTDIR, "logs")
LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl") LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl")

View File

@@ -1,5 +1,6 @@
import os import os
import shutil import shutil
from dataclasses import dataclass
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from PIL import Image from PIL import Image
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@@ -15,10 +16,10 @@ except ImportError:
from modules.logger import logger from modules.logger import logger
from modules.argumentparser import Args from modules.argumentparser import Args
from modules.css_color import extract_theme_color, extract_colorscheme from modules.css_color import extract_colorscheme
# Define constants for static files directory and icon sizes # Define constants for static files directory and icon sizes
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
STATIC_FILES_DIR = os.path.join(SCRIPTDIR, "files") STATIC_FILES_DIR = os.path.join(SCRIPTDIR, "files")
ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"] ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"]
@@ -26,6 +27,7 @@ ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512
env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates"))) env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates")))
@dataclass
class Icon: class Icon:
src: str src: str
type: str type: str
@@ -68,11 +70,12 @@ def save_png_icon(content: str, iconspath: str) -> None:
iconspath : str iconspath : str
Path to the directory where the PNG icon will be saved. Path to the directory where the PNG icon will be saved.
""" """
tmpimg = BytesIO() if SVGSUPPORT:
cairosvg.svg2png(bytestring=content, write_to=tmpimg) tmpimg = BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
with Image.open(tmpimg) as iconfile: cairosvg.svg2png(bytestring=content, write_to=tmpimg) # pyright: ignore[reportPossiblyUnboundVariable]
logger.info("saving png icon", extra={"iconspath": iconspath}) with Image.open(tmpimg) as iconfile:
iconfile.save(os.path.join(iconspath, "icon.png")) logger.info("saving png icon", extra={"iconspath": iconspath})
iconfile.save(os.path.join(iconspath, "icon.png"))
def generate_favicon(iconspath: str, root_directory: str) -> None: def generate_favicon(iconspath: str, root_directory: str) -> None:
@@ -148,7 +151,7 @@ def render_manifest_json(_args: Args, icon_list: list[Icon], colors: dict[str, s
short_name=_args.site_title, short_name=_args.site_title,
icons=icon_list, icons=icon_list,
background_color=colors["bcolor1"], background_color=colors["bcolor1"],
theme_color=colors["theme_color"], theme_color=colors["color1"],
) )
with open(os.path.join(_args.root_directory, ".static", "manifest.webmanifest"), "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.webmanifest", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.webmanifest")}) logger.info("rendering manifest.webmanifest", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.webmanifest")})
@@ -176,40 +179,20 @@ def create_icons_from_svg(files: list[str], iconspath: str, _args: Args) -> list
svg = [file for file in files if file.endswith(".svg")][0] svg = [file for file in files if file.endswith(".svg")][0]
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg}) logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
icon_list = [ icon_list = [
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"}, Icon(src=f"{_args.web_root_url}.static/icons/{svg}", type="image/svg+xml", sizes="512x512", purpose="maskable"),
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"}, Icon(src=f"{_args.web_root_url}.static/icons/{svg}", type="image/svg+xml", sizes="512x512", purpose="any"),
] ]
for size in ICON_SIZES: for size in ICON_SIZES:
tmpimg = BytesIO() tmpimg = BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
sizes = size.split("x") sizes = size.split("x")
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png") iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
logger.info("converting svg to png", extra={"svg": svg, "size": size}) logger.info("converting svg to png", extra={"svg": svg, "size": size})
cairosvg.svg2png( cairosvg.svg2png(url=os.path.join(iconspath, svg), write_to=tmpimg, output_width=int(sizes[0]), output_height=int(sizes[1]), scale=1) # pyright: ignore[reportPossiblyUnboundVariable]
url=os.path.join(iconspath, svg),
write_to=tmpimg,
output_width=int(sizes[0]),
output_height=int(sizes[1]),
scale=1,
)
with Image.open(tmpimg) as iconfile: with Image.open(tmpimg) as iconfile:
logger.info("saving png file", extra={"iconpath": iconpath}) logger.info("saving png file", extra={"iconpath": iconpath})
iconfile.save(iconpath, format="PNG") iconfile.save(iconpath, format="PNG")
icon_list.append( icon_list.append(Icon(src=f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png", sizes=size, type="image/png", purpose="maskable"))
{ icon_list.append(Icon(src=f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png", sizes=size, type="image/png", purpose="any"))
"src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png",
"sizes": size,
"type": "image/png",
"purpose": "maskable",
}
)
icon_list.append(
{
"src": f"{_args.web_root_url}.static/icons/{os.path.splitext(svg)[0]}-{size}.png",
"sizes": size,
"type": "image/png",
"purpose": "any",
}
)
return icon_list return icon_list
@@ -236,8 +219,8 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> list[Icon]:
with Image.open(os.path.join(iconspath, icon)) as iconfile: with Image.open(os.path.join(iconspath, icon)) as iconfile:
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}" iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
logger.info("using icon", extra={"iconspath": iconspath, "icon": icon, "size": iconsize}) logger.info("using icon", extra={"iconspath": iconspath, "icon": icon, "size": iconsize})
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"}) icon_list.append(Icon(src=f"{web_root_url}.static/icons/{icon}", sizes=iconsize, type="image/png", purpose="maskable"))
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"}) icon_list.append(Icon(src=f"{web_root_url}.static/icons/{icon}", sizes=iconsize, type="image/png", purpose="any"))
return icon_list return icon_list
@@ -254,7 +237,9 @@ def webmanifest(_args: Args) -> None:
iconspath = os.path.join(_args.root_directory, ".static", "icons") iconspath = os.path.join(_args.root_directory, ".static", "icons")
files = os.listdir(iconspath) files = os.listdir(iconspath)
icon_list = create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url) icon_list = (
create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url)
)
if not icon_list: if not icon_list:
print("No icons found in the static/icons folder!") print("No icons found in the static/icons folder!")
@@ -262,5 +247,4 @@ def webmanifest(_args: Args) -> None:
return return
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css")) colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))
colorscheme["theme_color"] = extract_theme_color(os.path.join(_args.root_directory, ".static", "theme.css"))
render_manifest_json(_args, icon_list, colorscheme) render_manifest_json(_args, icon_list, colorscheme)

View File

@@ -4,34 +4,73 @@ class PhotoGallery {
this.items = []; this.items = [];
this.shown = []; this.shown = [];
this.subfolders = []; this.subfolders = [];
this.controllers = {};
this.tagDropdownShown = false; 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.darkMode = this.darkMode.bind(this); this.darkMode = this.darkMode.bind(this);
this.lightMode = this.lightMode.bind(this);
this.darkModeToggle = this.darkModeToggle.bind(this); this.darkModeToggle = this.darkModeToggle.bind(this);
this.debounce = this.debounce.bind(this);
this.detectDarkMode = this.detectDarkMode.bind(this); this.detectDarkMode = this.detectDarkMode.bind(this);
this.detectDarkMode();
this.filter = this.filter.bind(this);
this.finalize = this.finalize.bind(this);
this.insertPath = this.insertPath.bind(this);
this.lightMode = this.lightMode.bind(this);
this.onLoad = this.onLoad.bind(this);
this.openSwipe = this.openSwipe.bind(this);
this.parseHierarchicalTags = this.parseHierarchicalTags.bind(this);
this.prefetch = this.prefetch.bind(this);
this.prefetchCancel = this.prefetchCancel.bind(this);
this.recursive = this.recursive.bind(this);
this.renderTree = this.renderTree.bind(this);
this.requestMetadata = this.requestMetadata.bind(this);
this.reset = this.reset.bind(this);
this.resetHoverTimer = this.resetHoverTimer.bind(this);
this.scrollFunction = this.scrollFunction.bind(this);
this.setFilter = this.setFilter.bind(this);
this.setupClickHandlers = this.setupClickHandlers.bind(this);
this.setupDropdownToggle = this.setupDropdownToggle.bind(this);
this.setupTagHandlers = this.setupTagHandlers.bind(this);
this.showLoader = this.showLoader.bind(this);
this.toggleTag = this.toggleTag.bind(this);
this.topFunction = this.topFunction.bind(this);
this.updateImageList = this.updateImageList.bind(this);
this.init(); this.init();
} }
darkMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
localStorage.setItem("theme", "dark");
if (themeLink) themeLink.disabled = true;
if (darkThemeLink) darkThemeLink.disabled = false;
}
darkModeToggle(mode) {
const switchState = document.getElementById("dark-mode-switch-check");
if (mode == "dark") {
this.darkMode();
if (switchState) {
switchState.checked = true;
}
} else if (mode == "light") {
this.lightMode();
if (switchState) {
switchState.checked = false;
}
} else {
if (switchState.checked) {
switchState.checked = false;
this.lightMode();
} else {
switchState.checked = true;
this.darkMode();
}
}
}
debounce(fn, delay) { debounce(fn, delay) {
let timeoutId; let timeoutId;
return (...args) => { return (...args) => {
@@ -40,55 +79,169 @@ class PhotoGallery {
}; };
} }
detectDarkMode() {
if (document.getElementById("darktheme")) {
const switchState = document.getElementById("dark-mode-switch-check");
const localStorageTheme = localStorage.getItem("theme");
if (localStorageTheme === "dark") {
switchState.checked = true;
this.darkModeToggle("dark");
return;
} else if (localStorageTheme === "light") {
switchState.checked = true;
this.darkModeToggle("light");
return;
}
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
switchState.checked = true;
this.darkModeToggle("dark");
} else {
switchState.checked = false;
this.darkModeToggle("light");
}
}
}
filter() {
this.showLoader();
const searchParams = new URLSearchParams(window.location.search);
this.shown = [];
let path = decodeURIComponent(window.location.origin + window.location.pathname.replace("index.html", ""));
if (path.startsWith("null")) {
path = window.location.protocol + "//" + path.substring(4);
}
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.hash = urltags;
const pid = searchParams.get("pid") - 1;
if (pid != -1) {
this.openSwipe(pid);
}
}
finalize(obj) {
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
const result = {};
Object.keys(obj)
.sort()
.forEach((key) => {
if (obj[key] === null) {
result[key] = [];
} else {
result[key] = this.finalize(obj[key]);
}
});
return result;
}
return obj || [];
}
insertPath(obj, path) {
let current = obj;
for (let i = 0; i < path.length; i++) {
const part = path[i];
if (i === path.length - 1) {
if (!current[part]) {
current[part] = null;
}
} else {
if (!current[part] || typeof current[part] !== "object") {
current[part] = {};
}
current = current[part];
}
}
}
lightMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
localStorage.setItem("theme", "light");
if (themeLink) themeLink.disabled = false;
if (darkThemeLink) darkThemeLink.disabled = true;
}
onLoad() {
document.querySelectorAll(".tagtoggle").forEach((toggle) => {
toggle.addEventListener("mouseup", (event) => {
event.stopPropagation();
const tagid = toggle.getAttribute("data-tagid");
this.toggleTag(tagid);
});
});
this.requestMetadata();
this.setupDropdownToggle();
this.setupTagHandlers();
this.setupClickHandlers();
window.addEventListener("scroll", this.scrollFunction);
}
openSwipe(imgIndex) { openSwipe(imgIndex) {
const options = { index: imgIndex }; const options = { index: imgIndex };
const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options); const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options);
gallery.init(); gallery.init();
} }
parseHierarchicalTags(tags, delimiter = "|") {
const tree = {};
for (const tag of tags) {
const parts = tag.split(delimiter);
this.insertPath(tree, parts);
}
return this.finalize(tree);
}
prefetch(imgIndex) { prefetch(imgIndex) {
if (this.controllers[imgIndex]) { const prefetchDiv = document.getElementById("img-prefetch");
this.cancel(imgIndex); if (!prefetchDiv) return;
}
const controller = new AbortController(); const img = document.createElement("img");
const signal = controller.signal; img.src = this.shown[imgIndex]?.src || "";
this.controllers[imgIndex] = controller; prefetchDiv.appendChild(img);
const urlToFetch = this.shown[imgIndex]?.src;
if (urlToFetch) {
fetch(urlToFetch, { method: "GET", signal }).catch(() => {});
}
} }
cancel(imgIndex) { prefetchCancel() {
if (this.controllers[imgIndex]) { const prefetchDiv = document.getElementById("img-prefetch");
this.controllers[imgIndex].abort(); if (!prefetchDiv) return;
delete this.controllers[imgIndex]; if (prefetchDiv.firstChild) {
prefetchDiv.removeChild(prefetchDiv.firstChild);
} }
} }
reset() {
const content = document.documentElement.innerHTML;
const title = document.title;
const folders = document.querySelector(".folders");
let path = window.location.origin + window.location.pathname;
if (path.startsWith("null")) {
path = window.location.protocol + "//" + path.substring(4);
}
if (folders) folders.style.display = "";
document.getElementById("recursive").checked = false;
document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false));
window.history.replaceState({ html: content, pageTitle: title }, "", path);
this.requestMetadata();
}
showLoader() {
const imagelist = document.getElementById("imagelist");
imagelist.innerHTML = '<span class="loader"></span>';
imagelist.classList.add("centerload");
imagelist.classList.remove("row");
}
async recursive() { async recursive() {
this.showLoader(); this.showLoader();
const loc = new URL(window.location.href); const loc = new URL(window.location.href);
@@ -105,6 +258,7 @@ class PhotoGallery {
return; return;
} }
this.showLoader();
if (folders) folders.style.display = "none"; if (folders) folders.style.display = "none";
loc.searchParams.delete("recursive"); loc.searchParams.delete("recursive");
loc.searchParams.append("recursive", true); loc.searchParams.append("recursive", true);
@@ -154,11 +308,28 @@ class PhotoGallery {
if (nextLevel.length > 0) await fetchFoldersRecursively(nextLevel); if (nextLevel.length > 0) await fetchFoldersRecursively(nextLevel);
}; };
this.showLoader();
await fetchFoldersRecursively(this.subfolders); await fetchFoldersRecursively(this.subfolders);
this.items = [...newItems]; this.items = [...newItems];
this.filter(); this.filter();
} }
renderTree = (obj, depth = 0) => {
let lines = [];
const indent = "&nbsp;&nbsp;".repeat(depth);
for (const key of Object.keys(obj)) {
lines.push(indent + key);
if (Array.isArray(obj[key])) {
for (const val of obj[key]) {
lines.push("&nbsp;&nbsp;".repeat(depth + 1) + val);
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
lines = lines.concat(this.renderTree(obj[key], depth + 1));
}
}
return lines.join("\n");
};
requestMetadata() { requestMetadata() {
this.showLoader(); this.showLoader();
const hash = window.location.hash; const hash = window.location.hash;
@@ -187,129 +358,40 @@ class PhotoGallery {
.catch(() => {}); .catch(() => {});
} }
filter() { reset() {
const searchParams = new URLSearchParams(window.location.search); const content = document.documentElement.innerHTML;
this.shown = []; const title = document.title;
let path = decodeURIComponent(window.location.origin + window.location.pathname.replace("index.html", "")); const folders = document.querySelector(".folders");
let path = window.location.origin + window.location.pathname;
if (path.startsWith("null")) { if (path.startsWith("null")) {
path = window.location.protocol + "//" + path.substring(4); path = window.location.protocol + "//" + path.substring(4);
} }
const selectedTags = [];
document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => { if (folders) folders.style.display = "";
let tag = checkbox.parentElement.id.trim().substring(1); document.getElementById("recursive").checked = false;
if (checkbox.parentElement.parentElement.children.length > 1) tag += "|"; document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false));
selectedTags.push(tag); window.history.replaceState({ html: content, pageTitle: title }, "", path);
}); this.requestMetadata();
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.hash = urltags;
const pid = searchParams.get("pid") - 1;
if (pid != -1) {
this.openSwipe(pid);
}
} }
insertPath(obj, path) { resetHoverTimer(index) {
let current = obj; if (this.hoverTimer) {
for (let i = 0; i < path.length; i++) { clearTimeout(this.hoverTimer);
const part = path[i];
if (i === path.length - 1) {
if (!current[part]) {
current[part] = null;
}
} else {
if (!current[part] || typeof current[part] !== "object") {
current[part] = {};
}
current = current[part];
}
} }
this.prefetchCancel();
this.hoverTimer = setTimeout(() => {
this.prefetch(index);
}, 500);
} }
finalize(obj) { scrollFunction() {
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) { const totopbutton = document.getElementById("totop");
const result = {}; if (!totopbutton) return;
Object.keys(obj) if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
.sort() totopbutton.style.display = "block";
.forEach((key) => { } else {
if (obj[key] === null) { totopbutton.style.display = "none";
result[key] = [];
} else {
result[key] = this.finalize(obj[key]);
}
});
return result;
} }
return obj || [];
}
parseHierarchicalTags(tags, delimiter = "|") {
const tree = {};
for (const tag of tags) {
const parts = tag.split(delimiter);
this.insertPath(tree, parts);
}
return this.finalize(tree);
}
renderTree = (obj, depth = 0) => {
let lines = [];
const indent = "&nbsp;&nbsp;".repeat(depth);
for (const key of Object.keys(obj)) {
lines.push(indent + key);
if (Array.isArray(obj[key])) {
for (const val of obj[key]) {
lines.push("&nbsp;&nbsp;".repeat(depth + 1) + val);
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
lines = lines.concat(this.renderTree(obj[key], depth + 1));
}
}
return lines.join("\n");
};
updateImageList() {
const imagelist = document.getElementById("imagelist");
if (!imagelist) return;
let str = "";
this.shown.forEach((item, index) => {
let tags = this.parseHierarchicalTags(item.tags || []);
str += `<div class="column"><figure title="${this.renderTree(tags)}"><img src="${
item.msrc
}" data-index="${index}" /><figcaption class="caption">${item.name}`;
if (item.tiff) str += `&nbsp;<a href="${item.tiff}">TIFF</a>`;
if (item.raw) str += `&nbsp;<a href="${item.raw}">RAW</a>`;
str += "</figcaption></figure></div>";
});
imagelist.classList.add("row");
imagelist.classList.remove("centerload");
imagelist.innerHTML = str;
} }
setFilter(selected) { setFilter(selected) {
@@ -322,13 +404,49 @@ class PhotoGallery {
}); });
} }
toggleTag(tagid) { setupClickHandlers() {
const tag = document.getElementById(tagid); const resetEl = document.getElementById("reset-filter")?.querySelector("label");
const ol = tag?.closest(".tagentry")?.querySelector(".tagentryparent"); if (resetEl) resetEl.addEventListener("click", this.reset);
const svg = tag?.parentElement.querySelector(".tagtoggle svg");
if (!ol || !svg) return; const recurseEl = document.getElementById("recursive");
ol.classList.toggle("show"); if (recurseEl) recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
const totop = document.getElementById("totop");
if (totop) totop.addEventListener("click", this.topFunction);
const darkModeSwitch = document.getElementById("dark-mode-switch");
if (darkModeSwitch) darkModeSwitch.addEventListener("click", this.darkModeToggle);
const imagelist = document.getElementById("imagelist");
if (imagelist) {
imagelist.addEventListener("click", (event) => {
const img = event.target.closest("img");
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.openSwipe(index);
});
imagelist.addEventListener("mouseenter", (event) => {
const img = event.target;
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.resetHoverTimer(index);
});
imagelist.addEventListener("mousemove", (event) => {
const img = event.target;
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.resetHoverTimer(index);
});
imagelist.addEventListener("mouseleave", () => {
if (this.hoverTimer) {
clearTimeout(this.hoverTimer);
}
this.prefetchCancel();
});
}
} }
setupDropdownToggle() { setupDropdownToggle() {
@@ -371,121 +489,44 @@ class PhotoGallery {
}); });
} }
setupClickHandlers() { showLoader() {
const resetEl = document.getElementById("reset-filter")?.querySelector("label");
if (resetEl) resetEl.addEventListener("click", this.reset);
const recurseEl = document.getElementById("recursive");
if (recurseEl) recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
const totop = document.getElementById("totop");
if (totop) totop.addEventListener("click", this.topFunction);
const darkModeSwitch = document.getElementById("dark-mode-switch");
if (darkModeSwitch) darkModeSwitch.addEventListener("click", this.darkModeToggle);
const imagelist = document.getElementById("imagelist"); const imagelist = document.getElementById("imagelist");
if (imagelist) { imagelist.innerHTML = '<span class="loader"></span>';
imagelist.addEventListener("click", (event) => { imagelist.classList.add("centerload");
const img = event.target.closest("img"); imagelist.classList.remove("row");
if (!img || !img.dataset.index) return;
const index = parseInt(img.dataset.index);
if (!isNaN(index)) this.openSwipe(index);
});
imagelist.addEventListener("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() { toggleTag(tagid) {
const totopbutton = document.getElementById("totop"); const tag = document.getElementById(tagid);
if (!totopbutton) return; const ol = tag?.closest(".tagentry")?.querySelector(".tagentryparent");
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) { const svg = tag?.parentElement.querySelector(".tagtoggle svg");
totopbutton.style.display = "block"; if (!ol || !svg) return;
} else { ol.classList.toggle("show");
totopbutton.style.display = "none"; svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
}
} }
topFunction() { topFunction() {
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
darkMode() { updateImageList() {
const themeLink = document.getElementById("theme"); this.showLoader();
const darkThemeLink = document.getElementById("darktheme"); const imagelist = document.getElementById("imagelist");
if (themeLink) themeLink.disabled = true; if (!imagelist) return;
if (darkThemeLink) darkThemeLink.disabled = false; let str = "";
} this.shown.sort((a, b) => a.src.replace(a.name, "").localeCompare(b.src.replace(b.name, "")));
this.shown.forEach((item, index) => {
lightMode() { let tags = this.parseHierarchicalTags(item.tags || []);
const themeLink = document.getElementById("theme"); str += `<div class="column"><figure title="${this.renderTree(tags)}"><img src="${
const darkThemeLink = document.getElementById("darktheme"); item.msrc
if (themeLink) themeLink.disabled = false; }" data-index="${index}" /><figcaption class="caption">${item.name}`;
if (darkThemeLink) darkThemeLink.disabled = true; if (item.tiff) str += `&nbsp;<a href="${item.tiff}">TIFF</a>`;
} if (item.raw) str += `&nbsp;<a href="${item.raw}">RAW</a>`;
str += "</figcaption></figure></div>";
darkModeToggle(mode) {
const switchState = document.getElementById("dark-mode-switch-check");
if (mode == "dark") {
this.darkMode();
if (switchState) {
switchState.checked = true;
}
} else if (mode == "light") {
this.lightMode();
if (switchState) {
switchState.checked = false;
}
} else {
if (switchState.checked) {
switchState.checked = false;
this.lightMode();
} else {
switchState.checked = true;
this.darkMode();
}
}
}
detectDarkMode() {
if (document.getElementById("darktheme")) {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
this.darkModeToggle("dark");
} else {
this.darkModeToggle("light");
}
}
}
onLoad() {
document.querySelectorAll(".tagtoggle").forEach((toggle) => {
toggle.addEventListener("mouseup", (event) => {
event.stopPropagation();
const tagid = toggle.getAttribute("data-tagid");
this.toggleTag(tagid);
});
}); });
imagelist.classList.add("row");
this.requestMetadata(); imagelist.classList.remove("centerload");
this.setupDropdownToggle(); imagelist.innerHTML = str;
this.setupTagHandlers();
this.setupClickHandlers();
this.detectDarkMode();
window.addEventListener("scroll", this.scrollFunction);
} }
init() { init() {

View File

@@ -101,8 +101,8 @@
{%- endif %} {%- endif %}
{%- if darktheme %} {%- if darktheme %}
<li class="darkmodeswitch"> <li class="darkmodeswitch">
<a class="button" id="dark-mode-switch"> <a id="dark-mode-switch">
<input type="checkbox" class="checkbox" id="dark-mode-switch-check" /> <input type="checkbox" id="dark-mode-switch-check" />
<div class="knobs"> <div class="knobs">
<span class="light">☀︎</span> <span class="light">☀︎</span>
<span class="slider"></span> <span class="slider"></span>
@@ -197,6 +197,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="imgprefetch" id="img-prefetch" aria-hidden="true"></div>
</body> </body>
<script> <script>
new PhotoGallery(); new PhotoGallery();

2
themes

Submodule themes updated: 3bb36480e7...e5f2b0cd98