6 Commits

Author SHA1 Message Date
002e9c62db added tagging function as requested in issue #7 2025-06-22 16:20:45 +02:00
cf494401c8 removed pbardict 2025-05-30 11:25:32 +02:00
2a0323e579 oop 2025-05-20 09:02:31 +02:00
7e23b3625a fixed logo timeout 2025-03-31 20:24:27 +02:00
bce51dc3d6 added folderthumbnails 2025-02-20 14:59:00 +01:00
57250b3adc updated figcaptions 2025-02-18 12:53:37 +01:00
52 changed files with 394 additions and 184 deletions

2
.gitignore vendored
View File

@@ -164,7 +164,7 @@ cython_debug/
test/.static test/.static
test/.thumbnails test/.thumbnails
test/**/index.html test/**/index.html
test/**/.sizelist.json test/**/.metadata.json
test/manifest.json test/manifest.json
themes/previews themes/previews
logs logs

View File

@@ -1 +1 @@
3.12 StaticGalleryBuilder

View File

@@ -1 +1 @@
2.5.1 2.7.0

View File

@@ -97,6 +97,7 @@ To generate a web manifest file:
- The script generates the preview thumbnails in a `.thumbnails` subdirectory within the root folder. - The script generates the preview thumbnails in a `.thumbnails` subdirectory within the root folder.
- The `.lock` file prevents multiple instances of the script from running simultaneously. Make sure to remove it if the script terminates unexpectedly. - The `.lock` file prevents multiple instances of the script from running simultaneously. Make sure to remove it if the script terminates unexpectedly.
- Add a `info` file into any directory containing pictures and it will be read and displayed as a tooltip on the website. - Add a `info` file into any directory containing pictures and it will be read and displayed as a tooltip on the website.
- Add tags to the Image exif or to `.metadata.json` to tag images for filtering.
## License ## License

View File

@@ -40,6 +40,7 @@
"--reverse-sort", "--reverse-sort",
"--regenerate-thumbnails", "--regenerate-thumbnails",
"--reread-metadata", "--reread-metadata",
"--folderthumbnails",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "Testfolder", "name": "Testfolder",
@@ -64,6 +65,7 @@
"-m", "-m",
// "--regenerate-thumbnails", // "--regenerate-thumbnails",
// "--reread-metadata", // "--reread-metadata",
"--folderthumbnails",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "woek", "name": "woek",
@@ -75,7 +77,7 @@
{ {
"args": [ "args": [
"${workspaceFolder}/themes", "${workspaceFolder}/themes",
"https://pictures.sorogon.eu/Analog/Example/" "https://pictures.sorogon.eu/public/Example/"
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "Generate Themes previews", "name": "Generate Themes previews",
@@ -99,7 +101,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
}, },
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "charliermarsh.ruff",
}, },
"black-formatter.args": [ "black-formatter.args": [
"-l 260", "-l 260",
@@ -151,6 +153,7 @@
"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"
}, },
"ruff.lineLength": 180,
}, },
"tasks": { "tasks": {
"version": "2.0.0", "version": "2.0.0",

View File

@@ -3,7 +3,7 @@ import os
import re import re
import sys import sys
import shutil import shutil
import fnmatch import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from multiprocessing import Pool, freeze_support from multiprocessing import Pool, freeze_support
@@ -34,8 +34,6 @@ IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"]
NOT_LIST = ["*/Galleries/*", "Archives"] NOT_LIST = ["*/Galleries/*", "Archives"]
# fmt: on # fmt: on
pbardict: dict[str, tqdm] = {}
args = parse_arguments(VERSION) args = parse_arguments(VERSION)
lock_file = os.path.join(args.root_directory, ".lock") lock_file = os.path.join(args.root_directory, ".lock")
@@ -45,7 +43,7 @@ if os.path.exists(lock_file):
else: else:
from modules.logger import logger from modules.logger import logger
from modules.svg_handling import icons, webmanifest, extract_colorscheme from modules.svg_handling import icons, webmanifest, extract_colorscheme
from modules.generate_html import list_folder, EXCLUDES from modules.generate_html import list_folder
def init_globals(_args: Args, raw: list[str]) -> tuple[Args, list[str]]: def init_globals(_args: Args, raw: list[str]) -> tuple[Args, list[str]]:
@@ -161,37 +159,6 @@ def generate_thumbnail(arguments: tuple[str, str, str]) -> None:
logger.debug("thumbnail already exists for %s", item, extra={"path": image}) logger.debug("thumbnail already exists for %s", item, extra={"path": image})
def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
"""
Recursively count the total number of folders to be processed.
Parameters:
-----------
folder : str
The current folder being processed.
_args : Args
Parsed command-line arguments.
_total : int, optional
The running total of folders, default is 0.
Returns:
--------
int
The total number of folders.
"""
_total += 1
pbardict["traversingbar"].desc = f"Traversing filesystem - {folder}"
pbardict["traversingbar"].update(1)
items = sorted(os.listdir(folder))
for item in items:
if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)) and not item.startswith("."):
if item not in _args.exclude_folders and not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
logger.debug("Found folder %s in %s", item, folder)
_total = get_total_folders(os.path.join(folder, item), _args, _total)
return _total
def main(args) -> None: 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.
@@ -206,15 +173,18 @@ def main(args) -> None:
logger.info("getting logo from sorogon.eu") logger.info("getting logo from sorogon.eu")
req = urllib.request.Request("https://files.sorogon.eu/logo.svg") req = urllib.request.Request("https://files.sorogon.eu/logo.svg")
with urllib.request.urlopen(req) as res: try:
logo = res.read().decode() with urllib.request.urlopen(req, timeout=10) as res:
logo = res.read().decode()
if logo.startswith("<?xml"): if logo.startswith("<?xml"):
logo = re.sub(r"<\?xml.+\?>", "", logo).strip() logo = re.sub(r"<\?xml.+\?>", "", logo).strip()
if logo.startswith("<!--"): if logo.startswith("<!--"):
logo = re.sub(r"<!--.+-->", "", logo).strip() logo = re.sub(r"<!--.+-->", "", logo).strip()
logo = logo.replace("\n", " ") logo = logo.replace("\n", " ")
logo = " ".join(logo.split()) logo = " ".join(logo.split())
except urllib.error.URLError:
logo = "&lt;/srgn&gt;"
if args.reread_metadata: if args.reread_metadata:
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")
@@ -235,20 +205,13 @@ def main(args) -> None:
if args.non_interactive_mode: if args.non_interactive_mode:
logger.info("generating HTML files") logger.info("generating HTML files")
print("Generating HTML files...") print("Generating HTML files...")
thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION, logo) thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails") logger.info("generating thumbnails")
print("Generating thumbnails...") print("Generating thumbnails...")
pool.map(generate_thumbnail, thumbnails) pool.map(generate_thumbnail, thumbnails)
else: else:
pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True) thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo)
logger.info("getting total number of folders to process")
total = get_total_folders(args.root_directory, args)
pbardict["traversingbar"].desc = "Traversing filesystem"
pbardict["traversingbar"].update(0)
pbardict["traversingbar"].close()
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION, logo)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails") logger.info("generating thumbnails")

