30 Commits

Author SHA1 Message Date
df96782500 Merge pull request 'Fixing footer link and menu appear' (#17) from fixing into main
Reviewed-on: #17
2025-08-28 19:21:59 +02:00
febf4d2be5 Version 2025-08-28 19:13:01 +02:00
617545e1bb Fixed img margin-bottom for mobile 2025-08-28 18:49:46 +02:00
922ce99679 Fixed menu appear > 6 2025-08-28 18:46:45 +02:00
cd0428990a Footer link updated 2025-08-28 18:36:07 +02:00
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
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
a02da47e73 fixed scroll to tup button 2025-08-18 10:24:08 +00:00
f7f2356510 Better comments 2025-08-15 13:36:48 +00:00
25 changed files with 1147 additions and 526 deletions

View File

@ -6,9 +6,8 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY build.py gallery.py VERSION /app/
COPY ./src/ ./src/ COPY ./src/ ./src/
COPY ./build.py ./build.py
COPY ./gallery.py ./gallery.py
COPY ./config /app/default COPY ./config /app/default
COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh

View File

@ -18,7 +18,7 @@ The project includes two thoughtfully designed themes—one modern, one minimali
- **Typewriter** — [View Demo](https://typewriter.djeex.fr) - **Typewriter** — [View Demo](https://typewriter.djeex.fr)
> [!NOTE] > [!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, releases, and bug-checking assisted by an LLM. > _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_.
## 📌 Table of Contents ## 📌 Table of Contents
@ -41,20 +41,26 @@ The project includes two thoughtfully designed themes—one modern, one minimali
- Typewriter — [Demo](https://typewriter.djeex.fr) - Typewriter — [Demo](https://typewriter.djeex.fr)
- Supports Google Fonts and locally hosted fonts - Supports Google Fonts and locally hosted fonts
### No-Code Builder (YAML Based) ### No-Code Builder (WebUI Manager)
- Configure site info, SEO, colors, fonts, and more through simple YAML files  
- Reference and tag photos without any coding required <div align="center">
- *(Optional)* Automatically update photo references via script <img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex-webui.png" alt="Lumeex Screenshot" />
</div>
&nbsp;
### Simple Build Process
- Compiles static site from YAML configuration files (themes, templates, fonts, colors) - 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 - Converts favicon automatically to all required formats
- Resizes social sharing thumbnails - Resizes social sharing thumbnails
- *(Optional)* Automatically resizes photos to a maximum width of 1140px - *(Optional)* Automatically resizes photos to a maximum width of 1140px
- *(Optional)* Converts images to WebP format for optimized performance - *(Optional)* Converts images to WebP format for optimized performance
- Outputs a complete static website ready to deploy on any web server - Build your static site in one click and get a zip archive or an output folder, ready to deploy to your preferred webserver
### Don't want a WebUI ?
- CLI process is documented
## 🐳 Docker or 🐍 Python Installation ## 🐳 Docker or 🐍 Python Installation
For comprehensive documentation on installation, configuration options, customization, and demos, please visit: For comprehensive documentation on installation, configuration options, customization, and demos, please visit:

1
VERSION Normal file
View File

@ -0,0 +1 @@
2.0.1

View File

@ -43,16 +43,24 @@ start_server() {
cat /tmp/build_logs_fifo >&2 & cat /tmp/build_logs_fifo >&2 &
cat /tmp/build_logs_fifo2 >&2 & cat /tmp/build_logs_fifo2 >&2 &
echo "Starting HTTP server on port 3000..." echo "Starting preview HTTP server on port 3000..."
python3 -u -m http.server 3000 -d /app/output & python3 -u -m http.server 3000 -d /app/output &
SERVER_PID=$! SERVER_PID=$!
trap "echo 'Stopping server...'; kill -TERM $SERVER_PID 2>/dev/null; wait $SERVER_PID; exit 0" SIGINT SIGTERM
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 $SERVER_PID
wait $WEBUI_PID
} }
VERSION=$(cat VERSION)
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
echo -e "${CYAN}╭───────────────────────────────────────────╮${NC}" echo -e "${CYAN}╭───────────────────────────────────────────╮${NC}"
echo -e "${CYAN}${NC} Lum${CYAN}eex${NC} - Version 1.3.1${NC} ${CYAN}${NC}" echo -e "${CYAN}${NC} Lum${CYAN}eex${NC} - Version ${VERSION}${NC} ${CYAN}${NC}"
echo -e "${CYAN}├───────────────────────────────────────────┤${NC}" echo -e "${CYAN}├───────────────────────────────────────────┤${NC}"
echo -e "${CYAN}${NC} Source: https://git.djeex.fr/Djeex/lumeex ${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} Mirror: https://github.com/Djeex/lumeex ${CYAN}${NC}"

View File

@ -7,4 +7,5 @@ services:
- ../output:/app/output # mount output directory - ../output:/app/output # mount output directory
ports: ports:
- "3000:3000" - "3000:3000"
- "5000:5000"

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

View File

@ -3,6 +3,13 @@
// Fade in effect for elements with class 'appear' // Fade in effect for elements with class 'appear'
const setupIntersectionObserver = () => { const setupIntersectionObserver = () => {
document.querySelectorAll('.appear').forEach(parent => {
const children = parent.querySelectorAll('.appear');
children.forEach((child, i) => {
child.style.transitionDelay = `${i * 0.2}s`;
});
});
const items = document.querySelectorAll('.appear'); const items = document.querySelectorAll('.appear');
const io = new IntersectionObserver((entries) => { const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
@ -32,6 +39,7 @@ const randomizeHeroBackground = () => {
if (images.length === 0) return; if (images.length === 0) return;
let currentIndex = Math.floor(Math.random() * images.length); let currentIndex = Math.floor(Math.random() * images.length);
heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`; heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`;
if (images.length < 2) return; // <-- Prevent interval if only one image
setInterval(() => { setInterval(() => {
let nextIndex; let nextIndex;
do { do {

View File

@ -192,36 +192,6 @@ h2 {
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 */ /* img fade in */
.fade-in-img { .fade-in-img {
@ -491,7 +461,7 @@ h2 {
} }
.section img { .section img {
margin: 0px 0 60px 0; margin: 0px 0 40px 0;
} }
.tag { .tag {

View File

@ -21,12 +21,14 @@ STYLE_DIR = SRC_DIR / "src/public/style"
GALLERY_FILE = SRC_DIR / "config/gallery.yaml" GALLERY_FILE = SRC_DIR / "config/gallery.yaml"
SITE_FILE = SRC_DIR / "config/site.yaml" SITE_FILE = SRC_DIR / "config/site.yaml"
THEMES_DIR = SRC_DIR / "config/themes" 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(): def build():
build_version = "v1.3.1"
logging.info("\n") logging.info("\n")
logging.info("=" * 24) logging.info("=" * 24)
logging.info(f"🚀 Lumeex builder {build_version}") logging.info(f"🚀 Lumeex builder v{build_version}")
logging.info("=" * 24) logging.info("=" * 24)
logging.info("\n === Starting build === ") logging.info("\n === Starting build === ")
ensure_dir(BUILD_DIR) ensure_dir(BUILD_DIR)

View File

@ -18,6 +18,10 @@ from src.py.webui.upload import upload_bp
logging.basicConfig(level=logging.INFO, format="%(message)s") logging.basicConfig(level=logging.INFO, format="%(message)s")
# --- Flask app setup --- # --- 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 WEBUI_PATH = Path(__file__).parents[2] / "webui" # Path to static/templates
app = Flask( app = Flask(
__name__, __name__,
@ -68,6 +72,10 @@ def get_local_fonts(theme_name):
def index(): def index():
return render_template("index.html") return render_template("index.html")
@app.context_processor
def inject_version():
return dict(lumeex_version=lumeex_version)
# --- Gallery & Hero API --- # --- Gallery & Hero API ---
@app.route("/gallery-editor") @app.route("/gallery-editor")
def gallery_editor(): def gallery_editor():
@ -479,5 +487,5 @@ def download_output_zip():
# --- Run server --- # --- Run server ---
if __name__ == "__main__": if __name__ == "__main__":
logging.info("Starting WebUI at http://127.0.0.1:5000") logging.info("Starting WebUI at http://0.0.0.0:5000")
app.run(debug=True) app.run(host="0.0.0.0", port=5000, debug=True)

View File

@ -12,7 +12,7 @@
</div> </div>
<div class="inner bottom-link appear"> <div class="inner bottom-link appear">
<p><span class="navigation-subtitle appear">{{ copyright }}</span><span class="nav-separator"></span><span class="navigation-bottom-link appear"><a href="{{ legal_link }}">{{ legal_label }}</a></span></p> <p><span class="navigation-subtitle appear">{{ copyright }}</span><span class="nav-separator"></span><span class="navigation-bottom-link appear"><a href="{{ legal_link }}">{{ legal_label }}</a></span></p>
<p class="navigation-subtitle appear"> Built with <a href="https://git.djeex.fr/Djeex/lumeex">Lumeex</a></p> <p class="navigation-subtitle appear"> Built with <a href="https://lumeex.djeex.fr">Lumeex</a></p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,48 +1,14 @@
<!DOCTYPE html>
<html lang="en"> {% extends "template/base.html" %}
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"> {% block title %}Lumeex - Gallery Editor{% endblock %}
<meta charset="UTF-8">
<title>Lumeex</title> {% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
</head>
<body>
<!-- Top bar -->
<div class="nav-bar">
<div class="content-inner nav">
<input type="checkbox" id="nav-check">
<div class="nav-header">
<div class="nav-title">
<img src="{{ url_for('static', filename='img/logo.svg') }}">
</div>
</div>
<div class="nav-btn">
<label for="nav-check">
<span></span>
<span></span>
<span></span>
</label>
</div>
<div class="nav-links">
<ul class="nav-list">
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
<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">
<div id="toast-container"></div>
<h1>Gallery editor</h1> <h1>Gallery editor</h1>
<!-- Hero Upload Section --> <!-- Hero Upload Section -->
<div class="upload-section"> <div class="section">
<h2>Title Carrousel</h2> <h2>Title Carrousel</h2>
<p> Select photos to display in the Title Carrousel</p> <p> Select photos to display in the Title Carrousel</p>
<div class="upload-actions-row"> <div class="upload-actions-row">
@ -56,7 +22,7 @@
</div> </div>
<!-- Gallery Upload Section --> <!-- Gallery Upload Section -->
<div class="upload-section"> <div class="section">
<h2>Gallery</h2> <h2>Gallery</h2>
<p> Select and tags photos to display in the Gallery</p> <p> Select and tags photos to display in the Gallery</p>
<div class="upload-actions-row"> <div class="upload-actions-row">
@ -68,9 +34,19 @@
<input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden> <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
<div id="gallery"></div> <div id="gallery"></div>
</div> </div>
<script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/upload.js') }}" defer></script> <div class="section">
<script src="{{ url_for('static', filename='js/build.js') }}" defer></script> <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> </div>
<!-- Delete confirmation modal --> <!-- Delete confirmation modal -->
<div id="delete-modal" class="modal" style="display:none;"> <div id="delete-modal" class="modal" style="display:none;">
@ -84,15 +60,11 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Build success modal -->
<div id="build-success-modal" class="modal" style="display:none;">
<div class="modal-content"> {% endblock %}
<span id="build-success-modal-close" class="modal-close">&times;</span>
<h3>✅ Build completed!</h3> {% block scripts %}
<p>Your files are available in the output folder.</p> <script src="{{ url_for('static', filename='js/gallery-editor.js') }}"></script>
<button id="download-zip-btn" class="modal-btn">Download ZIP</button> <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
<div id="zip-loader" style="display:none;">Creating ZIP...</div> {% endblock %}
</div>
</div>
</body>
</html>

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

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 %}

View File

@ -4,6 +4,21 @@
* @param {string} type - "success" or "error". * @param {string} type - "success" or "error".
* @param {number} duration - Duration in ms. * @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) { function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container"); const container = document.getElementById("toast-container");
if (!container) return; if (!container) return;
@ -21,24 +36,34 @@ function showToast(message, type = "success", duration = 3000) {
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
// Get build button and modal elements // Get build button and modal elements
const buildBtn = document.getElementById("build-btn"); 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 buildModal = document.getElementById("build-success-modal");
const buildModalClose = document.getElementById("build-success-modal-close"); const buildModalClose = document.getElementById("build-success-modal-close");
const downloadZipBtn = document.getElementById("download-zip-btn"); const downloadZipBtn = document.getElementById("download-zip-btn");
const zipLoader = document.getElementById("zip-loader"); const zipLoader = document.getElementById("zip-loader");
// Handle build button click // Build action handler
if (buildBtn) { async function handleBuildClick() {
buildBtn.addEventListener("click", async () => { showLoader("Building static site...");
// Trigger build on backend // Trigger build on backend
const res = await fetch("/api/build", { method: "POST" }); const res = await fetch("/api/build", { method: "POST" });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
// Show build success modal // Show build success modal
if (buildModal) buildModal.style.display = "flex"; if (buildModal) buildModal.style.display = "flex";
} else { } else {
showToast(result.message || "❌ Build failed!", "error"); 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 // Handle download zip button click

View File

@ -92,6 +92,14 @@ function renderTags(imgIndex, tags) {
input.placeholder = 'Add tag...'; input.placeholder = 'Add tag...';
inputContainer.appendChild(input); 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'); const suggestionBox = document.createElement('ul');
suggestionBox.className = 'suggestions'; suggestionBox.className = 'suggestions';
inputContainer.appendChild(suggestionBox); inputContainer.appendChild(suggestionBox);
@ -148,8 +156,14 @@ function renderTags(imgIndex, tags) {
} }
}; };
input.addEventListener('input', updateSuggestions); input.addEventListener('input', () => {
input.addEventListener('focus', updateSuggestions); 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) => { input.addEventListener('keydown', (e) => {
const items = suggestionBox.querySelectorAll('li'); const items = suggestionBox.querySelectorAll('li');
@ -172,11 +186,13 @@ function renderTags(imgIndex, tags) {
} }
input.value = ''; input.value = '';
updateSuggestions(); updateSuggestions();
validateBtn.style.display = 'none';
} else if ([' ', ','].includes(e.key)) { } else if ([' ', ','].includes(e.key)) {
e.preventDefault(); e.preventDefault();
addTag(input.value); addTag(input.value);
input.value = ''; input.value = '';
updateSuggestions(); updateSuggestions();
validateBtn.style.display = 'none';
} }
}); });
@ -184,9 +200,20 @@ function renderTags(imgIndex, tags) {
setTimeout(() => { setTimeout(() => {
suggestionBox.style.display = 'none'; suggestionBox.style.display = 'none';
input.value = ''; input.value = '';
validateBtn.style.display = 'none';
}, 150); }, 150);
}); });
// --- Validate button action ---
validateBtn.onclick = () => {
if (input.value.trim()) {
addTag(input.value.trim());
input.value = '';
updateSuggestions();
validateBtn.style.display = 'none';
}
};
input.focus(); input.focus();
updateSuggestions(); updateSuggestions();
} }

View File

@ -12,6 +12,19 @@ function showToast(message, type = "success", duration = 3000) {
}, duration); }, 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", () => { document.addEventListener("DOMContentLoaded", () => {
// Form and menu logic // Form and menu logic
const form = document.getElementById("site-info-form"); const form = document.getElementById("site-info-form");
@ -130,10 +143,12 @@ document.addEventListener("DOMContentLoaded", () => {
thumbnailUpload.addEventListener("change", async (e) => { thumbnailUpload.addEventListener("change", async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
showLoader("Uploading thumbnail...");
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData }); const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
if (thumbnailInput) thumbnailInput.value = result.filename; if (thumbnailInput) thumbnailInput.value = result.filename;
updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`); updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
@ -183,12 +198,14 @@ document.addEventListener("DOMContentLoaded", () => {
themeUpload.addEventListener("change", async (e) => { themeUpload.addEventListener("change", async (e) => {
const files = Array.from(e.target.files); const files = Array.from(e.target.files);
if (files.length === 0) return; if (files.length === 0) return;
showLoader("Uploading theme...");
const formData = new FormData(); const formData = new FormData();
files.forEach(file => { files.forEach(file => {
formData.append("files", file, file.webkitRelativePath || file.name); formData.append("files", file, file.webkitRelativePath || file.name);
}); });
const res = await fetch("/api/theme/upload", { method: "POST", body: formData }); const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("✅ Theme uploaded!", "success"); showToast("✅ Theme uploaded!", "success");
// Refresh theme select after upload // Refresh theme select after upload
@ -239,12 +256,14 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
deleteThemeModalConfirm.onclick = async () => { deleteThemeModalConfirm.onclick = async () => {
if (!themeToDelete) return; if (!themeToDelete) return;
showLoader("Removing theme...");
const res = await fetch("/api/theme/remove", { const res = await fetch("/api/theme/remove", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme: themeToDelete }) body: JSON.stringify({ theme: themeToDelete })
}); });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("✅ Theme removed!", "success"); showToast("✅ Theme removed!", "success");
// Refresh theme select // Refresh theme select
@ -379,7 +398,9 @@ document.addEventListener("DOMContentLoaded", () => {
// Check if thumbnail is set before saving (uploaded or present in input) // Check if thumbnail is set before saving (uploaded or present in input)
if (!thumbnailInput || !thumbnailInput.value) { if (!thumbnailInput || !thumbnailInput.value) {
showLoader("Saving...");
showToast("❌ Thumbnail is required.", "error"); showToast("❌ Thumbnail is required.", "error");
hideLoader();
return; return;
} }
@ -417,6 +438,8 @@ document.addEventListener("DOMContentLoaded", () => {
intellectual_property: ipParagraphs intellectual_property: ipParagraphs
} }
}; };
// --- REMOVE loader for save ---
// showLoader("Saving...");
const res = await fetch("/api/site-info", { const res = await fetch("/api/site-info", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@ -31,6 +31,20 @@ function showToast(message, type = "success", duration = 3000) {
}, duration); }, 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) { function setupColorPicker(colorId, btnId, textId, initial) {
const colorInput = document.getElementById(colorId); const colorInput = document.getElementById(colorId);
const colorBtn = document.getElementById(btnId); const colorBtn = document.getElementById(btnId);
@ -40,7 +54,6 @@ function setupColorPicker(colorId, btnId, textId, initial) {
colorBtn.style.background = initial; colorBtn.style.background = initial;
textInput.value = initial.toUpperCase(); textInput.value = initial.toUpperCase();
// Color input is positioned over the button and is clickable
colorInput.addEventListener("input", () => { colorInput.addEventListener("input", () => {
colorBtn.style.background = colorInput.value; colorBtn.style.background = colorInput.value;
textInput.value = colorInput.value.toUpperCase(); textInput.value = colorInput.value.toUpperCase();
@ -178,11 +191,13 @@ document.addEventListener("DOMContentLoaded", async () => {
showToast("Only .woff and .woff2 fonts are allowed.", "error"); showToast("Only .woff and .woff2 fonts are allowed.", "error");
return; return;
} }
showLoader("Uploading font...");
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("theme", themeInfo.theme_name); formData.append("theme", themeInfo.theme_name);
const res = await fetch("/api/font/upload", { method: "POST", body: formData }); const res = await fetch("/api/font/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("✅ Font uploaded!", "success"); showToast("✅ Font uploaded!", "success");
localFonts = await fetchLocalFonts(themeInfo.theme_name); localFonts = await fetchLocalFonts(themeInfo.theme_name);
@ -219,9 +234,11 @@ document.addEventListener("DOMContentLoaded", async () => {
}; };
deleteFontModalConfirm.onclick = async () => { deleteFontModalConfirm.onclick = async () => {
if (!fontToDelete) return; if (!fontToDelete) return;
showLoader("Removing font...");
const result = await removeFont(themeInfo.theme_name, fontToDelete); const result = await removeFont(themeInfo.theme_name, fontToDelete);
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("Font removed!", "success"); showToast("Font removed!", "success");
localFonts = await fetchLocalFonts(themeInfo.theme_name); localFonts = await fetchLocalFonts(themeInfo.theme_name);
refreshLocalFonts(); refreshLocalFonts();
} else { } else {
@ -272,11 +289,13 @@ document.addEventListener("DOMContentLoaded", async () => {
showToast("Invalid file type for favicon.", "error"); showToast("Invalid file type for favicon.", "error");
return; return;
} }
showLoader("Uploading favicon...");
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("theme", themeInfo.theme_name); formData.append("theme", themeInfo.theme_name);
const res = await fetch("/api/favicon/upload", { method: "POST", body: formData }); const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
faviconInput.value = result.filename; faviconInput.value = result.filename;
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`); updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
@ -303,12 +322,14 @@ document.addEventListener("DOMContentLoaded", async () => {
} }
}; };
deleteFaviconModalConfirm.onclick = async () => { deleteFaviconModalConfirm.onclick = async () => {
showLoader("Removing favicon...");
const res = await fetch("/api/favicon/remove", { const res = await fetch("/api/favicon/remove", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme: themeInfo.theme_name }) body: JSON.stringify({ theme: themeInfo.theme_name })
}); });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
faviconInput.value = ""; faviconInput.value = "";
updateFaviconPreview(""); updateFaviconPreview("");
@ -335,13 +356,11 @@ document.addEventListener("DOMContentLoaded", async () => {
if (addGoogleFontBtn) { if (addGoogleFontBtn) {
addGoogleFontBtn.addEventListener("click", async () => { addGoogleFontBtn.addEventListener("click", async () => {
googleFonts.push({ family: "", weights: [] }); googleFonts.push({ family: "", weights: [] });
// Save immediately to backend
await fetch("/api/theme-google-fonts", { await fetch("/api/theme-google-fonts", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
}); });
// Fetch updated theme info and refresh dropdowns
const updatedThemeInfo = await fetchThemeInfo(); const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0; googleFonts.length = 0;
@ -353,13 +372,11 @@ document.addEventListener("DOMContentLoaded", async () => {
const googleFontsFields = document.getElementById("google-fonts-fields"); const googleFontsFields = document.getElementById("google-fonts-fields");
if (googleFontsFields) { if (googleFontsFields) {
// Save on blur for family/weights fields
googleFontsFields.addEventListener("blur", async (e) => { googleFontsFields.addEventListener("blur", async (e) => {
if ( if (
e.target.name && e.target.name &&
(e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]")) (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
) { ) {
// Update googleFonts array from the form fields
const fontFields = googleFontsFields.querySelectorAll(".input-field"); const fontFields = googleFontsFields.querySelectorAll(".input-field");
googleFonts.length = 0; googleFonts.length = 0;
fontFields.forEach(field => { fontFields.forEach(field => {
@ -368,13 +385,11 @@ document.addEventListener("DOMContentLoaded", async () => {
.split(",").map(w => w.trim()).filter(Boolean); .split(",").map(w => w.trim()).filter(Boolean);
googleFonts.push({ family, weights }); googleFonts.push({ family, weights });
}); });
// Save immediately to backend
await fetch("/api/theme-google-fonts", { await fetch("/api/theme-google-fonts", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
}); });
// Fetch updated theme info and refresh dropdowns
const updatedThemeInfo = await fetchThemeInfo(); const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0; googleFonts.length = 0;
@ -382,20 +397,17 @@ document.addEventListener("DOMContentLoaded", async () => {
renderGoogleFonts(googleFonts); renderGoogleFonts(googleFonts);
refreshFontDropdowns(); refreshFontDropdowns();
} }
}, true); // Use capture phase to catch blur from children }, true);
// Delegate remove button click for Google Fonts
googleFontsFields.addEventListener("click", async (e) => { googleFontsFields.addEventListener("click", async (e) => {
if (e.target.classList.contains("remove-google-font")) { if (e.target.classList.contains("remove-google-font")) {
const idx = Number(e.target.dataset.idx); const idx = Number(e.target.dataset.idx);
googleFonts.splice(idx, 1); googleFonts.splice(idx, 1);
// Save immediately to backend
await fetch("/api/theme-google-fonts", { await fetch("/api/theme-google-fonts", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
}); });
// Fetch updated theme info and refresh dropdowns
const updatedThemeInfo = await fetchThemeInfo(); const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0; googleFonts.length = 0;
@ -406,9 +418,9 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
} }
// Form submit
document.getElementById("theme-editor-form").addEventListener("submit", async (e) => { document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
showLoader("Saving theme...");
const data = {}; const data = {};
data.colors = { data.colors = {
primary: document.getElementById("color-primary-text").value, primary: document.getElementById("color-primary-text").value,
@ -445,6 +457,7 @@ document.addEventListener("DOMContentLoaded", async () => {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data }) body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
}); });
hideLoader();
if (res.ok) { if (res.ok) {
showToast("✅ Theme saved!", "success"); showToast("✅ Theme saved!", "success");
} else { } else {

View File

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

View File

@ -1,43 +1,9 @@
<!DOCTYPE html> {% extends "template/base.html" %}
<html lang="en">
<head> {% block title %}Lumeex - Site Info{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"> {% block content %}
<title>Lumeex</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
</head>
<body>
<!-- Top bar -->
<div class="nav-bar">
<div class="content-inner nav">
<input type="checkbox" id="nav-check">
<div class="nav-header">
<div class="nav-title">
<img src="{{ url_for('static', filename='img/logo.svg') }}">
</div>
</div>
<div class="nav-btn">
<label for="nav-check">
<span></span>
<span></span>
<span></span>
</label>
</div>
<div class="nav-links">
<ul class="nav-list">
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
<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 id="site-info" class="content-inner">
<div id="toast-container"></div>
<h1>Edit Site Info</h1> <h1>Edit Site Info</h1>
<form id="site-info-form"> <form id="site-info-form">
<!-- Info Section --> <!-- Info Section -->
@ -166,8 +132,22 @@
</fieldset> </fieldset>
<button type="submit">Save</button> <button type="submit">Save</button>
</form> </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 confirmation modal (now outside .content-inner) --> </div>
<!-- Delete thumbnail confirmation modal-->
<div class="content-inner">
<div id="delete-modal" class="modal" style="display:none;"> <div id="delete-modal" class="modal" style="display:none;">
<div class="modal-content"> <div class="modal-content">
<span id="delete-modal-close" class="modal-close">&times;</span> <span id="delete-modal-close" class="modal-close">&times;</span>
@ -179,7 +159,9 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Delete theme confirmation modal --> <!-- Delete theme confirmation modal -->
<div class="content-inner">
<div id="delete-theme-modal" class="modal" style="display:none;"> <div id="delete-theme-modal" class="modal" style="display:none;">
<div class="modal-content"> <div class="modal-content">
<span id="delete-theme-modal-close" class="modal-close">&times;</span> <span id="delete-theme-modal-close" class="modal-close">&times;</span>
@ -191,17 +173,10 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Build success modal -->
<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>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/site-info.js') }}"></script> <script src="{{ url_for('static', filename='js/site-info.js') }}"></script>
<script src="{{ url_for('static', filename='js/build.js') }}"></script> {% endblock %}
</body>
</html>

View File

@ -1,18 +1,18 @@
/* --- Base Styles --- */ /* --- Base Styles --- */
body { body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
margin: 20px;
background: #111010; background: #111010;
/* background:radial-gradient(ellipse at bottom center, #002a30, #000000bd), radial-gradient(ellipse at top center, #0558a8, #000000fa); */
color: #FBFBFB; color: #FBFBFB;
display: flex;
flex-direction: column;
min-height: 100vh; min-height: 100vh;
margin:0px; margin:0px;
padding-top: 70px; width: 100vw;
} }
.content-inner { a {
max-width: 90%; text-decoration: none;
margin: 0 auto; color: #d3d3d3;
} }
h1, h2 { h1, h2 {
@ -22,37 +22,217 @@ h2 {
color: #55c3ec; color: #55c3ec;
} }
/* --- Toolbar --- */ .content-inner {
.toolbar { margin: 0 auto;
margin-bottom: 20px; max-width: 1220px;
padding-top: 70px;
width: 100%;
}
.inner {
padding: 0 40px;
margin: auto;
width: 100%;
box-sizing: border-box;
} }
.toolbar button {
margin-right: 10px; /* --- Navbar & Burger Menu --- */
padding: 8px 12px;
.nav-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 70px;
background: #0c0d0c29;
z-index: 1000;
backdrop-filter: blur(20px);
border-bottom: 1px solid #21212157;
display: flex;
align-items: center;
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1140px;
width: 100%;
padding: 0 40px;
margin: 0 auto;
height: 70px;
position: relative;
}
.nav-header {
display: flex;
align-items: center;
}
.nav-title {
display: flex;
align-items: center;
font-size: 22px;
color: #fff;
}
.nav img {
height: 30px;
}
.nav-links {
display: flex;
align-items: center;
}
.nav-list {
display: flex;
align-items: center;
gap: 0;
list-style: none;
margin: 0;
padding: 0;
}
.nav-item {
display: flex;
align-items: center;
}
.nav-item a {
display: flex;
align-items: center;
font-weight: bold;
color:#fff;
transition: all 0.2s ease;
}
.nav-item a:hover {
display: flex;
align-items: center;
font-weight: bold;
color: #55c3ec;
transition: all 0.2s ease;
}
.nav-list > li + li::before {
content: " → ";
color: #ffc700;
margin: 0 8px;
}
.button {
padding: 10px 25px;
border-radius: 40px;
margin-left: 10px;
font-size: 14px;
background: linear-gradient(135deg, #26c4ff, #016074);
color: #fff;
font-weight: 700;
border: none; border: none;
background-color: #4CAF50;
color: white;
cursor: pointer; cursor: pointer;
border-radius: 4px; transition: background 0.2s;
transition: background-color 0.2s;
} }
.toolbar button:hover { .button:hover {
background-color: #45a049; background: linear-gradient(135deg, #72d9ff, #26657e);
} }
/* --- Burger Menu --- */
.nav-burger {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
cursor: pointer;
margin-left: auto;
z-index: 1100;
}
.nav-burger span {
display: block;
width: 28px;
height: 4px;
margin: 4px;
background: #fff;
border-radius: 2px;
transition: 0.3s;
position: relative;
}
/* --- Responsive Navbar & Burger --- */
@media (max-width: 768px) {
.nav-burger {
display: flex;
}
.nav-list > li::before {
display: none;
}
.nav-links {
position: absolute;
top: 70px;
left: 0;
width: 100vw;
flex-direction: column;
align-items: flex-start;
padding: 24px 0 12px 0;
display: none;
z-index: 1099;
backdrop-filter: blur(20px);
background-color: #000000d4
}
.nav-links .nav-list {
flex-direction: column;
width: 100%;
}
.nav-links .nav-item {
width: 100%;
margin: 0;
padding: 0;
}
.nav-links .nav-item a,
.nav-links .nav-item button {
width: 100%;
padding: 16px 24px;
text-align: center;
font-size: 18px;
border: none;
justify-content: center;
margin: 0 20px;
}
/* Show menu when burger is checked */
.nav-toggle:checked ~ .nav-burger + .nav-links {
display: flex;
}
/* Animate burger to X */
.nav-toggle:checked ~ .nav-burger span:nth-child(1) {
transform: translateY(12px) rotate(45deg);
}
.nav-toggle:checked ~ .nav-burger span:nth-child(2) {
opacity: 0;
}
.nav-toggle:checked ~ .nav-burger span:nth-child(3) {
transform: translateY(-12px) rotate(-45deg);
}
}
/* --- Upload Section --- */ /* --- Upload Section --- */
.upload-section { .section {
margin-bottom: 30px; margin-bottom: 30px;
background-color: rgb(67 67 67 / 26%); background-color: rgb(67 67 67 / 26%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #2f2e2e80; border: 1px solid #2f2e2e80;
border-radius: 8px; border-radius: 8px;
padding: 0px 20px 20px 20px; padding: 0px 20px 20px 20px;
width: 100%;
box-sizing: border-box;
} }
.upload-section label { .section label {
cursor: pointer; cursor: pointer;
} }
@ -103,22 +283,6 @@ h2 {
background-color: #d32f2f; background-color: #d32f2f;
} }
/* --- Responsive Adjustments --- */
@media (max-width: 500px) {
body {
margin: 10px;
}
.toolbar button {
margin-bottom: 8px;
width: 100%;
}
.upload-section label {
display: block;
margin-bottom: 10px;
}
}
/* --- Toast Notifications --- */ /* --- Toast Notifications --- */
#toast-container { #toast-container {
@ -155,7 +319,6 @@ h2 {
/* --- Tags --- */ /* --- Tags --- */
.tag-input { .tag-input {
display: flex; display: flex;
flex-wrap: wrap-reverse;
align-content: flex-start; align-content: flex-start;
gap: 4px; gap: 4px;
padding: 4px; padding: 4px;
@ -221,6 +384,24 @@ h2 {
margin-top: 8px; margin-top: 8px;
} }
.photo button.validate-tag-btn {
border-radius: 30px;
border: none;
background: #049b3d;
color: #fff;
font-size: 10px;
cursor: pointer;
margin-left: 4px;
transition: all ease 0.2s;
width: 35px;
border: 1px solid #585858;
}
.photo button.validate-tag-btn:hover {
background: #02cb4e;
}
.suggestions li.selected { .suggestions li.selected {
background-color: #007782; background-color: #007782;
color: white; color: white;
@ -249,150 +430,8 @@ h2 {
width: 100% width: 100%
} }
/* --- Top Bar & Navigation --- */ /* --- Upload Buttons --- */
.nav { .up-btn, .footer-links a{
height: 100%;
max-width: 1140px;
padding: 0 40px;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 70px;
background-color: #0c0d0c29;
z-index: 1000;
backdrop-filter: blur(20px);
border-bottom: 1px solid #21212157;
}
.nav img {
height: 30px;
padding: 0px;
margin-top: 10px;
}
.nav > .nav-header {
display: inline;
}
.nav > .nav-header > .nav-title {
display: inline-block;
font-size: 22px;
color: #fff;
padding: 0;
margin-top: 10px;
}
.nav > .nav-btn {
display: none;
}
.nav-btn label {
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
width: 32px;
height: 32px;
}
.nav > .nav-links {
display: inline;
float: right;
font-size: 14px;
height: 100%;
line-height: 70px;
}
.nav-item {
display: inline;
}
.nav-list {
list-style-type: disc;
margin: 0px;
padding: 0px;
}
.nav > .nav-links > .nav-list > .nav-item > a {
display: inline-block;
padding: 0px 15px 0px 15px;
text-decoration: none;
height: 100%;
font-weight: 700;
color:#fff
}
.nav > .nav-links > .nav-list > .nav-item > a:hover {
color: #00b0f0;
}
.nav > .nav-links > .nav-list > .nav-item > a:active {
color: #00b0f0;
}
.nav > #nav-check {
display: none;
}
.nav-list > li + li::before{
content: " → ";
color: #ffc700;
}
.nav-cta {
display: inline;
float: right;
height: 70px;
line-height: 70px;
}
.nav-cta > .arrow {
font-size: 12px;
display: inline;
color: #ffc700;
font-weight: 700;
}
.nav-bar .button {
padding: 10px 25px;
border-radius: 40px;
margin: 15px 20px 15px 10px;
font-size: 12px;
display: inline;
background: linear-gradient(135deg, #26c4ff, #016074);
transition: all 0.2s ease;
text-decoration: none;
color: #fff;
font-weight: 700;
border: none;
cursor: pointer;
}
.nav-bar .button:hover {
background: linear-gradient(135deg, #72d9ff, #26657e);
transition: all 0.2s ease;
cursor: pointer;
}
.nav-links > ul {
display: inline-block;
}
.nav-btn span {
display: block;
height: 4px;
width: 28px;
background: #fff;
margin: 4px 0;
border-radius: 2px;
transition: all 0.3s;
}
/* --- Custom Upload Buttons --- */
.up-btn {
display: inline-block; display: inline-block;
background: #00000000; background: #00000000;
color: #fff; color: #fff;
@ -403,12 +442,11 @@ h2 {
text-align: center; text-align: center;
transition: all 0.1s ease; transition: all 0.1s ease;
user-select: none; user-select: none;
/* box-shadow: 0 4px 10px rgba(0,0,0,0.25);*/
font-size: 14px; font-size: 14px;
border: 1px solid #585858; border: 1px solid #585858;
} }
.up-btn:hover { .up-btn:hover, .footer-links a:hover {
background: #2d2d2d; background: #2d2d2d;
} }
@ -420,18 +458,20 @@ h2 {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0 30px;
} }
.modal-content { .modal-content {
background: #131313; background: #000000a3;
color: #fff; color: #fff;
padding: 2rem 2.5rem; padding: 2rem 2.5rem;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
min-width: 300px; min-width: 200px;
max-width: 90vw; max-width: 90vw;
position: relative; position: relative;
text-align: center; text-align: center;
backdrop-filter: blur(20px);
} }
.modal-close { .modal-close {
@ -483,11 +523,12 @@ h2 {
margin-bottom: 10px; margin-bottom: 10px;
} }
/* Remove All Buttons */ /* --- Remove All Buttons --- */
#remove-all-hero, #remove-all-gallery { #remove-all-hero, #remove-all-gallery {
background: #2d2d2d; background: #2d2d2d;
color: white; color: white;
display: none; display: none;
margin-bottom: 6px;
} }
#remove-all-gallery:hover, #remove-all-gallery:hover,
@ -495,7 +536,6 @@ h2 {
background: rgb(121, 26, 19); background: rgb(121, 26, 19);
} }
/* Responsive: stack buttons vertically on small screens */
@media (max-width: 500px) { @media (max-width: 500px) {
.upload-actions-row { .upload-actions-row {
flex-direction: column; flex-direction: column;
@ -504,16 +544,7 @@ h2 {
} }
} }
/* --- Site Info --- */ /* --- Forms --- */
/* --- Site Info Form --- */
#site-info.content-inner, #theme-editor.content-inner {
margin-right: auto;
margin-left: auto;
max-width: 1140px;
padding: 0 40px 40px 40px;
}
fieldset { fieldset {
background-color: rgb(67 67 67 / 26%); background-color: rgb(67 67 67 / 26%);
@ -541,7 +572,7 @@ legend {
.input-field { .input-field {
flex: 1 1 calc(33.333% - 18px); flex: 1 1 calc(33.333% - 18px);
min-width: 220px; min-width: 150px;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@ -573,6 +604,16 @@ label {
box-shadow: 0 2px 8px rgba(0,0,0,0.07); box-shadow: 0 2px 8px rgba(0,0,0,0.07);
} }
#theme-editor-form input, #theme-editor-form textarea,#theme-editor-form select {
margin-bottom: 18px;
}
#theme-editor-form .fields {
gap: 0 18px;
}
#site-info-form input::placeholder, #site-info-form input::placeholder,
#theme-editor-form input::placeholder, #theme-editor-form input::placeholder,
#site-info-form textarea::placeholder, #site-info-form textarea::placeholder,
@ -618,7 +659,7 @@ img#thumbnail-preview {
border-radius: 30px; border-radius: 30px;
padding: 12px 32px; padding: 12px 32px;
font-size: 1.1em; font-size: 1.1em;
margin-top: 18px; margin: 0 0 45px 0;
cursor: pointer; cursor: pointer;
box-shadow: 0 4px 16px rgba(38,196,255,0.15); box-shadow: 0 4px 16px rgba(38,196,255,0.15);
transition: background 0.2s; transition: background 0.2s;
@ -632,7 +673,7 @@ img#thumbnail-preview {
background: #00000000; background: #00000000;
color: #fff; color: #fff;
border: none; border: none;
border-radius: 18px; border-radius: 30px;
padding: 7px 18px; padding: 7px 18px;
font-size: 0.98em; font-size: 0.98em;
margin-top: 8px; margin-top: 8px;
@ -646,20 +687,6 @@ img#thumbnail-preview {
color: #fff; color: #fff;
} }
@media (max-width: 900px) {
#site-info-form, #theme-editor-form {
padding: 18px 8px;
}
.fields,
fieldset {
flex-direction: column;
gap: 0;
}
.input-field {
min-width: 100%;
margin-bottom: 12px;
}
}
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph { #site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph {
margin-top: 0px; margin-top: 0px;
@ -691,19 +718,21 @@ img#thumbnail-preview {
} }
.color-fields { .fields.color-fields {
gap: 8px; gap: 8px;
position: relative; position: relative;
flex-wrap: nowrap;
} }
#theme-editor button.color-btn { #theme-editor-form button.color-btn {
height: 100%; height: 40px;
border-radius: 8px; border-radius: 40px;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
margin-top: 0px; margin-top: 0px;
margin-bottom: 4px; margin-bottom: 4px;
width: 40px;
} }
input[type="color"].color-input { input[type="color"].color-input {
@ -717,7 +746,7 @@ input[type="color"].color-input {
opacity:0; opacity:0;
} }
fieldset p { fieldset p, .section p {
font-size: 14px; font-size: 14px;
font-style: italic; font-style: italic;
color: #b3b3b3; color: #b3b3b3;
@ -741,3 +770,270 @@ fieldset p {
#theme-editor-form button[type="button"]#choose-font-btn { #theme-editor-form button[type="button"]#choose-font-btn {
margin-top: 16px; margin-top: 16px;
} }
/* --- Stepper --- */
#stepper {
display: flex;
gap: 18px;
flex-wrap: nowrap;
align-items: stretch;
padding: 0;
margin-left: auto;
margin-right: auto;
}
#stepper li,
#stepper > div {
display: flex;
align-items: center;
}
#stepper > div::before {
content: "→";
color: #ffc700;
}
#stepper li a, #stepper li button, #stepper > div {
justify-content: center;
min-width: 100px;
border: 1px solid #585858;
padding: 16px;
border-radius: 8px;
background: #111010;
text-align: center;
box-sizing: border-box;
width: 100%;
}
#stepper li a, #stepper li button {
height: 100%;
align-items: center;
display: flex;
font-weight: bold;
}
#stepper li button#stepper-build {
background: linear-gradient(135deg, #26c4ff, #016074);
transition: all 0.2s ease;
font-weight: bold;
color:#fff;
font-size: 16px;
}
#stepper li button#stepper-build:hover {
background: linear-gradient(135deg, #72d9ff, #26657e);
transition: all 0.2s ease;
color:#fff;
cursor: pointer;
}
#stepper li a:hover, #stepper li a.step-active {
color: #fff;
transition: all 0.2s ease;
background: #277fa0;
font-weight: bold;
}
#stepper li button#stepper-build::before {
content: "🚀 ";
margin-right: 8px;
}
#stepper li a {
text-decoration: none;
color:#d3d3d3
}
#stepper li {
flex: 1 1 auto;
}
#stepper > div {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
color: #ffc700;
border: none;
background: none;
padding: 0;
min-width: 0;
width: auto;
}
.stepper {
width: 100%;
}
/* --- Footer --- */
#footer {
background-color: #0c0d0c29;
z-index: 1000;
backdrop-filter: blur(20px);
border-top: 1px solid #21212157;
width: 100%;
margin-top: auto;
}
.footer-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
}
.footer-links, .footer-links a {
display: flex;
gap: 12px;
}
.footer-links a {
gap: 8px;
}
.lum-first {
font-weight: bold;
}
.lum-second {
color: #55c3ec;
font-weight: bold;
}
#footer a {
color: #fff;
}
.footer-credit .lum-first::before {
content: url(/img/favicon.svg);
display: inline-block;
}
#footer .content-inner {
padding-top: 0px;
}
.icon {
width: 16px;
height: 16px;
display: flex;
}
.icon-text {
display: flex;
}
/* --- Global Loader & Spinner --- */
#global-loader {
display: none;
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 99999;
background: rgba(0,0,0,0.4);
align-items: center;
justify-content: center;
}
#global-loader.active {
display: flex;
}
.loader-inner {
opacity: 0;
transition: opacity 0.9s cubic-bezier(.4,0,.2,1);
background: #0c0d0c29;
padding: 32px 48px;
border-radius: 16px;
box-shadow: 0 2px 24px #000;
display: flex;
flex-direction: column;
align-items: center;
backdrop-filter: blur(20px);
}
#global-loader.active .loader-inner {
opacity: 1;
}
.loader-spinner {
width: 48px;
height: 48px;
border: 6px solid #55c3ec;
border-top: 6px solid #222;
border-radius: 50%;
animation: spin 1s linear infinite;
}
#loader-text {
margin-top: 18px;
color: #fff;
font-size: 18px;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
/* --- Responsive Adjustments --- */
@media (max-width: 768px) {
.nav {
padding: 0 20px;
}
.inner {
padding: 0 20px;
}
.section label {
display: block;
margin-bottom: 10px;
}
#menu-items-list > div {
flex-direction: column;
}
#stepper {
flex-direction: column;
}
#stepper li {
width: 100%;
}
.footer-container, .footer-links {
flex-direction: column;
}
#ip-list > div {
flex-direction: column;
}
#stepper > div {
flex-direction: column;
}
#stepper > div::before {
content: "↓";
color: #ffc700;
}
#stepper > div {
/* Hide the default arrow */
color: transparent;
}
#site-info-form, #theme-editor-form {
padding: 18px 8px;
}
.input-field {
min-width: 100%;
margin-bottom: 12px;
}
#color-picker .input-field{
min-width: 170px;
}
#theme-editor-form .fields.color-fields {
gap: 0 8px;
position: relative;
flex-wrap: nowrap;
}
}

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

@ -1,43 +1,9 @@
<!DOCTYPE html> {% extends "template/base.html" %}
<html lang="en">
<head> {% block title %}Lumeex - Theme Editor{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"> {% block content %}
<title>Theme Editor</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
</head>
<body>
<!-- Top bar -->
<div class="nav-bar">
<div class="content-inner nav">
<input type="checkbox" id="nav-check">
<div class="nav-header">
<div class="nav-title">
<img src="{{ url_for('static', filename='img/logo.svg') }}">
</div>
</div>
<div class="nav-btn">
<label for="nav-check">
<span></span>
<span></span>
<span></span>
</label>
</div>
<div class="nav-links">
<ul class="nav-list">
<li class="nav-item"><a href="/gallery-editor">Gallery</a>
<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 id="theme-editor" class="content-inner">
<div id="toast-container"></div>
<h1>Edit Theme</h1> <h1>Edit Theme</h1>
<!-- Show current theme --> <!-- Show current theme -->
<div class="theme-info"> <div class="theme-info">
@ -45,7 +11,7 @@
</div> </div>
<form id="theme-editor-form"> <form id="theme-editor-form">
<!-- Colors Section --> <!-- Colors Section -->
<fieldset> <fieldset id="color-picker">
<h2>Colors</h2> <h2>Colors</h2>
<p>Set the color values for your theme</p> <p>Set the color values for your theme</p>
<div class="fields"> <div class="fields">
@ -169,6 +135,20 @@
</fieldset> </fieldset>
<button type="submit">Save Theme</button> <button type="submit">Save Theme</button>
</form> </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> </div>
<!-- Delete confirmation modal for favicon --> <!-- Delete confirmation modal for favicon -->
<div id="delete-favicon-modal" class="modal" style="display:none;"> <div id="delete-favicon-modal" class="modal" style="display:none;">
@ -194,17 +174,9 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Build success modal -->
<div id="build-success-modal" class="modal" style="display:none;"> {% endblock %}
<div class="modal-content">
<span id="build-success-modal-close" class="modal-close">&times;</span> {% block scripts %}
<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>
<script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script> <script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
<script src="{{ url_for('static', filename='js/build.js') }}"></script> {% endblock %}
</body>
</html>