mirror of
https://github.com/greflm13/StaticGalleryBuilder.git
synced 2026-04-17 11:30:08 +02:00
Compare commits
26 Commits
v2.9.0
...
ad6ef5fe01
| Author | SHA1 | Date | |
|---|---|---|---|
| ad6ef5fe01 | |||
| a7a0fee815 | |||
| cedb187c28 | |||
| c316653b01 | |||
| c60645d019 | |||
| f653f41b10 | |||
| 61c7f0cd43 | |||
| 45cc7c98f1 | |||
| 4f1c0388a5 | |||
| 8c60bd1eb1 | |||
| 4e5e6f2f91 | |||
| 9814f078eb | |||
| 6f7a3fe180 | |||
| d88351d0ab | |||
| b37dbf4bf4 | |||
| ce6b5ebb39 | |||
| 104f0c18e9 | |||
| bc1da773c9 | |||
| 10005d2bc0 | |||
| 8e1f9a738f | |||
| 7d086a7a20 | |||
| 7d254f5a3e | |||
| 895ac03590 | |||
| cad6d88b22 | |||
| e06df9444d | |||
| 9d5ce13e14 |
@@ -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 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -128,4 +128,4 @@ def licensepicswitch(cclicense: str) -> list[str]:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
return switch.get(cclicense, "")
|
return switch.get(cclicense, [])
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = " ".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(" ".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 = " ".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(" ".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 += ` <a href="${item.tiff}">TIFF</a>`;
|
|
||||||
if (item.raw) str += ` <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 += ` <a href="${item.tiff}">TIFF</a>`;
|
||||||
}
|
if (item.raw) str += ` <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() {
|
||||||
|
|||||||
@@ -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
2
themes
Submodule themes updated: 3bb36480e7...e5f2b0cd98
Reference in New Issue
Block a user