commit b9e3467c01d5176b90871bff680950f5e77c7674 Author: Djeex Date: Wed Aug 6 11:16:18 2025 +0000 1st commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea00f27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv +.output +__pycache__ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a07638 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License +Copyright (c) 2025 > Djeex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..e09990f --- /dev/null +++ b/README.MD @@ -0,0 +1,226 @@ +

Lumeex

+
+ Lumeex Screenshot +
+ +**Lumeex** - Yet another minimalist photo gallery with a static site generator. + +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 comes with two themes: one modern, the other more minimalistic, both designed to keep the focus on the artworks. + +> [!NOTE] +> _This GitHub repository is a mirror of https://git.djeex.fr/Djeex/lumeex. You’ll find the complete package, history, and release notes there. An LLM is used for bug checking and language file generation._ + +## πŸ“Œ Table of Contents + +- [Features](#-features) +- [Python Installation](#-python-installation) +- [Configuration](#-configuration) +- [Build the Site](#-build-the-site) + +## 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). +- Supports Google Fonts and local fonts. + +**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. + +**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. + +## Python Installation + +Instructions to run the Python scripts directly. + +**Requirements** + +- Python 3.11 or higher +- PyYAML +- Pillow + +**Installation** + +```sh +git clone https://git.djeex.fr/Djeex/lumeex.git +cd lumeex +python3 -m venv .venv +source .venv/bin/activate +pip install requirements.txt +``` + +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. +> You’ll just need to tag the photos in `gallery.yaml`. + +**`site.yaml`** + +This file contains all your site’s 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 +``` + +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. diff --git a/build.py b/build.py new file mode 100644 index 0000000..7a1ea00 --- /dev/null +++ b/build.py @@ -0,0 +1,164 @@ +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'' + 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'' + 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"" + body = f""" + +
+ {hero} + {gallery} + {footer} + + """ + output_file = BUILD_DIR / "index.html" + with open(output_file, "w", encoding="utf-8") as f: + f.write(f"\n{signature}\n\n{head}\n{body}\n") + 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"

{item['paragraph']}

" 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"\n{signature}\n\n{head}\n{legals_body}\n{footer}\n" + 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.") + +if __name__ == "__main__": + build() + \ No newline at end of file diff --git a/config/gallery.yaml b/config/gallery.yaml new file mode 100644 index 0000000..d28f0b4 --- /dev/null +++ b/config/gallery.yaml @@ -0,0 +1,30 @@ +# 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 + +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"] diff --git a/config/photos/.DS_Store b/config/photos/.DS_Store new file mode 100644 index 0000000..5cf1141 Binary files /dev/null and b/config/photos/.DS_Store differ diff --git a/config/photos/gallery/almos-bechtold-3402kvtHhOo-unsplash.jpg b/config/photos/gallery/almos-bechtold-3402kvtHhOo-unsplash.jpg new file mode 100644 index 0000000..46c580c Binary files /dev/null and b/config/photos/gallery/almos-bechtold-3402kvtHhOo-unsplash.jpg differ diff --git a/config/photos/gallery/arthur-savary-nLfAqmZ2hJo-unsplash.jpg b/config/photos/gallery/arthur-savary-nLfAqmZ2hJo-unsplash.jpg new file mode 100644 index 0000000..7d45057 Binary files /dev/null and b/config/photos/gallery/arthur-savary-nLfAqmZ2hJo-unsplash.jpg differ diff --git a/config/photos/gallery/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg b/config/photos/gallery/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg new file mode 100644 index 0000000..fa9d8f9 Binary files /dev/null and b/config/photos/gallery/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg differ diff --git a/config/photos/gallery/gilley-aguilar-ywGDhTlf93E-unsplash.jpg b/config/photos/gallery/gilley-aguilar-ywGDhTlf93E-unsplash.jpg new file mode 100644 index 0000000..8ac743e Binary files /dev/null and b/config/photos/gallery/gilley-aguilar-ywGDhTlf93E-unsplash.jpg differ diff --git a/config/photos/gallery/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg b/config/photos/gallery/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg new file mode 100644 index 0000000..a7a1ca2 Binary files /dev/null and b/config/photos/gallery/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg differ diff --git a/config/photos/gallery/jonas-degener-LueP5EdWGFY-unsplash.jpg b/config/photos/gallery/jonas-degener-LueP5EdWGFY-unsplash.jpg new file mode 100644 index 0000000..aa323ea Binary files /dev/null and b/config/photos/gallery/jonas-degener-LueP5EdWGFY-unsplash.jpg differ diff --git a/config/photos/gallery/michiel-annaert-M27pZnHV6M0-unsplash.jpg b/config/photos/gallery/michiel-annaert-M27pZnHV6M0-unsplash.jpg new file mode 100644 index 0000000..b4543d5 Binary files /dev/null and b/config/photos/gallery/michiel-annaert-M27pZnHV6M0-unsplash.jpg differ diff --git a/config/photos/gallery/nir-himi-AjecvkfSHxA-unsplash.jpg b/config/photos/gallery/nir-himi-AjecvkfSHxA-unsplash.jpg new file mode 100644 index 0000000..aa74bae Binary files /dev/null and b/config/photos/gallery/nir-himi-AjecvkfSHxA-unsplash.jpg differ diff --git a/config/photos/gallery/rachel-mcdermott-0fN7Fxv1eWA-unsplash.jpg b/config/photos/gallery/rachel-mcdermott-0fN7Fxv1eWA-unsplash.jpg new file mode 100644 index 0000000..f92dc7c Binary files /dev/null and b/config/photos/gallery/rachel-mcdermott-0fN7Fxv1eWA-unsplash.jpg differ diff --git a/config/photos/gallery/tianlei-wu-g5o6T-PWT3g-unsplash.jpg b/config/photos/gallery/tianlei-wu-g5o6T-PWT3g-unsplash.jpg new file mode 100644 index 0000000..0f04e21 Binary files /dev/null and b/config/photos/gallery/tianlei-wu-g5o6T-PWT3g-unsplash.jpg differ diff --git a/config/photos/gallery/we-care-wild-zLweeVLU9Fo-unsplash.jpg b/config/photos/gallery/we-care-wild-zLweeVLU9Fo-unsplash.jpg new file mode 100644 index 0000000..8918425 Binary files /dev/null and b/config/photos/gallery/we-care-wild-zLweeVLU9Fo-unsplash.jpg differ diff --git a/config/photos/gallery/y-s-z90w7yStOkk-unsplash.jpg b/config/photos/gallery/y-s-z90w7yStOkk-unsplash.jpg new file mode 100644 index 0000000..fff36c5 Binary files /dev/null and b/config/photos/gallery/y-s-z90w7yStOkk-unsplash.jpg differ diff --git a/config/photos/hero/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg b/config/photos/hero/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg new file mode 100644 index 0000000..fa9d8f9 Binary files /dev/null and b/config/photos/hero/francesco-ungaro-Zbc9Ka8msdI-unsplash.jpg differ diff --git a/config/photos/hero/gilley-aguilar-ywGDhTlf93E-unsplash.jpg b/config/photos/hero/gilley-aguilar-ywGDhTlf93E-unsplash.jpg new file mode 100644 index 0000000..8ac743e Binary files /dev/null and b/config/photos/hero/gilley-aguilar-ywGDhTlf93E-unsplash.jpg differ diff --git a/config/photos/hero/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg b/config/photos/hero/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg new file mode 100644 index 0000000..a7a1ca2 Binary files /dev/null and b/config/photos/hero/jacob-reinikainen-nGG6m3RbjSk-unsplash.jpg differ diff --git a/config/site.yaml b/config/site.yaml new file mode 100644 index 0000000..215319c --- /dev/null +++ b/config/site.yaml @@ -0,0 +1,55 @@ +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 + + +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. + + 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 + legal_link: '/legals/' + legal_label: Legal notice + +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) + +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." diff --git a/config/themes/modern/favicon.png b/config/themes/modern/favicon.png new file mode 100644 index 0000000..16299b3 Binary files /dev/null and b/config/themes/modern/favicon.png differ diff --git a/config/themes/modern/theme.css b/config/themes/modern/theme.css new file mode 100644 index 0000000..b011db8 --- /dev/null +++ b/config/themes/modern/theme.css @@ -0,0 +1,61 @@ +/*-----------------------------------*/ +/* 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); + border-radius: 15px 15px 0 0; + max-width: 1140px; + 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; + } + + .hero-background { + max-width: 90%; + margin: auto; + } + + .back-button { + margin-left: 0px; +} + +} \ No newline at end of file diff --git a/config/themes/modern/theme.yaml b/config/themes/modern/theme.yaml new file mode 100644 index 0000000..0dc980d --- /dev/null +++ b/config/themes/modern/theme.yaml @@ -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: '#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 \ No newline at end of file diff --git a/config/themes/typewriter/favicon.png b/config/themes/typewriter/favicon.png new file mode 100644 index 0000000..16299b3 Binary files /dev/null and b/config/themes/typewriter/favicon.png differ diff --git a/config/themes/typewriter/fonts/trixie.eot b/config/themes/typewriter/fonts/trixie.eot new file mode 100644 index 0000000..d74d5a2 Binary files /dev/null and b/config/themes/typewriter/fonts/trixie.eot differ diff --git a/config/themes/typewriter/fonts/trixie.svg b/config/themes/typewriter/fonts/trixie.svg new file mode 100644 index 0000000..f2c00b9 --- /dev/null +++ b/config/themes/typewriter/fonts/trixie.svg @@ -0,0 +1,610 @@ + + + + +Created by FontForge 20230101 at Mon Apr 30 18:04:47 2007 + By Unknown +Copr. LettError; Erik van Blokland 1991 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/themes/typewriter/fonts/trixie.ttf b/config/themes/typewriter/fonts/trixie.ttf new file mode 100644 index 0000000..4dab21a Binary files /dev/null and b/config/themes/typewriter/fonts/trixie.ttf differ diff --git a/config/themes/typewriter/fonts/trixie.woff b/config/themes/typewriter/fonts/trixie.woff new file mode 100644 index 0000000..d070991 Binary files /dev/null and b/config/themes/typewriter/fonts/trixie.woff differ diff --git a/config/themes/typewriter/fonts/trixie.woff2 b/config/themes/typewriter/fonts/trixie.woff2 new file mode 100644 index 0000000..7a0213c Binary files /dev/null and b/config/themes/typewriter/fonts/trixie.woff2 differ diff --git a/config/themes/typewriter/theme.yaml b/config/themes/typewriter/theme.yaml new file mode 100644 index 0000000..bb4846e --- /dev/null +++ b/config/themes/typewriter/theme.yaml @@ -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: '#333' + background: '#fff' + browser_color: '#fff' +favicon: + path: favicon.png +fonts: + primary: + name: Trixie + fallback: sans-serif + secondary: + name: Trixie + fallback: serif \ No newline at end of file diff --git a/gallery.py b/gallery.py new file mode 100644 index 0000000..0e78138 --- /dev/null +++ b/gallery.py @@ -0,0 +1,81 @@ +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("public/img/gallery") +HERO_DIR = Path("public/img/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") + +if __name__ == "__main__": + update_gallery() + update_hero() diff --git a/illustration/lumeex.png b/illustration/lumeex.png new file mode 100644 index 0000000..a68fe96 Binary files /dev/null and b/illustration/lumeex.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..96b5dca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyyaml +pillow \ No newline at end of file diff --git a/src/public/js/lazy.js b/src/public/js/lazy.js new file mode 100644 index 0000000..0dc1c3d --- /dev/null +++ b/src/public/js/lazy.js @@ -0,0 +1,45 @@ +// js for Lumeex +// https://git.djeex.fr/Djeex/lumeex + +window.addEventListener("DOMContentLoaded", () => { + // Lazy loading + const lazyImages = document.querySelectorAll('img.lazyload'); + + const observer = new IntersectionObserver((entries, obs) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + console.log("Lazy-loading image:", img.dataset.src); + img.src = img.dataset.src; + img.onload = () => { + img.classList.add("loaded"); + }; + obs.unobserve(img); + } + }); + }, { + rootMargin: "0px 0px 300px 0px", + threshold: 0.01 + }); + + lazyImages.forEach(img => observer.observe(img)); + + // Fade-in effect for loaded images (even outside lazy ones) + const fadeImages = document.querySelectorAll("img.fade-in-img"); + + fadeImages.forEach(img => { + const onLoad = () => { + console.log("Image loaded (fade-in):", img.src); + img.classList.add("loaded"); + }; + + if (img.complete && img.naturalHeight !== 0) { + onLoad(); // already loaded + } else { + img.addEventListener("load", onLoad, { once: true }); + img.addEventListener("error", () => { + console.warn("Image failed to load:", img.dataset.src || img.src); + }); + } + }); +}); diff --git a/src/public/js/lumeex.js b/src/public/js/lumeex.js new file mode 100644 index 0000000..6900382 --- /dev/null +++ b/src/public/js/lumeex.js @@ -0,0 +1,152 @@ +// js for Lumeex +// https://git.djeex.fr/Djeex/lumeex + +// Fade in effect for elements with class 'appear' +const setupIntersectionObserver = () => { + const items = document.querySelectorAll('.appear'); + const io = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + entry.target.classList.toggle('inview', entry.isIntersecting); + }); + }); + items.forEach((item) => io.observe(item)); +}; + +// Loader fade out after page load +const setupLoader = () => { + window.addEventListener('load', () => { + setTimeout(() => { + const loader = document.querySelector('.page-loader'); + 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"); + if (!heroBg) return; + fetch("/data/gallery.json") + .then((res) => res.json()) + .then((images) => { + if (images.length === 0) return; + let currentIndex = Math.floor(Math.random() * images.length); + heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`; + setInterval(() => { + let nextIndex; + do { + nextIndex = Math.floor(Math.random() * images.length); + } while (nextIndex === currentIndex); + const nextImage = images[nextIndex]; + heroBg.style.setProperty("--next-image", `url(/img/${nextImage})`); + heroBg.classList.add("fade-in"); + const onTransitionEnd = () => { + heroBg.style.backgroundImage = `url(/img/${nextImage})`; + heroBg.classList.remove("fade-in"); + heroBg.removeEventListener("transitionend", onTransitionEnd); + }; + heroBg.addEventListener("transitionend", onTransitionEnd); + currentIndex = nextIndex; + }, 7000); + }) + .catch(console.error); +}; + +// Tags filter functionality +const setupTagFilter = () => { + const allSections = document.querySelectorAll('.section[data-tags]'); + const allTags = document.querySelectorAll('.tag'); + let activeTags = []; + const applyFilter = () => { + allSections.forEach((section) => { + const sectionTags = section.dataset.tags.toLowerCase().split(/\s+/); + const hasAllTags = activeTags.every((tag) => sectionTags.includes(tag)); + section.style.display = hasAllTags ? '' : 'none'; + }); + allTags.forEach((tagEl) => { + const tagText = tagEl.textContent.replace('#', '').toLowerCase(); + tagEl.classList.toggle('active', activeTags.includes(tagText)); + }); + const base = window.location.pathname; + const query = activeTags.length > 0 ? `?tag=${activeTags.join(',')}` : ''; + window.history.pushState({}, '', base + query); + }; + + allTags.forEach((tagEl) => { + tagEl.addEventListener('click', () => { + const tagText = tagEl.textContent.replace('#', '').toLowerCase(); + activeTags = activeTags.includes(tagText) + ? activeTags.filter((t) => t !== tagText) + : [...activeTags, tagText]; + applyFilter(); + }); + }); + + window.addEventListener('DOMContentLoaded', () => { + const params = new URLSearchParams(window.location.search); + const urlTags = params.get('tag'); + if (urlTags) { + activeTags = urlTags.split(',').map((t) => t.toLowerCase()); + applyFilter(); + } + }); +}; + +// Disable right-click context menu and image dragging +const disableRightClickAndDrag = () => { + document.addEventListener("contextmenu", (e) => e.preventDefault()); + document.addEventListener("dragstart", (e) => { + if (e.target.tagName === "IMG") { + e.preventDefault(); + } + }); +}; + +// Scroll-to-top button functionality +const setupScrollToTopButton = () => { + const scrollBtn = document.getElementById("scrollToTop"); + window.addEventListener("scroll", () => { + scrollBtn.style.display = window.scrollY > 300 ? "block" : "none"; + }); + scrollBtn.addEventListener("click", () => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }); +}; + +// Adjust navigation list items +const fixNavSeparators = () => { + const items = document.querySelectorAll('.nav-list li'); + let prevTop = null; + items.forEach((item) => { + const top = item.getBoundingClientRect().top; + item.classList.toggle('first-on-line', prevTop !== null && top !== prevTop); + prevTop = top; + }); +}; + +// Initialize all functions +document.addEventListener("DOMContentLoaded", () => { + setupIntersectionObserver(); + setupLoader(); + shuffleGallery(); + randomizeHeroBackground(); + setupTagFilter(); + disableRightClickAndDrag(); + setupScrollToTopButton(); + fixNavSeparators(); +}); + +window.addEventListener('resize', fixNavSeparators); diff --git a/src/public/style/.DS_Store b/src/public/style/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/public/style/.DS_Store differ diff --git a/src/public/style/style.css b/src/public/style/style.css new file mode 100644 index 0000000..f63c592 --- /dev/null +++ b/src/public/style/style.css @@ -0,0 +1,496 @@ +/*-----------------------------------*/ +/* CSS style for Lumeex */ +/* https://git.djeex.fr/Djeex/lumeex */ +/*-----------------------------------*/ + +:root { + --color-primary: #0065a1; + --color-primary-dark: #005384; + --color-secondary: #00b0f0; + --color-accent: #ffc700; + --color-text-dark: #333333; + --color-background: #ffffff; +} + + +/* Custom scroll bar */ + +/* width */ +::-webkit-scrollbar { + width: 3px; +} + +/* Track */ +::-webkit-scrollbar-track { + border-radius: 10px; + background-color:var(--color-background) +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background-color: var(--color-primary); + border-radius: 10px; +} + +/* Scroll to top */ +.scroll-up { + position: fixed; + bottom: 20px; + right: 20px; + background: none; + color: var(--color-primary-dark); + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 20px; + cursor: pointer; + z-index: 1000; + display: none; + transition: opacity 0.3s ease; +} + +.scroll-up:hover, .back-button:hover { + color:var(--color-secondary); +} + +/* back button */ + +.back-button { + padding: 20px; + text-decoration: none; + color: var(--color-primary-dark); + font-size: 1.1rem; + font-family: arial; + border-radius: 8px; + transition: background 0.2s ease; +} + + +/* Body structure */ + +html,body { + height: 100%; + margin: 0px; + padding: 0px; + font-family: var(--font-secondary), Helvetica, sans-serif; + min-width:320px; + font-weight: 400; + line-height:1.5; + color:var(--color-primary-dark); +} + +html { + scroll-behavior: smooth; +} + +@media screen and (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + +body a, body a:hover { + transition: all 0.25s ease-out; + text-decoration:none; +} + +.inner { + max-width:1200px; + margin-left: auto; + margin-right: auto; +} + +h2 { + font-family: var(--font-primary), Arial, sans-serif; + font-size: 18px; + text-align: left; +} + +/* Loader */ +.page-loader { + width: 100%; + height: 100vh; + position: fixed; + background: var(--color-background); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + opacity: 1; + transition: opacity 1s ease-out; +} + +/* Hide the loader with a fade-out effect */ +.page-loader.hidden { + opacity: 0; + pointer-events: none; +} + +/* Spinner */ +.spinner { + width: 80px; + height: 80px; + background-color: var(--color-primary); + border-radius: 100%; + animation: sk-scaleout 1.0s infinite ease-in-out; +} + +@keyframes sk-scaleout { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1.0); + opacity: 0; + } +} + +/* navigation */ + +.nav-item { + display: inline; + +} + +.nav-list { + list-style-type: disc; + margin: 0px; + padding: 0px; +} + +.nav-list li { + white-space: nowrap; +} + +.nav-list > li + li::before { + content: " β€’ "; + color: var(--color-accent); + margin: -5px; +} + +.nav-list li.first-on-line::before { + visibility: hidden; +} + +/* animation */ + +.appear { + -webkit-transition: all 0.3s; + transition: all 0.3s; + opacity: 0; + -webkit-transform: translateY(20px); + transform: translateY(20px); +} + +.appear.inview { + opacity: 1; + -webkit-transform: none; + transform: none; +} + +.appear.inview:nth-child(1) { + -webkit-transition-delay: 0s; + transition-delay: 0s; +} + +.appear.inview:nth-child(2) { + -webkit-transition-delay: 0.2s; + transition-delay: 0.2s; +} + +.appear.inview:nth-child(3) { + -webkit-transition-delay: 0.4s; + transition-delay: 0.4s; +} + +.appear.inview:nth-child(4) { + -webkit-transition-delay: 0.6s; + transition-delay: 0.6s; +} + +.appear.inview:nth-child(5) { + -webkit-transition-delay: 0.8s; + transition-delay: 0.8s; +} + +.appear.inview:nth-child(6) { + -webkit-transition-delay: 1s; + transition-delay: 1s; +} + +/* img fade in */ + +.fade-in-img { + opacity: 0; + transform: scale(1.02); + transition: opacity 1.2s ease-out, transform 1.2s ease-out; + will-change: opacity, transform; +} + +.fade-in-img.loaded { + opacity: 1; + transform: scale(1); +} + +/* tag */ + +.tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} + +.tag { + padding: 5px 5px; + cursor: pointer; + font-size: 18px; + transition: background 0.2s ease; +} + +.tag:hover, +.tag.active { + color: var(--color-secondary); +} + +/* Content */ + +/* wrapper */ +.content-wrapper { + margin: 0 100px; +} +/* Hero */ + +#hero { + height: 100%; + width: 100%; +} +#hero .content-wrapper, #hero .section { + height:100%; +} +.hero-background { + height: 66%; + width: 100%; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + position: relative; + display: flex; + align-items: center; + flex-direction: column-reverse; + z-index: 0; + overflow: hidden; +} + +.hero-background::after { + content: ""; + position: absolute; + inset: 0; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + opacity: 0; + transition: opacity 1.5s ease-in-out; + z-index: 1; /* derriΓ¨re le contenu */ + pointer-events: none; + background-image: var(--next-image); +} + +.hero-background.fade-in::after { + opacity: 1; + background-image: var(--next-image); +} + +.hero-menu { + font-size: 18px; +} + +.hero-title { + text-align: center; + margin-bottom: 40px; + color: var(--color-background); + margin-right: auto; + margin-left: auto; + display: flex; + position: relative; + z-index: 2; + +} + +.hero-title h1 { + font-size: 38px; + margin: 0; +} + +.hero-title p { + margin: 0; + font-style: italic; + font-size: 22px; +} + +/* Sections */ + +.section { + max-width: 1140px; + margin:auto; +} + +.section img { + width:100%; + margin: 0 0 60px 0; +} + +.text-block { + padding:10px; + margin:10px; +} + +.text-block p { + font-size: 18px; + font-family: var(--font-secondary), Helvetica, sans-serif; + font-weight: 200; +} + +/* Buttons */ + +/* Text */ + +.text-inner { + margin-left: 10%; + margin-right:10%; +} + +/* Footer */ + +.navigation { + text-align: center; + padding-top: 40px; +} + +.navigation-title { + font-family: var(--font-primary), Arial, sans-serif; + font-weight: bold; + font-size: 32px; + color:var(--color-text-dark); + margin-top: 0px; + margin-bottom: 20px; +} + +.navigation a { + color:var(--color-primary); +} + +.navigation a:hover { + color:var(--color-secondary); +} + + +.navigation-subtitle { + color:var(--color-text-dark); + font-size: 12px; +} + +.navigation-bottom-link { + color:var(--color-accent); + font-size: 12px; +} + + +.social { + display: inline; + text-align: center; + padding: 0; +} + +.social-item { + display: inline; + text-align: center; + font-size: 25px; + padding: 10px; +} + +.navigation .nav-links { + margin: 20px 0; + font-family: var(--font-secondary), Helvetica, sans-serif; + font-weight: 200; +} + +.nav-list > li + li::before { + margin: 3px; +} + +.bottom-link { + padding-bottom: 40px; +} + +.bottom-link p { + margin-left:10px; + margin-right:10px; +} +/* legals */ + +#legals.content-wrapper { + max-width: 1140px; + margin-top: 100px; + margin-bottom: 100px; + margin-left: auto; + margin-right: auto; +} + +.legals-content h2 { + font-size: 22px; +} + +.legals-content { + margin: 0 100px; +} + +/* responsive */ + +@media (max-width: 1000px) { + .button { + font-size: 14px; + } +} + +@media (max-width: 768px) { + .content-wrapper { + margin: 0; + } + + .content-wrapper.gallery { + margin: 0 5%; + } + + .navigation .nav-links { + margin: 20px; + } + + .nav-links > ul { + padding: 0; + list-style-type: none; + font-size: 20px; + top: 15%; + position: relative; + } + + h2 { + font-size:18px; + } + + #legals.content-wrapper { + max-width: 90%; + margin: auto; + margin-top: 50px; + } + + .legals-content { + margin: 0; + } + + .back-button { + padding-left: 0; + margin-top: 60px; + } +} \ No newline at end of file diff --git a/src/py/__init__.py b/src/py/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/py/css_generator.py b/src/py/css_generator.py new file mode 100644 index 0000000..82737ca --- /dev/null +++ b/src/py/css_generator.py @@ -0,0 +1,71 @@ +import logging +from pathlib import Path +from shutil import copyfile + +def generate_css_variables(colors_dict, output_path): + css_lines = [":root {"] + for key, value in colors_dict.items(): + css_lines.append(f" --color-{key.replace('_', '-')}: {value};") + css_lines.append("}") + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + f.write("\n".join(css_lines)) + logging.info(f"[βœ“] CSS variables written to {output_path}") + +def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None): + font_files = list(fonts_dir.glob("*")) + font_faces = {} + preload_links = [] + format_map = {".woff2": "woff2", ".woff": "woff", ".ttf": "truetype", ".otf": "opentype"} + + for font_file in font_files: + name = font_file.stem + ext = font_file.suffix.lower() + if ext not in format_map: + continue + font_faces.setdefault(name, []).append((font_file.name, format_map[ext])) + dest_font_path = output_path.parent.parent / "fonts" / font_file.name + dest_font_path.parent.mkdir(parents=True, exist_ok=True) + copyfile(font_file, dest_font_path) + preload_links.append( + f'' + ) + + css_lines = [] + for font_name, sources in font_faces.items(): + css_lines.append(f"@font-face {{") + css_lines.append(f" font-family: '{font_name}';") + srcs = [f"url('../fonts/{file}') format('{fmt}')" for file, fmt in sorted(sources)] + css_lines.append(f" src: {', '.join(srcs)};") + css_lines.append(" font-weight: normal;") + css_lines.append(" font-style: normal;") + css_lines.append("}") + + if fonts_cfg: + css_lines.append(":root {") + if "primary" in fonts_cfg: + p = fonts_cfg["primary"] + css_lines.append(f" --font-primary: '{p['name']}', {p['fallback']};") + if "secondary" in fonts_cfg: + s = fonts_cfg["secondary"] + css_lines.append(f" --font-secondary: '{s['name']}', {s['fallback']};") + css_lines.append("}") + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text("\n\n".join(css_lines), encoding="utf-8") + logging.info(f"[βœ“] Generated fonts CSS: {output_path}") + return preload_links + +def generate_google_fonts_link(fonts): + if not fonts: + return "" + families = [] + for font in fonts: + family = font["family"].replace(" ", "+") + weights = font.get("weights", []) + if weights: + families.append(f"{family}:wght@{';'.join(weights)}") + else: + families.append(family) + href = "https://fonts.googleapis.com/css2?" + "&".join(f"family={f}" for f in families) + "&display=swap" + return f'' diff --git a/src/py/html_generator.py b/src/py/html_generator.py new file mode 100644 index 0000000..df13ba4 --- /dev/null +++ b/src/py/html_generator.py @@ -0,0 +1,60 @@ +import json +import logging +from pathlib import Path + +def render_template(template_path, context): + with open(template_path, encoding="utf-8") as f: + content = f.read() + for key, value in context.items(): + placeholder = "{{ " + key + " }}" + content = content.replace(placeholder, str(value) if value is not None else "") + return content + +def render_gallery_images(images): + html = "" + for img in images: + tags = " ".join(img.get("tags", [])) + tag_html = "".join(f'#{t}' for t in img.get("tags", [])) + html += f""" +
+
{tag_html}
+ {img.get('alt', '')} +
+ """ + return html + +def generate_gallery_json_from_images(images, output_path): + try: + img_list = [img["src"] for img in images] + 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) + logging.info(f"[βœ“] Generated hero gallery JSON: {output_path}") + except Exception as e: + logging.error(f"[βœ—] Error generating gallery JSON: {e}") + +def generate_robots_txt(canonical_url, allowed_paths): + 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): + urlset_start = '\n\n' + urlset_end = '\n' + urls = "" + for path in allowed_paths: + loc = canonical_url.rstrip("/") + path + urls += f" \n {loc}\n \n" + sitemap_content = urlset_start + urls + urlset_end + output_path = Path(".output/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}") diff --git a/src/py/image_processor.py b/src/py/image_processor.py new file mode 100644 index 0000000..12e5ade --- /dev/null +++ b/src/py/image_processor.py @@ -0,0 +1,73 @@ +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 diff --git a/src/py/utils.py b/src/py/utils.py new file mode 100644 index 0000000..da3c49e --- /dev/null +++ b/src/py/utils.py @@ -0,0 +1,34 @@ +import yaml +import logging +from pathlib import Path +from shutil import copytree, rmtree, copyfile + +def load_yaml(path): + if not path.exists(): + logging.warning(f"[!] YAML file not found: {path}") + return {} + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + +def load_theme_config(theme_name, themes_dir): + theme_dir = themes_dir / theme_name + theme_config_path = theme_dir / "theme.yaml" + if not theme_config_path.exists(): + raise FileNotFoundError(f"[βœ—] Theme config not found: {theme_config_path}") + with open(theme_config_path, "r", encoding="utf-8") as f: + 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 copy_assets(js_dir, style_dir, build_dir): + for folder in [js_dir, style_dir]: + if folder.exists(): + dest = build_dir / folder.name + copytree(folder, dest) + logging.info(f"[βœ“] Copied assets from {folder.name}") + else: + logging.warning(f"[~] Skipped missing folder: {folder.name}") diff --git a/src/templates/footer.html b/src/templates/footer.html new file mode 100644 index 0000000..0c0a8fd --- /dev/null +++ b/src/templates/footer.html @@ -0,0 +1,20 @@ + + + + diff --git a/src/templates/gallery.html b/src/templates/gallery.html new file mode 100644 index 0000000..31c18b3 --- /dev/null +++ b/src/templates/gallery.html @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/templates/head.html b/src/templates/head.html new file mode 100644 index 0000000..80001ee --- /dev/null +++ b/src/templates/head.html @@ -0,0 +1,44 @@ + + + {{ title }} + + + + + + + + + + + + + + + + + + + + + + {{ google_fonts_link }} + {{ font_preloads }} + + + + {{ theme_css }} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/templates/hero.html b/src/templates/hero.html new file mode 100644 index 0000000..c57ef8c --- /dev/null +++ b/src/templates/hero.html @@ -0,0 +1,29 @@ + +
+
+
+
+
+
+

{{ title }}

+

{{ subtitle }}

+
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/templates/legals.html b/src/templates/legals.html new file mode 100644 index 0000000..4d1fccc --- /dev/null +++ b/src/templates/legals.html @@ -0,0 +1,12 @@ +
+ ← +
+

Legals

+

Hoster

+

Name: {{ hoster_name }}

+

Adress: {{ hoster_adress }}

+

Contact: {{ hoster_contact }}

+

Intellectual Property

+ {{ intellectual_property }} +
+
\ No newline at end of file