6 Commits

Author SHA1 Message Date
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
9 changed files with 204 additions and 66 deletions

1
.gitignore vendored
View File

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

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ./src/ ./src/
COPY ./build.py ./build.py
COPY ./gallery.py ./gallery.py
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"]

View File

@ -24,8 +24,7 @@ The project includes two thoughtfully designed themes—one modern, one minimali
## 📌 Table of Contents
- [✨ Features](#-features)
- [🐍 Python Installation](#-python-installation)
- [⚙️ Configuration](#-configuration)
- [🐳 Docker or 🐍 Python Installation](#-docker-or--python-installation)
## ✨ Features
@ -57,28 +56,6 @@ The project includes two thoughtfully designed themes—one modern, one minimali
- *(Optional)* Converts images to WebP format for optimized performance
- Outputs a complete static website ready to deploy on any web server
## 🐍 Python Installation
Run the Python scripts directly with the following prerequisites:
### Prerequisites
- Git
- Python 3.11 or above
### Installation Steps
```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
```
You are now ready to use Lumeex!
## ⚙️ Configuration
For comprehensive documentation on configuration options, customization, and demos, please visit:
## 🐳 Docker or 🐍 Python Installation
For comprehensive documentation on installation, configuration options, customization, and demos, please visit:
https://lumeex.djeex.fr

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

@ -0,0 +1,78 @@
#!/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 HTTP server on port 3000..."
python3 -u -m http.server 3000 -d /app/output &
SERVER_PID=$!
trap "echo 'Stopping server...'; kill -TERM $SERVER_PID 2>/dev/null; wait $SERVER_PID; exit 0" SIGINT SIGTERM
wait $SERVER_PID
}
if [ $# -eq 0 ]; then
echo -e "${CYAN}╭───────────────────────────────────────────╮${NC}"
echo -e "${CYAN}${NC} Lum${CYAN}eex${NC} - Version 1.3.1${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,10 @@
services:
lumeex:
container_name: lmx
build: ..
volumes:
- ../config:/app/config # mount config directory
- ../output:/app/output # mount output directory
ports:
- "3000:3000"

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");
@ -65,32 +52,74 @@ 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; // mémorise le dernier tag cliqué
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) {
filteredSections.push(section);
if (lastClickedTag && sectionTags.includes(lastClickedTag) && !matchingSection) {
matchingSection = section;
}
}
});
// Réorganise : la photo correspondante au dernier tag cliqué en premier
if (matchingSection && galleryContainer.contains(matchingSection)) {
galleryContainer.prepend(matchingSection);
}
// Met à jour le style des tags
allTags.forEach((tagEl) => {
const tagText = tagEl.textContent.replace('#', '').toLowerCase();
tagEl.classList.toggle('active', activeTags.includes(tagText));
});
// Met à jour l'URL
const base = window.location.pathname;
const query = activeTags.length > 0 ? `?tag=${activeTags.join(',')}` : '';
window.history.pushState({}, '', base + query);
// Scroll jusqu'à la galerie
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; // mémorise le dernier tag cliqué
if (activeTags.includes(tagText)) {
activeTags = activeTags.filter((t) => t !== tagText);
} else {
activeTags.push(tagText);
}
applyFilter();
});
});
@ -100,29 +129,24 @@ const setupTagFilter = () => {
const urlTags = params.get('tag');
if (urlTags) {
activeTags = urlTags.split(',').map((t) => t.toLowerCase());
lastClickedTag = activeTags[activeTags.length - 1] || null;
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
// Scroll to top button
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" });
const scrollToTopButton = document.querySelector('.scroll-to-top');
if (!scrollToTopButton) return;
scrollToTopButton.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
};

View File

@ -333,8 +333,12 @@ h2 {
font-size: 22px;
}
/* Sections */
.gallery {
padding-top: 15px;
}
/* Sections */
.section {
max-width: 1140px;
margin:auto;
@ -508,4 +512,9 @@ h2 {
padding-left: 0;
margin-top: 60px;
}
.gallery {
margin: 10% 5% 0 5%;
padding-top: 15px;
}
}

View File

@ -23,7 +23,12 @@ SITE_FILE = SRC_DIR / "config/site.yaml"
THEMES_DIR = SRC_DIR / "config/themes"
def build():
logging.info("🚀 Starting build...")
build_version = "v1.3.1"
logging.info("\n")
logging.info("=" * 24)
logging.info(f"🚀 Lumeex builder {build_version}")
logging.info("=" * 24)
logging.info("\n === Starting build === ")
ensure_dir(BUILD_DIR)
copy_assets(JS_DIR, STYLE_DIR, BUILD_DIR)
@ -87,7 +92,7 @@ def build():
# Adding Google fonts if existing
google_fonts_link = generate_google_fonts_link(theme_vars.get("google_fonts", []))
logging.info(f"[✓] Google Fonts link generated:\n{google_fonts_link}")
logging.info(f"[✓] Google Fonts link generated")
# Generating thumbnail
thumbnail_path = site_vars.get("social", {}).get("thumbnail")
@ -125,7 +130,7 @@ def build():
gallery_html = render_gallery_images(gallery_images)
gallery = render_template(TEMPLATE_DIR / "gallery.html", {"gallery_images": gallery_html})
signature = f"<!-- Build with Lumeex v1.2 | https://git.djeex.fr/Djeex/lumeex | {build_date_version} -->"
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>

View File

@ -19,10 +19,25 @@ 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):
if not path.exists():
path.mkdir(parents=True)
return
# Remove all files and subdirectories inside path, but not path itself
for child in path.iterdir():
if child.is_file() or child.is_symlink():
child.unlink() # delete file or symlink
elif child.is_dir():
rmtree(child) # delete directory and contents
# Then replace your ensure_dir with this:
def ensure_dir(path: Path):
if not path.exists():
path.mkdir(parents=True)
else:
clear_dir(path)
def copy_assets(js_dir, style_dir, build_dir):
for folder in [js_dir, style_dir]: