44 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
6593e63ea9 updated themes version 2026-02-04 08:41:54 +01:00
027a5688cb updated dependencies 2026-02-04 07:53:28 +01:00
1aa9dd716b recursive darktheme applied 2026-02-04 07:17:29 +01:00
d1f7f62229 added automatic dark theme detection and dark mode switch 2026-02-03 15:35:45 +01:00
2f37f78039 updated subproject 2026-01-26 15:49:28 +01:00
b109627bf9 fixed so symlink gets resolved 2026-01-26 15:47:10 +01:00
4ef1b429f2 version bump 2026-01-07 14:36:21 +01:00
b8314c0e3d don't leave progressbar 2026-01-07 08:15:56 +01:00
60f3ca1f20 reduced thumbnail quality 2025-12-11 08:59:40 +01:00
bc519ac5d0 Remove duplicate 'Star History' section
Removed duplicate 'Star History' section from README.
2025-12-10 13:30:26 +01:00
b0c1b2ce48 Add Star History section to README 2025-12-10 13:29:56 +01:00
faa283efa3 tags optional 2025-11-25 07:07:47 +01:00
ce70aa7bd9 changed python version 2025-11-07 09:22:47 +01:00
59c7cfd406 breaking change: new requirement ConfigArgParse 2025-11-07 09:04:17 +01:00
cfe1585c39 added exception to log 2025-10-27 15:05:10 +01:00
57b6b56388 fixed TypeError 2025-10-27 12:06:14 +01:00
2c6f8fbcb7 move thumbdir declare 2025-10-27 11:12:03 +01:00
30eb1ed8df added metadata dataclass 2025-10-27 09:51:00 +01:00
20 changed files with 1028 additions and 559 deletions

View File

@@ -8,7 +8,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.x' python-version: '3.12'
- name: Install PyInstaller
run: pip install pyinstaller
- name: Install Dependencies - name: Install Dependencies
run: pip install -r requirements.txt run: pip install -r requirements.txt
- name: Build Package - name: Build Package

View File

@@ -1 +1 @@
2.8.6 2.9.1

View File

@@ -103,3 +103,7 @@ To generate a web manifest file:
## License ## License
This project is licensed under the AGPL-3.0 License. See the [LICENSE](LICENSE) file for details. This project is licensed under the AGPL-3.0 License. See the [LICENSE](LICENSE) file for details.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=greflm13/StaticGalleryBuilder&type=date&legend=top-left)](https://www.star-history.com/#greflm13/StaticGalleryBuilder&type=date&legend=top-left)

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",

View File

@@ -18,11 +18,7 @@ from modules.argumentparser import parse_arguments, Args
# fmt: off # fmt: off
# Constants # Constants
if __package__ is None: SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
STATIC_FILES_DIR = os.path.join(os.path.abspath(SCRIPTDIR), "files") STATIC_FILES_DIR = os.path.join(os.path.abspath(SCRIPTDIR), "files")
VERSION = open(os.path.join(SCRIPTDIR, ".version"), "r", encoding="utf-8").read() VERSION = open(os.path.join(SCRIPTDIR, ".version"), "r", encoding="utf-8").read()
RAW_EXTENSIONS = [ RAW_EXTENSIONS = [
@@ -37,8 +33,8 @@ NOT_LIST = ["*/Galleries/*", "Archives"]
args = parse_arguments(VERSION) args = parse_arguments(VERSION)
lock_file = os.path.join(args.root_directory, ".lock") LOCKFILE = os.path.join(args.root_directory, ".lock")
if os.path.exists(lock_file): if os.path.exists(LOCKFILE):
print("Another instance of this program is running.") print("Another instance of this program is running.")
sys.exit() sys.exit()
else: else:
@@ -75,7 +71,48 @@ def init_globals(_args: Args, raw: list[str]) -> tuple[Args, list[str]]:
return _args, raw return _args, raw
def copy_static_files(_args: Args) -> None: def handle_theme_icon(themepath: str, dest: str) -> None:
"""
Handle the icon specified in the theme file.
"""
logger.info("reading theme file", extra={"theme": themepath})
with open(themepath, "r", encoding="utf-8") as f:
theme = f.read()
split = theme.split(".foldericon {")
split2 = split[1].split("}", maxsplit=1)
themehead = split[0]
themetail = split2[1]
foldericon = split2[0].strip()
foldericon = re.sub(r"/\*.*\*/", "", foldericon)
for match in re.finditer(r"content: (.*);", foldericon):
foldericon = match[1]
foldericon = foldericon.replace('"', "")
logger.info("found foldericon", extra={"foldericon": foldericon})
break
if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(themepath, dest)
else:
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
logger.info("Reading foldericon svg")
svg = f.read()
if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(themepath)
for color_key, color_value in colorscheme.items():
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
logger.info("replaced colors in svg")
svg = urllib.parse.quote(svg)
with open(dest, "x", encoding="utf-8") as f:
logger.info("writing theme file")
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
def copy_static_files(_args: Args) -> bool:
""" """
Copy static files to the root directory. Copy static files to the root directory.
@@ -85,6 +122,7 @@ def copy_static_files(_args: Args) -> None:
Parsed command-line arguments. Parsed command-line arguments.
""" """
static_dir = os.path.join(_args.root_directory, ".static") static_dir = os.path.join(_args.root_directory, ".static")
darktheme = False
if os.path.exists(static_dir): if os.path.exists(static_dir):
print("Removing existing .static folder...") print("Removing existing .static folder...")
logger.info("removing existing .static folder") logger.info("removing existing .static folder")
@@ -93,42 +131,21 @@ def copy_static_files(_args: Args) -> None:
print("Copying static files...") print("Copying static files...")
logger.info("copying static files") logger.info("copying static files")
shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True) shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True)
logger.info("reading theme file", extra={"theme": _args.theme_path})
with open(_args.theme_path, "r", encoding="utf-8") as f: theme = os.path.splitext(os.path.abspath(_args.theme_path))[0]
theme = f.read() darktheme_path = f"{theme}-dark.css"
split = theme.split(".foldericon {") if os.path.exists(darktheme_path):
split2 = split[1].split("}", maxsplit=1) handle_theme_icon(darktheme_path, os.path.join(static_dir, "theme-dark.css"))
themehead = split[0] darktheme = True
themetail = split2[1] handle_theme_icon(_args.theme_path, os.path.join(static_dir, "theme.css"))
foldericon = split2[0].strip()
foldericon = re.sub(r"/\*.*\*/", "", foldericon)
for match in re.finditer(r"content: (.*);", foldericon):
foldericon = match[1]
foldericon = foldericon.replace('"', "")
logger.info("found foldericon", extra={"foldericon": foldericon})
break
if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
return
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
logger.info("Reading foldericon svg")
svg = f.read()
if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(_args.theme_path)
for color_key, color_value in colorscheme.items():
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
logger.info("replaced colors in svg")
svg = urllib.parse.quote(svg)
with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f:
logger.info("writing theme file")
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
logger.info("minifying javascript") logger.info("minifying javascript")
with open(os.path.join(SCRIPTDIR, "templates", "functionality.js"), "r", encoding="utf-8") as js_file: with open(os.path.join(SCRIPTDIR, "templates", "functionality.js"), "r", encoding="utf-8") as js_file:
with open(os.path.join(static_dir, "functionality.min.js"), "w+", encoding="utf-8") as min_file: with open(os.path.join(static_dir, "functionality.min.js"), "w+", encoding="utf-8") as min_file:
min_file.write(jsmin(js_file.read())) min_file.write(jsmin(js_file.read()))
return darktheme
def generate_thumbnail(arguments: tuple[str, str, str]) -> None: def generate_thumbnail(arguments: tuple[str, str, str]) -> None:
""" """
@@ -155,7 +172,7 @@ def generate_thumbnail(arguments: tuple[str, str, str]) -> None:
imgrgb = imgfile.convert("RGB") imgrgb = imgfile.convert("RGB")
img = ImageOps.exif_transpose(imgrgb) img = ImageOps.exif_transpose(imgrgb)
img.thumbnail((512, 512)) img.thumbnail((512, 512))
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB") img.save(path, "JPEG", quality=50, optimize=True, mode="RGB", subsampling=2)
except OSError: except OSError:
logger.error("Failed to generate thumbnail for %s", item, extra={"path": image}) logger.error("Failed to generate thumbnail for %s", item, extra={"path": image})
print(f"Failed to generate thumbnail for {image}") print(f"Failed to generate thumbnail for {image}")
@@ -168,12 +185,13 @@ def main(args) -> None:
""" """
Main function to process images and generate a static image hosting website. Main function to process images and generate a static image hosting website.
""" """
thumbnails: list[tuple[str, str, str, bool]] = [] thumbnails: list[tuple[str, str, str]] = []
args, raw = init_globals(args, RAW_EXTENSIONS) args, raw = init_globals(args, RAW_EXTENSIONS)
thumbdir = os.path.join(args.root_directory, ".thumbnails")
try: try:
Path(lock_file).touch() Path(LOCKFILE).touch()
logger.info("starting builder", extra={"version": VERSION, "arguments": args}) logger.info("starting builder", extra={"version": VERSION, "arguments": args})
logger.info("getting logo from sorogon.eu") logger.info("getting logo from sorogon.eu")
@@ -195,12 +213,12 @@ def main(args) -> None:
logger.warning("reread metadata flag is set to true, all image metadata will be reread") logger.warning("reread metadata flag is set to true, all image metadata will be reread")
if args.regenerate_thumbnails: if args.regenerate_thumbnails:
logger.warning("regenerate thumbnails flag is set to true, all thumbnails will be regenerated") logger.warning("regenerate thumbnails flag is set to true, all thumbnails will be regenerated")
if os.path.exists(os.path.join(args.root_directory, ".thumbnails")): if os.path.exists(thumbdir):
logger.info("removing old thumbnails folder") logger.info("removing old thumbnails folder")
shutil.rmtree(os.path.join(args.root_directory, ".thumbnails")) shutil.rmtree(thumbdir)
os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True) os.makedirs(thumbdir, exist_ok=True)
copy_static_files(args) args.darktheme = copy_static_files(args)
icons(args) icons(args)
if args.generate_webmanifest: if args.generate_webmanifest:
@@ -229,8 +247,11 @@ def main(args) -> None:
dynamic_ncols=True, dynamic_ncols=True,
): ):
pass pass
except Exception as e:
logger.critical("an unhandled exception occurred: %s", str(e), exc_info=True)
print(f"An unhandled exception occurred: {str(e)}")
finally: finally:
os.remove(lock_file) os.remove(LOCKFILE)
logger.info("finished builder", extra={"version": VERSION}) logger.info("finished builder", extra={"version": VERSION})

3
example.config Normal file
View File

@@ -0,0 +1,3 @@
root-directory = /path/to/webroot
site-title = Website Title
web-root-url = https://www.example.com

View File