View File

@@ -28,6 +28,7 @@ body {
.folders figure { .folders figure {
margin-bottom: 32px; margin-bottom: 32px;
margin-top: 50px; margin-top: 50px;
width: 180px;
} }
.header h1 { .header h1 {
@@ -42,11 +43,21 @@ body {
} }
.folders figcaption { .folders figcaption {
width: 120px; width: 100%;
overflow: hidden;
font-size: smaller; font-size: smaller;
text-align: center; text-align: center;
} }
.folderthumb {
height: 40px;
width: 70px !important;
overflow: hidden;
aspect-ratio: 1 / 1;
object-fit: contain;
margin: 20px 20px 0px -90px;
}
.row { .row {
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
@@ -120,14 +131,18 @@ figure {
text-decoration: none; text-decoration: none;
} }
.navbar .title { .navbar .navleft {
float: left;
}
.navbar .navcenter {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
.navbar .license { .navbar .navright {
float: right; float: right
} }
.navbar li .header { .navbar li .header {
@@ -139,18 +154,28 @@ figure {
.tooltip { .tooltip {
position: relative; position: relative;
cursor: pointer;
} }
.tooltip .tooltiptext { .tooltip .tooltiptext {
display: none; display: none;
cursor: default;
width: max-content; width: max-content;
position: absolute; position: absolute;
z-index: 100; z-index: 100;
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
float: left;
}
.tooltip .infotext {
padding: 12px; padding: 12px;
} }
.tooltip .tagdropdown {
padding: 0;
}
.tooltip:hover .tooltiptext { .tooltip:hover .tooltiptext {
display: block; display: block;
opacity: 1; opacity: 1;
@@ -161,6 +186,22 @@ figure {
opacity: 1; opacity: 1;
} }
.tooltip .tooltiptext .tagentry {
list-style: none;
width: 100%;
cursor: pointer;
margin: 0;
padding: 0;
}
.tooltip .tooltiptext .tagentry label {
cursor: pointer;
width: 100%;
height: 100%;
padding: 12px;
display: block;
}
.column { .column {
-ms-flex: 12.5%; -ms-flex: 12.5%;
flex: 12.5%; flex: 12.5%;
@@ -206,15 +247,27 @@ figure {
max-width: 25%; max-width: 25%;
} }
.folders figure {
width: 160px;
}
.folders img { .folders img {
width: 80px; width: 80px;
} }
.folders figcaption { .folders figcaption {
width: 100px;
font-size: small; font-size: small;
} }
.folderthumb {
height: 30px;
width: 50px !important;
overflow: hidden;
aspect-ratio: 1 / 1;
object-fit: contain;
margin: 15px 20px 0px -70px;
}
.navbar { .navbar {
font-size: medium; font-size: medium;
} }
@@ -227,15 +280,27 @@ figure {
max-width: 50%; max-width: 50%;
} }
.folders figure {
width: 140px;
}
.folders img { .folders img {
width: 60px; width: 60px;
} }
.folders figcaption { .folders figcaption {
width: 80px;
font-size: x-small; font-size: x-small;
} }
.folderthumb {
height: 25px;
width: 30px !important;
overflow: hidden;
aspect-ratio: 1 / 1;
object-fit: contain;
margin: 10px 20px 0px -50px;
}
.navbar { .navbar {
font-size: small; font-size: small;
} }
@@ -256,15 +321,27 @@ figure {
max-width: 100%; max-width: 100%;
} }
.folders figure {
width: 120px;
}
.folders img { .folders img {
width: 40px; width: 40px;
} }
.folders figcaption { .folders figcaption {
width: 60px;
font-size: xx-small; font-size: xx-small;
} }
.folderthumb {
height: 15px;
width: 20px !important;
overflow: hidden;
aspect-ratio: 1 / 1;
object-fit: contain;
margin: 5px 10px 0px -35px;
}
.navbar { .navbar {
font-size: xx-small; font-size: xx-small;
} }

View File

@@ -1,7 +1,6 @@
import os import os
import re import re
import sys import sys
import time
import shutil import shutil
import base64 import base64
import fileinput import fileinput

View File

@@ -33,6 +33,8 @@ class Args:
A list of folders to exclude from processing. A list of folders to exclude from processing.
file_extensions : list[str] file_extensions : list[str]
A list of file extensions to include. A list of file extensions to include.
folder_thumbs : bool
Wether to generate subfolder thumbnails.
generate_webmanifest : bool generate_webmanifest : bool
Whether to generate a web manifest file. Whether to generate a web manifest file.
ignore_other_files : bool ignore_other_files : bool
@@ -58,6 +60,7 @@ class Args:
author_name: str author_name: str
exclude_folders: list[str] exclude_folders: list[str]
file_extensions: list[str] file_extensions: list[str]
folder_thumbs: bool
generate_webmanifest: bool generate_webmanifest: bool
ignore_other_files: bool ignore_other_files: bool
license_type: Optional[str] license_type: Optional[str]
@@ -76,14 +79,15 @@ class Args:
result["author_name"] = self.author_name result["author_name"] = self.author_name
result["exclude_folders"] = self.exclude_folders result["exclude_folders"] = self.exclude_folders
result["file_extensions"] = self.file_extensions result["file_extensions"] = self.file_extensions
result["folder_thumbs"] = self.folder_thumbs
result["generate_webmanifest"] = self.generate_webmanifest result["generate_webmanifest"] = self.generate_webmanifest
result["ignore_other_files"] = self.ignore_other_files result["ignore_other_files"] = self.ignore_other_files
if self.license_type is not None: if self.license_type is not None:
result["license_type"] = self.license_type result["license_type"] = self.license_type
result["non_interactive_mode"] = self.non_interactive_mode result["non_interactive_mode"] = self.non_interactive_mode
result["regenerate_thumbnails"] = self.regenerate_thumbnails result["regenerate_thumbnails"] = self.regenerate_thumbnails
result["reverse_sort"] = self.reverse_sort
result["reread_metadata"] = self.reread_metadata result["reread_metadata"] = self.reread_metadata
result["reverse_sort"] = self.reverse_sort
result["root_directory"] = self.root_directory result["root_directory"] = self.root_directory
result["site_title"] = self.site_title result["site_title"] = self.site_title
result["theme_path"] = self.theme_path result["theme_path"] = self.theme_path
@@ -120,6 +124,7 @@ def parse_arguments(version: str) -> Args:
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("--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")
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", )
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")
@@ -135,6 +140,7 @@ def parse_arguments(version: str) -> Args:
author_name=parsed_args.author_name, author_name=parsed_args.author_name,
exclude_folders=parsed_args.exclude_folders, exclude_folders=parsed_args.exclude_folders,
file_extensions=parsed_args.file_extensions, file_extensions=parsed_args.file_extensions,
folder_thumbs=parsed_args.folder_thumbs,
generate_webmanifest=parsed_args.generate_webmanifest, generate_webmanifest=parsed_args.generate_webmanifest,
ignore_other_files=parsed_args.ignore_other_files, ignore_other_files=parsed_args.ignore_other_files,
license_type=parsed_args.license_type, license_type=parsed_args.license_type,

View File

@@ -7,7 +7,7 @@ from typing import Any
from datetime import datetime from datetime import datetime
from tqdm.auto import tqdm from tqdm.auto import tqdm
from PIL import Image, ExifTags, TiffImagePlugin from PIL import Image, ExifTags, TiffImagePlugin, UnidentifiedImageError
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from modules.logger import logger from modules.logger import logger
@@ -24,60 +24,65 @@ 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"]
# Set the maximum image pixels to prevent decompression bomb DOS attacks # Set the maximum image pixels
Image.MAX_IMAGE_PIXELS = 933120000 Image.MAX_IMAGE_PIXELS = 933120000
# Initialize Jinja2 environment for template rendering # Initialize Jinja2 environment for template rendering
env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates"))) env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates")))
thumbnails: list[tuple[str, str]] = [] thumbnails: list[tuple[str, str, str]] = []
info: dict[str, str] = {} info: dict[str, str] = {}
licens: dict[str, str] = {} licens: dict[str, str] = {}
pbardict: dict[str, tqdm] = {}
def initialize_sizelist(folder: str) -> dict[str, dict[str, int]]: def initialize_metadata(folder: str) -> dict[str, dict[str, int]]:
""" """
Initializes the size list JSON file if it doesn't exist. Initializes the metadata JSON file if it doesn't exist.
Args: Args:
folder (str): The folder in which the size list file is located. folder (str): The folder in which the metadata file is located.
Returns: Returns:
dict[str, dict[str, int]]: The size list dictionary. dict[str, dict[str, int]]: The metadata dictionary.
""" """
sizelist = {} metadata = {}
sizelist_path = os.path.join(folder, ".sizelist.json") metadata_path = os.path.join(folder, ".metadata.json")
if not os.path.exists(sizelist_path): if not os.path.exists(metadata_path):
logger.info("creating new size list file", extra={"file": sizelist_path}) logger.info("creating new metadata file", extra={"file": metadata_path})
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile: with open(metadata_path, "x", encoding="utf-8") as metadatafile:
sizelistfile.write("{}") metadatafile.write("{}")
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile: with open(metadata_path, "r+", encoding="utf-8") as metadatafile:
logger.info("reading size list file", extra={"file": sizelist_path}) logger.info("reading metadata file", extra={"file": metadata_path})
try: try:
sizelist = json.loads(sizelistfile.read()) metadata = json.loads(metadatafile.read())
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
logger.warning("invalid JSON in size list file", extra={"file": sizelist_path}) logger.warning("invalid JSON in metadata file", extra={"file": metadata_path})
sizelist = {} metadata = {}
return sizelist
# remove old sizelist if it exists
sizelist_path = os.path.join(folder, ".sizelist.json")
if os.path.exists(sizelist_path):
logger.warning("found old .sizelist.json, removing it...", extra={"path": sizelist_path})
os.remove(sizelist_path)
return metadata
def update_sizelist(sizelist: dict[str, dict[str, Any]], folder: str) -> None: def update_metadata(metadata: dict[str, dict[str, Any]], folder: str) -> None:
""" """
Updates the size list JSON file. Updates the metadata JSON file.
Args: Args:
sizelist (dict[str, dict[str, int]]): The size list dictionary to be written to the file. metadata (dict[str, dict[str, int]]): The metadata dictionary to be written to the file.
folder (str): The folder in which the size list file is located. folder (str): The folder in which the metadata file is located.
""" """
sizelist_path = os.path.join(folder, ".sizelist.json") metadata_path = os.path.join(folder, ".metadata.json")
if sizelist: if metadata:
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile: with open(metadata_path, "w", encoding="utf-8") as metadatafile:
logger.info("writing size list file", extra={"file": sizelist_path}) logger.info("writing metadata file", extra={"file": metadata_path})
sizelistfile.write(json.dumps(sizelist, indent=4)) metadatafile.write(json.dumps(metadata, indent=4))
else: else:
if os.path.exists(sizelist_path): if os.path.exists(metadata_path):
logger.info("deleting empty size list file", extra={"file": sizelist_path}) logger.info("deleting empty metadata file", extra={"file": metadata_path})
os.remove(sizelist_path) os.remove(metadata_path)
def get_image_info(item: str, folder: str) -> dict[str, Any]: def get_image_info(item: str, folder: str) -> dict[str, Any]:
@@ -92,10 +97,15 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
dict[str, Any]: A dictionary containing image width, height, and EXIF data. dict[str, Any]: A dictionary containing image width, height, and EXIF data.
""" """
file = os.path.join(folder, item) file = os.path.join(folder, item)
with Image.open(file) as img: try:
logger.info("extracting image information", extra={"file": file}) with Image.open(file) as img:
exif = img.getexif() logger.info("extracting image information", extra={"file": file})
width, height = img.size exif = img.getexif()
width, height = img.size
except UnidentifiedImageError:
logger.error("cannot identify image file", extra={"file": file})
print(f"cannot identify image file: {file}")
return {"width": None, "height": None, "tags": None, "exifdata": None}
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)
@@ -117,9 +127,9 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
content = newtuple content = newtuple
if tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]: if tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]:
epr = r"\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}" epr = r"\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}"
if re.match(epr, content): if re.match(epr, str(content)):
try: try:
content = datetime.strptime(content, "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d %H:%M:%S") content = datetime.strptime(str(content), "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d %H:%M:%S")
except ValueError: except ValueError:
content = None content = None
else: else:
@@ -131,12 +141,12 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]:
for key in ["PrintImageMatching", "UserComment", "MakerNote"]: for key in ["PrintImageMatching", "UserComment", "MakerNote"]:
if key in exifdata: if key in exifdata:
del exifdata[key] del exifdata[key]
return {"width": width, "height": height, "exifdata": exifdata} return {"width": width, "height": height, "tags": [], "exifdata": exifdata}
else: else:
return {"width": width, "height": height, "exifdata": None} return {"width": width, "height": height, "tags": [], "exifdata": None}
def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: dict[str, dict[str, int]], raw: list[str]) -> dict[str, Any]: def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: dict[str, dict[str, int]], raw: list[str]) -> dict[str, Any]:
""" """
Processes an image and prepares its data for the HTML template. Processes an image and prepares its data for the HTML template.
@@ -145,23 +155,24 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: d
folder (str): The folder containing the image. folder (str): The folder containing the image.
_args (Args): Parsed command line arguments. _args (Args): Parsed command line arguments.
baseurl (str): Base URL for the web root. baseurl (str): Base URL for the web root.
sizelist (dict[str, dict[str, int]]): dictionary containing size information for images. metadata (dict[str, dict[str, int]]): dictionary containing size information for images.
raw (list[str]): list of raw image file extensions. raw (list[str]): list of raw image file extensions.
Returns: Returns:
dict[str, Any]: dictionary containing image details for HTML rendering. dict[str, Any]: dictionary containing image details for HTML rendering.
""" """
extsplit = os.path.splitext(item) extsplit = os.path.splitext(item)
if item not in sizelist or _args.reread_metadata: if item not in metadata or _args.reread_metadata:
sizelist[item] = get_image_info(item, folder) metadata[item] = get_image_info(item, folder)
image = { image = {
"url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}", "url": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}",
"thumbnail": f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg", "thumbnail": f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}.jpg",
"name": item, "name": item,
"width": sizelist[item]["width"], "width": metadata[item]["width"],
"height": sizelist[item]["height"], "height": metadata[item]["height"],
"exifdata": sizelist[item].get("exifdata", ""), "tags": metadata[item]["tags"],
"exifdata": metadata[item].get("exifdata", ""),
} }
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:
@@ -194,10 +205,10 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
""" """
logger.info("processing folder", extra={"folder": folder}) logger.info("processing folder", extra={"folder": folder})
if _args.regenerate_thumbnails: if _args.regenerate_thumbnails:
if os.path.exists(os.path.join(folder, ".sizelist.json")): if os.path.exists(os.path.join(folder, ".metadata.json")):
logger.info("removing .sizelist.json", extra={"folder": folder}) logger.info("removing .metadata.json", extra={"folder": folder})
os.remove(os.path.join(folder, ".sizelist.json")) os.remove(os.path.join(folder, ".metadata.json"))
sizelist = initialize_sizelist(folder) metadata = initialize_metadata(folder)
items = sorted(os.listdir(folder)) items = sorted(os.listdir(folder))
contains_files = False contains_files = False
@@ -209,30 +220,35 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
create_thumbnail_folder(foldername, _args.root_directory) create_thumbnail_folder(foldername, _args.root_directory)
if not _args.non_interactive_mode:
pbardict[folder] = tqdm(total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True)
logger.info("processing contents", extra={"folder": folder}) logger.info("processing contents", extra={"folder": folder})
for item in items:
if item not in EXCLUDES and not item.startswith("."):
if os.path.isdir(os.path.join(folder, item)):
process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo)
else:
contains_files = True
if os.path.splitext(item)[1].lower() in _args.file_extensions:
images.append(process_image(item, folder, _args, baseurl, sizelist, raw))
if item == "info":
process_info_file(folder, item)
if item == "LICENSE":
process_license(folder, item)
if not _args.non_interactive_mode:
pbardict[folder].update(1)
if not _args.non_interactive_mode: if not _args.non_interactive_mode:
pbardict[folder].close() for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True):
if item not in EXCLUDES and not item.startswith("."):
if os.path.isdir(os.path.join(folder, item)):
process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo)
else:
contains_files = True
if os.path.splitext(item)[1].lower() in _args.file_extensions:
images.append(process_image(item, folder, _args, baseurl, metadata, raw))
if item == "info":
process_info_file(folder, item)
if item == "LICENSE":
process_license(folder, item)
else:
for item in items:
if item not in EXCLUDES and not item.startswith("."):
if os.path.isdir(os.path.join(folder, item)):
process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo)
else:
contains_files = True
if os.path.splitext(item)[1].lower() in _args.file_extensions:
images.append(process_image(item, folder, _args, baseurl, metadata, raw))
if item == "info":
process_info_file(folder, item)
if item == "LICENSE":
process_license(folder, item)
update_sizelist(sizelist, folder) update_metadata(metadata, folder)
if should_generate_html(images, contains_files, _args): if should_generate_html(images, contains_files, _args):
create_html_file(folder, title, foldername, images, subfolders, _args, version, logo) create_html_file(folder, title, foldername, images, subfolders, _args, version, logo)
@@ -241,9 +257,6 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
logger.info("removing existing index.html", extra={"folder": folder}) logger.info("removing existing index.html", extra={"folder": folder})
os.remove(os.path.join(folder, "index.html")) os.remove(os.path.join(folder, "index.html"))
if not _args.non_interactive_mode:
pbardict["htmlbar"].update(1)
def create_thumbnail_folder(foldername: str, root_directory: str) -> None: def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
""" """
@@ -259,7 +272,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]], _args: Args, raw: list[str], version: str, logo: str) -> None: def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dict[str, str | None]], _args: Args, raw: list[str], version: str, logo: str) -> None:
""" """
Processes a subfolder. Processes a subfolder.
@@ -271,8 +284,21 @@ def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dic
_args (Args): Parsed command line arguments. _args (Args): Parsed command line arguments.
raw (list[str]): Raw image file extensions. raw (list[str]): Raw image file extensions.
""" """
subfolder_url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html" if _args.web_root_url.startswith("file://") else f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}" subfolder_url = (
subfolders.append({"url": subfolder_url, "name": item}) f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html"
if _args.web_root_url.startswith("file://")
else f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}"
)
thumb = None
if _args.folder_thumbs:
thumbitems = [i for i in sorted(os.listdir(os.path.join(folder, item))) if os.path.splitext(i)[1].lower() in _args.file_extensions]
if len(thumbitems) > 0:
if _args.reverse_sort:
thumb = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}/{urllib.parse.quote(thumbitems[-1])}.jpg"
else:
thumb = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}/{urllib.parse.quote(thumbitems[0])}.jpg"
subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb})
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):
generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo) generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo)
@@ -288,7 +314,9 @@ def process_license(folder: str, item: str) -> None:
""" """
with open(os.path.join(folder, item), encoding="utf-8") as f: with open(os.path.join(folder, item), encoding="utf-8") as f:
logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)}) logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)})
licens[urllib.parse.quote(folder)] = f.read().replace("\n", "</br>\n").replace(" ", "&emsp;").replace(" ", "&ensp;").replace("sp; ", "sp;&ensp;").replace("&ensp;&ensp;", "&emsp;") licens[urllib.parse.quote(folder)] = (
f.read().replace("\n", "</br>\n").replace(" ", "&emsp;").replace(" ", "&ensp;").replace("sp; ", "sp;&ensp;").replace("&ensp;&ensp;", "&emsp;")
)
def process_info_file(folder: str, item: str) -> None: def process_info_file(folder: str, item: str) -> None:
@@ -349,6 +377,12 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict
else None else None
) )
alltags = set()
for img in images:
for tag in img["tags"]:
alltags.add(tag)
alltags = sorted(alltags)
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: if _args.reverse_sort:
@@ -397,6 +431,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict
version=version, version=version,
logo=logo, logo=logo,
licensefile=license_url, licensefile=license_url,
tags=alltags,
) )
with open(html_file, "w", encoding="utf-8") as f: with open(html_file, "w", encoding="utf-8") as f:
@@ -404,7 +439,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict
f.write(content) f.write(content)
def list_folder(total: int, folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str]]: def list_folder(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str, str]]:
""" """
lists and processes a folder, generating HTML files. lists and processes a folder, generating HTML files.
@@ -418,7 +453,5 @@ def list_folder(total: int, folder: str, title: str, _args: Args, raw: list[str]
Returns: Returns:
list[tuple[str, str]]: list of thumbnails generated. list[tuple[str, str]]: list of thumbnails generated.
""" """
if not _args.non_interactive_mode:
pbardict["htmlbar"] = tqdm(total=total, desc="Generating HTML files", unit="folders", ascii=True, dynamic_ncols=True)
generate_html(folder, title, _args, raw, version, logo) generate_html(folder, title, _args, raw, version, logo)
return thumbnails return thumbnails

View File

@@ -1,8 +1,8 @@
CairoSVG==2.7.1 CairoSVG==2.7.1
Jinja2==3.1.4 Jinja2==3.1.5
pillow==10.4.0 Pillow==11.1.0
pyinstaller==6.11.1 pyinstaller==6.11.1
python-json-logger==2.0.7 python_json_logger==2.0.7
rich-argparse==1.5.2 rich_argparse==1.7.0
setuptools==70.3.0 selenium==4.28.1
tqdm==4.66.4 tqdm==4.66.4

View File

@@ -52,6 +52,10 @@
background-color: var(--color2); background-color: var(--color2);
} }
.tagentry:hover {
background-color: var(--color4);
}
.column img { .column img {
background-color: var(--color2); background-color: var(--color2);
} }

