diff --git a/builder.py b/builder.py index 71dd311..3b8330f 100755 --- a/builder.py +++ b/builder.py @@ -18,11 +18,7 @@ from modules.argumentparser import parse_arguments, Args # fmt: off # Constants -if __package__ is None: - PACKAGE = "" -else: - PACKAGE = __package__ -SCRIPTDIR = os.path.dirname(os.path.realpath(__file__).removesuffix(PACKAGE)) +SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__ if __package__ else "") STATIC_FILES_DIR = os.path.join(os.path.abspath(SCRIPTDIR), "files") VERSION = open(os.path.join(SCRIPTDIR, ".version"), "r", encoding="utf-8").read() RAW_EXTENSIONS = [ @@ -75,7 +71,48 @@ def init_globals(_args: Args, raw: list[str]) -> tuple[Args, list[str]]: 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. @@ -85,6 +122,7 @@ def copy_static_files(_args: Args) -> None: Parsed command-line arguments. """ static_dir = os.path.join(_args.root_directory, ".static") + darktheme = False if os.path.exists(static_dir): print("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...") logger.info("copying static files") 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 = 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(_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) + + theme = os.path.splitext(os.path.abspath(_args.theme_path))[0] + darktheme_path = f"{theme}-dark.css" + if os.path.exists(darktheme_path): + handle_theme_icon(darktheme_path, os.path.join(static_dir, "theme-dark.css")) + darktheme = True + handle_theme_icon(_args.theme_path, os.path.join(static_dir, "theme.css")) + 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(static_dir, "functionality.min.js"), "w+", encoding="utf-8") as min_file: min_file.write(jsmin(js_file.read())) + return darktheme + def generate_thumbnail(arguments: tuple[str, str, str]) -> None: """ @@ -201,7 +218,7 @@ def main(args) -> None: shutil.rmtree(thumbdir) os.makedirs(thumbdir, exist_ok=True) - copy_static_files(args) + darktheme = copy_static_files(args) icons(args) if args.generate_webmanifest: @@ -211,13 +228,13 @@ def main(args) -> None: if args.non_interactive_mode: logger.info("generating HTML files") print("Generating HTML files...") - thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo) + thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo, darktheme=darktheme) with Pool(os.cpu_count()) as pool: logger.info("generating thumbnails") print("Generating thumbnails...") pool.map(generate_thumbnail, thumbnails) else: - thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo) + thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo, darktheme=darktheme) with Pool(os.cpu_count()) as pool: logger.info("generating thumbnails") diff --git a/files/global.css b/files/global.css index 2f04164..6bdb99d 100644 --- a/files/global.css +++ b/files/global.css @@ -77,10 +77,6 @@ figure { margin: 0; } -.licensefile { - padding: 30px; -} - .caption { padding-top: 4px; text-align: center; @@ -93,7 +89,6 @@ figure { position: absolute; bottom: 0; right: 0; - font-size: xx-small; padding: 6px; } @@ -102,7 +97,8 @@ figure { bottom: 0; width: 100%; padding: 6px; - min-height: calc(6.75pt + 12px); + height: calc(9.75pt + 12px); + font-size: small; } .footer a { @@ -110,7 +106,7 @@ figure { } .footer a img { - height: 22px !important; + height: 9.75pt !important; margin-left: 3px; vertical-align: text-bottom; } @@ -140,16 +136,19 @@ figure { .navbar .navleft { float: left; + height: 100%; } .navbar .navcenter { position: absolute; left: 50%; transform: translateX(-50%); + height: 100%; } .navbar .navright { - float: right + float: right; + height: 100%; } .navbar li .header { @@ -303,6 +302,54 @@ input { border-style: none; } +#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) { .column { -ms-flex: 25%; @@ -310,6 +357,14 @@ input { max-width: 25%; } + .footer { + font-size: small; + } + + .footer a img { + height: 9.75pt !important; + } + .folders figure { width: 160px; } @@ -343,6 +398,14 @@ input { max-width: 50%; } + .footer { + font-size: x-small; + } + + .footer a img { + height: 7.5pt !important; + } + .folders figure { width: 140px; } @@ -384,6 +447,14 @@ input { max-width: 100%; } + .footer { + font-size: xx-small; + } + + .footer a img { + height: 6.75pt !important; + } + .folders figure { width: 120px; } diff --git a/modules/argumentparser.py b/modules/argumentparser.py index 02d72e9..20e82d3 100644 --- a/modules/argumentparser.py +++ b/modules/argumentparser.py @@ -12,11 +12,7 @@ except ModuleNotFoundError: RICH = False -if __package__ is None: - PACKAGE = "" -else: - PACKAGE = __package__ -SCRIPTDIR = os.path.dirname(os.path.realpath(__file__).removesuffix(PACKAGE)) +SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) DEFAULT_THEME_PATH = os.path.join(SCRIPTDIR, "templates", "default.css") DEFAULT_AUTHOR = "Author" diff --git a/modules/css_color.py b/modules/css_color.py index 1b46915..d9e5f61 100644 --- a/modules/css_color.py +++ b/modules/css_color.py @@ -19,7 +19,7 @@ def extract_colorscheme(theme_path: str) -> dict[str, str]: dictionary containing color scheme variables and their hexadecimal values. """ 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 = {} with open(theme_path, "r", encoding="utf-8") as f: diff --git a/modules/generate_html.py b/modules/generate_html.py index ba6a3ca..c4881da 100644 --- a/modules/generate_html.py +++ b/modules/generate_html.py @@ -20,11 +20,7 @@ from modules.argumentparser import Args from modules.datatypes.metadata import Metadata, ImageMetadata, SubfolderMetadata # Constants for file paths and exclusions -if __package__ is None: - PACKAGE = "" -else: - PACKAGE = __package__ -SCRIPTDIR = os.path.dirname(os.path.realpath(__file__).removesuffix(PACKAGE)) +SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) FAVICON_PATH = ".static/favicon.ico" GLOBAL_CSS_PATH = ".static/global.css" EXCLUDES = ["index.html", "manifest.json", "robots.txt"] @@ -393,7 +389,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: M return image, metadata -def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> set[str]: +def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str, darktheme: bool = False) -> set[str]: """ Generates HTML content for a folder of images. @@ -463,7 +459,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: update_metadata(metadata, folder) if should_generate_html(images, contains_files, _args): - subfoldertags = create_html_file(folder, title, foldername, images, subfolders, _args, version, logo, subfoldertags) + subfoldertags = create_html_file(folder, title, foldername, images, subfolders, _args, version, logo, subfoldertags, darktheme=darktheme) else: if os.path.exists(os.path.join(folder, "index.html")): logger.info("removing existing index.html", extra={"folder": folder}) @@ -568,7 +564,16 @@ def format_html(html: str) -> str: def create_html_file( - folder: str, title: str, foldername: str, images: list[ImageMetadata], subfolders: list[SubfolderMetadata], _args: Args, version: str, logo: str, subfoldertags: set[str] + folder: str, + title: str, + foldername: str, + images: list[ImageMetadata], + subfolders: list[SubfolderMetadata], + _args: Args, + version: str, + logo: str, + subfoldertags: set[str], + darktheme: bool = False, ) -> set[str]: """ Creates the HTML file using the template. @@ -625,6 +630,7 @@ def create_html_file( favicon=f"{_args.web_root_url}{FAVICON_PATH}", stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}", theme=f"{_args.web_root_url}.static/theme.css", + darktheme=f"{_args.web_root_url}.static/theme-dark.css" if darktheme else None, root=_args.web_root_url, parent=f"{_args.web_root_url}{urllib.parse.quote(foldername)}", header=f"{header} - LICENSE", @@ -642,6 +648,7 @@ def create_html_file( favicon=f"{_args.web_root_url}{FAVICON_PATH}", stylesheet=f"{_args.web_root_url}{GLOBAL_CSS_PATH}", theme=f"{_args.web_root_url}.static/theme.css", + darktheme=f"{_args.web_root_url}.static/theme-dark.css" if darktheme else None, root=_args.web_root_url, parent=parent, header=header, @@ -662,7 +669,7 @@ def create_html_file( 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, darktheme: bool = False) -> list[tuple[str, str, str]]: """ lists and processes a folder, generating HTML files. @@ -676,5 +683,5 @@ def list_folder(folder: str, title: str, _args: Args, raw: list[str], version: s Returns: list[tuple[str, str]]: list of thumbnails generated. """ - generate_html(folder, title, _args, raw, version, logo) + generate_html(folder, title, _args, raw, version, logo, darktheme=darktheme) return thumbnails diff --git a/modules/logger.py b/modules/logger.py index 15d91b0..a04675e 100644 --- a/modules/logger.py +++ b/modules/logger.py @@ -21,11 +21,7 @@ from datetime import datetime from pythonjsonlogger import jsonlogger # Constants for file paths and exclusions -if __package__ is None: - PACKAGE = "" -else: - PACKAGE = __package__ -SCRIPTDIR = os.path.dirname(os.path.realpath(__file__).removesuffix(PACKAGE)) +SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) LOG_DIR = os.path.join(SCRIPTDIR, "logs") LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl") diff --git a/modules/svg_handling.py b/modules/svg_handling.py index d1b5f82..5ba5677 100644 --- a/modules/svg_handling.py +++ b/modules/svg_handling.py @@ -18,11 +18,7 @@ from modules.argumentparser import Args from modules.css_color import extract_theme_color, extract_colorscheme # Define constants for static files directory and icon sizes -if __package__ is None: - PACKAGE = "" -else: - PACKAGE = __package__ -SCRIPTDIR = os.path.dirname(os.path.realpath(__file__).removesuffix(PACKAGE)) +SCRIPTDIR = os.path.dirname(os.path.realpath(__file__)).removesuffix(__package__) STATIC_FILES_DIR = os.path.join(SCRIPTDIR, "files") ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"] diff --git a/templates/default-dark.css b/templates/default-dark.css new file mode 100644 index 0000000..24001d3 --- /dev/null +++ b/templates/default-dark.css @@ -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; +} \ No newline at end of file diff --git a/templates/functionality.js b/templates/functionality.js index 8d56864..ce80128 100644 --- a/templates/functionality.js +++ b/templates/functionality.js @@ -24,6 +24,10 @@ class PhotoGallery { this.scrollFunction = this.scrollFunction.bind(this); this.topFunction = this.topFunction.bind(this); this.onLoad = this.onLoad.bind(this); + this.darkMode = this.darkMode.bind(this); + this.lightMode = this.lightMode.bind(this); + this.darkModeToggle = this.darkModeToggle.bind(this); + this.detectDarkMode = this.detectDarkMode.bind(this); this.init(); } @@ -38,12 +42,7 @@ class PhotoGallery { openSwipe(imgIndex) { const options = { index: imgIndex }; - const gallery = new PhotoSwipe( - this.pswpElement, - PhotoSwipeUI_Default, - this.shown, - options - ); + const gallery = new PhotoSwipe(this.pswpElement, PhotoSwipeUI_Default, this.shown, options); gallery.init(); } @@ -78,9 +77,7 @@ class PhotoGallery { if (folders) folders.style.display = ""; document.getElementById("recursive").checked = false; - document - .querySelectorAll("#tagdropdown input.tagcheckbox:checked") - .forEach((checkbox) => (checkbox.checked = false)); + document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => (checkbox.checked = false)); window.history.replaceState({ html: content, pageTitle: title }, "", path); this.requestMetadata(); } @@ -150,10 +147,9 @@ class PhotoGallery { existingItems.add(image.src); } } - if (Array.isArray(data.subfolders)) - nextLevel.push(...data.subfolders); + if (Array.isArray(data.subfolders)) nextLevel.push(...data.subfolders); } catch {} - }) + }), ); if (nextLevel.length > 0) await fetchFoldersRecursively(nextLevel); }; @@ -194,39 +190,30 @@ class PhotoGallery { filter() { const searchParams = new URLSearchParams(window.location.search); this.shown = []; - let path = decodeURIComponent( - window.location.origin + - window.location.pathname.replace("index.html", "") - ); + let path = decodeURIComponent(window.location.origin + window.location.pathname.replace("index.html", "")); if (path.startsWith("null")) { path = window.location.protocol + "//" + path.substring(4); } const selectedTags = []; - document - .querySelectorAll("#tagdropdown input.tagcheckbox:checked") - .forEach((checkbox) => { - let tag = checkbox.parentElement.id.trim().substring(1); - if (checkbox.parentElement.parentElement.children.length > 1) - tag += "|"; - selectedTags.push(tag); - }); + document.querySelectorAll("#tagdropdown input.tagcheckbox:checked").forEach((checkbox) => { + let tag = checkbox.parentElement.id.trim().substring(1); + if (checkbox.parentElement.parentElement.children.length > 1) tag += "|"; + selectedTags.push(tag); + }); const urltags = selectedTags.join(","); let isRecursiveChecked = false; try { - isRecursiveChecked = - document.getElementById("recursive")?.checked || false; + 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); + return isParent ? tags.some((t) => t.startsWith(selected)) : tags.includes(selected); }); if (include || selectedTags.length === 0) { @@ -313,9 +300,7 @@ class PhotoGallery { let str = ""; this.shown.forEach((item, index) => { let tags = this.parseHierarchicalTags(item.tags || []); - str += `