@@ -77,10 +77,6 @@ figure {
margin: 0; margin: 0;
} }
.licensefile {
padding: 30px;
}
.caption { .caption {
padding-top: 4px; padding-top: 4px;
text-align: center; text-align: center;
@@ -93,7 +89,6 @@ figure {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0; right: 0;
font-size: xx-small;
padding: 6px; padding: 6px;
} }
@@ -102,7 +97,8 @@ figure {
bottom: 0; bottom: 0;
width: 100%; width: 100%;
padding: 6px; padding: 6px;
min-height: calc(6.75pt + 12px); height: calc(9.75pt + 12px);
font-size: small;
} }
.footer a { .footer a {
@@ -110,7 +106,7 @@ figure {
} }
.footer a img { .footer a img {
height: 22px !important; height: 9.75pt !important;
margin-left: 3px; margin-left: 3px;
vertical-align: text-bottom; vertical-align: text-bottom;
} }
@@ -136,20 +132,25 @@ 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 {
float: left; float: left;
height: 100%;
} }
.navbar .navcenter { .navbar .navcenter {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
height: 100%;
} }
.navbar .navright { .navbar .navright {
float: right float: right;
height: 100%;
} }
.navbar li .header { .navbar li .header {
@@ -201,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 {
@@ -223,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 {
@@ -303,6 +307,62 @@ input {
border-style: none; border-style: none;
} }
.darkmodeswitch a {
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
position: relative;
}
.darkmodeswitch a input[type="checkbox"] {
position: absolute;
width: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
z-index: 2;
}
.darkmodeswitch a .knobs {
display: flex;
align-items: center;
justify-content: center;
column-gap: 1.25em;
position: relative;
width: 100%;
}
.darkmodeswitch a .light,
.darkmodeswitch a .dark {
position: relative;
top: -0.111em;
}
.darkmodeswitch a .slider {
position: absolute;
width: calc(2em - 2px);
height: calc(2em - 2px);
border: 1px solid currentColor;
border-radius: 3px;
top: calc(50% - 2px);
transform: translate(-50%, -50%);
transition: left 0.25s ease, transform 0.25s ease;
left: calc(50% - 1em + 1px);
}
.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) { @media screen and (max-width: 1000px) {
.column { .column {
-ms-flex: 25%; -ms-flex: 25%;
@@ -310,6 +370,14 @@ input {
max-width: 25%; max-width: 25%;
} }
.footer {
font-size: small;
}
.footer a img {
height: 9.75pt !important;
}
.folders figure { .folders figure {
width: 160px; width: 160px;
} }
@@ -343,6 +411,14 @@ input {
max-width: 50%; max-width: 50%;
} }
.footer {
font-size: x-small;
}
.footer a img {
height: 7.5pt !important;
}
.folders figure { .folders figure {
width: 140px; width: 140px;
} }
@@ -384,6 +460,14 @@ input {
max-width: 100%; max-width: 100%;
} }
.footer {
font-size: xx-small;
}
.footer a img {
height: 6.75pt !important;
}
.folders figure { .folders figure {
width: 120px; width: 120px;
} }

192
help.svg
View File

@@ -1,4 +1,4 @@
<svg class="rich-terminal" viewBox="0 0 1482 977.1999999999999" xmlns="http://www.w3.org/2000/svg"> <svg class="rich-terminal" viewBox="0 0 1482 1074.8" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io --> <!-- Generated with Rich https://www.textualize.io -->
<style> <style>
@@ -19,191 +19,207 @@
font-weight: 700; font-weight: 700;
} }
.terminal-1232074479-matrix { .terminal-1915255058-matrix {
font-family: Fira Code, monospace; font-family: Fira Code, monospace;
font-size: 20px; font-size: 20px;
line-height: 24.4px; line-height: 24.4px;
font-variant-east-asian: full-width; font-variant-east-asian: full-width;
} }
.terminal-1232074479-title { .terminal-1915255058-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
font-family: arial; font-family: arial;
} }
.terminal-1232074479-r1 { fill: #ff8700 } .terminal-1915255058-r1 { fill: #ff8700 }
.terminal-1232074479-r2 { fill: #c5c8c6 } .terminal-1915255058-r2 { fill: #c5c8c6 }
.terminal-1232074479-r3 { fill: #808080 } .terminal-1915255058-r3 { fill: #808080 }
.terminal-1232074479-r4 { fill: #68a0b3 } .terminal-1915255058-r4 { fill: #68a0b3 }
.terminal-1232074479-r5 { fill: #00af87 } .terminal-1915255058-r5 { fill: #00af87 }
</style> </style>
<defs> <defs>
<clipPath id="terminal-1232074479-clip-terminal"> <clipPath id="terminal-1915255058-clip-terminal">
<rect x="0" y="0" width="1463.0" height="926.1999999999999" /> <rect x="0" y="0" width="1463.0" height="1023.8" />
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-0"> <clipPath id="terminal-1915255058-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/> <rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-1"> <clipPath id="terminal-1915255058-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/> <rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-2"> <clipPath id="terminal-1915255058-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/> <rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-3"> <clipPath id="terminal-1915255058-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/> <rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-4"> <clipPath id="terminal-1915255058-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/> <rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-5"> <clipPath id="terminal-1915255058-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/> <rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-6"> <clipPath id="terminal-1915255058-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/> <rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-7"> <clipPath id="terminal-1915255058-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/> <rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-8"> <clipPath id="terminal-1915255058-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/> <rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-9"> <clipPath id="terminal-1915255058-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/> <rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-10"> <clipPath id="terminal-1915255058-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/> <rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-11"> <clipPath id="terminal-1915255058-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/> <rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-12"> <clipPath id="terminal-1915255058-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/> <rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-13"> <clipPath id="terminal-1915255058-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/> <rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-14"> <clipPath id="terminal-1915255058-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/> <rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-15"> <clipPath id="terminal-1915255058-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/> <rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-16"> <clipPath id="terminal-1915255058-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/> <rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-17"> <clipPath id="terminal-1915255058-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/> <rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-18"> <clipPath id="terminal-1915255058-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/> <rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-19"> <clipPath id="terminal-1915255058-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/> <rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-20"> <clipPath id="terminal-1915255058-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/> <rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-21"> <clipPath id="terminal-1915255058-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/> <rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-22"> <clipPath id="terminal-1915255058-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/> <rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-23"> <clipPath id="terminal-1915255058-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/> <rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-24"> <clipPath id="terminal-1915255058-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/> <rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-25"> <clipPath id="terminal-1915255058-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/> <rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-26"> <clipPath id="terminal-1915255058-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/> <rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-27"> <clipPath id="terminal-1915255058-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/> <rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-28"> <clipPath id="terminal-1915255058-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/> <rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-29"> <clipPath id="terminal-1915255058-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/> <rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-30"> <clipPath id="terminal-1915255058-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/> <rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-31"> <clipPath id="terminal-1915255058-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/> <rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-32"> <clipPath id="terminal-1915255058-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/> <rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-33"> <clipPath id="terminal-1915255058-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/> <rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-34"> <clipPath id="terminal-1915255058-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/> <rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-35"> <clipPath id="terminal-1915255058-line-35">
<rect x="0" y="855.5" width="1464" height="24.65"/> <rect x="0" y="855.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1232074479-line-36"> <clipPath id="terminal-1915255058-line-36">
<rect x="0" y="879.9" width="1464" height="24.65"/> <rect x="0" y="879.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-1915255058-line-37">
<rect x="0" y="904.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1915255058-line-38">
<rect x="0" y="928.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1915255058-line-39">
<rect x="0" y="953.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-1915255058-line-40">
<rect x="0" y="977.5" width="1464" height="24.65"/>
</clipPath>
</defs> </defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="975.2" rx="8"/> <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="1072.8" rx="8"/>
<g transform="translate(26,22)"> <g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/> <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/> <circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/> <circle cx="44" cy="0" r="7" fill="#28c840"/>
</g> </g>
<g transform="translate(9, 41)" clip-path="url(#terminal-1232074479-clip-terminal)"> <g transform="translate(9, 41)" clip-path="url(#terminal-1915255058-clip-terminal)">
<g class="terminal-1232074479-matrix"> <g class="terminal-1915255058-matrix">
<text class="terminal-1232074479-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-1232074479-line-0)">Usage:</text><text class="terminal-1232074479-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-1232074479-line-0)">builder.py</text><text class="terminal-1232074479-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-h</text><text class="terminal-1232074479-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-a</text><text class="terminal-1232074479-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-1232074479-line-0)">AUTHOR</text><text class="terminal-1232074479-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-e</text><text class="terminal-1232074479-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-1232074479-line-0)">EXTENSION</text><text class="terminal-1232074479-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-l</text><text class="terminal-1232074479-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-1232074479-line-0)">LICENSE</text><text class="terminal-1232074479-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-m</text><text class="terminal-1232074479-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">]&#160;[</text><text class="terminal-1232074479-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-n</text><text class="terminal-1232074479-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">]&#160;</text><text class="terminal-1232074479-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-p</text><text class="terminal-1232074479-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-1232074479-line-0)">ROOT</text><text class="terminal-1232074479-r4" x="988.2" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-t</text><text class="terminal-1232074479-r5" x="1024.8" y="20" textLength="61" clip-path="url(#terminal-1232074479-line-0)">TITLE</text><text class="terminal-1232074479-r4" x="1098" y="20" textLength="24.4" clip-path="url(#terminal-1232074479-line-0)">-w</text><text class="terminal-1232074479-r5" x="1134.6" y="20" textLength="36.6" clip-path="url(#terminal-1232074479-line-0)">URL</text><text class="terminal-1232074479-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-1232074479-line-0)"> <text class="terminal-1915255058-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-1915255058-line-0)">Usage:</text><text class="terminal-1915255058-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-1915255058-line-0)">builder.py</text><text class="terminal-1915255058-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-h</text><text class="terminal-1915255058-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-a</text><text class="terminal-1915255058-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-1915255058-line-0)">AUTHOR</text><text class="terminal-1915255058-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-e</text><text class="terminal-1915255058-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-1915255058-line-0)">EXTENSION</text><text class="terminal-1915255058-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-l</text><text class="terminal-1915255058-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-1915255058-line-0)">LICENSE</text><text class="terminal-1915255058-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-m</text><text class="terminal-1915255058-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">]&#160;[</text><text class="terminal-1915255058-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-n</text><text class="terminal-1915255058-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">]&#160;</text><text class="terminal-1915255058-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-p</text><text class="terminal-1915255058-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-1915255058-line-0)">ROOT</text><text class="terminal-1915255058-r4" x="988.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-t</text><text class="terminal-1915255058-r5" x="1024.8" y="20" textLength="61" clip-path="url(#terminal-1915255058-line-0)">TITLE</text><text class="terminal-1915255058-r4" x="1098" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-w</text><text class="terminal-1915255058-r5" x="1134.6" y="20" textLength="36.6" clip-path="url(#terminal-1915255058-line-0)">URL</text><text class="terminal-1915255058-r2" x="1171.2" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">&#160;[</text><text class="terminal-1915255058-r4" x="1195.6" y="20" textLength="24.4" clip-path="url(#terminal-1915255058-line-0)">-c</text><text class="terminal-1915255058-r5" x="1232.2" y="20" textLength="134.2" clip-path="url(#terminal-1915255058-line-0)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1366.4" y="20" textLength="12.2" clip-path="url(#terminal-1915255058-line-0)">]</text><text class="terminal-1915255058-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-1915255058-line-0)">
</text><text class="terminal-1232074479-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-1232074479-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-1232074479-line-1)">--exclude-folder</text><text class="terminal-1232074479-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-1232074479-line-1)">FOLDER</text><text class="terminal-1232074479-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-1232074479-line-1)">]&#160;[</text><text class="terminal-1232074479-r4" x="549" y="44.4" textLength="219.6" clip-path="url(#terminal-1232074479-line-1)">--folderthumbnails</text><text class="terminal-1232074479-r2" x="768.6" y="44.4" textLength="36.6" clip-path="url(#terminal-1232074479-line-1)">]&#160;[</text><text class="terminal-1232074479-r4" x="805.2" y="44.4" textLength="244" clip-path="url(#terminal-1232074479-line-1)">--ignore-other-files</text><text class="terminal-1232074479-r2" x="1049.2" y="44.4" textLength="36.6" clip-path="url(#terminal-1232074479-line-1)">]&#160;[</text><text class="terminal-1232074479-r4" x="1085.8" y="44.4" textLength="219.6" clip-path="url(#terminal-1232074479-line-1)">--ignore-extension</text><text class="terminal-1232074479-r5" x="1317.6" y="44.4" textLength="109.8" clip-path="url(#terminal-1232074479-line-1)">EXTENSION</text><text class="terminal-1232074479-r2" x="1427.4" y="44.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-1)">]</text><text class="terminal-1232074479-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-1)"> </text><text class="terminal-1915255058-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-1915255058-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-1915255058-line-1)">--exclude-folder</text><text class="terminal-1915255058-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-1915255058-line-1)">FOLDER</text><text class="terminal-1915255058-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-1915255058-line-1)">]&#160;[</text><text class="terminal-1915255058-r4" x="549" y="44.4" textLength="219.6" clip-path="url(#terminal-1915255058-line-1)">--folderthumbnails</text><text class="terminal-1915255058-r2" x="768.6" y="44.4" textLength="36.6" clip-path="url(#terminal-1915255058-line-1)">]&#160;[</text><text class="terminal-1915255058-r4" x="805.2" y="44.4" textLength="244" clip-path="url(#terminal-1915255058-line-1)">--ignore-other-files</text><text class="terminal-1915255058-r2" x="1049.2" y="44.4" textLength="36.6" clip-path="url(#terminal-1915255058-line-1)">]&#160;[</text><text class="terminal-1915255058-r4" x="1085.8" y="44.4" textLength="219.6" clip-path="url(#terminal-1915255058-line-1)">--ignore-extension</text><text class="terminal-1915255058-r5" x="1317.6" y="44.4" textLength="109.8" clip-path="url(#terminal-1915255058-line-1)">EXTENSION</text><text class="terminal-1915255058-r2" x="1427.4" y="44.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-1)">]</text><text class="terminal-1915255058-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-1)">
</text><text class="terminal-1232074479-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-1232074479-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="68.8" textLength="280.6" clip-path="url(#terminal-1232074479-line-2)">--regenerate-thumbnails</text><text class="terminal-1232074479-r2" x="512.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-2)">]&#160;[</text><text class="terminal-1232074479-r4" x="549" y="68.8" textLength="207.4" clip-path="url(#terminal-1232074479-line-2)">--reread-metadata</text><text class="terminal-1232074479-r2" x="756.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-2)">]&#160;[</text><text class="terminal-1232074479-r4" x="793" y="68.8" textLength="195.2" clip-path="url(#terminal-1232074479-line-2)">--reread-sidecar</text><text class="terminal-1232074479-r2" x="988.2" y="68.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-2)">]&#160;[</text><text class="terminal-1232074479-r4" x="1024.8" y="68.8" textLength="170.8" clip-path="url(#terminal-1232074479-line-2)">--reverse-sort</text><text class="terminal-1232074479-r2" x="1195.6" y="68.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-2)">]</text><text class="terminal-1232074479-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-2)"> </text><text class="terminal-1915255058-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-1915255058-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="68.8" textLength="280.6" clip-path="url(#terminal-1915255058-line-2)">--regenerate-thumbnails</text><text class="terminal-1915255058-r2" x="512.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-2)">]&#160;[</text><text class="terminal-1915255058-r4" x="549" y="68.8" textLength="207.4" clip-path="url(#terminal-1915255058-line-2)">--reread-metadata</text><text class="terminal-1915255058-r2" x="756.4" y="68.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-2)">]&#160;[</text><text class="terminal-1915255058-r4" x="793" y="68.8" textLength="195.2" clip-path="url(#terminal-1915255058-line-2)">--reread-sidecar</text><text class="terminal-1915255058-r2" x="988.2" y="68.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-2)">]&#160;[</text><text class="terminal-1915255058-r4" x="1024.8" y="68.8" textLength="170.8" clip-path="url(#terminal-1915255058-line-2)">--reverse-sort</text><text class="terminal-1915255058-r2" x="1195.6" y="68.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-2)">]</text><text class="terminal-1915255058-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-2)">
</text><text class="terminal-1232074479-r2" x="0" y="93.2" textLength="231.8" clip-path="url(#terminal-1232074479-line-3)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1232074479-r4" x="231.8" y="93.2" textLength="146.4" clip-path="url(#terminal-1232074479-line-3)">--theme-path</text><text class="terminal-1232074479-r5" x="390.4" y="93.2" textLength="48.8" clip-path="url(#terminal-1232074479-line-3)">PATH</text><text class="terminal-1232074479-r2" x="439.2" y="93.2" textLength="36.6" clip-path="url(#terminal-1232074479-line-3)">]&#160;[</text><text class="terminal-1232074479-r4" x="475.8" y="93.2" textLength="231.8" clip-path="url(#terminal-1232074479-line-3)">--use-fancy-folders</text><text class="terminal-1232074479-r2" x="707.6" y="93.2" textLength="36.6" clip-path="url(#terminal-1232074479-line-3)">]&#160;[</text><text class="terminal-1232074479-r4" x="744.2" y="93.2" textLength="109.8" clip-path="url(#terminal-1232074479-line-3)">--version</text><text class="terminal-1232074479-r2" x="854" y="93.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-3)">]</text><text class="terminal-1232074479-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-3)"> </text><text class="terminal-1915255058-r2" x="0" y="93.2" textLength="231.8" clip-path="url(#terminal-1915255058-line-3)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-1915255058-r4" x="231.8" y="93.2" textLength="146.4" clip-path="url(#terminal-1915255058-line-3)">--theme-path</text><text class="terminal-1915255058-r5" x="390.4" y="93.2" textLength="48.8" clip-path="url(#terminal-1915255058-line-3)">PATH</text><text class="terminal-1915255058-r2" x="439.2" y="93.2" textLength="36.6" clip-path="url(#terminal-1915255058-line-3)">]&#160;[</text><text class="terminal-1915255058-r4" x="475.8" y="93.2" textLength="231.8" clip-path="url(#terminal-1915255058-line-3)">--use-fancy-folders</text><text class="terminal-1915255058-r2" x="707.6" y="93.2" textLength="36.6" clip-path="url(#terminal-1915255058-line-3)">]&#160;[</text><text class="terminal-1915255058-r4" x="744.2" y="93.2" textLength="109.8" clip-path="url(#terminal-1915255058-line-3)">--version</text><text class="terminal-1915255058-r2" x="854" y="93.2" textLength="36.6" clip-path="url(#terminal-1915255058-line-3)">]&#160;[</text><text class="terminal-1915255058-r4" x="890.6" y="93.2" textLength="170.8" clip-path="url(#terminal-1915255058-line-3)">--write-config</text><text class="terminal-1915255058-r5" x="1073.6" y="93.2" textLength="134.2" clip-path="url(#terminal-1915255058-line-3)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1207.8" y="93.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-3)">]</text><text class="terminal-1915255058-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-3)">
</text><text class="terminal-1232074479-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-4)"> </text><text class="terminal-1915255058-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-4)">
</text><text class="terminal-1232074479-r2" x="0" y="142" textLength="658.8" clip-path="url(#terminal-1232074479-line-5)">generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website</text><text class="terminal-1232074479-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-1232074479-line-5)"> </text><text class="terminal-1915255058-r2" x="0" y="142" textLength="658.8" clip-path="url(#terminal-1915255058-line-5)">generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website</text><text class="terminal-1915255058-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-1915255058-line-5)">
</text><text class="terminal-1232074479-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-6)"> </text><text class="terminal-1915255058-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-6)">
</text><text class="terminal-1232074479-r1" x="0" y="190.8" textLength="97.6" clip-path="url(#terminal-1232074479-line-7)">Options:</text><text class="terminal-1232074479-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-7)"> </text><text class="terminal-1915255058-r1" x="0" y="190.8" textLength="97.6" clip-path="url(#terminal-1915255058-line-7)">Options:</text><text class="terminal-1915255058-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-7)">
</text><text class="terminal-1232074479-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-8)">-h</text><text class="terminal-1232074479-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-8)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="215.2" textLength="73.2" clip-path="url(#terminal-1232074479-line-8)">--help</text><text class="terminal-1232074479-r2" x="292.8" y="215.2" textLength="378.2" clip-path="url(#terminal-1232074479-line-8)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-1232074479-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-8)"> </text><text class="terminal-1915255058-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-8)">-h</text><text class="terminal-1915255058-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-8)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="215.2" textLength="73.2" clip-path="url(#terminal-1915255058-line-8)">--help</text><text class="terminal-1915255058-r2" x="292.8" y="215.2" textLength="378.2" clip-path="url(#terminal-1915255058-line-8)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-1915255058-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-8)">
</text><text class="terminal-1232074479-r4" x="24.4" y="239.6" textLength="24.4" clip-path="url(#terminal-1232074479-line-9)">-a</text><text class="terminal-1232074479-r2" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-1232074479-line-9)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="239.6" textLength="158.6" clip-path="url(#terminal-1232074479-line-9)">--author-name</text><text class="terminal-1232074479-r5" x="244" y="239.6" textLength="73.2" clip-path="url(#terminal-1232074479-line-9)">AUTHOR</text><text class="terminal-1232074479-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-9)"> </text><text class="terminal-1915255058-r4" x="24.4" y="239.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-9)">-a</text><text class="terminal-1915255058-r2" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-9)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="239.6" textLength="158.6" clip-path="url(#terminal-1915255058-line-9)">--author-name</text><text class="terminal-1915255058-r5" x="244" y="239.6" textLength="73.2" clip-path="url(#terminal-1915255058-line-9)">AUTHOR</text><text class="terminal-1915255058-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-9)">
</text><text class="terminal-1232074479-r2" x="292.8" y="264" textLength="390.4" clip-path="url(#terminal-1232074479-line-10)">name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images</text><text class="terminal-1232074479-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-1232074479-line-10)"> </text><text class="terminal-1915255058-r2" x="292.8" y="264" textLength="390.4" clip-path="url(#terminal-1915255058-line-10)">name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images</text><text class="terminal-1915255058-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-1915255058-line-10)">
</text><text class="terminal-1232074479-r4" x="24.4" y="288.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-11)">-e</text><text class="terminal-1232074479-r2" x="48.8" y="288.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-11)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="288.4" textLength="207.4" clip-path="url(#terminal-1232074479-line-11)">--file-extensions</text><text class="terminal-1232074479-r5" x="292.8" y="288.4" textLength="109.8" clip-path="url(#terminal-1232074479-line-11)">EXTENSION</text><text class="terminal-1232074479-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-11)"> </text><text class="terminal-1915255058-r4" x="24.4" y="288.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-11)">-e</text><text class="terminal-1915255058-r2" x="48.8" y="288.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-11)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="288.4" textLength="207.4" clip-path="url(#terminal-1915255058-line-11)">--file-extensions</text><text class="terminal-1915255058-r5" x="292.8" y="288.4" textLength="109.8" clip-path="url(#terminal-1915255058-line-11)">EXTENSION</text><text class="terminal-1915255058-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-11)">
</text><text class="terminal-1232074479-r2" x="292.8" y="312.8" textLength="732" clip-path="url(#terminal-1232074479-line-12)">file&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1232074479-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-12)"> </text><text class="terminal-1915255058-r2" x="292.8" y="312.8" textLength="732" clip-path="url(#terminal-1915255058-line-12)">file&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1915255058-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-12)">
</text><text class="terminal-1232074479-r4" x="24.4" y="337.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-13)">-l</text><text class="terminal-1232074479-r2" x="48.8" y="337.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-13)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="337.2" textLength="170.8" clip-path="url(#terminal-1232074479-line-13)">--license-type</text><text class="terminal-1232074479-r5" x="256.2" y="337.2" textLength="85.4" clip-path="url(#terminal-1232074479-line-13)">LICENSE</text><text class="terminal-1232074479-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-13)"> </text><text class="terminal-1915255058-r4" x="24.4" y="337.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-13)">-l</text><text class="terminal-1915255058-r2" x="48.8" y="337.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-13)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="337.2" textLength="170.8" clip-path="url(#terminal-1915255058-line-13)">--license-type</text><text class="terminal-1915255058-r5" x="256.2" y="337.2" textLength="85.4" clip-path="url(#terminal-1915255058-line-13)">LICENSE</text><text class="terminal-1915255058-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-13)">
</text><text class="terminal-1232074479-r2" x="292.8" y="361.6" textLength="475.8" clip-path="url(#terminal-1232074479-line-14)">specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images</text><text class="terminal-1232074479-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-14)"> </text><text class="terminal-1915255058-r2" x="292.8" y="361.6" textLength="475.8" clip-path="url(#terminal-1915255058-line-14)">specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images</text><text class="terminal-1915255058-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-14)">
</text><text class="terminal-1232074479-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-1232074479-line-15)">-m</text><text class="terminal-1232074479-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-1232074479-line-15)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="386" textLength="170.8" clip-path="url(#terminal-1232074479-line-15)">--web-manifest</text><text class="terminal-1232074479-r2" x="292.8" y="386" textLength="341.6" clip-path="url(#terminal-1232074479-line-15)">generate&#160;a&#160;web&#160;manifest&#160;file</text><text class="terminal-1232074479-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-1232074479-line-15)"> </text><text class="terminal-1915255058-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-1915255058-line-15)">-m</text><text class="terminal-1915255058-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-1915255058-line-15)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="386" textLength="170.8" clip-path="url(#terminal-1915255058-line-15)">--web-manifest</text><text class="terminal-1915255058-r2" x="292.8" y="386" textLength="341.6" clip-path="url(#terminal-1915255058-line-15)">generate&#160;a&#160;web&#160;manifest&#160;file</text><text class="terminal-1915255058-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-1915255058-line-15)">
</text><text class="terminal-1232074479-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-16)">-n</text><text class="terminal-1232074479-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-1232074479-line-16)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-1232074479-line-16)">--non-interactive-mode</text><text class="terminal-1232074479-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-16)"> </text><text class="terminal-1915255058-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-16)">-n</text><text class="terminal-1915255058-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-1915255058-line-16)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-1915255058-line-16)">--non-interactive-mode</text><text class="terminal-1915255058-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-16)">
</text><text class="terminal-1232074479-r2" x="292.8" y="434.8" textLength="634.4" clip-path="url(#terminal-1232074479-line-17)">run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars</text><text class="terminal-1232074479-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-17)"> </text><text class="terminal-1915255058-r2" x="292.8" y="434.8" textLength="634.4" clip-path="url(#terminal-1915255058-line-17)">run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars</text><text class="terminal-1915255058-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-17)">
</text><text class="terminal-1232074479-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-18)">-p</text><text class="terminal-1232074479-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-1232074479-line-18)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="459.2" textLength="195.2" clip-path="url(#terminal-1232074479-line-18)">--root-directory</text><text class="terminal-1232074479-r5" x="280.6" y="459.2" textLength="48.8" clip-path="url(#terminal-1232074479-line-18)">ROOT</text><text class="terminal-1232074479-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-18)"> </text><text class="terminal-1915255058-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-18)">-p</text><text class="terminal-1915255058-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-1915255058-line-18)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="459.2" textLength="195.2" clip-path="url(#terminal-1915255058-line-18)">--root-directory</text><text class="terminal-1915255058-r5" x="280.6" y="459.2" textLength="48.8" clip-path="url(#terminal-1915255058-line-18)">ROOT</text><text class="terminal-1915255058-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-18)">
</text><text class="terminal-1232074479-r2" x="292.8" y="483.6" textLength="439.2" clip-path="url(#terminal-1232074479-line-19)">root&#160;directory&#160;containing&#160;the&#160;images</text><text class="terminal-1232074479-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-19)"> </text><text class="terminal-1915255058-r2" x="292.8" y="483.6" textLength="439.2" clip-path="url(#terminal-1915255058-line-19)">root&#160;directory&#160;containing&#160;the&#160;images</text><text class="terminal-1915255058-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-19)">
</text><text class="terminal-1232074479-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-1232074479-line-20)">-t</text><text class="terminal-1232074479-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-1232074479-line-20)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="508" textLength="146.4" clip-path="url(#terminal-1232074479-line-20)">--site-title</text><text class="terminal-1232074479-r5" x="231.8" y="508" textLength="61" clip-path="url(#terminal-1232074479-line-20)">TITLE</text><text class="terminal-1232074479-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-1232074479-line-20)"> </text><text class="terminal-1915255058-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-1915255058-line-20)">-t</text><text class="terminal-1915255058-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-1915255058-line-20)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="508" textLength="146.4" clip-path="url(#terminal-1915255058-line-20)">--site-title</text><text class="terminal-1915255058-r5" x="231.8" y="508" textLength="61" clip-path="url(#terminal-1915255058-line-20)">TITLE</text><text class="terminal-1915255058-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-1915255058-line-20)">
</text><text class="terminal-1232074479-r2" x="292.8" y="532.4" textLength="378.2" clip-path="url(#terminal-1232074479-line-21)">title&#160;of&#160;the&#160;image&#160;hosting&#160;site</text><text class="terminal-1232074479-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-21)"> </text><text class="terminal-1915255058-r2" x="292.8" y="532.4" textLength="378.2" clip-path="url(#terminal-1915255058-line-21)">title&#160;of&#160;the&#160;image&#160;hosting&#160;site</text><text class="terminal-1915255058-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-21)">
</text><text class="terminal-1232074479-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-1232074479-line-22)">-w</text><text class="terminal-1232074479-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-1232074479-line-22)">,&#160;</text><text class="terminal-1232074479-r4" x="73.2" y="556.8" textLength="170.8" clip-path="url(#terminal-1232074479-line-22)">--web-root-url</text><text class="terminal-1232074479-r5" x="256.2" y="556.8" textLength="36.6" clip-path="url(#terminal-1232074479-line-22)">URL</text><text class="terminal-1232074479-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-22)"> </text><text class="terminal-1915255058-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-1915255058-line-22)">-w</text><text class="terminal-1915255058-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-1915255058-line-22)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="556.8" textLength="170.8" clip-path="url(#terminal-1915255058-line-22)">--web-root-url</text><text class="terminal-1915255058-r5" x="256.2" y="556.8" textLength="36.6" clip-path="url(#terminal-1915255058-line-22)">URL</text><text class="terminal-1915255058-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-22)">
</text><text class="terminal-1232074479-r2" x="292.8" y="581.2" textLength="622.2" clip-path="url(#terminal-1232074479-line-23)">base&#160;URL&#160;of&#160;the&#160;web&#160;root&#160;for&#160;the&#160;image&#160;hosting&#160;site</text><text class="terminal-1232074479-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-23)"> </text><text class="terminal-1915255058-r2" x="292.8" y="581.2" textLength="622.2" clip-path="url(#terminal-1915255058-line-23)">base&#160;URL&#160;of&#160;the&#160;web&#160;root&#160;for&#160;the&#160;image&#160;hosting&#160;site</text><text class="terminal-1915255058-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-23)">
</text><text class="terminal-1232074479-r4" x="24.4" y="605.6" textLength="195.2" clip-path="url(#terminal-1232074479-line-24)">--exclude-folder</text><text class="terminal-1232074479-r5" x="231.8" y="605.6" textLength="73.2" clip-path="url(#terminal-1232074479-line-24)">FOLDER</text><text class="terminal-1232074479-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-24)"> </text><text class="terminal-1915255058-r4" x="24.4" y="605.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-24)">-c</text><text class="terminal-1915255058-r2" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-1915255058-line-24)">,&#160;</text><text class="terminal-1915255058-r4" x="73.2" y="605.6" textLength="158.6" clip-path="url(#terminal-1915255058-line-24)">--config-file</text><text class="terminal-1915255058-r5" x="244" y="605.6" textLength="134.2" clip-path="url(#terminal-1915255058-line-24)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-24)">
</text><text class="terminal-1232074479-r2" x="292.8" y="630" textLength="1037" clip-path="url(#terminal-1232074479-line-25)">folders&#160;to&#160;exclude&#160;from&#160;processing,&#160;globs&#160;supported&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1232074479-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-1232074479-line-25)"> </text><text class="terminal-1915255058-r2" x="292.8" y="630" textLength="195.2" clip-path="url(#terminal-1915255058-line-25)">config&#160;file&#160;path</text><text class="terminal-1915255058-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-1915255058-line-25)">
</text><text class="terminal-1232074479-r4" x="24.4" y="654.4" textLength="219.6" clip-path="url(#terminal-1232074479-line-26)">--folderthumbnails</text><text class="terminal-1232074479-r2" x="292.8" y="654.4" textLength="817.4" clip-path="url(#terminal-1232074479-line-26)">generate&#160;subfolder&#160;thumbnails&#160;(first&#160;image&#160;in&#160;folder&#160;will&#160;be&#160;shown)</text><text class="terminal-1232074479-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-26)"> </text><text class="terminal-1915255058-r4" x="24.4" y="654.4" textLength="195.2" clip-path="url(#terminal-1915255058-line-26)">--exclude-folder</text><text class="terminal-1915255058-r5" x="231.8" y="654.4" textLength="73.2" clip-path="url(#terminal-1915255058-line-26)">FOLDER</text><text class="terminal-1915255058-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-26)">
</text><text class="terminal-1232074479-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-1232074479-line-27)">--ignore-other-files</text><text class="terminal-1232074479-r2" x="292.8" y="678.8" textLength="671" clip-path="url(#terminal-1232074479-line-27)">ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions</text><text class="terminal-1232074479-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-27)"> </text><text class="terminal-1915255058-r2" x="292.8" y="678.8" textLength="1037" clip-path="url(#terminal-1915255058-line-27)">folders&#160;to&#160;exclude&#160;from&#160;processing,&#160;globs&#160;supported&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1915255058-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-27)">
</text><text class="terminal-1232074479-r4" x="24.4" y="703.2" textLength="219.6" clip-path="url(#terminal-1232074479-line-28)">--ignore-extension</text><text class="terminal-1232074479-r5" x="256.2" y="703.2" textLength="109.8" clip-path="url(#terminal-1232074479-line-28)">EXTENSION</text><text class="terminal-1232074479-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-28)"> </text><text class="terminal-1915255058-r4" x="24.4" y="703.2" textLength="219.6" clip-path="url(#terminal-1915255058-line-28)">--folderthumbnails</text><text class="terminal-1915255058-r2" x="292.8" y="703.2" textLength="817.4" clip-path="url(#terminal-1915255058-line-28)">generate&#160;subfolder&#160;thumbnails&#160;(first&#160;image&#160;in&#160;folder&#160;will&#160;be&#160;shown)</text><text class="terminal-1915255058-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-28)">
</text><text class="terminal-1232074479-r2" x="292.8" y="727.6" textLength="719.8" clip-path="url(#terminal-1232074479-line-29)">file&#160;extensions&#160;to&#160;ignore&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1232074479-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-29)"> </text><text class="terminal-1915255058-r4" x="24.4" y="727.6" textLength="244" clip-path="url(#terminal-1915255058-line-29)">--ignore-other-files</text><text class="terminal-1915255058-r2" x="292.8" y="727.6" textLength="671" clip-path="url(#terminal-1915255058-line-29)">ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions</text><text class="terminal-1915255058-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-29)">
</text><text class="terminal-1232074479-r4" x="24.4" y="752" textLength="280.6" clip-path="url(#terminal-1232074479-line-30)">--regenerate-thumbnails</text><text class="terminal-1232074479-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-1232074479-line-30)"> </text><text class="terminal-1915255058-r4" x="24.4" y="752" textLength="219.6" clip-path="url(#terminal-1915255058-line-30)">--ignore-extension</text><text class="terminal-1915255058-r5" x="256.2" y="752" textLength="109.8" clip-path="url(#terminal-1915255058-line-30)">EXTENSION</text><text class="terminal-1915255058-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-1915255058-line-30)">
</text><text class="terminal-1232074479-r2" x="292.8" y="776.4" textLength="585.6" clip-path="url(#terminal-1232074479-line-31)">regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist</text><text class="terminal-1232074479-r2" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-31)"> </text><text class="terminal-1915255058-r2" x="292.8" y="776.4" textLength="719.8" clip-path="url(#terminal-1915255058-line-31)">file&#160;extensions&#160;to&#160;ignore&#160;(can&#160;be&#160;specified&#160;multiple&#160;times)</text><text class="terminal-1915255058-r2" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-31)">
</text><text class="terminal-1232074479-r4" x="24.4" y="800.8" textLength="207.4" clip-path="url(#terminal-1232074479-line-32)">--reread-metadata</text><text class="terminal-1232074479-r2" x="292.8" y="800.8" textLength="256.2" clip-path="url(#terminal-1232074479-line-32)">reread&#160;image&#160;metadata</text><text class="terminal-1232074479-r2" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-32)"> </text><text class="terminal-1915255058-r4" x="24.4" y="800.8" textLength="280.6" clip-path="url(#terminal-1915255058-line-32)">--regenerate-thumbnails</text><text class="terminal-1915255058-r2" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-32)">
</text><text class="terminal-1232074479-r4" x="24.4" y="825.2" textLength="195.2" clip-path="url(#terminal-1232074479-line-33)">--reread-sidecar</text><text class="terminal-1232074479-r2" x="292.8" y="825.2" textLength="244" clip-path="url(#terminal-1232074479-line-33)">reread&#160;sidecar&#160;files</text><text class="terminal-1232074479-r2" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-1232074479-line-33)"> </text><text class="terminal-1915255058-r2" x="292.8" y="825.2" textLength="585.6" clip-path="url(#terminal-1915255058-line-33)">regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist</text><text class="terminal-1915255058-r2" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-33)">
</text><text class="terminal-1232074479-r4" x="24.4" y="849.6" textLength="170.8" clip-path="url(#terminal-1232074479-line-34)">--reverse-sort</text><text class="terminal-1232074479-r2" x="292.8" y="849.6" textLength="341.6" clip-path="url(#terminal-1232074479-line-34)">sort&#160;images&#160;in&#160;reverse&#160;order</text><text class="terminal-1232074479-r2" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-1232074479-line-34)"> </text><text class="terminal-1915255058-r4" x="24.4" y="849.6" textLength="207.4" clip-path="url(#terminal-1915255058-line-34)">--reread-metadata</text><text class="terminal-1915255058-r2" x="292.8" y="849.6" textLength="256.2" clip-path="url(#terminal-1915255058-line-34)">reread&#160;image&#160;metadata</text><text class="terminal-1915255058-r2" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-34)">
</text><text class="terminal-1232074479-r4" x="24.4" y="874" textLength="146.4" clip-path="url(#terminal-1232074479-line-35)">--theme-path</text><text class="terminal-1232074479-r5" x="183" y="874" textLength="48.8" clip-path="url(#terminal-1232074479-line-35)">PATH</text><text class="terminal-1232074479-r2" x="292.8" y="874" textLength="317.2" clip-path="url(#terminal-1232074479-line-35)">path&#160;to&#160;the&#160;CSS&#160;theme&#160;file</text><text class="terminal-1232074479-r2" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-1232074479-line-35)"> </text><text class="terminal-1915255058-r4" x="24.4" y="874" textLength="195.2" clip-path="url(#terminal-1915255058-line-35)">--reread-sidecar</text><text class="terminal-1915255058-r2" x="292.8" y="874" textLength="244" clip-path="url(#terminal-1915255058-line-35)">reread&#160;sidecar&#160;files</text><text class="terminal-1915255058-r2" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-1915255058-line-35)">
</text><text class="terminal-1232074479-r4" x="24.4" y="898.4" textLength="231.8" clip-path="url(#terminal-1232074479-line-36)">--use-fancy-folders</text><text class="terminal-1232074479-r2" x="292.8" y="898.4" textLength="878.4" clip-path="url(#terminal-1232074479-line-36)">enable&#160;fancy&#160;folder&#160;view&#160;instead&#160;of&#160;the&#160;default&#160;Apache&#160;directory&#160;listing</text><text class="terminal-1232074479-r2" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-1232074479-line-36)"> </text><text class="terminal-1915255058-r4" x="24.4" y="898.4" textLength="170.8" clip-path="url(#terminal-1915255058-line-36)">--reverse-sort</text><text class="terminal-1915255058-r2" x="292.8" y="898.4" textLength="341.6" clip-path="url(#terminal-1915255058-line-36)">sort&#160;images&#160;in&#160;reverse&#160;order</text><text class="terminal-1915255058-r2" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-36)">
</text><text class="terminal-1232074479-r4" x="24.4" y="922.8" textLength="109.8" clip-path="url(#terminal-1232074479-line-37)">--version</text><text class="terminal-1232074479-r2" x="292.8" y="922.8" textLength="463.6" clip-path="url(#terminal-1232074479-line-37)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-1232074479-r2" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-1232074479-line-37)"> </text><text class="terminal-1915255058-r4" x="24.4" y="922.8" textLength="146.4" clip-path="url(#terminal-1915255058-line-37)">--theme-path</text><text class="terminal-1915255058-r5" x="183" y="922.8" textLength="48.8" clip-path="url(#terminal-1915255058-line-37)">PATH</text><text class="terminal-1915255058-r2" x="292.8" y="922.8" textLength="317.2" clip-path="url(#terminal-1915255058-line-37)">path&#160;to&#160;the&#160;CSS&#160;theme&#160;file</text><text class="terminal-1915255058-r2" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-1915255058-line-37)">
</text><text class="terminal-1915255058-r4" x="24.4" y="947.2" textLength="231.8" clip-path="url(#terminal-1915255058-line-38)">--use-fancy-folders</text><text class="terminal-1915255058-r2" x="292.8" y="947.2" textLength="878.4" clip-path="url(#terminal-1915255058-line-38)">enable&#160;fancy&#160;folder&#160;view&#160;instead&#160;of&#160;the&#160;default&#160;Apache&#160;directory&#160;listing</text><text class="terminal-1915255058-r2" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-1915255058-line-38)">
</text><text class="terminal-1915255058-r4" x="24.4" y="971.6" textLength="109.8" clip-path="url(#terminal-1915255058-line-39)">--version</text><text class="terminal-1915255058-r2" x="292.8" y="971.6" textLength="463.6" clip-path="url(#terminal-1915255058-line-39)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-1915255058-r2" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-1915255058-line-39)">
</text><text class="terminal-1915255058-r4" x="24.4" y="996" textLength="170.8" clip-path="url(#terminal-1915255058-line-40)">--write-config</text><text class="terminal-1915255058-r5" x="207.4" y="996" textLength="134.2" clip-path="url(#terminal-1915255058-line-40)">CONFIG_FILE</text><text class="terminal-1915255058-r2" x="1464" y="996" textLength="12.2" clip-path="url(#terminal-1915255058-line-40)">
</text><text class="terminal-1915255058-r2" x="292.8" y="1020.4" textLength="561.2" clip-path="url(#terminal-1915255058-line-41)">write&#160;current&#160;command&#160;line&#160;args&#160;to&#160;config&#160;file</text><text class="terminal-1915255058-r2" x="1464" y="1020.4" textLength="12.2" clip-path="url(#terminal-1915255058-line-41)">
</text> </text>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,7 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
import os import os
import argparse
import configargparse
try: try:
from rich_argparse import RichHelpFormatter, HelpPreviewAction from rich_argparse import RichHelpFormatter, HelpPreviewAction
@@ -11,14 +12,18 @@ except ModuleNotFoundError:
RICH = False RICH = False
if __package__ is None: SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
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"
if "APPDATA" in os.environ:
CONFIGHOME = os.environ["APPDATA"]
elif "XDG_CONFIG_HOME" in os.environ:
CONFIGHOME = os.environ["XDG_CONFIG_HOME"]
else:
CONFIGHOME = os.path.join(os.environ["HOME"], ".config")
CONFIGPATH = os.path.join(CONFIGHOME, "StaticGalleryBuilder")
@dataclass(init=True) @dataclass(init=True)
class Args: class Args:
@@ -55,6 +60,8 @@ class Args:
Whether to enable fancy folder view. Whether to enable fancy folder view.
web_root_url : str web_root_url : str
The base URL of the web root for the image hosting site. The base URL of the web root for the image hosting site.
darktheme : bool
Whether a dark theme is present.
""" """
author_name: str author_name: str
@@ -75,6 +82,7 @@ class Args:
theme_path: str theme_path: str
use_fancy_folders: bool use_fancy_folders: bool
web_root_url: str web_root_url: str
darktheme: bool = False
def to_dict(self) -> dict: def to_dict(self) -> dict:
result: dict = {} result: dict = {}
@@ -97,6 +105,7 @@ class Args:
result["theme_path"] = self.theme_path result["theme_path"] = self.theme_path
result["use_fancy_folders"] = self.use_fancy_folders result["use_fancy_folders"] = self.use_fancy_folders
result["web_root_url"] = self.web_root_url result["web_root_url"] = self.web_root_url
result["darktheme"] = self.darktheme
return result return result
@@ -116,9 +125,9 @@ def parse_arguments(version: str) -> Args:
""" """
# fmt: off # fmt: off
if RICH: if RICH:
parser = argparse.ArgumentParser(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 = argparse.ArgumentParser(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")
parser.add_argument("-e", "--file-extensions", help="file extensions to include (can be specified multiple times)", action="append", dest="file_extensions", metavar="EXTENSION") parser.add_argument("-e", "--file-extensions", help="file extensions to include (can be specified multiple times)", action="append", dest="file_extensions", metavar="EXTENSION")
parser.add_argument("-l", "--license-type", help="specify the license type for the images", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE") parser.add_argument("-l", "--license-type", help="specify the license type for the images", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE")
@@ -127,10 +136,11 @@ def parse_arguments(version: str) -> Args:
parser.add_argument("-p", "--root-directory", help="root directory containing the images", required=True, type=str, dest="root_directory", metavar="ROOT") parser.add_argument("-p", "--root-directory", help="root directory containing the images", required=True, type=str, dest="root_directory", metavar="ROOT")
parser.add_argument("-t", "--site-title", help="title of the image hosting site", required=True, type=str, dest="site_title", metavar="TITLE") parser.add_argument("-t", "--site-title", help="title of the image hosting site", required=True, type=str, dest="site_title", metavar="TITLE")
parser.add_argument("-w", "--web-root-url", help="base URL of the web root for the image hosting site", required=True, type=str, dest="web_root_url", metavar="URL") parser.add_argument("-w", "--web-root-url", help="base URL of the web root for the image hosting site", required=True, type=str, dest="web_root_url", metavar="URL")
parser.add_argument('-c', '--config-file', is_config_file=True, help='config file path', metavar="CONFIG_FILE")
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")
@@ -140,7 +150,12 @@ def parse_arguments(version: str) -> Args:
parser.add_argument("--theme-path", help="path to the CSS theme file", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH") parser.add_argument("--theme-path", help="path to the CSS theme file", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
parser.add_argument("--use-fancy-folders", help="enable fancy folder view instead of the default Apache directory listing", action="store_true", default=False, dest="use_fancy_folders") parser.add_argument("--use-fancy-folders", help="enable fancy folder view instead of the default Apache directory listing", action="store_true", default=False, dest="use_fancy_folders")
parser.add_argument("--version", action="version", version=f"%(prog)s {version}") parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
parser.add_argument("--write-config", type=str, required=False, help="write current command line args to config file", metavar="CONFIG_FILE")
parsed_args = parser.parse_args() parsed_args = parser.parse_args()
if parsed_args.write_config:
config_path = parsed_args.write_config
del parsed_args.write_config
parser.write_config_file(parsed_args, [config_path], exit_after=False)
# fmt: on # fmt: on
_args = Args( _args = Args(
author_name=parsed_args.author_name, author_name=parsed_args.author_name,
@@ -161,5 +176,6 @@ def parse_arguments(version: str) -> Args:
theme_path=parsed_args.theme_path, theme_path=parsed_args.theme_path,
use_fancy_folders=parsed_args.use_fancy_folders, use_fancy_folders=parsed_args.use_fancy_folders,
web_root_url=parsed_args.web_root_url, web_root_url=parsed_args.web_root_url,
darktheme=False,
) )
return _args return _args

View File

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

View File

@@ -19,7 +19,7 @@ def extract_colorscheme(theme_path: str) -> dict[str, str]:
dictionary containing color scheme variables and their hexadecimal values. dictionary containing color scheme variables and their hexadecimal values.
""" """
logger.info("extracting color scheme from theme file", extra={"theme_path": theme_path}) logger.info("extracting color scheme from theme file", extra={"theme_path": theme_path})
pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);" pattern = r"--(color\d+|bcolor\d+):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);"
colorscheme = {} colorscheme = {}
with open(theme_path, "r", encoding="utf-8") as f: with open(theme_path, "r", encoding="utf-8") as f:
@@ -95,7 +95,8 @@ def css_color_to_hex(css_color: str) -> str:
# Helper function to convert HSL tuple to RGB tuple # Helper function to convert HSL tuple to RGB tuple
def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]: def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]:
logger.debug("converting hsl tuple to rgb tuple", extra={"hsl": hsl}) logger.debug("converting hsl tuple to rgb tuple", extra={"hsl": hsl})
return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100)) r, g, b = colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100)
return (round(r * 255), round(g * 255), round(b * 255))
# Regular expression pattern to match CSS colors # Regular expression pattern to match CSS colors
color_pattern = re.compile( color_pattern = re.compile(

View File

@@ -0,0 +1,158 @@
from dataclasses import dataclass
from typing import List, Optional, Any, Dict, TypeVar, Callable, Type, cast
T = TypeVar("T")
def from_int(x: Any) -> int:
assert isinstance(x, int) and not isinstance(x, bool)
return x
def from_str(x: Any) -> str:
assert isinstance(x, str)
return x
def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
assert isinstance(x, list)
return [f(y) for y in x]
def from_none(x: Any) -> Any:
assert x is None
return x
def from_union(fs, x):
for f in fs:
try:
return f(x)
except AssertionError:
pass
assert False
def to_class(c: Type[T], x: Any) -> dict:
assert isinstance(x, c)
return cast(Any, x).to_dict()
def from_dict(f: Callable[[Any], T], x: Any) -> Dict[str, T]:
assert isinstance(x, dict)
return {k: f(v) for (k, v) in x.items()}
def from_native_dict(f: Callable[[Any], T], x: Any) -> Dict[Any, T]:
assert isinstance(x, dict)
return x
@dataclass
class ImageMetadata:
w: Optional[int]
h: Optional[int]
tags: Optional[List[str]]
exifdata: Optional[Dict[str, Any]]
xmp: Optional[Dict[str, Any]]
src: str
msrc: str
name: str
title: str
tiff: Optional[str] = None
raw: Optional[str] = None
@staticmethod
def from_dict(obj: Any) -> "ImageMetadata":
assert isinstance(obj, dict)
w = from_union([from_int, from_none], obj.get("w"))
h = from_union([from_int, from_none], obj.get("h"))
tags = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tags"))
exifdata = from_union([lambda x: from_native_dict(dict, x), from_none], obj.get("exifdata"))
xmp = from_union([lambda x: from_native_dict(dict, x), from_none], obj.get("xmp"))
src = from_str(obj.get("src"))
msrc = from_str(obj.get("msrc"))
name = from_str(obj.get("name"))
title = from_str(obj.get("title"))
tiff = from_union([from_str, from_none], obj.get("tiff"))
raw = from_union([from_str, from_none], obj.get("raw"))
return ImageMetadata(w, h, tags, exifdata, xmp, src, msrc, name, title, tiff, raw)
def to_dict(self) -> dict:
result: dict = {}
if self.w is not None:
result["w"] = from_union([from_int, from_none], self.w)
if self.h is not None:
result["h"] = from_union([from_int, from_none], self.h)
if self.tags is not None:
result["tags"] = from_union([lambda x: from_list(from_str, x), from_none], self.tags)
result["src"] = from_str(self.src)
result["msrc"] = from_str(self.msrc)
result["name"] = from_str(self.name)
result["title"] = from_str(self.title)
if self.tiff is not None:
result["tiff"] = from_union([from_str, from_none], self.tiff)
if self.raw is not None:
result["raw"] = from_union([from_str, from_none], self.raw)
if self.exifdata is not None:
result["exifdata"] = from_union([lambda x: from_native_dict(dict, x), from_none], self.exifdata)
if self.xmp is not None:
result["xmp"] = from_union([lambda x: from_native_dict(dict, x), from_none], self.xmp)
return result
@dataclass
class SubfolderMetadata:
url: str
name: str
metadata: Optional[str] = None
thumb: Optional[str] = None
@staticmethod
def from_dict(obj: Any) -> "SubfolderMetadata":
assert isinstance(obj, dict)
url = from_str(obj.get("url"))
name = from_str(obj.get("name"))
metadata = from_union([from_none, from_str], obj.get("metadata"))
thumb = from_union([from_none, from_str], obj.get("thumb"))
return SubfolderMetadata(url, name, metadata, thumb)
def to_dict(self) -> dict:
result: dict = {}
result["url"] = from_str(self.url)
result["name"] = from_str(self.name)
result["metadata"] = from_union([from_none, from_str], self.metadata)
result["thumb"] = from_union([from_none, from_str], self.thumb)
return result
@dataclass
class Metadata:
images: Dict[str, ImageMetadata]
subfolders: Optional[List[SubfolderMetadata]] = None
@staticmethod
def from_dict(obj: Any) -> "Metadata":
assert isinstance(obj, dict)
images = from_dict(ImageMetadata.from_dict, obj.get("images"))
subfolders = from_union([lambda x: from_list(SubfolderMetadata.from_dict, x), from_none], obj.get("subfolders"))
return Metadata(images, subfolders)
def to_dict(self) -> dict:
result: dict = {}
result["images"] = from_dict(lambda x: to_class(ImageMetadata, x), self.images)
if self.subfolders is not None:
result["subfolders"] = from_union([lambda x: from_list(lambda x: to_class(SubfolderMetadata, x), x), from_none], self.subfolders)
return result
def sort(self, reverse=False) -> "Metadata":
self.images = {key: self.images[key] for key in sorted(self.images, reverse=reverse)}
return self
def top_level_from_dict(s: Any) -> Metadata:
return Metadata.from_dict(s)
def top_level_to_dict(x: Metadata) -> Any:
return to_class(Metadata, x)

View File

@@ -17,13 +17,10 @@ from bs4 import BeautifulSoup
from modules.logger import logger from modules.logger import logger
from modules import cclicense from modules import cclicense
from modules.argumentparser import Args from modules.argumentparser import Args
from modules.datatypes.metadata import Metadata, ImageMetadata, SubfolderMetadata
# Constants for file paths and exclusions # Constants for file paths and exclusions
if __package__ is None: SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
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"]
@@ -73,7 +70,7 @@ def getxmp(strbuffer: str) -> dict[str, Any]:
return {get_name(root.tag): get_value(root)} return {get_name(root.tag): get_value(root)}
def initialize_metadata(folder: str) -> dict[str, dict[str, int]]: def initialize_metadata(folder: str) -> Metadata:
""" """
Initializes the metadata JSON file if it doesn't exist. Initializes the metadata JSON file if it doesn't exist.
@@ -128,10 +125,10 @@ def initialize_metadata(folder: str) -> dict[str, dict[str, int]]:
if "title" not in v: if "title" not in v:
metadata["images"][k]["title"] = v["name"] metadata["images"][k]["title"] = v["name"]
return metadata return Metadata.from_dict(metadata)
def update_metadata(metadata: dict[str, dict[str, Any]], folder: str) -> None: def update_metadata(metadata: Metadata, folder: str) -> None:
""" """
Updates the metadata JSON file. Updates the metadata JSON file.
@@ -141,16 +138,21 @@ def update_metadata(metadata: dict[str, dict[str, Any]], folder: str) -> None:
""" """
metadata_path = os.path.join(folder, ".metadata.json") metadata_path = os.path.join(folder, ".metadata.json")
if metadata: if metadata:
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: 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))
metadatafile.write(json.dumps(metadata, 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})
os.remove(metadata_path) os.remove(metadata_path)
def get_image_info(item: str, folder: str) -> dict[str, Any]: def get_image_info(item: str, folder: str) -> ImageMetadata:
""" """
Extracts image information and EXIF data. Extracts image information and EXIF data.
@@ -172,7 +174,7 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
except UnidentifiedImageError: except UnidentifiedImageError:
logger.error("cannot identify image file", extra={"file": file}) logger.error("cannot identify image file", extra={"file": file})
print(f"cannot identify image file: {file}") print(f"cannot identify image file: {file}")
return {"w": None, "h": None, "tags": None, "exifdata": None, "xmp": None} return ImageMetadata(w=None, h=None, tags=[], exifdata=None, xmp=None, src="", msrc="", name="", title="")
if exif: if exif:
logger.info("extracting EXIF data", extra={"file": file}) logger.info("extracting EXIF data", extra={"file": file})
ifd = exif.get_ifd(ExifTags.IFD.Exif) ifd = exif.get_ifd(ExifTags.IFD.Exif)
@@ -253,16 +255,18 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
tags = get_tags(sidecarfile) tags = get_tags(sidecarfile)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
if None in tags: if None in tags: # type: ignore
tags.remove(None) tags.remove(None) # type: ignore
return {"w": width, "h": height, "tags": tags, "exifdata": exifdata, "xmp": xmp} if not isinstance(tags, list):
tags = []
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]
@@ -331,12 +335,12 @@ def get_tags(sidecarfile: str) -> list[str]:
pass pass
except KeyError: except KeyError:
pass pass
if None in tags: if None in tags: # type: ignore
tags.remove(None) tags.remove(None) # type: ignore
return tags return tags # type: ignore
def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: dict[str, dict[str, int]], raw: list[str]) -> dict[str, Any]: def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: Metadata, raw: list[str]) -> tuple[ImageMetadata, Metadata]:
""" """
Processes an image and prepares its data for the HTML template. Processes an image and prepares its data for the HTML template.
@@ -353,24 +357,21 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: d
""" """
extsplit = os.path.splitext(item) extsplit = os.path.splitext(item)
sidecarfile = os.path.join(folder, item + ".xmp") sidecarfile = os.path.join(folder, item + ".xmp")
if item not in metadata["images"] or _args.reread_metadata: if item not in metadata.images or _args.reread_metadata:
metadata["images"][item] = get_image_info(item, folder) metadata.images[item] = get_image_info(item, folder)
if _args.reread_sidecar and os.path.exists(sidecarfile): if _args.reread_sidecar and os.path.exists(sidecarfile):
logger.info("xmp sidecar file found", extra={"file": sidecarfile}) logger.info("xmp sidecar file found", extra={"file": sidecarfile})
try: try:
metadata["images"][item]["tags"] = get_tags(sidecarfile) metadata.images[item].tags = get_tags(sidecarfile)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
image = { image = metadata.images[item]
"src": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}", image.src = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}"
"msrc": f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg", image.msrc = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg"
"name": item, image.name = item
"w": metadata["images"][item]["w"], image.title = item
"h": metadata["images"][item]["h"],
"tags": metadata["images"][item]["tags"],
"title": item,
}
path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg") path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg")
if not os.path.exists(path) or _args.regenerate_thumbnails: if not os.path.exists(path) or _args.regenerate_thumbnails:
if os.path.exists(path): if os.path.exists(path):
@@ -382,18 +383,16 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: d
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].update(image) metadata.images[item] = image
return image, metadata return image, metadata
def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[str]: def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> set[str]:
""" """
Generates HTML content for a folder of images. Generates HTML content for a folder of images.
@@ -412,22 +411,22 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
items = sorted(os.listdir(folder)) items = sorted(os.listdir(folder))
contains_files = False contains_files = False
images = [] images: list[ImageMetadata] = []
subfolders = [] subfolders: list[SubfolderMetadata] = []
subfoldertags = set() subfoldertags: set[str] = set()
foldername = folder.removeprefix(_args.root_directory) foldername = folder.removeprefix(_args.root_directory)
foldername = f"{foldername}/" if foldername else "" foldername = f"{foldername}/" if foldername else ""
baseurl = urllib.parse.quote(foldername) baseurl = urllib.parse.quote(foldername)
gone = [item for item in metadata["images"] if item not in items] gone = [item for item in metadata.images if item not in items]
for gon in gone: for gon in gone:
del metadata["images"][gon] del metadata.images[gon]
create_thumbnail_folder(foldername, _args.root_directory) create_thumbnail_folder(foldername, _args.root_directory)
logger.info("processing contents", extra={"folder": folder}) logger.info("processing contents", extra={"folder": folder})
if not _args.non_interactive_mode: if not _args.non_interactive_mode:
for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True): for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True, leave=False):
if item not in EXCLUDES and not item.startswith(".") and os.path.splitext(item)[1][1:].lower() not in _args.ignore_extensions: if item not in EXCLUDES and not item.startswith(".") and os.path.splitext(item)[1][1:].lower() not in _args.ignore_extensions:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
subfoldertags.update(process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo)) subfoldertags.update(process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo))
@@ -455,11 +454,11 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
if item == "LICENSE": if item == "LICENSE":
process_license(folder, item) process_license(folder, item)
metadata["subfolders"] = subfolders metadata.subfolders = subfolders
if _args.reverse_sort: if _args.reverse_sort:
metadata["images"] = {key: metadata["images"][key] for key in sorted(metadata["images"], reverse=True)} metadata.sort(reverse=True)
else: else:
metadata["images"] = {key: metadata["images"][key] for key in sorted(metadata["images"])} metadata.sort()
update_metadata(metadata, folder) update_metadata(metadata, folder)
if should_generate_html(images, contains_files, _args): if should_generate_html(images, contains_files, _args):
@@ -485,7 +484,7 @@ def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
os.mkdir(thumbnails_path) os.mkdir(thumbnails_path)
def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dict[str, str | None]], _args: Args, raw: list[str], version: str, logo: str) -> list[str]: def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[SubfolderMetadata], _args: Args, raw: list[str], version: str, logo: str) -> set[str]:
""" """
Processes a subfolder. Processes a subfolder.
@@ -513,10 +512,10 @@ def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dic
if item not in _args.exclude_folders: if item not in _args.exclude_folders:
if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders): if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/.metadata.json"}) subfolders.append(SubfolderMetadata(url=subfolder_url, name=item, thumb=thumb, metadata=f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/.metadata.json"))
return generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo) return generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo)
subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": None}) subfolders.append(SubfolderMetadata(url=subfolder_url, name=item, thumb=thumb))
return [] return set()
def process_license(folder: str, item: str) -> None: def process_license(folder: str, item: str) -> None:
@@ -548,7 +547,7 @@ def process_info_file(folder: str, item: str) -> None:
info[urllib.parse.quote(folder)] = f.read() info[urllib.parse.quote(folder)] = f.read()
def should_generate_html(images: list[dict[str, Any]], contains_files, _args: Args) -> bool: def should_generate_html(images: list[ImageMetadata], contains_files, _args: Args) -> bool:
""" """
Determines if HTML should be generated. Determines if HTML should be generated.
@@ -559,7 +558,7 @@ def should_generate_html(images: list[dict[str, Any]], contains_files, _args: Ar
Returns: Returns:
bool: True if HTML should be generated, False otherwise. bool: True if HTML should be generated, False otherwise.
""" """
return images or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files) return bool(images) or (_args.use_fancy_folders and not contains_files) or (_args.use_fancy_folders and _args.ignore_other_files)
def format_html(html: str) -> str: def format_html(html: str) -> str:
@@ -568,8 +567,8 @@ def format_html(html: str) -> str:
def create_html_file( def create_html_file(
folder: str, title: str, foldername: str, images: list[dict[str, Any]], subfolders: list[dict[str, str]], _args: Args, version: str, logo: str, subfoldertags: list[str] folder: str, title: str, foldername: str, images: list[ImageMetadata], subfolders: list[SubfolderMetadata], _args: Args, version: str, logo: str, subfoldertags: set[str]
) -> list[str]: ) -> set[str]:
""" """
Creates the HTML file using the template. Creates the HTML file using the template.
@@ -602,15 +601,13 @@ def create_html_file(
alltags = set() alltags = set()
for img in images: for img in images:
if img["tags"]: if img.tags:
alltags.update(img["tags"]) alltags.update(img.tags)
alltags.update(set(subfoldertags)) alltags.update(subfoldertags)
folder_info = info.get(urllib.parse.quote(folder), "").split("\n") folder_info = info.get(urllib.parse.quote(folder), "").split("\n")
_info = [i for i in folder_info if len(i) > 1] if folder_info else None _info = [i for i in folder_info if len(i) > 1] if folder_info else None
if _args.reverse_sort:
images.sort(key=lambda i: i["name"], reverse=True)
folder_license = licens.get(urllib.parse.quote(folder), False) folder_license = licens.get(urllib.parse.quote(folder), False)
@@ -627,6 +624,7 @@ def create_html_file(
favicon=f"{_args.web_root_url}{FAVICON_PATH}", favicon=f"{_args.web_root_url}{FAVICON_PATH}",
stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}", stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}",
theme=f"{_args.web_root_url}.static/theme.css", theme=f"{_args.web_root_url}.static/theme.css",
darktheme=f"{_args.web_root_url}.static/theme-dark.css" if _args.darktheme else None,
root=_args.web_root_url, root=_args.web_root_url,
parent=f"{_args.web_root_url}{urllib.parse.quote(foldername)}", parent=f"{_args.web_root_url}{urllib.parse.quote(foldername)}",
header=f"{header} - LICENSE", header=f"{header} - LICENSE",
@@ -644,6 +642,7 @@ def create_html_file(
favicon=f"{_args.web_root_url}{FAVICON_PATH}", favicon=f"{_args.web_root_url}{FAVICON_PATH}",
stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}", stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}",
theme=f"{_args.web_root_url}.static/theme.css", theme=f"{_args.web_root_url}.static/theme.css",
darktheme=f"{_args.web_root_url}.static/theme-dark.css" if _args.darktheme else None,
root=_args.web_root_url, root=_args.web_root_url,
parent=parent, parent=parent,
header=header, header=header,
@@ -661,7 +660,7 @@ def create_html_file(
logger.info("writing formatted html file", extra={"path": html_file}) logger.info("writing formatted html file", extra={"path": html_file})
f.write(format_html(content)) f.write(format_html(content))
return sorted(alltags) return set(sorted(alltags))
def list_folder(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str, str]]: def list_folder(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str, str]]:

