move python stuff out of the way
This commit is contained in:
parent
94a5e30a8f
commit
ceb872590f
16 changed files with 0 additions and 0 deletions
0
py/photojawn/__init__.py
Normal file
0
py/photojawn/__init__.py
Normal file
0
py/photojawn/clean.py
Normal file
0
py/photojawn/clean.py
Normal file
175
py/photojawn/cli.py
Normal file
175
py/photojawn/cli.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import logging
|
||||
import sys
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from pathlib import Path
|
||||
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from photojawn.config import DEFAULT_CONFIG_PATH, Config
|
||||
from photojawn.generate import generate
|
||||
|
||||
logger = logging.getLogger("photojawn.cli")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
setup_logging(args.logging)
|
||||
|
||||
# Load config from file if it exists
|
||||
if hasattr(args, "album_path"):
|
||||
conf_path = Path(args.album_path) / Path(args.config)
|
||||
if conf_path.exists():
|
||||
logger.debug(f"Reading config from {conf_path}")
|
||||
config = Config.from_yaml(conf_path.read_bytes())
|
||||
elif args.action != "init":
|
||||
logger.error(
|
||||
f"No config file found at {conf_path}. If this is a new photo directory, "
|
||||
"please run `photojawn init` in there first."
|
||||
)
|
||||
return
|
||||
|
||||
# Call the subcommand function
|
||||
match args.action:
|
||||
case "init":
|
||||
cmd_init(args)
|
||||
case "generate":
|
||||
if args.quick:
|
||||
config.quick = args.quick
|
||||
cmd_generate(args, config)
|
||||
case "clean":
|
||||
cmd_clean(args, config)
|
||||
|
||||
|
||||
def parse_args() -> Namespace:
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
"-c",
|
||||
default=DEFAULT_CONFIG_PATH,
|
||||
help="Path to photojawn.config.json for the album",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--logging",
|
||||
default="warning",
|
||||
choices=[level.lower() for level in logging.getLevelNamesMapping().keys()],
|
||||
help="Log level",
|
||||
)
|
||||
|
||||
subcommands = parser.add_subparsers(title="subcommands")
|
||||
|
||||
init_cmd = subcommands.add_parser(
|
||||
"init",
|
||||
help="Initialize an photo directory",
|
||||
)
|
||||
init_cmd.set_defaults(action="init")
|
||||
init_cmd.add_argument(
|
||||
"album_path",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Path to the main photos directory",
|
||||
)
|
||||
|
||||
# Generate subcommand
|
||||
generate_cmd = subcommands.add_parser(
|
||||
"generate",
|
||||
help="Generate the HTML photo album",
|
||||
)
|
||||
generate_cmd.set_defaults(action="generate")
|
||||
generate_cmd.add_argument(
|
||||
"--quick",
|
||||
action="store_true",
|
||||
help="Quick mode - don't regenerate thumbnails",
|
||||
)
|
||||
generate_cmd.add_argument(
|
||||
"album_path",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Path to the main photos directory",
|
||||
)
|
||||
|
||||
# Clean subcommand
|
||||
clean_cmd = subcommands.add_parser(
|
||||
"clean",
|
||||
help="Remove all generated content from the photo album directory",
|
||||
)
|
||||
clean_cmd.set_defaults(action="clean")
|
||||
clean_cmd.add_argument(
|
||||
"album_path",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Path to the main photos directory",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if not hasattr(args, "action"):
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
########################################
|
||||
# Command functions
|
||||
def cmd_init(args: Namespace) -> None:
|
||||
"""
|
||||
Generate a basic config and template files
|
||||
"""
|
||||
album_path = Path(args.album_path)
|
||||
config_path = album_path / args.config
|
||||
if config_path.exists():
|
||||
logger.warning(
|
||||
f"Looks like {album_path} is already set up. If you want to start over and "
|
||||
f"overwrite any of your customizations, remove {config_path}"
|
||||
)
|
||||
return
|
||||
|
||||
skel_dir = Path(__file__).parent / "skel"
|
||||
logger.debug(f"Skeleton dir: {skel_dir}")
|
||||
|
||||
skel_files = []
|
||||
for parent_path, dirnames, filenames in skel_dir.walk():
|
||||
for filename in filenames:
|
||||
skel_file_path = parent_path / filename
|
||||
rel_path = skel_file_path.relative_to(skel_dir)
|
||||
album_file_path = album_path / rel_path
|
||||
|
||||
skel_files.append(album_file_path)
|
||||
|
||||
album_file_path.parent.mkdir(exist_ok=True)
|
||||
album_file_path.write_bytes(skel_file_path.read_bytes())
|
||||
logger.debug(f"Created skeleton file {album_file_path}")
|
||||
|
||||
print("Some basic files have been created for your album. Edit them as you need:")
|
||||
for p in skel_files:
|
||||
print(f" - {p}")
|
||||
|
||||
|
||||
def cmd_generate(args: Namespace, config: Config) -> None:
|
||||
logger.debug(f"Generating in {args.album_path}")
|
||||
generate(config, Path(args.album_path))
|
||||
|
||||
|
||||
def cmd_clean(args: Namespace, config: Config) -> None:
|
||||
"""
|
||||
Clean the photo album by all files that photojawn generated
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
########################################
|
||||
# CLI Util functions
|
||||
def setup_logging(level_str: str) -> None:
|
||||
levels = logging.getLevelNamesMapping()
|
||||
level = levels[level_str.upper()]
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format="[%(name)s] %(message)s",
|
||||
handlers=[RichHandler(rich_tracebacks=True)],
|
||||
)
|
||||
# Override PIL logging because debug is really noisy
|
||||
if level <= logging.DEBUG:
|
||||
logging.getLogger("PIL").setLevel(logging.INFO)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
41
py/photojawn/config.py
Normal file
41
py/photojawn/config.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
import yaml
|
||||
|
||||
DEFAULT_CONFIG_PATH = "photojawn.conf.yml"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
# Size of thumbnails when looking at a folder page
|
||||
thumbnail_size: tuple[int, int] = (256, 256)
|
||||
|
||||
# Size of the image when looking at the standalone image page
|
||||
view_size: tuple[int, int] = (1920, 1080)
|
||||
|
||||
# Directory inside the photo directory to output the site to
|
||||
output_dir: str = "site"
|
||||
|
||||
# Quick mode:
|
||||
# - Don't regenerate thumbnails if they already exist
|
||||
quick: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, contents: bytes) -> "Config":
|
||||
conf = cls()
|
||||
data = yaml.safe_load(contents)
|
||||
if data is None:
|
||||
return conf
|
||||
|
||||
for key, val in data.items():
|
||||
match key:
|
||||
case "output_dir":
|
||||
conf.output_dir = val
|
||||
case "thumnail_size":
|
||||
conf.thumbnail_size = tuple(val)
|
||||
case "view_size":
|
||||
conf.view_size = tuple(val)
|
||||
return conf
|
||||
282
py/photojawn/generate.py
Normal file
282
py/photojawn/generate.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Optional
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from markdown import markdown
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from rich.progress import Progress, track
|
||||
|
||||
from photojawn.config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageDirectory:
|
||||
path: Path
|
||||
children: list["ImageDirectory"]
|
||||
images: list["ImagePath"]
|
||||
is_root: bool = False
|
||||
description: str = ""
|
||||
|
||||
cover_path: Optional["ImagePath"] = None
|
||||
|
||||
def walk(self) -> Iterator["ImageDirectory"]:
|
||||
yield self
|
||||
for child in self.children:
|
||||
yield from child.walk()
|
||||
|
||||
def image_paths(self) -> list["ImagePath"]:
|
||||
"""
|
||||
Iterate through all images in this dir and children
|
||||
"""
|
||||
images = []
|
||||
for image_dir in self.walk():
|
||||
images += image_dir.images
|
||||
return images
|
||||
|
||||
def cover_image_paths(self) -> list["ImagePath"]:
|
||||
images = []
|
||||
for image_dir in self.walk():
|
||||
if image_dir.cover_path is not None:
|
||||
images.append(image_dir.cover_path)
|
||||
return images
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImagePath:
|
||||
path: Path
|
||||
description: str = ""
|
||||
|
||||
def thumbnail_filename(self) -> str:
|
||||
return self.path.stem + ".thumb" + self.path.suffix
|
||||
|
||||
def thumbnail_path(self) -> Path:
|
||||
return self.path.parent / "slides" / self.thumbnail_filename()
|
||||
|
||||
def display_filename(self) -> str:
|
||||
return self.path.stem + ".screen" + self.path.suffix
|
||||
|
||||
def display_path(self) -> Path:
|
||||
return self.path.parent / "slides" / self.display_filename()
|
||||
|
||||
def html_filename(self) -> str:
|
||||
return self.path.with_suffix(".html").name
|
||||
|
||||
def html_path(self) -> Path:
|
||||
return self.path.parent / "slides" / self.html_filename()
|
||||
|
||||
|
||||
def generate(config: Config, album_path: Path) -> None:
|
||||
"""
|
||||
Main generation function
|
||||
"""
|
||||
# Change the working directory to the album_path so that all paths are relative to
|
||||
# it when we find images. We need to do this because all the paths in HTML need to
|
||||
# be relative to it and we don't want to have to do a bunch of path gymnastics to
|
||||
# re-relative all those paths.
|
||||
orig_wd = Path.cwd()
|
||||
os.chdir(album_path)
|
||||
|
||||
root_dir = find_images(config, Path("."))
|
||||
generate_images(config, root_dir)
|
||||
generate_html(config, root_dir)
|
||||
shutil.copytree("static", Path(config.output_dir) / "static")
|
||||
|
||||
os.chdir(orig_wd)
|
||||
|
||||
|
||||
def find_images(config: Config, root_path: Path) -> ImageDirectory:
|
||||
"""
|
||||
Build up an ImageDirectory to track all of the directories and their images.
|
||||
|
||||
A directory with no images nor childern with images will not be tracked, since we
|
||||
don't want to render an album page for those.
|
||||
"""
|
||||
# image_dirs keeps track of all directories we find with images in them, so we can
|
||||
# attach them as children to parent directories
|
||||
image_dirs: dict[Path, ImageDirectory] = {
|
||||
root_path: ImageDirectory(path=root_path, children=[], images=[], is_root=True)
|
||||
}
|
||||
|
||||
for dirpath, dirnames, filenames in root_path.walk(top_down=False):
|
||||
if dirpath.name in {"slides", "_templates", "static", config.output_dir}:
|
||||
continue
|
||||
|
||||
image_dir = image_dirs.get(
|
||||
dirpath,
|
||||
ImageDirectory(
|
||||
path=dirpath,
|
||||
children=[],
|
||||
images=[],
|
||||
),
|
||||
)
|
||||
|
||||
for dirname in sorted(dirnames):
|
||||
child_path = dirpath / dirname
|
||||
if child_path in image_dirs:
|
||||
image_dir.children.append(image_dirs[child_path])
|
||||
|
||||
for filename in sorted(filenames):
|
||||
file_path = dirpath / filename
|
||||
if filename == "description.txt":
|
||||
image_dir.description = file_path.read_text()
|
||||
elif filename == "description.md":
|
||||
image_dir.description = markdown(file_path.read_text())
|
||||
elif is_image(file_path):
|
||||
ip = ImagePath(file_path)
|
||||
|
||||
# Set a cover image for the album. Use "cover.jpg" if one exists,
|
||||
# otherwise use the first image we find.
|
||||
if file_path.stem == "cover":
|
||||
image_dir.cover_path = ip
|
||||
# Don't add the cover image to the list of images, we want to handle
|
||||
# that separately
|
||||
continue
|
||||
|
||||
# If there's an associated .txt or .md file, read it in as the image's
|
||||
# description
|
||||
if file_path.with_suffix(".md").exists():
|
||||
ip.description = markdown(file_path.with_suffix(".md").read_text())
|
||||
elif file_path.with_suffix(".txt").exists():
|
||||
ip.description = file_path.with_suffix(".txt").read_text()
|
||||
|
||||
image_dir.images.append(ip)
|
||||
|
||||
if image_dir.cover_path is None:
|
||||
if len(image_dir.images) > 0:
|
||||
image_dir.cover_path = image_dir.images[0]
|
||||
elif len(image_dir.children) > 0:
|
||||
cover = image_dir.children[0].cover_path
|
||||
logger.debug(f"nested cover path for {image_dir.path.name}: {cover}")
|
||||
image_dir.cover_path = cover
|
||||
|
||||
image_dirs[image_dir.path] = image_dir
|
||||
|
||||
return image_dirs[root_path]
|
||||
|
||||
|
||||
def is_image(path: Path) -> bool:
|
||||
"""
|
||||
Returns True if PIL thinks the file is an image
|
||||
"""
|
||||
try:
|
||||
Image.open(path)
|
||||
return True
|
||||
except UnidentifiedImageError:
|
||||
return False
|
||||
|
||||
|
||||
def generate_images(config: Config, root_dir: ImageDirectory) -> None:
|
||||
"""
|
||||
Find all of the images and generate various image sizes
|
||||
"""
|
||||
# Include cover images here because we want thumbnails for all of them
|
||||
all_images = root_dir.image_paths() + root_dir.cover_image_paths()
|
||||
|
||||
for image_path in track(all_images, description="Making smaller images..."):
|
||||
orig_img = Image.open(image_path.path)
|
||||
|
||||
slides_path = config.output_dir / image_path.path.parent / "slides"
|
||||
slides_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# Copy the original image in
|
||||
orig_new_path = config.output_dir / image_path.path
|
||||
if not orig_new_path.exists() or not config.quick:
|
||||
logger.info(f"Copying original image to {orig_new_path}")
|
||||
orig_img.save(orig_new_path)
|
||||
|
||||
thumb_path = config.output_dir / image_path.thumbnail_path()
|
||||
if not thumb_path.exists() or not config.quick:
|
||||
thumb_img = orig_img.copy()
|
||||
thumb_img.thumbnail(config.thumbnail_size)
|
||||
thumb_img.save(thumb_path)
|
||||
logger.info(
|
||||
f'Generated thumbnail size "{image_path.path}" -> "{thumb_path}"'
|
||||
)
|
||||
|
||||
screen_path = config.output_dir / image_path.display_path()
|
||||
if not screen_path.exists() or not config.quick:
|
||||
screen_img = orig_img.copy()
|
||||
screen_img.thumbnail(config.view_size)
|
||||
screen_img.save(screen_path)
|
||||
logger.info(f'Generated screen size "{image_path.path}" -> "{screen_path}"')
|
||||
|
||||
|
||||
def generate_html(config: Config, root_dir: ImageDirectory) -> None:
|
||||
"""
|
||||
Recursively generate HTML files for this directory and all children
|
||||
"""
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader(root_dir.path / "_templates"),
|
||||
autoescape=select_autoescape(),
|
||||
)
|
||||
|
||||
album_tmpl = jinja_env.get_template("album.html")
|
||||
photo_tmpl = jinja_env.get_template("photo.html")
|
||||
|
||||
with Progress() as progress:
|
||||
task = progress.add_task("Rendering HTML...", total=len(root_dir.image_paths()))
|
||||
|
||||
for album_dir in root_dir.walk():
|
||||
html_path = album_dir.path / "index.html"
|
||||
root_path = root_dir.path.relative_to(html_path.parent, walk_up=True)
|
||||
html_path = config.output_dir / html_path
|
||||
|
||||
breadcrumbs = []
|
||||
if not album_dir.is_root:
|
||||
crumb_pos = album_dir.path.parent
|
||||
while crumb_pos != root_dir.path:
|
||||
breadcrumbs.append(
|
||||
(
|
||||
str(crumb_pos.relative_to(album_dir.path, walk_up=True)),
|
||||
crumb_pos.name,
|
||||
)
|
||||
)
|
||||
crumb_pos = crumb_pos.parent
|
||||
breadcrumbs.reverse()
|
||||
|
||||
logger.debug(f"Rendering {html_path}")
|
||||
with html_path.open("w") as f:
|
||||
f.write(
|
||||
album_tmpl.render(
|
||||
root_path=root_path,
|
||||
album_dir=album_dir,
|
||||
breadcrumbs=breadcrumbs,
|
||||
)
|
||||
)
|
||||
|
||||
for pos, image_path in enumerate(album_dir.images):
|
||||
# TODO: If a file with a matching name but .txt or .md, add that as the
|
||||
# description for the image
|
||||
if image_path.path.stem == "cover":
|
||||
continue
|
||||
|
||||
html_path = image_path.html_path()
|
||||
root_path = root_dir.path.relative_to(html_path.parent, walk_up=True)
|
||||
html_path = config.output_dir / html_path
|
||||
html_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
prev_image = None
|
||||
next_image = None
|
||||
if pos != 0:
|
||||
prev_image = album_dir.images[pos - 1]
|
||||
if pos < len(album_dir.images) - 1:
|
||||
next_image = album_dir.images[pos + 1]
|
||||
|
||||
logger.debug(f"Rendering {html_path}")
|
||||
with html_path.open("w") as f:
|
||||
f.write(
|
||||
photo_tmpl.render(
|
||||
root_path=root_path,
|
||||
image_path=image_path,
|
||||
prev_image=prev_image,
|
||||
next_image=next_image,
|
||||
)
|
||||
)
|
||||
|
||||
progress.update(task, advance=1)
|
||||
55
py/photojawn/skel/_templates/album.html
Normal file
55
py/photojawn/skel/_templates/album.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% if not album_dir.is_root %}
|
||||
<h1>
|
||||
<a href="{{root_path}}">Home</a>
|
||||
{% for href, name in breadcrumbs %}
|
||||
/ <a href="{{href}}">{{name}}</a>
|
||||
{% endfor %}
|
||||
/ {{album_dir.path.name}}
|
||||
</h1>
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% if album_dir.description %}
|
||||
<div class="caption">
|
||||
{{ album_dir.description | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if album_dir.children %}
|
||||
<h2>Albums</h2>
|
||||
<div id="album-children">
|
||||
{% for child in album_dir.children %}
|
||||
<div class="album">
|
||||
<a href="{{child.path.name}}/">
|
||||
<div>
|
||||
{% if child.cover_path %}
|
||||
<img
|
||||
src="{{child.cover_path.path.parent.relative_to(album_dir.path)}}/slides/{{child.cover_path.thumbnail_filename()}}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{{child.path.name}}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if album_dir.images %}
|
||||
{% if album_dir.children %}
|
||||
<h2>Photos</h2>
|
||||
{% endif %}
|
||||
<div id="album-photos">
|
||||
{% for image in album_dir.images %}
|
||||
<div class="thumbnail">
|
||||
<a href="slides/{{image.html_filename()}}">
|
||||
<img src="slides/{{image.thumbnail_filename()}}" />
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
24
py/photojawn/skel/_templates/base.html
Normal file
24
py/photojawn/skel/_templates/base.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>My Photos</title>
|
||||
<link rel="stylesheet" href="{{ root_path }}/static/index.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<a href="{{ root_path }}">
|
||||
<h1>My Photos</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div id="content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
{% block js %}
|
||||
{% endblock %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
54
py/photojawn/skel/_templates/photo.html
Normal file
54
py/photojawn/skel/_templates/photo.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block js %}
|
||||
// Do left/right navigation on keypresses
|
||||
document.onkeydown = function(event) {
|
||||
if (event.key == "ArrowLeft") {
|
||||
{% if prev_image %}
|
||||
location.href = "{{prev_image.html_filename()}}";
|
||||
{% endif %}
|
||||
} else if (event.key == "ArrowRight") {
|
||||
{% if next_image %}
|
||||
location.href = "{{next_image.html_filename()}}";
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="photo">
|
||||
<img src="{{image_path.display_filename()}}" />
|
||||
</div>
|
||||
|
||||
<div id="nav">
|
||||
<div>
|
||||
{% if prev_image %}
|
||||
<a href="{{prev_image.html_filename()}}">
|
||||
<i class="arrow arrow-left"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="..">
|
||||
<i class="arrow arrow-up"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
{% if next_image %}
|
||||
<a href="{{next_image.html_filename()}}">
|
||||
<i class="arrow arrow-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="photo-description" class="caption">
|
||||
{% if image_path.description %}
|
||||
{{ image_path.description | safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="download">
|
||||
<a href="../{{image_path.path.name}}">view full size</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
py/photojawn/skel/photojawn.conf.yml
Normal file
9
py/photojawn/skel/photojawn.conf.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Max size of thumbnails when viewing an album
|
||||
thumbnail_size: [256, 256]
|
||||
|
||||
# Max size of images when viewing a single one on screen
|
||||
view_size: [1024, 768]
|
||||
|
||||
# Directory where the generated site will be created in. All original images will be
|
||||
# copied in to here with the same directory structure.
|
||||
output_dir: "site"
|
||||
112
py/photojawn/skel/static/index.css
Normal file
112
py/photojawn/skel/static/index.css
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ba1200;
|
||||
}
|
||||
|
||||
#header {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#header * {
|
||||
margin-top: 0;
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
#content {
|
||||
text-align: center;
|
||||
max-width: 1200px;
|
||||
margin: 0.5em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#content > * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
#album-children {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#album-children > * {
|
||||
margin: 1em;
|
||||
padding: 0.75em;
|
||||
background-color: lightgrey;
|
||||
height: min-content;
|
||||
border: thin solid black;
|
||||
box-shadow: 0.25em 0.25em #ccc;
|
||||
}
|
||||
|
||||
#album-photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
margin: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thumbnail * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#nav {
|
||||
width: 150px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
#nav div {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#photo img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.caption {
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
border: solid black;
|
||||
border-width: 0 3px 3px 0;
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
transform: rotate(-45deg);
|
||||
-webkit-transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
transform: rotate(135deg);
|
||||
-webkit-transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.arrow-up {
|
||||
transform: rotate(-135deg);
|
||||
-webkit-transform: rotate(-135deg);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue