Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
00b5020642
|
|||
|
492ea8755f
|
|||
|
39da474db6
|
|||
|
44bcd5607f
|
|||
|
5ff44a1912
|
|||
|
4241f3965a
|
|||
|
002e9c62db
|
|||
| cf494401c8 | |||
|
2a0323e579
|
|||
|
7e23b3625a
|
|||
|
bce51dc3d6
|
|||
| 57250b3adc |
2
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3.12
|
StaticGalleryBuilder
|
||||||
|
|||||||
@@ -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 xmp `subject` or to `.metadata.json` to tag images for filtering.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
53
builder.py
@@ -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,7 +173,8 @@ 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:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as res:
|
||||||
logo = res.read().decode()
|
logo = res.read().decode()
|
||||||
|
|
||||||
if logo.startswith("<?xml"):
|
if logo.startswith("<?xml"):
|
||||||
@@ -215,6 +183,8 @@ def main(args) -> None:
|
|||||||
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 = "</srgn>"
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,17 @@ 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)
|
||||||
|
try:
|
||||||
with Image.open(file) as img:
|
with Image.open(file) as img:
|
||||||
logger.info("extracting image information", extra={"file": file})
|
logger.info("extracting image information", extra={"file": file})
|
||||||
exif = img.getexif()
|
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
|
exif = img.getexif()
|
||||||
|
xmpdata = img.getxmp()
|
||||||
|
|
||||||
|
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, "xmp": 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 +129,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 +143,29 @@ 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}
|
|
||||||
else:
|
else:
|
||||||
return {"width": width, "height": height, "exifdata": None}
|
exifdata = None
|
||||||
|
tags = []
|
||||||
|
xmp = None
|
||||||
|
if xmpdata:
|
||||||
|
if xmpdata.get("xmpmeta", False):
|
||||||
|
if isinstance(xmpdata["xmpmeta"]["RDF"]["Description"], dict):
|
||||||
|
if xmpdata["xmpmeta"]["RDF"]["Description"].get("subject", False):
|
||||||
|
tags = xmpdata["xmpmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
|
||||||
|
if isinstance(tags, str):
|
||||||
|
tags = [tags]
|
||||||
|
xmp = xmpdata
|
||||||
|
if xmpdata.get("xapmeta", False):
|
||||||
|
if isinstance(xmpdata["xapmeta"]["RDF"]["Description"], dict):
|
||||||
|
if xmpdata["xapmeta"]["RDF"]["Description"].get("subject", False):
|
||||||
|
tags = xmpdata["xapmeta"]["RDF"]["Description"]["subject"]["Bag"]["li"]
|
||||||
|
if isinstance(tags, str):
|
||||||
|
tags = [tags]
|
||||||
|
xmp = xmpdata
|
||||||
|
return {"width": width, "height": height, "tags": tags, "exifdata": exifdata, "xmp": xmp}
|
||||||
|
|
||||||
|
|
||||||
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 +174,25 @@ 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", ""),
|
||||||
|
"xmp": metadata[item].get("xmp", ""),
|
||||||
}
|
}
|
||||||
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 +225,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,10 +240,21 @@ 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})
|
||||||
|
if not _args.non_interactive_mode:
|
||||||
|
for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True):
|
||||||
|
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:
|
for item in items:
|
||||||
if item not in EXCLUDES and not item.startswith("."):
|
if item not in EXCLUDES and not item.startswith("."):
|
||||||
if os.path.isdir(os.path.join(folder, item)):
|
if os.path.isdir(os.path.join(folder, item)):
|
||||||
@@ -220,19 +262,13 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
|
|||||||
else:
|
else:
|
||||||
contains_files = True
|
contains_files = True
|
||||||
if os.path.splitext(item)[1].lower() in _args.file_extensions:
|
if os.path.splitext(item)[1].lower() in _args.file_extensions:
|
||||||
images.append(process_image(item, folder, _args, baseurl, sizelist, raw))
|
images.append(process_image(item, folder, _args, baseurl, metadata, raw))
|
||||||
if item == "info":
|
if item == "info":
|
||||||
process_info_file(folder, item)
|
process_info_file(folder, item)
|
||||||
if item == "LICENSE":
|
if item == "LICENSE":
|
||||||
process_license(folder, item)
|
process_license(folder, item)
|
||||||
|
|
||||||
if not _args.non_interactive_mode:
|
update_metadata(metadata, folder)
|
||||||
pbardict[folder].update(1)
|
|
||||||
|
|
||||||
if not _args.non_interactive_mode:
|
|
||||||
pbardict[folder].close()
|
|
||||||
|
|
||||||
update_sizelist(sizelist, 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 +277,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 +292,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 +304,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 +334,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(" ", " ").replace(" ", " ").replace("sp; ", "sp; ").replace("  ", " ")
|
licens[urllib.parse.quote(folder)] = (
|
||||||
|
f.read().replace("\n", "</br>\n").replace(" ", " ").replace(" ", " ").replace("sp; ", "sp; ").replace("  ", " ")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_info_file(folder: str, item: str) -> None:
|
def process_info_file(folder: str, item: str) -> None:
|
||||||
@@ -349,6 +397,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 +451,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 +459,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 +473,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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,23 +31,38 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<ul class="navbar">
|
<ol class="navbar">
|
||||||
|
<div class="navleft">
|
||||||
<li><a href="{{ root }}">Home</a></li>
|
<li><a href="{{ root }}">Home</a></li>
|
||||||
{%- if parent %}
|
{%- if parent %}
|
||||||
<li><a href="{{ parent }}">Parent Directory</a></li>
|
<li><a href="{{ parent }}">Parent Directory</a></li>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if info %}
|
{%- if info %}
|
||||||
<li class="tooltip"><a>Info</a><span class="tooltiptext">
|
<li class="tooltip"><a>Info</a><span class="tooltiptext infotext">
|
||||||
{%- for infoline in info -%}
|
{%- for infoline in info -%}
|
||||||
{{ infoline }}<br />
|
{{ infoline }}<br />
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</span></li>
|
</span></li>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
<div class="navcenter">
|
||||||
<li class="title"><span class="header">{{ header }}</span></li>
|
<li class="title"><span class="header">{{ header }}</span></li>
|
||||||
|
</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 %}
|
{%- if licensefile %}
|
||||||
<li class="license"><a href="{{ licensefile }}">License</a></li>
|
<li class="license"><a href="{{ licensefile }}">License</a></li>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</ul>
|
</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 %}
|
||||||
];
|
];
|
||||||
@@ -211,6 +229,35 @@
|
|||||||
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>
|
||||||
|
|||||||
BIN
test/example/DSC00009.jpg
Normal file
|
After Width: | Height: | Size: 28 MiB |
BIN
test/example/DSC01106.jpg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
@@ -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.1</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"></srgn></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"></srgn></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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||