View File

@@ -21,11 +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
if __package__ is None: SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
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,14 +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
if __package__ is None: SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "")
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
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"]
@@ -30,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
@@ -72,8 +70,9 @@ 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]
cairosvg.svg2png(bytestring=content, write_to=tmpimg) # pyright: ignore[reportPossiblyUnboundVariable]
with Image.open(tmpimg) as iconfile: with Image.open(tmpimg) as iconfile:
logger.info("saving png icon", extra={"iconspath": iconspath}) logger.info("saving png icon", extra={"iconspath": iconspath})
iconfile.save(os.path.join(iconspath, "icon.png")) iconfile.save(os.path.join(iconspath, "icon.png"))
@@ -152,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")})
@@ -180,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
@@ -240,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
@@ -258,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!")
@@ -266,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

@@ -1,12 +1,12 @@
beautifulsoup4~=4.13.4 beautifulsoup4~=4.14.3
CairoSVG~=2.7.1 CairoSVG~=2.7.1
ConfigArgParse~=1.7.1
defusedxml~=0.7.1 defusedxml~=0.7.1
html5lib~=1.1 html5lib~=1.1
Jinja2~=3.1.6 Jinja2~=3.1.6
jsmin~=3.0.1 jsmin~=3.0.1
Pillow~=11.3.0 Pillow~=12.1.0
pyinstaller~=6.11.1
python_json_logger~=2.0.7 python_json_logger~=2.0.7
rich_argparse~=1.7.1 rich_argparse~=1.7.2
selenium~=4.34.2 selenium~=4.40.0
tqdm~=4.66.4 tqdm~=4.66.6