View File

@@ -31,23 +31,38 @@
<body> <body>
<div class="header"> <div class="header">
<ul class="navbar"> <ol class="navbar">
<li><a href="{{ root }}">Home</a></li> <div class="navleft">
{%- if parent %} <li><a href="{{ root }}">Home</a></li>
<li><a href="{{ parent }}">Parent Directory</a></li> {%- if parent %}
{%- endif %} <li><a href="{{ parent }}">Parent Directory</a></li>
{%- if info %} {%- endif %}
<li class="tooltip"><a>Info</a><span class="tooltiptext"> {%- if info %}
{%- for infoline in info -%} <li class="tooltip"><a>Info</a><span class="tooltiptext infotext">
{{ infoline }}<br /> {%- for infoline in info -%}
{%- endfor -%} {{ infoline }}<br />
</span></li> {%- endfor -%}
{%- endif %} </span></li>
<li class="title"><span class="header">{{ header }}</span></li> {%- endif %}
{%- if licensefile %} </div>
<li class="license"><a href="{{ licensefile }}">License</a></li> <div class="navcenter">
{%- endif %} <li class="title"><span class="header">{{ header }}</span></li>
</ul> </div>
<div class="navright">
{%- if tags|length > 0 %}
<li class="tooltip"><a>Filter by Tags</a>
<ol class="tooltiptext tagdropdown" id="tagdropdown">
{%- for tag in tags -%}
<li class="tagentry"><label onclick="filter()"><input type="checkbox" />{{ tag }}</label></li><br />
{%- endfor -%}
</ol>
</li>
{%- endif %}
{%- if licensefile %}
<li class="license"><a href="{{ licensefile }}">License</a></li>
{%- endif %}
</div>
</ol>
{% if subdirectories %} {% if subdirectories %}
{%- for subdirectory in subdirectories %} {%- for subdirectory in subdirectories %}
<link rel="preload" href="{{ subdirectory.url }}/index.html" type="text/html"> <link rel="preload" href="{{ subdirectory.url }}/index.html" type="text/html">
@@ -57,6 +72,9 @@
<a href="{{ subdirectory.url }}"> <a href="{{ subdirectory.url }}">
<figure> <figure>
<img class="foldericon" /> <img class="foldericon" />
{%- if subdirectory.thumb %}
<img class="folderthumb" src="{{ subdirectory.thumb }}" />
{%- endif %}
<figcaption>{{ subdirectory.name }}</figcaption> <figcaption>{{ subdirectory.name }}</figcaption>
</figure> </figure>
</a> </a>
@@ -66,7 +84,7 @@
</div> </div>
{% if images %} {% if images %}
{%- set ns = namespace(count = 0) -%} {%- set ns = namespace(count = 0) -%}
<div class="row"> <div class="row" id="imagelist">
{%- for image in images %} {%- for image in images %}
<div class="column"> <div class="column">
<figure> <figure>
@@ -157,9 +175,9 @@
var items = [ var items = [
{%- for image in images %} {%- for image in images %}
{%- if image.exifdata.DateTime %} {%- if image.exifdata.DateTime %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}", title: "Captured: {{ image.exifdata.DateTime }}" }, { src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}", tags: "{{ image.tags }}", title: "Captured: {{ image.exifdata.DateTime }}" },
{%- else %} {%- else %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" }, { src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}", tags: "{{ image.tags }}" },
{%- endif %} {%- endif %}
{%- endfor %} {%- endfor %}
]; ];
@@ -204,13 +222,42 @@
fetch(urlToFetch, { fetch(urlToFetch, {
method: 'get', method: 'get',
signal: signal, signal: signal,
}).catch(function (err) {}); }).catch(function (err) { });
} }
function cancel(img) { function cancel(img) {
controllers[img].abort(); controllers[img].abort();
delete controllers[img]; delete controllers[img];
} }
{%- if tags|length > 0 %}
function filter() {
var selected_tags = [];
var tagdropdown, imagelist, figures, i, j, tags, incl;
tagdropdown = document.getElementById("tagdropdown").getElementsByTagName("li");
for (i = 0; i < tagdropdown.length; i++) {
if (tagdropdown[i].firstChild.firstChild.checked) {
selected_tags.push([tagdropdown[i].innerText])
}
}
imagelist = document.getElementById("imagelist");
figures = imagelist.getElementsByTagName("div");
for (i = 0; i < figures.length; i++) {
tags = items[i].tags;
incl = true;
for (j = 0; j < selected_tags.length; j++) {
if (tags.indexOf(selected_tags[j]) == -1) {
incl = false;
}
}
if (incl || selected_tags == []) {
figures[i].style.display = "";
} else {
figures[i].style.display = "none";
}
}
}
{%- endif %}
</script> </script>
{%- endif %} {%- endif %}
</body> </body>

