6 Commits

13 changed files with 371 additions and 142 deletions

View File

@@ -9,6 +9,8 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.12' 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

@@ -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 = [
@@ -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:
""" """
@@ -201,7 +218,7 @@ def main(args) -> None:
shutil.rmtree(thumbdir) shutil.rmtree(thumbdir)
os.makedirs(thumbdir, 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:

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;
} }
@@ -140,16 +136,19 @@ figure {
.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 {
@@ -303,6 +302,59 @@ input {
border-style: none; border-style: none;
} }
.darkmodeswitch {
font-size: smaller;
height: 100%;
}
#dark-mode-switch {
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
position: relative;
}
#dark-mode-switch .checkbox {
position: absolute;
width: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
z-index: 2;
}
#dark-mode-switch .knobs {
display: flex;
align-items: center;
justify-content: center;
column-gap: 1em;
position: relative;
width: 100%;
}
#dark-mode-switch .light,
#dark-mode-switch .dark {
text-align: center;
}
#dark-mode-switch .slider {
position: absolute;
width: calc(2em - 2px);
height: calc(2em - 2px);
border: 1px solid currentColor;
border-radius: 3px;
top: 50%;
transform: translate(-50%, -50%);
transition: left 0.25s ease, transform 0.25s ease;
left: calc(50% - 1em + 1px);
}
#dark-mode-switch .checkbox:checked+.knobs .slider {
left: calc(50% + 1em - 1px);
}
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
.column { .column {
-ms-flex: 25%; -ms-flex: 25%;
@@ -310,6 +362,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 +403,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 +452,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;
} }

View File

@@ -12,11 +12,7 @@ except ModuleNotFoundError:
RICH = False RICH = False
if __package__ is None: SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__)
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"
@@ -64,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
@@ -84,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 = {}
@@ -106,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
@@ -176,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

@@ -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:

View File

@@ -20,11 +20,7 @@ from modules.argumentparser import Args
from modules.datatypes.metadata import Metadata, ImageMetadata, SubfolderMetadata from modules.datatypes.metadata import Metadata, ImageMetadata, SubfolderMetadata
# Constants for file paths and exclusions # Constants for file paths and exclusions
if __package__ is None: SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__)
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"]
@@ -625,6 +621,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",
@@ -642,6 +639,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,

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__)
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

@@ -18,11 +18,7 @@ from modules.argumentparser import Args
from modules.css_color import extract_theme_color, extract_colorscheme from modules.css_color import extract_theme_color, 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__)
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"]

View File

@@ -1,13 +1,12 @@
beautifulsoup4~=4.13.4 beautifulsoup4~=4.14.3
CairoSVG~=2.7.1 CairoSVG~=2.7.1
ConfigArgParse~=1.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

