mirror of
https://github.com/greflm13/StaticGalleryBuilder.git
synced 2026-04-17 11:30:08 +02:00
Compare commits
13 Commits
895ac03590
...
8c60bd1eb1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c60bd1eb1 | |||
| 4e5e6f2f91 | |||
| 9814f078eb | |||
| 6f7a3fe180 | |||
| d88351d0ab | |||
| b37dbf4bf4 | |||
| ce6b5ebb39 | |||
| 104f0c18e9 | |||
| bc1da773c9 | |||
| 10005d2bc0 | |||
| 8e1f9a738f | |||
| 7d086a7a20 | |||
| 7d254f5a3e |
@@ -129,8 +129,6 @@
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"black-formatter.args": ["-l 260"],
|
||||
"black-formatter.interpreter": ["/usr/bin/python3"],
|
||||
"editor.formatOnSave": false,
|
||||
"emmet.includeLanguages": {
|
||||
"jinja-css": "css",
|
||||
@@ -157,7 +155,9 @@
|
||||
"json.schemaDownload.enable": true,
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["manifest.json.j2"],
|
||||
"fileMatch": [
|
||||
"manifest.json.j2"
|
||||
],
|
||||
"url": "https://json.schemastore.org/web-manifest-combined.json"
|
||||
}
|
||||
],
|
||||
@@ -169,9 +169,10 @@
|
||||
"packageManager": "ms-python.python:pip"
|
||||
}
|
||||
],
|
||||
"python.analysis.inlayHints.callArgumentNames": "off",
|
||||
"python.analysis.inlayHints.functionReturnTypes": false,
|
||||
"python.analysis.inlayHints.variableTypes": false,
|
||||
"python.analysis.inlayHints.callArgumentNames": "all",
|
||||
"python.analysis.inlayHints.functionReturnTypes": true,
|
||||
"python.analysis.inlayHints.variableTypes": true,
|
||||
"python.analysis.typeCheckingMode": "standard",
|
||||
"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"
|
||||
},
|
||||
@@ -228,7 +229,9 @@
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": ["Clean"]
|
||||
"dependsOn": [
|
||||
"Clean"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "rm -rf build dist",
|
||||
@@ -278,4 +281,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,8 @@ figure {
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
height: 1.222em;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.navbar .navleft {
|
||||
@@ -302,12 +304,7 @@ input {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
.darkmodeswitch {
|
||||
font-size: smaller;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#dark-mode-switch {
|
||||
.darkmodeswitch a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -316,7 +313,7 @@ input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#dark-mode-switch .checkbox {
|
||||
.darkmodeswitch a input[type="checkbox"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -325,36 +322,44 @@ input {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#dark-mode-switch .knobs {
|
||||
.darkmodeswitch a .knobs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 1em;
|
||||
column-gap: 1.25em;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#dark-mode-switch .light,
|
||||
#dark-mode-switch .dark {
|
||||
text-align: center;
|
||||
.darkmodeswitch a .light,
|
||||
.darkmodeswitch a .dark {
|
||||
position: relative;
|
||||
top: -0.111em;
|
||||
}
|
||||
|
||||
#dark-mode-switch .slider {
|
||||
.darkmodeswitch a .slider {
|
||||
position: absolute;
|
||||
width: calc(2em - 2px);
|
||||
height: calc(2em - 2px);
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 3px;
|
||||
top: 50%;
|
||||
top: calc(50% - 2px);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: left 0.25s ease, transform 0.25s ease;
|
||||
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);
|
||||
}
|
||||
|
||||
.imgprefetch {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.column {
|
||||
-ms-flex: 25%;
|
||||
|
||||
@@ -125,7 +125,7 @@ def parse_arguments(version: str) -> Args:
|
||||
"""
|
||||
# fmt: off
|
||||
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:
|
||||
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")
|
||||
@@ -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("--folderthumbnails", help="generate subfolder thumbnails (first image in folder will be shown)", action="store_true", default=False, dest="folder_thumbs")
|
||||
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-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")
|
||||
|
||||
@@ -128,4 +128,4 @@ def licensepicswitch(cclicense: str) -> list[str]:
|
||||
],
|
||||
}
|
||||
|
||||
return switch.get(cclicense, "")
|
||||
return switch.get(cclicense, [])
|
||||
|
||||
@@ -138,9 +138,14 @@ def update_metadata(metadata: Metadata, folder: str) -> None:
|
||||
"""
|
||||
metadata_path = os.path.join(folder, ".metadata.json")
|
||||
if metadata:
|
||||
with open(metadata_path, "w", encoding="utf-8") as metadatafile:
|
||||
logger.info("writing metadata file", extra={"file": metadata_path})
|
||||
metadatafile.write(json.dumps(metadata.to_dict(), indent=4))
|
||||
if os.path.exists(metadata_path):
|
||||
logger.info("updating metadata file", extra={"file": metadata_path})
|
||||
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:
|
||||
if os.path.exists(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="")
|
||||
|
||||
|
||||
def nested_dict():
|
||||
def nested_dict() -> defaultdict[Any, Any]:
|
||||
return defaultdict(nested_dict)
|
||||
|
||||
|
||||
def insert_path(d, path):
|
||||
def insert_path(d, path) -> None:
|
||||
for part in path[:-1]:
|
||||
d = d[part]
|
||||
last = path[-1]
|
||||
|
||||
@@ -18,7 +18,7 @@ import gzip
|
||||
import shutil
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pythonjsonlogger import json as jsonlogger
|
||||
from pythonjsonlogger import jsonlogger
|
||||
|
||||
# Constants for file paths and exclusions
|
||||
SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from subprocess import Popen, PIPE
|
||||
from PIL import Image
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
@@ -26,6 +27,7 @@ ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512
|
||||
env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates")))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Icon:
|
||||
src: str
|
||||
type: str
|
||||
@@ -68,11 +70,12 @@ def save_png_icon(content: str, iconspath: str) -> None:
|
||||
iconspath : str
|
||||
Path to the directory where the PNG icon will be saved.
|
||||
"""
|
||||
tmpimg = BytesIO()
|
||||
cairosvg.svg2png(bytestring=content, write_to=tmpimg)
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
logger.info("saving png icon", extra={"iconspath": iconspath})
|
||||
iconfile.save(os.path.join(iconspath, "icon.png"))
|
||||
if SVGSUPPORT:
|
||||
tmpimg = BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
|
||||
cairosvg.svg2png(bytestring=content, write_to=tmpimg) # pyright: ignore[reportPossiblyUnboundVariable]
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
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:
|
||||
@@ -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]
|
||||
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
|
||||
icon_list = [
|
||||
{"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="maskable"),
|
||||
Icon(src=f"{_args.web_root_url}.static/icons/{svg}", type="image/svg+xml", sizes="512x512", purpose="any"),
|
||||
]
|
||||
for size in ICON_SIZES:
|
||||
tmpimg = BytesIO()
|
||||
tmpimg = BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
|
||||
sizes = size.split("x")
|
||||
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
|
||||
logger.info("converting svg to png", extra={"svg": svg, "size": size})
|
||||
cairosvg.svg2png(
|
||||
url=os.path.join(iconspath, svg),
|
||||
write_to=tmpimg,
|
||||
output_width=int(sizes[0]),
|
||||
output_height=int(sizes[1]),
|
||||
scale=1,
|
||||
)
|
||||
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]
|
||||
with Image.open(tmpimg) as iconfile:
|
||||
logger.info("saving png file", extra={"iconpath": iconpath})
|
||||
iconfile.save(iconpath, format="PNG")
|
||||
icon_list.append(
|
||||
{
|
||||
"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",
|
||||
}
|
||||
)
|
||||
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"))
|
||||
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:
|
||||
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
|
||||
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({"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="maskable"))
|
||||
icon_list.append(Icon(src=f"{web_root_url}.static/icons/{icon}", sizes=iconsize, type="image/png", purpose="any"))
|
||||
return icon_list
|
||||
|
||||
|
||||
@@ -254,7 +237,9 @@ def webmanifest(_args: Args) -> None:
|
||||
|
||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||
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:
|
||||
print("No icons found in the static/icons folder!")
|
||||
|
||||
@@ -4,13 +4,11 @@ class PhotoGallery {
|
||||
this.items = [];
|
||||
this.shown = [];
|
||||
this.subfolders = [];
|
||||
this.controllers = {};
|
||||
this.tagDropdownShown = false;
|
||||
|
||||
this.debounce = this.debounce.bind(this);
|
||||
this.openSwipe = this.openSwipe.bind(this);
|
||||
this.prefetch = this.prefetch.bind(this);
|
||||
this.cancel = this.cancel.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.recursive = this.recursive.bind(this);
|
||||
this.requestMetadata = this.requestMetadata.bind(this);
|
||||
@@ -47,23 +45,13 @@ class PhotoGallery {
|
||||
}
|
||||
|
||||
prefetch(imgIndex) {
|
||||
if (this.controllers[imgIndex]) {
|
||||
this.cancel(imgIndex);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
this.controllers[imgIndex] = controller;
|
||||
const urlToFetch = this.shown[imgIndex]?.src;
|
||||
if (urlToFetch) {
|
||||
fetch(urlToFetch, { method: "GET", signal }).catch(() => {});
|
||||
}
|
||||
}
|
||||
const prefetchDiv = document.getElementById("img-prefetch");
|
||||
if (!prefetchDiv) return;
|
||||
|
||||
cancel(imgIndex) {
|
||||
if (this.controllers[imgIndex]) {
|
||||
this.controllers[imgIndex].abort();
|
||||
delete this.controllers[imgIndex];
|
||||
}
|
||||
const img = document.createElement("img");
|
||||
img.src = this.shown[imgIndex]?.src || "";
|
||||
prefetchDiv.removeChild(prefetchDiv.firstChild);
|
||||
prefetchDiv.appendChild(img);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -393,19 +381,12 @@ class PhotoGallery {
|
||||
if (!isNaN(index)) this.openSwipe(index);
|
||||
});
|
||||
|
||||
imagelist.addEventListener("mouseover", (event) => {
|
||||
imagelist.addEventListener("mousemove", (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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +407,7 @@ class PhotoGallery {
|
||||
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;
|
||||
}
|
||||
@@ -433,6 +415,7 @@ class PhotoGallery {
|
||||
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;
|
||||
}
|
||||
@@ -462,9 +445,22 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +101,8 @@
|
||||
{%- endif %}
|
||||
{%- if darktheme %}
|
||||
<li class="darkmodeswitch">
|
||||
<a class="button" id="dark-mode-switch">
|
||||
<input type="checkbox" class="checkbox" id="dark-mode-switch-check" />
|
||||
<a id="dark-mode-switch">
|
||||
<input type="checkbox" id="dark-mode-switch-check" />
|
||||
<div class="knobs">
|
||||
<span class="light">☀︎</span>
|
||||
<span class="slider"></span>
|
||||
@@ -197,6 +197,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="imgprefetch" id="img-prefetch" aria-hidden="true"></div>
|
||||
</body>
|
||||
<script>
|
||||
new PhotoGallery();
|
||||
|
||||
2
themes
2
themes
Submodule themes updated: 3bb36480e7...e5f2b0cd98
Reference in New Issue
Block a user