111
templates/default-dark.css Normal file
View File

@@ -0,0 +1,111 @@
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
* {
--color1: #262a2b;
--color2: #0d0e0e;
--color3: #313537;
--color4: #181a1b;
--color5: #5483ef;
--bcolor1: #e8e6e3;
--bcolor2: #0c0d0e;
}
.navbar {
font-weight: bold;
color: var(--bcolor1);
background-color: var(--color1);
}
.navbar li a {
font-weight: bold;
color: var(--bcolor1);
}
/* Change the link color on hover */
.navbar li a:hover {
background-color: var(--color2);
}
.footer {
color: var(--bcolor1);
background-color: var(--color3);
font-weight: 500;
}
.footer a {
color: var(--color5);
text-decoration: none;
}
.foldericon {
content: "themes/icons/folder-2.svg.j2";
}
.folders a {
font-weight: 700;
color: var(--color5);
text-decoration: none;
}
.tooltiptext {
font-weight: 400;
background-color: var(--color3);
}
.tagentry label:hover {
background-color: var(--color4);
}
.tagentry .tagtoggle:hover {
background-color: var(--color4);
}
.column img {
background-color: var(--bcolor2);
}
#totop:hover {
background-color: var(--color2);
}
#totop {
background-color: var(--color1);
color: var(--bcolor1);
font-weight: 800;
}
.loader {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
border-top: 3px solid var(--bcolor1);
border-right: 3px solid transparent;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
body {
color: var(--bcolor1);
background-color: var(--color4);
font-family: "Ubuntu", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
}
body a {
font-weight: 400;
color: var(--color5);
text-decoration: none;
}

View File

@@ -4,30 +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.darkMode = this.darkMode.bind(this);
this.darkModeToggle = this.darkModeToggle.bind(this);
this.debounce = this.debounce.bind(this); this.debounce = this.debounce.bind(this);
this.openSwipe = this.openSwipe.bind(this); this.detectDarkMode = this.detectDarkMode.bind(this);
this.prefetch = this.prefetch.bind(this);
this.cancel = this.cancel.bind(this); this.detectDarkMode();
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.filter = this.filter.bind(this);
this.updateImageList = this.updateImageList.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.setFilter = this.setFilter.bind(this);
this.toggleTag = this.toggleTag.bind(this); this.setupClickHandlers = this.setupClickHandlers.bind(this);
this.setupDropdownToggle = this.setupDropdownToggle.bind(this); this.setupDropdownToggle = this.setupDropdownToggle.bind(this);
this.setupTagHandlers = this.setupTagHandlers.bind(this); this.setupTagHandlers = this.setupTagHandlers.bind(this);
this.setupClickHandlers = this.setupClickHandlers.bind(this); this.showLoader = this.showLoader.bind(this);
this.scrollFunction = this.scrollFunction.bind(this); this.toggleTag = this.toggleTag.bind(this);
this.topFunction = this.topFunction.bind(this); this.topFunction = this.topFunction.bind(this);
this.onLoad = this.onLoad.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) => {
@@ -36,60 +79,167 @@ class PhotoGallery {
}; };
} }
openSwipe(imgIndex) { detectDarkMode() {
const options = { index: imgIndex }; if (document.getElementById("darktheme")) {
const gallery = new PhotoSwipe( const switchState = document.getElementById("dark-mode-switch-check");
this.pswpElement, const localStorageTheme = localStorage.getItem("theme");
PhotoSwipeUI_Default, if (localStorageTheme === "dark") {
this.shown, switchState.checked = true;
options this.darkModeToggle("dark");
); return;
gallery.init(); } else if (localStorageTheme === "light") {
switchState.checked = true;
this.darkModeToggle("light");
return;
} }
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
prefetch(imgIndex) { switchState.checked = true;
if (this.controllers[imgIndex]) { this.darkModeToggle("dark");
this.cancel(imgIndex); } else {
switchState.checked = false;
this.darkModeToggle("light");
} }
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(() => {});
} }
} }
cancel(imgIndex) { filter() {
if (this.controllers[imgIndex]) { this.showLoader();
this.controllers[imgIndex].abort(); const searchParams = new URLSearchParams(window.location.search);
delete this.controllers[imgIndex]; this.shown = [];
} let path = decodeURIComponent(window.location.origin + window.location.pathname.replace("index.html", ""));
}
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")) { if (path.startsWith("null")) {
path = window.location.protocol + "//" + path.substring(4); path = window.location.protocol + "//" + path.substring(4);
} }
const selectedTags = [];
if (folders) folders.style.display = ""; document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => {
document.getElementById("recursive").checked = false; let tag = checkbox.parentElement.id.trim().substring(1);
document if (checkbox.parentElement.parentElement.children.length > 1) tag += "|";
.querySelectorAll("#tagdropdown input.tagcheckbox:checked") selectedTags.push(tag);
.forEach((checkbox) => (checkbox.checked = false)); });
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);
}
} }
showLoader() { finalize(obj) {
const imagelist = document.getElementById("imagelist"); if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
imagelist.innerHTML = '<span class="loader"></span>'; const result = {};
imagelist.classList.add("centerload"); Object.keys(obj)
imagelist.classList.remove("row"); .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) {
const options = { index: imgIndex };
const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options);
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) {
const prefetchDiv = document.getElementById("img-prefetch");
if (!prefetchDiv) return;
const img = document.createElement("img");
img.src = this.shown[imgIndex]?.src || "";
prefetchDiv.appendChild(img);
}
prefetchCancel() {
const prefetchDiv = document.getElementById("img-prefetch");
if (!prefetchDiv) return;
if (prefetchDiv.firstChild) {
prefetchDiv.removeChild(prefetchDiv.firstChild);
}
} }
async recursive() { async recursive() {
@@ -108,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);
@@ -150,19 +301,35 @@ class PhotoGallery {
existingItems.add(image.src); existingItems.add(image.src);
} }
} }
if (Array.isArray(data.subfolders)) if (Array.isArray(data.subfolders)) nextLevel.push(...data.subfolders);
nextLevel.push(...data.subfolders);
} catch {} } catch {}
}) }),
); );
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;
@@ -191,168 +358,95 @@ 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( const folders = document.querySelector(".folders");
window.location.origin + let path = window.location.origin + window.location.pathname;
window.location.pathname.replace("index.html", "")
);
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 if (folders) folders.style.display = "";
.querySelectorAll("#tagdropdown input.tagcheckbox:checked") document.getElementById("recursive").checked = false;
.forEach((checkbox) => { document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false));
let tag = checkbox.parentElement.id.trim().substring(1); window.history.replaceState({ html: content, pageTitle: title }, "", path);
if (checkbox.parentElement.parentElement.children.length > 1) this.requestMetadata();
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);
} }
resetHoverTimer(index) {
if (this.hoverTimer) {
clearTimeout(this.hoverTimer);
}
this.prefetchCancel();
this.hoverTimer = setTimeout(() => {
this.prefetch(index);
}, 500);
}
scrollFunction() {
const totopbutton = document.getElementById("totop");
if (!totopbutton) return;
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
totopbutton.style.display = "block";
} else { } else {
this.shown.push(item); totopbutton.style.display = "none";
} }
} }
}
this.updateImageList();
window.location.hash = urltags;
const pid = searchParams.get("pid") - 1;
if (pid != -1) {
this.openSwipe(pid);
}
}
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];
}
}
}
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 || [];
}
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) {
document document.querySelectorAll("#tagdropdown input.tagcheckbox").forEach((checkbox) => {
.querySelectorAll("#tagdropdown input.tagcheckbox")
.forEach((checkbox) => {
selected.forEach((tag) => { selected.forEach((tag) => {
if ( if (checkbox.parentElement.id.trim().substring(1).replace(" ", "%20") === tag) {
checkbox.parentElement.id
.trim()
.substring(1)
.replace(" ", "%20") === tag
) {
checkbox.checked = true; checkbox.checked = true;
} }
}); });
}); });
} }
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)" const totop = document.getElementById("totop");
: "rotate(0deg)"; 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() {
@@ -364,18 +458,12 @@ class PhotoGallery {
event.stopPropagation(); event.stopPropagation();
const svg = toggleLink.querySelector("svg"); const svg = toggleLink.querySelector("svg");
dropdown.classList.toggle("show"); dropdown.classList.toggle("show");
if (svg) if (svg) svg.style.transform = dropdown.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
svg.style.transform = dropdown.classList.contains("show")
? "rotate(180deg)"
: "rotate(0deg)";
this.tagDropdownShown = dropdown.classList.contains("show"); this.tagDropdownShown = dropdown.classList.contains("show");
}); });
document.addEventListener("click", (event) => { document.addEventListener("click", (event) => {
if ( if (!dropdown.contains(event.target) && !toggleLink.contains(event.target)) {
!dropdown.contains(event.target) &&
!toggleLink.contains(event.target)
) {
dropdown.classList.remove("show"); dropdown.classList.remove("show");
this.tagDropdownShown = false; this.tagDropdownShown = false;
const svg = toggleLink.querySelector("svg"); const svg = toggleLink.querySelector("svg");
@@ -401,76 +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 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 ( const svg = tag?.parentElement.querySelector(".tagtoggle svg");
document.body.scrollTop > 20 || if (!ol || !svg) return;
document.documentElement.scrollTop > 20 ol.classList.toggle("show");
) { svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
totopbutton.style.display = "block";
} else {
totopbutton.style.display = "none";
}
} }
topFunction() { topFunction() {
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
onLoad() { updateImageList() {
document.querySelectorAll(".tagtoggle").forEach((toggle) => { this.showLoader();
toggle.addEventListener("mouseup", (event) => { const imagelist = document.getElementById("imagelist");
event.stopPropagation(); if (!imagelist) return;
const tagid = toggle.getAttribute("data-tagid"); let str = "";
this.toggleTag(tagid); this.shown.sort((a, b) => a.src.replace(a.name, "").localeCompare(b.src.replace(b.name, "")));
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");
this.requestMetadata(); imagelist.innerHTML = str;
this.setupDropdownToggle();
this.setupTagHandlers();
this.setupClickHandlers();
window.addEventListener("scroll", this.scrollFunction);
} }
init() { init() {

View File

@@ -34,10 +34,16 @@
{%- if theme %} {%- if theme %}
<link rel="preload" href="{{ theme }}" as="style"> <link rel="preload" href="{{ theme }}" as="style">
{%- endif %} {%- endif %}
{%- if darktheme %}
<link rel="preload" href="{{ darktheme }}" as="style">
{%- endif %}
<link rel="icon" type="image/x-icon" href="{{ favicon }}"> <link rel="icon" type="image/x-icon" href="{{ favicon }}">
<link rel="stylesheet" href="{{ stylesheet }}"> <link rel="stylesheet" href="{{ stylesheet }}">
{%- if theme %} {%- if theme %}
<link rel="stylesheet" href="{{ theme }}"> <link rel="stylesheet" href="{{ theme }}" id="theme">
{%- endif %}
{%- if darktheme %}
<link rel="stylesheet" href="{{ darktheme }}" id="darktheme" disabled>
{%- endif %} {%- endif %}
<link rel="preload" href="{{ root }}.static/pswp/photoswipe.css" as="style"> <link rel="preload" href="{{ root }}.static/pswp/photoswipe.css" as="style">
<link rel="preload" href="{{ root }}.static/pswp/default-skin/default-skin.css" as="style"> <link rel="preload" href="{{ root }}.static/pswp/default-skin/default-skin.css" as="style">
@@ -89,10 +95,22 @@
{{ render_tags(tags, '') }} {{ render_tags(tags, '') }}
</ol> </ol>
</li> </li>
{% endif %} {%- endif %}
{%- if licensefile %} {%- if licensefile %}
<li class="license"><a href="{{ licensefile }}">License</a></li> <li class="license"><a href="{{ licensefile }}">License</a></li>
{%- endif %} {%- endif %}
{%- if darktheme %}
<li class="darkmodeswitch">
<a id="dark-mode-switch">
<input type="checkbox" id="dark-mode-switch-check" />
<div class="knobs">
<span class="light">☀︎</span>
<span class="slider"></span>
<span class="dark">☽</span>
</div>
</a>
</li>
{%- endif %}
</div> </div>
</ol> </ol>
{% if subdirectories %} {% if subdirectories %}
@@ -120,7 +138,7 @@
<a property="dct:title" rel="cc:attributionURL" href="{{ root }}">{{ license.project }}</a> by <span property="cc:attributionName">{{ license.author }}</span> is marked with <a property="dct:title" rel="cc:attributionURL" href="{{ root }}">{{ license.project }}</a> by <span property="cc:attributionName">{{ license.author }}</span> is marked with
<a href="{{ license.url }}" target="_blank" rel="license noopener noreferrer" style="display: inline-block">CC0 1.0 <a href="{{ license.url }}" target="_blank" rel="license noopener noreferrer" style="display: inline-block">CC0 1.0
{%- for pic in license.pics %} {%- for pic in license.pics %}
<img style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom" src="{{ pic }}" alt="" /> <img src="{{ pic }}" alt="" />
{%- endfor %} {%- endfor %}
</a> </a>
{%- else %} {%- else %}
@@ -179,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: 2d2f96abb3...e5f2b0cd98