View File

@@ -6,18 +6,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>example - LICENSE</title> <title>example - LICENSE</title>
<link rel="manifest" href="/.static/manifest.json"> <link rel="manifest" href="/.static/manifest.json">
<link rel="preload" href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/.static/global.css" as="style"> <link rel="preload" href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/.static/global.css" as="style">
<link rel="preload" href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/.static/theme.css" as="style"> <link rel="preload" href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/.static/theme.css" as="style">
<link rel="icon" type="image/x-icon" href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/.static/favicon.ico"> <link rel="icon" type="image/x-icon" href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/.static/favicon.ico">
<link rel="stylesheet" href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/.static/global.css"> <link rel="stylesheet" href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/.static/global.css">
<link rel="stylesheet" href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/.static/theme.css"> <link rel="stylesheet" href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/.static/theme.css">
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<ul class="navbar"> <ul class="navbar">
<li><a href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/">Home</a></li> <li><a href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/">Home</a></li>
<li><a href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/example/">Parent Directory</a></li> <li><a href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/example/">Parent Directory</a></li>
<li class="title"><span class="header">example - LICENSE</span></li> <li class="title"><span class="header">example - LICENSE</span></li>
</ul> </ul>
</div> </div>
@@ -453,14 +453,14 @@ Creative Commons may be contacted at creativecommons.org.</br>
</div> </div>
<div class="footer" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"> <div class="footer" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/test/">Pictures</a> by <span property="cc:attributionName">Author</span> is licensed under <a property="dct:title" rel="cc:attributionURL" href="file:///home/user/git/github.com/greflm13/simple-picture-server/test/">Pictures</a> by <span property="cc:attributionName">Author</span> is licensed under
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="license noopener noreferrer">CC BY-NC-SA 4.0 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="license noopener noreferrer">CC BY-NC-SA 4.0
<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" /> <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" />
<img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" /> <img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" />
<img src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" /> <img src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" />
<img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" /> <img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
</a> </a>
<span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder 2.5.0</a> by <a <span class="attribution">Made with <a href="https://github.com/greflm13/StaticGalleryBuilder" target="_blank" rel="noopener noreferrer">StaticGalleryBuilder 2.7.0</a> by <a
href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer"><svg width="48.858002mm" height="21.24mm" viewBox="0 0 48.858002 21.24" version="1.1" id="svg1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <text xml:space="preserve" x="21.124891" y="17.106812" font-style="normal" font-variant="normal" font-weight="300" font-stretch="condensed" font-size="2.46944px" line-height="9.33327px" font-family="Barlow Condensed Light" fill="#6e6e6e" stroke-width="1" fill-opacity="1">sorogon</text> <text xml:space="preserve" x="2.3734004" y="14.853325" font-style="normal" font-variant="normal" font-weight="250" font-stretch="condensed" font-size="16.9333px" line-height="63.9997px" font-family="Barlow Condensed Thin" fill="#6e6e6e" stroke-width="1" fill-opacity="1">&lt;/srgn&gt;</text> </svg></a>.</span> href="https://github.com/greflm13" target="_blank" rel="noopener noreferrer"><svg width="48.858002mm" height="21.24mm" viewBox="0 0 48.858002 21.24" version="1.1" id="svg1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <text xml:space="preserve" x="21.124891" y="17.106812" font-style="normal" font-variant="normal" font-weight="300" font-stretch="condensed" font-size="2.46944px" line-height="9.33327px" font-family="Barlow Condensed Light" fill="#6e6e6e" stroke-width="1" fill-opacity="1">sorogon</text> <text xml:space="preserve" x="2.3734004" y="14.853325" font-style="normal" font-variant="normal" font-weight="250" font-stretch="condensed" font-size="16.9333px" line-height="63.9997px" font-family="Barlow Condensed Thin" fill="#6e6e6e" stroke-width="1" fill-opacity="1">&lt;/srgn&gt;</text> </svg></a>.</span>
<button onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button> <button onclick="topFunction()" id="totop" title="Back to Top">Back to Top</button>
</div> </div>

