Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0973868782 | |||
| cd06c526af | |||
| d155b05798 | |||
| 3352894e4d | |||
| d743ede95d | |||
| 9403e84d78 | |||
| 961d79754e | |||
| 383dd59851 | |||
| 549c15ca6c | |||
| 57d949677f | |||
| cc6ad14506 | |||
| cace0f8593 | |||
| 0484200fad | |||
| 74d97dac2a | |||
| 6c636905e2 | |||
| c79473d646 | |||
|
3cb5269985
|
|||
|
3bde09aebc
|
|||
|
effc05826a
|
|||
|
8ec9701aa9
|
|||
| a95a6e1722 | |||
| 9de971d2ac | |||
|
e0cb13771c
|
|||
| cd3f2959c0 | |||
| 285d286baf | |||
| 08299abd2a | |||
| d76d2e146d | |||
| f52b0e7778 | |||
| d6a4e1cc82 |
7
.github/workflows/build-release.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: build-release
|
name: build-release
|
||||||
run-name: build-release
|
run-name: build-release
|
||||||
on: [push]
|
on: push
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -12,11 +12,12 @@ jobs:
|
|||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip install -r requirements.txt
|
run: pip install -r requirements.txt
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder -F --add-data files:files --add-data templates:templates --add-data .version:.
|
run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
make_latest: true
|
make_latest: true
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
dist/StaticGalleryBuilder
|
dist/StaticGalleryBuilder*
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -167,3 +167,4 @@ test/**/index.html
|
|||||||
test/**/.sizelist.json
|
test/**/.sizelist.json
|
||||||
test/manifest.json
|
test/manifest.json
|
||||||
themes/previews
|
themes/previews
|
||||||
|
logs
|
||||||
23
README.md
@@ -26,13 +26,14 @@ Python script to generate static HTML files for website galleries.
|
|||||||
- `Pillow` library
|
- `Pillow` library
|
||||||
- `rich_argparse` library
|
- `rich_argparse` library
|
||||||
- `cairosvg` library (for SVG to PNG icon conversion)
|
- `cairosvg` library (for SVG to PNG icon conversion)
|
||||||
|
- `python-json-logger` library (for logging)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install the required libraries using pip:
|
Install the required libraries using pip:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg
|
pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg python-json-logger
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -43,20 +44,20 @@ The script supports several command-line options to customize its behavior. Belo
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
- `-h, --help`: Show the help message and exit.
|
|
||||||
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. This option is required.
|
|
||||||
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. This option is required.
|
|
||||||
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. This option is required.
|
|
||||||
- `-r, --regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
|
|
||||||
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
|
|
||||||
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
|
|
||||||
- `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author".
|
- `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author".
|
||||||
- `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times.
|
- `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times.
|
||||||
|
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
|
||||||
|
- `-m, --web-manifest`: Generate a web manifest file.
|
||||||
|
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
|
||||||
|
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. **(This option is required)**.
|
||||||
|
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. **(This option is required)**.
|
||||||
|
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. **(This option is required)**.
|
||||||
|
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
|
||||||
|
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
|
||||||
|
- `--regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
|
||||||
|
- `--reread-metadata`: Reread image metadata if it already exists.
|
||||||
- `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme.
|
- `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme.
|
||||||
- `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing.
|
- `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing.
|
||||||
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
|
|
||||||
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
|
|
||||||
- `-m, --web-manifest`: Generate a web manifest file.
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@
|
|||||||
"cc-by-nc-sa",
|
"cc-by-nc-sa",
|
||||||
"-n",
|
"-n",
|
||||||
"-m",
|
"-m",
|
||||||
"-r"
|
"--regenerate-thumbnails",
|
||||||
|
"--reread-metadata",
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"name": "Testfolder",
|
"name": "Testfolder",
|
||||||
@@ -57,12 +58,13 @@
|
|||||||
"-t",
|
"-t",
|
||||||
"Pictures",
|
"Pictures",
|
||||||
"--theme",
|
"--theme",
|
||||||
"themes/catpuccin.css",
|
"themes/default.css",
|
||||||
"--use-fancy-folders",
|
"--use-fancy-folders",
|
||||||
"--web-manifest",
|
"--web-manifest",
|
||||||
"-n",
|
"-n",
|
||||||
"-m",
|
"-m",
|
||||||
"-r",
|
// "--regenerate-thumbnails",
|
||||||
|
// "--reread-metadata",
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"name": "woek",
|
"name": "woek",
|
||||||
@@ -101,7 +103,7 @@
|
|||||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||||
},
|
},
|
||||||
"black-formatter.args": [
|
"black-formatter.args": [
|
||||||
"-l 140",
|
"-l 260",
|
||||||
],
|
],
|
||||||
"black-formatter.interpreter": [
|
"black-formatter.interpreter": [
|
||||||
"/usr/bin/python3",
|
"/usr/bin/python3",
|
||||||
@@ -147,6 +149,9 @@
|
|||||||
"python.analysis.inlayHints.callArgumentNames": "off",
|
"python.analysis.inlayHints.callArgumentNames": "off",
|
||||||
"python.analysis.inlayHints.functionReturnTypes": false,
|
"python.analysis.inlayHints.functionReturnTypes": false,
|
||||||
"python.analysis.inlayHints.variableTypes": false,
|
"python.analysis.inlayHints.variableTypes": false,
|
||||||
|
"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"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -157,6 +162,14 @@
|
|||||||
"label": "Delete Lockfile",
|
"label": "Delete Lockfile",
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
"presentation": {
|
||||||
|
"echo": false,
|
||||||
|
"reveal": "never",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "rm -f /home/user/woek/Pictures/.lock",
|
"command": "rm -f /home/user/woek/Pictures/.lock",
|
||||||
@@ -164,7 +177,68 @@
|
|||||||
"label": "Delete Lockfile 2",
|
"label": "Delete Lockfile 2",
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
"presentation": {
|
||||||
|
"echo": false,
|
||||||
|
"reveal": "never",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.",
|
||||||
|
"isBackground": false,
|
||||||
|
"label": "Build",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"type": "shell",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": false
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"dependsOn": [
|
||||||
|
"Clean"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "rm -rf build dist",
|
||||||
|
"isBackground": true,
|
||||||
|
"label": "Clean",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"type": "shell",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "never",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true
|
||||||
|
},
|
||||||
|
"group": "build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "LESS=-SR hl logs/latest.jsonl --config hl_config.yaml",
|
||||||
|
"isBackground": false,
|
||||||
|
"label": "View Latest Log",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"type": "shell",
|
||||||
|
"presentation": {
|
||||||
|
"echo": false,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "dedicated",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
58
builder.py
@@ -11,17 +11,21 @@ from typing import Dict, List, Tuple
|
|||||||
from tqdm.auto import tqdm
|
from tqdm.auto import tqdm
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
|
|
||||||
|
from modules.logger import logger
|
||||||
from modules.argumentparser import parse_arguments, Args
|
from modules.argumentparser import parse_arguments, Args
|
||||||
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, EXCLUDES
|
||||||
|
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
# Constants
|
# Constants
|
||||||
if __package__ == None:
|
if __package__ is None:
|
||||||
__package__ = ""
|
PACKAGE = ""
|
||||||
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__).removesuffix(__package__))
|
else:
|
||||||
STATIC_FILES_DIR = os.path.join(os.path.abspath(SCRIPT_DIR), "files")
|
PACKAGE = __package__
|
||||||
VERSION = open(os.path.join(SCRIPT_DIR, ".version"), "r", encoding="utf-8").read()
|
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
|
||||||
|
STATIC_FILES_DIR = os.path.join(os.path.abspath(SCRIPTDIR), "files")
|
||||||
|
VERSION = open(os.path.join(SCRIPTDIR, ".version"), "r", encoding="utf-8").read()
|
||||||
RAW_EXTENSIONS = [
|
RAW_EXTENSIONS = [
|
||||||
".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr",
|
".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr",
|
||||||
".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos",
|
".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos",
|
||||||
@@ -75,10 +79,13 @@ def copy_static_files(_args: Args) -> None:
|
|||||||
static_dir = os.path.join(_args.root_directory, ".static")
|
static_dir = os.path.join(_args.root_directory, ".static")
|
||||||
if os.path.exists(static_dir):
|
if os.path.exists(static_dir):
|
||||||
print("Removing existing .static folder...")
|
print("Removing existing .static folder...")
|
||||||
|
logger.info("removing existing .static folder")
|
||||||
shutil.rmtree(static_dir)
|
shutil.rmtree(static_dir)
|
||||||
|
|
||||||
print("Copying static files...")
|
print("Copying static files...")
|
||||||
|
logger.info("copying static files")
|
||||||
shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True)
|
shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True)
|
||||||
|
logger.info("reading theme file", extra={"theme": _args.theme_path})
|
||||||
with open(_args.theme_path, "r", encoding="utf-8") as f:
|
with open(_args.theme_path, "r", encoding="utf-8") as f:
|
||||||
theme = f.read()
|
theme = f.read()
|
||||||
split = theme.split(".foldericon {")
|
split = theme.split(".foldericon {")
|
||||||
@@ -90,20 +97,24 @@ def copy_static_files(_args: Args) -> None:
|
|||||||
for match in re.finditer(r"content: (.*);", foldericon):
|
for match in re.finditer(r"content: (.*);", foldericon):
|
||||||
foldericon = match[1]
|
foldericon = match[1]
|
||||||
foldericon = foldericon.replace('"', "")
|
foldericon = foldericon.replace('"', "")
|
||||||
|
logger.info("found foldericon", extra={"foldericon": foldericon})
|
||||||
break
|
break
|
||||||
if "url" in foldericon:
|
if "url" in foldericon:
|
||||||
|
logger.info("foldericon in theme file, using it")
|
||||||
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
|
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
|
||||||
return
|
return
|
||||||
with open(os.path.join(SCRIPT_DIR, foldericon), "r", encoding="utf-8") as f:
|
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
|
||||||
|
logger.info("Reading foldericon svg")
|
||||||
svg = f.read()
|
svg = f.read()
|
||||||
if "svg.j2" in foldericon:
|
if "svg.j2" in foldericon:
|
||||||
|
logger.info("foldericon in theme file is a jinja2 template")
|
||||||
colorscheme = extract_colorscheme(_args.theme_path)
|
colorscheme = extract_colorscheme(_args.theme_path)
|
||||||
svg = svg.replace("{{ color1 }}", colorscheme["color1"])
|
for color_key, color_value in colorscheme.items():
|
||||||
svg = svg.replace("{{ color2 }}", colorscheme["color2"])
|
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
|
||||||
svg = svg.replace("{{ color3 }}", colorscheme["color3"])
|
logger.info("replaced colors in svg")
|
||||||
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
|
|
||||||
svg = urllib.parse.quote(svg)
|
svg = urllib.parse.quote(svg)
|
||||||
with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f:
|
with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f:
|
||||||
|
logger.info("writing theme file")
|
||||||
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
|
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
|
||||||
|
|
||||||
|
|
||||||
@@ -117,6 +128,7 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
|
|||||||
A tuple containing the folder, item, root directory, and regenerate thumbnails flag.
|
A tuple containing the folder, item, root directory, and regenerate thumbnails flag.
|
||||||
"""
|
"""
|
||||||
folder, item, root_directory = arguments
|
folder, item, root_directory = arguments
|
||||||
|
image = os.path.join(folder, item)
|
||||||
path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), item) + ".jpg"
|
path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), item) + ".jpg"
|
||||||
oldpath = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".jpg"
|
oldpath = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".jpg"
|
||||||
if os.path.exists(oldpath):
|
if os.path.exists(oldpath):
|
||||||
@@ -125,14 +137,19 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
|
logger.info("generating thumbnail for %s", item, extra={"path": image})
|
||||||
try:
|
try:
|
||||||
with Image.open(os.path.join(folder, item)) as imgfile:
|
with Image.open(image) as imgfile:
|
||||||
imgrgb = imgfile.convert("RGB")
|
imgrgb = imgfile.convert("RGB")
|
||||||
img = ImageOps.exif_transpose(imgrgb)
|
img = ImageOps.exif_transpose(imgrgb)
|
||||||
img.thumbnail((512, 512))
|
img.thumbnail((512, 512))
|
||||||
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB")
|
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB")
|
||||||
except OSError:
|
except OSError:
|
||||||
print(f"Failed to generate thumbnail for {os.path.join(folder, item)}")
|
logger.error("Failed to generate thumbnail for %s", item, extra={"path": image})
|
||||||
|
print(f"Failed to generate thumbnail for {image}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.debug("thumbnail already exists for %s", item, extra={"path": image})
|
||||||
|
|
||||||
|
|
||||||
def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
|
def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
|
||||||
@@ -159,8 +176,9 @@ def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
|
|||||||
|
|
||||||
items = sorted(os.listdir(folder))
|
items = sorted(os.listdir(folder))
|
||||||
for item in items:
|
for item in items:
|
||||||
if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)):
|
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):
|
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)
|
_total = get_total_folders(os.path.join(folder, item), _args, _total)
|
||||||
return _total
|
return _total
|
||||||
|
|
||||||
@@ -169,6 +187,7 @@ def main() -> 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.
|
||||||
"""
|
"""
|
||||||
|
logger.info("starting builder", extra={"version": VERSION})
|
||||||
thumbnails: List[Tuple[str, str, str, bool]] = []
|
thumbnails: List[Tuple[str, str, str, bool]] = []
|
||||||
|
|
||||||
args = parse_arguments(VERSION)
|
args = parse_arguments(VERSION)
|
||||||
@@ -177,10 +196,18 @@ def main() -> None:
|
|||||||
lock_file = os.path.join(args.root_directory, ".lock")
|
lock_file = os.path.join(args.root_directory, ".lock")
|
||||||
if os.path.exists(lock_file):
|
if os.path.exists(lock_file):
|
||||||
print("Another instance of this program is running.")
|
print("Another instance of this program is running.")
|
||||||
|
logger.error("another instance of this program is running")
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Path(lock_file).touch()
|
Path(lock_file).touch()
|
||||||
|
if args.reread_metadata:
|
||||||
|
logger.warning("reread metadata flag is set to true, all image metadata will be reread")
|
||||||
|
if args.regenerate_thumbnails:
|
||||||
|
logger.warning("regenerate thumbnails flag is set to true, all thumbnails will be regenerated")
|
||||||
|
if os.path.exists(os.path.join(args.root_directory, ".thumbnails")):
|
||||||
|
logger.info("removing old thumbnails folder")
|
||||||
|
shutil.rmtree(os.path.join(args.root_directory, ".thumbnails"))
|
||||||
os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True)
|
os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True)
|
||||||
|
|
||||||
copy_static_files(args)
|
copy_static_files(args)
|
||||||
@@ -191,13 +218,16 @@ def main() -> None:
|
|||||||
webmanifest(args)
|
webmanifest(args)
|
||||||
|
|
||||||
if args.non_interactive_mode:
|
if args.non_interactive_mode:
|
||||||
|
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)
|
thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION)
|
||||||
with Pool(os.cpu_count()) as pool:
|
with Pool(os.cpu_count()) as pool:
|
||||||
|
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)
|
pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True)
|
||||||
|
logger.info("getting total number of folders to process")
|
||||||
total = get_total_folders(args.root_directory, args)
|
total = get_total_folders(args.root_directory, args)
|
||||||
pbardict["traversingbar"].desc = "Traversing filesystem"
|
pbardict["traversingbar"].desc = "Traversing filesystem"
|
||||||
pbardict["traversingbar"].update(0)
|
pbardict["traversingbar"].update(0)
|
||||||
@@ -206,6 +236,7 @@ def main() -> None:
|
|||||||
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION)
|
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION)
|
||||||
|
|
||||||
with Pool(os.cpu_count()) as pool:
|
with Pool(os.cpu_count()) as pool:
|
||||||
|
logger.info("generating thumbnails")
|
||||||
for _ in tqdm(
|
for _ in tqdm(
|
||||||
pool.imap_unordered(generate_thumbnail, thumbnails),
|
pool.imap_unordered(generate_thumbnail, thumbnails),
|
||||||
total=len(thumbnails),
|
total=len(thumbnails),
|
||||||
@@ -217,6 +248,7 @@ def main() -> None:
|
|||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
os.remove(lock_file)
|
os.remove(lock_file)
|
||||||
|
logger.info("finished builder", extra={"version": VERSION})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ figure {
|
|||||||
.footer a img {
|
.footer a img {
|
||||||
height: 22px !important;
|
height: 22px !important;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
vertical-align: text-bottom
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
@@ -157,12 +157,12 @@ figure {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create eight equal columns that sits next to each other */
|
|
||||||
.column {
|
.column {
|
||||||
-ms-flex: 12.5%;
|
-ms-flex: 12.5%;
|
||||||
flex: 12.5%;
|
flex: 12.5%;
|
||||||
max-width: 12.5%;
|
max-width: 12.5%;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column img {
|
.column img {
|
||||||
@@ -187,7 +187,6 @@ figure {
|
|||||||
border-style: none;
|
border-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive layout - makes a four column-layout instead of eight columns */
|
|
||||||
@media screen and (max-width: 1000px) {
|
@media screen and (max-width: 1000px) {
|
||||||
.column {
|
.column {
|
||||||
-ms-flex: 25%;
|
-ms-flex: 25%;
|
||||||
@@ -209,7 +208,6 @@ figure {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive layout - makes a two column-layout instead of four columns */
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
.column {
|
.column {
|
||||||
-ms-flex: 50%;
|
-ms-flex: 50%;
|
||||||
@@ -239,7 +237,6 @@ figure {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
.column {
|
.column {
|
||||||
-ms-flex: 100%;
|
-ms-flex: 100%;
|
||||||
|
|||||||
@@ -272,13 +272,14 @@ a.pswp__share--download:hover {
|
|||||||
color: #BBB; }
|
color: #BBB; }
|
||||||
|
|
||||||
.pswp__caption__center {
|
.pswp__caption__center {
|
||||||
text-align: left;
|
text-align: center;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: #CCC; }
|
color: #CCC;
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
.pswp__caption--empty {
|
.pswp__caption--empty {
|
||||||
display: none; }
|
display: none; }
|
||||||
|
|||||||
@@ -4,21 +4,18 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import shutil
|
import shutil
|
||||||
import base64
|
import base64
|
||||||
import logging
|
|
||||||
import fileinput
|
import fileinput
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import List
|
from typing import List
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.chrome.service import Service
|
from selenium.webdriver.chrome.service import Service
|
||||||
from selenium.webdriver.chrome.options import Options
|
from selenium.webdriver.chrome.options import Options
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
|
|
||||||
|
from modules.logger import consolelogger as logger
|
||||||
from modules.css_color import extract_colorscheme
|
from modules.css_color import extract_colorscheme
|
||||||
|
|
||||||
# Set up logging
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
||||||
|
|
||||||
|
|
||||||
def replace_all(file, search_exp, replace_exp):
|
def replace_all(file, search_exp, replace_exp):
|
||||||
for line in fileinput.input(file, inplace=1):
|
for line in fileinput.input(file, inplace=1):
|
||||||
@@ -36,11 +33,14 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
|||||||
output_file (str): Path where the screenshot will be saved.
|
output_file (str): Path where the screenshot will be saved.
|
||||||
driver (webdriver.Chrome): The Chrome WebDriver instance.
|
driver (webdriver.Chrome): The Chrome WebDriver instance.
|
||||||
"""
|
"""
|
||||||
|
logger.info("taking screenshot for %s", css_file)
|
||||||
try:
|
try:
|
||||||
# Open the HTML file or URL
|
# Open the HTML file or URL
|
||||||
if html_file_path.startswith(("http://", "https://")):
|
if html_file_path.startswith(("http://", "https://")):
|
||||||
|
logger.info("opening URL: %s", html_file_path)
|
||||||
driver.get(html_file_path)
|
driver.get(html_file_path)
|
||||||
else:
|
else:
|
||||||
|
logger.info("opening file: %s", html_file_path)
|
||||||
driver.get(f"file://{os.path.abspath(html_file_path)}")
|
driver.get(f"file://{os.path.abspath(html_file_path)}")
|
||||||
|
|
||||||
# Remove current theme.css
|
# Remove current theme.css
|
||||||
@@ -52,9 +52,11 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
"""
|
"""
|
||||||
|
logger.info("removing current theme.css")
|
||||||
driver.execute_script(remove_css_script)
|
driver.execute_script(remove_css_script)
|
||||||
|
|
||||||
with open(css_file, "r", encoding="utf-8") as f:
|
with open(css_file, "r", encoding="utf-8") as f:
|
||||||
|
logger.info("reading CSS file: %s", css_file)
|
||||||
css_content = f.read()
|
css_content = f.read()
|
||||||
|
|
||||||
# Extract folder icon content
|
# Extract folder icon content
|
||||||
@@ -65,20 +67,26 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
|||||||
folder_icon_content = re.sub(r"/\*.*\*/", "", folder_icon_content)
|
folder_icon_content = re.sub(r"/\*.*\*/", "", folder_icon_content)
|
||||||
|
|
||||||
for match in re.finditer(r"content: (.*);", folder_icon_content):
|
for match in re.finditer(r"content: (.*);", folder_icon_content):
|
||||||
|
logger.info("found foldericon", extra={"foldericon": folder_icon_content})
|
||||||
folder_icon_content = match.group(1).replace('"', "")
|
folder_icon_content = match.group(1).replace('"', "")
|
||||||
break
|
break
|
||||||
|
|
||||||
if "url" not in folder_icon_content:
|
if "url" not in folder_icon_content:
|
||||||
|
logger.info("Reading foldericon svg")
|
||||||
with open(folder_icon_content, "r", encoding="utf-8") as f:
|
with open(folder_icon_content, "r", encoding="utf-8") as f:
|
||||||
svg = f.read()
|
svg = f.read()
|
||||||
if "svg.j2" in folder_icon_content:
|
if "svg.j2" in folder_icon_content:
|
||||||
|
logger.info("foldericon in theme file is a jinja2 template")
|
||||||
colorscheme = extract_colorscheme(css_file)
|
colorscheme = extract_colorscheme(css_file)
|
||||||
for color_key, color_value in colorscheme.items():
|
for color_key, color_value in colorscheme.items():
|
||||||
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
|
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
|
||||||
|
logger.info("replaced colors in svg")
|
||||||
svg = urllib.parse.quote(svg)
|
svg = urllib.parse.quote(svg)
|
||||||
|
|
||||||
css_content = f'{css_head}\n.foldericon {{\n content: url("data:image/svg+xml,{svg}");\n}}\n{css_tail}'
|
css_content = f'{css_head}\n.foldericon {{\n content: url("data:image/svg+xml,{svg}");\n}}\n{css_tail}'
|
||||||
|
|
||||||
# Encode CSS content as Base64
|
# Encode CSS content as Base64
|
||||||
|
logger.info("encoding css content as base64")
|
||||||
encoded_css = base64.b64encode(css_content.encode("utf-8")).decode("utf-8")
|
encoded_css = base64.b64encode(css_content.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
# Inject CSS into HTML using JavaScript
|
# Inject CSS into HTML using JavaScript
|
||||||
@@ -87,24 +95,28 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
|
|||||||
style.innerHTML = atob('{encoded_css}');
|
style.innerHTML = atob('{encoded_css}');
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
"""
|
"""
|
||||||
|
logger.info("injecting CSS into HTML")
|
||||||
driver.execute_script(apply_css_script)
|
driver.execute_script(apply_css_script)
|
||||||
|
|
||||||
# Wait for a while to ensure CSS is applied
|
# Wait for a while to ensure CSS is applied
|
||||||
time.sleep(2)
|
# time.sleep(1)
|
||||||
|
|
||||||
# Move mouse to info
|
# Move mouse to info
|
||||||
|
logger.info("moving mouse to info")
|
||||||
hoverable = driver.find_element(By.CLASS_NAME, "tooltip")
|
hoverable = driver.find_element(By.CLASS_NAME, "tooltip")
|
||||||
webdriver.ActionChains(driver).move_to_element(hoverable).perform()
|
webdriver.ActionChains(driver).move_to_element(hoverable).perform()
|
||||||
|
|
||||||
# Capture screenshot
|
# Capture screenshot
|
||||||
|
logger.info("taking screenshot")
|
||||||
driver.save_screenshot(output_file)
|
driver.save_screenshot(output_file)
|
||||||
logging.info("Screenshot saved to %s", output_file)
|
logger.info("screenshot saved to %s", output_file)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Failed to take screenshot for %s: %s", css_file, e)
|
logger.error("failed to take screenshot for %s: %s", css_file, e)
|
||||||
|
|
||||||
|
|
||||||
def create_preview(html_file_path: str, css_file: str, previews_folder: str):
|
def create_preview(html_file_path: str, css_file: str, previews_folder: str):
|
||||||
|
logger.info("creating preview for %s", css_file)
|
||||||
out_file = os.path.basename(css_file).removesuffix(".css") + ".html"
|
out_file = os.path.basename(css_file).removesuffix(".css") + ".html"
|
||||||
urllib.request.urlretrieve(html_file_path, os.path.join(previews_folder, out_file))
|
urllib.request.urlretrieve(html_file_path, os.path.join(previews_folder, out_file))
|
||||||
basename = os.path.basename(css_file)
|
basename = os.path.basename(css_file)
|
||||||
@@ -127,21 +139,25 @@ def create_preview(html_file_path: str, css_file: str, previews_folder: str):
|
|||||||
foldericon = foldericon.replace('"', "")
|
foldericon = foldericon.replace('"', "")
|
||||||
break
|
break
|
||||||
if "url" in foldericon:
|
if "url" in foldericon:
|
||||||
|
logger.info("foldericon in theme file, using it")
|
||||||
shutil.copyfile(css_file, os.path.join(path, "previews", basename))
|
shutil.copyfile(css_file, os.path.join(path, "previews", basename))
|
||||||
else:
|
return
|
||||||
with open(os.path.join(path, foldericon.removeprefix("themes/")), "r", encoding="utf-8") as f:
|
with open(os.path.join(path, foldericon.removeprefix("themes/")), "r", encoding="utf-8") as f:
|
||||||
|
logger.info("Reading foldericon svg")
|
||||||
svg = f.read()
|
svg = f.read()
|
||||||
if "svg.j2" in foldericon:
|
if "svg.j2" in foldericon:
|
||||||
|
logger.info("foldericon in theme file is a jinja2 template")
|
||||||
colorscheme = extract_colorscheme(css_file)
|
colorscheme = extract_colorscheme(css_file)
|
||||||
svg = svg.replace("{{ color1 }}", colorscheme["color1"])
|
for color_key, color_value in colorscheme.items():
|
||||||
svg = svg.replace("{{ color2 }}", colorscheme["color2"])
|
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
|
||||||
svg = svg.replace("{{ color3 }}", colorscheme["color3"])
|
logger.info("replaced colors in svg")
|
||||||
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
|
|
||||||
svg = urllib.parse.quote(svg)
|
svg = urllib.parse.quote(svg)
|
||||||
if os.path.exists(os.path.join(path, "previews", basename)):
|
if os.path.exists(os.path.join(path, "previews", basename)):
|
||||||
os.remove(os.path.join(path, "previews", basename))
|
os.remove(os.path.join(path, "previews", basename))
|
||||||
with open(os.path.join(path, "previews", basename), "x", encoding="utf-8") as f:
|
with open(os.path.join(path, "previews", basename), "x", encoding="utf-8") as f:
|
||||||
|
logger.info("writing theme file")
|
||||||
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
|
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
|
||||||
|
logger.info("preview created for %s", css_file)
|
||||||
|
|
||||||
|
|
||||||
def write_readme(directory_path: str, themes: List[str]) -> None:
|
def write_readme(directory_path: str, themes: List[str]) -> None:
|
||||||
@@ -155,6 +171,7 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
|
|||||||
readme_path = os.path.join(directory_path, "README.md")
|
readme_path = os.path.join(directory_path, "README.md")
|
||||||
try:
|
try:
|
||||||
with open(readme_path, "r", encoding="utf-8") as f:
|
with open(readme_path, "r", encoding="utf-8") as f:
|
||||||
|
logger.info("reading README.md", extra={"file": readme_path})
|
||||||
readme = f.read()
|
readme = f.read()
|
||||||
|
|
||||||
readme_head = readme.split("## Previews of included themes")[0]
|
readme_head = readme.split("## Previews of included themes")[0]
|
||||||
@@ -162,14 +179,15 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
|
|||||||
readme_head += "".join([f"\n### {theme}\n\n\n" for theme in themes])
|
readme_head += "".join([f"\n### {theme}\n\n\n" for theme in themes])
|
||||||
|
|
||||||
with open(readme_path, "w", encoding="utf-8") as f:
|
with open(readme_path, "w", encoding="utf-8") as f:
|
||||||
|
logger.info("writing README.md", extra={"file": readme_path})
|
||||||
f.write(readme_head)
|
f.write(readme_head)
|
||||||
|
|
||||||
logging.info("README.md updated with previews of included themes.")
|
logger.info("README.md updated with previews of included themes.")
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logging.error("README.md not found in %s", directory_path)
|
logger.error("README.md not found in %s", directory_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Failed to write README.md: %s", e)
|
logger.error("failed to write README.md: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def write_index(directory_path: str, themes: List[str]) -> None:
|
def write_index(directory_path: str, themes: List[str]) -> None:
|
||||||
@@ -198,7 +216,7 @@ def main(directory_path: str, html_file_path: str) -> None:
|
|||||||
html_file_path (str): Path to the HTML file or URL for rendering.
|
html_file_path (str): Path to the HTML file or URL for rendering.
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(directory_path):
|
if not os.path.exists(directory_path):
|
||||||
logging.error('Error: Folder path "%s" does not exist.', directory_path)
|
logger.error('Error: Folder path "%s" does not exist.', directory_path)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Setup Chrome options
|
# Setup Chrome options
|
||||||
@@ -207,8 +225,9 @@ def main(directory_path: str, html_file_path: str) -> None:
|
|||||||
chrome_options.add_argument("--window-size=1920,1080") # Set window size to at least 1920x1080
|
chrome_options.add_argument("--window-size=1920,1080") # Set window size to at least 1920x1080
|
||||||
|
|
||||||
# Initialize Chrome WebDriver
|
# Initialize Chrome WebDriver
|
||||||
chromedriver_path = "/usr/bin/chromedriver" # Replace with your actual path
|
chromedriver_path = "/usr/bin/chromedriver"
|
||||||
service = Service(chromedriver_path)
|
service = Service(chromedriver_path)
|
||||||
|
logger.info("Using chromedriver at %s", chromedriver_path, extra={"chrome_options": chrome_options})
|
||||||
driver = webdriver.Chrome(service=service, options=chrome_options)
|
driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -235,13 +254,16 @@ def main(directory_path: str, html_file_path: str) -> None:
|
|||||||
write_index(directory_path, themes)
|
write_index(directory_path, themes)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
logger.info("closing chrome webdriver")
|
||||||
driver.quit()
|
driver.quit()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
logging.error("Usage: python script_name.py directory_path html_file_path")
|
logger.error("Usage: python script_name.py directory_path html_file_path")
|
||||||
else:
|
else:
|
||||||
dir_path = sys.argv[1]
|
dir_path = sys.argv[1]
|
||||||
html_path = sys.argv[2]
|
html_path = sys.argv[2]
|
||||||
|
logger.info("Starting script", extra={"directory_path": dir_path, "html_file_path": html_path})
|
||||||
main(dir_path, html_path)
|
main(dir_path, html_path)
|
||||||
|
logger.info("Done!", extra={"directory_path": dir_path})
|
||||||
|
|||||||
142
help.svg
@@ -19,118 +19,118 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-3756623299-matrix {
|
.terminal-2271905219-matrix {
|
||||||
font-family: Fira Code, monospace;
|
font-family: Fira Code, monospace;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 24.4px;
|
line-height: 24.4px;
|
||||||
font-variant-east-asian: full-width;
|
font-variant-east-asian: full-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-3756623299-title {
|
.terminal-2271905219-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: arial;
|
font-family: arial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-3756623299-r1 { fill: #ff8700 }
|
.terminal-2271905219-r1 { fill: #ff8700 }
|
||||||
.terminal-3756623299-r2 { fill: #c5c8c6 }
|
.terminal-2271905219-r2 { fill: #c5c8c6 }
|
||||||
.terminal-3756623299-r3 { fill: #808080 }
|
.terminal-2271905219-r3 { fill: #808080 }
|
||||||
.terminal-3756623299-r4 { fill: #68a0b3 }
|
.terminal-2271905219-r4 { fill: #68a0b3 }
|
||||||
.terminal-3756623299-r5 { fill: #00af87 }
|
.terminal-2271905219-r5 { fill: #00af87 }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="terminal-3756623299-clip-terminal">
|
<clipPath id="terminal-2271905219-clip-terminal">
|
||||||
<rect x="0" y="0" width="1463.0" height="755.4" />
|
<rect x="0" y="0" width="1463.0" height="755.4" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-0">
|
<clipPath id="terminal-2271905219-line-0">
|
||||||
<rect x="0" y="1.5" width="1464" height="24.65"/>
|
<rect x="0" y="1.5" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-1">
|
<clipPath id="terminal-2271905219-line-1">
|
||||||
<rect x="0" y="25.9" width="1464" height="24.65"/>
|
<rect x="0" y="25.9" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-2">
|
<clipPath id="terminal-2271905219-line-2">
|
||||||
<rect x="0" y="50.3" width="1464" height="24.65"/>
|
<rect x="0" y="50.3" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-3">
|
<clipPath id="terminal-2271905219-line-3">
|
||||||
<rect x="0" y="74.7" width="1464" height="24.65"/>
|
<rect x="0" y="74.7" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-4">
|
<clipPath id="terminal-2271905219-line-4">
|
||||||
<rect x="0" y="99.1" width="1464" height="24.65"/>
|
<rect x="0" y="99.1" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-5">
|
<clipPath id="terminal-2271905219-line-5">
|
||||||
<rect x="0" y="123.5" width="1464" height="24.65"/>
|
<rect x="0" y="123.5" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-6">
|
<clipPath id="terminal-2271905219-line-6">
|
||||||
<rect x="0" y="147.9" width="1464" height="24.65"/>
|
<rect x="0" y="147.9" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-7">
|
<clipPath id="terminal-2271905219-line-7">
|
||||||
<rect x="0" y="172.3" width="1464" height="24.65"/>
|
<rect x="0" y="172.3" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-8">
|
<clipPath id="terminal-2271905219-line-8">
|
||||||
<rect x="0" y="196.7" width="1464" height="24.65"/>
|
<rect x="0" y="196.7" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-9">
|
<clipPath id="terminal-2271905219-line-9">
|
||||||
<rect x="0" y="221.1" width="1464" height="24.65"/>
|
<rect x="0" y="221.1" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-10">
|
<clipPath id="terminal-2271905219-line-10">
|
||||||
<rect x="0" y="245.5" width="1464" height="24.65"/>
|
<rect x="0" y="245.5" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-11">
|
<clipPath id="terminal-2271905219-line-11">
|
||||||
<rect x="0" y="269.9" width="1464" height="24.65"/>
|
<rect x="0" y="269.9" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-12">
|
<clipPath id="terminal-2271905219-line-12">
|
||||||
<rect x="0" y="294.3" width="1464" height="24.65"/>
|
<rect x="0" y="294.3" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-13">
|
<clipPath id="terminal-2271905219-line-13">
|
||||||
<rect x="0" y="318.7" width="1464" height="24.65"/>
|
<rect x="0" y="318.7" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-14">
|
<clipPath id="terminal-2271905219-line-14">
|
||||||
<rect x="0" y="343.1" width="1464" height="24.65"/>
|
<rect x="0" y="343.1" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-15">
|
<clipPath id="terminal-2271905219-line-15">
|
||||||
<rect x="0" y="367.5" width="1464" height="24.65"/>
|
<rect x="0" y="367.5" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-16">
|
<clipPath id="terminal-2271905219-line-16">
|
||||||
<rect x="0" y="391.9" width="1464" height="24.65"/>
|
<rect x="0" y="391.9" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-17">
|
<clipPath id="terminal-2271905219-line-17">
|
||||||
<rect x="0" y="416.3" width="1464" height="24.65"/>
|
<rect x="0" y="416.3" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-18">
|
<clipPath id="terminal-2271905219-line-18">
|
||||||
<rect x="0" y="440.7" width="1464" height="24.65"/>
|
<rect x="0" y="440.7" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-19">
|
<clipPath id="terminal-2271905219-line-19">
|
||||||
<rect x="0" y="465.1" width="1464" height="24.65"/>
|
<rect x="0" y="465.1" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-20">
|
<clipPath id="terminal-2271905219-line-20">
|
||||||
<rect x="0" y="489.5" width="1464" height="24.65"/>
|
<rect x="0" y="489.5" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-21">
|
<clipPath id="terminal-2271905219-line-21">
|
||||||
<rect x="0" y="513.9" width="1464" height="24.65"/>
|
<rect x="0" y="513.9" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-22">
|
<clipPath id="terminal-2271905219-line-22">
|
||||||
<rect x="0" y="538.3" width="1464" height="24.65"/>
|
<rect x="0" y="538.3" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-23">
|
<clipPath id="terminal-2271905219-line-23">
|
||||||
<rect x="0" y="562.7" width="1464" height="24.65"/>
|
<rect x="0" y="562.7" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-24">
|
<clipPath id="terminal-2271905219-line-24">
|
||||||
<rect x="0" y="587.1" width="1464" height="24.65"/>
|
<rect x="0" y="587.1" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-25">
|
<clipPath id="terminal-2271905219-line-25">
|
||||||
<rect x="0" y="611.5" width="1464" height="24.65"/>
|
<rect x="0" y="611.5" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-26">
|
<clipPath id="terminal-2271905219-line-26">
|
||||||
<rect x="0" y="635.9" width="1464" height="24.65"/>
|
<rect x="0" y="635.9" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-27">
|
<clipPath id="terminal-2271905219-line-27">
|
||||||
<rect x="0" y="660.3" width="1464" height="24.65"/>
|
<rect x="0" y="660.3" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-28">
|
<clipPath id="terminal-2271905219-line-28">
|
||||||
<rect x="0" y="684.7" width="1464" height="24.65"/>
|
<rect x="0" y="684.7" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="terminal-3756623299-line-29">
|
<clipPath id="terminal-2271905219-line-29">
|
||||||
<rect x="0" y="709.1" width="1464" height="24.65"/>
|
<rect x="0" y="709.1" width="1464" height="24.65"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
@@ -142,40 +142,40 @@
|
|||||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g transform="translate(9, 41)" clip-path="url(#terminal-3756623299-clip-terminal)">
|
<g transform="translate(9, 41)" clip-path="url(#terminal-2271905219-clip-terminal)">
|
||||||
|
|
||||||
<g class="terminal-3756623299-matrix">
|
<g class="terminal-2271905219-matrix">
|
||||||
<text class="terminal-3756623299-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">Usage:</text><text class="terminal-3756623299-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-3756623299-line-0)">builder.py</text><text class="terminal-3756623299-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)"> [</text><text class="terminal-3756623299-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-h</text><text class="terminal-3756623299-r2" x="256.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">] </text><text class="terminal-3756623299-r4" x="280.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-p</text><text class="terminal-3756623299-r5" x="317.2" y="20" textLength="48.8" clip-path="url(#terminal-3756623299-line-0)">ROOT</text><text class="terminal-3756623299-r4" x="378.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-w</text><text class="terminal-3756623299-r5" x="414.8" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">URL</text><text class="terminal-3756623299-r4" x="463.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-t</text><text class="terminal-3756623299-r5" x="500.2" y="20" textLength="61" clip-path="url(#terminal-3756623299-line-0)">TITLE</text><text class="terminal-3756623299-r2" x="561.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)"> [</text><text class="terminal-3756623299-r4" x="585.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-r</text><text class="terminal-3756623299-r2" x="610" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="646.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-n</text><text class="terminal-3756623299-r2" x="671" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="707.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-l</text><text class="terminal-3756623299-r5" x="744.2" y="20" textLength="85.4" clip-path="url(#terminal-3756623299-line-0)">LICENSE</text><text class="terminal-3756623299-r2" x="829.6" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-a</text><text class="terminal-3756623299-r5" x="902.8" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">AUTHOR</text><text class="terminal-3756623299-r2" x="976" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="1012.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-e</text><text class="terminal-3756623299-r5" x="1049.2" y="20" textLength="109.8" clip-path="url(#terminal-3756623299-line-0)">EXTENSION</text><text class="terminal-3756623299-r2" x="1159" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">] [</text><text class="terminal-3756623299-r4" x="1195.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-m</text><text class="terminal-3756623299-r2" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)">]</text><text class="terminal-3756623299-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)">
|
<text class="terminal-2271905219-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-2271905219-line-0)">Usage:</text><text class="terminal-2271905219-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-2271905219-line-0)">builder.py</text><text class="terminal-2271905219-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)"> [</text><text class="terminal-2271905219-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-h</text><text class="terminal-2271905219-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">] [</text><text class="terminal-2271905219-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-a</text><text class="terminal-2271905219-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-2271905219-line-0)">AUTHOR</text><text class="terminal-2271905219-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">] [</text><text class="terminal-2271905219-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-e</text><text class="terminal-2271905219-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-2271905219-line-0)">EXTENSION</text><text class="terminal-2271905219-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">] [</text><text class="terminal-2271905219-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-l</text><text class="terminal-2271905219-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-2271905219-line-0)">LICENSE</text><text class="terminal-2271905219-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">] [</text><text class="terminal-2271905219-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-m</text><text class="terminal-2271905219-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">] [</text><text class="terminal-2271905219-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-n</text><text class="terminal-2271905219-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">] </text><text class="terminal-2271905219-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-p</text><text class="terminal-2271905219-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-2271905219-line-0)">ROOT</text><text class="terminal-2271905219-r2" x="976" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)"> [</text><text class="terminal-2271905219-r4" x="1000.4" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-r</text><text class="terminal-2271905219-r2" x="1024.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">] </text><text class="terminal-2271905219-r4" x="1049.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-t</text><text class="terminal-2271905219-r5" x="1085.8" y="20" textLength="61" clip-path="url(#terminal-2271905219-line-0)">TITLE</text><text class="terminal-2271905219-r4" x="1159" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-w</text><text class="terminal-2271905219-r5" x="1195.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">URL</text><text class="terminal-2271905219-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-2271905219-line-0)">
|
||||||
</text><text class="terminal-3756623299-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">                  [</text><text class="terminal-3756623299-r4" x="231.8" y="44.4" textLength="146.4" clip-path="url(#terminal-3756623299-line-1)">--theme-path</text><text class="terminal-3756623299-r5" x="390.4" y="44.4" textLength="48.8" clip-path="url(#terminal-3756623299-line-1)">PATH</text><text class="terminal-3756623299-r2" x="439.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">] [</text><text class="terminal-3756623299-r4" x="475.8" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="707.6" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">] [</text><text class="terminal-3756623299-r4" x="744.2" y="44.4" textLength="244" clip-path="url(#terminal-3756623299-line-1)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="988.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">] [</text><text class="terminal-3756623299-r4" x="1024.8" y="44.4" textLength="195.2" clip-path="url(#terminal-3756623299-line-1)">--exclude-folder</text><text class="terminal-3756623299-r5" x="1232.2" y="44.4" textLength="73.2" clip-path="url(#terminal-3756623299-line-1)">FOLDER</text><text class="terminal-3756623299-r2" x="1305.4" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)">]</text><text class="terminal-3756623299-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)">
|
</text><text class="terminal-2271905219-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-2271905219-line-1)">                  [</text><text class="terminal-2271905219-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-2271905219-line-1)">--exclude-folder</text><text class="terminal-2271905219-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-2271905219-line-1)">FOLDER</text><text class="terminal-2271905219-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">] [</text><text class="terminal-2271905219-r4" x="549" y="44.4" textLength="244" clip-path="url(#terminal-2271905219-line-1)">--ignore-other-files</text><text class="terminal-2271905219-r2" x="793" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">] [</text><text class="terminal-2271905219-r4" x="829.6" y="44.4" textLength="146.4" clip-path="url(#terminal-2271905219-line-1)">--theme-path</text><text class="terminal-2271905219-r5" x="988.2" y="44.4" textLength="48.8" clip-path="url(#terminal-2271905219-line-1)">PATH</text><text class="terminal-2271905219-r2" x="1037" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">] [</text><text class="terminal-2271905219-r4" x="1073.6" y="44.4" textLength="231.8" clip-path="url(#terminal-2271905219-line-1)">--use-fancy-folders</text><text class="terminal-2271905219-r2" x="1305.4" y="44.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-1)">]</text><text class="terminal-2271905219-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-1)">
|
||||||
</text><text class="terminal-3756623299-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-3756623299-line-2)">                  [</text><text class="terminal-3756623299-r4" x="231.8" y="68.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-2)">--version</text><text class="terminal-3756623299-r2" x="341.6" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)">]</text><text class="terminal-3756623299-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)">
|
</text><text class="terminal-2271905219-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-2271905219-line-2)">                  [</text><text class="terminal-2271905219-r4" x="231.8" y="68.8" textLength="109.8" clip-path="url(#terminal-2271905219-line-2)">--version</text><text class="terminal-2271905219-r2" x="341.6" y="68.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-2)">]</text><text class="terminal-2271905219-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-2)">
|
||||||
</text><text class="terminal-3756623299-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-3)">
|
</text><text class="terminal-2271905219-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-3)">
|
||||||
</text><text class="terminal-3756623299-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-3756623299-line-4)">Generate HTML files for a static image hosting website.</text><text class="terminal-3756623299-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-4)">
|
</text><text class="terminal-2271905219-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-2271905219-line-4)">Generate HTML files for a static image hosting website.</text><text class="terminal-2271905219-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-4)">
|
||||||
</text><text class="terminal-3756623299-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-3756623299-line-5)">
|
</text><text class="terminal-2271905219-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-2271905219-line-5)">
|
||||||
</text><text class="terminal-3756623299-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-3756623299-line-6)">Options:</text><text class="terminal-3756623299-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-6)">
|
</text><text class="terminal-2271905219-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-2271905219-line-6)">Options:</text><text class="terminal-2271905219-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-6)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">-h</text><text class="terminal-3756623299-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">, </text><text class="terminal-3756623299-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-3756623299-line-7)">--help</text><text class="terminal-3756623299-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-3756623299-line-7)">show this help message and exit</text><text class="terminal-3756623299-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-7)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-7)">-h</text><text class="terminal-2271905219-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-7)">, </text><text class="terminal-2271905219-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-2271905219-line-7)">--help</text><text class="terminal-2271905219-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-2271905219-line-7)">show this help message and exit</text><text class="terminal-2271905219-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-7)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">-p</text><text class="terminal-3756623299-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">, </text><text class="terminal-3756623299-r4" x="73.2" y="215.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-8)">--root-directory</text><text class="terminal-3756623299-r5" x="280.6" y="215.2" textLength="48.8" clip-path="url(#terminal-3756623299-line-8)">ROOT</text><text class="terminal-3756623299-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-8)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-8)">-a</text><text class="terminal-2271905219-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-8)">, </text><text class="terminal-2271905219-r4" x="73.2" y="215.2" textLength="158.6" clip-path="url(#terminal-2271905219-line-8)">--author-name</text><text class="terminal-2271905219-r5" x="244" y="215.2" textLength="73.2" clip-path="url(#terminal-2271905219-line-8)">AUTHOR</text><text class="terminal-2271905219-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-8)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="239.6" textLength="451.4" clip-path="url(#terminal-3756623299-line-9)">Root directory containing the images.</text><text class="terminal-3756623299-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-9)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="239.6" textLength="402.6" clip-path="url(#terminal-2271905219-line-9)">Name of the author of the images.</text><text class="terminal-2271905219-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-9)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">-w</text><text class="terminal-3756623299-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">, </text><text class="terminal-3756623299-r4" x="73.2" y="264" textLength="170.8" clip-path="url(#terminal-3756623299-line-10)">--web-root-url</text><text class="terminal-3756623299-r5" x="256.2" y="264" textLength="36.6" clip-path="url(#terminal-3756623299-line-10)">URL</text><text class="terminal-3756623299-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-3756623299-line-10)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-2271905219-line-10)">-e</text><text class="terminal-2271905219-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-2271905219-line-10)">, </text><text class="terminal-2271905219-r4" x="73.2" y="264" textLength="207.4" clip-path="url(#terminal-2271905219-line-10)">--file-extensions</text><text class="terminal-2271905219-r5" x="292.8" y="264" textLength="109.8" clip-path="url(#terminal-2271905219-line-10)">EXTENSION</text><text class="terminal-2271905219-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-2271905219-line-10)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="288.4" textLength="634.4" clip-path="url(#terminal-3756623299-line-11)">Base URL of the web root for the image hosting site.</text><text class="terminal-3756623299-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-11)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="288.4" textLength="744.2" clip-path="url(#terminal-2271905219-line-11)">File extensions to include (can be specified multiple times).</text><text class="terminal-2271905219-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-11)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">-t</text><text class="terminal-3756623299-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">, </text><text class="terminal-3756623299-r4" x="73.2" y="312.8" textLength="146.4" clip-path="url(#terminal-3756623299-line-12)">--site-title</text><text class="terminal-3756623299-r5" x="231.8" y="312.8" textLength="61" clip-path="url(#terminal-3756623299-line-12)">TITLE</text><text class="terminal-3756623299-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-12)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-12)">-l</text><text class="terminal-2271905219-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-12)">, </text><text class="terminal-2271905219-r4" x="73.2" y="312.8" textLength="170.8" clip-path="url(#terminal-2271905219-line-12)">--license-type</text><text class="terminal-2271905219-r5" x="256.2" y="312.8" textLength="85.4" clip-path="url(#terminal-2271905219-line-12)">LICENSE</text><text class="terminal-2271905219-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-12)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="337.2" textLength="390.4" clip-path="url(#terminal-3756623299-line-13)">Title of the image hosting site.</text><text class="terminal-3756623299-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-13)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="337.2" textLength="488" clip-path="url(#terminal-2271905219-line-13)">Specify the license type for the images.</text><text class="terminal-2271905219-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-13)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">-r</text><text class="terminal-3756623299-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">, </text><text class="terminal-3756623299-r4" x="73.2" y="361.6" textLength="280.6" clip-path="url(#terminal-3756623299-line-14)">--regenerate-thumbnails</text><text class="terminal-3756623299-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-14)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-14)">-m</text><text class="terminal-2271905219-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-14)">, </text><text class="terminal-2271905219-r4" x="73.2" y="361.6" textLength="170.8" clip-path="url(#terminal-2271905219-line-14)">--web-manifest</text><text class="terminal-2271905219-r2" x="292.8" y="361.6" textLength="353.8" clip-path="url(#terminal-2271905219-line-14)">Generate a web manifest file.</text><text class="terminal-2271905219-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-14)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="386" textLength="597.8" clip-path="url(#terminal-3756623299-line-15)">Regenerate thumbnails even if they already exist.</text><text class="terminal-3756623299-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-3756623299-line-15)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-2271905219-line-15)">-n</text><text class="terminal-2271905219-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-2271905219-line-15)">, </text><text class="terminal-2271905219-r4" x="73.2" y="386" textLength="268.4" clip-path="url(#terminal-2271905219-line-15)">--non-interactive-mode</text><text class="terminal-2271905219-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-2271905219-line-15)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">-n</text><text class="terminal-3756623299-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">, </text><text class="terminal-3756623299-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-3756623299-line-16)">--non-interactive-mode</text><text class="terminal-3756623299-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-16)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="410.4" textLength="646.6" clip-path="url(#terminal-2271905219-line-16)">Run in non-interactive mode, disabling progress bars.</text><text class="terminal-2271905219-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-16)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="434.8" textLength="646.6" clip-path="url(#terminal-3756623299-line-17)">Run in non-interactive mode, disabling progress bars.</text><text class="terminal-3756623299-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-17)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="434.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-17)">-p</text><text class="terminal-2271905219-r2" x="48.8" y="434.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-17)">, </text><text class="terminal-2271905219-r4" x="73.2" y="434.8" textLength="195.2" clip-path="url(#terminal-2271905219-line-17)">--root-directory</text><text class="terminal-2271905219-r5" x="280.6" y="434.8" textLength="48.8" clip-path="url(#terminal-2271905219-line-17)">ROOT</text><text class="terminal-2271905219-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-17)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">-l</text><text class="terminal-3756623299-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">, </text><text class="terminal-3756623299-r4" x="73.2" y="459.2" textLength="170.8" clip-path="url(#terminal-3756623299-line-18)">--license-type</text><text class="terminal-3756623299-r5" x="256.2" y="459.2" textLength="85.4" clip-path="url(#terminal-3756623299-line-18)">LICENSE</text><text class="terminal-3756623299-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-18)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="459.2" textLength="451.4" clip-path="url(#terminal-2271905219-line-18)">Root directory containing the images.</text><text class="terminal-2271905219-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-18)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="483.6" textLength="488" clip-path="url(#terminal-3756623299-line-19)">Specify the license type for the images.</text><text class="terminal-3756623299-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-19)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="483.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-19)">-r</text><text class="terminal-2271905219-r2" x="48.8" y="483.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-19)">, </text><text class="terminal-2271905219-r4" x="73.2" y="483.6" textLength="280.6" clip-path="url(#terminal-2271905219-line-19)">--regenerate-thumbnails</text><text class="terminal-2271905219-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-19)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">-a</text><text class="terminal-3756623299-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">, </text><text class="terminal-3756623299-r4" x="73.2" y="508" textLength="158.6" clip-path="url(#terminal-3756623299-line-20)">--author-name</text><text class="terminal-3756623299-r5" x="244" y="508" textLength="73.2" clip-path="url(#terminal-3756623299-line-20)">AUTHOR</text><text class="terminal-3756623299-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-3756623299-line-20)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="508" textLength="597.8" clip-path="url(#terminal-2271905219-line-20)">Regenerate thumbnails even if they already exist.</text><text class="terminal-2271905219-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-2271905219-line-20)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="532.4" textLength="402.6" clip-path="url(#terminal-3756623299-line-21)">Name of the author of the images.</text><text class="terminal-3756623299-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-21)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="532.4" textLength="24.4" clip-path="url(#terminal-2271905219-line-21)">-t</text><text class="terminal-2271905219-r2" x="48.8" y="532.4" textLength="24.4" clip-path="url(#terminal-2271905219-line-21)">, </text><text class="terminal-2271905219-r4" x="73.2" y="532.4" textLength="146.4" clip-path="url(#terminal-2271905219-line-21)">--site-title</text><text class="terminal-2271905219-r5" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-2271905219-line-21)">TITLE</text><text class="terminal-2271905219-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-21)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">-e</text><text class="terminal-3756623299-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">, </text><text class="terminal-3756623299-r4" x="73.2" y="556.8" textLength="207.4" clip-path="url(#terminal-3756623299-line-22)">--file-extensions</text><text class="terminal-3756623299-r5" x="292.8" y="556.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-22)">EXTENSION</text><text class="terminal-3756623299-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-22)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="556.8" textLength="390.4" clip-path="url(#terminal-2271905219-line-22)">Title of the image hosting site.</text><text class="terminal-2271905219-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-22)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="581.2" textLength="744.2" clip-path="url(#terminal-3756623299-line-23)">File extensions to include (can be specified multiple times).</text><text class="terminal-3756623299-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-23)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="581.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-23)">-w</text><text class="terminal-2271905219-r2" x="48.8" y="581.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-23)">, </text><text class="terminal-2271905219-r4" x="73.2" y="581.2" textLength="170.8" clip-path="url(#terminal-2271905219-line-23)">--web-root-url</text><text class="terminal-2271905219-r5" x="256.2" y="581.2" textLength="36.6" clip-path="url(#terminal-2271905219-line-23)">URL</text><text class="terminal-2271905219-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-23)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">-m</text><text class="terminal-3756623299-r2" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">, </text><text class="terminal-3756623299-r4" x="73.2" y="605.6" textLength="170.8" clip-path="url(#terminal-3756623299-line-24)">--web-manifest</text><text class="terminal-3756623299-r2" x="292.8" y="605.6" textLength="353.8" clip-path="url(#terminal-3756623299-line-24)">Generate a web manifest file.</text><text class="terminal-3756623299-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-24)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="605.6" textLength="634.4" clip-path="url(#terminal-2271905219-line-24)">Base URL of the web root for the image hosting site.</text><text class="terminal-2271905219-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-24)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="630" textLength="146.4" clip-path="url(#terminal-3756623299-line-25)">--theme-path</text><text class="terminal-3756623299-r5" x="183" y="630" textLength="48.8" clip-path="url(#terminal-3756623299-line-25)">PATH</text><text class="terminal-3756623299-r2" x="292.8" y="630" textLength="329.4" clip-path="url(#terminal-3756623299-line-25)">Path to the CSS theme file.</text><text class="terminal-3756623299-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-3756623299-line-25)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="630" textLength="195.2" clip-path="url(#terminal-2271905219-line-25)">--exclude-folder</text><text class="terminal-2271905219-r5" x="231.8" y="630" textLength="73.2" clip-path="url(#terminal-2271905219-line-25)">FOLDER</text><text class="terminal-2271905219-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-2271905219-line-25)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="654.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-26)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="292.8" y="654.4" textLength="890.6" clip-path="url(#terminal-3756623299-line-26)">Enable fancy folder view instead of the default Apache directory listing.</text><text class="terminal-3756623299-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-26)">
|
</text><text class="terminal-2271905219-r2" x="292.8" y="654.4" textLength="1049.2" clip-path="url(#terminal-2271905219-line-26)">Folders to exclude from processing, globs supported (can be specified multiple times).</text><text class="terminal-2271905219-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-26)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-3756623299-line-27)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="292.8" y="678.8" textLength="683.2" clip-path="url(#terminal-3756623299-line-27)">Ignore files that do not match the specified extensions.</text><text class="terminal-3756623299-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-27)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-2271905219-line-27)">--ignore-other-files</text><text class="terminal-2271905219-r2" x="292.8" y="678.8" textLength="683.2" clip-path="url(#terminal-2271905219-line-27)">Ignore files that do not match the specified extensions.</text><text class="terminal-2271905219-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-27)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="703.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-28)">--exclude-folder</text><text class="terminal-3756623299-r5" x="231.8" y="703.2" textLength="73.2" clip-path="url(#terminal-3756623299-line-28)">FOLDER</text><text class="terminal-3756623299-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-28)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="703.2" textLength="146.4" clip-path="url(#terminal-2271905219-line-28)">--theme-path</text><text class="terminal-2271905219-r5" x="183" y="703.2" textLength="48.8" clip-path="url(#terminal-2271905219-line-28)">PATH</text><text class="terminal-2271905219-r2" x="292.8" y="703.2" textLength="329.4" clip-path="url(#terminal-2271905219-line-28)">Path to the CSS theme file.</text><text class="terminal-2271905219-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-28)">
|
||||||
</text><text class="terminal-3756623299-r2" x="292.8" y="727.6" textLength="1049.2" clip-path="url(#terminal-3756623299-line-29)">Folders to exclude from processing, globs supported (can be specified multiple times).</text><text class="terminal-3756623299-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-29)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="727.6" textLength="231.8" clip-path="url(#terminal-2271905219-line-29)">--use-fancy-folders</text><text class="terminal-2271905219-r2" x="292.8" y="727.6" textLength="890.6" clip-path="url(#terminal-2271905219-line-29)">Enable fancy folder view instead of the default Apache directory listing.</text><text class="terminal-2271905219-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-29)">
|
||||||
</text><text class="terminal-3756623299-r4" x="24.4" y="752" textLength="109.8" clip-path="url(#terminal-3756623299-line-30)">--version</text><text class="terminal-3756623299-r2" x="292.8" y="752" textLength="463.6" clip-path="url(#terminal-3756623299-line-30)">show program's version number and exit</text><text class="terminal-3756623299-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-3756623299-line-30)">
|
</text><text class="terminal-2271905219-r4" x="24.4" y="752" textLength="109.8" clip-path="url(#terminal-2271905219-line-30)">--version</text><text class="terminal-2271905219-r2" x="292.8" y="752" textLength="463.6" clip-path="url(#terminal-2271905219-line-30)">show program's version number and exit</text><text class="terminal-2271905219-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-2271905219-line-30)">
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
73
hl_config.yaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
|
||||||
|
$schema: https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
|
||||||
|
|
||||||
|
# Time format, see https://man7.org/linux/man-pages/man1/date.1.html for details.
|
||||||
|
time-format: "%b %d %T.%3N"
|
||||||
|
|
||||||
|
# Time zone name, see column "TZ identifier" at
|
||||||
|
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones page.
|
||||||
|
time-zone: "Europe/Vienna"
|
||||||
|
|
||||||
|
# Settings for fields processing.
|
||||||
|
fields:
|
||||||
|
# Configuration of the predefined set of fields.
|
||||||
|
predefined:
|
||||||
|
time:
|
||||||
|
show: auto
|
||||||
|
names: ["asctime"]
|
||||||
|
logger:
|
||||||
|
names: ["defaultlogger", "consolelogger"]
|
||||||
|
level:
|
||||||
|
show: auto
|
||||||
|
variants:
|
||||||
|
- names: ["levelname"]
|
||||||
|
values:
|
||||||
|
debug: ["DEBUG"]
|
||||||
|
info: ["INFO"]
|
||||||
|
warning: ["WARNING", "WARN"]
|
||||||
|
error: ["ERROR", "FATAL", "CRITICAL"]
|
||||||
|
- names: ["levelno"]
|
||||||
|
values:
|
||||||
|
debug: [10]
|
||||||
|
info: [20]
|
||||||
|
warning: [30]
|
||||||
|
error: [40, 50]
|
||||||
|
message:
|
||||||
|
names: ["message"]
|
||||||
|
caller:
|
||||||
|
names: ["funcName"]
|
||||||
|
caller-file:
|
||||||
|
names: ["filename"]
|
||||||
|
caller-line:
|
||||||
|
names: ["lineno"]
|
||||||
|
# List of wildcard field names to ignore.
|
||||||
|
ignore: ["_*"]
|
||||||
|
# List of exact field names to hide.
|
||||||
|
hide: ["pathname", "created", "levelno", "taskname", "relativeCreated", "thread", "process", "msecs"]
|
||||||
|
|
||||||
|
# Formatting settings.
|
||||||
|
formatting:
|
||||||
|
flatten: always
|
||||||
|
punctuation:
|
||||||
|
logger-name-separator: ":"
|
||||||
|
field-key-value-separator: "="
|
||||||
|
string-opening-quote: "'"
|
||||||
|
string-closing-quote: "'"
|
||||||
|
source-location-separator: "→ "
|
||||||
|
hidden-fields-indicator: " ..."
|
||||||
|
level-left-separator: "│"
|
||||||
|
level-right-separator: "│"
|
||||||
|
input-number-prefix: "#"
|
||||||
|
input-number-left-separator: ""
|
||||||
|
input-number-right-separator: " │ "
|
||||||
|
input-name-left-separator: ""
|
||||||
|
input-name-right-separator: " │ "
|
||||||
|
input-name-clipping: "··"
|
||||||
|
input-name-common-part: "··"
|
||||||
|
array-separator: " "
|
||||||
|
|
||||||
|
# Number of processing threads, configured automatically based on CPU count if not specified.
|
||||||
|
concurrency: ~
|
||||||
|
|
||||||
|
# Currently selected theme.
|
||||||
|
theme: "neutral"
|
||||||
@@ -1,15 +1,28 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
try:
|
||||||
from rich_argparse import RichHelpFormatter, HelpPreviewAction
|
from rich_argparse import RichHelpFormatter, HelpPreviewAction
|
||||||
|
|
||||||
|
RICH = True
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
RICH = False
|
||||||
|
|
||||||
if __package__ == None:
|
|
||||||
__package__ = ""
|
from modules.logger import logger
|
||||||
DEFAULT_THEME_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__).removesuffix(__package__)), "templates", "default.css")
|
|
||||||
|
if __package__ is None:
|
||||||
|
PACKAGE = ""
|
||||||
|
else:
|
||||||
|
PACKAGE = __package__
|
||||||
|
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
|
||||||
|
DEFAULT_THEME_PATH = os.path.join(SCRIPTDIR, "templates", "default.css")
|
||||||
DEFAULT_AUTHOR = "Author"
|
DEFAULT_AUTHOR = "Author"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(init=True)
|
||||||
class Args:
|
class Args:
|
||||||
"""
|
"""
|
||||||
A class to store command-line arguments for the script.
|
A class to store command-line arguments for the script.
|
||||||
@@ -52,12 +65,32 @@ class Args:
|
|||||||
license_type: Optional[str]
|
license_type: Optional[str]
|
||||||
non_interactive_mode: bool
|
non_interactive_mode: bool
|
||||||
regenerate_thumbnails: bool
|
regenerate_thumbnails: bool
|
||||||
|
reread_metadata: bool
|
||||||
root_directory: str
|
root_directory: str
|
||||||
site_title: str
|
site_title: str
|
||||||
theme_path: str
|
theme_path: str
|
||||||
use_fancy_folders: bool
|
use_fancy_folders: bool
|
||||||
web_root_url: str
|
web_root_url: str
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
result: dict = {}
|
||||||
|
result["author_name"] = self.author_name
|
||||||
|
result["exclude_folders"] = self.exclude_folders
|
||||||
|
result["file_extensions"] = self.file_extensions
|
||||||
|
result["generate_webmanifest"] = self.generate_webmanifest
|
||||||
|
result["ignore_other_files"] = self.ignore_other_files
|
||||||
|
if self.license_type is not None:
|
||||||
|
result["license_type"] = self.license_type
|
||||||
|
result["non_interactive_mode"] = self.non_interactive_mode
|
||||||
|
result["regenerate_thumbnails"] = self.regenerate_thumbnails
|
||||||
|
result["reread_metadata"] = self.reread_metadata
|
||||||
|
result["root_directory"] = self.root_directory
|
||||||
|
result["site_title"] = self.site_title
|
||||||
|
result["theme_path"] = self.theme_path
|
||||||
|
result["use_fancy_folders"] = self.use_fancy_folders
|
||||||
|
result["web_root_url"] = self.web_root_url
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments(version: str) -> Args:
|
def parse_arguments(version: str) -> Args:
|
||||||
"""
|
"""
|
||||||
@@ -73,35 +106,45 @@ def parse_arguments(version: str) -> Args:
|
|||||||
Args
|
Args
|
||||||
An instance of the Args class containing the parsed arguments.
|
An instance of the Args class containing the parsed arguments.
|
||||||
"""
|
"""
|
||||||
|
# fmt: off
|
||||||
|
if RICH:
|
||||||
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter)
|
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter)
|
||||||
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")
|
else:
|
||||||
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg")
|
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.")
|
||||||
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("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
|
|
||||||
parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders")
|
|
||||||
parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
|
|
||||||
parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
|
parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
|
||||||
parser.add_argument("-e", "--file-extensions", help="File extensions to include (can be specified multiple times).", action="append", dest="file_extensions", metavar="EXTENSION")
|
parser.add_argument("-e", "--file-extensions", help="File extensions to include (can be specified multiple times).", action="append", dest="file_extensions", metavar="EXTENSION")
|
||||||
parser.add_argument("-l", "--license-type", help="Specify the license type for the images.", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE")
|
parser.add_argument("-l", "--license-type", help="Specify the license type for the images.", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE")
|
||||||
parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest")
|
parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest")
|
||||||
parser.add_argument("-n", "--non-interactive-mode", help="Run in non-interactive mode, disabling progress bars.", action="store_true", default=False, dest="non_interactive_mode")
|
parser.add_argument("-n", "--non-interactive-mode", help="Run in non-interactive mode, disabling progress bars.", action="store_true", default=False, dest="non_interactive_mode")
|
||||||
parser.add_argument("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT")
|
parser.add_argument("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT")
|
||||||
parser.add_argument("-r", "--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
|
|
||||||
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")
|
||||||
|
if RICH:
|
||||||
|
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("--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
|
||||||
|
parser.add_argument("--reread-metadata", help="Reread image metadata", action="store_true", default=False, dest="reread_metadata")
|
||||||
|
parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
|
||||||
|
parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders")
|
||||||
|
parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
|
||||||
parsed_args = parser.parse_args()
|
parsed_args = parser.parse_args()
|
||||||
_args = Args()
|
# fmt: on
|
||||||
_args.author_name = parsed_args.author_name
|
_args = Args(
|
||||||
_args.exclude_folders = parsed_args.exclude_folders
|
author_name=parsed_args.author_name,
|
||||||
_args.file_extensions = parsed_args.file_extensions
|
exclude_folders=parsed_args.exclude_folders,
|
||||||
_args.generate_webmanifest = parsed_args.generate_webmanifest
|
file_extensions=parsed_args.file_extensions,
|
||||||
_args.ignore_other_files = parsed_args.ignore_other_files
|
generate_webmanifest=parsed_args.generate_webmanifest,
|
||||||
_args.license_type = parsed_args.license_type
|
ignore_other_files=parsed_args.ignore_other_files,
|
||||||
_args.non_interactive_mode = parsed_args.non_interactive_mode
|
license_type=parsed_args.license_type,
|
||||||
_args.regenerate_thumbnails = parsed_args.regenerate_thumbnails
|
non_interactive_mode=parsed_args.non_interactive_mode,
|
||||||
_args.root_directory = parsed_args.root_directory
|
regenerate_thumbnails=parsed_args.regenerate_thumbnails,
|
||||||
_args.site_title = parsed_args.site_title
|
reread_metadata=parsed_args.reread_metadata,
|
||||||
_args.theme_path = parsed_args.theme_path
|
root_directory=parsed_args.root_directory,
|
||||||
_args.use_fancy_folders = parsed_args.use_fancy_folders
|
site_title=parsed_args.site_title,
|
||||||
_args.web_root_url = parsed_args.web_root_url
|
theme_path=parsed_args.theme_path,
|
||||||
|
use_fancy_folders=parsed_args.use_fancy_folders,
|
||||||
|
web_root_url=parsed_args.web_root_url,
|
||||||
|
)
|
||||||
|
logger.debug("parsed arguments", extra={"args": _args.to_dict()})
|
||||||
return _args
|
return _args
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import re
|
|||||||
import colorsys
|
import colorsys
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
from modules.logger import logger
|
||||||
|
|
||||||
|
|
||||||
def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@@ -17,6 +19,7 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
|||||||
Dict[str, str]
|
Dict[str, str]
|
||||||
Dictionary containing color scheme variables and their hexadecimal values.
|
Dictionary containing color scheme variables and their hexadecimal values.
|
||||||
"""
|
"""
|
||||||
|
logger.info("extracting color scheme from theme file", extra={"theme_path": theme_path})
|
||||||
pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);"
|
pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);"
|
||||||
colorscheme = {}
|
colorscheme = {}
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
|
|||||||
color_value = match[1]
|
color_value = match[1]
|
||||||
hex_color_value = css_color_to_hex(color_value)
|
hex_color_value = css_color_to_hex(color_value)
|
||||||
colorscheme[variable_name] = hex_color_value
|
colorscheme[variable_name] = hex_color_value
|
||||||
|
logger.debug("extracted variable", extra={"variable": variable_name, "value": hex_color_value})
|
||||||
|
|
||||||
return colorscheme
|
return colorscheme
|
||||||
|
|
||||||
@@ -86,10 +90,12 @@ def css_color_to_hex(css_color: str) -> str:
|
|||||||
|
|
||||||
# Helper function to convert RGB tuple to hexadecimal string
|
# Helper function to convert RGB tuple to hexadecimal string
|
||||||
def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
||||||
|
logger.debug("converting rgb tuple to hex string", extra={"rgb": rgb})
|
||||||
return "#{:02x}{:02x}{:02x}".format(*rgb)
|
return "#{:02x}{:02x}{:02x}".format(*rgb)
|
||||||
|
|
||||||
# Helper function to convert HSL tuple to RGB tuple
|
# Helper function to convert HSL tuple to RGB tuple
|
||||||
def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]:
|
def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]:
|
||||||
|
logger.debug("converting hsl tuple to rgb tuple", extra={"hsl": hsl})
|
||||||
return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100))
|
return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100))
|
||||||
|
|
||||||
# Regular expression pattern to match CSS colors
|
# Regular expression pattern to match CSS colors
|
||||||
@@ -103,6 +109,7 @@ def css_color_to_hex(css_color: str) -> str:
|
|||||||
match = color_pattern.match(css_color.strip())
|
match = color_pattern.match(css_color.strip())
|
||||||
|
|
||||||
if not match:
|
if not match:
|
||||||
|
logger.error("invalid CSS color format", extra={"css_color": css_color})
|
||||||
raise ValueError("Invalid CSS color format")
|
raise ValueError("Invalid CSS color format")
|
||||||
|
|
||||||
groups = match.groupdict()
|
groups = match.groupdict()
|
||||||
@@ -119,8 +126,10 @@ def css_color_to_hex(css_color: str) -> str:
|
|||||||
b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"])
|
b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"])
|
||||||
a = float(groups["a"]) if groups["a"] else 1.0
|
a = float(groups["a"]) if groups["a"] else 1.0
|
||||||
if a < 1.0:
|
if a < 1.0:
|
||||||
|
logger.debug("converting rgba color to hex", extra={"color": css_color, "r": r, "g": g, "b": b, "a": a})
|
||||||
return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255))
|
return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255))
|
||||||
else:
|
else:
|
||||||
|
logger.debug("converting rgb color to hex", extra={"color": css_color, "r": r, "g": g, "b": b})
|
||||||
return rgb_to_hex((r, g, b))
|
return rgb_to_hex((r, g, b))
|
||||||
|
|
||||||
elif groups["hsl"]:
|
elif groups["hsl"]:
|
||||||
@@ -130,8 +139,10 @@ def css_color_to_hex(css_color: str) -> str:
|
|||||||
a = float(groups["a"]) if groups["a"] else 1.0
|
a = float(groups["a"]) if groups["a"] else 1.0
|
||||||
rgb_color = hsl_to_rgb((h, s, l))
|
rgb_color = hsl_to_rgb((h, s, l))
|
||||||
if a < 1.0:
|
if a < 1.0:
|
||||||
|
logger.debug("converting hsla color to hex", extra={"color": css_color, "hsl": (h, s, l), "a": a})
|
||||||
return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255))
|
return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255))
|
||||||
else:
|
else:
|
||||||
|
logger.debug("converting hsl color to hex", extra={"color": css_color, "hsl": (h, s, l)})
|
||||||
return rgb_to_hex(rgb_color)
|
return rgb_to_hex(rgb_color)
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
@@ -182,7 +193,9 @@ def css_color_to_hex(css_color: str) -> str:
|
|||||||
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
|
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
|
||||||
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32'
|
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32'
|
||||||
}
|
}
|
||||||
|
logger.debug("parsing css color string", extra={"css_color": css_color})
|
||||||
return named_colors[groups['name'].lower()]
|
return named_colors[groups['name'].lower()]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
logger.error("invalid CSS color format", extra={"css_color": css_color})
|
||||||
raise ValueError("Invalid CSS color format")
|
raise ValueError("Invalid CSS color format")
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm.auto import tqdm
|
from tqdm.auto import tqdm
|
||||||
from PIL import Image, ExifTags
|
from PIL import Image, ExifTags, TiffImagePlugin
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
from modules.logger import logger
|
||||||
import modules.cclicense as cclicense
|
import modules.cclicense as cclicense
|
||||||
from modules.argumentparser import Args
|
from modules.argumentparser import Args
|
||||||
|
|
||||||
# Constants for file paths and exclusions
|
# Constants for file paths and exclusions
|
||||||
if __package__ == None:
|
if __package__ is None:
|
||||||
__package__ = ""
|
PACKAGE = ""
|
||||||
|
else:
|
||||||
|
PACKAGE = __package__
|
||||||
|
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
|
||||||
FAVICON_PATH = ".static/favicon.ico"
|
FAVICON_PATH = ".static/favicon.ico"
|
||||||
GLOBAL_CSS_PATH = ".static/global.css"
|
GLOBAL_CSS_PATH = ".static/global.css"
|
||||||
EXCLUDES = ["index.html", "manifest.json", "robots.txt"]
|
EXCLUDES = ["index.html", "manifest.json", "robots.txt"]
|
||||||
@@ -23,7 +29,7 @@ EXCLUDES = ["index.html", "manifest.json", "robots.txt"]
|
|||||||
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(os.path.abspath(os.path.dirname(__file__).removesuffix(__package__)), "templates")))
|
env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates")))
|
||||||
thumbnails: List[Tuple[str, str]] = []
|
thumbnails: List[Tuple[str, str]] = []
|
||||||
info: Dict[str, str] = {}
|
info: Dict[str, str] = {}
|
||||||
pbardict: Dict[str, tqdm] = {}
|
pbardict: Dict[str, tqdm] = {}
|
||||||
@@ -42,17 +48,20 @@ def initialize_sizelist(folder: str) -> Dict[str, Dict[str, int]]:
|
|||||||
sizelist = {}
|
sizelist = {}
|
||||||
sizelist_path = os.path.join(folder, ".sizelist.json")
|
sizelist_path = os.path.join(folder, ".sizelist.json")
|
||||||
if not os.path.exists(sizelist_path):
|
if not os.path.exists(sizelist_path):
|
||||||
|
logger.info("creating new size list file", extra={"file": sizelist_path})
|
||||||
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile:
|
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile:
|
||||||
sizelistfile.write("{}")
|
sizelistfile.write("{}")
|
||||||
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile:
|
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile:
|
||||||
|
logger.info("reading size list file", extra={"file": sizelist_path})
|
||||||
try:
|
try:
|
||||||
sizelist = json.loads(sizelistfile.read())
|
sizelist = json.loads(sizelistfile.read())
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
|
logger.warning("invalid JSON in size list file", extra={"file": sizelist_path})
|
||||||
sizelist = {}
|
sizelist = {}
|
||||||
return sizelist
|
return sizelist
|
||||||
|
|
||||||
|
|
||||||
def update_sizelist(sizelist: Dict[str, Dict[str, int]], folder: str) -> None:
|
def update_sizelist(sizelist: Dict[str, Dict[str, Any]], folder: str) -> None:
|
||||||
"""
|
"""
|
||||||
Updates the size list JSON file.
|
Updates the size list JSON file.
|
||||||
|
|
||||||
@@ -61,11 +70,13 @@ def update_sizelist(sizelist: Dict[str, Dict[str, int]], folder: str) -> None:
|
|||||||
folder (str): The folder in which the size list file is located.
|
folder (str): The folder in which the size list file is located.
|
||||||
"""
|
"""
|
||||||
sizelist_path = os.path.join(folder, ".sizelist.json")
|
sizelist_path = os.path.join(folder, ".sizelist.json")
|
||||||
if sizelist != {}:
|
if sizelist:
|
||||||
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile:
|
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile:
|
||||||
|
logger.info("writing size list file", extra={"file": sizelist_path})
|
||||||
sizelistfile.write(json.dumps(sizelist, indent=4))
|
sizelistfile.write(json.dumps(sizelist, indent=4))
|
||||||
else:
|
else:
|
||||||
if os.path.exists(sizelist_path):
|
if os.path.exists(sizelist_path):
|
||||||
|
logger.info("deleting empty size list file", extra={"file": sizelist_path})
|
||||||
os.remove(sizelist_path)
|
os.remove(sizelist_path)
|
||||||
|
|
||||||
|
|
||||||
@@ -80,13 +91,46 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: A dictionary containing image width, height, and EXIF data.
|
Dict[str, Any]: A dictionary containing image width, height, and EXIF data.
|
||||||
"""
|
"""
|
||||||
with Image.open(os.path.join(folder, item)) as img:
|
file = os.path.join(folder, item)
|
||||||
|
with Image.open(file) as img:
|
||||||
|
logger.info("extracting image information", extra={"file": file})
|
||||||
exif = img.getexif()
|
exif = img.getexif()
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
exifdata = {ExifTags.TAGS.get(key, key): val for key, val in exif.items()}
|
if exif:
|
||||||
|
logger.info("extracting EXIF data", extra={"file": file})
|
||||||
|
ifd = exif.get_ifd(ExifTags.IFD.Exif)
|
||||||
|
exifdatas = dict(exif.items()) | ifd
|
||||||
|
exifdata = {}
|
||||||
|
for tag_id in exifdatas:
|
||||||
|
tag = ExifTags.TAGS.get(tag_id, tag_id)
|
||||||
|
content = exifdatas.get(tag_id)
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
content = "0x" + content.hex()
|
||||||
|
if isinstance(content, TiffImagePlugin.IFDRational):
|
||||||
|
content = content.limit_rational(1000000)
|
||||||
|
if isinstance(content, tuple):
|
||||||
|
newtuple = ()
|
||||||
|
for i in content:
|
||||||
|
if isinstance(i, TiffImagePlugin.IFDRational):
|
||||||
|
newtuple = newtuple + (i.limit_rational(1000000),)
|
||||||
|
if newtuple:
|
||||||
|
content = newtuple
|
||||||
|
if tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]:
|
||||||
|
epr = r'\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}'
|
||||||
|
if re.match(epr, content):
|
||||||
|
content = datetime.strptime(content, "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
else:
|
||||||
|
content = None
|
||||||
|
exifdata[tag] = content
|
||||||
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
|
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
|
||||||
|
logger.info("image is rotated", extra={"file": file})
|
||||||
width, height = height, width
|
width, height = height, width
|
||||||
return {"width": width, "height": height}
|
for key in ["PrintImageMatching", "UserComment", "MakerNote"]:
|
||||||
|
if key in exifdata:
|
||||||
|
del exifdata[key]
|
||||||
|
return {"width": width, "height": height, "exifdata": exifdata}
|
||||||
|
else:
|
||||||
|
return {"width": width, "height": height, "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, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]:
|
||||||
@@ -105,7 +149,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
|
|||||||
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.regenerate_thumbnails:
|
if item not in sizelist or _args.reread_metadata:
|
||||||
sizelist[item] = get_image_info(item, folder)
|
sizelist[item] = get_image_info(item, folder)
|
||||||
|
|
||||||
image = {
|
image = {
|
||||||
@@ -114,6 +158,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
|
|||||||
"name": item,
|
"name": item,
|
||||||
"width": sizelist[item]["width"],
|
"width": sizelist[item]["width"],
|
||||||
"height": sizelist[item]["height"],
|
"height": sizelist[item]["height"],
|
||||||
|
"exifdata": sizelist[item]["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:
|
||||||
@@ -122,12 +167,15 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
|
|||||||
thumbnails.append((folder, item, _args.root_directory))
|
thumbnails.append((folder, item, _args.root_directory))
|
||||||
|
|
||||||
for _raw in raw:
|
for _raw in raw:
|
||||||
if os.path.exists(os.path.join(folder, extsplit[0] + _raw)):
|
file = os.path.join(folder, extsplit[0] + _raw)
|
||||||
url = urllib.parse.quote(extsplit[0]) + _raw
|
if os.path.exists(file):
|
||||||
|
url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}"
|
||||||
if _raw in (".tif", ".tiff"):
|
if _raw in (".tif", ".tiff"):
|
||||||
image["tiff"] = f"{_args.web_root_url}{baseurl}{url}"
|
logger.info("tiff file found", extra={"file": file})
|
||||||
|
image["tiff"] = url
|
||||||
else:
|
else:
|
||||||
image["raw"] = f"{_args.web_root_url}{baseurl}{url}"
|
logger.info("raw file found", extra={"file": file, "extension": _raw})
|
||||||
|
image["raw"] = url
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
@@ -141,6 +189,11 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
|
|||||||
_args (Args): Parsed command line arguments.
|
_args (Args): Parsed command line arguments.
|
||||||
raw (List[str]): Raw image file names.
|
raw (List[str]): Raw image file names.
|
||||||
"""
|
"""
|
||||||
|
logger.info("processing folder", extra={"folder": folder})
|
||||||
|
if _args.regenerate_thumbnails:
|
||||||
|
if os.path.exists(os.path.join(folder, ".sizelist.json")):
|
||||||
|
logger.info("removing .sizelist.json", extra={"folder": folder})
|
||||||
|
os.remove(os.path.join(folder, ".sizelist.json"))
|
||||||
sizelist = initialize_sizelist(folder)
|
sizelist = initialize_sizelist(folder)
|
||||||
items = sorted(os.listdir(folder))
|
items = sorted(os.listdir(folder))
|
||||||
|
|
||||||
@@ -156,6 +209,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
|
|||||||
if not _args.non_interactive_mode:
|
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)
|
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})
|
||||||
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)):
|
||||||
@@ -179,6 +233,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
|
|||||||
create_html_file(folder, title, foldername, images, subfolders, _args, version)
|
create_html_file(folder, title, foldername, images, subfolders, _args, version)
|
||||||
else:
|
else:
|
||||||
if os.path.exists(os.path.join(folder, "index.html")):
|
if os.path.exists(os.path.join(folder, "index.html")):
|
||||||
|
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:
|
if not _args.non_interactive_mode:
|
||||||
@@ -195,6 +250,7 @@ def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
|
|||||||
"""
|
"""
|
||||||
thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername)
|
thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername)
|
||||||
if not os.path.exists(thumbnails_path):
|
if not os.path.exists(thumbnails_path):
|
||||||
|
logger.info("creating thumbnail folder", extra={"path": thumbnails_path})
|
||||||
os.mkdir(thumbnails_path)
|
os.mkdir(thumbnails_path)
|
||||||
|
|
||||||
|
|
||||||
@@ -226,6 +282,7 @@ def process_info_file(folder: str, item: str) -> None:
|
|||||||
item (str): The info file name.
|
item (str): The info file name.
|
||||||
"""
|
"""
|
||||||
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 info file", extra={"path": os.path.join(folder, item)})
|
||||||
info[urllib.parse.quote(folder)] = f.read()
|
info[urllib.parse.quote(folder)] = f.read()
|
||||||
|
|
||||||
|
|
||||||
@@ -255,7 +312,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
|
|||||||
subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML.
|
subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML.
|
||||||
_args (Args): Parsed command line arguments.
|
_args (Args): Parsed command line arguments.
|
||||||
"""
|
"""
|
||||||
image_chunks = np.array_split(images, 8) if images else []
|
html_file = os.path.join(folder, "index.html")
|
||||||
|
logger.info("generating html file with jinja2", extra={"path": html_file})
|
||||||
header = os.path.basename(folder) or title
|
header = os.path.basename(folder) or title
|
||||||
parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}"
|
parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}"
|
||||||
if parent and _args.web_root_url.startswith("file://"):
|
if parent and _args.web_root_url.startswith("file://"):
|
||||||
@@ -287,14 +345,15 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
|
|||||||
header=header,
|
header=header,
|
||||||
license=license_info,
|
license=license_info,
|
||||||
subdirectories=subfolders,
|
subdirectories=subfolders,
|
||||||
images=image_chunks,
|
images=images,
|
||||||
info=_info,
|
info=_info,
|
||||||
allimages=images,
|
allimages=images,
|
||||||
webmanifest=_args.generate_webmanifest,
|
webmanifest=_args.generate_webmanifest,
|
||||||
version=version,
|
version=version,
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f:
|
with open(html_file, "w", encoding="utf-8") as f:
|
||||||
|
logger.info("writing html file", extra={"path": html_file})
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
140
modules/logger.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
logger.py
|
||||||
|
|
||||||
|
This module provides functionality for setting up a centralized logging system using the
|
||||||
|
`logging` library and the `python-json-logger` to output logs in JSON format. It handles
|
||||||
|
log rotation by renaming old log files and saving them based on the first timestamp entry.
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- log_format(keys): Generates the logging format string based on the list of keys.
|
||||||
|
- rotate_log_file(): Handles renaming the existing log file to a timestamp-based name.
|
||||||
|
- setup_logger(): Configures the logging system, applies a JSON format, and returns a logger instance.
|
||||||
|
- setup_consolelogger(): Configures the logging system to output logs in console format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import gzip
|
||||||
|
import shutil
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from pythonjsonlogger import jsonlogger
|
||||||
|
|
||||||
|
# Constants for file paths and exclusions
|
||||||
|
if __package__ is None:
|
||||||
|
PACKAGE = ""
|
||||||
|
else:
|
||||||
|
PACKAGE = __package__
|
||||||
|
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
|
||||||
|
LOG_DIR = os.path.join(SCRIPTDIR, "logs")
|
||||||
|
LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl")
|
||||||
|
|
||||||
|
if not os.path.exists(LOG_DIR):
|
||||||
|
os.makedirs(LOG_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def log_format(keys):
|
||||||
|
"""
|
||||||
|
Generates a list of format strings based on the given keys.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keys (list): A list of string keys that represent the log attributes (e.g., 'asctime', 'levelname').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of formatted strings for each key, in the format "%(key)s".
|
||||||
|
"""
|
||||||
|
return [f"%({i})s" for i in keys]
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_log_file(compress=False):
|
||||||
|
"""
|
||||||
|
Truncates the 'latest.jsonl' file after optionally compressing its contents to a timestamped file.
|
||||||
|
The 'latest.jsonl' file is not deleted or moved, just emptied.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
compress (bool): If True, compress the old log file using gzip.
|
||||||
|
"""
|
||||||
|
if os.path.exists(LATEST_LOG_FILE):
|
||||||
|
with open(LATEST_LOG_FILE, "r+", encoding="utf-8") as f:
|
||||||
|
first_line = f.readline()
|
||||||
|
try:
|
||||||
|
first_log = json.loads(first_line)
|
||||||
|
first_timestamp = first_log.get("asctime")
|
||||||
|
first_timestamp = first_timestamp.split(",")[0]
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
first_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
|
||||||
|
safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_")
|
||||||
|
old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl")
|
||||||
|
|
||||||
|
# Write contents to the new file
|
||||||
|
with open(old_log_filename, "w", encoding="utf-8") as old_log_file:
|
||||||
|
f.seek(0) # Go back to the beginning of the file
|
||||||
|
shutil.copyfileobj(f, old_log_file)
|
||||||
|
|
||||||
|
if compress:
|
||||||
|
with open(old_log_filename, "rb") as f_in:
|
||||||
|
with gzip.open(f"{old_log_filename}.gz", "wb") as f_out:
|
||||||
|
shutil.copyfileobj(f_in, f_out)
|
||||||
|
os.remove(old_log_filename)
|
||||||
|
|
||||||
|
# Truncate the original file
|
||||||
|
f.seek(0)
|
||||||
|
f.truncate()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logger(level=logging.INFO):
|
||||||
|
"""
|
||||||
|
Configures the logging system with a custom format and outputs logs in JSON format.
|
||||||
|
|
||||||
|
The logger will write to the 'logs/latest.jsonl' file, and it will include
|
||||||
|
multiple attributes such as the time of logging, the filename, function name, log level, etc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logging.Logger: A configured logger instance that can be used to log messages.
|
||||||
|
"""
|
||||||
|
_logger = logging.getLogger(name="defaultlogger")
|
||||||
|
|
||||||
|
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
|
||||||
|
|
||||||
|
custom_format = " ".join(log_format(supported_keys))
|
||||||
|
formatter = jsonlogger.JsonFormatter(custom_format)
|
||||||
|
|
||||||
|
log_handler = logging.FileHandler(LATEST_LOG_FILE)
|
||||||
|
log_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
_logger.addHandler(log_handler)
|
||||||
|
_logger.setLevel(level=level)
|
||||||
|
|
||||||
|
return _logger
|
||||||
|
|
||||||
|
|
||||||
|
def setup_consolelogger(level=logging.INFO):
|
||||||
|
"""
|
||||||
|
Configures the logging system to output logs in console and JSON format.
|
||||||
|
|
||||||
|
The logger will write to the 'logs/latest.jsonl' file, and it will include
|
||||||
|
multiple attributes such as the time of logging, the filename, function name, log level, etc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logging.Logger: A configured logger instance that can be used to log messages.
|
||||||
|
"""
|
||||||
|
_logger = logging.getLogger(name="consolelogger")
|
||||||
|
|
||||||
|
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
|
||||||
|
|
||||||
|
custom_format = " ".join(log_format(supported_keys))
|
||||||
|
formatter = jsonlogger.JsonFormatter(custom_format)
|
||||||
|
|
||||||
|
log_handler = logging.FileHandler(LATEST_LOG_FILE)
|
||||||
|
log_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
_logger.addHandler(log_handler)
|
||||||
|
_logger.addHandler(logging.StreamHandler())
|
||||||
|
_logger.setLevel(level=level)
|
||||||
|
return _logger
|
||||||
|
|
||||||
|
|
||||||
|
rotate_log_file(compress=True)
|
||||||
|
logger = setup_logger()
|
||||||
|
consolelogger = setup_consolelogger()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
@@ -13,17 +14,21 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
SVGSUPPORT = False
|
SVGSUPPORT = False
|
||||||
|
|
||||||
|
from modules.logger import logger
|
||||||
from modules.argumentparser import Args
|
from modules.argumentparser import Args
|
||||||
from modules.css_color import css_color_to_hex, extract_theme_color, extract_colorscheme
|
from modules.css_color import extract_theme_color, extract_colorscheme
|
||||||
|
|
||||||
# Define constants for static files directory and icon sizes
|
# Define constants for static files directory and icon sizes
|
||||||
if __package__ == None:
|
if __package__ is None:
|
||||||
__package__ = ""
|
PACKAGE = ""
|
||||||
STATIC_FILES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__).removesuffix(__package__)), "files")
|
else:
|
||||||
|
PACKAGE = __package__
|
||||||
|
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
|
||||||
|
STATIC_FILES_DIR = os.path.join(SCRIPTDIR, "files")
|
||||||
ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"]
|
ICON_SIZES = ["36x36", "48x48", "72x72", "96x96", "144x144", "192x192", "512x512"]
|
||||||
|
|
||||||
# Initialize Jinja2 environment for template rendering
|
# Initialize Jinja2 environment for template rendering
|
||||||
env = Environment(loader=FileSystemLoader(os.path.join(os.path.abspath(os.path.dirname(__file__).removesuffix(__package__)), "templates")))
|
env = Environment(loader=FileSystemLoader(os.path.join(SCRIPTDIR, "templates")))
|
||||||
|
|
||||||
|
|
||||||
class Icon:
|
class Icon:
|
||||||
@@ -52,6 +57,7 @@ def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str:
|
|||||||
svg = env.get_template("icon.svg.j2")
|
svg = env.get_template("icon.svg.j2")
|
||||||
content = svg.render(colorscheme=colorscheme)
|
content = svg.render(colorscheme=colorscheme)
|
||||||
with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f:
|
with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f:
|
||||||
|
logger.info("writing svg icon", extra={"iconspath": iconspath})
|
||||||
f.write(content)
|
f.write(content)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
@@ -70,6 +76,7 @@ def save_png_icon(content: str, iconspath: str) -> None:
|
|||||||
tmpimg = BytesIO()
|
tmpimg = BytesIO()
|
||||||
cairosvg.svg2png(bytestring=content, write_to=tmpimg)
|
cairosvg.svg2png(bytestring=content, write_to=tmpimg)
|
||||||
with Image.open(tmpimg) as iconfile:
|
with Image.open(tmpimg) as iconfile:
|
||||||
|
logger.info("saving png icon", extra={"iconspath": iconspath})
|
||||||
iconfile.save(os.path.join(iconspath, "icon.png"))
|
iconfile.save(os.path.join(iconspath, "icon.png"))
|
||||||
|
|
||||||
|
|
||||||
@@ -84,10 +91,25 @@ def generate_favicon(iconspath: str, root_directory: str) -> None:
|
|||||||
root_directory : str
|
root_directory : str
|
||||||
Root directory of the project where the favicon will be saved.
|
Root directory of the project where the favicon will be saved.
|
||||||
"""
|
"""
|
||||||
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
|
favicon = os.path.join(root_directory, ".static", "favicon.ico")
|
||||||
|
logger.info("generating favicon with imagemagick", extra={"iconspath": iconspath, "favicon": favicon})
|
||||||
|
_env = dict(os.environ)
|
||||||
|
lp_key = "LD_LIBRARY_PATH"
|
||||||
|
lp_orig = _env.get(lp_key + "_ORIG")
|
||||||
|
if lp_orig is not None:
|
||||||
|
_env[lp_key] = lp_orig
|
||||||
|
else:
|
||||||
|
_env.pop(lp_key, None)
|
||||||
if not shutil.which("magick"):
|
if not shutil.which("magick"):
|
||||||
command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
|
magick = shutil.which("convert")
|
||||||
os.system(command)
|
else:
|
||||||
|
magick = shutil.which("magick")
|
||||||
|
command = [magick, os.path.join(iconspath, "icon.png"), "-define", "icon:auto-resize=16,32,48,64,72,96,144,192", favicon]
|
||||||
|
with Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=_env, errors="ignore") as p:
|
||||||
|
out, err = p.communicate()
|
||||||
|
if p.returncode != 0:
|
||||||
|
logger.error("error generating favicon: %s", err, extra={"command": command, "out": out, "err": err})
|
||||||
|
logger.info("favicon generated successfully", extra={"command": command, "out": out, "err": err})
|
||||||
|
|
||||||
|
|
||||||
def icons(_args: Args) -> None:
|
def icons(_args: Args) -> None:
|
||||||
@@ -99,12 +121,14 @@ def icons(_args: Args) -> None:
|
|||||||
_args : Args
|
_args : Args
|
||||||
Parsed command-line arguments.
|
Parsed command-line arguments.
|
||||||
"""
|
"""
|
||||||
print("Generating icons...")
|
|
||||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||||
|
logger.info("generating icons", extra={"iconspath": iconspath})
|
||||||
|
print("Generating icons...")
|
||||||
colorscheme = extract_colorscheme(_args.theme_path)
|
colorscheme = extract_colorscheme(_args.theme_path)
|
||||||
content = render_svg_icon(colorscheme, iconspath)
|
content = render_svg_icon(colorscheme, iconspath)
|
||||||
if not SVGSUPPORT:
|
if not SVGSUPPORT:
|
||||||
print("Please install cairosvg to generate favicon from svg icon.")
|
print("Please install cairosvg to generate favicon from svg icon.")
|
||||||
|
logger.error("svg support not available")
|
||||||
return
|
return
|
||||||
save_png_icon(content, iconspath)
|
save_png_icon(content, iconspath)
|
||||||
generate_favicon(iconspath, _args.root_directory)
|
generate_favicon(iconspath, _args.root_directory)
|
||||||
@@ -132,6 +156,7 @@ def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, s
|
|||||||
theme_color=colors["theme_color"],
|
theme_color=colors["theme_color"],
|
||||||
)
|
)
|
||||||
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f:
|
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f:
|
||||||
|
logger.info("rendering manifest.json", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.json")})
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
@@ -154,6 +179,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
|
|||||||
List of icons created from the SVG file.
|
List of icons created from the SVG file.
|
||||||
"""
|
"""
|
||||||
svg = [file for file in files if file.endswith(".svg")][0]
|
svg = [file for file in files if file.endswith(".svg")][0]
|
||||||
|
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
|
||||||
icon_list = [
|
icon_list = [
|
||||||
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"},
|
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"},
|
||||||
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"},
|
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"},
|
||||||
@@ -162,6 +188,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
|
|||||||
tmpimg = BytesIO()
|
tmpimg = BytesIO()
|
||||||
sizes = size.split("x")
|
sizes = size.split("x")
|
||||||
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
|
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
|
||||||
|
logger.info("converting svg to png", extra={"svg": svg, "size": size})
|
||||||
cairosvg.svg2png(
|
cairosvg.svg2png(
|
||||||
url=os.path.join(iconspath, svg),
|
url=os.path.join(iconspath, svg),
|
||||||
write_to=tmpimg,
|
write_to=tmpimg,
|
||||||
@@ -170,6 +197,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
|
|||||||
scale=1,
|
scale=1,
|
||||||
)
|
)
|
||||||
with Image.open(tmpimg) as iconfile:
|
with Image.open(tmpimg) as iconfile:
|
||||||
|
logger.info("saving png file", extra={"iconpath": iconpath})
|
||||||
iconfile.save(iconpath, format="PNG")
|
iconfile.save(iconpath, format="PNG")
|
||||||
icon_list.append(
|
icon_list.append(
|
||||||
{
|
{
|
||||||
@@ -212,6 +240,7 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
|
|||||||
continue
|
continue
|
||||||
with Image.open(os.path.join(iconspath, icon)) as iconfile:
|
with Image.open(os.path.join(iconspath, icon)) as iconfile:
|
||||||
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
|
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
|
||||||
|
logger.info("using icon", extra={"iconspath": iconspath, "icon": icon, "size": iconsize})
|
||||||
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"})
|
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"})
|
||||||
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
|
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
|
||||||
return icon_list
|
return icon_list
|
||||||
@@ -226,16 +255,15 @@ def webmanifest(_args: Args) -> None:
|
|||||||
_args : Args
|
_args : Args
|
||||||
Parsed command-line arguments.
|
Parsed command-line arguments.
|
||||||
"""
|
"""
|
||||||
|
logger.info("generating webmanifest")
|
||||||
|
|
||||||
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
iconspath = os.path.join(_args.root_directory, ".static", "icons")
|
||||||
files = os.listdir(iconspath)
|
files = os.listdir(iconspath)
|
||||||
icon_list = (
|
icon_list = create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url)
|
||||||
create_icons_from_svg(files, iconspath, _args)
|
|
||||||
if SVGSUPPORT and any(file.endswith(".svg") for file in files)
|
|
||||||
else create_icons_from_png(iconspath, _args.web_root_url)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not icon_list:
|
if not icon_list:
|
||||||
print("No icons found in the static/icons folder!")
|
print("No icons found in the static/icons folder!")
|
||||||
|
logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath})
|
||||||
return
|
return
|
||||||
|
|
||||||
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))
|
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ Jinja2==3.1.4
|
|||||||
numpy==2.0.0
|
numpy==2.0.0
|
||||||
pillow==10.4.0
|
pillow==10.4.0
|
||||||
pyinstaller==6.9.0
|
pyinstaller==6.9.0
|
||||||
|
python-json-logger==2.0.7
|
||||||
rich-argparse==1.5.2
|
rich-argparse==1.5.2
|
||||||
|
setuptools==70.3.0
|
||||||
tqdm==4.66.4
|
tqdm==4.66.4
|
||||||
@@ -56,9 +56,8 @@
|
|||||||
{% if images %}
|
{% if images %}
|
||||||
{%- set ns = namespace(count = 0) -%}
|
{%- set ns = namespace(count = 0) -%}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{%- for imageblock in images %}
|
{%- for image in images %}
|
||||||
<div class="column">
|
<div class="column">
|
||||||
{%- for image in imageblock %}
|
|
||||||
<figure>
|
<figure>
|
||||||
<img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" />
|
<img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" />
|
||||||
{%- set ns.count = ns.count + 1 %}
|
{%- set ns.count = ns.count + 1 %}
|
||||||
@@ -71,7 +70,6 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
{%- endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +145,11 @@
|
|||||||
var pswpElement = document.querySelectorAll('.pswp')[0];
|
var pswpElement = document.querySelectorAll('.pswp')[0];
|
||||||
var items = [
|
var items = [
|
||||||
{%- for image in allimages %}
|
{%- for image in allimages %}
|
||||||
|
{%- if image.exifdata.DateTime %}
|
||||||
|
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}", title: "Captured: {{ image.exifdata.DateTime }}" },
|
||||||
|
{%- 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 }}" },
|
||||||
|
{%- endif %}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
];
|
];
|
||||||
var re = /pid=(\d+)/;
|
var re = /pid=(\d+)/;
|
||||||
@@ -178,8 +180,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function topFunction() {
|
function topFunction() {
|
||||||
document.body.scrollTop = 0;
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
document.documentElement.scrollTop = 0;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|||||||
BIN
test/example/DSC03470.JPG
Executable file
|
After Width: | Height: | Size: 20 MiB |
BIN
test/example/DSC03508.ARW
Executable file
BIN
test/example/DSC03508.JPG
Executable file
|
After Width: | Height: | Size: 20 MiB |
@@ -12,8 +12,7 @@
|
|||||||
--bcolor2: #20123b;
|
--bcolor2: #20123b;
|
||||||
--bcolor3: #2b1753;
|
--bcolor3: #2b1753;
|
||||||
--bcolor4: #321c64;
|
--bcolor4: #321c64;
|
||||||
--gradient: linear-gradient(
|
--gradient: linear-gradient(80deg,
|
||||||
80deg,
|
|
||||||
var(--bcolor2) 0%,
|
var(--bcolor2) 0%,
|
||||||
var(--color3) 1%,
|
var(--color3) 1%,
|
||||||
var(--color4) 2%,
|
var(--color4) 2%,
|
||||||
@@ -23,8 +22,7 @@
|
|||||||
var(--color5) 97%,
|
var(--color5) 97%,
|
||||||
var(--color4) 98%,
|
var(--color4) 98%,
|
||||||
var(--color3) 99%,
|
var(--color3) 99%,
|
||||||
var(--bcolor2) 100%
|
var(--bcolor2) 100%);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -5,22 +5,22 @@
|
|||||||
--color2: #b22222;
|
--color2: #b22222;
|
||||||
--color3: #ff4500;
|
--color3: #ff4500;
|
||||||
--color4: #6e0000;
|
--color4: #6e0000;
|
||||||
--bcolor1: #ebebeb;
|
--bcolor1: #171717;
|
||||||
--bcolor2: #191919;
|
--bcolor2: #191919;
|
||||||
--bcolor3: #171717;
|
--bcolor3: #ebebeb;
|
||||||
--bcolor4: #0a0a0a;
|
--bcolor4: #0a0a0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
background-color: var(--color1);
|
background-color: var(--color1);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar li a {
|
.navbar li a {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Change the link color on hover */
|
/* Change the link color on hover */
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
background-color: var(--color3);
|
background-color: var(--color3);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,22 @@
|
|||||||
--color2: #1346a4;
|
--color2: #1346a4;
|
||||||
--color3: #0e3377;
|
--color3: #0e3377;
|
||||||
--color4: #3674e7;
|
--color4: #3674e7;
|
||||||
--bcolor1: #ebebeb;
|
--bcolor1: #171717;
|
||||||
--bcolor2: #191919;
|
--bcolor2: #191919;
|
||||||
--bcolor3: #171717;
|
--bcolor3: #ebebeb;
|
||||||
--bcolor4: #0a0a0a;
|
--bcolor4: #0a0a0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
background-color: var(--color1);
|
background-color: var(--color1);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar li a {
|
.navbar li a {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Change the link color on hover */
|
/* Change the link color on hover */
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
background-color: var(--color3);
|
background-color: var(--color3);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<svg width='64' height='64' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
<svg width='64' height='64' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
<path d='M20 6H10L8 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6Z' fill='{{ color1 }}' />
|
<path d='M20 6H10L8 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6Z' fill='{{ color1 }}' />
|
||||||
<path d='M4 4H8L10 6H20C21.1046 6 22 6.89543 22 8H2C2 6.89543 2.89543 6 4 6V4Z' fill='{{ color4 }}' />
|
<path d='M4 4H8L10 6H20C21.1046 6 22 6.89543 22 8H2C2 6.89543 2.89543 6 4 6V4Z' fill='{{ color4 }}' />
|
||||||
<path d='M10 6H14L12 4H8L10 6Z' fill='{{ color2 }}' />
|
<path d='M10 6H14L12 4H8L10 6Z' fill='{{ color3 }}' />
|
||||||
<path d='M14 6H18L16 4H12L14 6Z' fill='{{ color3 }}' />
|
<path d='M14 6H18L16 4H12L14 6Z' fill='{{ color2 }}' />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 493 B |
@@ -5,22 +5,22 @@
|
|||||||
--color2: #008000;
|
--color2: #008000;
|
||||||
--color3: #32cd32;
|
--color3: #32cd32;
|
||||||
--color4: #004300;
|
--color4: #004300;
|
||||||
--bcolor1: #ebebeb;
|
--bcolor1: #171717;
|
||||||
--bcolor2: #191919;
|
--bcolor2: #191919;
|
||||||
--bcolor3: #171717;
|
--bcolor3: #ebebeb;
|
||||||
--bcolor4: #0a0a0a;
|
--bcolor4: #0a0a0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
background-color: var(--color1);
|
background-color: var(--color1);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar li a {
|
.navbar li a {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Change the link color on hover */
|
/* Change the link color on hover */
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
background-color: var(--color3);
|
background-color: var(--color3);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -5,9 +5,9 @@
|
|||||||
--color2: #ffd700;
|
--color2: #ffd700;
|
||||||
--color3: #ffe135;
|
--color3: #ffe135;
|
||||||
--color4: #ce8c00;
|
--color4: #ce8c00;
|
||||||
--bcolor1: #ebebeb;
|
--bcolor1: #171717;
|
||||||
--bcolor2: #191919;
|
--bcolor2: #191919;
|
||||||
--bcolor3: #171717;
|
--bcolor3: #ebebeb;
|
||||||
--bcolor4: #0a0a0a;
|
--bcolor4: #0a0a0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
.row a {
|
.row a {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--color2);
|
color: var(--color1);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
.tooltiptext {
|
.tooltiptext {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--bcolor1);
|
color: var(--bcolor3);
|
||||||
background-color: var(--bcolor2);
|
background-color: var(--bcolor2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
view-latest-log.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
LESS=-SR hl logs/latest.jsonl --config hl_config.yaml
|
||||||
|
|
||||||
3
view-logs.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
LESS=-SR hl $(ls -tr logs/*.{jsonl,jsonl.gz}) --config hl_config.yaml
|
||||||