@@ -24,6 +24,10 @@ class PhotoGallery {
this.scrollFunction = this.scrollFunction.bind(this); this.scrollFunction = this.scrollFunction.bind(this);
this.topFunction = this.topFunction.bind(this); this.topFunction = this.topFunction.bind(this);
this.onLoad = this.onLoad.bind(this); this.onLoad = this.onLoad.bind(this);
this.darkMode = this.darkMode.bind(this);
this.lightMode = this.lightMode.bind(this);
this.darkModeToggle = this.darkModeToggle.bind(this);
this.detectDarkMode = this.detectDarkMode.bind(this);
this.init(); this.init();
} }
@@ -38,12 +42,7 @@ class PhotoGallery {
openSwipe(imgIndex) { openSwipe(imgIndex) {
const options = { index: imgIndex }; const options = { index: imgIndex };
const gallery = new PhotoSwipe( const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options);
this.pswpElement,
PhotoSwipeUI_Default,
this.shown,
options
);
gallery.init(); gallery.init();
} }
@@ -78,9 +77,7 @@ class PhotoGallery {
if (folders) folders.style.display = ""; if (folders) folders.style.display = "";
document.getElementById("recursive").checked = false; document.getElementById("recursive").checked = false;
document document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false));
.querySelectorAll("#tagdropdown input.tagcheckbox:checked")
.forEach((checkbox) => (checkbox.checked = false));
window.history.replaceState({ html: content, pageTitle: title }, "", path); window.history.replaceState({ html: content, pageTitle: title }, "", path);
this.requestMetadata(); this.requestMetadata();
} }
@@ -150,10 +147,9 @@ 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);
}; };
@@ -194,39 +190,30 @@ class PhotoGallery {
filter() { filter() {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
this.shown = []; this.shown = [];
let path = decodeURIComponent( let path = decodeURIComponent(window.location.origin + window.location.pathname.replace("index.html", ""));
window.location.origin +
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 = []; const selectedTags = [];
document document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => {
.querySelectorAll("#tagdropdown input.tagcheckbox:checked") let tag = checkbox.parentElement.id.trim().substring(1);
.forEach((checkbox) => { if (checkbox.parentElement.parentElement.children.length > 1) tag += "|";
let tag = checkbox.parentElement.id.trim().substring(1); selectedTags.push(tag);
if (checkbox.parentElement.parentElement.children.length > 1) });
tag += "|";
selectedTags.push(tag);
});
const urltags = selectedTags.join(","); const urltags = selectedTags.join(",");
let isRecursiveChecked = false; let isRecursiveChecked = false;
try { try {
isRecursiveChecked = isRecursiveChecked = document.getElementById("recursive")?.checked || false;
document.getElementById("recursive")?.checked || false;
} catch {} } catch {}
for (const item of this.items) { for (const item of this.items) {
const tags = item.tags || []; const tags = item.tags || [];
const include = selectedTags.every((selected) => { const include = selectedTags.every((selected) => {
const isParent = selected.endsWith("|"); const isParent = selected.endsWith("|");
return isParent return isParent ? tags.some((t) => t.startsWith(selected)) : tags.includes(selected);
? tags.some((t) => t.startsWith(selected))
: tags.includes(selected);
}); });
if (include || selectedTags.length === 0) { if (include || selectedTags.length === 0) {
@@ -313,9 +300,7 @@ class PhotoGallery {
let str = ""; let str = "";
this.shown.forEach((item, index) => { this.shown.forEach((item, index) => {
let tags = this.parseHierarchicalTags(item.tags || []); let tags = this.parseHierarchicalTags(item.tags || []);
str += `<div class="column"><figure title="${this.renderTree( str += `<div class="column"><figure title="${this.renderTree(tags)}"><img src="${
tags
)}"><img src="${
item.msrc item.msrc
}" data-index="${index}" /><figcaption class="caption">${item.name}`; }" data-index="${index}" /><figcaption class="caption">${item.name}`;
if (item.tiff) str += `&nbsp;<a href="${item.tiff}">TIFF</a>`; if (item.tiff) str += `&nbsp;<a href="${item.tiff}">TIFF</a>`;
@@ -328,20 +313,13 @@ class PhotoGallery {
} }
setFilter(selected) { setFilter(selected) {
document document.querySelectorAll("#tagdropdown input.tagcheckbox").forEach((checkbox) => {
.querySelectorAll("#tagdropdown input.tagcheckbox") selected.forEach((tag) => {
.forEach((checkbox) => { if (checkbox.parentElement.id.trim().substring(1).replace(" ", "%20") === tag) {
selected.forEach((tag) => { checkbox.checked = true;
if ( }
checkbox.parentElement.id
.trim()
.substring(1)
.replace(" ", "%20") === tag
) {
checkbox.checked = true;
}
});
}); });
});
} }
toggleTag(tagid) { toggleTag(tagid) {
@@ -350,9 +328,7 @@ class PhotoGallery {
const svg = tag?.parentElement.querySelector(".tagtoggle svg"); const svg = tag?.parentElement.querySelector(".tagtoggle svg");
if (!ol || !svg) return; if (!ol || !svg) return;
ol.classList.toggle("show"); ol.classList.toggle("show");
svg.style.transform = ol.classList.contains("show") svg.style.transform = ol.classList.contains("show") ? "rotate(180deg)" : "rotate(0deg)";
? "rotate(180deg)"
: "rotate(0deg)";
} }
setupDropdownToggle() { setupDropdownToggle() {
@@ -364,18 +340,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");
@@ -402,18 +372,18 @@ class PhotoGallery {
} }
setupClickHandlers() { setupClickHandlers() {
const resetEl = document const resetEl = document.getElementById("reset-filter")?.querySelector("label");
.getElementById("reset-filter")
?.querySelector("label");
if (resetEl) resetEl.addEventListener("click", this.reset); if (resetEl) resetEl.addEventListener("click", this.reset);
const recurseEl = document.getElementById("recursive"); const recurseEl = document.getElementById("recursive");
if (recurseEl) if (recurseEl) recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
recurseEl.addEventListener("change", this.debounce(this.recursive, 150));
const totop = document.getElementById("totop"); const totop = document.getElementById("totop");
if (totop) totop.addEventListener("click", this.topFunction); if (totop) totop.addEventListener("click", this.topFunction);
const darkModeSwitch = document.getElementById("dark-mode-switch");
if (darkModeSwitch) darkModeSwitch.addEventListener("click", this.darkModeToggle);
const imagelist = document.getElementById("imagelist"); const imagelist = document.getElementById("imagelist");
if (imagelist) { if (imagelist) {
imagelist.addEventListener("click", (event) => { imagelist.addEventListener("click", (event) => {
@@ -442,10 +412,7 @@ class PhotoGallery {
scrollFunction() { scrollFunction() {
const totopbutton = document.getElementById("totop"); const totopbutton = document.getElementById("totop");
if (!totopbutton) return; if (!totopbutton) return;
if ( if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
document.body.scrollTop > 20 ||
document.documentElement.scrollTop > 20
) {
totopbutton.style.display = "block"; totopbutton.style.display = "block";
} else { } else {
totopbutton.style.display = "none"; totopbutton.style.display = "none";
@@ -456,6 +423,53 @@ class PhotoGallery {
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
darkMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
if (themeLink) themeLink.disabled = true;
if (darkThemeLink) darkThemeLink.disabled = false;
}
lightMode() {
const themeLink = document.getElementById("theme");
const darkThemeLink = document.getElementById("darktheme");
if (themeLink) themeLink.disabled = false;
if (darkThemeLink) darkThemeLink.disabled = true;
}
darkModeToggle(mode) {
const switchState = document.getElementById("dark-mode-switch-check");
if (mode == "dark") {
this.darkMode();
if (switchState) {
switchState.checked = true;
}
} else if (mode == "light") {
this.lightMode();
if (switchState) {
switchState.checked = false;
}
} else {
if (switchState.checked) {
switchState.checked = false;
this.lightMode();
} else {
switchState.checked = true;
this.darkMode();
}
}
}
detectDarkMode() {
if (document.getElementById("darktheme")) {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
this.darkModeToggle("dark");
} else {
this.darkModeToggle("light");
}
}
}
onLoad() { onLoad() {
document.querySelectorAll(".tagtoggle").forEach((toggle) => { document.querySelectorAll(".tagtoggle").forEach((toggle) => {
toggle.addEventListener("mouseup", (event) => { toggle.addEventListener("mouseup", (event) => {
@@ -469,6 +483,7 @@ class PhotoGallery {
this.setupDropdownToggle(); this.setupDropdownToggle();
this.setupTagHandlers(); this.setupTagHandlers();
this.setupClickHandlers(); this.setupClickHandlers();
this.detectDarkMode();
window.addEventListener("scroll", this.scrollFunction); window.addEventListener("scroll", this.scrollFunction);
} }

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 class="button" id="dark-mode-switch">
<input type="checkbox" class="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 %}

2
themes

Submodule themes updated: fa79867f4a...3bb36480e7