80 Commits

Author SHA1 Message Date
d3af86be8c Hotfix - code hanging 2025-08-26 20:47:51 +02:00
c825798b13 Typo 2025-08-26 11:53:58 +02:00
b5375343a8 Merge pull request '2.0 - WebUI builder ("Cielight" merge)' (#9) from beta into main
Reviewed-on: #9
2025-08-26 10:52:12 +02:00
757e676d2d Typo 2025-08-26 10:36:16 +02:00
0079c166e8 Dark reader lock 2025-08-26 00:09:49 +02:00
ee6d4a1fa2 Fixed inner issues + README 2025-08-23 10:30:35 +02:00
c6c3162b83 Merge branch 'main' into front 2025-08-22 18:41:36 +02:00
04c1214cd1 Fully responsive 2025-08-22 18:30:49 +02:00
b03779b487 Responsive 2025-08-22 16:35:10 +02:00
b5f8ceeb31 Styled loader and modal 2025-08-22 15:29:18 +02:00
1591886505 Build and upload loader 2025-08-22 12:30:10 +02:00
a6b63c2d2b Added tag validate button 2025-08-22 11:54:29 +02:00
8a04fe5aa6 Better title 2025-08-21 23:25:15 +02:00
2cb171806c Fixed title 2025-08-21 23:19:06 +02:00
ded97700d9 Fixed color btn 2025-08-21 23:17:27 +02:00
8533ce72e9 Flask webui templates 2025-08-21 23:12:10 +02:00
b2ba1d7c7f Fixed compose 2025-08-21 18:55:43 +02:00
5d238fcf33 Version and docker OK 2025-08-21 18:55:05 +02:00
7675b90909 Footer 2025-08-21 18:07:15 +02:00
a916c80c2a Stepper 2025-08-21 00:00:37 +02:00
cb91b92555 4 steps OK 2025-08-20 20:19:24 +02:00
f6e6a11fc1 Changed gallery url 2025-08-19 19:42:36 +02:00
e9a3a5a189 Build system with zip download 2025-08-19 19:29:06 +02:00
4ac176f8a9 Theme editor UI refinement 2025-08-19 12:17:40 +02:00
1ea94b469b Theme editor UI ajdustment 2025-08-19 11:52:38 +02:00
2ec4be624b Theme editor 2025-08-18 23:36:36 +02:00
369704a87c Better h2 title 2025-08-18 22:21:59 +02:00
330e467dcb Better global UI 2025-08-18 20:05:08 +02:00
305042b365 Lumeex scroll 2 top merge 2025-08-18 13:11:34 +02:00
7a95ef0255 Fixed demo tag 2025-08-18 11:07:04 +00:00
906699f023 Merge pull request 'v1.3.2 - Hotfix -> Scroll to tup button + tag selection move to top' (#8) from comments into main
Reviewed-on: #8
2025-08-18 13:05:28 +02:00
643a729f94 Hotfix 2025-08-18 13:01:51 +02:00
7a7876e5ef Better form 2025-08-18 12:29:45 +02:00
a02da47e73 fixed scroll to tup button 2025-08-18 10:24:08 +00:00
8cb81a74cf Refreshed site.yaml 2025-08-17 23:51:15 +02:00
c193fd49aa Better UI form 2025-08-17 23:49:36 +02:00
5d863223e3 Form UI 1st step 2025-08-17 22:44:54 +02:00
a8f3c1b497 All fields OK 2025-08-17 14:26:31 +02:00
b74f1bb350 Site-info front 2025-08-17 13:52:25 +02:00
5a6f08644a Fixed responsive issue 2025-08-17 12:38:29 +02:00
031ff62168 Better css 2025-08-17 11:26:21 +02:00
b56d03303e Remove all button 2025-08-17 10:57:59 +02:00
d3484a4b50 Confirmation modale for deletion 2025-08-17 00:37:55 +02:00
9d37b0a60f Better tag UI 2025-08-16 22:57:56 +02:00
080eb2593d Header 2025-08-16 21:40:38 +02:00
73a0dd0ce6 Better UI 2025-08-16 16:30:56 +02:00
97645b06fa Most used tags 2025-08-16 14:08:34 +02:00
142c042b86 Better ui tag system 2025-08-16 13:14:04 +02:00
041db66b3d Reworked flow 2025-08-16 11:17:15 +02:00
1b0b228273 Gallery front 2025-08-16 10:29:51 +02:00
f7f2356510 Better comments 2025-08-15 13:36:48 +00:00
41450837f2 Versionning 2025-08-13 22:14:16 +00:00
4edeb8709a Click on photo tag -> Photo on top + scroll to top 2025-08-13 22:10:03 +00:00
6fc573c510 Merge pull request 'Slimmer docker image + fifo file check' (#7) from docker into main
Reviewed-on: #7
2025-08-13 18:21:24 +02:00
43c007c1fe Slimmer docker image + fifo file check 2025-08-13 16:20:46 +00:00
dfbd532efd Merge pull request 'Docker support' (#6) from docker into main
Reviewed-on: #6
2025-08-13 17:47:25 +02:00
efe1bbca29 new ensure_dir logic
+ better logs
+ docker files
+ new docs
2025-08-13 15:45:33 +00:00
7e1a5e659f Merge pull request 'errors' (#5) from errors into main
Reviewed-on: #5
2025-08-12 19:35:54 +02:00
f5a5aefd09 gallery.py starter + rename builder.py 2025-08-12 17:31:46 +00:00
f76420b2c3 favicon log + better robot.txt + modular starter 2025-08-12 17:08:31 +00:00
3901bf8acf Fixed typo 2025-08-11 17:15:46 +00:00
39b24a05cb Fixed div balise 2025-08-11 12:51:46 +00:00
d379fc63d1 Merge pull request 'New documentations and default files' (#4) from docs into main
Reviewed-on: #4
2025-08-11 14:50:42 +02:00
af6b2289e0 Fixed typo 2025-08-11 12:48:28 +00:00
5728ebb649 Logo 2025-08-11 12:46:54 +00:00
7f86f8f522 Better readme 2025-08-11 12:36:40 +00:00
080209d202 New modern favicon 2025-08-11 09:58:49 +00:00
e4a9c57b31 Output folder instead of .output + now build.py pass the build dir var to html_generator.py + new README with link to the wiki 2025-08-10 09:29:56 +00:00
d0fe57fe9c Updating build.py version 2025-08-09 23:40:35 +00:00
bf71ac6dde Hero carrousel in gallery.py instead of site.yaml + removed deleted img for gallery.py 2025-08-09 23:25:23 +00:00
f069ee1065 Demo folder + default config 2025-08-09 22:21:21 +00:00
b0c991af58 Convert and resize by default to True 2025-08-08 22:29:04 +00:00
224441f629 Hotfix - Build date version 2025-08-08 11:34:22 +00:00
b378e9a386 Hotfix - Build version 2025-08-08 11:18:13 +00:00
2749302082 Hotfix - Mobile img margin-top 2025-08-08 11:13:27 +00:00
810698c578 Merge pull request 'Head errors fixed + legals canonical url fixed + better comments' (#3) from errors into main
Reviewed-on: #3
2025-08-08 12:48:24 +02:00
216abe46cd Merge branch 'main' into errors 2025-08-08 12:47:53 +02:00
5c2996f87c Merge pull request 'Fixed img margin' (#2) from ui into main
Reviewed-on: #2
2025-08-08 12:46:23 +02:00
81c6064446 Head errors fixed + legals canonical url fixed + better comments 2025-08-08 10:41:00 +00:00
104d8f4ef9 Fixed img margin 2025-08-06 20:55:52 +00:00
75 changed files with 5173 additions and 1264 deletions

8
.gitignore vendored
View File

@ -1,3 +1,5 @@
.venv
.output
__pycache__
.*
!.sh
!.gitignore
output/
__pycache__/

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY build.py gallery.py VERSION /app/
COPY ./src/ ./src/
COPY ./config /app/default
COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
RUN printf '#!/bin/sh\n/app/entrypoint.sh build\n' > /usr/local/bin/build && chmod +x /usr/local/bin/build && \
printf '#!/bin/sh\n/app/entrypoint.sh gallery\n' > /usr/local/bin/gallery && chmod +x /usr/local/bin/gallery
ENTRYPOINT ["/app/entrypoint.sh"]

263
README.MD
View File

@ -1,234 +1,67 @@
<h1 align="center">Lumeex</h1>
<div align="center" >
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex.png" alt="Lumeex Screenshot">
<div align="center">
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/logo.svg" alt="Lumeex Screenshot" width="400"/>
</div>
<p/>
<div align="center">
<p>Yet another minimalist, lightweight photo gallery static site generator.</p>
</div>
</p>
<div align="center">
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex.png" alt="Lumeex Screenshot" />
</div>
**Lumeex** - Yet another minimalist photo gallery with a static site generator.
Lumeex is a static site generator designed to create minimalist photo galleries that highlight your artworks over the author. It empowers users to organize and explore images using tags, with each page load presenting photos in a random order to encourage discovery of new content.
Lumeex is a static site generator that builds a minimalist photo gallery. The project was born from the need to create a gallery focused more on the artworks than the author, while allowing users to organize works using tags and share filtered views. In this spirit, each page load displays the photos in random order, allowing users to discover new content they might not have seen before.
The project includes two thoughtfully designed themes—one modern, one minimalistic—both crafted to keep the spotlight on your photos:
The project comes with two themes: one modern, the other more minimalistic, both designed to keep the focus on the artworks:
- Modern 👉 [demo](https://modern.djeex.fr)
- Typewriter 👉 [demo](https://typewriter.djeex.fr)
- **Modern** — [View Demo](https://modern.djeex.fr)
- **Typewriter** — [View Demo](https://typewriter.djeex.fr)
> [!NOTE]
> _This GitHub repository is a mirror of the primary source at [git.djeex.fr/Djeex/lumeex](https://git.djeex.fr/Djeex/lumeex). The main repository includes the full history and releases_.
> [!NOTE]
> _This GitHub repository is a mirror of https://git.djeex.fr/Djeex/lumeex. Youll find the complete package, history, and release notes there. An LLM is used for bug checking._
## 📌 Table of Contents
- [Features](#features)
- [Python Installation](#python-installation)
- [Configuration](#configuration)
- [Build the Site](#build-the-site)
- [Features](#-features)
- [🐳 Docker or 🐍 Python Installation](#-docker-or--python-installation)
## Features
**Gallery (Static Website)**
- Photos displayed in a random order on each page load.
- Tag-based filtering (with the ability to combine multiple tags).
- Shareable URLs with active tag filters.
- A photo carousel on the landing page.
- A legal notice page.
- Two visual themes (easily customizable):
- Modern 👉 [demo](https://modern.djeex.fr)
- Typewriter 👉 [demo](https://typewriter.djeex.fr)
- Supports Google Fonts and local fonts.
## ✨ Features
**No-Code Builder Based on YAML Files**
- YAML files to configure site information, SEO, colors, fonts, etc.—no code needed
- YAML files to reference and tag photos—no code needed.
- *(Optional)* Automatically add photos to the reference file.
### Gallery (Static Website)
**Simple Build Process**
- Compiles from YAML config files (theme selection, template building, fonts, colors, etc.).
- Automatically converts the favicon to all required formats.
- Automatically resize social thumbnail
- *(Optional)* Automatically resizes photos to a max width of 1140px.
- *(Optional)* Converts images to WebP for better performance.
- Outputs a fully generated static website, ready to be copied to any web server.
- Photos displayed in a new random order with every page load
- Tag-based filtering with multi-tag support
- Shareable URLs that retain active tag filters
- Photo carousel on the homepage
- Legal notice page included
- Two customizable visual themes:
- Modern — [Demo](https://modern.djeex.fr)
- Typewriter — [Demo](https://typewriter.djeex.fr)
- Supports Google Fonts and locally hosted fonts
## Python Installation
### No-Code Builder (WebUI Manager)
Instructions to run the Python scripts directly.
&nbsp;
<div align="center">
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex-webui.png" alt="Lumeex Screenshot" />
</div>
&nbsp;
**Requirements**
- Python 3.11 or higher
- PyYAML
- Pillow
- Configure site info, SEO, colors, fonts, and more through a simple convenient WebUI
- Add and tag your photo photos without any coding required
- Converts favicon automatically to all required formats
- Resizes social sharing thumbnails
- *(Optional)* Automatically resizes photos to a maximum width of 1140px
- *(Optional)* Converts images to WebP format for optimized performance
- Build your static site in one click and get a zip archive or an output folder, ready to deploy to your preferred webserver
**Installation**
### Don't want a WebUI ?
```sh
git clone https://git.djeex.fr/Djeex/lumeex.git
cd lumeex
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
- CLI process is documented
You're ready to go!
## Configuration
All user configuration files are located in the `config` folder.
```sh
Lumeex/
└── config/
├── photos/
│ ├── gallery
│ └── hero
├── themes/
│ ├── modern
│ └── typewriter
├── gallery.yaml
└── site.yaml
```
**`photos/`**
- `gallery/`: place your gallery photos here.
- `hero/`: place carousel photos for the homepage here.
> [!TIP]
> You can use `gallery.py` to automatically reference all photos in `gallery/` and `hero/` into `gallery.yaml` and `site.yaml` by running `python3 gallery.py` from the `lumeex` directory.
> Youll just need to tag the photos in `gallery.yaml`.
**`site.yaml`**
This file contains all your sites metadata and settings. For example:
```yaml
info:
title: your title
subtitle: your subtitle
description: your description
canonical: all, your, keywords
author: you
google_analytics_id: G-XXXXXXX # optional
social:
instagram_url: https://www.instagram.com/yourprofile
thumbnail: gallery/anyphoto.png # put the path from your photo folder to your file
menu:
items:
- label: your_home
href: /
- label: your_second_menu
href: /?tag=yourtag1
- label: Your_third_menu
href: /?tag=yourtag2
hero:
images:
- src: hero/your_photo_1.jpg
- src: hero/your_photo_2.jpg
- src: hero/your_photo_3.jpg
footer:
copyright: Copyright © 2025 You
legal_link: '/legals.html'
legal_label: Legal notice
build:
theme: modern
convert_images: false
resize_images: false
legals:
hoster_name: Your_hoster
hoster_adress: Your hoster address
hoster_contact: Your hoster contact
intellectual_property:
- paragraph: "Your text here"
- paragraph: "Your second paragraph here"
- paragraph: "Etc..."
```
**`gallery.yaml`**
Use this file to reference the images in `photos/gallery/`. You can do this manually or automatically by running `python3 gallery.py`. You can also assign tags to the photos here.
```yaml
images:
- src: gallery/your_photo_1.jpg
tags: ["portrait"]
- src: gallery/your_photo_2.jpg
tags: ["portrait", "sunset", "boat"]
- src: gallery/your_photo_3.jpg
tags: ["landscape", "sea", "beach", "sand"]
```
**`themes/`**
```sh
themes/
└── yourtheme/
├── fonts (optional)
├── theme.yaml
├── theme.css (optional)
└── favicon.png
```
**Lumeex** is shipped with two prebuilt themes:
- Modern 👉 [demo](https://modern.djeex.fr)
- Typewriter 👉 [demo](https://typewriter.djeex.fr)
You can edit existing themes or create your own. Each theme can include:
- **Required:** a `theme.yaml` file for visual settings (colors, fonts, etc.)
- *(Optional)* a `theme.css` file for additional styling
- *(Optional)* a `fonts` folder for local fonts
- *(Optional)* a square `favicon.png` (min 196px) that will be automatically converted to all required formats.
Example `theme.yaml`:
```yaml
colors:
primary: '#0065a1'
primary_dark: '#005384'
secondary: '#00b0f0'
accent: '#ffc700'
text_dark: '#333'
background: '#fff'
browser_color: '#fff'
favicon:
path: favicon.png
google_fonts:
- family: Lato
weights:
- '200'
- '400'
- '700'
- family: Montserrat
weights:
- '200'
- '400'
- '700'
fonts:
primary:
name: Lato
fallback: sans-serif
secondary:
name: Montserrat
fallback: serif
```
## Build the Site
Once everything is configured, make sure you're in the `lumeex` directory and your Python virtual environment is activated (`source .venv/bin/activate`).
- *(Optional)* Run `python3 gallery.py` to auto-fill `gallery.yaml` and add carousel photos to `site.yaml`. Don't forget to add tags to your photos in `gallery.yaml`.
- Run `python3 build.py` to generate the static site.
- *(Optional)* Serve locally with:
```sh
python3 -m http.server 3000 --directory .output
```
Then visit `http://localhost:3000` or, if remote, `http://your-server-ip:3000`.
> [!WARNING]
> Use this only to test your site. Don't use python server for production !
- Finally, copy the contents of the `.output/` directory to your favorite web server.
## 🐳 Docker or 🐍 Python Installation
For comprehensive documentation on installation, configuration options, customization, and demos, please visit:
https://lumeex.djeex.fr

1
VERSION Normal file
View File

@ -0,0 +1 @@
2.0.0

164
build.py
View File

@ -1,164 +1,6 @@
import logging
from datetime import datetime
from pathlib import Path
from shutil import copyfile
from PIL import Image
from src.py.utils import ensure_dir, copy_assets, load_yaml, load_theme_config
from src.py.css_generator import generate_css_variables, generate_fonts_css, generate_google_fonts_link
from src.py.image_processor import process_images, copy_original_images, convert_and_resize_image, generate_favicons_from_logo, generate_favicon_ico
from src.py.html_generator import render_template, render_gallery_images, generate_gallery_json_from_images, generate_robots_txt, generate_sitemap_xml
# Configure logging to display only the messages
logging.basicConfig(level=logging.INFO, format='%(message)s')
# Define key directories used throughout the script
SRC_DIR = Path.cwd()
BUILD_DIR = SRC_DIR / ".output"
TEMPLATE_DIR = SRC_DIR / "src/templates"
IMG_DIR = SRC_DIR / "config/photos"
JS_DIR = SRC_DIR / "src/public/js"
STYLE_DIR = SRC_DIR / "src/public/style"
GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
SITE_FILE = SRC_DIR / "config/site.yaml"
THEMES_DIR = SRC_DIR / "config/themes"
def build():
logging.info("🚀 Starting build...")
ensure_dir(BUILD_DIR)
copy_assets(JS_DIR, STYLE_DIR, BUILD_DIR)
build_date = datetime.now().strftime("%Y%m%d%H%M%S")
site_vars = load_yaml(SITE_FILE)
gallery_sections = load_yaml(GALLERY_FILE)
build_section = site_vars.get("build", {})
theme_name = site_vars.get("build", {}).get("theme", "default")
theme_vars, theme_dir = load_theme_config(theme_name, THEMES_DIR)
fonts_dir = theme_dir / "fonts"
theme_css_path = theme_dir / "theme.css"
if theme_css_path.exists():
dest_theme_css = BUILD_DIR / "style" / "theme.css"
dest_theme_css.parent.mkdir(parents=True, exist_ok=True)
copyfile(theme_css_path, dest_theme_css)
theme_css = f'<link rel="stylesheet" href="/style/theme.css?build_date={build_date}">'
logging.info(f"[✓] Theme CSS found, copied to build folder: {dest_theme_css}")
else:
theme_css = ""
logging.warning(f"[~] No theme.css found in {theme_css_path}, skipping theme CSS injection.")
preload_links = generate_fonts_css(fonts_dir, BUILD_DIR / "style" / "fonts.css", fonts_cfg=theme_vars.get("fonts"))
generate_css_variables(theme_vars.get("colors", {}), BUILD_DIR / "style" / "colors.css")
generate_favicons_from_logo(theme_vars, theme_dir, BUILD_DIR / "img" / "favicon")
generate_favicon_ico(theme_vars, theme_dir, BUILD_DIR / "favicon.ico")
convert_images = build_section.get("convert_images", False)
resize_images = build_section.get("resize_images", False)
logging.info(f"[~] convert_images = {convert_images}")
logging.info(f"[~] resize_images = {resize_images}")
hero_images = site_vars.get("hero", {}).get("images", [])
gallery_images = [img for section in gallery_sections for img in section["images"]] if isinstance(gallery_sections, list) else gallery_sections.get("images", [])
if convert_images:
process_images(hero_images, resize_images, IMG_DIR, BUILD_DIR)
process_images(gallery_images, resize_images, IMG_DIR, BUILD_DIR)
else:
copy_original_images(hero_images, IMG_DIR, BUILD_DIR)
copy_original_images(gallery_images, IMG_DIR, BUILD_DIR)
menu_html = "\n".join(
f'<li class="nav-item appear"><a href="{item["href"]}">{item["label"]}</a></li>'
for item in site_vars.get("menu", {}).get("items", [])
)
site_vars["hero"]["menu_items"] = menu_html
if "footer" in site_vars:
site_vars["footer"]["menu_items"] = menu_html
google_fonts_link = generate_google_fonts_link(theme_vars.get("google_fonts", []))
logging.info(f"[✓] Google Fonts link generated:\n{google_fonts_link}")
thumbnail_path = site_vars.get("social", {}).get("thumbnail")
if thumbnail_path:
src_thumb = IMG_DIR / thumbnail_path
dest_thumb_dir = BUILD_DIR / "img" / "social"
dest_thumb_dir.mkdir(parents=True, exist_ok=True)
dest_thumb = dest_thumb_dir / Path(thumbnail_path).name
try:
img = Image.open(src_thumb)
img = img.convert("RGB")
img = img.resize((1200, 630), Image.LANCZOS)
img.save(dest_thumb, "JPEG", quality=90)
logging.info(f"[✓] Thumbnail resized and saved to {dest_thumb}")
except Exception as e:
logging.error(f"[✗] Failed to process thumbnail: {e}")
else:
logging.warning("[~] No thumbnail found in social section")
head_vars = dict(site_vars.get("info", {}))
head_vars.update(theme_vars.get("colors", {}))
head_vars.update(site_vars.get("social", {}))
head_vars["thumbnail"] = f"/img/social/{Path(thumbnail_path).name}" if thumbnail_path else ""
head_vars["google_fonts_link"] = google_fonts_link
head_vars["font_preloads"] = "\n".join(preload_links)
head_vars["theme_css"] = theme_css
head_vars["build_date"] = build_date
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
hero = render_template(TEMPLATE_DIR / "hero.html", {**site_vars["hero"], **head_vars})
footer = render_template(TEMPLATE_DIR / "footer.html", {**site_vars.get("footer", {}), **head_vars})
gallery_html = render_gallery_images(gallery_images)
gallery = render_template(TEMPLATE_DIR / "gallery.html", {"gallery_images": gallery_html})
signature = f"<!-- Build with Lumeex v1.0 | https://git.djeex.fr/Djeex/lumeex | {build_date} -->"
body = f"""
<body>
<div class="page-loader"><div class="spinner"></div></div>
{hero}
{gallery}
{footer}
</body>
"""
output_file = BUILD_DIR / "index.html"
with open(output_file, "w", encoding="utf-8") as f:
f.write(f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{body}\n</html>")
logging.info(f"[✓] HTML generated: {output_file}")
legals_vars = site_vars.get("legals", {})
if legals_vars:
ip_paragraphs = legals_vars.get("intellectual_property", [])
paragraphs_html = "\n".join(f"<p>{item['paragraph']}</p>" for item in ip_paragraphs)
legals_context = {
"hoster_name": legals_vars.get("hoster_name", ""),
"hoster_adress": legals_vars.get("hoster_adress", ""),
"hoster_contact": legals_vars.get("hoster_contact", ""),
"intellectual_property": paragraphs_html,
}
legals_body = render_template(TEMPLATE_DIR / "legals.html", legals_context)
legals_html = f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{legals_body}\n{footer}\n</html>"
output_legals = BUILD_DIR / "legals" / "index.html"
output_legals.parent.mkdir(parents=True, exist_ok=True)
with open(output_legals, "w", encoding="utf-8") as f:
f.write(legals_html)
logging.info(f"[✓] Legals page generated: {output_legals}")
else:
logging.warning("[~] No legals section found in site.yaml")
if hero_images:
generate_gallery_json_from_images(hero_images, BUILD_DIR / "data" / "gallery.json")
else:
logging.warning("[~] No hero images found, skipping JSON generation.")
site_info = site_vars.get("info", {})
canonical_url = site_info.get("canonical", "").rstrip("/")
if canonical_url:
allowed_pages = ["/", "/legals/"]
generate_robots_txt(canonical_url, allowed_pages)
generate_sitemap_xml(canonical_url, allowed_pages)
else:
logging.warning("[~] No canonical URL found in site.yaml info section, skipping robots.txt and sitemap.xml generation.")
logging.info("✅ Build complete.")
from src.py.builder.site_builder import build
if __name__ == "__main__":
build()
logging.basicConfig(level=logging.INFO, format="%(message)s")
build()

View File

@ -1,30 +1,4 @@
# Source your photos here
# Relative path is set from built img folder
# You can also use gallery.py to automatically add photos stored in your /config/photos/gallery folder
# Add tags to your photos as shown below
# remove the # before [] if you removed all images to use gallery.py
images: #[]
- src: gallery/almos-bechtold-3402kvtHhOo-unsplash.jpg
tags: ["portrait"]
- src: gallery/arthur-savary-nLfAqmZ2hJo-unsplash.jpg
tags: ["portrait", "sunset", "boat"]
- src: gallery/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg
tags: ["landscape", "sea", "beach", "sand"]
- src: gallery/gilley-aguilar-ywGDhTlf93E-unsplash.jpg
tags: ["landscape", "sky", "cloud", "mountains"]
- src: gallery/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg
tags: ["lanscape", "sunset", "mountains"]
- src: gallery/jonas-degener-LueP5EdWGFY-unsplash.jpg
tags: ["landscape", "mountains", "fog"]
- src: gallery/michiel-annaert-M27pZnHV6M0-unsplash.jpg
tags: ["flowers", "nature"]
- src: gallery/nir-himi-AjecvkfSHxA-unsplash.jpg
tags: ["landscape", "mountains", "sky"]
- src: gallery/rachel-mcdermott-0fN7Fxv1eWA-unsplash.jpg
tags: ["portrait", "black and white"]
- src: gallery/tianlei-wu-g5o6T-PWT3g-unsplash.jpg
tags: ["cat", "animals"]
- src: gallery/we-care-wild-zLweeVLU9Fo-unsplash.jpg
tags: ["bison", "animals"]
- src: gallery/y-s-z90w7yStOkk-unsplash.jpg
tags: ["frog", "green", "animals"]
hero:
images: []
gallery:
images: []

View File

@ -1,59 +1,36 @@
# This file is filled with the demo info
# Please change this by your settings
# Please change this by your settings.
info:
title: Lumeex
subtitle: A minimalistic Gallery
description: A minimalistic Gallery
canonical: https://lumeex.djeex.fr
keywords: photography, lumen, demo, gallery, minimalistic
author: Djeex
google_analytics_id: G-XXXXXXX # optional
title:
subtitle:
description:
canonical:
keywords:
author:
social:
instagram_url: https://www.instagram.com/
thumbnail: hero/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg
instagram_url:
thumbnail:
menu:
items:
- label: Home
href: /
- label: Nature
href: /?tag=Nature
- label: Landscape
href: /?tag=landscape
- label: Portrait
href: /?tag=portrait
- label: Animals
href: /?tag=animals
hero:
# Source your hero carrousel images here.
# Root folder is img.
# You can also use gallery.py to automatically add images from config/photos/hero folder
# remove the # before [] if you removed all images to use gallery.py
images: #[]
- src: hero/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg
- src: hero/gilley-aguilar-ywGDhTlf93E-unsplash.jpg
- src: hero/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg
footer:
copyright: Copyright © 2025 Lumeex
copyright: Copyright © 2025
legal_link: '/legals/'
legal_label: Legal notice
# Build parameters
build:
theme: modern # choose a theme in config/theme folder.
convert_images: true # use true to automatically convert images to webp small weight images.
resize_images: true # use true to automatically resize to width 1140px (maximum width used in the gallery)
theme: modern # choose a theme in config/theme folder
convert_images: true # true to enable image conversion
resize_images: true # true to enable image resizing
# Change this by your legals
legals:
hoster_name: Djeex
hoster_adress: Paris, France
hoster_contact: contact@djeex.fr
hoster_name:
hoster_adress:
hoster_contact:
intellectual_property:
- paragraph: "Users of this website are required to comply with the provisions of the French Data Protection Act (Loi Informatique et Libertés), the violation of which may result in criminal penalties. In particular, they must refrain from any collection or misuse of personal data accessible through the site, and more generally, from any act likely to infringe upon the privacy or reputation of individuals."
- paragraph: "The overall structure, as well as the software, texts, animated or still images, know-how, and all other components of the site, are the exclusive property of Lumeex"
- paragraph: "Any total or partial reproduction of this website, by any means whatsoever, without the express authorization of Lumeex, is prohibited and constitutes an infringement punishable under articles L.335-2 and following of the French Intellectual Property Code. The same applies to the databases appearing on the website, which are protected by the provisions of the law of July 1, 1998, implementing into the Intellectual Property Code the European directive of March 11, 1996, on the legal protection of databases."
- paragraph: ""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -8,8 +8,8 @@ colors:
secondary: '#00b0f0'
accent: '#ffc700'
text_dark: '#616161'
background: '#fff'
browser_color: '#fff'
background: '#ffffff'
browser_color: '#ffffff'
favicon:
path: favicon.png
google_fonts:

View File

@ -1,610 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20230101 at Mon Apr 30 18:04:47 2007
By Unknown
Copr. LettError; Erik van Blokland 1991
</metadata>
<defs>
<font id="Trixie-Text" horiz-adv-x="550" >
<font-face
font-family="Trixie-Text"
font-weight="500"
font-stretch="normal"
units-per-em="1000"
panose-1="0 0 0 0 0 0 0 0 0 0"
ascent="800"
descent="-200"
x-height="404"
cap-height="560"
bbox="-103 -286 657 882"
underline-thickness="20"
underline-position="-143"
stemh="12"
stemv="11"
unicode-range="U+0020-FB02"
/>
<missing-glyph horiz-adv-x="280"
/>
<glyph glyph-name=".notdef" horiz-adv-x="280"
/>
<glyph glyph-name="space" unicode=" "
d="M0 0z" />
<glyph glyph-name="exclam" unicode="!"
d="M324 133l24 -52l-17 -76l-42 -8l-52 8l-25 24l-9 27l9 43l43 58zM297 569l51 -69l-24 -60l-10 -61l10 -25l-10 -128v-25l-42 -17h-10l-25 42l-7 94v34l7 8l-25 96l18 76l25 35h42z" />
<glyph glyph-name="quotedbl" unicode="&#x22;"
d="M423 635l8 -35l-8 -177l-9 -27l9 -8l-17 -24l-10 -10h-34l-17 59l9 44l-24 50h7l8 -7l9 7l-9 27l-25 76l25 25l26 9zM140 627v-9l-24 9h24zM244 644l42 -51l-8 -7l18 -52h-10l-8 -10l-17 17l-17 -17l-10 -59l10 -17v-35l-10 -8l10 -9v-17l-17 -15h-35l-17 111l9 42
l-17 24l-10 86l27 17h60z" />
<glyph glyph-name="numbersign" unicode="#"
d="M267 596l-28 -3l-2 17l5 3l1 2l-1 21h4l18 26h4l9 -9l-2 -14h2l-3 -13zM323 351l-24 -6l4 16l10 4h30l18 41h2l4 22v7l11 16l-1 2l9 14l-3 4v10l4 3v2l-3 24l9 9l2 7l14 18l3 11l15 58l1 2l-1 21h4l29 3h2l-3 -13l-3 -11l-4 -15l-3 -2v-15l5 -9l-2 -5l-9 -21l-5 -15
l-30 -88l-5 -11l11 -19l-2 -9l-3 1l-5 -38h5h36l24 3l10 -3h4h23l-10 -18h-27l-7 8l-34 -11l-9 6c4 0 -44 -22 -44 -22l-7 -44l-3 -13h2l-2 -15h1l-4 -16h35l2 3h11h33l42 -3l19 -17l-9 -18l-25 4h-50h-20l-52 2l-4 -4l-1 -11l-4 -15l-13 -70l-12 -16l-7 -56l-19 -71
l-28 -13l-11 2l-2 2h-7l1 4l-2 1l2 8v48l8 13l1 10l19 96l4 4l1 2v6l8 9l22 31l4 23l-7 -2l4 3l-14 4l-84 -7l-24 7l-14 -2l-6 -53l-4 -1l-5 -19l-8 -13l3 -20l-29 -89l2 -6l-2 -1l-5 -19l-9 -17l-50 -7l-7 1l15 60l1 10l6 24h-2l-2 9l32 68l1 7l5 4l-4 13l18 23l4 24h-1h-5
h-36l-24 -3l-10 3h-4h-23l10 18h27l7 -8l34 11l9 -6s8 2 18 2l4 9l13 43l-1 7l13 13l1 6l1 12l2 18h-29l-2 -3h-11h-33l-42 3l-19 17l9 18l25 -4h50h20l40 -2l10 33h2l4 22v7l16 33l20 34s27 -2 27 -1l-10 -50l-12 -6l-5 -11l11 -19l-2 -9l-3 1l-19 -29v-12l18 6l35 -3
l8 -11l-6 -12l-36 1l-41 -59l-3 -13h2l-2 -15h1l-5 -19l-3 -4h10l10 7l10 -7l110 6l1 2l12 50l13 13l1 6l1 12l-8 18z" />
<glyph glyph-name="dollar" unicode="$"
d="M356 517v-8l27 -24l15 15v34l10 10h17l8 -10v-25l-17 -17l17 -103l-8 -7l-34 34l-8 25l-44 44l-66 32h-51l-18 -32h-26l-25 24h-8l-9 -9l9 -42l-17 -25l17 -68l33 -25h26v-9l8 -8l44 8l110 -25l76 -77l17 -109l-34 -77l-34 -44l-94 -17h-17l-7 -7l-27 17h-34l-34 24h-25
l-25 -24l-44 7l-8 27l18 84l-10 76v9l10 8l34 -8l17 -17l8 -51l59 -59l61 -17l66 17l27 34l25 25l17 85l-10 25l-76 35l-93 7l-17 35l-17 -18v-7h-17v17l-42 25l-42 51l-10 59l17 44l35 24v18l42 7l51 27zM252 612l11 -26h4l-6 -21v-2l4 -3l-6 -17v-14l4 -9l-2 -15v-11
l9 -18v-7l7 -9l-10 -24v-2l3 -3l-3 -10l-4 -4l5 -14l-1 -2l7 -16l-2 -7l-2 -22h2l2 -55l-4 -25l-2 -12v-6l9 -13l-3 -7l3 -8l-1 -16l4 -4l-4 -15l1 -15h-1l-4 -35l12 -23l-7 -13l4 -4l-1 -7l5 -9l-1 -6v-2l3 -4l2 -5l-4 -14l2 -5l4 -17l2 -6l-4 -9h-2v-24l-2 -10l4 -13l2 -9
v-17l-6 -11l-1 -10l-7 -1l-1 -8l-2 -1v-4h-7l-2 -2l-12 -2l-11 13l-2 15l-5 17v19l-2 1l4 6l-4 13l6 9l-6 12v16v10l4 9l-8 16l8 20l-4 13v19l-4 1l6 17v15l2 11l-8 13l4 4l-5 13v19h1l2 15h2v13l-2 2v26v13l2 19l-4 3l6 27l-11 29l-3 -1v9l16 19l-2 11l-10 6v7h3v13l-7 8
l7 12v10l1 32l-1 15l-4 21v5l7 9l4 15l-2 2v15v11v13h2l2 14l11 9h4z" />
<glyph glyph-name="percent" unicode="%"
d="M477 292h27l43 -76h9v-94h-9v8l-8 10l-27 -27l-52 -49l-66 -18l-69 25l-25 42l8 78l9 8l-9 25l34 44l103 41zM477 667l-7 -59l-18 -35l-58 18l-104 -94l-25 -7l-9 7l-32 -35l14 -66l10 -27l-41 -42v-24l-45 -35h-24l-35 -27l-42 27l-35 49l-9 69l17 35l44 41v11l17 17
l67 7l10 10l41 -10l18 17l59 17l18 17h26l77 87v14l41 28zM428 556l-10 -66l-84 -146v-52l-26 -17l-18 -17v-34l-25 -25v-17l-41 -42l-28 -76h-7l-17 -35l7 -9l-17 -35l-42 -50l-7 -51l-27 -25h-25l-10 25v34l35 42l9 42l60 93l24 52l28 27l6 42v18l43 41l17 59l35 35l9 44
l84 143h27zM477 207l-7 44h-42l-17 -35h-10l-35 -34l11 -69h17v9l7 8h27l32 35zM207 428l-35 17l-34 28l-43 -52l-26 -87l17 -25h27l83 52v25z" />
<glyph glyph-name="ampersand" unicode="&#x26;"
d="M319 524l27 -32l7 -62v-17l-24 -25l-10 -34l-60 -59v-25l43 -44l27 9l17 52l31 33l62 27l42 -18l25 -26l-8 -60l-26 -17l-25 9l-25 35h-27l-24 -35v-58l-18 -17l18 -70l51 -42l67 60l9 7l25 -7l18 -18l-8 -51l-77 -59l-68 17h-17l-35 34h-17l-25 -27l-44 -24l-66 -11
l-11 11l-24 -11l-17 18h-25l-44 44l-18 67l10 128l84 52l34 34v35l-24 34l-10 49l17 52l45 42l17 10zM259 150l-58 85l-35 18l-34 -35l-43 -68v-35l35 -50l60 -26l58 17l35 42zM284 474l-25 25h-17l-41 -34l-11 -42l28 -69l-10 -7l10 -10h7l86 76v17l8 27l-17 17h-18z" />
<glyph glyph-name="quoteright" unicode="&#x2019;"
d="M346 728l14 -13l-8 -103v-20l-33 -41v-13l-56 -48h-19l-8 -6l-28 34l8 27h7l21 28l19 19v20l-33 22l-22 61l28 41l35 19z" />
<glyph glyph-name="parenleft" unicode="("
d="M421 662v-42l-52 -51v-17l-60 -59l-17 -128l-17 -17l-7 -139l17 -25v-17l-10 -7l17 -17l11 -52l31 -76l69 -76l10 -44v-18l-27 -15l-77 77l-6 34l-45 59l-24 118l-18 111l18 145l34 77l66 120l62 59h25z" />
<glyph glyph-name="parenright" unicode=")"
d="M261 653l8 -25l75 -101l43 -145l9 -180l-34 -145l-42 -84l-51 -78l-25 -8h-35l-9 17v35l103 127l34 77l7 76l18 35l-8 42l8 9l-8 32l8 11l-25 100l-34 87l-69 85l-17 33l9 9z" />
<glyph glyph-name="asterisk" unicode="*"
d="M390 676l37 5l9 -12v-18l12 -21h9l-4 -4v-9h-8l-22 -33l-24 -6l-18 -37l-4 4l-17 -13v-5l9 -4h12l9 9l9 -9h28v-8l14 -9h4l3 5l36 -34h3v-8h-3v3l-6 5v-30l9 -12l-3 -14l7 -16l-29 -5l-14 -13l-25 9l-12 9h-9v9l-9 7h-3l-9 -7l-12 12h-6l-3 -21l9 -27l-6 -33l-21 -25v-13
l-28 -8l3 -6v-7h-9v4l-24 3l-9 22l-4 5l-5 21l-4 4l9 17l-5 30v8l9 8l-13 13l-21 -4v4l-5 5h-4l-3 -5v-4l7 -9h-13l-12 -12h-30l-16 4h-14l-7 -8l-36 29h-3l-13 21l-8 -7v16l8 21v21l46 9l18 9h37l9 9l5 -5l4 5v3l-9 9l-9 -9l-3 4h9l3 5l-9 9l-7 -5h-14l-16 17l4 4v5l-25 7
l9 9l-6 30h-3v13h3v21l-1 -2l-6 4l4 3l3 -5l6 18v12l12 -3h16l5 3v-9l22 -12h21l9 9l3 -6l-9 -7l6 -5l12 -21l4 5l21 -26l-4 -4l13 -5h12l5 5l16 9v16l13 21v12l5 -3l21 21l-5 4v5l18 9z" />
<glyph glyph-name="plus" unicode="+"
d="M316 402l8 -69l-8 -10l8 -66l44 -17l84 6l52 -6l7 -11v-34l-42 -32l-76 8l-42 -8h-17l-18 -27l8 -83l-8 -52l-41 -35l-28 35l-6 75l6 43v34l-17 18l-153 9l-9 8h-8v34l35 24h135l17 18l-6 121l17 31l24 18z" />
<glyph glyph-name="comma" unicode=","
d="M361 156l18 -17l-10 -128v-25l-42 -51v-17l-69 -60h-24l-10 -7l-35 42l10 34h8l27 35l24 24v25l-42 27l-27 76l35 52l44 24z" />
<glyph glyph-name="hyphen" unicode="-"
d="M381 326h18l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-59 -9l-10 9l-76 -9l-69 9l-8 -9l-86 17l-17 17v35l-7 7l24 45l86 17z" />
<glyph glyph-name="period" unicode="."
d="M307 173l69 -67l-7 -86l-62 -42l-34 8l-8 -8l-59 25l-27 68l17 52l52 43l42 7h17z" />
<glyph glyph-name="slash" unicode="/"
d="M538 671v-72l-48 -49l9 -7l-40 -48v-33l-88 -143l-97 -176l-33 -49v-23l-95 -178l-16 -31l-65 -32h-16l-7 39l7 49l39 56v23l75 113l55 111l23 25l40 80l26 32v25l39 47l9 32l56 81v23l32 26l7 30l56 58z" />
<glyph glyph-name="zero" unicode="0"
d="M351 577l77 -76l10 -34l24 -17l25 -129l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14l-103 17v18l-16 17l-9 -8v-9h-18l10 9v25l-27 17v18l-17 17l-7 -11v-6h-17l-11 68v25l-6 9l17 95v31l-11 10l18 59l27 77l42 62l103 66zM317 484l-25 34h-34
l-34 -7l-43 -27l-17 -17l8 -42l-17 -25h-16l16 -189l26 -25v-52h18l34 -41h25l17 24l17 -18h17l42 35v17l-7 10l17 17l8 85l25 27v58l-18 17v25l10 10l-10 8v25l-32 51h-27z" />
<glyph glyph-name="one" unicode="1"
d="M213 566l8 -10l35 18l41 -18l11 -66l-11 -121l11 -66l-11 -181l28 -58h24l10 7l18 -17h83l17 -25l-34 -34l-52 -8l-8 8l-24 -8h-17l-17 16l-11 -8l-66 8l-69 -16l-24 16l-28 -8h-34l-17 25l10 27l17 17l59 7l7 10l27 -10l35 34l-10 25l17 52l10 10l-27 25v17h10l25 69
l-8 41l17 17l-9 52v25l-8 7l8 11l-8 34l8 7l-18 17l-59 10l-59 -10l-27 10l-7 7v35l17 8z" />
<glyph glyph-name="two" unicode="2"
d="M190 446v12h11v-12h-11zM343 549l26 -17h32l17 -17l10 -42l35 -51l-10 -42v-10l10 -8l-69 -86l-51 -41l-68 -28l-17 -17l-10 10l-34 -27l-24 -25v-25l-25 -27l32 -17l69 10l25 -10l78 10l42 -10h25l27 -25l-10 -51l-59 -17l-42 10l-61 -10l-8 10l-25 -10h-10l-7 10
l-10 -10l-34 10l-7 -10l-42 10l-10 -10l-59 17l-10 45l10 66l42 59l137 103l50 17l44 25l49 93l-7 62l-42 42h-52l-42 25l-103 -50l-17 -79l-17 -17v-7h-25l-10 24l27 62l25 25h17l18 42l24 17l69 10z" />
<glyph glyph-name="three" unicode="3"
d="M433 550l8 -27l-8 -35l-68 -42l-77 -83l25 -18l62 -9l58 -35l25 -77l-8 -100l-34 -79h-10l-17 -32l-93 -69l-33 -9l-17 -16l-10 8l-51 -35l-67 -7l-26 17l-8 8v25l25 26l9 -9l49 27l11 7h-11v17h11v-7l7 -10h17l61 51h25l35 35h17l42 59l7 69l10 7l-10 35l-76 76l-25 10
l-86 -61h-41l-8 9l25 60l51 34l67 69l27 24v10l8 8l-178 7l-35 -42h-34l-10 42l10 35l42 27l128 7z" />
<glyph glyph-name="four" unicode="4"
d="M331 608l24 -25l-7 -214l17 -77l-10 -41v-35l17 -17l45 10l50 -10l9 -25l-9 -27l-68 -8h-27l-17 -41l10 -69l-10 -9l10 -8v-25l-51 -35l-35 35l10 8l-10 51l18 76l-35 25l-33 -8l-51 8l-45 -17l-66 27l-9 7l34 87l69 101l17 44l25 17l9 42l42 52v24l25 17v43l27 17h25z
M289 251l17 152l-17 87l-27 -35l-17 -59l-42 -104l-25 -24l7 -27l-24 -25l59 -7l69 7l8 25z" />
<glyph glyph-name="five" unicode="5"
d="M205 616l76 10l45 -10l76 10l25 -27v-18l-35 -31h-59l-17 17l-111 -27h-17l-35 -34l10 -35h-10l-14 -15l7 -27l34 -24l60 24l69 -18l7 11h17l27 -28h15l52 -66l10 -61l-18 -111l-34 -50l-59 -52l-121 -68h-34l-18 -17h-59l-7 10v34l18 15h34l14 17l79 17l66 35l52 59
l10 27l32 24l-17 94l-59 69l-67 -10l-9 -7h-25l-10 7l-83 -49h-28l-17 256v25l28 24l41 10z" />
<glyph glyph-name="six" unicode="6"
d="M442 599l17 -17v-24l-59 -28l-86 -6l-94 -60l-17 -44l-25 -42l-27 -69l10 -7l67 41h44l8 11l9 -11l49 11l62 -35l59 -77v-17l8 -9l-8 -8l8 -76l-8 -35l-52 -59l-42 -44l-85 -17l-77 26l-104 119l11 51l-11 18l11 51l-11 8l11 59l7 10l-7 7l24 79v50l52 69l103 68l128 17z
M314 302l-42 -8l-9 8l-35 -8l-33 -34l-9 7l-35 -83l10 -11l7 -58l43 -52l78 -17l59 9l52 50l7 62l-7 75l-27 35z" />
<glyph glyph-name="seven" unicode="7"
d="M394 651l8 -9l9 9l77 -27l8 -15v-34l-50 -62l-61 -125l-49 -111l-18 -197l8 -25l-17 -79l-35 -7l-32 17l-10 60l17 127l17 35l8 77l52 110l17 59l17 45l25 42l-32 34h-10l-7 -10l-70 10l-69 -10l-7 10l-34 -10l-17 -76l-25 -25l-27 17l-8 25l8 59l-8 10l8 49l44 18
l178 -8l76 17h9z" />
<glyph glyph-name="eight" unicode="8"
d="M326 625l24 -17l52 -52l27 -76l-18 -76l-34 -60v-17l59 -59l18 -86v-17l7 -8l-7 -10l7 -34l-15 -32l-52 -61l-68 -42l-94 -8l-52 8l-76 42l-8 27l-9 7l-17 27l-8 49l-17 35l25 86l75 101l-49 79l-9 49l9 51l32 52l79 52zM360 286l-59 17l-86 7l-76 -42l-35 -69l17 -42
l-17 -27l24 -25v-6l111 -70h27l84 42l52 103l-18 67zM360 531l-42 35h-44l-84 -10l-34 -32v-27l-11 -41l18 -43l94 -44l61 10l25 25l34 69z" />
<glyph glyph-name="nine" unicode="9"
d="M343 617h17l59 -59l17 -34l10 -59l17 -35l8 -61l-8 -60l8 -7l-8 -44l-17 -50l-18 -110l-27 -44l-100 -77h-25l-18 -17l-120 -8l-25 17v35l25 25l120 8l68 44l34 41l51 94l-10 27l10 8v24l-10 10l-24 -27l-86 -32l-59 -10l-42 17l-25 25l-34 10l-52 86l8 142l51 87l86 42
l42 9zM326 548l-35 -7l-59 7l-59 -24l-17 -59l-18 -18l35 -121l103 -34l59 17l52 52l7 52l-7 7l7 52l-7 7z" />
<glyph glyph-name="colon" unicode=":"
d="M331 265l49 -35l18 -41l-8 -62l-10 -7v-18l-66 -25l-52 16l-51 61l9 41l60 52l24 18h27zM355 494l43 -60l-8 -58l-42 -35l-34 -10l-59 17l-27 45v18l-8 6l35 69l59 25z" />
<glyph glyph-name="semicolon" unicode=";"
d="M375 147l17 -32l-8 -103l-27 -35v-25l-69 -85l-32 -8h-34l-10 17l10 49l34 35v17l7 10l-51 59l-7 25l7 51l44 42l49 17zM332 420l52 -59l-9 -52l-18 -24l-17 -17l-77 -10l-24 17l-27 27l10 76l41 42h69z" />
<glyph glyph-name="equal" unicode="="
d="M233 199l79 9l7 -9l52 9l7 8l34 -8l70 8l42 -35v-48l-35 -18l-67 -10l-145 17l-9 -7l-136 7h-25l-27 25h-7v27l42 34l41 -9l52 17zM516 354l8 -10v-35l-35 -24l-67 -10l-34 17l-51 -17l-52 17l-25 -7l-59 7l-10 10l-24 -17l-35 17h-25l-17 18l32 34l45 7l152 -17l86 17
l50 8z" />
<glyph glyph-name="question" unicode="?"
d="M343 61l18 -44l-10 -25l-76 -17l-77 17v34l67 52zM351 522l52 -27l52 -67l7 -62l-69 -76l-17 -17l-60 -34l-7 -18l7 -49l-7 -10v-18l-27 -6h-24l-17 17l-8 52l-10 7l18 25h17l24 44h17l17 17l45 25l15 34l-25 76l-42 35l-93 8l-52 -25l-10 -10v-15l44 -27l8 -7v-28
l-17 -17l-42 -7l-8 7l-34 -7l-17 17l24 94l35 42l86 34l32 10z" />
<glyph glyph-name="A" unicode="A"
d="M268 461l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18l17 -77l17 -24v-51l25 -25l27 -128l17 -28v-35l25 -23l59 -8l17 -17v-27l-17 -18h-32l-9 -7l-153 17l-17 17l6 18l28 25h17l17 23l-10 28v7l-24 28
l-146 -11l-9 -6l9 -52l35 -17l25 -43l-25 -27h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25zM292 537l17 -17v-17l-41 10v24h24z" />
<glyph glyph-name="B" unicode="B"
d="M89 39l17 -18v-17l-27 -7h-25l-9 7l17 35h27zM156 541l44 17l35 -7l75 7l84 -24l35 -17l51 -87v-52l-34 -49l-17 -17l17 -44l34 -33l8 -85l10 -11l-27 -83l-49 -42l-28 -17l-118 -10l-52 17h-101v17l33 18l17 42l-7 44l7 118l-17 239l-8 8h-17l-17 -18l-77 27v35l25 17
l44 7zM353 472l-8 -7l-110 17l-18 -25l-10 -154h59l27 17l8 -8h59l51 49v35l-34 69zM301 260l-60 -9l-24 17l-27 -50l27 -172h7l181 10v34l34 25l7 59l-41 52l-45 25z" />
<glyph glyph-name="C" unicode="C"
d="M329 569l42 -28l68 28l8 -28l-17 -93l-8 -8l34 -86l-9 -42l-25 8l-34 34v17l-7 11l7 6l-17 35v34l-77 50l-25 10l-44 -10l-24 -32h-17l-52 -79v-8l-17 -17l9 -7l-9 -10l9 -42v-9l-9 -8l17 -34l-8 -8l25 -69l10 -51l76 -69l59 9l35 35l17 8l25 27v17l27 24v42l32 10
l26 -18l8 -34l-17 -68l-66 -85l-18 -34l-86 -17l-84 24l-78 69v25l-33 45l-17 83l8 27l-8 77l17 93l50 84v17l69 45l68 17z" />
<glyph glyph-name="D" unicode="D"
d="M505 377l17 -68l-8 -28l-27 52v44h18zM292 232v12h13v-12h-13zM214 537h17l10 -7l34 17l77 -27l25 10l44 -17l41 -25l18 -69v-9l7 -8l-76 94l-69 17l-32 -10l-52 10l-27 -25l-7 -61l7 -8v-34l-7 -8l7 -180l-7 -51l7 -76v-17l121 -8l42 25l59 69l9 41l25 25v17l-7 10
l17 25l8 10h9l8 -10l-25 -129l-35 -58l-85 -69l-111 -8l-25 8l-93 -18l-52 10h-25l-17 17v18l17 17h101l18 14l9 87l8 10l-17 66l9 52l8 7l-8 11l8 6l-8 28l15 110v18l-7 7l-17 52l-112 -10l-7 10v24l67 17z" />
<glyph glyph-name="E" unicode="E"
d="M461 536h7l27 -118l-10 -7l10 -45l-10 -7l-35 52l-6 58l-52 60l-8 -8l-10 8l45 24zM205 546l9 -10l32 17l28 -7l58 7l8 -7v-17l-126 -17l-17 -18l8 -100l-8 -11l8 -41l24 -28l34 -7l17 17l35 35l11 35l6 6l35 -6l7 -70l-17 -51l10 -69l-27 -25h-8l-17 34l11 25l-18 27
l-17 17h-77l-17 -17l-10 -121l18 -24v-42l17 -17l76 -10l17 10l59 -10l52 52l35 101l7 9h17l17 -17l-17 -69l10 -17l-17 -101l-10 -10l-32 10h-35l-17 -17l-44 7l-49 -7l-45 7l-76 -7l-52 7l-7 -7l-24 7l-35 -7l-27 24l10 35l93 17l34 35l8 34l-8 18l8 66l-8 266l-17 17
l-41 -10l-62 10l-7 8l17 41z" />
<glyph glyph-name="F" unicode="F"
d="M360 417v15h12v-15h-12zM377 189v-28l-10 -7h-7l-8 41l8 52zM377 528l17 18l76 -18l25 -49l-7 -27v-24l17 -18v-34l-10 -8l18 -35l-8 -26v-8h-17l-18 34v8l8 10l-8 7l8 24l-25 46l10 6l-18 28l-51 32l-68 -8l-8 8l-35 -15l-27 7l-15 -17l8 -24v-11l-18 -17l10 -35
l-10 -66l25 -27l45 -7l25 34l26 25l8 41l24 -24l-17 -42v-34l-7 -10h-8l-9 10l-25 -18l-87 -24l-17 -111l17 -35v-44l43 -25l34 11h35l17 -17v-18l-17 -17l-42 8l-52 -8l-18 8l-69 -8l-6 8l-43 -8h-26v41l17 11l58 -11l35 43l-7 9l17 35l-10 66l10 11l-10 24v10l-7 7l7 17
l-17 215l-18 24h-17l-7 -7l-7 7l-27 -7l-35 7l-7 25v10l17 17l52 8l7 -8l136 17z" />
<glyph glyph-name="G" unicode="G"
d="M347 553l18 -17l58 17h10l17 -17l-17 -145l7 -9v-25l-7 -8h-27l-32 50l-9 44l-69 49l-94 -14l-41 -45l-18 -93l-17 -17l17 -84l18 -120l58 -69l52 -8l59 34l44 43l8 42v34l-86 17h-8l-10 10l18 34l26 15l52 -8l66 8l86 -15l8 -10v-34l-94 -17l-7 -128v-32l7 -10l-17 -24
l-7 -10h-17l-25 34l-52 -41l-103 7l-76 76l-7 44l-27 34l-8 84l8 10v24l-17 17l9 87v24l25 25l27 68l49 60l34 17z" />
<glyph glyph-name="H" unicode="H"
d="M241 536v-41l-59 -17l-10 9l-15 -17l8 -9l-8 -77l8 -52l69 -24h17l7 7v10l69 -17l52 41l-10 42l10 10l-17 52l7 8l-25 26h-17l-10 -9l-42 34h-7l-10 7v11l10 6h7l8 -6l17 17l145 7l35 -7h25l9 -28v-32l-41 -9h-28l-7 -8l7 -27l-24 -273l7 -8l-7 -86v-32l17 -10l52 10
l7 -10h8v-25l-15 -17l-180 8v9l-10 8l79 35l15 152l-15 52h-10l-7 -8l-18 18h-61l-8 -10l-76 10h-34l-17 -44v-25l9 -10l-17 -49l8 -69v-25l24 -17l45 7l7 -7v-27l-7 -8l-10 8l-25 -8l-34 8l-52 -8l-8 -9l-51 17l-8 9v18l18 7l58 -7l8 118l-8 42l8 27l-17 34l17 43l-17 69
l9 49l-9 9l9 43l-34 34l-51 7v28l17 17l41 7z" />
<glyph glyph-name="I" unicode="I"
d="M192 553h25l10 -10l31 17l28 -7l84 7l78 -7l8 -10v-31l-59 -35l-27 7h-16l-9 -7l-8 7h-27l-7 -7l7 -44l-7 -212l24 -139l18 -14h111l17 -27l7 -8l-14 -44l-35 -17l-9 10l-85 -10l-51 10l-34 -10l-69 10l-25 -10l-52 10l-35 17l-6 25l23 27l87 17h25l9 8l8 -8l17 205l-7 7
l7 76l-7 27l7 50v34l-17 10l-52 -10l-66 17l-28 28l11 23l76 25z" />
<glyph glyph-name="J" unicode="J"
d="M333 555l32 8l103 -18v-49l-17 -17h-42l-17 -17l-10 7h-7l-18 -52v-42l-9 -7l17 -35l-17 -103l-8 -128l8 -8l-77 -93l-66 -27l-79 18l-25 26l-34 7l-25 60l7 103l35 35h59l27 -35l-9 -52l-60 10l-7 -10l17 -76l25 -24h110l52 66l8 104l-18 69l10 58l-17 136l-86 17
l-25 25l8 27l17 17l59 8z" />
<glyph glyph-name="K" unicode="K"
d="M242 547l35 -35l32 35l204 7v-67h-7l-17 -17l-34 32l-153 -153l-25 -58v-18l49 -51l28 -60l41 -41v-18l52 -44l42 -7h24v-77l-236 17l-10 8l-34 -17l-94 9l-34 -9l-77 9l-17 17v18l44 25h25l17 24l8 86l-8 8l8 78l-8 49l8 181l-25 17h-59l-10 7v28l17 17h43l9 -11l111 28
zM343 76l-83 79v76l-10 -27h-17l-42 -34l-10 -49l10 -62h69l25 -15l58 15v17zM309 436v51l-76 8l-52 -25l-8 -9l-6 -136l14 -52v7l35 35v17l51 45z" />
<glyph glyph-name="L" unicode="L"
d="M166 555l27 8h31l18 -18l86 10l52 -10l8 -7v-18l-43 -17l-59 8h-34l-10 -32l10 -80l-10 -6l27 -136l-17 -86l7 -52l-7 -8v-26l24 -25l146 7l41 44l-7 43l7 24l-7 28v24l25 34h34l10 -110l-10 -69l10 -18v-31l-35 -44l-27 9l-58 -9l-8 9l-77 -9l-44 9l-58 -9l-25 9l-79 -9
l-77 9v52l129 7h17l17 290l10 8l-10 59v34l24 18v17l-24 25l-152 9l-11 -9v44l11 -10l58 18z" />
<glyph glyph-name="M" unicode="M"
d="M-8 524l26 17l42 -8l7 7l77 -17l10 10h17l41 -45v-66l-58 41l-17 -17l-10 -76l10 -51l-10 -18l27 -195l-10 -9v-17l44 -35h35l14 -25l10 -7l8 7l-25 187l-24 17l17 18l-17 94l6 9l-6 60h17l-11 -35l18 -17l17 -101l25 -52v-34l17 -17l10 17v24h25l9 -7v-27l-9 -7l9 -8
l-9 -35l9 -61h-9l-18 -59l-7 -7l-27 7v10l-17 17l-32 -27l-79 -7h-32l-18 17h-52l-9 24l9 11v14h35l25 27l-8 84l18 120l-10 25l10 52l7 10l-7 7l7 42l-17 27v34l-17 15l-77 10zM459 405l10 -35v-25l7 -9l-7 -25l7 -59l-7 -10l7 -76l-7 -52l17 -42l-10 -10l18 -17l51 -8
l10 -24v-10l-35 -17l-9 10l-52 -17l-32 17l-51 -17h-11l-24 24v28l76 24l28 350h14zM365 377l-24 -101h-8l15 94l10 7h7zM341 242v-18l-8 -6v24h8zM469 533l7 7h10l17 -17l-34 -42l-18 -42v-17l-24 76l-10 8l-24 -33v-34l-11 -10l-24 10l-10 7l34 77l17 17zM154 503v-15h12
v15h-12z" />
<glyph glyph-name="N" unicode="N"
d="M559 530l-22 -31l-17 -17l-42 7l-27 -25l10 -59l-17 -85l7 -25l-7 -60v-27h-11l-14 52l8 60l-18 137v7l-25 25l-9 -7l-52 17l-14 17l6 25l52 17l25 -7l121 7zM170 541l10 10l59 -10l24 -25l-17 -52l45 -42l-10 -27l42 -100l9 -35v-35l-41 43l-28 86l-24 41l-27 27l10 35
l-10 7h-7l-17 -69l9 -66l-9 -9l17 -257l58 -17h18l17 -25v-17l-17 -18l-59 11l-10 -11l-7 11l-8 -11l-61 11l-25 -11l-69 11l-7 7l24 42l59 27l10 52l-17 25l17 51l-10 7l18 27l9 60l-27 34v25h27l8 10l-25 66l8 27v7l-18 18l-76 17v8l-7 9l52 42zM384 285v17h8v-17h-8z
M315 174v-32l-17 32l11 10zM384 174h25v-24h-7l-10 -8v-10l27 -25l25 8l17 -17l7 -77l-17 -24l-7 -11l-35 11l-17 17v24l-25 25h-17l-10 10l10 25h7l18 17l-25 41l7 28zM451 167v-35h-18l-6 35h24z" />
<glyph glyph-name="O" unicode="O"
d="M37 58v13h11v-13h-11zM317 -43v12h12v-12h-12zM351 577l77 -76l10 -34l24 -17l25 -129l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14l-103 17v18l-16 17l-9 -8v-9h-18l10 9v25l-27 17v18l-17 17l-7 -11v-6h-17l-11 68v25l-6 9l17 95v31l-11 10
l18 59l27 77l42 62l103 66zM317 484l-25 34h-34l-34 -7l-43 -27l-17 -17l8 -42l-17 -25h-16l16 -189l26 -25v-52h18l34 -41h25l17 24l17 -18h17l42 35v17l-7 10l17 17l8 85l25 27v58l-18 17v25l10 10l-10 8v25l-32 51h-27zM317 314v12h12v-12h-12zM317 195v13h12v-13h-12z
" />
<glyph glyph-name="P" unicode="P"
d="M269 552l6 10l129 -27l10 -8h7l18 17l34 -34l-7 -8l14 -17v-27l27 -25l7 8l10 -42l-10 -101h10v-35h-10v60l-7 7l-27 -25l-66 -59l-69 -17h-8l-10 7l-83 -17l-18 -58l18 -85l14 -17l62 -10l7 10h18l9 -17l-9 -51l-8 -11l-44 11l-111 -11l-66 11l-10 -11l-7 11l-35 -11
l-34 17l-18 18v27l42 24l62 10l7 -10h17l25 70l-8 76l18 212l-18 78h-24l-10 -9l-69 17l-17 17l17 35zM327 485l-24 -10l-34 18l-25 -8l-10 -52l10 -103l-10 -7l10 -10v-8l59 -7l51 7l8 -7l17 15l25 10l35 52l-8 41l-27 42h-25z" />
<glyph glyph-name="Q" unicode="Q"
d="M273 587l10 -7l7 7l35 -7v-17l41 -17l62 -60l24 -34l8 -70l17 -24v-76l10 -10l-35 -135l-17 -60l-34 -27l-24 -25l6 -24v-9l11 -8l34 8l32 33h17l27 -33l-9 -42l-52 -52l-25 7h-24l-63 45v17l-41 25h-69l-25 24l-34 9l-76 112l-25 179l7 60l18 52l9 41l49 69l63 17l41 32
h25zM290 60l10 25l-17 17l-35 -34l8 -8h34zM265 521l-9 7h-18l-31 -25l-52 -75l-11 -46l-24 -41l7 -59l-7 -10l7 -94l28 -41l7 7h10l7 -7l52 24l17 17l42 -7l69 -52l18 18l51 110l-10 42l10 27l-17 84l-28 17l-52 94h-24l-34 17z" />
<glyph glyph-name="R" unicode="R"
d="M150 546l6 -10l42 10l77 -10l10 10l17 -17l35 7l93 -24l52 -60l7 -93l-17 -45l-86 -66l34 -69l10 -59l-10 -44v-15l27 -27h17l8 -8l24 18l35 -10v-8l10 -9l-28 -18l-6 -7l-43 7l-9 -7l-18 17h-7l-10 -10l-25 45l-9 59l9 24l-17 69l-59 52l-111 17l-17 -17l-10 -110
l17 -28v-59l18 -17l59 -8h17v-9l10 -8l-17 -17l-42 7l-10 -7l-35 7l-118 -17l-26 17v27l96 25l14 136l-14 26l6 35l-17 101l17 34l-24 60l7 9l-17 18l-7 -10l-69 17l-8 27v15l16 17l85 10h11zM361 477l-42 -7l-10 7l-49 -7l-44 -27v-129h34l25 -24h17l17 17l86 24l25 46v24
l-7 7l7 27z" />
<glyph glyph-name="S" unicode="S"
d="M356 517v-8l27 -24l15 15v34l10 10h17l8 -10v-25l-17 -17l17 -103l-8 -7l-34 34l-8 25l-44 44l-66 32h-51l-18 -32h-26l-25 24h-8l-9 -9l9 -42l-17 -25l17 -68l33 -25h26v-9l8 -8l44 8l110 -25l76 -77l17 -109l-34 -77l-34 -44l-94 -17h-17l-7 -7l-27 17h-34l-34 24h-25
l-25 -24l-44 7l-8 27l18 84l-10 76v9l10 8l34 -8l17 -17l8 -51l59 -59l61 -17l66 17l27 34l25 25l17 85l-10 25l-76 35l-93 7l-17 35l-17 -18v-7h-17v17l-42 25l-42 51l-10 59l17 44l35 24v18l42 7l51 27z" />
<glyph glyph-name="T" unicode="T"
d="M139 535l62 10l59 -10l8 10l52 -10l51 10l51 -17l25 -27l10 -60l-10 -76l10 -7v-27l-10 -8l10 -9v-25l-17 -18h-18l-26 111l9 25l-27 52l-66 27h-10l-24 -27l-10 -212l-8 -62v-24l-9 -8l9 -9v-17l-9 -8l17 -52v-10l44 -25l49 10l52 -17l9 -10v-17l-26 -17l-229 10
l-69 -10l-17 27l9 24l8 10l59 -10l10 10l51 -10l17 77l16 44l-16 15l-9 155l17 25l-8 93v27l-34 25h-44l-32 -25v-34l-17 -18l-10 -68l10 -17v-35l-27 -25h-18l-17 18l10 69l-10 7l17 76l-7 10v17l7 8l-7 10l7 24l35 35z" />
<glyph glyph-name="U" unicode="U"
d="M237 484v14h13v-14h-13zM348 365v13h12v-13h-12zM348 228v-16l-7 -9h-10l-7 25h24zM434 -129v-18l-9 18h9zM348 577l10 10l76 -18l25 18l44 -10l15 -17v-32l-59 -10l-8 10l-26 -17l9 -111l-9 -76l9 -79l-9 -42l9 -69l-52 -93l-93 -49v6l-7 8l-35 -8l-76 25h-17l-61 111
l-8 34l8 10l-8 25l17 69l-9 17l9 49l-9 17l9 52l-17 27l8 42l-18 52h-7l-8 -10l-17 17h-27l-8 25l8 9l34 18l43 -10l78 10l49 -10l10 -17v-32l-69 -10l-17 -17l10 -42l-17 -34l17 -35l-17 -25v-10l7 -7l-7 -24l7 -52l-7 -17l7 -69l27 -25v-34l66 -35h70l24 24v28l34 17
l-7 111l18 52l-18 126l18 17l-18 44l-86 24v25l59 34z" />
<glyph glyph-name="V" unicode="V"
d="M133 576l62 8l7 -8l10 8l25 -8l8 -27v-34l-33 -8h-27l-7 -10l7 -24l-7 -25l17 -26v-43l32 -25l-7 -26l7 -42l-7 -10l25 -84l17 -17l34 42v76l18 17l17 62l17 67l7 9l-7 49l-52 27l-7 8v27l17 17l93 -9l50 9l61 -9l18 -18v-42l-35 -17l-9 7h-25l-43 -31l-61 -222l-24 -79
l-8 -7l8 -52l-17 -24l-8 -52l-44 -42h-17l-33 42v41l-10 11l10 7l-34 155l-17 49l-17 104l-18 51l-17 59l-8 8l-61 -8h-8l-17 17v43l25 17z" />
<glyph glyph-name="W" unicode="W"
d="M344 528l10 -8v-17l-45 -18l-31 -51l-10 -7h-8l-9 7l17 94l17 9zM278 180v13h12v-13h-12zM98 537l7 8l62 -17l7 -8v-7l-52 -17l17 -94l11 -10v-17l-11 -8l28 -44l7 -59l8 -7h9l17 66v17l35 45v17h8l17 -62h17l35 35l-11 35l-6 6h6l11 11l7 -11l-7 -6l51 -112l-10 -6
l10 -28l7 -7l18 41l17 17l-10 8v27l18 17v94h9l17 18l-26 34v15l44 -8l34 17h15v-41h-24l-18 -52l10 -59l-27 -79l-17 -94l9 -7l-17 -52l-27 -145l-15 -17v-8l-51 8l-28 44l11 101l-17 35v9l6 8l-17 24l11 52l-18 34l-7 8l-18 -25l-17 -162l8 -25l-8 -76l-44 -27l-32 10
l-28 34l11 7l-11 42l11 35l-17 17l-11 110l-17 18l10 94l-17 76l-17 62h-35l-17 17v24l17 8zM292 498v-13h15v13h-15z" />
<glyph glyph-name="X" unicode="X"
d="M167 558h24l10 -7l7 7l27 -7h25l9 -10l-17 -34l-44 10l-7 -10l24 -77v-24l-7 -10l67 -42l27 28l7 48l27 27v25l-52 42v17l69 17l144 -7l9 -10v-24l-77 -17h-17l-59 -94l-51 -94l7 -59l27 -52l49 -41l10 -69l25 -18l17 -17h69l25 -25v-27l-25 -17l-162 10l-8 -10l-34 10
l-18 17v34l43 16l9 9l-9 52l-25 25l-10 59l-17 27l-8 7l-17 -17v-25h-8l-27 -27v-41l-34 -60l52 -25h17l9 -26l-9 -8v-17l-111 -10l-25 10l-61 -10l-42 17l-17 18v34l69 8h25l34 34l10 35l25 10l17 41l24 25l17 44l27 25v25l8 9l-42 42l7 10l-58 93l-52 50l-25 -7l-52 7
l-7 27l24 24l18 11z" />
<glyph glyph-name="Y" unicode="Y"
d="M219 552l18 -35l-35 -52l-9 -7l26 -79l25 -25l35 -51l17 62l35 41l24 69l10 8l-17 24l-34 10l-11 7v28l62 6l7 -6l25 6l10 -6l25 6l9 -6l25 6l35 -6l6 -11v-24l-41 -10l-69 -67l-8 -44v-17l-34 -14l-24 -87l-35 -77l18 -120l34 -17h59l25 -25v-34l-118 -18l-60 -7
l-138 25l-7 9v25l35 25l24 -8l42 8h17l17 69l10 24l-10 17l18 79l-69 84l-8 42l-52 79l-24 25l-69 17l-17 17v24h9l8 11l10 -11l111 17z" />
<glyph glyph-name="Z" unicode="Z"
d="M415 552l34 -25v-42l-34 -34l7 -10l-59 -83l-35 -18l-25 -69h-9l-25 -25v-27l-34 -7l-17 -42l-60 -51l-25 -70v-7l8 -10l25 17l44 -7l42 7l7 10l27 -17l69 7l50 52l-8 8l8 69v24l27 17l24 -24l-17 -86l-7 -17v-33l7 -10l-7 -17v-24l-27 -10h-25l-10 10l-24 -17l-35 7
l-42 -18l-42 18l-86 10l-42 -17h-10l-17 17v24l52 60l25 34l27 59l51 86l49 87l27 24l-9 10v15l17 9l27 25v27l-17 17l-76 -9l-18 17h-26l-35 -42l-33 -103h-26l-8 68l17 111l42 25h35l8 -7l44 17z" />
<glyph glyph-name="backslash" unicode="\"
d="M502 -121l9 -34l-14 -4l-25 16l-25 57l-30 16l-9 18l4 3l-12 18l5 3l-14 13h-4l-3 17v9l-18 25h-9l-12 27l-4 24l-5 18l-25 12l-5 13h-4l-9 39l-16 21l-51 106l-34 21l-14 50l-21 27l-12 42l-25 30l-12 28l-36 48v21l-12 16l-37 57l-6 -5v12l6 14l-9 7l3 22l18 5l16 -12
l21 -6l14 -12l22 -46l33 -52l30 -93l13 -4l16 -30l-4 -21l18 -27l49 -71l18 -51l-4 -5l46 -46l22 -52l24 -37l-3 -5l39 -73l21 -12l7 -30l-3 -21l12 -13h5l13 -30l-9 -12l9 -13l3 4l18 -4v-12z" />
<glyph glyph-name="asciicircum" unicode="^"
d="M266 795l4 6l42 -13l34 -35l21 -43l21 -15v-13l48 -35l-5 -50v-5l-29 -12l-21 3l-48 39l-16 25l-42 30l-27 5l-16 25l-42 -12l-5 -6l-22 14l-3 -47l-18 -38l-30 -30l-25 9l-12 37l58 85l15 6l21 24l67 55z" />
<glyph glyph-name="underscore" unicode="_"
d="M184 -14l110 -9l8 9h27l8 -9l41 17l52 -8l9 8h8l10 -8l25 18l86 -18l17 -27l-10 -66l-24 -17l-112 -10l-9 10l-25 -10l-59 10l-44 -10l-50 10l-154 -17l-84 17l-34 32l6 61l60 25l44 -8l42 18z" />
<glyph glyph-name="quoteleft" unicode="&#x2018;"
d="M205 534l14 -13l75 -27l35 19l28 41l-22 61l-33 22v20l19 19l21 28h7l8 27l-28 34l-8 -6h-19l-56 -48v-13l-33 -41v-20z" />
<glyph glyph-name="a" unicode="a"
d="M279 404l7 10l69 -18l25 -9l35 -43l7 -110l-7 -59l7 -18v-34l-7 -10l24 -25l62 8l7 -76l-35 -42l-58 -8l-52 33l-8 9l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76l27 25l85 27zM321 148l7 27h-17l-66 -18l-10 8l-8 -8h-51
l-43 -27l8 -7l-8 -35l18 -24h94l58 18l35 41zM346 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17z" />
<glyph glyph-name="b" unicode="b"
d="M160 541l18 -34l-11 -43l11 -26l-18 -84l7 -11h17l35 35l52 17l52 -7l7 7h35l24 -52l44 -58l8 -69l9 -8l-17 -86l-27 -66l-76 -79l-59 -8l-52 8l-24 27h-28l-34 -27h-7l-18 17l10 104l-17 83l7 180l-17 94l-59 17l-35 35l11 24l24 17zM296 309h-35l-25 10l-58 -94
l-11 -75l11 -18l-11 -10l69 -66l52 -18l8 8l27 17l17 42h14l11 62l-18 49l7 9l-7 43l-42 51z" />
<glyph glyph-name="c" unicode="c"
d="M342 404l76 -35l17 -25l25 -26l-7 -84l-25 -17h-68l-18 41l35 35l-28 34l-41 25l-52 -8l-42 -17l-59 -69l17 -128l18 -25l6 -9h18l34 -32l35 -17l49 17l86 76h52l8 -27l-18 -49l-49 -52l-79 -34l-59 -8l-25 8h-27l-77 52l-58 75l-8 77l18 93l59 87l41 34l60 8l9 10z" />
<glyph glyph-name="d" unicode="d"
d="M329 558l8 7l41 -7l18 -25l-8 -35l8 -34l-8 -76l8 -28l-8 -41l8 -8l-8 -34l8 -10l-8 -25l17 -69l-9 -7l17 -76l42 -27l44 -8l8 -10v-25l-17 -17h-25l-18 -17l-42 8h-9l-8 -8l-59 34l-44 -17l-17 -24l-35 7l-7 -7l-59 15l-34 26l-25 25v27l-27 25l-17 87l17 93l27 66
l66 52l59 10l45 -10l42 -24h17l24 34l-17 24l-7 60l-17 17l-8 -8l-79 18l-7 7l7 35l18 7zM295 311l-27 25l-50 7l-44 -24l-41 -60v-34l6 -17l-6 -7v-28l17 -34l34 -42l34 -34l50 17l27 34h17v18l25 52l-17 75l9 25l-17 27h-17z" />
<glyph glyph-name="e" unicode="e"
d="M335 386l70 -76l26 -52v-34l-9 -7l9 -8l-9 -27l-42 10l-45 -18l-24 18l-42 -10l-17 -17v9l-52 8l-69 -8l-8 -9l18 -60l51 -41l67 -27l34 10l10 -10l84 76h27l8 -25l-17 -34l-60 -51l-93 -25l-28 17h-49l-61 59v17l-32 34l-17 94l-11 10l28 77l24 49l42 26l27 25l25 10z
M210 241v17h-44l17 -34zM286 335h-27l-49 9l-10 -9l10 -8l-10 -7v-28l10 -6h7l35 24l24 -24v-11l-24 -41l59 7v69z" />
<glyph glyph-name="f" unicode="f"
d="M450 541l49 -52l-18 -42l-7 -10l-41 17l24 52l-24 35h-18l-10 -8l-7 25zM338 558l8 -11l-77 -49l-24 -69l49 -52l62 18h25l17 -18l-27 -17l-25 11l-25 -11l-27 11h-17l-17 -18l9 -42l-9 -27l17 -118v-86l94 -17l10 9h24l28 -27v-25l-18 -17l-94 -9h-27l-7 9l-10 -9h-25
l-7 9l-17 -17h-10l-17 17l-59 -17l-69 17l-8 25l17 27l60 17l25 -9h34l17 17l10 69l-10 7l17 121l-7 42v24l-44 28l-42 -18l-60 18v6l-9 11l86 17h25l17 -17h17l17 17v32l-7 10l17 42l32 52l44 17h17zM252 360v17l-7 11l-10 -28h17z" />
<glyph glyph-name="g" unicode="g"
d="M305 395l52 -41l32 41l44 10l50 -62l-8 -24l-25 -25h-17l-27 49h-7l-10 11l-24 -35l17 -34v-60l-42 -58h-17l-27 -28l-60 -7l-69 24l-32 -7l-34 -34l25 -27l34 -15l59 24l17 -9l77 9l17 -17h76l44 -25l17 -34l16 -69l9 -7l-42 -79l-79 -25l-14 -17h-17l-10 10l-42 -27
l-27 17l-42 -8l-153 50l-27 35l-7 61l25 93l-8 60v34l35 35l7 -11l27 11l8 7l-18 34v84l94 96zM246 21l-34 -18l-17 18l-77 -35v-24h8l9 -10v-24l101 -35l69 17l8 -9l52 9l34 35v32l-10 9l-49 25l-44 -8l-18 18h-32zM278 354h-25l-41 6l-34 -24l-11 -51l17 -43l-6 -44l6 -7
l69 -7l43 7l44 34v35l-10 59z" />
<glyph glyph-name="h" unicode="h"
d="M133 549l9 8l25 -18v-49l-7 -10l7 -17l-7 -59l7 -8l-7 -44v-17l17 -17l83 69l52 27l59 -10l59 -60l18 -58l-8 -28l8 -51l-8 -119l8 -24l86 -17v-52l-17 -17h-35l-8 8l-9 -8l-66 8l-79 -8l-33 42l33 34h51l10 10l-10 24l17 146l-17 35l10 34l-34 32l-44 9l-33 -9l-52 -60
l-41 -51l-17 -24v-70l7 -7l-7 -10l7 -42l24 -24h62v-10l17 -17l-17 -25l-35 -17l-58 -8l-27 16l-43 -8l-59 8l-10 9l10 52h67l17 83l-7 180l7 8l-7 96l7 66l-76 17h-8l-10 10v32l18 18z" />
<glyph glyph-name="i" unicode="i"
d="M257 403l7 10h25l27 -35l8 -162l-18 -34l10 -66v-28l17 -17l94 10h35l17 -17l7 -44l-17 -25l-35 -8l-59 8l-7 -8h-18l-9 8l-8 -8l-27 8l-83 -18l-45 10l-7 8l-8 -8l-44 8h-25l-27 34l10 35l111 17l7 -10h35l17 17l-7 28l-10 6h10l17 87l-10 66l10 69l-17 17l-28 -24
l-34 15l-7 9l-17 -9h-35l-8 69zM316 548l8 -41l-18 -28l-31 -17l-63 17l-6 45l17 24l41 25z" />
<glyph glyph-name="j" unicode="j"
d="M286 430l42 8h26l25 -35l10 -128l-10 -7v-17l10 -8l-17 -79l7 -127l-7 -50l-27 -76l-17 -35l-67 -34l-94 7l-9 -7l-59 69l-10 83v35l61 42l50 -8l17 -51l-7 -60h-35l-8 -10l18 -17l59 10h17l18 25l24 27v7l-7 10l-35 -17h-9v25l17 17h17l10 -8l7 8l-7 59l7 34l-7 77l7 27
l-7 59l7 69l-17 15l-101 -8l-52 25h-10v27l44 34zM328 608l61 -60v-24l-35 -34l-51 -11l-34 17l-17 28v34l51 50h25z" />
<glyph glyph-name="k" unicode="k"
d="M480 396h10v-17l-10 -10h-7l-17 17l17 18zM157 566l17 -25l-9 -61v-24l-8 -11l8 -7l-8 -59l17 -35l-17 -24l17 -62l35 -24l42 24l76 86v17l-7 8l-45 -8v35l52 8l27 -25l25 25h52l7 -8l-42 -44h-42l-34 -32v-28l-34 -41l6 -27l52 -32l10 -27v-25l32 -35l45 -41l25 7l41 -7
l10 -17v-35h-86l-8 -9l-27 9l-49 -9l-10 -8l-35 17l-17 17v25h28l17 17v34l-34 52l-18 60h-17l-34 -35l-35 -25l-8 -35v-34l25 -24h52l7 -10l-7 -51h-8l-9 -8l-25 17l-10 -9h-7l-10 9l-25 -17l-58 8l-18 -8h-34l-18 17v35l18 17l58 7l18 69l-7 77l7 7l-7 44l17 52l-10 142
l10 11l-10 34l-76 24h-10l-8 10v15l104 18z" />
<glyph glyph-name="l" unicode="l"
d="M213 566l8 -10l35 18l41 -18l11 -66l-11 -121l11 -66l-11 -181l28 -58h24l10 7l18 -17h83l17 -25l-34 -34l-52 -8l-8 8l-24 -8h-17l-17 16l-11 -8l-66 8l-69 -16l-24 16l-28 -8h-34l-17 25l10 27l17 17l59 7l7 10l27 -10l35 34l-10 25l17 52l10 10l-27 25v17h10l25 69
l-8 41l17 17l-9 52v25l-8 7l8 11l-8 34l8 7l-18 17l-59 10l-59 -10l-27 10l-7 7v35l17 8z" />
<glyph glyph-name="m" unicode="m"
d="M-3 3l14 44l79 7l17 17l8 146l7 7l-15 27l8 59v17l-17 17h-35l-34 25v18l34 27l76 -27l17 -18h28l24 35l42 10l35 -27l27 -25l42 7l24 27h44l8 8l52 -25l17 -69l-17 -118l17 -128v-10l25 -7h27v-44l-190 -8l-7 8l-86 -8h-35l-15 25l50 34l17 17l-8 52l8 7l-8 62l8 8
l-8 69l25 24l-25 34l-59 8l-44 -42l-18 -52l-6 -7l17 -152v-18l24 -17l10 -10v-34l-79 -17zM447 293l-42 42l-27 9l-34 -51l-24 -76l17 -25l-17 -79l17 -49l-8 -10h32l27 -7l42 17l-8 49l18 27l-10 67l17 17v69z" />
<glyph glyph-name="n" unicode="n"
d="M524 21l-28 -26l-52 8l-69 -8h-34l-18 17v25l87 17h9l8 10l-8 49v35l8 9l-17 153l-60 34l-52 -26l-69 -94l-24 -59l7 -60l-7 -23v-11l24 -24l69 7h-6l-18 -42v-9l-17 -17l-28 9l-24 -9l-7 9l-35 -9l-44 9l-60 -9l-17 17h-7l7 27l28 24l49 10l17 18l10 75l-10 129v24
l-66 42l-17 27v25l24 17l59 -17l35 24l34 -32h7l28 -27l101 52l69 -7l58 -62l11 -101l-11 -197z" />
<glyph glyph-name="o" unicode="o"
d="M334 413l60 -44l51 -66l24 -52l10 -77l-17 -27l-17 -76l-69 -76h-25l-34 -25l-111 17l-34 25l-42 35l-52 66l17 162l35 60l69 61l83 17h52zM369 292l-27 28l-67 32l-27 -17l-49 -8l-27 -17l-25 -69l17 -49l-9 -10l9 -42l49 -76l79 -10l25 17l8 -7h9l8 7l34 59l18 87z" />
<glyph glyph-name="p" unicode="p"
d="M148 412h17l34 -35l59 11l42 34h35l17 -17l52 -11l66 -24h17v-10l-7 -7l-35 17l-7 7l-10 -7l10 -27l32 -42l10 -100l-18 -104l-41 -60l-52 -34v-17l-77 10l-17 -17v-10l-34 42h-7l-10 -8l-7 8h-10l-8 -49v-35l35 -17l58 -10l8 -8v-34l-25 -17h-27l-7 9l-7 -9h-27l-8 9
l-61 -17l-33 8l-9 -8l-59 17l-17 18v42l24 7h17l10 -7l52 17l15 187l-15 17l15 42l10 180l-10 7l-60 -7l-24 7l-10 10v35l27 17zM360 336h-16l-44 7l-42 -32l-51 -79v-42l27 -41l-10 -10v-17l59 -60l52 -7l69 67v17l-10 10l10 7h7v17l-17 34l10 25l-17 79z" />
<glyph glyph-name="q" unicode="q"
d="M292 403l17 -17h25l34 27h17l18 -27l7 -66l-7 -11l17 -367l25 -17l44 10l32 -34v-17l-32 -35l-35 10l-59 -17l-35 7l-61 -7l-41 17h-8l-10 8v34l45 24h31l10 -7l17 17l8 59l-17 27l-8 8l-68 -35l-94 10l-87 111l-14 143l32 79l61 58l33 18zM258 337v17h-43l-9 -17
l-42 -28l-8 -58l-17 -43l7 -44l27 -42l33 -34l52 -7l41 52l35 31v27l9 25l-9 27l-8 -10l-17 18v17l7 7l-24 34z" />
<glyph glyph-name="r" unicode="r"
d="M161 405l11 7l41 -7l35 -35l-7 -51l14 -25l35 42l27 41h25l17 17l52 11l17 7h34l42 -52l-10 -66l-25 -18h-7l-27 25l-7 42l-17 17h-28l-66 -49l-10 -27l-42 -42l-17 -86l17 -17v-50l25 -27l9 10l84 -17l10 -10v-25l-27 -24l-59 -10l-25 10l-41 -10l-63 10l-24 -17l-86 17
l-17 32v17l27 17l66 10l10 -10h18l24 52l10 52l-10 7l10 28v6l-10 11l10 7l-10 51l10 18v34l-28 25l-83 -10l-27 34l17 35z" />
<glyph glyph-name="s" unicode="s"
d="M305 404l59 -35l35 27l7 8l25 -25l9 -27l-17 -34l8 -43v-6h-15l-45 58l-51 42l-101 10l-35 -27l-24 -25l-10 -34l17 -35l69 -17l69 -7l83 -17l35 -17l42 -60l-7 -44v-25l-76 -68l-94 -25l-87 25l-76 -8h-17l-17 17l8 145l9 8h17l35 -42l35 -18h6v-9l-6 -8l24 -17l34 11
l8 -11v-7l-8 -10l8 -7l34 17l10 -10h25l7 10l34 18v23l28 25l-17 27l-28 25l-49 18l-62 -8l-48 15l-70 34l-17 34l8 77l34 35l45 17h110z" />
<glyph glyph-name="t" unicode="t"
d="M418 224v15h12v-15h-12zM256 557l24 -43l-15 -17l8 -69l-8 -7v-7l32 -18l80 8l49 -8l9 -27l-17 -25h-41l-11 8l-86 -17v-17l-24 -25l17 -93l7 -8l-24 -27l41 -25l-17 -17v-27l28 -14h6l18 41h10l7 -10l69 69h17l17 -34l-9 -25v-27l-35 -42v-17h-7l-18 -34l-41 -17v-8
l-10 -8l-59 8h-25l-44 52l-17 41l9 25l-9 44l17 195l-49 9l-69 -9l-27 17v35l34 27l94 -10l17 153h52z" />
<glyph glyph-name="u" unicode="u"
d="M154 394l7 -24l-17 -103l10 -8l-10 -52l17 -110v-8l52 -17l52 8l52 59v27l31 41l18 112l10 9v15l-28 17h-76l-7 28l52 24l49 -18l27 11l24 -11l11 -24l-11 -76l11 -62l-11 -7l18 -128l7 -8l10 8l77 -42l9 -10l-9 -42l-69 -17l-77 17h-24l-42 25l-111 -42l-69 25l-17 17
l-10 -8l-15 25l8 128l-8 52l8 69l-7 40l-12 16l-32 -6h-17l-12 16l-9 2l-10 10l-8 16l26 20l56 4z" />
<glyph glyph-name="v" unicode="v"
d="M263 414l10 -10v-25l-34 -35h-18l-9 -9l9 -42l35 -52h17l15 -34l69 79l-8 24l17 17l-27 25l-25 10l-9 7v27l27 25l111 -7l52 7l32 -17l17 -17v-18l-49 -25l-28 8l-34 -34l-42 -143l-34 -70v-41h-8l-17 -52l-35 -34h-34l-24 34l-35 111l-27 69l-15 66l-17 17l-10 52
l-17 25h-66l-18 17v35l18 17l31 7zM288 177v-12h15v12h-15z" />
<glyph glyph-name="w" unicode="w"
d="M89 386l10 10h24l17 -17l-17 -44l17 -25l-17 -35l17 -24v-34l-9 -8l17 -35l35 -17h9l18 17l-10 67l10 10h31l11 -10l-28 -170l11 -7l-25 -77l-27 -17h-18l-42 67l8 34l-25 76v35l8 10l-15 25l-10 58l-18 17l-6 43l-35 26l-10 8l10 27zM328 386h7l10 -7v-27h-17l-18 -17
v-32h-17v7l-7 10h-28l-6 -10l6 -7v-11l-17 60l-24 9v18l7 7l28 -7l51 17zM507 404l8 -8v-27l-25 -25l-27 -110l-24 -42l-8 -79l-9 -8l9 -58l-17 -52l-17 -17l-27 9l-35 33l-7 93l-18 34l10 45l-27 59v7l17 17l18 -17v-24l25 -25l9 8h8l-17 -25l17 -45l9 -7h18l17 17v25
l17 17l-9 93l9 28l8 7l-25 25h-10v27l76 25h27z" />
<glyph glyph-name="x" unicode="x"
d="M389 387l111 9l10 -9h7v-35l-52 -8l-7 8h-18l-26 -25h-15l-28 -58l-41 -45l-10 -32l51 -44l28 -25l7 -35l25 -17l17 -17l76 -7l11 -10v-34l-18 -17l-77 9l-41 -9h-34l-11 9l-7 -9h-27l-7 17v34l24 17l10 10l-27 41l-7 35l-17 17h-8l-44 -75v-28l9 -7h18v-44l-18 -17l-9 9
l-43 -9l-24 9l-86 -9h-34l-25 34l7 10v17l77 17l44 24l32 52h17l52 60l-8 24l-69 79v15l-24 26h-86l-25 25l8 10l24 17l128 -9l10 9l52 -9v-25l-45 -10l-7 -8l34 -26l18 -60l7 -7h18l34 52l41 24l11 8v17l-11 10l-24 -18h-17l-10 18v34l17 8z" />
<glyph glyph-name="y" unicode="y"
d="M257 396v-17h-8l-10 8l10 9h8zM496 387l9 -8v-10l-9 -7h-8l-17 17l17 8h8zM427 387h26l-9 -43l-25 -26l-10 -67l-42 -111l-17 -17l-35 -180l-41 -59v-17l-52 -34l-77 -8l-41 18l-34 31l-8 52l8 52l23 25l28 9l24 -9l28 -42v-18l-28 -25l35 -34l42 25l35 52l27 42v61
l-27 18l-35 100l10 10l-27 25v52l9 6l-41 43l-10 34l-24 27h-11l-7 -10l-10 10l-24 -10l-11 10v17l69 -9l28 9h32l9 -127l25 -62l27 -25l8 -59l24 -35l34 35l18 84l44 96l8 59l-70 17l11 25z" />
<glyph glyph-name="z" unicode="z"
d="M175 404l10 9l204 -9h17l18 -18v-25l-59 -51v-18l-34 -17l-11 -41l-34 -35l-94 -94l10 -24l17 -17l8 7l35 -7l41 17l28 -10l6 -7l43 58v35l26 35h18l25 -35l-18 -86l10 -7l-17 -61l-69 -16l-24 -9l-45 17h-24l-25 -35l-18 27h-52l-24 -27l-17 27l-69 8l-10 17l10 52
l25 24l103 86l7 25l111 104l17 7l35 34l-180 8l-25 -17l-17 -84v-17l-24 -25h-10l-17 15l10 103l7 8v9l-7 8l7 9v25l44 27z" />
<glyph glyph-name="bar" unicode="|"
d="M242 662l11 -26h4l-6 -21v-2l4 -3l-6 -17v-14l4 -9l-2 -15v-11l9 -18v-7l7 -9l-10 -24v-2l3 -3l-3 -10l-4 -4l5 -14l-1 -2l7 -16l-2 -7l-2 -22h2l2 -55l-4 -25l-2 -12v-6l9 -13l-3 -7l3 -8l-1 -16l4 -4l-4 -15l1 -15h-1l-4 -35l12 -23l-7 -13l4 -4l-1 -7l5 -9l-1 -6v-2
l3 -4l2 -5l-4 -14l2 -5l4 -17l2 -6l-4 -9h-2v-24l-2 -10l4 -13l2 -9v-17l-6 -11l-1 -10l-7 -1l-1 -8l-2 -1v-4h-7l-2 -2l-12 -2l-11 13l-2 15l-5 17v19l-2 1l4 6l-4 13l6 9l-6 12v16v10l4 9l-8 16l8 20l-4 13v19l-4 1l6 17v15l2 11l-8 13l4 4l-5 13v19h1l2 15h2v13l-2 2v26
v13l2 19l-4 3l6 27l-11 29l-3 -1v9l16 19l-2 11l-10 6v7h3v13l-7 8l7 12v10l1 32l-1 15l-4 21v5l7 9l4 15l-2 2v15v11v13h2l2 14l11 9h4z" />
<glyph glyph-name="asciitilde" unicode="~"
d="M210 311l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14
l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33z" />
<glyph glyph-name="exclamdown" unicode="&#xa1;" horiz-adv-x="500"
d="M172 485l24 -52l69 -24l43 58l9 43l-9 27l-25 24l-52 8l-42 -8zM172 66l51 -69h42l25 35l18 76l-25 96l7 8v34l-7 94l-25 42h-10l-42 -17v-25l-10 -128l10 -25l-10 -61z" />
<glyph glyph-name="fraction" unicode="&#x2044;"
d="M538 671v-72l-48 -49l9 -7l-40 -48v-33l-88 -143l-97 -176l-33 -49v-23l-95 -178l-16 -31l-65 -32h-16l-7 39l7 49l39 56v23l75 113l55 111l23 25l40 80l26 32v25l39 47l9 32l56 81v23l32 26l7 30l56 58z" />
<glyph glyph-name="section" unicode="&#xa7;"
d="M330 654l7 -25l34 -35l-9 -25v-27l-49 -17l-27 27l9 42h8l17 17v18l-25 17h-9l-8 -10v-17l-9 10h-33l-35 -60l8 -44l42 -66l35 -18l85 -110l60 -69v-17l9 -8l-9 -10l9 -7l-9 -52h-8l-35 -69l-26 -24l17 -87l-17 -76l-67 -59l-121 8l-48 34l-10 76l58 52h27l43 -42l7 -27
v-8l10 -7l42 42l-8 52l-26 17l-25 49l-52 62h-18l-58 66v18l-35 34v93l52 60v17l24 17l-17 103l27 60l59 34zM330 286l-79 86l-32 35l-35 -10l-27 -25l10 -58l77 -87l42 -66l9 -28l35 -6v6l32 69z" />
<glyph glyph-name="quotesingle" unicode="'"
d="M316 667l35 -52l-8 -84l-17 -34l14 -57l-14 -106l-51 -34l-8 -8l-27 8l-25 34l10 106l-35 124l-9 9l9 25l25 62l35 14z" />
<glyph glyph-name="quotedblleft" unicode="&#x201c;"
d="M314 534l14 -13l75 -27l35 19l28 41l-22 61l-33 22v20l19 19l21 28h7l8 27l-28 34l-8 -6h-19l-56 -48v-13l-33 -41v-20zM99 534l14 -13l75 -27l35 19l28 41l-22 61l-33 22v20l19 19l21 28h7l8 27l-28 34l-8 -6h-19l-56 -48v-13l-33 -41v-20z" />
<glyph glyph-name="fi" unicode="&#xfb01;"
d="M310 541l49 -52l-18 -42l-7 -10l-41 17l24 52l-24 35h-18l-10 -8l-7 25zM198 558l8 -11l-77 -49l-24 -69l49 -52l62 18h25l17 -18l-27 -17l-25 11l-25 -11l-27 11h-17l-17 -18l9 -42l-9 -27l17 -118v-86l94 -17l10 9h24l28 -27v-25l-18 -17l-94 -9h-27l-7 9l-10 -9h-25
l-7 9l-17 -17h-10l-17 17l-59 -17l-69 17l-8 25l17 27l60 17l25 -9h34l17 17l10 69l-10 7l17 121l-7 42v24l-44 28l-42 -18l-60 18v6l-9 11l86 17h25l17 -17h17l17 17v32l-7 10l17 42l32 52l44 17h17zM112 360v17l-7 11l-10 -28h17zM280 381l-6 -5l18 -4h25l18 -21l-55 -4
h-30l-4 16l-9 9l4 9h39zM416 360l7 -13l-7 -42l3 -5l4 -22l-12 -25l12 -42v-21l-12 -27l5 -49l7 -27l-7 -34l3 -12v-5l34 -7l21 7l9 -4l30 9l30 4l43 -16l7 -39l-16 -18l-55 5h-30l-60 4l-25 5l-9 -9l-21 4h-4h-60l-9 -9l-24 9l-69 5v37l42 13l18 -7l3 3l57 -9h-5v9h5v-3
l42 12l13 21v16l-5 6l5 85h3l6 3l-14 39l21 42l-16 22l9 25h3l-3 33l-30 21v9l24 13z" />
<glyph glyph-name="fl" unicode="&#xfb02;"
d="M310 541l49 -52l-18 -42l-7 -10l-41 17l24 52l-24 35h-18l-10 -8l-7 25zM198 558l8 -11l-77 -49l-24 -69l49 -52l62 18h25l17 -18l-27 -17l-25 11l-25 -11l-27 11h-17l-17 -18l9 -42l-9 -27l17 -118v-86l94 -17l10 9h24l28 -27v-25l-18 -17l-94 -9h-27l-7 9l-10 -9h-25
l-7 9l-17 -17h-10l-17 17l-59 -17l-69 17l-8 25l17 27l60 17l25 -9h34l17 17l10 69l-10 7l17 121l-7 42v24l-44 28l-42 -18l-60 18v6l-9 11l86 17h25l17 -17h17l17 17v32l-7 10l17 42l32 52l44 17h17zM112 360v17l-7 11l-10 -28h17zM393 566l8 -10l35 18l41 -18l11 -66
l-11 -121l11 -66l-11 -181l28 -58h24l10 7l18 -17h83l17 -25l-34 -34l-52 -8l-8 8l-24 -8h-17l-17 16l-11 -8l-66 8l-69 -16l-24 16l-28 -8h-34l-17 25l10 27l17 17l59 7l7 10l27 -10l35 34l-10 25l17 52l10 10l-27 25v17h10l25 69l-8 41l17 17l-9 52v25l-8 7l8 11l-8 34
l8 7l-18 17l-59 10l-59 -10l-27 10l-7 7v35l17 8z" />
<glyph glyph-name="endash" unicode="&#x2013;"
d="M381 326h18l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-59 -9l-10 9l-76 -9l-69 9l-8 -9l-86 17l-17 17v35l-7 7l24 45l86 17z" />
<glyph glyph-name="dagger" unicode="&#x2020;"
d="M238 -20v11l-45 13l-78 69v25l-33 45l-17 83l8 27l-8 77l17 93l50 84v17l69 45l68 17l60 -17l42 -28l68 28l8 -28l-17 -93l-8 -8l34 -86l-9 -42l-25 8l-34 34v17l-7 11l7 6l-17 35v34l-77 50l-25 10l-44 -10l-24 -32h-17l-52 -79v-8l-17 -17l9 -7l-9 -10l9 -42v-9l-9 -8
l17 -34l-8 -8l25 -69l10 -51l76 -69l59 9l35 35l17 8l25 27v17l27 24v42l32 10l26 -18l8 -34l-17 -68l-66 -85l-18 -34l-76 -15l-4 -9v-8l2 -2l-4 -17l4 -14l6 -2l13 -16l11 -1l17 -17l4 -15l5 -2l6 -5l-2 -9l3 -4l-1 -17v-2l5 -19l-5 -13l-2 2l-12 -19v-13l-13 -15l-16 -6
l-13 -9l-23 -7l-2 -2v-4h-2l-16 -3l-19 3l-4 -3l-13 1l-4 6h-7v8l-12 9l-3 11l-8 8l8 14l-2 2l8 16l-3 10l10 19h4h5h34l19 -9l11 -15l9 -2h4l6 11v14l-2 18l-9 11v8l-16 5v4l-11 6h-5l-24 1l-12 12l-11 5l-8 14l6 14l9 4l12 3l1 4l10 -4l7 12l-2 20l4 4z" />
<glyph glyph-name="periodcentered" unicode="&#xb7;"
d="M307 328l69 -67l-7 -86l-62 -42l-34 8l-8 -8l-59 25l-27 68l17 52l52 43l42 7h17z" />
<glyph glyph-name="bullet" unicode="&#x2022;"
d="M307 328l69 -67l-7 -86l-62 -42l-34 8l-8 -8l-59 25l-27 68l17 52l52 43l42 7h17z" />
<glyph glyph-name="quotesinglbase" unicode="&#x201a;"
d="M326 133l14 -13l-8 -103v-20l-33 -41v-13l-56 -48h-19l-8 -6l-28 34l8 27h7l21 28l19 19v20l-33 22l-22 61l28 41l35 19z" />
<glyph glyph-name="quotedblbase" unicode="&#x201e;"
d="M216 133l14 -13l-8 -103v-20l-33 -41v-13l-56 -48h-19l-8 -6l-28 34l8 27h7l21 28l19 19v20l-33 22l-22 61l28 41l35 19zM431 133l14 -13l-8 -103v-20l-33 -41v-13l-56 -48h-19l-8 -6l-28 34l8 27h7l21 28l19 19v20l-33 22l-22 61l28 41l35 19z" />
<glyph glyph-name="quotedblright" unicode="&#x201d;"
d="M231 728l14 -13l-8 -103v-20l-33 -41v-13l-56 -48h-19l-8 -6l-28 34l8 27h7l21 28l19 19v20l-33 22l-22 61l28 41l35 19zM446 728l14 -13l-8 -103v-20l-33 -41v-13l-56 -48h-19l-8 -6l-28 34l8 27h7l21 28l19 19v20l-33 22l-22 61l28 41l35 19z" />
<glyph glyph-name="questiondown" unicode="&#xbf;" horiz-adv-x="500"
d="M158 497l18 -44l78 -17l67 52v34l-77 17l-76 -17zM116 19l52 -27l86 -17l32 10l86 34l35 42l24 94l-17 17l-34 -7l-8 7l-42 -7l-17 -17v-28l8 -7l44 -27v-15l-10 -10l-52 -25l-93 8l-42 35l-25 76l15 34l45 25l17 17h17l24 44h17l18 25l-10 7l-8 52l-17 17h-24l-27 -6
v-18l-7 -10l7 -49l-7 -18l-60 -34l-17 -17l-69 -76l7 -62z" />
<glyph glyph-name="grave" unicode="`"
d="M243 648l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7z" />
<glyph glyph-name="acute" unicode="&#xb4;"
d="M332 677l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21z" />
<glyph glyph-name="circumflex" unicode="&#x2c6;"
d="M317 814l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21z" />
<glyph glyph-name="tilde" unicode="&#x2dc;"
d="M210 311l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14
l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33z" />
<glyph glyph-name="breve" unicode="&#x2d8;"
d="M324 594l-5 4l-55 -18l-30 21l-68 27l-47 85l-46 46l-4 39l7 30h14l13 4h30v-4l21 -27l21 -42v-4l25 -38l51 -22l4 4l5 9l67 42l25 43h9l30 26h60l13 -39l-9 -9l3 -28l-46 -39v-12l-21 -13l-33 -63z" />
<glyph glyph-name="dotaccent" unicode="&#x2d9;"
d="M292 524l21 -7l9 -26l-13 -30l-17 5l-29 -9l-21 9l-14 21v4l9 9l-4 3v9l22 12l24 9z" />
<glyph glyph-name="dieresis" unicode="&#xa8;"
d="M156 565l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM385 558l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25z" />
<glyph glyph-name="ring" unicode="&#x2da;"
d="M280 653h13v-8l17 -34l13 -48l-4 -3l7 -13l-24 -47l-39 -34l-25 -9h-25l-5 -4l-30 22l-25 25l-4 17v30l-17 34l12 16h5l7 30l30 30h6l25 4l21 17zM263 606l-16 17l-34 9l-14 -9v-42l-13 -34l31 -42l30 16v9l9 17l3 59h4z" />
<glyph glyph-name="cedilla" unicode="&#xb8;"
d="M273 73v-11l3 -2h1l-5 -15l5 -8l10 -20l-4 -9v-8l2 -2l-4 -17l4 -14l6 -2l13 -16l11 -1l17 -17l4 -15l5 -2l6 -5l-2 -9l3 -4l-1 -17v-2l5 -19l-5 -13l-2 2l-12 -19v-13l-13 -15l-16 -6l-13 -9l-23 -7l-2 -2v-4h-2l-16 -3l-19 3l-4 -3l-13 1l-4 6h-7v8l-12 9l-3 11l-8 8
l8 14l-2 2l8 16l-3 10l10 19h4h5h34l19 -9l11 -15l9 -2h4l6 11v14l-2 18l-9 11v8l-16 5v4l-11 6h-5l-24 1l-12 12l-11 5l-8 14l6 14l9 4l12 3l1 4l10 -4l7 12l-2 20l4 4l-2 2v12l-5 7l5 17l14 22l14 2z" />
<glyph glyph-name="hungarumlaut" unicode="&#x2dd;"
d="M257 677l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21zM487 677l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8
l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21z" />
<glyph glyph-name="caron" unicode="&#x2c7;"
d="M324 594l-5 4l-55 -18l-30 21l-68 27l-47 85l-46 46l-4 39l7 30h14l13 4h30v-4l21 -27l21 -42v-4l25 -38l51 -22l4 4l5 9l67 42l25 43h9l30 26h60l13 -39l-9 -9l3 -28l-46 -39v-12l-21 -13l-33 -63z" />
<glyph glyph-name="emdash" unicode="&#x2014;"
d="M381 326h78l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-119 -9l-10 9l-76 -9l-199 9l-8 -9l-61 17l-17 17v35l-7 7l24 45l61 17z" />
<glyph glyph-name="AE" unicode="&#xc6;"
d="M551 536h7l27 -118l-10 -7l10 -45l-10 -7l-35 52l-6 58l-52 60l-8 -8l-10 8l45 24zM287 265l-4 -53l15 -73l17 -28v-35l19 -18l54 -7l17 10l59 -10l52 52l35 101l7 9h17l17 -17l-17 -69l10 -17l-17 -101l-10 -10l-32 10h-35l-17 -17l-44 7l-18 -3l-13 -13h-32l-9 -7
l-153 17l-1 2l-3 -3l-24 7l-35 -7l-12 11l-19 -20h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18
l17 -77l27 16l-4 104l-17 17l-41 -10l-62 10l-7 8l17 41l153 -7l9 -10l32 17l28 -7l58 7l8 -7v-17l-126 -17l-17 -18l8 -100l-8 -11l8 -41l24 -28l34 -7l17 17l35 35l11 35l6 6l35 -6l7 -70l-17 -51l10 -69l-27 -25h-8l-17 34l11 25l-18 27l-17 17h-77zM218 68l30 31l-2 5v7
l-24 28l-146 -11l-9 -6l9 -52l35 -17l10 -17l4 15z" />
<glyph glyph-name="Oslash" unicode="&#xd8;"
d="M202 -2l-56 -105l-16 -31l-65 -32h-16l-7 39l7 49l39 56v23l38 57l-4 4l-27 17v18l-17 17l-7 -11v-6h-17l-11 68v25l-6 9l17 95v31l-11 10l18 59l27 77l42 62l103 66l118 -18l50 -49l10 15v23l32 26l7 30l56 58l32 -9v-72l-48 -49l9 -7l-40 -48v-33l-4 -7l7 -5l25 -129
l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14zM391 351l-20 -32l-97 -176l-33 -49v-1h17l17 24l17 -18h17l42 35v17l-7 10l17 17l8 85l25 27v58zM181 146l37 75l23 25l40 80l26 32v25l39 47l9 32l1 2l-12 20h-27l-25 34h-34l-34 -7l-43 -27l-17 -17
l8 -42l-17 -25h-16l16 -189l26 -25v-40z" />
<glyph glyph-name="OE" unicode="&#x152;"
d="M525 536h7l27 -118l-10 -7l10 -45l-10 -7l-35 52l-6 58l-52 60l-8 -8l-10 8l45 24zM-69 309l17 68h18v-44l-27 -52zM126 0l-50 1l-85 69l-35 58l-25 129l8 10h9l8 -10l17 -25l-7 -10v-17l25 -25l9 -41l59 -69l63 -26l23 8s45 15 47 16l34 35l8 34l-8 18l8 66l-8 266
l-17 17s-56 -1 -66 -1l-32 10l-69 -17l-76 -94l7 8v9l18 69l41 25l44 17l55 23l58 -10l33 9l15 12l32 -18l9 -10l32 17l28 -7l58 7l8 -7v-17l-126 -17l-17 -18l8 -100l-8 -11l8 -41l24 -28l34 -7l17 17l35 35l11 35l6 6l35 -6l7 -70l-17 -51l10 -69l-27 -25h-8l-17 34l11 25
l-18 27l-17 17h-77l-17 -17l-10 -121l18 -24v-42l17 -17l76 -10l17 10l59 -10l52 52l35 101l7 9h17l17 -17l-17 -69l10 -17l-17 -101l-10 -10l-32 10h-35l-17 -17l-44 7l-49 -7l-45 7l-76 -7l-36 5l-31 -19z" />
<glyph glyph-name="ae" unicode="&#xe6;"
d="M260 387l18 -22l22 21l25 10l135 -10l70 -76l26 -52v-34l-9 -7l9 -8l-9 -27l-42 10l-45 -18l-24 18l-42 -10l-17 -17v9l-52 8l-30 -3v-4l7 -18v-34l-7 -10l24 -25l62 8l5 -58l32 9l10 -10l84 76h27l8 -25l-17 -34l-60 -51l-93 -25l-16 10l-8 -10l-58 -8l-52 33l-8 9
l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76l27 25l85 27l78 -10l7 10l69 -18zM411 335h-27l-49 9l-10 -9l10 -8l-10 -7v-28l10 -6h7l35 24l24 -24v-11l-24 -41l59 7v69zM201 148l7 27h-17l-66 -18l-10 8l-8 -8h-51l-43 -27
l8 -7l-8 -35l18 -24h94l58 18l35 41zM226 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17zM335 241v17h-44l17 -34z" />
<glyph glyph-name="dotlessi" unicode="&#x131;"
d="M170 381l-6 -5l18 -4h25l18 -21l-55 -4h-30l-4 16l-9 9l4 9h39zM306 360l7 -13l-7 -42l3 -5l4 -22l-12 -25l12 -42v-21l-12 -27l5 -49l7 -27l-7 -34l3 -12v-5l34 -7l21 7l9 -4l30 9l30 4l43 -16l7 -39l-16 -18l-55 5h-30l-60 4l-25 5l-9 -9l-21 4h-4h-60l-9 -9l-24 9
l-69 5v37l42 13l18 -7l3 3l57 -9h-5v9h5v-3l42 12l13 21v16l-5 6l5 85h3l6 3l-14 39l21 42l-16 22l9 25h3l-3 33l-30 21v9l24 13z" />
<glyph glyph-name="oslash" unicode="&#xf8;"
d="M199 -8l-53 -99l-16 -31l-65 -32h-16l-7 39l7 49l39 56v23l37 56l-47 60l17 162l35 60l69 61l83 17h50l14 17l9 32l56 81v23l32 26l7 30l56 58l32 -9v-72l-48 -49l9 -7l-40 -48v-33l-61 -99l47 -60l24 -52l10 -77l-17 -27l-17 -76l-69 -76h-25l-34 -25l-111 17zM361 301
l-87 -158l-33 -49v-23l-5 -10l56 -7l25 17l8 -7h9l8 7l34 59l18 87l-25 75zM172 128l46 93l23 25l40 80l13 17l-19 9l-27 -17l-49 -8l-27 -17l-25 -69l17 -49l-9 -10l9 -42z" />
<glyph glyph-name="oe" unicode="&#x153;"
d="M295 -5l-7 7l-7 -7h-25l-34 -25l-111 17l-34 25l-42 35l-52 66l17 162l35 60l69 61l83 17h52l47 -35l9 8l25 10l135 -10l70 -76l26 -52v-34l-9 -7l9 -8l-9 -27l-42 10l-45 -18l-24 18l-42 -10l-5 -5v-3l-17 -27l-17 -76l-15 -16l44 -18l34 10l10 -10l84 76h27l8 -25
l-17 -34l-60 -51l-93 -25l-28 17h-49zM406 335h-27l-49 9l-10 -9l10 -8l-10 -7v-28l10 -6h7l35 24l24 -24v-11l-24 -41l59 7v69zM261 105l4 -3l16 28l10 49l-40 -5l-8 -9zM202 286l22 45l-44 21l-27 -17l-49 -8l-27 -17l-25 -69l17 -49l-9 -10l9 -42l49 -76l79 -10l25 17
l8 -7h4v7l-32 34l-17 94l-11 10zM330 241v17h-44l17 -34z" />
<glyph glyph-name="germandbls" unicode="&#xdf;"
d="M381 565l61 -75l-10 -87l-17 -42l-25 -35v-17l42 -24l35 -59l7 -70l-15 -17l-9 -58l-69 -69l-60 -18l-41 10l-35 25l-7 -8l-35 -17h-110l-18 25v8l24 26h18l17 18l-7 93l-10 7l25 87l-25 69l17 142l25 45l52 41l75 18zM314 514l-10 -7l-52 7l-59 -42l-17 -77l10 -9
l-10 -49l10 -28l-18 -17l18 -128l-10 -66l44 -35l8 8h10v51l31 34h17l45 -17l7 -34l-34 -34l44 -17l42 27l25 52l-8 6l8 8v9l-17 52l9 8l-51 59l-42 -7l-10 7l-66 10l-18 17v18l18 17l110 17l25 24l25 35l-8 7v59z" />
<glyph glyph-name="Adieresis" unicode="&#xc4;"
d="M268 461l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18l17 -77l17 -24v-51l25 -25l27 -128l17 -28v-35l25 -23l59 -8l17 -17v-27l-17 -18h-32l-9 -7l-153 17l-17 17l6 18l28 25h17l17 23l-10 28v7l-24 28
l-146 -11l-9 -6l9 -52l35 -17l25 -43l-25 -27h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25zM292 537l17 -17v-17l-41 10v24h24zM181 725l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM410 718l35 -35
l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25z" />
<glyph glyph-name="Aring" unicode="&#xc5;"
d="M268 461l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18l17 -77l17 -24v-51l25 -25l27 -128l17 -28v-35l25 -23l59 -8l17 -17v-27l-17 -18h-32l-9 -7l-153 17l-17 17l6 18l28 25h17l17 23l-10 28v7l-24 28
l-146 -11l-9 -6l9 -52l35 -17l25 -43l-25 -27h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25zM292 537l17 -17v-17l-41 10v24h24zM335 708h13v-8l17 -34l13 -48l-4 -3l7 -13l-24 -47l-39 -34l-25 -9h-25l-5 -4
l-30 22l-25 25l-4 17v30l-17 34l12 16h5l7 30l30 30h6l25 4l21 17zM318 661l-16 17l-34 9l-14 -9v-42l-13 -34l31 -42l30 16v9l9 17l3 59h4z" />
<glyph glyph-name="Ccedilla" unicode="&#xc7;" horiz-adv-x="500"
d="M329 569l42 -28l68 28l8 -28l-17 -93l-8 -8l34 -86l-9 -42l-25 8l-34 34v17l-7 11l7 6l-17 35v34l-77 50l-25 10l-44 -10l-24 -32h-17l-52 -79v-8l-17 -17l9 -7l-9 -10l9 -42v-9l-9 -8l17 -34l-8 -8l25 -69l10 -51l76 -69l59 9l35 35l17 8l25 27v17l27 24v42l32 10
l26 -18l8 -34l-17 -68l-66 -85l-18 -34l-86 -17l-84 24l-78 69v25l-33 45l-17 83l8 27l-8 77l17 93l50 84v17l69 45l68 17zM361 76l18 -17l-10 -128v-25l-42 -51v-17l-69 -60h-24l-10 -7l-35 42l10 34h8l27 35l24 24v25l-42 27l-27 76l35 52l44 24z" />
<glyph glyph-name="Eacute" unicode="&#xc9;"
d="M461 536h7l27 -118l-10 -7l10 -45l-10 -7l-35 52l-6 58l-52 60l-8 -8l-10 8l45 24zM205 546l9 -10l32 17l28 -7l58 7l8 -7v-17l-126 -17l-17 -18l8 -100l-8 -11l8 -41l24 -28l34 -7l17 17l35 35l11 35l6 6l35 -6l7 -70l-17 -51l10 -69l-27 -25h-8l-17 34l11 25l-18 27
l-17 17h-77l-17 -17l-10 -121l18 -24v-42l17 -17l76 -10l17 10l59 -10l52 52l35 101l7 9h17l17 -17l-17 -69l10 -17l-17 -101l-10 -10l-32 10h-35l-17 -17l-44 7l-49 -7l-45 7l-76 -7l-52 7l-7 -7l-24 7l-35 -7l-27 24l10 35l93 17l34 35l8 34l-8 18l8 66l-8 266l-17 17
l-41 -10l-62 10l-7 8l17 41zM432 837l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21z" />
<glyph glyph-name="Ntilde" unicode="&#xd1;"
d="M210 771l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14
l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33zM559 530l-22 -31l-17 -17l-42 7l-27 -25l10 -59l-17 -85l7 -25l-7 -60v-27h-11l-14 52l8 60l-18 137v7l-25 25l-9 -7l-52 17l-14 17l6 25l52 17l25 -7l121 7zM170 541l10 10l59 -10l24 -25l-17 -52l45 -42
l-10 -27l42 -100l9 -35v-35l-41 43l-28 86l-24 41l-27 27l10 35l-10 7h-7l-17 -69l9 -66l-9 -9l17 -257l58 -17h18l17 -25v-17l-17 -18l-59 11l-10 -11l-7 11l-8 -11l-61 11l-25 -11l-69 11l-7 7l24 42l59 27l10 52l-17 25l17 51l-10 7l18 27l9 60l-27 34v25h27l8 10l-25 66
l8 27v7l-18 18l-76 17v8l-7 9l52 42zM384 285v17h8v-17h-8zM315 174v-32l-17 32l11 10zM384 174h25v-24h-7l-10 -8v-10l27 -25l25 8l17 -17l7 -77l-17 -24l-7 -11l-35 11l-17 17v24l-25 25h-17l-10 10l10 25h7l18 17l-25 41l7 28zM451 167v-35h-18l-6 35h24z" />
<glyph glyph-name="Odieresis" unicode="&#xd6;"
d="M37 58v13h11v-13h-11zM317 -43v12h12v-12h-12zM351 577l77 -76l10 -34l24 -17l25 -129l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14l-103 17v18l-16 17l-9 -8v-9h-18l10 9v25l-27 17v18l-17 17l-7 -11v-6h-17l-11 68v25l-6 9l17 95v31l-11 10
l18 59l27 77l42 62l103 66zM216 725l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM445 718l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25zM317 484l-25 34h-34l-34 -7l-43 -27l-17 -17l8 -42l-17 -25h-16l16 -189l26 -25v-52h18l34 -41h25l17 24l17 -18h17
l42 35v17l-7 10l17 17l8 85l25 27v58l-18 17v25l10 10l-10 8v25l-32 51h-27zM317 195v13h12v-13h-12zM317 314v12h12v-12h-12z" />
<glyph glyph-name="Udieresis" unicode="&#xdc;"
d="M237 484v14h13v-14h-13zM348 365v13h12v-13h-12zM348 228v-16l-7 -9h-10l-7 25h24zM434 -129v-18l-9 18h9zM348 577l10 10l76 -18l25 18l44 -10l15 -17v-32l-59 -10l-8 10l-26 -17l9 -111l-9 -76l9 -79l-9 -42l9 -69l-52 -93l-93 -49v6l-7 8l-35 -8l-76 25h-17l-61 111
l-8 34l8 10l-8 25l17 69l-9 17l9 49l-9 17l9 52l-17 27l8 42l-18 52h-7l-8 -10l-17 17h-27l-8 25l8 9l34 18l43 -10l78 10l49 -10l10 -17v-32l-69 -10l-17 -17l10 -42l-17 -34l17 -35l-17 -25v-10l7 -7l-7 -24l7 -52l-7 -17l7 -69l27 -25v-34l66 -35h70l24 24v28l34 17
l-7 111l18 52l-18 126l18 17l-18 44l-86 24v25l59 34zM181 720l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM410 713l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25z" />
<glyph glyph-name="aacute" unicode="&#xe1;"
d="M279 404l7 10l69 -18l25 -9l35 -43l7 -110l-7 -59l7 -18v-34l-7 -10l24 -25l62 8l7 -76l-35 -42l-58 -8l-52 33l-8 9l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76l27 25l85 27zM387 677l16 -5l17 -25l-21 -47l-51 -29
l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21zM346 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17zM321 148l7 27h-17l-66 -18l-10 8l-8 -8h-51l-43 -27
l8 -7l-8 -35l18 -24h94l58 18l35 41z" />
<glyph glyph-name="agrave" unicode="&#xe0;"
d="M279 404l7 10l69 -18l25 -9l35 -43l7 -110l-7 -59l7 -18v-34l-7 -10l24 -25l62 8l7 -76l-35 -42l-58 -8l-52 33l-8 9l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76l27 25l85 27zM146 648l25 -16l3 -5l-7 -9l25 -24v-31l21 -3
l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7zM346 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17zM321 148l7 27h-17l-66 -18l-10 8l-8 -8h-51l-43 -27l8 -7
l-8 -35l18 -24h94l58 18l35 41z" />
<glyph glyph-name="acircumflex" unicode="&#xe2;"
d="M279 404l7 10l69 -18l25 -9l35 -43l7 -110l-7 -59l7 -18v-34l-7 -10l24 -25l62 8l7 -76l-35 -42l-58 -8l-52 33l-8 9l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76l27 25l85 27zM300 669l5 4l34 -22l33 -63l21 -13v-12
l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21zM346 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17zM321 148
l7 27h-17l-66 -18l-10 8l-8 -8h-51l-43 -27l8 -7l-8 -35l18 -24h94l58 18l35 41z" />
<glyph glyph-name="adieresis" unicode="&#xe4;"
d="M267 404l7 10l69 -18l25 -9l35 -43l7 -110l-7 -59l7 -18v-34l-7 -10l24 -25l62 8l7 -76l-35 -42l-58 -8l-52 33l-8 9l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76l27 25l85 27zM138 565l25 -24v-52l-35 -52l-49 27l-17 25
l8 52l44 24h24zM367 558l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25zM309 148l7 27h-17l-66 -18l-10 8l-8 -8h-51l-43 -27l8 -7l-8 -35l18 -24h94l58 18l35 41zM334 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17z" />
<glyph glyph-name="atilde" unicode="&#xe3;"
d="M230 761l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14
l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33zM279 404l7 10l69 -18l25 -9l35 -43l7 -110l-7 -59l7 -18v-34l-7 -10l24 -25l62 8l7 -76l-35 -42l-58 -8l-52 33l-8 9l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76
l27 25l85 27zM321 148l7 27h-17l-66 -18l-10 8l-8 -8h-51l-43 -27l8 -7l-8 -35l18 -24h94l58 18l35 41zM346 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17z" />
<glyph glyph-name="aring" unicode="&#xe5;"
d="M279 404l7 10l69 -18l25 -9l35 -43l7 -110l-7 -59l7 -18v-34l-7 -10l24 -25l62 8l7 -76l-35 -42l-58 -8l-52 33l-8 9l-103 -34h-59l-10 8l-7 -8l-43 8h-26l-49 51l-10 76v35l68 59h8l9 10l-9 7h-17l-25 62l7 76l27 25l85 27zM292 653h13v-8l17 -34l13 -48l-4 -3l7 -13
l-24 -47l-39 -34l-25 -9h-25l-5 -4l-30 22l-25 25l-4 17v30l-17 34l12 16h5l7 30l30 30h6l25 4l21 17zM346 310l-25 17l-94 25l-51 -17v-8l25 -41l-8 -11v-6l-52 -35h42l79 17l17 18h24l52 17l-9 7v17zM321 148l7 27h-17l-66 -18l-10 8l-8 -8h-51l-43 -27l8 -7l-8 -35
l18 -24h94l58 18l35 41zM275 606l-16 17l-34 9l-14 -9v-42l-13 -34l31 -42l30 16v9l9 17l3 59h4z" />
<glyph glyph-name="ccedilla" unicode="&#xe7;" horiz-adv-x="500"
d="M342 404l76 -35l17 -25l25 -26l-7 -84l-25 -17h-68l-18 41l35 35l-28 34l-41 25l-52 -8l-42 -17l-59 -69l17 -128l18 -25l6 -9h18l34 -32l35 -17l49 17l86 76h52l8 -27l-18 -49l-49 -52l-79 -34l-59 -8l-25 8h-27l-77 52l-58 75l-8 77l18 93l59 87l41 34l60 8l9 10z
M361 66l18 -17l-10 -128v-25l-42 -51v-17l-69 -60h-24l-10 -7l-35 42l10 34h8l27 35l24 24v25l-42 27l-27 76l35 52l44 24z" />
<glyph glyph-name="eacute" unicode="&#xe9;"
d="M335 386l70 -76l26 -52v-34l-9 -7l9 -8l-9 -27l-42 10l-45 -18l-24 18l-42 -10l-17 -17v9l-52 8l-69 -8l-8 -9l18 -60l51 -41l67 -27l34 10l10 -10l84 76h27l8 -25l-17 -34l-60 -51l-93 -25l-28 17h-49l-61 59v17l-32 34l-17 94l-11 10l28 77l24 49l42 26l27 25l25 10z
M434 677l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21zM286 335h-27l-49 9l-10 -9l10 -8l-10 -7v-28l10 -6h7l35 24l24 -24v-11l-24 -41l59 7v69zM210 241v17h-44
l17 -34z" />
<glyph glyph-name="egrave" unicode="&#xe8;"
d="M335 386l70 -76l26 -52v-34l-9 -7l9 -8l-9 -27l-42 10l-45 -18l-24 18l-42 -10l-17 -17v9l-52 8l-69 -8l-8 -9l18 -60l51 -41l67 -27l34 10l10 -10l84 76h27l8 -25l-17 -34l-60 -51l-93 -25l-28 17h-49l-61 59v17l-32 34l-17 94l-11 10l28 77l24 49l42 26l27 25l25 10z
M153 648l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7zM286 335h-27l-49 9l-10 -9l10 -8l-10 -7v-28l10 -6h7l35 24l24 -24v-11l-24 -41l59 7v69zM210 241v17h-44l17 -34z
" />
<glyph glyph-name="ecircumflex" unicode="&#xea;"
d="M335 386l70 -76l26 -52v-34l-9 -7l9 -8l-9 -27l-42 10l-45 -18l-24 18l-42 -10l-17 -17v9l-52 8l-69 -8l-8 -9l18 -60l51 -41l67 -27l34 10l10 -10l84 76h27l8 -25l-17 -34l-60 -51l-93 -25l-28 17h-49l-61 59v17l-32 34l-17 94l-11 10l28 77l24 49l42 26l27 25l25 10z
M312 669l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21zM286 335h-27l-49 9l-10 -9l10 -8l-10 -7v-28l10 -6h7l35 24l24 -24
v-11l-24 -41l59 7v69zM210 241v17h-44l17 -34z" />
<glyph glyph-name="edieresis" unicode="&#xeb;"
d="M335 386l70 -76l26 -52v-34l-9 -7l9 -8l-9 -27l-42 10l-45 -18l-24 18l-42 -10l-17 -17v9l-52 8l-69 -8l-8 -9l18 -60l51 -41l67 -27l34 10l10 -10l84 76h27l8 -25l-17 -34l-60 -51l-93 -25l-28 17h-49l-61 59v17l-32 34l-17 94l-11 10l28 77l24 49l42 26l27 25l25 10z
M165 565l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM394 558l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25zM210 241v17h-44l17 -34zM286 335h-27l-49 9l-10 -9l10 -8l-10 -7v-28l10 -6h7l35 24l24 -24v-11l-24 -41l59 7v69z" />
<glyph glyph-name="iacute" unicode="&#xed;"
d="M170 381l-6 -5l18 -4h25l18 -21l-55 -4h-30l-4 16l-9 9l4 9h39zM306 360l7 -13l-7 -42l3 -5l4 -22l-12 -25l12 -42v-21l-12 -27l5 -49l7 -27l-7 -34l3 -12v-5l34 -7l21 7l9 -4l30 9l30 4l43 -16l7 -39l-16 -18l-55 5h-30l-60 4l-25 5l-9 -9l-21 4h-4h-60l-9 -9l-24 9
l-69 5v37l42 13l18 -7l3 3l57 -9h-5v9h5v-3l42 12l13 21v16l-5 6l5 85h3l6 3l-14 39l21 42l-16 22l9 25h3l-3 33l-30 21v9l24 13zM415 677l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27
l21 24l6 22l46 21z" />
<glyph glyph-name="igrave" unicode="&#xec;"
d="M170 381l-6 -5l18 -4h25l18 -21l-55 -4h-30l-4 16l-9 9l4 9h39zM306 360l7 -13l-7 -42l3 -5l4 -22l-12 -25l12 -42v-21l-12 -27l5 -49l7 -27l-7 -34l3 -12v-5l34 -7l21 7l9 -4l30 9l30 4l43 -16l7 -39l-16 -18l-55 5h-30l-60 4l-25 5l-9 -9l-21 4h-4h-60l-9 -9l-24 9
l-69 5v37l42 13l18 -7l3 3l57 -9h-5v9h5v-3l42 12l13 21v16l-5 6l5 85h3l6 3l-14 39l21 42l-16 22l9 25h3l-3 33l-30 21v9l24 13zM174 648l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27
l-30 28l12 36l14 7z" />
<glyph glyph-name="icircumflex" unicode="&#xee;"
d="M170 381l-6 -5l18 -4h25l18 -21l-55 -4h-30l-4 16l-9 9l4 9h39zM306 360l7 -13l-7 -42l3 -5l4 -22l-12 -25l12 -42v-21l-12 -27l5 -49l7 -27l-7 -34l3 -12v-5l34 -7l21 7l9 -4l30 9l30 4l43 -16l7 -39l-16 -18l-55 5h-30l-60 4l-25 5l-9 -9l-21 4h-4h-60l-9 -9l-24 9
l-69 5v37l42 13l18 -7l3 3l57 -9h-5v9h5v-3l42 12l13 21v16l-5 6l5 85h3l6 3l-14 39l21 42l-16 22l9 25h3l-3 33l-30 21v9l24 13zM317 684l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42
l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21z" />
<glyph glyph-name="idieresis" unicode="&#xef;"
d="M170 381l-6 -5l18 -4h25l18 -21l-55 -4h-30l-4 16l-9 9l4 9h39zM306 360l7 -13l-7 -42l3 -5l4 -22l-12 -25l12 -42v-21l-12 -27l5 -49l7 -27l-7 -34l3 -12v-5l34 -7l21 7l9 -4l30 9l30 4l43 -16l7 -39l-16 -18l-55 5h-30l-60 4l-25 5l-9 -9l-21 4h-4h-60l-9 -9l-24 9
l-69 5v37l42 13l18 -7l3 3l57 -9h-5v9h5v-3l42 12l13 21v16l-5 6l5 85h3l6 3l-14 39l21 42l-16 22l9 25h3l-3 33l-30 21v9l24 13zM155 565l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM384 558l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25z" />
<glyph glyph-name="ntilde" unicode="&#xf1;"
d="M524 21l-28 -26l-52 8l-69 -8h-34l-18 17v25l87 17h9l8 10l-8 49v35l8 9l-17 153l-60 34l-52 -26l-69 -94l-24 -59l7 -60l-7 -23v-11l24 -24l69 7h-6l-18 -42v-9l-17 -17l-28 9l-24 -9l-7 9l-35 -9l-44 9l-60 -9l-17 17h-7l7 27l28 24l49 10l17 18l10 75l-10 129v24
l-66 42l-17 27v25l24 17l59 -17l35 24l34 -32h7l28 -27l101 52l69 -7l58 -62l11 -101l-11 -197zM210 741l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21
l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33z" />
<glyph glyph-name="oacute" unicode="&#xf3;"
d="M334 413l60 -44l51 -66l24 -52l10 -77l-17 -27l-17 -76l-69 -76h-25l-34 -25l-111 17l-34 25l-42 35l-52 66l17 162l35 60l69 61l83 17h52zM437 677l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25
l22 12l3 27l21 24l6 22l46 21zM369 292l-27 28l-67 32l-27 -17l-49 -8l-27 -17l-25 -69l17 -49l-9 -10l9 -42l49 -76l79 -10l25 17l8 -7h9l8 7l34 59l18 87z" />
<glyph glyph-name="ograve" unicode="&#xf2;"
d="M19 404v12h12v-12h-12zM334 413l60 -44l51 -66l24 -52l10 -77l-17 -27l-17 -76l-69 -76h-25l-34 -25l-111 17l-34 25l-42 35l-52 66l17 162l35 60l69 61l83 17h52zM183 648l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5
l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7zM369 292l-27 28l-67 32l-27 -17l-49 -8l-27 -17l-25 -69l17 -49l-9 -10l9 -42l49 -76l79 -10l25 17l8 -7h9l8 7l34 59l18 87z" />
<glyph glyph-name="ocircumflex" unicode="&#xf4;"
d="M19 404v12h12v-12h-12zM334 413l60 -44l51 -66l24 -52l10 -77l-17 -27l-17 -76l-69 -76h-25l-34 -25l-111 17l-34 25l-42 35l-52 66l17 162l35 60l69 61l83 17h52zM335 654l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9
l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21zM369 292l-27 28l-67 32l-27 -17l-49 -8l-27 -17l-25 -69l17 -49l-9 -10l9 -42l49 -76l79 -10l25 17l8 -7h9l8 7l34 59l18 87z" />
<glyph glyph-name="odieresis" unicode="&#xf6;"
d="M19 404v12h12v-12h-12zM334 413l60 -44l51 -66l24 -52l10 -77l-17 -27l-17 -76l-69 -76h-25l-34 -25l-111 17l-34 25l-42 35l-52 66l17 162l35 60l69 61l83 17h52zM190 565l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM419 558l35 -35l-8 -59l-27 -27l-49 10
l-27 25l10 51l41 35h25zM369 292l-27 28l-67 32l-27 -17l-49 -8l-27 -17l-25 -69l17 -49l-9 -10l9 -42l49 -76l79 -10l25 17l8 -7h9l8 7l34 59l18 87z" />
<glyph glyph-name="otilde" unicode="&#xf5;"
d="M253 781l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14
l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33zM334 413l60 -44l51 -66l24 -52l10 -77l-17 -27l-17 -76l-69 -76h-25l-34 -25l-111 17l-34 25l-42 35l-52 66l17 162l35 60l69 61l83 17h52zM369 292l-27 28l-67 32l-27 -17l-49 -8l-27 -17l-25 -69l17 -49
l-9 -10l9 -42l49 -76l79 -10l25 17l8 -7h9l8 7l34 59l18 87z" />
<glyph glyph-name="uacute" unicode="&#xfa;"
d="M437 677l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21zM154 394l7 -24l-17 -103l10 -8l-10 -52l17 -110v-8l52 -17l52 8l52 59v27l31 41l18 112l10 9v15l-28 17h-76
l-7 28l52 24l49 -18l27 11l24 -11l11 -24l-11 -76l11 -62l-11 -7l18 -128l7 -8l10 8l77 -42l9 -10l-9 -42l-69 -17l-77 17h-24l-42 25l-111 -42l-69 25l-17 17l-10 -8l-15 25l8 128l-8 52l8 69l-7 40l-12 16l-32 -6h-17l-12 16l-9 2l-10 10l-8 16l26 20l56 4z" />
<glyph glyph-name="ugrave" unicode="&#xf9;"
d="M154 394l7 -24l-17 -103l10 -8l-10 -52l17 -110v-8l52 -17l52 8l52 59v27l31 41l18 112l10 9v15l-28 17h-76l-7 28l52 24l49 -18l27 11l24 -11l11 -24l-11 -76l11 -62l-11 -7l18 -128l7 -8l10 8l77 -42l9 -10l-9 -42l-69 -17l-77 17h-24l-42 25l-111 -42l-69 25l-17 17
l-10 -8l-15 25l8 128l-8 52l8 69l-25 118zM172 648l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7z" />
<glyph glyph-name="ucircumflex" unicode="&#xfb;"
d="M331 674l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21zM154 394l7 -24l-17 -103l10 -8l-10 -52l17 -110v-8l52 -17l52 8
l52 59v27l31 41l18 112l10 9v15l-28 17h-76l-7 28l52 24l49 -18l27 11l24 -11l11 -24l-11 -76l11 -62l-11 -7l18 -128l7 -8l10 8l77 -42l9 -10l-9 -42l-69 -17l-77 17h-24l-42 25l-111 -42l-69 25l-17 17l-10 -8l-15 25l8 128l-8 52l8 69l-7 40l-12 16l-32 -6h-17l-12 16
l-9 2l-10 10l-8 16l26 20l56 4z" />
<glyph glyph-name="udieresis" unicode="&#xfc;"
d="M174 565l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM403 558l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25zM154 394l7 -24l-17 -103l10 -8l-10 -52l17 -110v-8l52 -17l52 8l52 59v27l31 41l18 112l10 9v15l-28 17h-76l-7 28l52 24l49 -18l27 11l24 -11
l11 -24l-11 -76l11 -62l-11 -7l18 -128l7 -8l10 8l77 -42l9 -10l-9 -42l-69 -17l-77 17h-24l-42 25l-111 -42l-69 25l-17 17l-10 -8l-15 25l8 128l-8 52l8 69l-7 40l-12 16l-32 -6h-17l-12 16l-9 2l-10 10l-8 16l26 20l56 4z" />
<glyph glyph-name="degree" unicode="&#xb0;"
d="M238 -28l-4 6h-13l-77 52l-58 75l-8 77l18 93l59 87l41 34l60 8l9 10l77 -10l76 -35l17 -25l25 -26l-7 -84l-25 -17h-68l-18 41l35 35l-28 34l-41 25l-52 -8l-42 -17l-59 -69l17 -128l18 -25l6 -9h18l34 -32l35 -17l49 17l86 76h52l8 -27l-18 -49l-49 -52l-79 -34l-50 -7
l5 -9l-4 -9v-8l2 -2l-4 -17l4 -14l6 -2l13 -16l11 -1l17 -17l4 -15l5 -2l6 -5l-2 -9l3 -4l-1 -17v-2l5 -19l-5 -13l-2 2l-12 -19v-13l-13 -15l-16 -6l-13 -9l-23 -7l-2 -2v-4h-2l-16 -3l-19 3l-4 -3l-13 1l-4 6h-7v8l-12 9l-3 11l-8 8l8 14l-2 2l8 16l-3 10l10 19h4h5h34
l19 -9l11 -15l9 -2h4l6 11v14l-2 18l-9 11v8l-16 5v4l-11 6h-5l-24 1l-12 12l-11 5l-8 14l6 14l9 4l12 3l1 4l10 -4l7 12l-2 20l4 4l-2 2v12z" />
<glyph glyph-name="partialdiff" unicode="&#x2202;"
d="M146 533l76 -67v-61l10 -8l25 8h17v-8l10 -7l14 15h17l11 -50l-28 -27v44l-14 18l-10 -18h10v-34h-10v7l-8 10l-9 -17v-35l9 -9h8l-17 -84l-8 -10v-24l8 -8l9 8l18 -25l14 32v10h11l6 -52l-6 -59l-18 42l-25 -25l18 -59l14 17h17l11 -26v-35l-11 -8h-17l-14 18l-10 -10
l-8 10h-17l17 34v17l-9 8l-35 -34l-49 -52l-45 -8l-52 8l-6 -8l-43 17l-9 8v27l17 17l-17 32l9 10l-9 59l9 10l-9 76l17 76l-8 8l8 27l-25 34v25l35 35l-27 69l9 7v7l25 -14l18 14l41 18h35zM326 490l7 -24v-34l-42 41l18 17h17zM274 456v17h10v-17h-10zM520 515l17 -42
l-7 -76l7 -7l-17 -35l27 -69l-10 -34l-7 -7l17 -35l-10 -138l10 -32v-34l-17 -18h-17l-42 43l17 75v18l-9 9l-25 -27l-10 -41l10 -42v-17l-35 -18h-25l-27 25h-17l10 44l17 15l-17 61l7 43l-17 42l10 61l59 -61l42 120v25l10 9l34 -34l8 7v18l-8 9l8 18l-8 49l-17 17l8 52
l9 7h15zM488 227v-17h8v17h-8zM139 456l17 17h-34l-35 -58l-11 -52l-24 -25v-52l35 -34l-11 -7v-69l18 -52l-7 -8l-11 -85h28l18 26l41 15l17 86l-24 35l24 25l25 137l-25 35v25l8 7l-49 17v17z" />
<glyph glyph-name="nbspace" unicode="&#xa0;"
d="M0 0z" />
<glyph glyph-name="Agrave" unicode="&#xc0;"
d="M268 461l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18l17 -77l17 -24v-51l25 -25l27 -128l17 -28v-35l25 -23l59 -8l17 -17v-27l-17 -18h-32l-9 -7l-153 17l-17 17l6 18l28 25h17l17 23l-10 28v7l-24 28
l-146 -11l-9 -6l9 -52l35 -17l25 -43l-25 -27h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25zM292 537l17 -17v-17l-41 10v24h24zM168 808l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7
l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7z" />
<glyph glyph-name="Atilde" unicode="&#xc3;"
d="M268 461l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18l17 -77l17 -24v-51l25 -25l27 -128l17 -28v-35l25 -23l59 -8l17 -17v-27l-17 -18h-32l-9 -7l-153 17l-17 17l6 18l28 25h17l17 23l-10 28v7l-24 28
l-146 -11l-9 -6l9 -52l35 -17l25 -43l-25 -27h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25zM292 537l17 -17v-17l-41 10v24h24zM253 781l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18
l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33z" />
<glyph glyph-name="Otilde" unicode="&#xd5;"
d="M37 58v13h11v-13h-11zM317 -43v12h12v-12h-12zM351 577l77 -76l10 -34l24 -17l25 -129l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14l-103 17v18l-16 17l-9 -8v-9h-18l10 9v25l-27 17v18l-17 17l-7 -11v-6h-17l-11 68v25l-6 9l17 95v31l-11 10
l18 59l27 77l42 62l103 66zM317 484l-25 34h-34l-34 -7l-43 -27l-17 -17l8 -42l-17 -25h-16l16 -189l26 -25v-52h18l34 -41h25l17 24l17 -18h17l42 35v17l-7 10l17 17l8 85l25 27v58l-18 17v25l10 10l-10 8v25l-32 51h-27zM317 314v12h12v-12h-12zM317 195v13h12v-13h-12z
M253 781l22 -12l3 3l36 -21l3 -25l12 -9l9 9l30 -5l34 30l4 -4h8l-3 -5h21l4 18l21 17l34 -5l21 -16l9 -14l-4 -42l-18 -16h6v-6h-6l-30 -24l-21 -4l-7 -21l-5 3l-16 -12h-21l-6 -5l-16 8l-17 -8l-25 8h-6l-33 9l-34 30l-26 22h-22l-46 -64h-8l-9 -9l-7 9l-15 -5l-24 14
l-4 -6l-14 43l21 42l14 13v5l16 25l22 5l5 -5l25 18l30 12h33z" />
<glyph glyph-name="ydieresis" unicode="&#xff;"
d="M257 396v-17h-8l-10 8l10 9h8zM496 387l9 -8v-10l-9 -7h-8l-17 17l17 8h8zM427 387h26l-9 -43l-25 -26l-10 -67l-42 -111l-17 -17l-35 -180l-41 -59v-17l-52 -34l-77 -8l-41 18l-34 31l-8 52l8 52l23 25l28 9l24 -9l28 -42v-18l-28 -25l35 -34l42 25l35 52l27 42v61
l-27 18l-35 100l10 10l-27 25v52l9 6l-41 43l-10 34l-24 27h-11l-7 -10l-10 10l-24 -10l-11 10v17l69 -9l28 9h32l9 -127l25 -62l27 -25l8 -59l24 -35l34 35l18 84l44 96l8 59l-70 17l11 25zM195 590l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM424 583l35 -35
l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25z" />
<glyph glyph-name="Ydieresis" unicode="&#x178;"
d="M185 735l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM414 728l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25zM219 552l18 -35l-35 -52l-9 -7l26 -79l25 -25l35 -51l17 62l35 41l24 69l10 8l-17 24l-34 10l-11 7v28l62 6l7 -6l25 6l10 -6l25 6l9 -6l25 6
l35 -6l6 -11v-24l-41 -10l-69 -67l-8 -44v-17l-34 -14l-24 -87l-35 -77l18 -120l34 -17h59l25 -25v-34l-118 -18l-60 -7l-138 25l-7 9v25l35 25l24 -8l42 8h17l17 69l10 24l-10 17l18 79l-69 84l-8 42l-52 79l-24 25l-69 17l-17 17v24h9l8 11l10 -11l111 17z" />
<glyph glyph-name="Acircumflex" unicode="&#xc2;"
d="M268 461l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18l17 -77l17 -24v-51l25 -25l27 -128l17 -28v-35l25 -23l59 -8l17 -17v-27l-17 -18h-32l-9 -7l-153 17l-17 17l6 18l28 25h17l17 23l-10 28v7l-24 28
l-146 -11l-9 -6l9 -52l35 -17l25 -43l-25 -27h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25zM292 537l17 -17v-17l-41 10v24h24zM337 769l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26
h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21z" />
<glyph glyph-name="Ecircumflex" unicode="&#xca;"
d="M461 536h7l27 -118l-10 -7l10 -45l-10 -7l-35 52l-6 58l-52 60l-8 -8l-10 8l45 24zM205 546l9 -10l32 17l28 -7l58 7l8 -7v-17l-126 -17l-17 -18l8 -100l-8 -11l8 -41l24 -28l34 -7l17 17l35 35l11 35l6 6l35 -6l7 -70l-17 -51l10 -69l-27 -25h-8l-17 34l11 25l-18 27
l-17 17h-77l-17 -17l-10 -121l18 -24v-42l17 -17l76 -10l17 10l59 -10l52 52l35 101l7 9h17l17 -17l-17 -69l10 -17l-17 -101l-10 -10l-32 10h-35l-17 -17l-44 7l-49 -7l-45 7l-76 -7l-52 7l-7 -7l-24 7l-35 -7l-27 24l10 35l93 17l34 35l8 34l-8 18l8 66l-8 266l-17 17
l-41 -10l-62 10l-7 8l17 41zM342 824l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21z" />
<glyph glyph-name="Aacute" unicode="&#xc1;"
d="M268 461l-17 -17l-8 -94h-9l-18 -17l-7 -76l7 -8l-7 -27v-7l17 -18l83 8l11 -8l24 35l-7 60l-28 23l11 28l-11 76v25h18l17 -77l17 -24v-51l25 -25l27 -128l17 -28v-35l25 -23l59 -8l17 -17v-27l-17 -18h-32l-9 -7l-153 17l-17 17l6 18l28 25h17l17 23l-10 28v7l-24 28
l-146 -11l-9 -6l9 -52l35 -17l25 -43l-25 -27h-10l-7 -7l-69 7l-69 -7l-42 17h-7l-10 8l10 44l83 25l28 34l17 76l24 42v52l17 18v23l25 18l10 86l17 42h25zM292 537l17 -17v-17l-41 10v24h24zM477 837l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3
l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21z" />
<glyph glyph-name="Edieresis" unicode="&#xcb;"
d="M461 536h7l27 -118l-10 -7l10 -45l-10 -7l-35 52l-6 58l-52 60l-8 -8l-10 8l45 24zM205 546l9 -10l32 17l28 -7l58 7l8 -7v-17l-126 -17l-17 -18l8 -100l-8 -11l8 -41l24 -28l34 -7l17 17l35 35l11 35l6 6l35 -6l7 -70l-17 -51l10 -69l-27 -25h-8l-17 34l11 25l-18 27
l-17 17h-77l-17 -17l-10 -121l18 -24v-42l17 -17l76 -10l17 10l59 -10l52 52l35 101l7 9h17l17 -17l-17 -69l10 -17l-17 -101l-10 -10l-32 10h-35l-17 -17l-44 7l-49 -7l-45 7l-76 -7l-52 7l-7 -7l-24 7l-35 -7l-27 24l10 35l93 17l34 35l8 34l-8 18l8 66l-8 266l-17 17
l-41 -10l-62 10l-7 8l17 41zM216 725l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM445 718l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25z" />
<glyph glyph-name="Egrave" unicode="&#xc8;"
d="M461 536h7l27 -118l-10 -7l10 -45l-10 -7l-35 52l-6 58l-52 60l-8 -8l-10 8l45 24zM205 546l9 -10l32 17l28 -7l58 7l8 -7v-17l-126 -17l-17 -18l8 -100l-8 -11l8 -41l24 -28l34 -7l17 17l35 35l11 35l6 6l35 -6l7 -70l-17 -51l10 -69l-27 -25h-8l-17 34l11 25l-18 27
l-17 17h-77l-17 -17l-10 -121l18 -24v-42l17 -17l76 -10l17 10l59 -10l52 52l35 101l7 9h17l17 -17l-17 -69l10 -17l-17 -101l-10 -10l-32 10h-35l-17 -17l-44 7l-49 -7l-45 7l-76 -7l-52 7l-7 -7l-24 7l-35 -7l-27 24l10 35l93 17l34 35l8 34l-8 18l8 66l-8 266l-17 17
l-41 -10l-62 10l-7 8l17 41zM233 808l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7z" />
<glyph glyph-name="Iacute" unicode="&#xcd;"
d="M192 553h25l10 -10l31 17l28 -7l84 7l78 -7l8 -10v-31l-59 -35l-27 7h-16l-9 -7l-8 7h-27l-7 -7l7 -44l-7 -212l24 -139l18 -14h111l17 -27l7 -8l-14 -44l-35 -17l-9 10l-85 -10l-51 10l-34 -10l-69 10l-25 -10l-52 10l-35 17l-6 25l23 27l87 17h25l9 8l8 -8l17 205l-7 7
l7 76l-7 27l7 50v34l-17 10l-52 -10l-66 17l-28 28l11 23l76 25zM422 837l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21z" />
<glyph glyph-name="Icircumflex" unicode="&#xce;"
d="M192 553h25l10 -10l31 17l28 -7l84 7l78 -7l8 -10v-31l-59 -35l-27 7h-16l-9 -7l-8 7h-27l-7 -7l7 -44l-7 -212l24 -139l18 -14h111l17 -27l7 -8l-14 -44l-35 -17l-9 10l-85 -10l-51 10l-34 -10l-69 10l-25 -10l-52 10l-35 17l-6 25l23 27l87 17h25l9 8l8 -8l17 205l-7 7
l7 76l-7 27l7 50v34l-17 10l-52 -10l-66 17l-28 28l11 23l76 25zM342 809l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21z
" />
<glyph glyph-name="Idieresis" unicode="&#xcf;"
d="M192 553h25l10 -10l31 17l28 -7l84 7l78 -7l8 -10v-31l-59 -35l-27 7h-16l-9 -7l-8 7h-27l-7 -7l7 -44l-7 -212l24 -139l18 -14h111l17 -27l7 -8l-14 -44l-35 -17l-9 10l-85 -10l-51 10l-34 -10l-69 10l-25 -10l-52 10l-35 17l-6 25l23 27l87 17h25l9 8l8 -8l17 205l-7 7
l7 76l-7 27l7 50v34l-17 10l-52 -10l-66 17l-28 28l11 23l76 25zM186 725l25 -24v-52l-35 -52l-49 27l-17 25l8 52l44 24h24zM415 718l35 -35l-8 -59l-27 -27l-49 10l-27 25l10 51l41 35h25z" />
<glyph glyph-name="Igrave" unicode="&#xcc;"
d="M192 553h25l10 -10l31 17l28 -7l84 7l78 -7l8 -10v-31l-59 -35l-27 7h-16l-9 -7l-8 7h-27l-7 -7l7 -44l-7 -212l24 -139l18 -14h111l17 -27l7 -8l-14 -44l-35 -17l-9 10l-85 -10l-51 10l-34 -10l-69 10l-25 -10l-52 10l-35 17l-6 25l23 27l87 17h25l9 8l8 -8l17 205l-7 7
l7 76l-7 27l7 50v34l-17 10l-52 -10l-66 17l-28 28l11 23l76 25zM193 808l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7z" />
<glyph glyph-name="Oacute" unicode="&#xd3;"
d="M351 577l77 -76l10 -34l24 -17l25 -129l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14l-103 17v18l-16 17l-9 -8v-9h-18l10 9v25l-27 17v18l-17 17l-35 51v25l-6 9l17 95v31l-11 10l18 59l27 77l42 62l103 66zM457 837l16 -5l17 -25l-21 -47
l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21zM317 484l-25 34h-34l-34 -7l-43 -27l-17 -17l8 -42l-17 -25h-16l16 -189l26 -25v-52h18l34 -41h25l17 24l17 -18h17l42 35v17l-7 10l17 17
l8 85l25 27v58l-18 17v25l10 10l-10 8v25l-32 51h-27zM317 314v12h12v-12h-12z" />
<glyph glyph-name="Ocircumflex" unicode="&#xd4;"
d="M37 58v13h11v-13h-11zM317 -43v12h12v-12h-12zM351 577l77 -76l10 -34l24 -17l25 -129l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14l-103 17v18l-16 17l-9 -8v-9h-18l10 9v25l-27 17v18l-17 17l-7 -11v-6h-17l-11 68v25l-6 9l17 95v31l-11 10
l18 59l27 77l42 62l103 66zM362 819l5 4l34 -22l33 -63l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21zM317 484l-25 34h-34l-34 -7l-43 -27
l-17 -17l8 -42l-17 -25h-16l16 -189l26 -25v-52h18l34 -41h25l17 24l17 -18h17l42 35v17l-7 10l17 17l8 85l25 27v58l-18 17v25l10 10l-10 8v25l-32 51h-27z" />
<glyph glyph-name="apple" unicode="&#xf8ff;"
d="M381 871h78l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-119 -9l-10 9l-76 -9l-199 9l-8 -9l-61 17l-17 17v35l-7 7l24 45l61 17zM381 681h78l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-119 -9l-10 9l-76 -9l-199 9l-8 -9l-61 17l-17 17v35l-7 7l24 45
l61 17zM381 496h78l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-119 -9l-10 9l-76 -9l-199 9l-8 -9l-61 17l-17 17v35l-7 7l24 45l61 17zM381 316h78l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-119 -9l-10 9l-76 -9l-199 9l-8 -9l-61 17l-17 17v35l-7 7
l24 45l61 17zM381 126h78l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-119 -9l-10 9l-76 -9l-199 9l-8 -9l-61 17l-17 17v35l-7 7l24 45l61 17zM381 -64h78l9 -6l49 6l52 -6l27 -28v-59l-35 -34l-34 -8l-76 17l-119 -9l-10 9l-76 -9l-199 9l-8 -9l-61 17l-17 17v35
l-7 7l24 45l61 17z" />
<glyph glyph-name="Ograve" unicode="&#xd2;"
d="M37 58v13h11v-13h-11zM317 -43v12h12v-12h-12zM351 577l77 -76l10 -34l24 -17l25 -129l9 -58l-34 -136l-17 -28v-14l-51 -52l-8 -27l-17 -17l-94 -14l-17 14l-103 17v18l-16 17l-9 -8v-9h-18l10 9v25l-27 17v18l-17 17l-7 -11v-6h-17l-11 68v25l-6 9l17 95v31l-11 10
l18 59l27 77l42 62l103 66zM223 808l25 -16l3 -5l-7 -9l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7zM317 484l-25 34h-34l-34 -7l-43 -27l-17 -17l8 -42l-17 -25h-16l16 -189l26 -25v-52h18
l34 -41h25l17 24l17 -18h17l42 35v17l-7 10l17 17l8 85l25 27v58l-18 17v25l10 10l-10 8v25l-32 51h-27zM317 195v13h12v-13h-12zM317 314v12h12v-12h-12z" />
<glyph glyph-name="Uacute" unicode="&#xda;"
d="M348 365v13h12v-13h-12zM348 228v-16l-7 -9h-10l-7 25h24zM348 577l10 10l76 -18l25 18l44 -10l15 -17v-32l-59 -10l-8 10l-26 -17l9 -111l-9 -76l9 -79l-9 -42l9 -69l-52 -93l-93 -49v6l-7 8l-35 -8l-76 25h-17l-61 111l-8 34l8 10l-8 25l17 69l-9 17l9 49l-9 17l9 52
l-17 27l8 42l-18 52h-7l-8 -10l-17 17h-27l-8 25l8 9l34 18l43 -10l78 10l49 -10l10 -17v-32l-69 -10l-17 -17l10 -42l-17 -34l17 -35l-17 -25v-10l7 -7l-7 -24l7 -52l-7 -17l7 -69l27 -25v-34l66 -35h70l24 24v28l34 17l-7 111l18 52l-18 126l18 17l-18 44l-86 24v25l59 34
zM452 837l16 -5l17 -25l-21 -47l-51 -29l-52 -60l-7 -34l-35 -26l-13 14h-3l-12 -14v14l-22 25l4 17l12 8l-3 14l21 16l-5 5l17 25l22 12l3 27l21 24l6 22l46 21z" />
<glyph glyph-name="Ucircumflex" unicode="&#xdb;"
d="M237 484v14h13v-14h-13zM348 577l10 10l76 -18l25 18l44 -10l15 -17v-32l-59 -10l-8 10l-26 -17l9 -111l-9 -76l9 -79l-9 -42l9 -69l-52 -93l-93 -49v6l-7 8l-35 -8l-76 25h-17l-61 111l-8 34l8 10l-8 25l17 69l-9 17l9 49l-9 17l9 52l-17 27l8 42l-18 52h-7l-8 -10
l-17 17h-27l-8 25l8 9l34 18l43 -10l78 10l49 -10l10 -17v-32l-69 -10l-17 -17l10 -42l-17 -34l17 -35l-17 -25v-10l7 -7l-7 -24l7 -52l-7 -17l7 -69l27 -25v-34l66 -35h70l24 24v28l34 17l-7 111l18 52l-18 126l18 17l-18 44l-86 24v25l59 34zM327 814l5 4l34 -22l33 -63
l21 -13v-12l46 -39l-3 -28l9 -9l-13 -39h-60l-30 26h-9l-25 43l-67 42l-5 9l-4 4l-51 -22l-25 -38v-4l-21 -42l-21 -27v-4h-30l-13 4h-14l-7 30l4 39l46 46l47 85l68 27l30 21z" />
<glyph glyph-name="Ugrave" unicode="&#xd9;"
d="M348 365v13h12v-13h-12zM348 577l10 10l76 -18l25 18l44 -10l15 -17v-32l-59 -10l-8 10l-26 -17l9 -111l-9 -76l9 -79l-9 -42l9 -69l-52 -93l-93 -49v6l-7 8l-35 -8l-76 25h-17l-61 111l-8 34l8 10l-8 25l17 69l-9 17l9 49l-9 17l9 52l-17 27l8 42l-18 52h-7l-8 -10
l-17 17h-27l-8 25l8 9l34 18l43 -10l78 10l49 -10l10 -17v-32l-69 -10l-17 -17l10 -42l-17 -34l17 -35l-17 -25v-10l7 -7l-7 -24l7 -52l-7 -17l7 -69l27 -25v-34l66 -35h70l24 24v28l34 17l-7 111l18 52l-18 126l18 17l-18 44l-86 24v25l59 34zM213 808l25 -16l3 -5l-7 -9
l25 -24v-31l21 -3l46 -43l15 -8l-6 -34v-5l-25 -7l-63 37l3 5l-12 7l3 5l-7 16l-17 15l-4 -6l-48 21l-12 27l-30 28l12 36l14 7z" />
</font>
</defs></svg>

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -8,8 +8,8 @@ colors:
secondary: '#00b0f0'
accent: '#ffc700'
text_dark: '#616161'
background: '#fff'
browser_color: '#fff'
background: '#ffffff'
browser_color: '#ffffff'
favicon:
path: favicon.png
fonts:

36
demo/config/gallery.yaml Normal file
View File

@ -0,0 +1,36 @@
# Use gallery.py to automatically add photos stored in your /config/photos/gallery folder
# Add tags to your photos as shown below
# remove the # before [] if you removed all images to use gallery.py again
hero:
images: #[]
- src: hero/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg
- src: hero/gilley-aguilar-ywGDhTlf93E-unsplash.jpg
- src: hero/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg
gallery:
images: #[]
- src: gallery/almos-bechtold-3402kvtHhOo-unsplash.jpg
tags: [portrait]
- src: gallery/arthur-savary-nLfAqmZ2hJo-unsplash.jpg
tags: [portrait, sunset, boat]
- src: gallery/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg
tags: [landscape, sea, beach, sand]
- src: gallery/gilley-aguilar-ywGDhTlf93E-unsplash.jpg
tags: [landscape, sky, cloud, mountains]
- src: gallery/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg
tags: [landscape, sunset, mountains]
- src: gallery/jonas-degener-LueP5EdWGFY-unsplash.jpg
tags: [landscape, mountains, fog]
- src: gallery/michiel-annaert-M27pZnHV6M0-unsplash.jpg
tags: [flowers, nature]
- src: gallery/nir-himi-AjecvkfSHxA-unsplash.jpg
tags: [landscape, mountains, sky]
- src: gallery/rachel-mcdermott-0fN7Fxv1eWA-unsplash.jpg
tags: [portrait, black and white]
- src: gallery/tianlei-wu-g5o6T-PWT3g-unsplash.jpg
tags: [cat, animals]
- src: gallery/we-care-wild-zLweeVLU9Fo-unsplash.jpg
tags: [bison, animals]
- src: gallery/y-s-z90w7yStOkk-unsplash.jpg
tags: [frog, green, animals]

BIN
demo/config/photos/.DS_Store vendored Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.7 MiB

After

Width:  |  Height:  |  Size: 5.7 MiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 MiB

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

Before

Width:  |  Height:  |  Size: 8.4 MiB

After

Width:  |  Height:  |  Size: 8.4 MiB

View File

Before

Width:  |  Height:  |  Size: 706 KiB

After

Width:  |  Height:  |  Size: 706 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

49
demo/config/site.yaml Normal file
View File

@ -0,0 +1,49 @@
# This file is filled with the demo info
# Please change this by your settings
info:
title: Lumeex
subtitle: A minimalistic Gallery
description: A minimalistic Gallery
canonical: https://lumeex.djeex.fr
keywords: photography, lumen, demo, gallery, minimalistic
author: Djeex
google_analytics_id: G-XXXXXXX # optional
social:
instagram_url: https://www.instagram.com/
thumbnail: hero/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg
menu:
items:
- label: Home
href: /
- label: Nature
href: /?tag=Nature
- label: Landscape
href: /?tag=landscape
- label: Portrait
href: /?tag=portrait
- label: Animals
href: /?tag=animals
footer:
copyright: Copyright © 2025 Lumeex
legal_link: '/legals/'
legal_label: Legal notice
# Build parameters
build:
theme: modern # choose a theme in config/theme folder.
convert_images: true # use true to automatically convert images to webp small weight images.
resize_images: true # use true to automatically resize to width 1140px (maximum width used in the gallery)
# Change this by your legals
legals:
hoster_name: Djeex
hoster_adress: Paris, France
hoster_contact: contact@djeex.fr
intellectual_property:
- paragraph: "Users of this website are required to comply with the provisions of the French Data Protection Act (Loi Informatique et Libertés), the violation of which may result in criminal penalties. In particular, they must refrain from any collection or misuse of personal data accessible through the site, and more generally, from any act likely to infringe upon the privacy or reputation of individuals."
- paragraph: "The overall structure, as well as the software, texts, animated or still images, know-how, and all other components of the site, are the exclusive property of Lumeex"
- paragraph: "Any total or partial reproduction of this website, by any means whatsoever, without the express authorization of Lumeex, is prohibited and constitutes an infringement punishable under articles L.335-2 and following of the French Intellectual Property Code. The same applies to the databases appearing on the website, which are protected by the provisions of the law of July 1, 1998, implementing into the Intellectual Property Code the European directive of March 11, 1996, on the legal protection of databases."

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,63 @@
/*-----------------------------------*/
/* Modern theme for Lumeex */
/* https://git.djeex.fr/Djeex/lumeex */
/*-----------------------------------*/
.hero-background {
border-radius: 0 0 15px 15px;
}
img, tag {
border-radius: 15px;
}
.tag, .scroll-up, .back-button {
padding: 5px 10px;
background: rgb(245 245 245);
border-radius: 30px;
font-size: 15px;
}
.back-button {
padding: 8px 15px 10px 15px;
margin-left: 100px;
}
.tag:hover {
background: rgb(231, 231, 231);
color: var(--color-primary-dark);
}
.tag.active, .scroll-up:hover, .back-button:hover {
color: var(--color-background);
background: var(--color-primary-dark);
}
#footer {
box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
margin: auto;
}
@media (max-width: 768px) {
#footer {
box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
border-radius: 15px 15px 0 0;
max-width: 1140px;
margin: 0;
}
.tag {
font-size: 12px;
}
.hero-background {
max-width: 90%;
margin: auto;
}
.back-button {
margin-left: 0px;
}
}

View File

@ -0,0 +1,32 @@
#-----------------------------------#
# Modern theme for Lumeex #
# https://git.djeex.fr/Djeex/lumeex #
#-----------------------------------#
colors:
primary: '#0065a1'
primary_dark: '#005384'
secondary: '#00b0f0'
accent: '#ffc700'
text_dark: '#616161'
background: '#fff'
browser_color: '#fff'
favicon:
path: favicon.png
google_fonts:
- family: Lato
weights:
- '200'
- '400'
- '700'
- family: Montserrat
weights:
- '200'
- '400'
- '700'
fonts:
primary:
name: Lato
fallback: sans-serif
secondary:
name: Montserrat
fallback: serif

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,13 @@
/*-----------------------------------*/
/* Typewriter theme for Lumeex */
/* https://git.djeex.fr/Djeex/lumeex */
/*-----------------------------------*/
.tag {
line-height: 0.5em;
}
.tags {
gap:0px;
}

View File

@ -0,0 +1,21 @@
#-----------------------------------#
# Typewriter theme for Lumeex #
# https://git.djeex.fr/Djeex/lumeex #
#-----------------------------------#
colors:
primary: '#0065a1'
primary_dark: '#005384'
secondary: '#00b0f0'
accent: '#ffc700'
text_dark: '#616161'
background: '#fff'
browser_color: '#fff'
favicon:
path: favicon.png
fonts:
primary:
name: Trixie
fallback: sans-serif
secondary:
name: Trixie
fallback: serif

86
docker/.sh/entrypoint.sh Normal file
View File

@ -0,0 +1,86 @@
#!/bin/bash
set -e
CYAN="\033[1;36m"
NC="\033[0m"
copy_default_config() {
echo "Checking configuration directory..."
if [ ! -d "/app/config" ]; then
mkdir -p /app/config
fi
echo "Checking if default config files need to be copied..."
files_copied=false
for file in /app/default/*; do
filename=$(basename "$file")
target="/app/config/$filename"
if [ ! -e "$target" ]; then
echo "Copying default config file: $filename"
cp -r "$file" "$target"
files_copied=true
fi
done
if [ "$files_copied" = true ]; then
echo "Default configuration files copied successfully."
else
echo "No default files needed to be copied."
fi
}
start_server() {
# Clean up old FIFOs
[ -p /tmp/build_logs_fifo ] && rm /tmp/build_logs_fifo
[ -p /tmp/build_logs_fifo2 ] && rm /tmp/build_logs_fifo2
mkfifo /tmp/build_logs_fifo
mkfifo /tmp/build_logs_fifo2
cat /tmp/build_logs_fifo >&2 &
cat /tmp/build_logs_fifo2 >&2 &
echo "Starting preview HTTP server on port 3000..."
python3 -u -m http.server 3000 -d /app/output &
SERVER_PID=$!
echo "Starting Lumeex Flask webui..."
python3 -u -m src.py.webui.webui &
WEBUI_PID=$!
trap "echo 'Stopping servers...'; kill -TERM $SERVER_PID $WEBUI_PID 2>/dev/null; wait $SERVER_PID $WEBUI_PID; exit 0" SIGINT SIGTERM
wait $SERVER_PID
wait $WEBUI_PID
}
VERSION=$(cat VERSION)
if [ $# -eq 0 ]; then
echo -e "${CYAN}╭───────────────────────────────────────────╮${NC}"
echo -e "${CYAN}${NC} Lum${CYAN}eex${NC} - Version ${VERSION}${NC} ${CYAN}${NC}"
echo -e "${CYAN}├───────────────────────────────────────────┤${NC}"
echo -e "${CYAN}${NC} Source: https://git.djeex.fr/Djeex/lumeex ${CYAN}${NC}"
echo -e "${CYAN}${NC} Mirror: https://github.com/Djeex/lumeex ${CYAN}${NC}"
echo -e "${CYAN}${NC} Documentation: https://lumeex.djeex.fr ${CYAN}${NC}"
echo -e "${CYAN}╰───────────────────────────────────────────╯${NC}"
copy_default_config
start_server
fi
case "$1" in
build)
echo "Running build.py..."
python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo
;;
gallery)
echo "Running gallery.py..."
python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2
;;
*)
echo "Unknown command: $1"
exec "$@"
;;
esac

View File

@ -0,0 +1,11 @@
services:
lumeex:
container_name: lmx
build: ..
volumes:
- ../config:/app/config # mount config directory
- ../output:/app/output # mount output directory
ports:
- "3000:3000"
- "5000:5000"

View File

@ -1,81 +1,7 @@
import yaml
import os
from pathlib import Path
# YAML file paths
GALLERY_YAML = "config/gallery.yaml"
SITE_YAML = "config/site.yaml"
# Image directories
GALLERY_DIR = Path("config/photos/gallery")
HERO_DIR = Path("config/photos/hero")
def load_yaml(path):
print(f"[→] Loading {path}...")
if not os.path.exists(path):
print(f"[✗] File not found: {path}")
return {}
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
images = data.get("images") or []
print(f"[✓] Loaded {len(images)} image(s) from {path}")
return data
def save_yaml(data, path):
with open(path, "w", encoding="utf-8") as f:
yaml.dump(data, f, sort_keys=False, allow_unicode=True)
print(f"[✓] Saved updated YAML to {path}")
def get_all_image_paths(directory):
return sorted([
str(p.relative_to(directory.parent)).replace("\\", "/")
for p in directory.rglob("*")
if p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]
])
def update_gallery():
print("\n=== Updating gallery.yaml ===")
gallery = load_yaml(GALLERY_YAML)
gallery_images = gallery.get("images") or []
known = {img["src"] for img in gallery_images}
all_images = get_all_image_paths(GALLERY_DIR)
new_images = [
{"src": path, "tags": []}
for path in all_images
if path not in known
]
if new_images:
gallery_images.extend(new_images)
gallery["images"] = gallery_images
save_yaml(gallery, GALLERY_YAML)
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml")
else:
print("[✓] No new images to add to gallery.yaml")
def update_hero():
print("\n=== Updating site.yaml (hero section) ===")
site = load_yaml(SITE_YAML)
hero_section = site.get("hero", {})
hero_images = hero_section.get("images") or []
known = {img["src"] for img in hero_images}
all_images = get_all_image_paths(HERO_DIR)
new_images = [
{"src": path}
for path in all_images
if path not in known
]
if new_images:
hero_images.extend(new_images)
site["hero"]["images"] = hero_images
save_yaml(site, SITE_YAML)
print(f"[✓] Added {len(new_images)} new image(s) to site.yaml (hero)")
else:
print("[✓] No new images to add to site.yaml")
import logging
from src.py.builder.gallery_builder import update_gallery, update_hero
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")
update_gallery()
update_hero()
update_hero()

166
illustration/logo.svg Normal file
View File

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 6031 1000">
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) -->
<defs>
<style>
.st0 {
fill: #55c3ec;
}
.st1 {
fill: url(#Dégradé_sans_nom_265);
stroke: url(#Dégradé_sans_nom_33);
}
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8, .st9, .st10, .st11, .st12 {
stroke-miterlimit: 10;
}
.st2 {
fill: url(#Dégradé_sans_nom_269);
stroke: url(#Dégradé_sans_nom_334);
}
.st3 {
fill: url(#Dégradé_sans_nom_268);
stroke: url(#Dégradé_sans_nom_333);
}
.st4 {
fill: url(#Dégradé_sans_nom_266);
stroke: url(#Dégradé_sans_nom_331);
}
.st5 {
fill: url(#Dégradé_sans_nom_267);
stroke: url(#Dégradé_sans_nom_332);
}
.st13 {
fill: url(#Dégradé_sans_nom_261);
}
.st14 {
fill: url(#Dégradé_sans_nom_262);
}
.st15 {
fill: url(#Dégradé_sans_nom_264);
}
.st16 {
fill: url(#Dégradé_sans_nom_263);
}
.st6 {
fill: url(#Dégradé_sans_nom_2616);
stroke: url(#Dégradé_sans_nom_3311);
}
.st7 {
fill: url(#Dégradé_sans_nom_2615);
stroke: url(#Dégradé_sans_nom_3310);
}
.st17 {
fill: #fff;
}
.st18 {
fill: url(#Dégradé_sans_nom_26);
}
.st8 {
fill: url(#Dégradé_sans_nom_2610);
stroke: url(#Dégradé_sans_nom_335);
}
.st9 {
fill: url(#Dégradé_sans_nom_2613);
stroke: url(#Dégradé_sans_nom_338);
}
.st10 {
fill: url(#Dégradé_sans_nom_2614);
stroke: url(#Dégradé_sans_nom_339);
}
.st11 {
fill: url(#Dégradé_sans_nom_2611);
stroke: url(#Dégradé_sans_nom_336);
}
.st12 {
fill: url(#Dégradé_sans_nom_2612);
stroke: url(#Dégradé_sans_nom_337);
}
</style>
<linearGradient id="Dégradé_sans_nom_26" data-name="Dégradé sans nom 26" x1="373.2" y1="159.5" x2="625.1" y2="411.5" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_261" data-name="Dégradé sans nom 26" x1="143.1" y1="200.5" x2="395" y2="452.4" gradientTransform="translate(30.8 109.3)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_262" data-name="Dégradé sans nom 26" x1="81.3" y1="60.6" x2="333.2" y2="312.5" gradientTransform="translate(187.1 873.6) rotate(-90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_263" data-name="Dégradé sans nom 26" x1="-44.4" y1="16.5" x2="207.5" y2="268.4" gradientTransform="translate(705.4 808.2) rotate(-180)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_264" data-name="Dégradé sans nom 26" x1="-67.9" y1="-58.9" x2="184" y2="193" gradientTransform="translate(770.9 385.1) rotate(90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_265" data-name="Dégradé sans nom 26" x1="74.5" y1="560.2" x2="106.2" y2="591.8" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_33" data-name="Dégradé sans nom 33" x1="74.2" y1="559.9" x2="106.5" y2="592.2" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".5"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_266" data-name="Dégradé sans nom 26" x1="158.2" y1="648.8" x2="176.7" y2="667.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_331" data-name="Dégradé sans nom 33" x1="157.8" y1="648.4" x2="177.1" y2="667.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_267" data-name="Dégradé sans nom 26" x1="210.2" y1="714.7" x2="249.8" y2="754.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_332" data-name="Dégradé sans nom 33" x1="209.9" y1="714.3" x2="250.2" y2="754.6" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_268" data-name="Dégradé sans nom 26" x1="-54" y1="72.2" x2="-14.4" y2="111.8" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_333" data-name="Dégradé sans nom 33" x1="-54.4" y1="71.9" x2="-14.1" y2="112.2" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_269" data-name="Dégradé sans nom 26" x1="485.1" y1="-9.2" x2="503.7" y2="9.4" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_334" data-name="Dégradé sans nom 33" x1="484.8" y1="-9.6" x2="504" y2="9.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2610" data-name="Dégradé sans nom 26" x1="825.1" y1="682.3" x2="856.8" y2="713.9" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_335" data-name="Dégradé sans nom 33" x1="824.8" y1="681.9" x2="857.1" y2="714.3" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2611" data-name="Dégradé sans nom 26" x1="308.5" y1="356.5" x2="340.3" y2="388.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_336" data-name="Dégradé sans nom 33" x1="308.1" y1="356.2" x2="340.7" y2="388.7" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2612" data-name="Dégradé sans nom 26" x1="540.4" y1="450.4" x2="559" y2="469" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_337" data-name="Dégradé sans nom 33" x1="540.1" y1="450" x2="559.4" y2="469.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2613" data-name="Dégradé sans nom 26" x1="-336.4" y1="326.9" x2="-296.8" y2="366.5" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_338" data-name="Dégradé sans nom 33" x1="-336.7" y1="326.5" x2="-296.4" y2="366.8" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2614" data-name="Dégradé sans nom 26" x1="115" y1="-29.9" x2="133.6" y2="-11.3" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_339" data-name="Dégradé sans nom 33" x1="114.7" y1="-30.2" x2="134" y2="-11" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2615" data-name="Dégradé sans nom 26" x1="94.5" y1="304.2" x2="124.4" y2="334" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3310" data-name="Dégradé sans nom 33" x1="94.2" y1="303.8" x2="124.7" y2="334.4" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2616" data-name="Dégradé sans nom 26" x1="435.8" y1="254.1" x2="454.4" y2="272.7" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3311" data-name="Dégradé sans nom 33" x1="435.5" y1="253.8" x2="454.8" y2="273.1" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_33"/>
</defs>
<g id="Calque_1">
<circle class="st17" cx="499.5" cy="499.5" r="499.5"/>
<g>
<path class="st17" d="M1404,957.4V45h191v755h399v157.4h-590Z"/>
<path class="st17" d="M2321.5,971.3c-49.3,0-91.5-10.2-126.5-30.7-35-20.4-61.6-49.6-79.7-87.6-18.1-37.9-27.2-83.2-27.2-135.9v-437.6h184.6v399c0,44.3,10.4,78.6,31.3,103.1,20.9,24.5,51.9,36.7,93.3,36.7s39.3-3.6,56-10.7c16.6-7.2,30.9-17.4,42.7-30.7,11.8-13.3,20.9-29.1,27.2-47.4s9.5-38.5,9.5-60.4v-389.5h184.6v677.8h-184.6v-111.9h-4.4c-11.4,25.7-26.7,48.1-45.8,67-19.2,19-42.2,33.5-68.9,43.6-26.8,10.1-57.4,15.2-92,15.2Z"/>
<path class="st17" d="M2837.5,957.4V279.6h184.6v113.8h3.8c13.9-38.8,37.6-69.8,71.1-93,33.5-23.2,73-34.8,118.6-34.8s60.1,5.5,85.4,16.4c25.3,11,46.7,26.8,64.2,47.4,17.5,20.7,29.8,46,37,75.9h3.8c10.1-28.7,25.4-53.4,45.8-74.3,20.4-20.9,44.7-37,72.7-48.4,28-11.4,58.7-17.1,92-17.1s83,9.5,116.3,28.5c33.3,19,59.2,45.5,77.8,79.7,18.5,34.1,27.8,74.2,27.8,120.1v463.5h-184.6v-416.7c0-26.6-4.3-48.8-13-66.7-8.6-17.9-21.1-31.6-37.3-41.1-16.2-9.5-36.4-14.2-60.4-14.2s-43.5,5.4-61,16.1c-17.5,10.7-31.1,25.6-40.8,44.6-9.7,19-14.5,41.1-14.5,66.4v411.6h-177.7v-422.4c0-24.4-4.4-45.3-13.3-62.6-8.9-17.3-21.4-30.6-37.6-39.8-16.2-9.3-35.7-13.9-58.5-13.9s-43.6,5.6-61.3,16.8c-17.7,11.2-31.5,26.5-41.4,45.8-9.9,19.4-14.9,41.7-14.9,67v409.1h-184.6Z"/>
<path class="st0" d="M4264,971.3c-69.1,0-128.6-14.2-178.3-42.7-49.7-28.5-88.1-69-115.1-121.7-27-52.7-40.5-115.1-40.5-187.2v-.6c0-72.1,13.5-134.6,40.5-187.5,27-52.9,64.8-93.8,113.5-122.7,48.7-28.9,106.1-43.3,172.3-43.3s123.4,14,171.7,42c48.3,28,85.6,67.6,111.9,118.6,26.3,51,39.5,110.7,39.5,178.9v57.5h-558.3v-117h471.1l-87.9,109.4v-71.5c0-39.6-6.1-72.8-18.3-99.6-12.2-26.8-29.2-46.9-50.9-60.4-21.7-13.5-46.9-20.2-75.6-20.2s-54.1,7-76.2,20.9c-22.1,13.9-39.4,34.3-51.9,61-12.4,26.8-18.7,59.5-18.7,98.3v72.1c0,37.1,6.2,68.9,18.7,95.5,12.4,26.6,30.2,46.9,53.4,61,23.2,14.1,50.8,21.2,82.8,21.2s47.2-4,65.8-12c18.5-8,33.7-18.1,45.5-30.4,11.8-12.2,19.8-24.7,24-37.3l1.3-3.8h169.5l-1.9,7c-5.1,24.9-15,50-29.7,75.2-14.8,25.3-34.7,48.5-59.8,69.6-25.1,21.1-55.6,37.9-91.7,50.6-36,12.6-78.3,19-126.8,19Z"/>
<path class="st0" d="M4982.9,971.3c-69.1,0-128.6-14.2-178.3-42.7-49.7-28.5-88.1-69-115.1-121.7-27-52.7-40.5-115.1-40.5-187.2v-.6c0-72.1,13.5-134.6,40.5-187.5,27-52.9,64.8-93.8,113.5-122.7,48.7-28.9,106.1-43.3,172.3-43.3s123.4,14,171.7,42c48.3,28,85.6,67.6,111.9,118.6,26.3,51,39.5,110.7,39.5,178.9v57.5h-558.3v-117h471.1l-87.9,109.4v-71.5c0-39.6-6.1-72.8-18.3-99.6-12.2-26.8-29.2-46.9-50.9-60.4-21.7-13.5-46.9-20.2-75.6-20.2s-54.1,7-76.2,20.9c-22.1,13.9-39.4,34.3-51.9,61-12.4,26.8-18.7,59.5-18.7,98.3v72.1c0,37.1,6.2,68.9,18.7,95.5,12.4,26.6,30.2,46.9,53.4,61,23.2,14.1,50.8,21.2,82.8,21.2s47.2-4,65.8-12c18.5-8,33.7-18.1,45.5-30.4,11.8-12.2,19.8-24.7,24-37.3l1.3-3.8h169.5l-1.9,7c-5.1,24.9-15,50-29.7,75.2-14.8,25.3-34.7,48.5-59.8,69.6-25.1,21.1-55.6,37.9-91.7,50.6s-78.3,19-126.8,19Z"/>
<path class="st0" d="M5337,957.4l212.5-337.7-210.6-340.2h208l122,228.9h3.8l120.1-228.9h201.1l-211.8,335.1,209.9,342.7h-200.4l-129-234.6h-3.8l-127.1,234.6h-194.8Z"/>
</g>
</g>
<g id="Calque_2">
<g id="Calque_3">
<ellipse class="st18" cx="499.2" cy="285.5" rx="139.8" ry="209.5"/>
<ellipse class="st13" cx="299.9" cy="435.8" rx="139.8" ry="209.5" transform="translate(-207.3 586.3) rotate(-72)"/>
<ellipse class="st14" cx="373.6" cy="666.3" rx="209.5" ry="139.8" transform="translate(-385.1 576.9) rotate(-54)"/>
<ellipse class="st16" cx="623.9" cy="665.8" rx="139.8" ry="209.5" transform="translate(-272.2 493.9) rotate(-36)"/>
<ellipse class="st15" cx="703.9" cy="443.1" rx="209.5" ry="139.8" transform="translate(-94.9 211.2) rotate(-16)"/>
<circle class="st1" cx="90.4" cy="576" r="22.4"/>
<circle class="st4" cx="175.6" cy="607.9" r="13.1"/>
<circle class="st5" cx="140.8" cy="691.6" r="28"/>
<circle class="st3" cx="829.7" cy="602.6" r="28"/>
<circle class="st2" cx="908.9" cy="562.1" r="13.1"/>
<circle class="st8" cx="840.9" cy="698.1" r="22.4"/>
<circle class="st11" cx="466.1" cy="876.5" r="22.5"/>
<circle class="st12" cx="538.6" cy="839.8" r="13.1"/>
<circle class="st9" cx="686.1" cy="170.1" r="28"/>
<circle class="st10" cx="733.7" cy="247.7" r="13.1"/>
<circle class="st7" cx="236.9" cy="206.5" r="21.1"/>
<circle class="st6" cx="315.4" cy="164.9" r="13.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

View File

@ -1,2 +1,3 @@
pyyaml
pillow
pillow
flask

View File

@ -17,24 +17,11 @@ const setupLoader = () => {
window.addEventListener('load', () => {
setTimeout(() => {
const loader = document.querySelector('.page-loader');
if (loader) {
loader.classList.add('hidden');
}
if (loader) loader.classList.add('hidden');
}, 50);
});
};
// Gallery randomizer to shuffle gallery sections on page load
const shuffleGallery = () => {
const gallery = document.querySelector('.gallery');
if (!gallery) return;
const sections = Array.from(gallery.querySelectorAll('.section'));
while (sections.length) {
const randomIndex = Math.floor(Math.random() * sections.length);
gallery.appendChild(sections.splice(randomIndex, 1)[0]);
}
};
// Hero background randomizer
const randomizeHeroBackground = () => {
const heroBg = document.querySelector(".hero-background");
@ -45,6 +32,7 @@ const randomizeHeroBackground = () => {
if (images.length === 0) return;
let currentIndex = Math.floor(Math.random() * images.length);
heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`;
if (images.length < 2) return; // <-- Prevent interval if only one image
setInterval(() => {
let nextIndex;
do {
@ -65,32 +53,87 @@ const randomizeHeroBackground = () => {
.catch(console.error);
};
// Gallery randomizer to shuffle gallery sections on page load
const shuffleGallery = () => {
const gallery = document.querySelector('.gallery');
if (!gallery) return;
const sections = Array.from(gallery.querySelectorAll('.section'));
while (sections.length) {
const randomIndex = Math.floor(Math.random() * sections.length);
gallery.appendChild(sections.splice(randomIndex, 1)[0]);
}
};
// Tags filter functionality
const setupTagFilter = () => {
const galleryContainer = document.querySelector('#gallery');
const allSections = document.querySelectorAll('.section[data-tags]');
const allTags = document.querySelectorAll('.tag');
let activeTags = [];
let lastClickedTag = null; // remembers the last clicked tag
let lastClickedSection = null; // remembers the last clicked section (photo)
const applyFilter = () => {
let filteredSections = [];
let matchingSection = null;
allSections.forEach((section) => {
const sectionTags = section.dataset.tags.toLowerCase().split(/\s+/);
const hasAllTags = activeTags.every((tag) => sectionTags.includes(tag));
section.style.display = hasAllTags ? '' : 'none';
if (hasAllTags) {
if (lastClickedSection === section) {
matchingSection = section;
} else {
filteredSections.push(section);
}
}
});
// Remove all filtered sections from DOM before reordering
if (galleryContainer) {
[matchingSection, ...filteredSections].forEach(section => {
if (section && galleryContainer.contains(section)) {
galleryContainer.removeChild(section);
}
});
if (matchingSection) {
galleryContainer.prepend(matchingSection);
}
filteredSections.forEach(section => {
galleryContainer.appendChild(section);
});
}
// Update tag styles
allTags.forEach((tagEl) => {
const tagText = tagEl.textContent.replace('#', '').toLowerCase();
tagEl.classList.toggle('active', activeTags.includes(tagText));
});
// Update the URL
const base = window.location.pathname;
const query = activeTags.length > 0 ? `?tag=${activeTags.join(',')}` : '';
window.history.pushState({}, '', base + query);
// Scroll to the gallery
if (galleryContainer) {
galleryContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
allTags.forEach((tagEl) => {
tagEl.addEventListener('click', () => {
const tagText = tagEl.textContent.replace('#', '').toLowerCase();
activeTags = activeTags.includes(tagText)
? activeTags.filter((t) => t !== tagText)
: [...activeTags, tagText];
lastClickedTag = tagText; // remembers the last clicked tag
lastClickedSection = tagEl.closest('.section'); // remembers the last clicked section
if (activeTags.includes(tagText)) {
activeTags = activeTags.filter((t) => t !== tagText);
} else {
activeTags.push(tagText);
}
applyFilter();
});
});
@ -100,19 +143,17 @@ const setupTagFilter = () => {
const urlTags = params.get('tag');
if (urlTags) {
activeTags = urlTags.split(',').map((t) => t.toLowerCase());
lastClickedTag = activeTags[activeTags.length - 1] || null;
lastClickedSection = null; // No section selected from URL
applyFilter();
}
});
};
// Disable right-click context menu and image dragging
// Disable right click and drag
const disableRightClickAndDrag = () => {
document.addEventListener("contextmenu", (e) => e.preventDefault());
document.addEventListener("dragstart", (e) => {
if (e.target.tagName === "IMG") {
e.preventDefault();
}
});
document.addEventListener('contextmenu', (e) => e.preventDefault());
document.addEventListener('dragstart', (e) => e.preventDefault());
};
// Scroll-to-top button functionality
@ -149,4 +190,4 @@ document.addEventListener("DOMContentLoaded", () => {
fixNavSeparators();
});
window.addEventListener('resize', fixNavSeparators);
window.addEventListener('resize', fixNavSeparators);

View File

@ -333,8 +333,12 @@ h2 {
font-size: 22px;
}
/* Sections */
.gallery {
padding-top: 15px;
}
/* Sections */
.section {
max-width: 1140px;
margin:auto;
@ -342,7 +346,7 @@ h2 {
.section img {
width:100%;
margin: 0 0 60px 0;
margin: 5px 0 60px 0;
}
.text-block {
@ -486,6 +490,10 @@ h2 {
font-size:18px;
}
.section img {
margin: 0px 0 60px 0;
}
.tag {
font-size: 14px;
}
@ -504,4 +512,9 @@ h2 {
padding-left: 0;
margin-top: 60px;
}
.gallery {
margin: 10% 5% 0 5%;
padding-top: 15px;
}
}

View File

@ -3,6 +3,7 @@ from pathlib import Path
from shutil import copyfile
def generate_css_variables(colors_dict, output_path):
"""Generate css variables for theme colors"""
css_lines = [":root {"]
for key, value in colors_dict.items():
css_lines.append(f" --color-{key.replace('_', '-')}: {value};")
@ -13,6 +14,7 @@ def generate_css_variables(colors_dict, output_path):
logging.info(f"[✓] CSS variables written to {output_path}")
def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
"""Generate css variables fonts"""
font_files = list(fonts_dir.glob("*"))
font_faces = {}
preload_links = []
@ -57,6 +59,7 @@ def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
return preload_links
def generate_google_fonts_link(fonts):
"""Generate src link for Google fonts"""
if not fonts:
return ""
families = []

View File

@ -0,0 +1,114 @@
import yaml
import os
from pathlib import Path
# YAML file paths
GALLERY_YAML = "config/gallery.yaml"
# Image directories
GALLERY_DIR = Path("config/photos/gallery")
HERO_DIR = Path("config/photos/hero")
def load_yaml(path):
"""Load gallery config .yaml file"""
print(f"[→] Loading {path}...")
if not os.path.exists(path):
print(f"[✗] File not found: {path}")
return {}
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
images = data.get("images", []) or []
print(f"[✓] Loaded {len(images)} image(s) from {path}")
return data
def save_yaml(data, path):
"""Save modified gallery config .yaml file"""
with open(path, "w", encoding="utf-8") as f:
yaml.dump(data, f, sort_keys=False, allow_unicode=True)
print(f"[✓] Saved updated YAML to {path}")
def get_all_image_paths(directory):
"""Get the path to record for builded site"""
return sorted([
str(p.relative_to(directory.parent)).replace("\\", "/")
for p in directory.rglob("*")
if p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]
])
def update_gallery():
"""Update the gallery photo list"""
print("\n=== Updating gallery.yaml (gallery section) ===")
gallery = load_yaml(GALLERY_YAML)
# Access the 'gallery' section within the gallery data, or initialize it if it doesn't exist
gallery_section = gallery.get("gallery", {})
# Access the 'images' list within the 'gallery' section, or initialize it if it doesn't exist
gallery_images = gallery_section.get("images", [])
all_images = set(get_all_image_paths(GALLERY_DIR))
known_images = {img["src"] for img in gallery_images}
# Add new images
new_images = [
{"src": path, "tags": []}
for path in all_images
if path not in known_images
]
if new_images:
gallery_images.extend(new_images)
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (gallery)")
# Remove deleted images
deleted_images = known_images - all_images
if deleted_images:
gallery_images = [img for img in gallery_images if img["src"] not in deleted_images]
print(f"[✓] Removed {len(deleted_images)} deleted image(s) from gallery.yaml (gallery)")
# Update the 'gallery' section with the modified 'images' list
gallery_section["images"] = gallery_images
gallery["gallery"] = gallery_section
save_yaml(gallery, GALLERY_YAML)
if not new_images and not deleted_images:
print("[✓] No changes to gallery.yaml (gallery)")
def update_hero():
"""Update the hero photo list"""
print("\n=== Updating gallery.yaml (hero section) ===")
gallery = load_yaml(GALLERY_YAML)
# Access the 'hero' section within the gallery data, or initialize it if it doesn't exist
hero_section = gallery.get("hero", {})
# Access the 'images' list within the 'hero' section, or initialize it if it doesn't exist
hero_images = hero_section.get("images", [])
all_images = set(get_all_image_paths(HERO_DIR))
known_images = {img["src"] for img in hero_images}
# Add new images
new_images = [
{"src": path}
for path in all_images
if path not in known_images
]
if new_images:
hero_images.extend(new_images)
print(f"[✓] Added {len(new_images)} new image(s) to gallery.yaml (hero)")
# Remove deleted images
deleted_images = known_images - all_images
if deleted_images:
hero_images = [img for img in hero_images if img["src"] not in deleted_images]
print(f"[✓] Removed {len(deleted_images)} deleted image(s) from gallery.yaml (hero)")
# Update the 'hero' section with the modified 'images' list
hero_section["images"] = hero_images
gallery["hero"] = hero_section
save_yaml(gallery, GALLERY_YAML)
if not new_images and not deleted_images:
print("[✓] No changes to gallery.yaml (hero)")

View File

@ -3,6 +3,7 @@ import logging
from pathlib import Path
def render_template(template_path, context):
"""Render html templates"""
with open(template_path, encoding="utf-8") as f:
content = f.read()
for key, value in context.items():
@ -11,6 +12,7 @@ def render_template(template_path, context):
return content
def render_gallery_images(images):
"""Render the photo gallery"""
html = ""
for img in images:
tags = " ".join(img.get("tags", []))
@ -23,9 +25,11 @@ def render_gallery_images(images):
"""
return html
def generate_gallery_json_from_images(images, output_path):
def generate_gallery_json_from_images(images, output_dir):
"""Generte the hero carrousel photo list"""
try:
img_list = [img["src"] for img in images]
output_path = output_dir / "data" / "gallery.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(img_list, f, indent=2)
@ -33,20 +37,36 @@ def generate_gallery_json_from_images(images, output_path):
except Exception as e:
logging.error(f"[✗] Error generating gallery JSON: {e}")
def generate_robots_txt(canonical_url, allowed_paths):
def generate_robots_txt(canonical_url, allowed_paths, output_dir):
"""Generate the robot.txt"""
robots_lines = ["User-agent: *"]
for path in allowed_paths:
robots_lines.append(f"Allow: {path}")
robots_lines.append("Disallow: /")
robots_lines.append("")
robots_lines.append(f"Sitemap: {canonical_url}/sitemap.xml")
content = "\n".join(robots_lines)
output_path = Path(".output/robots.txt")
with open(output_path, "w", encoding="utf-8") as f:
f.write(content)
logging.info(f"[✓] robots.txt generated at {output_path}")
def generate_sitemap_xml(canonical_url, allowed_paths):
# Block everything by default
robots_lines.append("Disallow: /")
# Explicitly allow certain paths
for path in allowed_paths:
if not path.startswith("/"):
path = "/" + path
robots_lines.append(f"Allow: {path}")
robots_lines.append("")
robots_lines.append(f"Sitemap: {canonical_url.rstrip('/')}/sitemap.xml")
content = "\n".join(robots_lines)
output_path = Path(output_dir) / "robots.txt"
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(content)
logging.info(f"[✓] robots.txt generated at {output_path}")
except Exception as e:
logging.error(f"[✗] Failed to write robots.txt: {e}")
def generate_sitemap_xml(canonical_url, allowed_paths, output_dir):
"""Generate the sitemap"""
urlset_start = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
urlset_end = '</urlset>\n'
urls = ""
@ -54,7 +74,7 @@ def generate_sitemap_xml(canonical_url, allowed_paths):
loc = canonical_url.rstrip("/") + path
urls += f" <url>\n <loc>{loc}</loc>\n </url>\n"
sitemap_content = urlset_start + urls + urlset_end
output_path = Path(".output/sitemap.xml")
output_path = output_dir / "sitemap.xml"
with open(output_path, "w", encoding="utf-8") as f:
f.write(sitemap_content)
logging.info(f"[✓] sitemap.xml generated at {output_path}")

View File

@ -0,0 +1,123 @@
import logging
from pathlib import Path
from PIL import Image, features
from shutil import copyfile
def convert_and_resize_image(input_path, output_path, resize=True, max_width=1140):
"""Convert an image to WebP (or JPEG fallback) and optionally resize it."""
try:
if not input_path.exists():
logging.error(f"[✗] Image file not found: {input_path}")
return
img = Image.open(input_path)
if img.mode != "RGB":
img = img.convert("RGB")
if resize:
width, height = img.size
if width > max_width:
new_height = int((max_width / width) * height)
img = img.resize((max_width, new_height), Image.LANCZOS)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Check WebP support, otherwise fallback to JPEG
fmt = "WEBP" if features.check("webp") else "JPEG"
if fmt == "JPEG":
output_path = output_path.with_suffix(".jpg")
img.save(output_path, fmt, quality=90 if fmt == "JPEG" else 100)
logging.info(f"[✓] Processed image: {input_path}{output_path}")
except Exception as e:
logging.error(f"[✗] Error processing image {input_path}: {e}")
def process_images(images, resize_images, img_dir, build_dir):
"""Process a list of image references and update paths to optimized versions."""
for img in images:
src_path = img_dir / img["src"]
webp_path = build_dir / "img" / Path(img["src"]).with_suffix(".webp")
convert_and_resize_image(src_path, webp_path, resize=resize_images)
if webp_path.exists():
img["src"] = str(Path(img["src"]).with_suffix(".webp"))
else:
# Fallback if WebP not created
jpg_path = webp_path.with_suffix(".jpg")
if jpg_path.exists():
img["src"] = str(Path(img["src"]).with_suffix(".jpg"))
def copy_original_images(images, img_dir, build_dir):
"""Copy original image files without processing."""
for img in images:
src_path = img_dir / img["src"]
dest_path = build_dir / "img" / img["src"]
try:
if not src_path.exists():
logging.error(f"[✗] Original image not found: {src_path}")
continue
dest_path.parent.mkdir(parents=True, exist_ok=True)
copyfile(src_path, dest_path)
logging.info(f"[✓] Copied original: {src_path}{dest_path}")
except Exception as e:
logging.error(f"[✗] Error copying {src_path}: {e}")
def get_favicon_path(theme_vars, theme_dir):
"""Retrieve the favicon path from theme variables, ensuring it exists."""
fav_path = theme_vars.get("favicon", {}).get("path")
if not fav_path:
logging.warning("[~] No favicon path defined in theme.yaml")
return None
path = Path(fav_path)
if not path.is_absolute():
path = theme_dir / path
if not path.exists():
logging.error(f"[✗] Favicon not found: {path}")
return None
return path
def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
"""Generate multiple PNG favicons from a single source image."""
logo_path = get_favicon_path(theme_vars, theme_dir)
if not logo_path:
logging.warning("[~] PNG favicons not generated.")
return
try:
output_dir.mkdir(parents=True, exist_ok=True)
specs = [
(32, "favicon-32.png"), (96, "favicon-96.png"), (128, "favicon-128.png"),
(192, "favicon-192.png"), (196, "favicon-196.png"),
(152, "favicon-152.png"), (180, "favicon-180.png")
]
img = Image.open(logo_path).convert("RGBA")
for size, name in specs:
img.resize((size, size), Image.LANCZOS).save(output_dir / name, format="PNG")
logging.info(f"[✓] PNG favicons generated in {output_dir}")
except Exception as e:
logging.error(f"[✗] Error generating PNG favicons: {e}")
def generate_favicon_ico(theme_vars, theme_dir, output_path):
"""Generate a multi-size favicon.ico from a source image."""
logo_path = get_favicon_path(theme_vars, theme_dir)
if not logo_path:
logging.warning("[~] favicon.ico not generated.")
return
try:
img = Image.open(logo_path).convert("RGBA")
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(output_path, format="ICO", sizes=[(16, 16), (32, 32), (48, 48)])
logging.info(f"[✓] favicon.ico generated in {output_path}")
except Exception as e:
logging.error(f"[✗] Error generating favicon.ico: {e}")

View File

@ -0,0 +1,191 @@
import logging
from datetime import datetime
from pathlib import Path
from shutil import copyfile
from PIL import Image
from .utils import ensure_dir, copy_assets, load_yaml, load_theme_config
from .css_generator import generate_css_variables, generate_fonts_css, generate_google_fonts_link
from .image_processor import process_images, copy_original_images, convert_and_resize_image, generate_favicons_from_logo, generate_favicon_ico
from .html_generator import render_template, render_gallery_images, generate_gallery_json_from_images, generate_robots_txt, generate_sitemap_xml
# Configure logging to display only the messages
logging.basicConfig(level=logging.INFO, format='%(message)s')
# Define key directories used throughout the script
SRC_DIR = Path.cwd()
BUILD_DIR = SRC_DIR / "output"
TEMPLATE_DIR = SRC_DIR / "src/templates"
IMG_DIR = SRC_DIR / "config/photos"
JS_DIR = SRC_DIR / "src/public/js"
STYLE_DIR = SRC_DIR / "src/public/style"
GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
SITE_FILE = SRC_DIR / "config/site.yaml"
THEMES_DIR = SRC_DIR / "config/themes"
VERSION_FILE = SRC_DIR / "VERSION"
with open(VERSION_FILE, "r") as vf:
build_version = vf.read().strip()
def build():
logging.info("\n")
logging.info("=" * 24)
logging.info(f"🚀 Lumeex builder v{build_version}")
logging.info("=" * 24)
logging.info("\n === Starting build === ")
ensure_dir(BUILD_DIR)
copy_assets(JS_DIR, STYLE_DIR, BUILD_DIR)
# Defining build vars
build_date = datetime.now().strftime("%Y%m%d%H%M%S")
build_date_version = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
site_vars = load_yaml(SITE_FILE)
gallery_vars = load_yaml(GALLERY_FILE)
build_section = site_vars.get("build", {})
theme_name = site_vars.get("build", {}).get("theme", "default")
theme_vars, theme_dir = load_theme_config(theme_name, THEMES_DIR)
fonts_dir = theme_dir / "fonts"
theme_css_path = theme_dir / "theme.css"
canonical_url = site_vars.get("info", {}).get("canonical", "").rstrip("/")
canonical_home = f"{canonical_url}/"
canonical_legals = f"{canonical_url}/legals/"
# Copying theme.css if existing
if theme_css_path.exists():
dest_theme_css = BUILD_DIR / "style" / "theme.css"
dest_theme_css.parent.mkdir(parents=True, exist_ok=True)
copyfile(theme_css_path, dest_theme_css)
theme_css = f'<link rel="stylesheet" href="/style/theme.css?build_date={build_date}">'
logging.info(f"[✓] Theme CSS found, copied to build folder: {dest_theme_css}")
else:
theme_css = ""
logging.warning(f"[~] No theme.css found in {theme_css_path}, skipping theme CSS injection.")
preload_links = generate_fonts_css(fonts_dir, BUILD_DIR / "style" / "fonts.css", fonts_cfg=theme_vars.get("fonts"))
generate_css_variables(theme_vars.get("colors", {}), BUILD_DIR / "style" / "colors.css")
generate_favicons_from_logo(theme_vars, theme_dir, BUILD_DIR / "img" / "favicon")
generate_favicon_ico(theme_vars, theme_dir, BUILD_DIR / "favicon.ico")
# Converting and resizing images if enabled
convert_images = build_section.get("convert_images", True)
resize_images = build_section.get("resize_images", True)
logging.info(f"[~] convert_images = {convert_images}")
logging.info(f"[~] resize_images = {resize_images}")
hero_images = gallery_vars.get("hero", {}).get("images", [])
gallery_images = gallery_vars.get("gallery", {}).get("images", [])
if convert_images:
process_images(hero_images, resize_images, IMG_DIR, BUILD_DIR)
process_images(gallery_images, resize_images, IMG_DIR, BUILD_DIR)
else:
copy_original_images(hero_images, IMG_DIR, BUILD_DIR)
copy_original_images(gallery_images, IMG_DIR, BUILD_DIR)
if "hero" not in site_vars:
site_vars["hero"] = {} # Initialize an empty hero section
# Adding menu
menu_html = "\n".join(
f'<li class="nav-item appear"><a href="{item["href"]}">{item["label"]}</a></li>'
for item in site_vars.get("menu", {}).get("items", [])
)
site_vars["hero"]["menu_items"] = menu_html
if "footer" in site_vars:
site_vars["footer"]["menu_items"] = menu_html
# Adding Google fonts if existing
google_fonts_link = generate_google_fonts_link(theme_vars.get("google_fonts", []))
logging.info(f"[✓] Google Fonts link generated")
# Generating thumbnail
thumbnail_path = site_vars.get("social", {}).get("thumbnail")
if thumbnail_path:
src_thumb = IMG_DIR / thumbnail_path
dest_thumb_dir = BUILD_DIR / "img" / "social"
dest_thumb_dir.mkdir(parents=True, exist_ok=True)
dest_thumb = dest_thumb_dir / Path(thumbnail_path).name
try:
img = Image.open(src_thumb)
img = img.convert("RGB")
img = img.resize((1200, 630), Image.LANCZOS)
img.save(dest_thumb, "JPEG", quality=90)
logging.info(f"[✓] Thumbnail resized and saved to {dest_thumb}")
except Exception as e:
logging.error(f"[✗] Failed to process thumbnail: {e}")
else:
logging.warning("[~] No thumbnail found in social section")
# Defining head variables
head_vars = dict(site_vars.get("info", {}))
head_vars.update(theme_vars.get("colors", {}))
head_vars.update(site_vars.get("social", {}))
head_vars["thumbnail"] = f"/img/social/{Path(thumbnail_path).name}" if thumbnail_path else ""
head_vars["google_fonts_link"] = google_fonts_link
head_vars["font_preloads"] = "\n".join(preload_links)
head_vars["theme_css"] = theme_css
head_vars["build_date"] = build_date
head_vars["canonical"] = canonical_home
# Render the home page
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
hero = render_template(TEMPLATE_DIR / "hero.html", {**site_vars["hero"], **head_vars})
footer = render_template(TEMPLATE_DIR / "footer.html", {**site_vars.get("footer", {}), **head_vars})
gallery_html = render_gallery_images(gallery_images)
gallery = render_template(TEMPLATE_DIR / "gallery.html", {"gallery_images": gallery_html})
signature = f"<!-- Build with Lumeex {build_version} | https://git.djeex.fr/Djeex/lumeex | {build_date_version} -->"
body = f"""
<body>
<div class="page-loader"><div class="spinner"></div></div>
{hero}
{gallery}
{footer}
</body>
"""
output_file = BUILD_DIR / "index.html"
with open(output_file, "w", encoding="utf-8") as f:
f.write(f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{body}\n</html>")
logging.info(f"[✓] HTML generated: {output_file}")
# Rendering legals page
head_vars["canonical"] = canonical_legals
legals_vars = site_vars.get("legals", {})
if legals_vars:
head = render_template(TEMPLATE_DIR / "head.html", head_vars)
ip_paragraphs = legals_vars.get("intellectual_property", [])
paragraphs_html = "\n".join(f"<p>{item['paragraph']}</p>" for item in ip_paragraphs)
legals_context = {
"hoster_name": legals_vars.get("hoster_name", ""),
"hoster_adress": legals_vars.get("hoster_adress", ""),
"hoster_contact": legals_vars.get("hoster_contact", ""),
"intellectual_property": paragraphs_html,
}
legals_body = render_template(TEMPLATE_DIR / "legals.html", legals_context)
legals_html = f"<!DOCTYPE html>\n{signature}\n<html lang='en'>\n{head}\n{legals_body}\n{footer}\n</html>"
output_legals = BUILD_DIR / "legals" / "index.html"
output_legals.parent.mkdir(parents=True, exist_ok=True)
with open(output_legals, "w", encoding="utf-8") as f:
f.write(legals_html)
logging.info(f"[✓] Legals page generated: {output_legals}")
else:
logging.warning("[~] No legals section found in site.yaml")
# Hero carrousel generator
if hero_images:
generate_gallery_json_from_images(hero_images, BUILD_DIR)
else:
logging.warning("[~] No hero images found, skipping JSON generation.")
# Sitemap and robot.txt generator
site_info = site_vars.get("info", {})
canonical_url = site_info.get("canonical", "").rstrip("/")
if canonical_url:
allowed_pages = ["/", "/legals/"]
generate_robots_txt(canonical_url, allowed_pages, BUILD_DIR)
generate_sitemap_xml(canonical_url, allowed_pages, BUILD_DIR)
else:
logging.warning("[~] No canonical URL found in site.yaml info section, skipping robots.txt and sitemap.xml generation.")
logging.info("✅ Build complete.")

View File

@ -4,6 +4,7 @@ from pathlib import Path
from shutil import copytree, rmtree, copyfile
def load_yaml(path):
"""Load gallery and site .yaml conf"""
if not path.exists():
logging.warning(f"[!] YAML file not found: {path}")
return {}
@ -11,6 +12,7 @@ def load_yaml(path):
return yaml.safe_load(f)
def load_theme_config(theme_name, themes_dir):
"""Load theme.yaml"""
theme_dir = themes_dir / theme_name
theme_config_path = theme_dir / "theme.yaml"
if not theme_config_path.exists():
@ -19,12 +21,26 @@ def load_theme_config(theme_name, themes_dir):
theme_vars = yaml.safe_load(f)
return theme_vars, theme_dir
def ensure_dir(path):
if path.exists():
rmtree(path)
path.mkdir(parents=True)
def clear_dir(path: Path):
"""Clear the output dir"""
if not path.exists():
path.mkdir(parents=True)
return
for child in path.iterdir():
if child.is_file() or child.is_symlink():
child.unlink()
elif child.is_dir():
rmtree(child)
def ensure_dir(path: Path):
"""Create the output dir if it does not exist"""
if not path.exists():
path.mkdir(parents=True)
else:
clear_dir(path)
def copy_assets(js_dir, style_dir, build_dir):
"""Copy public assets to output dir"""
for folder in [js_dir, style_dir]:
if folder.exists():
dest = build_dir / folder.name

View File

@ -1,73 +0,0 @@
import logging
from pathlib import Path
from PIL import Image
from shutil import copyfile
def convert_and_resize_image(input_path, output_path, resize=True, max_width=1140):
try:
img = Image.open(input_path)
if img.mode != "RGB":
img = img.convert("RGB")
if resize:
width, height = img.size
if width > max_width:
new_height = int((max_width / width) * height)
img = img.resize((max_width, new_height), Image.LANCZOS)
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(output_path, "WEBP", quality=100)
logging.info(f"[✓] Processed: {input_path}{output_path}")
except Exception as e:
logging.error(f"[✗] Failed to process {input_path}: {e}")
def process_images(images, resize_images, img_dir, build_dir):
for img in images:
src_path = img_dir / img["src"]
webp_path = build_dir / "img" / Path(img["src"]).with_suffix(".webp")
convert_and_resize_image(src_path, webp_path, resize=resize_images)
img["src"] = str(Path(img["src"]).with_suffix(".webp"))
def copy_original_images(images, img_dir, build_dir):
for img in images:
src_path = img_dir / img["src"]
dest_path = build_dir / "img" / img["src"]
try:
dest_path.parent.mkdir(parents=True, exist_ok=True)
copyfile(src_path, dest_path)
logging.info(f"[✓] Copied original: {src_path}{dest_path}")
except Exception as e:
logging.error(f"[✗] Failed to copy {src_path}: {e}")
def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
logo_path = get_favicon_path(theme_vars, theme_dir)
if not logo_path:
logging.warning("[~] No favicon path defined, skipping favicon PNGs.")
return
output_dir.mkdir(parents=True, exist_ok=True)
specs = [(32, "favicon-32.png"), (96, "favicon-96.png"), (128, "favicon-128.png"),
(192, "favicon-192.png"), (196, "favicon-196.png"), (152, "favicon-152.png"), (180, "favicon-180.png")]
img = Image.open(logo_path).convert("RGBA")
for size, name in specs:
img.resize((size, size), Image.LANCZOS).save(output_dir / name, format="PNG")
logging.info(f"[✓] Favicons generated in {output_dir}")
def generate_favicon_ico(theme_vars, theme_dir, output_path):
logo_path = get_favicon_path(theme_vars, theme_dir)
if not logo_path:
logging.warning("[~] No favicon path defined, skipping .ico generation.")
return
try:
img = Image.open(logo_path).convert("RGBA")
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(output_path, format="ICO", sizes=[(16, 16), (32, 32), (48, 48)])
logging.info(f"[✓] favicon.ico generated at {output_path}")
except Exception as e:
logging.error(f"[✗] Failed to generate favicon.ico: {e}")
def get_favicon_path(theme_vars, theme_dir):
fav_path = theme_vars.get("favicon", {}).get("path")
if not fav_path:
return None
path = Path(fav_path)
if not path.is_absolute():
path = theme_dir / path
return path if path.exists() else None

0
src/py/webui/__init__.py Normal file
View File

66
src/py/webui/upload.py Normal file
View File

@ -0,0 +1,66 @@
import logging
from pathlib import Path
from flask import Blueprint, request, current_app
from werkzeug.utils import secure_filename
from src.py.builder.gallery_builder import update_gallery, update_hero
# --- Create Flask blueprint for upload routes ---
upload_bp = Blueprint("upload", __name__)
# --- Allowed file types ---
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
def allowed_file(filename: str) -> bool:
"""Check if the uploaded file has an allowed extension."""
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def save_uploaded_file(file, folder: Path):
"""Save an uploaded file to the specified folder."""
folder.mkdir(parents=True, exist_ok=True) # Create folder if not exists
filename = secure_filename(file.filename) # Sanitize filename
file.save(folder / filename) # Save to disk
logging.info(f"[✓] Uploaded {filename} to {folder}")
return filename
@upload_bp.route("/api/<section>/upload", methods=["POST"])
def upload_photo(section: str):
"""
Handle file uploads for gallery or hero section.
Accepts multiple files under 'files'.
"""
# Validate section
if section not in ["gallery", "hero"]:
return {"error": "Invalid section"}, 400
# Check if files are provided
if "files" not in request.files:
return {"error": "No files provided"}, 400
files = request.files.getlist("files")
if not files:
return {"error": "No selected files"}, 400
# Get photos directory from app config
PHOTOS_DIR = current_app.config.get("PHOTOS_DIR")
if not PHOTOS_DIR:
return {"error": "Server misconfiguration"}, 500
folder = PHOTOS_DIR / section # Target folder
uploaded = []
# Save each valid file
for file in files:
if file and allowed_file(file.filename):
filename = save_uploaded_file(file, folder)
uploaded.append(filename)
# Update YAML if any files were uploaded
if uploaded:
if section == "gallery":
update_gallery()
else:
update_hero()
return {"status": "ok", "uploaded": uploaded}
return {"error": "No valid files uploaded"}, 400

491
src/py/webui/webui.py Normal file
View File

@ -0,0 +1,491 @@
# --- Imports ---
import logging
import yaml
import subprocess
import zipfile
import os
from pathlib import Path
from flask import (
Flask, jsonify, request, send_from_directory, render_template,
send_file, after_this_request
)
from src.py.builder.gallery_builder import (
GALLERY_YAML, load_yaml, save_yaml, update_gallery, update_hero
)
from src.py.webui.upload import upload_bp
# --- Logging configuration ---
logging.basicConfig(level=logging.INFO, format="%(message)s")
# --- Flask app setup ---
VERSION_FILE = Path(__file__).resolve().parents[3] / "VERSION"
with open(VERSION_FILE, "r") as vf:
lumeex_version = vf.read().strip()
WEBUI_PATH = Path(__file__).parents[2] / "webui" # Path to static/templates
app = Flask(
__name__,
template_folder=WEBUI_PATH,
static_folder=WEBUI_PATH,
static_url_path=""
)
# --- Config paths ---
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
app.config["PHOTOS_DIR"] = PHOTOS_DIR
# --- Register upload blueprint ---
app.register_blueprint(upload_bp)
# --- Theme editor helper functions ---
def get_theme_name():
"""Get current theme name from site.yaml."""
site_yaml_path = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
with open(site_yaml_path, "r") as f:
site_yaml = yaml.safe_load(f)
return site_yaml.get("build", {}).get("theme", "modern")
def get_theme_yaml(theme_name):
"""Load theme.yaml for a given theme."""
theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
with open(theme_yaml_path, "r") as f:
return yaml.safe_load(f)
def save_theme_yaml(theme_name, theme_yaml):
"""Save theme.yaml for a given theme."""
theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
with open(theme_yaml_path, "w") as f:
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
def get_local_fonts(theme_name):
"""List local font files for a theme."""
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
if not fonts_dir.exists():
return []
return [f.name for f in fonts_dir.glob("*") if f.is_file() and f.suffix in [".woff", ".woff2"]]
# --- ROUTES ---
# --- Main page ---
@app.route("/")
def index():
return render_template("index.html")
@app.context_processor
def inject_version():
return dict(lumeex_version=lumeex_version)
# --- Gallery & Hero API ---
@app.route("/gallery-editor")
def gallery_editor():
"""Render gallery editor page."""
return render_template("gallery-editor/index.html")
@app.route("/api/gallery", methods=["GET"])
def get_gallery():
"""Get gallery images."""
data = load_yaml(GALLERY_YAML)
return jsonify(data.get("gallery", {}).get("images", []))
@app.route("/api/hero", methods=["GET"])
def get_hero():
"""Get hero images."""
data = load_yaml(GALLERY_YAML)
return jsonify(data.get("hero", {}).get("images", []))
@app.route("/api/gallery/update", methods=["POST"])
def update_gallery_api():
"""Update gallery images."""
images = request.json
data = load_yaml(GALLERY_YAML)
data["gallery"]["images"] = images
save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok"})
@app.route("/api/hero/update", methods=["POST"])
def update_hero_api():
"""Update hero images."""
images = request.json
data = load_yaml(GALLERY_YAML)
data["hero"]["images"] = images
save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok"})
@app.route("/api/gallery/refresh", methods=["POST"])
def refresh_gallery():
"""Refresh gallery images from disk."""
update_gallery()
return jsonify({"status": "ok"})
@app.route("/api/hero/refresh", methods=["POST"])
def refresh_hero():
"""Refresh hero images from disk."""
update_hero()
return jsonify({"status": "ok"})
# --- Gallery & Hero photo deletion ---
@app.route("/api/gallery/delete", methods=["POST"])
def delete_gallery_photo():
"""Delete a gallery photo."""
data = request.json
src = data.get("src")
file_path = PHOTOS_DIR / "gallery" / src
if file_path.exists():
file_path.unlink()
return {"status": "ok"}
return {"error": "❌ File not found"}, 404
@app.route("/api/hero/delete", methods=["POST"])
def delete_hero_photo():
"""Delete a hero photo."""
data = request.json
src = data.get("src")
file_path = PHOTOS_DIR / "hero" / src
if file_path.exists():
file_path.unlink()
return {"status": "ok"}
return {"error": "❌ File not found"}, 404
@app.route("/api/gallery/delete_all", methods=["POST"])
def delete_all_gallery_photos():
"""Delete all gallery photos."""
gallery_dir = PHOTOS_DIR / "gallery"
deleted = 0
for file in gallery_dir.glob("*"):
if file.is_file():
file.unlink()
deleted += 1
data = load_yaml(GALLERY_YAML)
data["gallery"]["images"] = []
save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok", "deleted": deleted})
@app.route("/api/hero/delete_all", methods=["POST"])
def delete_all_hero_photos():
"""Delete all hero photos."""
hero_dir = PHOTOS_DIR / "hero"
deleted = 0
for file in hero_dir.glob("*"):
if file.is_file():
file.unlink()
deleted += 1
data = load_yaml(GALLERY_YAML)
data["hero"]["images"] = []
save_yaml(data, GALLERY_YAML)
return jsonify({"status": "ok", "deleted": deleted})
# --- Serve photos ---
@app.route("/photos/<section>/<path:filename>")
def photos(section, filename):
"""Serve a photo from a section."""
return send_from_directory(PHOTOS_DIR / section, filename)
@app.route("/photos/<path:filename>")
def serve_photo(filename):
"""Serve a photo from the photos directory."""
photos_dir = Path(__file__).resolve().parents[3] / "config" / "photos"
return send_from_directory(photos_dir, filename)
# --- Site info page & API ---
@app.route("/site-info")
def site_info():
"""Render site info editor page."""
return render_template("site-info/index.html")
@app.route("/api/site-info", methods=["GET"])
def get_site_info():
"""Get site info YAML as JSON."""
with open(SITE_YAML, "r") as f:
data = yaml.safe_load(f)
return jsonify(data)
@app.route("/api/site-info", methods=["POST"])
def update_site_info():
"""Update site info YAML."""
data = request.json
with open(SITE_YAML, "w") as f:
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
return jsonify({"status": "ok"})
# --- Theme management ---
@app.route("/api/themes")
def list_themes():
"""List available themes."""
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
themes = [d.name for d in themes_dir.iterdir() if d.is_dir()]
return jsonify(themes)
# --- Thumbnail upload/remove ---
@app.route("/api/thumbnail/upload", methods=["POST"])
def upload_thumbnail():
"""Upload thumbnail image and update site.yaml."""
PHOTOS_DIR = app.config["PHOTOS_DIR"]
file = request.files.get("file")
if not file:
return {"error": "❌ No file provided"}, 400
filename = "thumbnail.png"
file.save(PHOTOS_DIR / filename)
with open(SITE_YAML, "r") as f:
data = yaml.safe_load(f)
data.setdefault("social", {})["thumbnail"] = filename
with open(SITE_YAML, "w") as f:
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
return jsonify({"status": "ok", "filename": filename})
@app.route("/api/thumbnail/remove", methods=["POST"])
def remove_thumbnail():
"""Remove thumbnail image and update site.yaml."""
PHOTOS_DIR = app.config["PHOTOS_DIR"]
thumbnail_path = PHOTOS_DIR / "thumbnail.png"
if thumbnail_path.exists():
thumbnail_path.unlink()
with open(SITE_YAML, "r") as f:
data = yaml.safe_load(f)
if "social" in data and "thumbnail" in data["social"]:
data["social"]["thumbnail"] = ""
with open(SITE_YAML, "w") as f:
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
return jsonify({"status": "ok"})
# --- Theme upload ---
@app.route("/api/theme/upload", methods=["POST"])
def upload_theme():
"""Upload a custom theme folder."""
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
files = request.files.getlist("files")
if not files:
return jsonify({"error": "❌ No files provided"}), 400
first_path = files[0].filename
folder_name = first_path.split("/")[0] if "/" in first_path else "custom"
theme_folder = themes_dir / folder_name
theme_folder.mkdir(parents=True, exist_ok=True)
for file in files:
rel_path = Path(file.filename)
dest_path = theme_folder / rel_path.relative_to(folder_name)
dest_path.parent.mkdir(parents=True, exist_ok=True)
file.save(dest_path)
return jsonify({"status": "ok", "theme": folder_name})
@app.route("/api/theme/remove", methods=["POST"])
def remove_theme():
"""Remove a custom theme folder."""
data = request.get_json()
theme_name = data.get("theme")
if not theme_name:
return jsonify({"error": "❌ Missing theme"}), 400
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
theme_folder = themes_dir / theme_name
if not theme_folder.exists() or not theme_folder.is_dir():
return jsonify({"error": "❌ Theme not found"}), 404
# Prevent removing default themes
if theme_name in ["modern", "classic"]:
return jsonify({"error": "❌ Cannot remove default theme"}), 400
# Remove folder and all contents
import shutil
shutil.rmtree(theme_folder)
return jsonify({"status": "ok"})
# --- Theme editor page & API ---
@app.route("/theme-editor")
def theme_editor():
"""Render theme editor page."""
return render_template("theme-editor/index.html")
@app.route("/api/theme-info", methods=["GET", "POST"])
def api_theme_info():
"""Get or update theme.yaml for current theme."""
theme_name = get_theme_name()
if request.method == "GET":
theme_yaml = get_theme_yaml(theme_name)
google_fonts = theme_yaml.get("google_fonts", [])
return jsonify({
"theme_name": theme_name,
"theme_yaml": theme_yaml,
"google_fonts": google_fonts
})
else:
data = request.get_json()
theme_yaml = data.get("theme_yaml")
theme_name = data.get("theme_name", theme_name)
save_theme_yaml(theme_name, theme_yaml)
return jsonify({"status": "ok"})
@app.route("/api/theme-google-fonts", methods=["POST"])
def update_theme_google_fonts():
"""Update only google_fonts in theme.yaml for current theme."""
data = request.get_json()
theme_name = data.get("theme_name")
google_fonts = data.get("google_fonts", [])
theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
with open(theme_yaml_path, "r") as f:
theme_yaml = yaml.safe_load(f)
theme_yaml["google_fonts"] = google_fonts
with open(theme_yaml_path, "w") as f:
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
return jsonify({"status": "ok"})
@app.route("/api/local-fonts")
def api_local_fonts():
"""List local fonts for a theme."""
theme_name = request.args.get("theme")
fonts = get_local_fonts(theme_name)
return jsonify(fonts)
# --- Favicon upload/remove ---
@app.route("/api/favicon/upload", methods=["POST"])
def upload_favicon():
"""Upload favicon for a theme."""
theme_name = request.form.get("theme")
file = request.files.get("file")
if not file or not theme_name:
return jsonify({"error": "❌ Missing file or theme"}), 400
ext = Path(file.filename).suffix.lower()
if ext not in [".png", ".jpg", ".jpeg", ".ico"]:
return jsonify({"error": "❌ Invalid file type"}), 400
filename = "favicon" + ext
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
file.save(theme_dir / filename)
theme_yaml_path = theme_dir / "theme.yaml"
with open(theme_yaml_path, "r") as f:
theme_yaml = yaml.safe_load(f)
theme_yaml.setdefault("favicon", {})["path"] = filename
with open(theme_yaml_path, "w") as f:
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
return jsonify({"status": "ok", "filename": filename})
@app.route("/api/favicon/remove", methods=["POST"])
def remove_favicon():
"""Remove favicon for a theme."""
data = request.get_json()
theme_name = data.get("theme")
if not theme_name:
return jsonify({"error": "❌ Missing theme"}), 400
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
for ext in [".png", ".jpg", ".jpeg", ".ico"]:
favicon_path = theme_dir / f"favicon{ext}"
if favicon_path.exists():
favicon_path.unlink()
theme_yaml_path = theme_dir / "theme.yaml"
with open(theme_yaml_path, "r") as f:
theme_yaml = yaml.safe_load(f)
if "favicon" in theme_yaml:
theme_yaml["favicon"]["path"] = ""
with open(theme_yaml_path, "w") as f:
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
return jsonify({"status": "ok"})
# --- Serve theme assets ---
@app.route("/themes/<theme>/<filename>")
def serve_theme_asset(theme, filename):
"""Serve a theme asset file."""
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme
return send_from_directory(theme_dir, filename)
# --- Font upload/remove ---
@app.route("/api/font/upload", methods=["POST"])
def upload_font():
"""Upload a font file for a theme."""
theme_name = request.form.get("theme")
file = request.files.get("file")
if not file or not theme_name:
return jsonify({"error": "❌ Missing theme or font"}), 400
ext = Path(file.filename).suffix.lower()
if ext not in [".woff", ".woff2"]:
return jsonify({"error": "❌ Invalid font file type"}), 400
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
fonts_dir.mkdir(parents=True, exist_ok=True)
file.save(fonts_dir / file.filename)
return jsonify({"status": "ok", "filename": file.filename})
@app.route("/api/font/remove", methods=["POST"])
def remove_font():
"""Remove a font file for a theme."""
data = request.get_json()
theme_name = data.get("theme")
font = data.get("font")
if not theme_name or not font:
return jsonify({"error": "❌ Missing theme or font"}), 400
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
font_path = fonts_dir / font
if font_path.exists():
font_path.unlink()
return jsonify({"status": "ok"})
return jsonify({"error": "❌ Font not found"}), 404
# --- Build & Download ZIP ---
@app.route("/api/build", methods=["POST"])
def trigger_build():
"""
Validate site.yaml and run build.py.
Does NOT create zip here; zip is created on demand in download route.
"""
site_yaml_path = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
output_folder = Path(__file__).resolve().parents[3] / "output"
if not site_yaml_path.exists():
return jsonify({"status": "error", "message": "❌ site.yaml not found"}), 400
with open(site_yaml_path, "r") as f:
site_data = yaml.safe_load(f) or {}
# Dynamically check all main sections and nested keys
main_sections = list(site_data.keys())
for section in main_sections:
value = site_data.get(section)
if not value:
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}"}), 400
if isinstance(value, dict):
for k, v in value.items():
if v is None or v == "" or (isinstance(v, list) and not v):
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}.{k}"}), 400
elif isinstance(value, list):
if not value:
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}"}), 400
for idx, item in enumerate(value):
if isinstance(item, dict):
for k, v in item.items():
if v is None or v == "" or (isinstance(v, list) and not v):
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}[{idx}].{k}"}), 400
elif item is None or item == "":
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}[{idx}]"}), 400
else:
if value is None or value == "":
return jsonify({"status": "error", "message": f"❌ Site info are not set: missing {section}"}), 400
try:
subprocess.run(["python3", "build.py"], check=True)
return jsonify({"status": "ok"})
except Exception as e:
return jsonify({"status": "error", "message": f"{str(e)}"}), 500
@app.route("/download-output-zip", methods=["POST"])
def download_output_zip():
"""
Create output zip on demand and send it to the user.
Zip is deleted after sending.
"""
output_folder = Path(__file__).resolve().parents[3] / "output"
zip_path = Path(__file__).resolve().parents[3] / "site_output.zip" # Store in lumeex/ root
# Create zip on demand
with zipfile.ZipFile(zip_path, "w") as zipf:
for root, dirs, files in os.walk(output_folder):
for file in files:
file_path = Path(root) / file
zipf.write(file_path, file_path.relative_to(output_folder))
@after_this_request
def remove_file(response):
try:
os.remove(zip_path)
except Exception:
pass
return response
return send_file(zip_path, as_attachment=True)
# --- Run server ---
if __name__ == "__main__":
logging.info("Starting WebUI at http://0.0.0.0:5000")
app.run(host="0.0.0.0", port=5000, debug=True)

View File

@ -1,5 +1,6 @@
<head>
<!-- Meta -->
<meta charset="utf-8">
<title>{{ title }}</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" href="/img/favicon/favicon-32.png" sizes="32x32">
@ -11,12 +12,13 @@
<!-- iOS -->
<link rel="apple-touch-icon" href="/img/favicon/favicon-152.png" sizes="152x152">
<link rel="apple-touch-icon" href="/img/favicon/favicon-180.png" sizes="180x180">
<meta charset="utf-8">
<link rel="apple-touch-icon" href="/img/favicon/favicon-192.png">
<meta name='robots' content='index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1' />
<meta name="theme-color" content="{{ browser_color }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-status-bar-style" content="{{ browser_color }}">
<meta name="description" content="{{ description }}">
<meta name="keywords" content="{{ keywords }}">
<meta name="author" content="{{ author }}">
@ -40,5 +42,4 @@
<!-- Scripts -->
<script src="https://kit.fontawesome.com/7c6bfe3c24.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="/js/lumeex.js?{{ build_date }}" defer></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
</head>

BIN
src/webui/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,70 @@
{% extends "template/base.html" %}
{% block title %}Lumeex - Gallery Editor{% endblock %}
{% block content %}
<h1>Gallery editor</h1>
<!-- Hero Upload Section -->
<div class="section">
<h2>Title Carrousel</h2>
<p> Select photos to display in the Title Carrousel</p>
<div class="upload-actions-row">
<label for="upload-hero" class="up-btn">
📸 Upload photos
</label>
<button id="remove-all-hero" class="up-btn">🗑 Delete all</button>
</div>
<input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
<div id="hero"></div>
</div>
<!-- Gallery Upload Section -->
<div class="section">
<h2>Gallery</h2>
<p> Select and tags photos to display in the Gallery</p>
<div class="upload-actions-row">
<label for="upload-gallery" class="up-btn">
📸 Upload photos
</label>
<button id="remove-all-gallery" class="up-btn">🗑 Delete all</button>
</div>
<input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
<div id="gallery"></div>
</div>
<div class="section">
<h2>Steps</h2>
<p> Follow the steps to generate your static gallery</p>
<ul id="stepper">
<li><a class="step-active" href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a href="/site-info">Configure site info</a></li>
<div></div>
<li><a href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div>
<!-- Delete confirmation modal -->
<div id="delete-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3>
<p id="delete-modal-text">Are you sure you want to delete this image?</p>
<div class="modal-actions">
<button id="delete-modal-confirm" class="modal-btn danger">Delete</button>
<button id="delete-modal-cancel" class="modal-btn">Cancel</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/gallery-editor.js') }}"></script>
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
{% endblock %}

154
src/webui/img/favicon.svg Normal file
View File

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1000 1000">
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) -->
<defs>
<style>
.st0 {
fill: url(#Dégradé_sans_nom_265);
stroke: url(#Dégradé_sans_nom_33);
}
.st0, .st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8, .st9, .st10, .st11 {
stroke-miterlimit: 10;
}
.st1 {
fill: url(#Dégradé_sans_nom_269);
stroke: url(#Dégradé_sans_nom_334);
}
.st2 {
fill: url(#Dégradé_sans_nom_268);
stroke: url(#Dégradé_sans_nom_333);
}
.st3 {
fill: url(#Dégradé_sans_nom_266);
stroke: url(#Dégradé_sans_nom_331);
}
.st4 {
fill: url(#Dégradé_sans_nom_267);
stroke: url(#Dégradé_sans_nom_332);
}
.st12 {
fill: url(#Dégradé_sans_nom_261);
}
.st13 {
fill: url(#Dégradé_sans_nom_262);
}
.st14 {
fill: url(#Dégradé_sans_nom_264);
}
.st15 {
fill: url(#Dégradé_sans_nom_263);
}
.st5 {
fill: url(#Dégradé_sans_nom_2616);
stroke: url(#Dégradé_sans_nom_3311);
}
.st6 {
fill: url(#Dégradé_sans_nom_2615);
stroke: url(#Dégradé_sans_nom_3310);
}
.st16 {
fill: #fff;
}
.st17 {
fill: url(#Dégradé_sans_nom_26);
}
.st7 {
fill: url(#Dégradé_sans_nom_2610);
stroke: url(#Dégradé_sans_nom_335);
}
.st8 {
fill: url(#Dégradé_sans_nom_2613);
stroke: url(#Dégradé_sans_nom_338);
}
.st9 {
fill: url(#Dégradé_sans_nom_2614);
stroke: url(#Dégradé_sans_nom_339);
}
.st10 {
fill: url(#Dégradé_sans_nom_2611);
stroke: url(#Dégradé_sans_nom_336);
}
.st11 {
fill: url(#Dégradé_sans_nom_2612);
stroke: url(#Dégradé_sans_nom_337);
}
</style>
<linearGradient id="Dégradé_sans_nom_26" data-name="Dégradé sans nom 26" x1="373.2" y1="159.5" x2="625.1" y2="411.5" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_261" data-name="Dégradé sans nom 26" x1="143.1" y1="200.5" x2="395" y2="452.4" gradientTransform="translate(30.8 109.3)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_262" data-name="Dégradé sans nom 26" x1="81.3" y1="60.6" x2="333.2" y2="312.5" gradientTransform="translate(187.1 873.6) rotate(-90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_263" data-name="Dégradé sans nom 26" x1="-44.4" y1="16.5" x2="207.5" y2="268.4" gradientTransform="translate(705.4 808.2) rotate(-180)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_264" data-name="Dégradé sans nom 26" x1="-67.9" y1="-58.9" x2="184" y2="193" gradientTransform="translate(770.9 385.1) rotate(90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_265" data-name="Dégradé sans nom 26" x1="74.5" y1="560.2" x2="106.2" y2="591.8" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_33" data-name="Dégradé sans nom 33" x1="74.2" y1="559.9" x2="106.5" y2="592.2" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".5"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_266" data-name="Dégradé sans nom 26" x1="158.2" y1="648.8" x2="176.7" y2="667.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_331" data-name="Dégradé sans nom 33" x1="157.8" y1="648.4" x2="177.1" y2="667.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_267" data-name="Dégradé sans nom 26" x1="210.2" y1="714.7" x2="249.8" y2="754.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_332" data-name="Dégradé sans nom 33" x1="209.9" y1="714.3" x2="250.2" y2="754.6" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_268" data-name="Dégradé sans nom 26" x1="-54" y1="72.2" x2="-14.4" y2="111.8" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_333" data-name="Dégradé sans nom 33" x1="-54.4" y1="71.9" x2="-14.1" y2="112.2" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_269" data-name="Dégradé sans nom 26" x1="485.1" y1="-9.2" x2="503.7" y2="9.4" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_334" data-name="Dégradé sans nom 33" x1="484.8" y1="-9.6" x2="504" y2="9.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2610" data-name="Dégradé sans nom 26" x1="825.1" y1="682.3" x2="856.8" y2="713.9" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_335" data-name="Dégradé sans nom 33" x1="824.8" y1="681.9" x2="857.1" y2="714.3" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2611" data-name="Dégradé sans nom 26" x1="308.5" y1="356.5" x2="340.3" y2="388.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_336" data-name="Dégradé sans nom 33" x1="308.1" y1="356.2" x2="340.7" y2="388.7" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2612" data-name="Dégradé sans nom 26" x1="540.4" y1="450.4" x2="559" y2="469" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_337" data-name="Dégradé sans nom 33" x1="540.1" y1="450" x2="559.4" y2="469.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2613" data-name="Dégradé sans nom 26" x1="-336.4" y1="326.9" x2="-296.8" y2="366.5" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_338" data-name="Dégradé sans nom 33" x1="-336.7" y1="326.5" x2="-296.4" y2="366.8" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2614" data-name="Dégradé sans nom 26" x1="115" y1="-29.9" x2="133.6" y2="-11.3" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_339" data-name="Dégradé sans nom 33" x1="114.7" y1="-30.2" x2="134" y2="-11" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2615" data-name="Dégradé sans nom 26" x1="94.5" y1="304.2" x2="124.4" y2="334" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3310" data-name="Dégradé sans nom 33" x1="94.2" y1="303.8" x2="124.7" y2="334.4" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2616" data-name="Dégradé sans nom 26" x1="435.8" y1="254.1" x2="454.4" y2="272.7" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3311" data-name="Dégradé sans nom 33" x1="435.5" y1="253.8" x2="454.8" y2="273.1" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_33"/>
</defs>
<g id="Calque_1">
<circle class="st16" cx="499.5" cy="499.5" r="499.5"/>
</g>
<g id="Calque_2">
<g id="Calque_3">
<ellipse class="st17" cx="499.2" cy="285.5" rx="139.8" ry="209.5"/>
<ellipse class="st12" cx="299.9" cy="435.8" rx="139.8" ry="209.5" transform="translate(-207.3 586.3) rotate(-72)"/>
<ellipse class="st13" cx="373.6" cy="666.3" rx="209.5" ry="139.8" transform="translate(-385.1 576.9) rotate(-54)"/>
<ellipse class="st15" cx="623.9" cy="665.8" rx="139.8" ry="209.5" transform="translate(-272.2 493.9) rotate(-36)"/>
<ellipse class="st14" cx="703.9" cy="443.1" rx="209.5" ry="139.8" transform="translate(-94.9 211.2) rotate(-16)"/>
<circle class="st0" cx="90.4" cy="576" r="22.4"/>
<circle class="st3" cx="175.6" cy="607.9" r="13.1"/>
<circle class="st4" cx="140.8" cy="691.6" r="28"/>
<circle class="st2" cx="829.7" cy="602.6" r="28"/>
<circle class="st1" cx="908.9" cy="562.1" r="13.1"/>
<circle class="st7" cx="840.9" cy="698.1" r="22.4"/>
<circle class="st10" cx="466.1" cy="876.5" r="22.5"/>
<circle class="st11" cx="538.6" cy="839.8" r="13.1"/>
<circle class="st8" cx="686.1" cy="170.1" r="28"/>
<circle class="st9" cx="733.7" cy="247.7" r="13.1"/>
<circle class="st6" cx="236.9" cy="206.5" r="21.1"/>
<circle class="st5" cx="315.4" cy="164.9" r="13.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

2
src/webui/img/gitea.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#48cf51ff" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Gitea icon</title><path d="M4.186 5.421C2.341 5.417-.13 6.59.006 9.531c.213 4.594 4.92 5.02 6.801 5.057.206.862 2.42 3.834 4.059 3.99h7.18c4.306-.286 7.53-13.022 5.14-13.07-3.953.186-6.296.28-8.305.296v3.975l-.626-.277-.004-3.696c-2.306-.001-4.336-.108-8.189-.298-.482-.003-1.154-.085-1.876-.087zm.261 1.625h.22c.262 2.355.688 3.732 1.55 5.836-2.2-.26-4.072-.899-4.416-3.285-.178-1.235.422-2.524 2.646-2.552zm8.557 2.315c.15.002.303.03.447.096l.749.323-.537.979a.672.597 0 0 0-.241.038.672.597 0 0 0-.405.764.672.597 0 0 0 .112.174l-.926 1.686a.672.597 0 0 0-.222.038.672.597 0 0 0-.405.764.672.597 0 0 0 .86.36.672.597 0 0 0 .404-.765.672.597 0 0 0-.158-.22l.902-1.642a.672.597 0 0 0 .293-.03.672.597 0 0 0 .213-.112c.348.146.633.265.838.366.308.152.417.253.45.365.033.11-.003.322-.177.694-.13.277-.345.67-.599 1.133a.672.597 0 0 0-.251.038.672.597 0 0 0-.405.764.672.597 0 0 0 .86.36.672.597 0 0 0 .404-.764.672.597 0 0 0-.137-.202c.251-.458.467-.852.606-1.148.188-.402.286-.701.2-.99-.086-.289-.35-.477-.7-.65-.23-.113-.517-.233-.86-.377a.672.597 0 0 0-.038-.239.672.597 0 0 0-.145-.209l.528-.963 2.924 1.263c.528.229.746.79.49 1.26l-2.01 3.68c-.257.469-.888.663-1.416.435l-4.137-1.788c-.528-.228-.747-.79-.49-1.26l2.01-3.679c.176-.323.53-.515.905-.53h.064z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

19
src/webui/img/github.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>github [#142]</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#ffffffff">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

166
src/webui/img/logo.svg Normal file
View File

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 6031 1000">
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) -->
<defs>
<style>
.st0 {
fill: #55c3ec;
}
.st1 {
fill: url(#Dégradé_sans_nom_265);
stroke: url(#Dégradé_sans_nom_33);
}
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8, .st9, .st10, .st11, .st12 {
stroke-miterlimit: 10;
}
.st2 {
fill: url(#Dégradé_sans_nom_269);
stroke: url(#Dégradé_sans_nom_334);
}
.st3 {
fill: url(#Dégradé_sans_nom_268);
stroke: url(#Dégradé_sans_nom_333);
}
.st4 {
fill: url(#Dégradé_sans_nom_266);
stroke: url(#Dégradé_sans_nom_331);
}
.st5 {
fill: url(#Dégradé_sans_nom_267);
stroke: url(#Dégradé_sans_nom_332);
}
.st13 {
fill: url(#Dégradé_sans_nom_261);
}
.st14 {
fill: url(#Dégradé_sans_nom_262);
}
.st15 {
fill: url(#Dégradé_sans_nom_264);
}
.st16 {
fill: url(#Dégradé_sans_nom_263);
}
.st6 {
fill: url(#Dégradé_sans_nom_2616);
stroke: url(#Dégradé_sans_nom_3311);
}
.st7 {
fill: url(#Dégradé_sans_nom_2615);
stroke: url(#Dégradé_sans_nom_3310);
}
.st17 {
fill: #fff;
}
.st18 {
fill: url(#Dégradé_sans_nom_26);
}
.st8 {
fill: url(#Dégradé_sans_nom_2610);
stroke: url(#Dégradé_sans_nom_335);
}
.st9 {
fill: url(#Dégradé_sans_nom_2613);
stroke: url(#Dégradé_sans_nom_338);
}
.st10 {
fill: url(#Dégradé_sans_nom_2614);
stroke: url(#Dégradé_sans_nom_339);
}
.st11 {
fill: url(#Dégradé_sans_nom_2611);
stroke: url(#Dégradé_sans_nom_336);
}
.st12 {
fill: url(#Dégradé_sans_nom_2612);
stroke: url(#Dégradé_sans_nom_337);
}
</style>
<linearGradient id="Dégradé_sans_nom_26" data-name="Dégradé sans nom 26" x1="373.2" y1="159.5" x2="625.1" y2="411.5" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_261" data-name="Dégradé sans nom 26" x1="143.1" y1="200.5" x2="395" y2="452.4" gradientTransform="translate(30.8 109.3)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_262" data-name="Dégradé sans nom 26" x1="81.3" y1="60.6" x2="333.2" y2="312.5" gradientTransform="translate(187.1 873.6) rotate(-90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_263" data-name="Dégradé sans nom 26" x1="-44.4" y1="16.5" x2="207.5" y2="268.4" gradientTransform="translate(705.4 808.2) rotate(-180)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_264" data-name="Dégradé sans nom 26" x1="-67.9" y1="-58.9" x2="184" y2="193" gradientTransform="translate(770.9 385.1) rotate(90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_265" data-name="Dégradé sans nom 26" x1="74.5" y1="560.2" x2="106.2" y2="591.8" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_33" data-name="Dégradé sans nom 33" x1="74.2" y1="559.9" x2="106.5" y2="592.2" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".5"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_266" data-name="Dégradé sans nom 26" x1="158.2" y1="648.8" x2="176.7" y2="667.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_331" data-name="Dégradé sans nom 33" x1="157.8" y1="648.4" x2="177.1" y2="667.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_267" data-name="Dégradé sans nom 26" x1="210.2" y1="714.7" x2="249.8" y2="754.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_332" data-name="Dégradé sans nom 33" x1="209.9" y1="714.3" x2="250.2" y2="754.6" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_268" data-name="Dégradé sans nom 26" x1="-54" y1="72.2" x2="-14.4" y2="111.8" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_333" data-name="Dégradé sans nom 33" x1="-54.4" y1="71.9" x2="-14.1" y2="112.2" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_269" data-name="Dégradé sans nom 26" x1="485.1" y1="-9.2" x2="503.7" y2="9.4" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_334" data-name="Dégradé sans nom 33" x1="484.8" y1="-9.6" x2="504" y2="9.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2610" data-name="Dégradé sans nom 26" x1="825.1" y1="682.3" x2="856.8" y2="713.9" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_335" data-name="Dégradé sans nom 33" x1="824.8" y1="681.9" x2="857.1" y2="714.3" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2611" data-name="Dégradé sans nom 26" x1="308.5" y1="356.5" x2="340.3" y2="388.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_336" data-name="Dégradé sans nom 33" x1="308.1" y1="356.2" x2="340.7" y2="388.7" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2612" data-name="Dégradé sans nom 26" x1="540.4" y1="450.4" x2="559" y2="469" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_337" data-name="Dégradé sans nom 33" x1="540.1" y1="450" x2="559.4" y2="469.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2613" data-name="Dégradé sans nom 26" x1="-336.4" y1="326.9" x2="-296.8" y2="366.5" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_338" data-name="Dégradé sans nom 33" x1="-336.7" y1="326.5" x2="-296.4" y2="366.8" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2614" data-name="Dégradé sans nom 26" x1="115" y1="-29.9" x2="133.6" y2="-11.3" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_339" data-name="Dégradé sans nom 33" x1="114.7" y1="-30.2" x2="134" y2="-11" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2615" data-name="Dégradé sans nom 26" x1="94.5" y1="304.2" x2="124.4" y2="334" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3310" data-name="Dégradé sans nom 33" x1="94.2" y1="303.8" x2="124.7" y2="334.4" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2616" data-name="Dégradé sans nom 26" x1="435.8" y1="254.1" x2="454.4" y2="272.7" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3311" data-name="Dégradé sans nom 33" x1="435.5" y1="253.8" x2="454.8" y2="273.1" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_33"/>
</defs>
<g id="Calque_1">
<circle class="st17" cx="499.5" cy="499.5" r="499.5"/>
<g>
<path class="st17" d="M1404,957.4V45h191v755h399v157.4h-590Z"/>
<path class="st17" d="M2321.5,971.3c-49.3,0-91.5-10.2-126.5-30.7-35-20.4-61.6-49.6-79.7-87.6-18.1-37.9-27.2-83.2-27.2-135.9v-437.6h184.6v399c0,44.3,10.4,78.6,31.3,103.1,20.9,24.5,51.9,36.7,93.3,36.7s39.3-3.6,56-10.7c16.6-7.2,30.9-17.4,42.7-30.7,11.8-13.3,20.9-29.1,27.2-47.4s9.5-38.5,9.5-60.4v-389.5h184.6v677.8h-184.6v-111.9h-4.4c-11.4,25.7-26.7,48.1-45.8,67-19.2,19-42.2,33.5-68.9,43.6-26.8,10.1-57.4,15.2-92,15.2Z"/>
<path class="st17" d="M2837.5,957.4V279.6h184.6v113.8h3.8c13.9-38.8,37.6-69.8,71.1-93,33.5-23.2,73-34.8,118.6-34.8s60.1,5.5,85.4,16.4c25.3,11,46.7,26.8,64.2,47.4,17.5,20.7,29.8,46,37,75.9h3.8c10.1-28.7,25.4-53.4,45.8-74.3,20.4-20.9,44.7-37,72.7-48.4,28-11.4,58.7-17.1,92-17.1s83,9.5,116.3,28.5c33.3,19,59.2,45.5,77.8,79.7,18.5,34.1,27.8,74.2,27.8,120.1v463.5h-184.6v-416.7c0-26.6-4.3-48.8-13-66.7-8.6-17.9-21.1-31.6-37.3-41.1-16.2-9.5-36.4-14.2-60.4-14.2s-43.5,5.4-61,16.1c-17.5,10.7-31.1,25.6-40.8,44.6-9.7,19-14.5,41.1-14.5,66.4v411.6h-177.7v-422.4c0-24.4-4.4-45.3-13.3-62.6-8.9-17.3-21.4-30.6-37.6-39.8-16.2-9.3-35.7-13.9-58.5-13.9s-43.6,5.6-61.3,16.8c-17.7,11.2-31.5,26.5-41.4,45.8-9.9,19.4-14.9,41.7-14.9,67v409.1h-184.6Z"/>
<path class="st0" d="M4264,971.3c-69.1,0-128.6-14.2-178.3-42.7-49.7-28.5-88.1-69-115.1-121.7-27-52.7-40.5-115.1-40.5-187.2v-.6c0-72.1,13.5-134.6,40.5-187.5,27-52.9,64.8-93.8,113.5-122.7,48.7-28.9,106.1-43.3,172.3-43.3s123.4,14,171.7,42c48.3,28,85.6,67.6,111.9,118.6,26.3,51,39.5,110.7,39.5,178.9v57.5h-558.3v-117h471.1l-87.9,109.4v-71.5c0-39.6-6.1-72.8-18.3-99.6-12.2-26.8-29.2-46.9-50.9-60.4-21.7-13.5-46.9-20.2-75.6-20.2s-54.1,7-76.2,20.9c-22.1,13.9-39.4,34.3-51.9,61-12.4,26.8-18.7,59.5-18.7,98.3v72.1c0,37.1,6.2,68.9,18.7,95.5,12.4,26.6,30.2,46.9,53.4,61,23.2,14.1,50.8,21.2,82.8,21.2s47.2-4,65.8-12c18.5-8,33.7-18.1,45.5-30.4,11.8-12.2,19.8-24.7,24-37.3l1.3-3.8h169.5l-1.9,7c-5.1,24.9-15,50-29.7,75.2-14.8,25.3-34.7,48.5-59.8,69.6-25.1,21.1-55.6,37.9-91.7,50.6-36,12.6-78.3,19-126.8,19Z"/>
<path class="st0" d="M4982.9,971.3c-69.1,0-128.6-14.2-178.3-42.7-49.7-28.5-88.1-69-115.1-121.7-27-52.7-40.5-115.1-40.5-187.2v-.6c0-72.1,13.5-134.6,40.5-187.5,27-52.9,64.8-93.8,113.5-122.7,48.7-28.9,106.1-43.3,172.3-43.3s123.4,14,171.7,42c48.3,28,85.6,67.6,111.9,118.6,26.3,51,39.5,110.7,39.5,178.9v57.5h-558.3v-117h471.1l-87.9,109.4v-71.5c0-39.6-6.1-72.8-18.3-99.6-12.2-26.8-29.2-46.9-50.9-60.4-21.7-13.5-46.9-20.2-75.6-20.2s-54.1,7-76.2,20.9c-22.1,13.9-39.4,34.3-51.9,61-12.4,26.8-18.7,59.5-18.7,98.3v72.1c0,37.1,6.2,68.9,18.7,95.5,12.4,26.6,30.2,46.9,53.4,61,23.2,14.1,50.8,21.2,82.8,21.2s47.2-4,65.8-12c18.5-8,33.7-18.1,45.5-30.4,11.8-12.2,19.8-24.7,24-37.3l1.3-3.8h169.5l-1.9,7c-5.1,24.9-15,50-29.7,75.2-14.8,25.3-34.7,48.5-59.8,69.6-25.1,21.1-55.6,37.9-91.7,50.6s-78.3,19-126.8,19Z"/>
<path class="st0" d="M5337,957.4l212.5-337.7-210.6-340.2h208l122,228.9h3.8l120.1-228.9h201.1l-211.8,335.1,209.9,342.7h-200.4l-129-234.6h-3.8l-127.1,234.6h-194.8Z"/>
</g>
</g>
<g id="Calque_2">
<g id="Calque_3">
<ellipse class="st18" cx="499.2" cy="285.5" rx="139.8" ry="209.5"/>
<ellipse class="st13" cx="299.9" cy="435.8" rx="139.8" ry="209.5" transform="translate(-207.3 586.3) rotate(-72)"/>
<ellipse class="st14" cx="373.6" cy="666.3" rx="209.5" ry="139.8" transform="translate(-385.1 576.9) rotate(-54)"/>
<ellipse class="st16" cx="623.9" cy="665.8" rx="139.8" ry="209.5" transform="translate(-272.2 493.9) rotate(-36)"/>
<ellipse class="st15" cx="703.9" cy="443.1" rx="209.5" ry="139.8" transform="translate(-94.9 211.2) rotate(-16)"/>
<circle class="st1" cx="90.4" cy="576" r="22.4"/>
<circle class="st4" cx="175.6" cy="607.9" r="13.1"/>
<circle class="st5" cx="140.8" cy="691.6" r="28"/>
<circle class="st3" cx="829.7" cy="602.6" r="28"/>
<circle class="st2" cx="908.9" cy="562.1" r="13.1"/>
<circle class="st8" cx="840.9" cy="698.1" r="22.4"/>
<circle class="st11" cx="466.1" cy="876.5" r="22.5"/>
<circle class="st12" cx="538.6" cy="839.8" r="13.1"/>
<circle class="st9" cx="686.1" cy="170.1" r="28"/>
<circle class="st10" cx="733.7" cy="247.7" r="13.1"/>
<circle class="st7" cx="236.9" cy="206.5" r="21.1"/>
<circle class="st6" cx="315.4" cy="164.9" r="13.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

31
src/webui/index.html Normal file
View File

@ -0,0 +1,31 @@
{% extends "template/base.html" %}
{% block title %}Lumeex{% endblock %}
{% block content %}
<h1>Static Gallery Generator</h1>
<p>Use this generator to create a static gallery from your photos. Then, upload the static files to your preferred web server.</p>
<!-- Hero Upload Section -->
<div class="section">
<h2>Steps</h2>
<p> Follow the steps to generate your static gallery</p>
<div class="stepper">
<ul id="stepper">
<li><a href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a href="/site-info">Configure site info</a></li>
<div></div>
<li><a href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

106
src/webui/js/build.js Normal file
View File

@ -0,0 +1,106 @@
/**
* Show a toast notification.
* @param {string} message - The message to display.
* @param {string} type - "success" or "error".
* @param {number} duration - Duration in ms.
*/
// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.classList.add("active");
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.classList.remove("active");
}
// --- Toast helpers ---
function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("show"));
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => container.removeChild(toast), 300);
}, duration);
}
document.addEventListener("DOMContentLoaded", () => {
// Get build button and modal elements
const buildBtn = document.getElementById("build-btn");
const stepperBuildBtn = document.getElementById("stepper-build"); // Added for stepper build button
const buildModal = document.getElementById("build-success-modal");
const buildModalClose = document.getElementById("build-success-modal-close");
const downloadZipBtn = document.getElementById("download-zip-btn");
const zipLoader = document.getElementById("zip-loader");
// Build action handler
async function handleBuildClick() {
showLoader("Building static site...");
// Trigger build on backend
const res = await fetch("/api/build", { method: "POST" });
const result = await res.json();
hideLoader();
if (result.status === "ok") {
// Show build success modal
if (buildModal) buildModal.style.display = "flex";
} else {
showToast(result.message || "❌ Build failed!", "error");
}
}
// Handle build button click
if (buildBtn) {
buildBtn.addEventListener("click", handleBuildClick);
}
// Handle stepper-build button click
if (stepperBuildBtn) {
stepperBuildBtn.addEventListener("click", handleBuildClick);
}
// Handle download zip button click
if (downloadZipBtn) {
downloadZipBtn.addEventListener("click", async () => {
if (zipLoader) zipLoader.style.display = "block";
downloadZipBtn.disabled = true;
// Request zip creation and download from backend
const res = await fetch("/download-output-zip", { method: "POST" });
if (res.ok) {
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "site_output.zip";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} else {
showToast("❌ Error creating ZIP", "error");
}
if (zipLoader) zipLoader.style.display = "none";
downloadZipBtn.disabled = false;
});
}
// Modal close logic
if (buildModal && buildModalClose) {
buildModalClose.onclick = () => {
buildModal.style.display = "none";
};
window.onclick = function(event) {
if (event.target === buildModal) {
buildModal.style.display = "none";
}
};
}
});

View File

@ -0,0 +1,438 @@
// --- Arrays to store gallery and hero images ---
let galleryImages = [];
let heroImages = [];
let allTags = []; // global tag list
// --- Load images from server on page load ---
async function loadData() {
try {
const galleryRes = await fetch('/api/gallery');
galleryImages = await galleryRes.json();
updateAllTags();
renderGallery();
const heroRes = await fetch('/api/hero');
heroImages = await heroRes.json();
renderHero();
} catch(err) {
console.error(err);
showToast("Error loading images!", "error");
}
}
// --- Update global tag list from galleryImages ---
function updateAllTags() {
allTags = [];
galleryImages.forEach(img => {
if (img.tags) img.tags.forEach(t => {
if (!allTags.includes(t)) allTags.push(t);
});
});
}
// --- Render gallery images with tags and delete buttons ---
function renderGallery() {
const container = document.getElementById('gallery');
container.innerHTML = '';
galleryImages.forEach((img, i) => {
const div = document.createElement('div');
div.className = 'photo flex-item flex-column';
div.innerHTML = `
<div class="flex-item">
<img src="/photos/${img.src}">
</div>
<div class="tags-display" data-index="${i}"></div>
<div class="flex-item flex-full">
<div class="flex-item flex-end">
<button onclick="showDeleteModal('gallery', ${i})">🗑 Delete</button>
</div>
<div class="tag-input" data-index="${i}"></div>
</div>
`;
container.appendChild(div);
renderTags(i, img.tags || []);
});
// Show/hide Remove All button
const removeAllBtn = document.getElementById('remove-all-gallery');
if (removeAllBtn) {
removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none';
}
}
// --- Render tags for a single image ---
function renderTags(imgIndex, tags) {
const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
tagsDisplay.innerHTML = '';
inputContainer.innerHTML = '';
tags.forEach(tag => {
const span = document.createElement('span');
span.className = 'tag';
span.textContent = tag;
const remove = document.createElement('span');
remove.className = 'remove-tag';
remove.textContent = '×';
remove.onclick = () => {
tags.splice(tags.indexOf(tag), 1);
updateTags(imgIndex, tags);
renderTags(imgIndex, tags);
};
span.appendChild(remove);
tagsDisplay.appendChild(span);
});
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Add tag...';
inputContainer.appendChild(input);
// --- Validate button ---
const validateBtn = document.createElement('button');
validateBtn.textContent = '✔️';
validateBtn.className = 'validate-tag-btn';
validateBtn.style.display = 'none'; // hidden by default
validateBtn.style.marginLeft = '4px';
inputContainer.appendChild(validateBtn);
const suggestionBox = document.createElement('ul');
suggestionBox.className = 'suggestions';
inputContainer.appendChild(suggestionBox);
let selectedIndex = -1;
const addTag = (tag) => {
tag = tag.trim();
if (!tag) return;
if (!tags.includes(tag)) tags.push(tag);
updateTags(imgIndex, tags);
renderTags(imgIndex, tags);
};
const updateSuggestions = () => {
const value = input.value.toLowerCase();
const allTagsFlat = galleryImages.flatMap(img => img.tags || []);
const tagCount = {};
allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1);
const allTagsSorted = Object.keys(tagCount)
.sort((a, b) => tagCount[b] - tagCount[a]);
const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t));
suggestionBox.innerHTML = '';
selectedIndex = -1;
if (suggestions.length) {
suggestionBox.style.display = 'block';
suggestions.forEach((s, idx) => {
const li = document.createElement('li');
li.style.fontStyle = 'italic';
li.style.textAlign = 'left';
const boldPart = `<b>${s.substring(0, input.value.length)}</b>`;
const rest = s.substring(input.value.length);
li.innerHTML = boldPart + rest;
li.addEventListener('mousedown', (e) => {
e.preventDefault();
addTag(s);
input.value = '';
input.focus();
updateSuggestions();
});
li.onmouseover = () => selectedIndex = idx;
suggestionBox.appendChild(li);
});
} else {
suggestionBox.style.display = 'none';
}
};
input.addEventListener('input', () => {
updateSuggestions();
validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
});
input.addEventListener('focus', () => {
updateSuggestions();
validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
});
input.addEventListener('keydown', (e) => {
const items = suggestionBox.querySelectorAll('li');
if (e.key === 'ArrowDown') {
e.preventDefault();
if (!items.length) return;
selectedIndex = (selectedIndex + 1) % items.length;
items.forEach((li, i) => li.classList.toggle('selected', i === selectedIndex));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (!items.length) return;
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
items.forEach((li, i) => li.classList.toggle('selected', i === selectedIndex));
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && items[selectedIndex]) {
addTag(items[selectedIndex].textContent);
} else {
addTag(input.value);
}
input.value = '';
updateSuggestions();
validateBtn.style.display = 'none';
} else if ([' ', ','].includes(e.key)) {
e.preventDefault();
addTag(input.value);
input.value = '';
updateSuggestions();
validateBtn.style.display = 'none';
}
});
input.addEventListener('blur', () => {
setTimeout(() => {
suggestionBox.style.display = 'none';
input.value = '';
validateBtn.style.display = 'none';
}, 150);
});
// --- Validate button action ---
validateBtn.onclick = () => {
if (input.value.trim()) {
addTag(input.value.trim());
input.value = '';
updateSuggestions();
validateBtn.style.display = 'none';
}
};
input.focus();
updateSuggestions();
}
// --- Update tags in galleryImages array ---
function updateTags(index, tags) {
galleryImages[index].tags = tags;
saveGallery();
}
// --- Render hero images with delete buttons ---
function renderHero() {
const container = document.getElementById('hero');
container.innerHTML = '';
heroImages.forEach((img, i) => {
const div = document.createElement('div');
div.className = 'photo flex-item flex-column';
div.innerHTML = `
<div class="flex-item">
<img src="/photos/${img.src}">
</div>
<div class="flex-item flex-full">
<div class="flex-item flex-end">
<button onclick="showDeleteModal('hero', ${i})">🗑 Delete</button>
</div>
</div>
`;
container.appendChild(div);
});
// Show/hide Remove All button
const removeAllBtn = document.getElementById('remove-all-hero');
if (removeAllBtn) {
removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
}
}
// --- Save gallery to server ---
async function saveGallery() {
await fetch('/api/gallery/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(galleryImages)
});
}
// --- Save hero to server ---
async function saveHero() {
await fetch('/api/hero/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(heroImages)
});
}
// --- Save all changes ---
async function saveChanges() {
await saveGallery();
await saveHero();
showToast('✅ Changes saved!', "success");
}
// --- Refresh gallery from folder ---
async function refreshGallery() {
await fetch('/api/gallery/refresh', { method: 'POST' });
await loadData();
showToast('🔄 Gallery updated from photos/gallery folder', "success");
}
// --- Refresh hero from folder ---
async function refreshHero() {
await fetch('/api/hero/refresh', { method: 'POST' });
await loadData();
showToast('🔄 Hero updated from photos/hero folder', "success");
}
// --- Show toast notification ---
function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("show"));
setTimeout(() => {
toast.classList.remove("show");
toast.addEventListener("transitionend", () => toast.remove());
}, duration);
}
let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
// --- Show delete confirmation modal ---
function showDeleteModal(type, index = null) {
pendingDelete = { type, index };
const modalText = document.getElementById('delete-modal-text');
if (type === 'gallery-all') {
modalText.textContent = "Are you sure you want to delete ALL gallery images?";
} else if (type === 'hero-all') {
modalText.textContent = "Are you sure you want to delete ALL hero images?";
} else {
modalText.textContent = "Are you sure you want to delete this image?";
}
document.getElementById('delete-modal').style.display = 'flex';
}
// --- Hide modal ---
function hideDeleteModal() {
document.getElementById('delete-modal').style.display = 'none';
pendingDelete = null;
}
// --- Confirm deletion ---
async function confirmDelete() {
if (!pendingDelete) return;
if (pendingDelete.type === 'gallery') {
await actuallyDeleteGalleryImage(pendingDelete.index);
} else if (pendingDelete.type === 'hero') {
await actuallyDeleteHeroImage(pendingDelete.index);
} else if (pendingDelete.type === 'gallery-all') {
await actuallyDeleteAllGalleryImages();
} else if (pendingDelete.type === 'hero-all') {
await actuallyDeleteAllHeroImages();
}
hideDeleteModal();
}
// --- Actual delete functions ---
async function actuallyDeleteGalleryImage(index) {
const img = galleryImages[index];
try {
const res = await fetch('/api/gallery/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ src: img.src.split('/').pop() })
});
const data = await res.json();
if (res.ok) {
galleryImages.splice(index, 1);
renderGallery();
await saveGallery();
showToast("✅ Gallery image deleted!", "success");
} else showToast("Error: " + data.error, "error");
} catch(err) {
console.error(err);
showToast("Server error!", "error");
}
}
async function actuallyDeleteHeroImage(index) {
const img = heroImages[index];
try {
const res = await fetch('/api/hero/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ src: img.src.split('/').pop() })
});
const data = await res.json();
if (res.ok) {
heroImages.splice(index, 1);
renderHero();
await saveHero();
showToast("✅ Hero image deleted!", "success");
} else showToast("Error: " + data.error, "error");
} catch(err) {
console.error(err);
showToast("Server error!", "error");
}
}
// --- Bulk delete functions ---
async function actuallyDeleteAllGalleryImages() {
try {
const res = await fetch('/api/gallery/delete_all', { method: 'POST' });
const data = await res.json();
if (res.ok) {
galleryImages = [];
renderGallery();
await saveGallery();
showToast("✅ All gallery images removed!", "success");
} else showToast("Error: " + data.error, "error");
} catch(err) {
console.error(err);
showToast("Server error!", "error");
}
}
async function actuallyDeleteAllHeroImages() {
try {
const res = await fetch('/api/hero/delete_all', { method: 'POST' });
const data = await res.json();
if (res.ok) {
heroImages = [];
renderHero();
await saveHero();
showToast("✅ All hero images removed!", "success");
} else showToast("Error: " + data.error, "error");
} catch(err) {
console.error(err);
showToast("Server error!", "error");
}
}
// --- Modal event listeners and bulk delete buttons ---
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('delete-modal-close').onclick = hideDeleteModal;
document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
document.getElementById('delete-modal-confirm').onclick = confirmDelete;
// Bulk delete buttons
const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
const removeAllHeroBtn = document.getElementById('remove-all-hero');
if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
});
// --- Initialize ---
loadData();

456
src/webui/js/site-info.js Normal file
View File

@ -0,0 +1,456 @@
function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("show"));
setTimeout(() => {
toast.classList.remove("show");
toast.addEventListener("transitionend", () => toast.remove());
}, duration);
}
// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.classList.add("active");
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.classList.remove("active");
}
document.addEventListener("DOMContentLoaded", () => {
// Form and menu logic
const form = document.getElementById("site-info-form");
const menuList = document.getElementById("menu-items-list");
const addMenuBtn = document.getElementById("add-menu-item");
let menuItems = [];
// Render menu items
function renderMenuItems() {
menuList.innerHTML = "";
menuItems.forEach((item, idx) => {
const div = document.createElement("div");
div.style.display = "flex";
div.style.gap = "8px";
div.style.marginBottom = "6px";
div.innerHTML = `
<input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
<input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
<button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
`;
menuList.appendChild(div);
});
}
// Update menu items from inputs
function updateMenuItemsFromInputs() {
const inputs = menuList.querySelectorAll("input");
const items = [];
for (let i = 0; i < inputs.length; i += 2) {
const label = inputs[i].value.trim();
const href = inputs[i + 1].value.trim();
if (label || href) items.push({ label, href });
}
menuItems = items;
}
// Intellectual property paragraphs logic
const ipList = document.getElementById("ip-list");
const addIpBtn = document.getElementById("add-ip-paragraph");
let ipParagraphs = [];
// Render IP paragraphs
function renderIpParagraphs() {
ipList.innerHTML = "";
ipParagraphs.forEach((item, idx) => {
const div = document.createElement("div");
div.style.display = "flex";
div.style.gap = "8px";
div.style.marginBottom = "6px";
div.innerHTML = `
<textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
<button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
`;
ipList.appendChild(div);
});
}
// Update IP paragraphs from textareas
function updateIpParagraphsFromInputs() {
const textareas = ipList.querySelectorAll("textarea");
ipParagraphs = Array.from(textareas).map(textarea => ({
paragraph: textarea.value.trim()
})).filter(item => item.paragraph !== "");
}
// Build options
const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
// Theme select
const themeSelect = document.getElementById("theme-select");
// Thumbnail upload and modal logic
const thumbnailInput = form?.elements["social.thumbnail"];
const thumbnailUpload = document.getElementById("thumbnail-upload");
const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
const thumbnailPreview = document.getElementById("thumbnail-preview");
const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
// Modal elements for delete confirmation
const deleteModal = document.getElementById("delete-modal");
const deleteModalClose = document.getElementById("delete-modal-close");
const deleteModalConfirm = document.getElementById("delete-modal-confirm");
const deleteModalCancel = document.getElementById("delete-modal-cancel");
// Modal elements for theme deletion
const deleteThemeModal = document.getElementById("delete-theme-modal");
const deleteThemeModalClose = document.getElementById("delete-theme-modal-close");
const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm");
const deleteThemeModalCancel = document.getElementById("delete-theme-modal-cancel");
const deleteThemeModalText = document.getElementById("delete-theme-modal-text");
let themeToDelete = null;
// Show/hide thumbnail preview, remove button, and choose button
function updateThumbnailPreview(src) {
if (thumbnailPreview) {
thumbnailPreview.src = src || "";
thumbnailPreview.style.display = src ? "block" : "none";
}
if (removeThumbnailBtn) {
removeThumbnailBtn.style.display = src ? "inline-block" : "none";
}
if (chooseThumbnailBtn) {
chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
}
}
// Choose thumbnail button triggers file input
if (chooseThumbnailBtn && thumbnailUpload) {
chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click());
}
// Handle thumbnail upload and refresh preview (with cache busting)
if (thumbnailUpload) {
thumbnailUpload.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
showLoader("Uploading thumbnail...");
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
const result = await res.json();
hideLoader();
if (result.status === "ok") {
if (thumbnailInput) thumbnailInput.value = result.filename;
updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
showToast("✅ Thumbnail uploaded!", "success");
} else {
showToast("❌ Error uploading thumbnail", "error");
}
});
}
// Remove thumbnail button triggers modal
if (removeThumbnailBtn) {
removeThumbnailBtn.addEventListener("click", () => {
deleteModal.style.display = "flex";
});
}
// Modal logic for thumbnail deletion
if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
deleteModalClose.onclick = deleteModalCancel.onclick = () => {
deleteModal.style.display = "none";
};
window.onclick = function(event) {
if (event.target === deleteModal) {
deleteModal.style.display = "none";
}
};
deleteModalConfirm.onclick = async () => {
const res = await fetch("/api/thumbnail/remove", { method: "POST" });
const result = await res.json();
if (result.status === "ok") {
if (thumbnailInput) thumbnailInput.value = "";
updateThumbnailPreview("");
showToast("✅ Thumbnail removed!", "success");
} else {
showToast("❌ Error removing thumbnail", "error");
}
deleteModal.style.display = "none";
};
}
// Theme upload logic (custom theme folder)
const themeUpload = document.getElementById("theme-upload");
const chooseThemeBtn = document.getElementById("choose-theme-btn");
if (chooseThemeBtn && themeUpload) {
chooseThemeBtn.addEventListener("click", () => themeUpload.click());
themeUpload.addEventListener("change", async (e) => {
const files = Array.from(e.target.files);
if (files.length === 0) return;
showLoader("Uploading theme...");
const formData = new FormData();
files.forEach(file => {
formData.append("files", file, file.webkitRelativePath || file.name);
});
const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
const result = await res.json();
hideLoader();
if (result.status === "ok") {
showToast("✅ Theme uploaded!", "success");
// Refresh theme select after upload
fetch("/api/themes")
.then(res => res.json())
.then(themes => {
themeSelect.innerHTML = "";
themes.forEach(theme => {
const option = document.createElement("option");
option.value = theme;
option.textContent = theme;
themeSelect.appendChild(option);
});
});
} else {
showToast("❌ Error uploading theme", "error");
}
});
}
// Remove theme button triggers modal
const removeThemeBtn = document.getElementById("remove-theme-btn");
if (removeThemeBtn && themeSelect) {
removeThemeBtn.addEventListener("click", () => {
const theme = themeSelect.value;
if (!theme) return showToast("❌ No theme selected", "error");
if (["modern", "classic"].includes(theme)) {
showToast("❌ Cannot remove default theme", "error");
return;
}
themeToDelete = theme;
deleteThemeModalText.textContent = `Are you sure you want to remove theme "${theme}"?`;
deleteThemeModal.style.display = "flex";
});
}
// Modal logic for theme deletion
if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) {
deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => {
deleteThemeModal.style.display = "none";
themeToDelete = null;
};
window.onclick = function(event) {
if (event.target === deleteThemeModal) {
deleteThemeModal.style.display = "none";
themeToDelete = null;
}
};
deleteThemeModalConfirm.onclick = async () => {
if (!themeToDelete) return;
showLoader("Removing theme...");
const res = await fetch("/api/theme/remove", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme: themeToDelete })
});
const result = await res.json();
hideLoader();
if (result.status === "ok") {
showToast("✅ Theme removed!", "success");
// Refresh theme select
fetch("/api/themes")
.then(res => res.json())
.then(themes => {
themeSelect.innerHTML = "";
themes.forEach(theme => {
const option = document.createElement("option");
option.value = theme;
option.textContent = theme;
themeSelect.appendChild(option);
});
});
} else {
showToast(result.error || "❌ Error removing theme", "error");
}
deleteThemeModal.style.display = "none";
themeToDelete = null;
};
}
// Fetch theme list and populate select
if (themeSelect) {
fetch("/api/themes")
.then(res => res.json())
.then(themes => {
themeSelect.innerHTML = "";
themes.forEach(theme => {
const option = document.createElement("option");
option.value = theme;
option.textContent = theme;
themeSelect.appendChild(option);
});
// Set selected value after loading config
fetch("/api/site-info")
.then(res => res.json())
.then(data => {
themeSelect.value = data.build?.theme || "";
});
});
}
// Load config from server and populate form
if (form) {
fetch("/api/site-info")
.then(res => res.json())
.then(data => {
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
? data.legals.intellectual_property
: [];
renderIpParagraphs();
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
renderMenuItems();
form.elements["info.title"].value = data.info?.title || "";
form.elements["info.subtitle"].value = data.info?.subtitle || "";
form.elements["info.description"].value = data.info?.description || "";
form.elements["info.canonical"].value = data.info?.canonical || "";
form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
form.elements["info.author"].value = data.info?.author || "";
form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
form.elements["footer.copyright"].value = data.footer?.copyright || "";
form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
if (themeSelect) {
themeSelect.value = data.build?.theme || "";
}
form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
form.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
// Build checkboxes
if (convertImagesCheckbox) {
convertImagesCheckbox.checked = !!data.build?.convert_images;
}
if (resizeImagesCheckbox) {
resizeImagesCheckbox.checked = !!data.build?.resize_images;
}
});
}
// Add menu item
if (addMenuBtn) {
addMenuBtn.addEventListener("click", () => {
menuItems.push({ label: "", href: "" });
renderMenuItems();
});
}
// Remove menu item
menuList.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-menu-item")) {
const idx = parseInt(e.target.getAttribute("data-idx"));
menuItems.splice(idx, 1);
renderMenuItems();
}
});
// Update menuItems on input change
menuList.addEventListener("input", () => {
updateMenuItemsFromInputs();
});
// Add paragraph
if (addIpBtn) {
addIpBtn.addEventListener("click", () => {
ipParagraphs.push({ paragraph: "" });
renderIpParagraphs();
});
}
// Remove paragraph
ipList.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-ip-paragraph")) {
const idx = parseInt(e.target.getAttribute("data-idx"));
ipParagraphs.splice(idx, 1);
renderIpParagraphs();
}
});
// Update ipParagraphs on input change
ipList.addEventListener("input", () => {
updateIpParagraphsFromInputs();
});
// Save config to server
if (form) {
form.addEventListener("submit", async (e) => {
e.preventDefault();
updateMenuItemsFromInputs();
updateIpParagraphsFromInputs();
// Check if thumbnail is set before saving (uploaded or present in input)
if (!thumbnailInput || !thumbnailInput.value) {
showLoader("Saving...");
showToast("❌ Thumbnail is required.", "error");
hideLoader();
return;
}
const build = {
theme: themeSelect ? themeSelect.value : "",
convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
};
const payload = {
info: {
title: form.elements["info.title"].value,
subtitle: form.elements["info.subtitle"].value,
description: form.elements["info.description"].value,
canonical: form.elements["info.canonical"].value,
keywords: form.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
author: form.elements["info.author"].value
},
social: {
instagram_url: form.elements["social.instagram_url"].value,
thumbnail: thumbnailInput ? thumbnailInput.value : ""
},
menu: {
items: menuItems
},
footer: {
copyright: form.elements["footer.copyright"].value,
legal_label: form.elements["footer.legal_label"].value
},
build,
legals: {
hoster_name: form.elements["legals.hoster_name"].value,
hoster_address: form.elements["legals.hoster_address"].value,
hoster_contact: form.elements["legals.hoster_contact"].value,
intellectual_property: ipParagraphs
}
};
// --- REMOVE loader for save ---
// showLoader("Saving...");
const res = await fetch("/api/site-info", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.status === "ok") {
showToast("✅ Site info saved!", "success");
} else {
showToast("❌ Error saving site info", "error");
}
});
}
});

View File

@ -0,0 +1,467 @@
async function fetchThemeInfo() {
const res = await fetch("/api/theme-info");
return await res.json();
}
async function fetchLocalFonts(theme) {
const res = await fetch(`/api/local-fonts?theme=${encodeURIComponent(theme)}`);
return await res.json();
}
async function removeFont(theme, font) {
const res = await fetch("/api/font/remove", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme, font })
});
return await res.json();
}
function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("show"));
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => container.removeChild(toast), 300);
}, duration);
}
// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.classList.add("active");
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.classList.remove("active");
}
// --- Color Picker
function setupColorPicker(colorId, btnId, textId, initial) {
const colorInput = document.getElementById(colorId);
const colorBtn = document.getElementById(btnId);
const textInput = document.getElementById(textId);
colorInput.value = initial;
colorBtn.style.background = initial;
textInput.value = initial.toUpperCase();
colorInput.addEventListener("input", () => {
colorBtn.style.background = colorInput.value;
textInput.value = colorInput.value.toUpperCase();
});
textInput.addEventListener("input", () => {
if (/^#[0-9A-F]{6}$/i.test(textInput.value)) {
colorInput.value = textInput.value;
colorBtn.style.background = textInput.value;
}
});
}
function setFontDropdown(selectId, value, options) {
const select = document.getElementById(selectId);
if (!select) return;
select.innerHTML = options.map(opt =>
`<option value="${opt}"${opt === value ? " selected" : ""}>${opt}</option>`
).join("");
}
function setFallbackDropdown(selectId, value) {
const select = document.getElementById(selectId);
if (!select) return;
select.value = (value === "serif" || value === "sans-serif") ? value : "sans-serif";
}
function setTextInput(inputId, value) {
const input = document.getElementById(inputId);
if (input) input.value = value;
}
function renderGoogleFonts(googleFonts) {
const container = document.getElementById("google-fonts-fields");
container.innerHTML = "";
googleFonts.forEach((font, idx) => {
container.innerHTML += `
<div class="input-field" data-idx="${idx}">
<label>Family</label>
<input type="text" name="google_fonts[${idx}][family]" value="${font.family || ""}">
<label>Weights (comma separated)</label>
<input type="text" name="google_fonts[${idx}][weights]" value="${(font.weights || []).join(',')}">
<button type="button" class="remove-google-font remove-btn" data-idx="${idx}"> 🗑️ Remove</button>
</div>
`;
});
}
function renderLocalFonts(fonts) {
const listDiv = document.getElementById("local-fonts-list");
if (!listDiv) return;
listDiv.innerHTML = "";
fonts.forEach(font => {
listDiv.innerHTML += `
<div class="font-item">
<span class="font-name">${font}</span>
<button type="button" class="remove-font-btn danger remove-btn" data-font="${font}">🗑️</button>
</div>
`;
});
}
document.addEventListener("DOMContentLoaded", async () => {
const themeInfo = await fetchThemeInfo();
const themeNameSpan = document.getElementById("current-theme");
if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
const themeYaml = themeInfo.theme_yaml;
const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : [];
let localFonts = await fetchLocalFonts(themeInfo.theme_name);
// Colors
if (themeYaml.colors) {
setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", themeYaml.colors.primary || "#0065a1");
setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384");
setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0");
setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", themeYaml.colors.accent || "#ffc700");
setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", themeYaml.colors.text_dark || "#616161");
setupColorPicker("color-background", "color-background-btn", "color-background-text", themeYaml.colors.background || "#fff");
setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", themeYaml.colors.browser_color || "#fff");
}
// Fonts
function refreshFontDropdowns() {
setFontDropdown("font-primary", document.getElementById("font-primary").value, [
...googleFonts.map(f => f.family),
...localFonts
]);
setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [
...googleFonts.map(f => f.family),
...localFonts
]);
}
if (themeYaml.fonts) {
setFontDropdown("font-primary", themeYaml.fonts.primary?.name || "Lato", [
...googleFonts.map(f => f.family),
...localFonts
]);
setFallbackDropdown("font-primary-fallback", themeYaml.fonts.primary?.fallback || "sans-serif");
setFontDropdown("font-secondary", themeYaml.fonts.secondary?.name || "Montserrat", [
...googleFonts.map(f => f.family),
...localFonts
]);
setFallbackDropdown("font-secondary-fallback", themeYaml.fonts.secondary?.fallback || "serif");
}
// Font upload logic
const fontUploadInput = document.getElementById("font-upload");
const chooseFontBtn = document.getElementById("choose-font-btn");
const fontUploadStatus = document.getElementById("font-upload-status");
const localFontsList = document.getElementById("local-fonts-list");
// Modal logic for font deletion
const deleteFontModal = document.getElementById("delete-font-modal");
const deleteFontModalClose = document.getElementById("delete-font-modal-close");
const deleteFontModalConfirm = document.getElementById("delete-font-modal-confirm");
const deleteFontModalCancel = document.getElementById("delete-font-modal-cancel");
let fontToDelete = null;
function refreshLocalFonts() {
renderLocalFonts(localFonts);
refreshFontDropdowns();
}
if (chooseFontBtn && fontUploadInput) {
chooseFontBtn.addEventListener("click", () => fontUploadInput.click());
}
if (fontUploadInput) {
fontUploadInput.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (!["woff", "woff2"].includes(ext)) {
showToast("Only .woff and .woff2 fonts are allowed.", "error");
return;
}
showLoader("Uploading font...");
const formData = new FormData();
formData.append("file", file);
formData.append("theme", themeInfo.theme_name);
const res = await fetch("/api/font/upload", { method: "POST", body: formData });
const result = await res.json();
hideLoader();
if (result.status === "ok") {
showToast("✅ Font uploaded!", "success");
localFonts = await fetchLocalFonts(themeInfo.theme_name);
refreshLocalFonts();
} else {
showToast("Error uploading font.", "error");
}
});
}
// Remove font button triggers modal
if (localFontsList) {
localFontsList.addEventListener("click", (e) => {
if (e.target.classList.contains("remove-font-btn")) {
fontToDelete = e.target.dataset.font;
document.getElementById("delete-font-modal-text").textContent =
`Are you sure you want to remove the font "${fontToDelete}"?`;
deleteFontModal.style.display = "flex";
}
});
}
// Modal logic for font deletion
if (deleteFontModal && deleteFontModalClose && deleteFontModalConfirm && deleteFontModalCancel) {
deleteFontModalClose.onclick = deleteFontModalCancel.onclick = () => {
deleteFontModal.style.display = "none";
fontToDelete = null;
};
window.onclick = function(event) {
if (event.target === deleteFontModal) {
deleteFontModal.style.display = "none";
fontToDelete = null;
}
};
deleteFontModalConfirm.onclick = async () => {
if (!fontToDelete) return;
showLoader("Removing font...");
const result = await removeFont(themeInfo.theme_name, fontToDelete);
hideLoader();
if (result.status === "ok") {
showToast("Font removed!", "success");
localFonts = await fetchLocalFonts(themeInfo.theme_name);
refreshLocalFonts();
} else {
showToast("Error removing font.", "error");
}
deleteFontModal.style.display = "none";
fontToDelete = null;
};
}
// Initial render of local fonts
refreshLocalFonts();
// Favicon logic
const faviconInput = document.getElementById("favicon-path");
const faviconUpload = document.getElementById("favicon-upload");
const chooseFaviconBtn = document.getElementById("choose-favicon-btn");
const faviconPreview = document.getElementById("favicon-preview");
const removeFaviconBtn = document.getElementById("remove-favicon-btn");
const deleteFaviconModal = document.getElementById("delete-favicon-modal");
const deleteFaviconModalClose = document.getElementById("delete-favicon-modal-close");
const deleteFaviconModalConfirm = document.getElementById("delete-favicon-modal-confirm");
const deleteFaviconModalCancel = document.getElementById("delete-favicon-modal-cancel");
function updateFaviconPreview(src) {
if (faviconPreview) {
faviconPreview.src = src || "";
faviconPreview.style.display = src ? "block" : "none";
}
if (removeFaviconBtn) {
removeFaviconBtn.style.display = src ? "block" : "none";
}
if (chooseFaviconBtn) {
chooseFaviconBtn.style.display = src ? "none" : "block";
}
}
if (chooseFaviconBtn && faviconUpload) {
chooseFaviconBtn.addEventListener("click", () => faviconUpload.click());
}
if (faviconUpload) {
faviconUpload.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (!["png", "jpg", "jpeg", "ico"].includes(ext)) {
showToast("Invalid file type for favicon.", "error");
return;
}
showLoader("Uploading favicon...");
const formData = new FormData();
formData.append("file", file);
formData.append("theme", themeInfo.theme_name);
const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
const result = await res.json();
hideLoader();
if (result.status === "ok") {
faviconInput.value = result.filename;
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
showToast("✅ Favicon uploaded!", "success");
} else {
showToast("Error uploading favicon", "error");
}
});
}
if (removeFaviconBtn) {
removeFaviconBtn.addEventListener("click", () => {
deleteFaviconModal.style.display = "flex";
});
}
if (deleteFaviconModal && deleteFaviconModalClose && deleteFaviconModalConfirm && deleteFaviconModalCancel) {
deleteFaviconModalClose.onclick = deleteFaviconModalCancel.onclick = () => {
deleteFaviconModal.style.display = "none";
};
window.onclick = function(event) {
if (event.target === deleteFaviconModal) {
deleteFaviconModal.style.display = "none";
}
};
deleteFaviconModalConfirm.onclick = async () => {
showLoader("Removing favicon...");
const res = await fetch("/api/favicon/remove", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme: themeInfo.theme_name })
});
const result = await res.json();
hideLoader();
if (result.status === "ok") {
faviconInput.value = "";
updateFaviconPreview("");
showToast("✅ Favicon removed!", "success");
} else {
showToast("Error removing favicon", "error");
}
deleteFaviconModal.style.display = "none";
};
}
if (themeYaml.favicon && themeYaml.favicon.path) {
faviconInput.value = themeYaml.favicon.path;
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`);
} else {
updateFaviconPreview("");
}
// Google Fonts
renderGoogleFonts(googleFonts);
// Add Google Font
const addGoogleFontBtn = document.getElementById("add-google-font");
if (addGoogleFontBtn) {
addGoogleFontBtn.addEventListener("click", async () => {
googleFonts.push({ family: "", weights: [] });
await fetch("/api/theme-google-fonts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
});
const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0;
googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts);
refreshFontDropdowns();
});
}
const googleFontsFields = document.getElementById("google-fonts-fields");
if (googleFontsFields) {
googleFontsFields.addEventListener("blur", async (e) => {
if (
e.target.name &&
(e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
) {
const fontFields = googleFontsFields.querySelectorAll(".input-field");
googleFonts.length = 0;
fontFields.forEach(field => {
const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value.trim();
const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
.split(",").map(w => w.trim()).filter(Boolean);
googleFonts.push({ family, weights });
});
await fetch("/api/theme-google-fonts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
});
const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0;
googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts);
refreshFontDropdowns();
}
}, true);
googleFontsFields.addEventListener("click", async (e) => {
if (e.target.classList.contains("remove-google-font")) {
const idx = Number(e.target.dataset.idx);
googleFonts.splice(idx, 1);
await fetch("/api/theme-google-fonts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
});
const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0;
googleFonts.push(...updatedGoogleFonts);
renderGoogleFonts(googleFonts);
refreshFontDropdowns();
}
});
}
document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
e.preventDefault();
showLoader("Saving theme...");
const data = {};
data.colors = {
primary: document.getElementById("color-primary-text").value,
primary_dark: document.getElementById("color-primary-dark-text").value,
secondary: document.getElementById("color-secondary-text").value,
accent: document.getElementById("color-accent-text").value,
text_dark: document.getElementById("color-text-dark-text").value,
background: document.getElementById("color-background-text").value,
browser_color: document.getElementById("color-browser-color-text").value
};
data.fonts = {
primary: {
name: document.getElementById("font-primary").value,
fallback: document.getElementById("font-primary-fallback").value
},
secondary: {
name: document.getElementById("font-secondary").value,
fallback: document.getElementById("font-secondary-fallback").value
}
};
data.favicon = {
path: faviconInput.value
};
data.google_fonts = [];
document.querySelectorAll("#google-fonts-fields .input-field").forEach(field => {
const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value;
const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
.split(",").map(w => w.trim()).filter(w => w);
if (family) data.google_fonts.push({ family, weights });
});
const res = await fetch("/api/theme-info", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
});
hideLoader();
if (res.ok) {
showToast("✅ Theme saved!", "success");
} else {
showToast("Error saving theme.", "error");
}
});
});

64
src/webui/js/upload.js Normal file
View File

@ -0,0 +1,64 @@
// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.classList.add("active");
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.classList.remove("active");
}
// --- Upload gallery images ---
const galleryInput = document.getElementById('upload-gallery');
if (galleryInput) {
galleryInput.addEventListener('change', async (e) => {
const files = e.target.files;
if (!files.length) return;
showLoader("Uploading photos...");
const formData = new FormData();
for (const file of files) formData.append('files', file);
try {
const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
const data = await res.json();
hideLoader();
if (res.ok) {
showToast(`${data.uploaded.length} gallery image(s) uploaded!`, "success");
if (typeof refreshGallery === "function") refreshGallery();
} else showToast('Error: ' + data.error, "error");
} catch(err) {
hideLoader();
console.error(err);
showToast('Server error!', "error");
} finally { e.target.value = ''; }
});
}
// --- Upload hero images ---
const heroInput = document.getElementById('upload-hero');
if (heroInput) {
heroInput.addEventListener('change', async (e) => {
const files = e.target.files;
if (!files.length) return;
showLoader("Uploading hero photos...");
const formData = new FormData();
for (const file of files) formData.append('files', file);
try {
const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
const data = await res.json();
hideLoader();
if (res.ok) {
showToast(`${data.uploaded.length} hero image(s) uploaded!`, "success");
if (typeof refreshHero === "function") refreshHero();
} else showToast('Error: ' + data.error, "error");
} catch(err) {
hideLoader();
console.error(err);
showToast('Server error!', "error");
} finally { e.target.value = ''; }
});
}

View File

@ -0,0 +1,182 @@
{% extends "template/base.html" %}
{% block title %}Lumeex - Site Info{% endblock %}
{% block content %}
<h1>Edit Site Info</h1>
<form id="site-info-form">
<!-- Info Section -->
<fieldset>
<h2>Info</h2>
<p>Set the basic information for your site and SEO</p>
<div class="fields">
<div class="input-field">
<label>Title</label>
<input type="text" name="info.title" placeholder="Your site title" required>
</div>
<div class="input-field">
<label>Subtitle</label>
<input type="text" name="info.subtitle" placeholder="Your site subtitle" required>
</div>
<div class="input-field">
<label>Description</label>
<input type="text" name="info.description" placeholder="Your site description" required>
</div>
<div class="input-field">
<label>Canonical URL</label>
<input type="text" name="info.canonical" placeholder="https://yoursite.com" required>
</div>
<div class="input-field">
<label>Keywords (comma separated)</label>
<input type="text" name="info.keywords" placeholder="photo, gallery, photography" required>
</div>
<div class="input-field">
<label>Author</label>
<input type="text" name="info.author" placeholder="Your Name" required>
</div>
</div>
</fieldset>
<!-- Social Section -->
<fieldset>
<h2>Social</h2>
<p>Set your social media links and thumbnail for link sharing</p>
<div class="fields">
<div class="input-field">
<label>Instagram URL</label>
<input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
<label class="thumbnail-form-label">Thumbnail</label>
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
<div class="thumbnail-form">
<input type="hidden" name="social.thumbnail" id="social-thumbnail">
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
<button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
</div>
</div>
</div>
</fieldset>
<!-- Menu Section -->
<fieldset>
<h2>Menu</h2>
<p>Manage your site menu items. You can use tag combination to propose custom filters</p>
<div class="fields">
<div class="input-field" style="flex: 1 1 100%;">
<div id="menu-items-list"></div>
<button type="button" id="add-menu-item">+ Add menu item</button>
</div>
</div>
</fieldset>
<!-- Footer Section -->
<fieldset>
<h2>Footer</h2>
<p>Set your copyright informations and legal link name</p>
<div class="fields">
<div class="input-field">
<label>Copyright</label>
<input type="text" name="footer.copyright" required>
</div>
<div class="input-field">
<label>Legal Label</label>
<input type="text" name="footer.legal_label" re>
</div>
</div>
</fieldset>
<!-- Legals Section -->
<fieldset>
<h2>Legals</h2>
<p>Set your legal informations</p>
<div class="fields">
<div class="input-field">
<label>Hoster Name</label>
<input type="text" name="legals.hoster_name" placeholder="Name" required>
</div>
<div class="input-field">
<label>Hoster Address</label>
<input type="text" name="legals.hoster_address" placeholder="Street, Postal Code, City, Country" required>
</div>
<div class="input-field">
<label>Hoster Contact</label>
<input type="text" name="legals.hoster_contact" placeholder="Email or/and Phone" required>
</div>
<div class="input-field" style="flex: 1 1 100%;">
<label>Intellectual Property</label>
<div id="ip-list"></div>
<button type="button" id="add-ip-paragraph">+ Add paragraph</button>
</div>
</div>
</fieldset>
<!-- Build Section -->
<fieldset>
<h2>Build</h2>
<p>Select a theme from the dropdown menu or add your custom theme folder</p>
<div class="fields">
<div class="input-field">
<label>Theme</label>
<select name="build.theme" id="theme-select" required></select>
<button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
<input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
<button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
<label class="thumbnail-form-label">Images processing</label>
<p>If checked, images will be converted for web and resized to fit the theme</p>
<label>
<input class="thumbnail-form-label" type="checkbox" name="build.convert_images" id="convert-images-checkbox">
Convert images
</label>
<label>
<input type="checkbox" name="build.resize_images" id="resize-images-checkbox">
Resize images
</label>
</div>
</div>
</fieldset>
<button type="submit">Save</button>
</form>
<div class="section">
<h2>Steps</h2>
<p> Follow the steps to generate your static gallery</p>
<ul id="stepper">
<li><a href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a class="step-active" href="/site-info">Configure site info</a></li>
<div></div>
<li><a href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div>
</div>
<!-- Delete thumbnail confirmation modal-->
<div class="content-inner">
<div id="delete-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3>
<p id="delete-modal-text">Are you sure you want to remove this thumbnail?</p>
<div class="modal-actions">
<button id="delete-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-modal-cancel" class="modal-btn">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Delete theme confirmation modal -->
<div class="content-inner">
<div id="delete-theme-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-theme-modal-close" class="modal-close">&times;</span>
<h3>Confirm Theme Deletion</h3>
<p id="delete-theme-modal-text">Are you sure you want to remove this theme?</p>
<div class="modal-actions">
<button id="delete-theme-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-theme-modal-cancel" class="modal-btn">Cancel</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/site-info.js') }}"></script>
{% endblock %}

1039
src/webui/style/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<title>{% block title %}Lumeex{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
<meta name="darkreader-lock">
</head>
<body>
<!-- Top bar -->
<div class="nav-bar">
<div class="content-inner nav">
<div class="nav-header">
<a href="/" class="nav-title">
<img src="{{ url_for('static', filename='img/logo.svg') }}">
</a>
</div>
<!-- Burger toggle input and label -->
<input type="checkbox" id="nav-toggle" class="nav-toggle" hidden>
<label for="nav-toggle" class="nav-burger">
<span></span>
<span></span>
<span></span>
</label>
<div class="nav-links">
<ul class="nav-list">
<li class="nav-item"><a href="/gallery-editor">Gallery</a></li>
<li class="nav-item"><a href="/site-info">Site info</a></li>
<li class="nav-item"><a href="/theme-editor">Theme info</a></li>
<li class="nav-item">
<button id="build-btn" class="button">🚀 Build!</button>
</li>
</ul>
</div>
</div>
</div>
<!-- Toast container for notifications -->
<div class="content-inner first-content">
<div class="inner">
<div id="toast-container"></div>
<!-- Page content -->
{% block content %}{% endblock %}
<!-- Build success modal -->
<div class="content-inner">
<div id="build-success-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="build-success-modal-close" class="modal-close">&times;</span>
<h3>✅ Build completed!</h3>
<p>Your files are available in the output folder.</p>
<button id="download-zip-btn" class="modal-btn">Download ZIP</button>
<div id="zip-loader" style="display:none;">Creating ZIP...</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div id="footer">
<div class="content-inner">
<div class="inner">
<div class="footer-container">
<div class="footer-credit">
<p><a href="https//lumeex.djeex.fr"><span class="lum-first">Lum</span><span class="lum-second">eex</span> v{{ lumeex_version }}</a> — © 2025</p>
</div>
<div class="footer-links">
<a class="footer-link documentation" href="https://lumeex.djeex.fr"><span class="icon"><img src="/img/favicon.svg"></span><span class="icon-text">Documentation</span></a>
<a class="footer-link gitea" href="https://gitea.com/Djeex/lumeex"><span class="icon"><img src="/img/gitea.svg"></span><span class="icon-text">Giteex</span></a>
<a class="footer-link github" href="https://github.com/Djeex/lumeex"><span class="icon"><img src="/img/github.svg"></span><span class="icon-text">Github</span></a>
</div>
</div>
</div>
</div>
</div>
<!-- Loader -->
<div id="global-loader">
<div class="loader-inner">
<div class="loader-spinner"></div>
<div id="loader-text">Uploading...</div>
</div>
</div>
<!-- Scripts -->
<script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,182 @@
{% extends "template/base.html" %}
{% block title %}Lumeex - Theme Editor{% endblock %}
{% block content %}
<h1>Edit Theme</h1>
<!-- Show current theme -->
<div class="theme-info">
<strong>Current theme:</strong> <span id="current-theme"></span>
</div>
<form id="theme-editor-form">
<!-- Colors Section -->
<fieldset id="color-picker">
<h2>Colors</h2>
<p>Set the color values for your theme</p>
<div class="fields">
<!-- Example for one color field, repeat for all -->
<div class="input-field">
<label>Primary</label>
<div class="fields color-fields">
<button type="button" id="color-primary-btn" class="color-btn"></button>
<input type="color" name="colors.primary" id="color-primary" class="color-input">
<input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Primary Dark</label>
<div class="fields color-fields">
<button type="button" id="color-primary-dark-btn" class="color-btn"></button>
<input type="color" name="colors.primary_dark" id="color-primary-dark" class="color-input">
<input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Secondary</label>
<div class="fields color-fields">
<button type="button" id="color-secondary-btn" class="color-btn"></button>
<input type="color" name="colors.secondary" id="color-secondary" class="color-input">
<input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Accent</label>
<div class="fields color-fields">
<button type="button" id="color-accent-btn" class="color-btn"></button>
<input type="color" name="colors.accent" id="color-accent" class="color-input">
<input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Text Dark</label>
<div class="fields color-fields">
<button type="button" id="color-text-dark-btn" class="color-btn"></button>
<input type="color" name="colors.text_dark" id="color-text-dark" class="color-input">
<input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Background</label>
<div class="fields color-fields">
<button type="button" id="color-background-btn" class="color-btn"></button>
<input type="color" name="colors.background" id="color-background" class="color-input">
<input type="text" name="colors.background_text" id="color-background-text" style="width:100px;">
</div>
</div>
<div class="input-field">
<label>Browser Color</label>
<div class="fields color-fields">
<button type="button" id="color-browser-color-btn" class="color-btn"></button>
<input type="color" name="colors.browser_color" id="color-browser-color" class="color-input">
<input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;">
</div>
</div>
</div>
</fieldset>
<!-- Google Fonts Section -->
<fieldset>
<h2>Google Fonts</h2>
<p>Add Google Fonts to your theme</p>
<div class="fields" id="google-fonts-fields">
<!-- JS will render font family and weights inputs here -->
</div>
<button type="button" id="add-google-font">Add Google Font</button>
</fieldset>
<!-- Custom Font Upload Section -->
<fieldset>
<h2>Upload Custom Font</h2>
<p>Supported formats: .woff, .woff2</p>
<input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
<div id="local-fonts-list" class="font-list"></div>
<button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
</fieldset>
<!-- Fonts Section -->
<fieldset>
<h2>Fonts</h2>
<p>Select where to apply your fonts</p>
<div class="fields">
<div class="input-field">
<label>Primary Font</label>
<select name="fonts.primary.name" id="font-primary"></select>
<label>Fallback</label>
<select name="fonts.primary.fallback" id="font-primary-fallback">
<option value="sans-serif">sans-serif</option>
<option value="serif">serif</option>
</select>
</div>
<div class="input-field">
<label>Secondary Font</label>
<select name="fonts.secondary.name" id="font-secondary"></select>
<label>Fallback</label>
<select name="fonts.secondary.fallback" id="font-secondary-fallback">
<option value="sans-serif">sans-serif</option>
<option value="serif">serif</option>
</select>
</div>
</div>
</fieldset>
<!-- Favicon Section -->
<fieldset>
<h2>Favicon</h2>
<p>Supported formats: .png, .jpg, .jpeg</p>
<div class="fields">
<div class="input-field">
<label>Favicon Path</label>
<input type="text" name="favicon.path" id="favicon-path" readonly>
<input type="file" id="favicon-upload" accept=".png,.jpg,.jpeg,.ico" style="display:none;">
<button type="button" id="choose-favicon-btn" class="up-btn">🖼️ Upload favicon</button>
<div class="favicon-form">
<img id="favicon-preview" src="" alt="Favicon preview" style="max-width:48px;display:none;">
<button type="button" id="remove-favicon-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
</div>
</div>
</div>
</fieldset>
<button type="submit">Save Theme</button>
</form>
<div class="section">
<h2>Steps</h2>
<p> Follow the steps to generate your static gallery</p>
<ul id="stepper">
<li><a href="/gallery-editor">Upload your photos</a></li>
<div></div>
<li><a href="/site-info">Configure site info</a></li>
<div></div>
<li><a class="step-active" href="/theme-editor">Customize your theme</a></li>
<div></div>
<li><button id="stepper-build">Generate your static site!</button></li>
</ul>
</div>
</div>
</div>
<!-- Delete confirmation modal for favicon -->
<div id="delete-favicon-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-favicon-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3>
<p id="delete-favicon-modal-text">Are you sure you want to remove this favicon?</p>
<div class="modal-actions">
<button id="delete-favicon-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
</div>
</div>
</div>
<!-- Delete confirmation modal for font -->
<div id="delete-font-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-font-modal-close" class="modal-close">&times;</span>
<h3>Confirm Deletion</h3>
<p id="delete-font-modal-text">Are you sure you want to remove this font?</p>
<div class="modal-actions">
<button id="delete-font-modal-confirm" class="modal-btn danger">Remove</button>
<button id="delete-font-modal-cancel" class="modal-btn">Cancel</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
{% endblock %}