View File

@@ -97,6 +97,10 @@ body {
background-color: var(--color6); background-color: var(--color6);
} }
.tagentry:hover {
background-color: var(--color3);
}
.column img { .column img {
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }

View File

@@ -96,6 +96,10 @@ body {
background-color: var(--color3); background-color: var(--color3);
} }
.tagentry:hover {
background-color: var(--color7);
}
.column img { .column img {
background-color: var(--bcolor1); background-color: var(--bcolor1);
} }

View File

@@ -74,6 +74,10 @@
background-color: var(--bcolor1); background-color: var(--bcolor1);
} }
.tagentry:hover {
background-color: var(--bcolor3);
}
.column img { .column img {
background-color: var(--bcolor1); background-color: var(--bcolor1);
} }

View File

@@ -74,6 +74,10 @@
background-color: var(--bcolor1); background-color: var(--bcolor1);
} }
.tagentry:hover {
background-color: var(--bcolor3);
}
.column img { .column img {
background-color: var(--bcolor1); background-color: var(--bcolor1);
} }

View File

@@ -79,6 +79,10 @@
font-family: "Playfair Display", serif; font-family: "Playfair Display", serif;
} }
.tagentry:hover {
background-color: var(--color3);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -73,6 +73,10 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover {
background-color: var(--bcolor4);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -79,6 +79,10 @@
font-family: "Nunito", sans-serif; font-family: "Nunito", sans-serif;
} }
.tagentry:hover {
background-color: var(--color4);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -73,6 +73,10 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover {
background-color: var(--bcolor4);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -52,6 +52,10 @@
background-color: var(--color3); background-color: var(--color3);
} }
.tagentry:hover {
background-color: var(--color4);
}
.column img { .column img {
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }

View File

@@ -52,6 +52,10 @@
background-color: var(--color2); background-color: var(--color2);
} }
.tagentry:hover {
background-color: var(--color4);
}
.column img { .column img {
background-color: var(--color2); background-color: var(--color2);
} }

View File

@@ -73,6 +73,10 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover {
background-color: var(--bcolor4);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -72,6 +72,10 @@
background-color: var(--color3); background-color: var(--color3);
} }
.tagentry:hover {
background-color: var(--color2);
}
.column img { .column img {
background-color: var(--bcolor3); background-color: var(--bcolor3);
} }

View File

@@ -55,6 +55,10 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover {
background-color: var(--bcolor3);
}
.column img { .column img {
background-color: var(--bcolor5); background-color: var(--bcolor5);
} }

View File

@@ -76,6 +76,10 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover {
background-color: var(--bcolor4);
}
.column img { .column img {
background-color: var(--bcolor5); background-color: var(--bcolor5);
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -79,6 +79,10 @@
font-family: "Lora", serif; font-family: "Lora", serif;
} }
.tagentry:hover {
background-color: var(--bcolor3);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -80,6 +80,10 @@
background-color: var(--color4); background-color: var(--color4);
} }
.tagentry:hover {
background-color: var(--color3);
}
.column img { .column img {
background-color: var(--color4); background-color: var(--color4);
} }

View File

@@ -80,6 +80,11 @@
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
} }
.tagentry:hover {
background-color: var(--bcolor3);
color: var(--bcolor2);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -74,6 +74,10 @@
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }
.tagentry:hover {
background-color: var(--bcolor4);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }

View File

@@ -88,6 +88,10 @@
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
} }
.tagentry:hover {
background-color: var(--color2);
}
.column img { .column img {
background-color: var(--bcolor4); background-color: var(--bcolor4);
} }