refactor(ui): implement base template and shared CSS

Introduce a `base.html` template to establish a common structure for all pages using Jinja2 template inheritance. This eliminates redundant HTML boilerplate across multiple files.

Additionally, common CSS rules have been extracted from individual templates and consolidated into a single `static/css/main.css` file. All pages now link to this shared stylesheet.

This refactoring significantly reduces code duplication, improves maintainability, and ensures a consistent user interface.
This commit is contained in:
Karl 2025-07-18 13:56:38 +01:00
parent 31b373b34a
commit aa8e1d1701
8 changed files with 296 additions and 403 deletions

51
static/css/main.css Normal file
View File

@ -0,0 +1,51 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: black;
color: white;
font-family: sans-serif;
padding: 2rem;
}
.button-link {
background: #333;
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
display: inline-block;
text-align: center;
border: none;
}
.button-link:hover {
background: #555;
}
.version {
position: fixed;
bottom: 8px;
right: 12px;
color: #666;
font-size: 12px;
font-family: monospace;
user-select: none;
opacity: 0.6;
}
.version a {
color: inherit;
text-decoration: none;
cursor: pointer;
}
.version a:hover {
text-decoration: underline;
}

20
templates/base.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}AI Image Frame{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<!-- Version number at bottom right -->
<div class="version">
<a href="{{ url_for('settings_route.config_editor') }}">v{{ version }}</a>
</div>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -1,28 +1,15 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}Create An Image{% endblock %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create An Image</title>
<style>
/* ---------- reset ---------- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ---------- layout ---------- */ {% block head %}
<style>
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100vh; height: 100vh;
background: black;
color: white;
font-family: Arial, sans-serif;
padding: 20px; padding: 20px;
} }
@ -63,7 +50,6 @@
background: #555; background: #555;
} }
/* ---------- spinner ---------- */
#spinner-overlay { #spinner-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@ -113,15 +99,15 @@
height: 150px; height: 150px;
} }
} }
</style> </style>
</head> {% endblock %}
<body> {% block content %}
<h1 style="margin-bottom: 20px;">Create An Image</h1> <h1 style="margin-bottom: 20px;">Create An Image</h1>
<textarea id="prompt-box" placeholder="Enter your custom prompt here..."></textarea> <textarea id="prompt-box" placeholder="Enter your custom prompt here..."></textarea>
<div class="button-group"> <div class="button-group">
<button onclick="showSpinner(); location.href='/'">Back</button> <button onclick="showSpinner(); location.href='/'">Back</button>
<button onclick="sendPrompt()">Send Prompt</button> <button onclick="sendPrompt()">Send Prompt</button>
@ -130,13 +116,11 @@
<select id="model-select"> <select id="model-select">
<option value="" selected>Random</option> <option value="" selected>Random</option>
<!-- Group: FLUX -->
<optgroup label="FLUX"> <optgroup label="FLUX">
{% for m in models if 'flux' in m|lower %} {% for m in models if 'flux' in m|lower %}
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option> <option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
{% endfor %} {% endfor %}
</optgroup> </optgroup>
<!-- Group: SDXL -->
<optgroup label="SDXL"> <optgroup label="SDXL">
{% for m in models if 'flux' not in m|lower %} {% for m in models if 'flux' not in m|lower %}
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option> <option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
@ -153,16 +137,15 @@
{% endfor %} {% endfor %}
</optgroup> </optgroup>
</select> </select>
</div>
<div id="spinner-overlay">
</div>
<!-- waiting overlay -->
<div id="spinner-overlay">
<div class="spinner"></div> <div class="spinner"></div>
</div> </div>
{% endblock %}
<script> {% block scripts %}
<script>
const overlay = document.getElementById('spinner-overlay'); const overlay = document.getElementById('spinner-overlay');
function showSpinner() { overlay.style.visibility = 'visible'; } function showSpinner() { overlay.style.visibility = 'visible'; }
@ -190,15 +173,14 @@
}); });
} }
// wrapper for Random Prompt button so it also sends the model
function randomPrompt() { function randomPrompt() {
showSpinner(); showSpinner();
const model = document.getElementById('model-select').value; const model = document.getElementById('model-select').value;
const topic = document.getElementById('topic-select').value; // this line was missing const topic = document.getElementById('topic-select').value;
const formData = new URLSearchParams(); const formData = new URLSearchParams();
formData.append('model', model); formData.append('model', model);
formData.append('topic', topic); // include topic in request formData.append('topic', topic);
fetch('/create', { fetch('/create', {
method: 'POST', method: 'POST',
@ -213,7 +195,5 @@
alert("Error requesting random prompt: " + error); alert("Error requesting random prompt: " + error);
}); });
} }
</script> </script>
</body> {% endblock %}
</html>

View File

@ -1,24 +1,9 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}Image Archive{% endblock %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> {% block head %}
<title>Image Archive</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: black;
color: white;
font-family: sans-serif;
padding: 2rem;
}
h1 { h1 {
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 2rem;
@ -102,18 +87,14 @@
text-align: left; text-align: left;
margin-top: 20px; margin-top: 20px;
max-height: 25vh; max-height: 25vh;
/* NEW: restrict height */
overflow-y: auto; overflow-y: auto;
/* NEW: allow vertical scroll */
} }
/* Back button fixed top right */
.home-button { .home-button {
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 20px; right: 20px;
z-index: 500; z-index: 500;
/* lower than lightbox (999) */
} }
.favourites-button { .favourites-button {
@ -132,24 +113,6 @@
cursor: pointer; cursor: pointer;
} }
.button-link {
background: #333;
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
display: inline-block;
text-align: center;
border: none;
}
.button-link:hover {
background: #555;
}
@media (max-width: 600px) { @media (max-width: 600px) {
body { body {
padding: 1rem; padding: 1rem;
@ -181,9 +144,7 @@
max-width: 90%; max-width: 90%;
padding: 8px 16px; padding: 8px 16px;
max-height: 20vh; max-height: 20vh;
/* smaller height for mobile */
overflow-y: auto; overflow-y: auto;
/* keep scroll on mobile too */
} }
.button-link { .button-link {
@ -192,18 +153,16 @@
} }
} }
</style> </style>
</head> {% endblock %}
<body> {% block content %}
<a href="/" class="button-link home-button">Home</a> <a href="/" class="button-link home-button">Home</a>
<button class="button-link favourites-button" id="favourites-button" onclick="toggleFavouritesView()">Show Favourites</button> <button class="button-link favourites-button" id="favourites-button" onclick="toggleFavouritesView()">Show Favourites</button>
<h1 id="page-title">Image Archive</h1> <h1 id="page-title">Image Archive</h1>
<!-- Empty gallery container; images will be loaded incrementally -->
<div class="gallery" id="gallery"></div> <div class="gallery" id="gallery"></div>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" tabindex="-1" onkeyup="handleLightboxKeys(event)"> <div class="lightbox" id="lightbox" tabindex="-1" onkeyup="handleLightboxKeys(event)">
<span class="close" onclick="closeLightbox()">&times;</span> <span class="close" onclick="closeLightbox()">&times;</span>
<span class="favourite-heart" id="favourite-heart" onclick="toggleFavourite()">&#9825;</span> <span class="favourite-heart" id="favourite-heart" onclick="toggleFavourite()">&#9825;</span>
@ -212,8 +171,9 @@
<p id="lightbox-prompt"></p> <p id="lightbox-prompt"></p>
<span class="arrow right" onclick="nextImage()">&#10095;</span> <span class="arrow right" onclick="nextImage()">&#10095;</span>
</div> </div>
{% endblock %}
<!-- Pass image filenames from Flask to JS --> {% block scripts %}
<script> <script>
let allImages = JSON.parse(`[ let allImages = JSON.parse(`[
{% for image in images %} {% for image in images %}
@ -224,10 +184,10 @@
<script> <script>
const gallery = document.getElementById('gallery'); const gallery = document.getElementById('gallery');
const batchSize = 9; // images to load per batch const batchSize = 9;
let loadedCount = 0; let loadedCount = 0;
let currentIndex = 0; let currentIndex = 0;
const detailsCache = {}; // Cache for image details const detailsCache = {};
let showingFavourites = false; let showingFavourites = false;
let filteredImages = allImages; let filteredImages = allImages;
@ -276,19 +236,16 @@
renderGallery(); renderGallery();
} }
// Load initial batch
renderGallery(); renderGallery();
// Load more images when scrolling near bottom
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
const imagesToLoad = showingFavourites ? filteredImages : allImages; const imagesToLoad = showingFavourites ? filteredImages : allImages;
if (loadedCount >= imagesToLoad.length) return; // all loaded if (loadedCount >= imagesToLoad.length) return;
if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 100)) { if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 100)) {
loadNextBatch(); loadNextBatch();
} }
}); });
// Get current images in gallery for lightbox navigation
function getGalleryImages() { function getGalleryImages() {
return Array.from(gallery.querySelectorAll('img')); return Array.from(gallery.querySelectorAll('img'));
} }
@ -304,7 +261,7 @@
function updateFavouriteHeart(isFavourited) { function updateFavouriteHeart(isFavourited) {
const heart = document.getElementById('favourite-heart'); const heart = document.getElementById('favourite-heart');
heart.innerHTML = isFavourited ? '&#9829;' : '&#9825;'; // solid vs outline heart heart.innerHTML = isFavourited ? '&#9829;' : '&#9825;';
heart.style.color = isFavourited ? 'red' : 'white'; heart.style.color = isFavourited ? 'red' : 'white';
} }
@ -361,7 +318,7 @@
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
detailsCache[filename] = data; // Cache the data detailsCache[filename] = data;
document.getElementById("lightbox-prompt").textContent = document.getElementById("lightbox-prompt").textContent =
`Model:${data.model} - Created:${data.date}\n\n${data.prompt}`; `Model:${data.model} - Created:${data.date}\n\n${data.prompt}`;
}) })
@ -376,7 +333,6 @@
const imagesToLoad = showingFavourites ? filteredImages : allImages; const imagesToLoad = showingFavourites ? filteredImages : allImages;
if (currentIndex + 1 >= images.length && loadedCount < imagesToLoad.length) { if (currentIndex + 1 >= images.length && loadedCount < imagesToLoad.length) {
loadNextBatch(); loadNextBatch();
// Wait briefly to ensure DOM updates
setTimeout(() => { setTimeout(() => {
const updatedImages = getGalleryImages(); const updatedImages = getGalleryImages();
if (currentIndex + 1 < updatedImages.length) { if (currentIndex + 1 < updatedImages.length) {
@ -396,11 +352,9 @@
showImageAndLoadDetails(currentIndex); showImageAndLoadDetails(currentIndex);
} }
function closeLightbox() { function closeLightbox() {
document.getElementById("lightbox").style.display = "none"; document.getElementById("lightbox").style.display = "none";
if (showingFavourites) { if (showingFavourites) {
// Refresh the gallery if a favourite was removed
const currentImage = getGalleryImages()[currentIndex]; const currentImage = getGalleryImages()[currentIndex];
const wasFavourited = currentImage.dataset.favourited === 'true'; const wasFavourited = currentImage.dataset.favourited === 'true';
const originalImage = allImages.find(img => img.filename === currentImage.dataset.filename); const originalImage = allImages.find(img => img.filename === currentImage.dataset.filename);
@ -422,6 +376,4 @@
} }
} }
</script> </script>
</body> {% endblock %}
</html>

View File

@ -1,25 +1,15 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}Image Created{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block head %}
<title>Image Created</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100vh; height: 100vh;
background: black;
color: white;
font-family: Arial, sans-serif;
padding: 20px;
text-align: center; text-align: center;
} }
.message { .message {
@ -50,13 +40,13 @@
background: #555; background: #555;
} }
</style> </style>
</head> {% endblock %}
<body>
{% block content %}
<div class="message">Image will be made with <i>{{ model }}</i> using prompt:</div> <div class="message">Image will be made with <i>{{ model }}</i> using prompt:</div>
<div class="prompt-text"> <div class="prompt-text">
{{ prompt }} {{ prompt }}
</div> </div>
<button onclick="location.href='/'">Home</button> <button onclick="location.href='/'">Home</button>
</body> {% endblock %}
</html>

View File

@ -1,32 +1,20 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}AI Image of the Day{% endblock %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> {% block head %}
<title>AI Image of the Day</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100vh; height: 100vh;
background: black;
color: white;
font-family: Arial, sans-serif;
position: relative; position: relative;
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
} }
.image-container { .image-container {
max-width: 90vw; max-width: 90vw;
max-height: 80vh; max-height: 80vh;
@ -54,9 +42,7 @@
max-width: 80vw; max-width: 80vw;
text-align: left; text-align: left;
max-height: 30vh; max-height: 30vh;
/* NEW: limit height */
overflow-y: auto; overflow-y: auto;
/* NEW: allow scrolling */
} }
.button-group { .button-group {
@ -66,46 +52,6 @@
justify-content: center; justify-content: center;
} }
.button-link {
background: #333;
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
display: inline-block;
text-align: center;
}
.button-link:hover {
background: #555;
}
/* New style for version number */
.version {
position: fixed;
bottom: 8px;
right: 12px;
color: #666;
font-size: 12px;
font-family: monospace;
user-select: none;
opacity: 0.6;
}
.version a {
color: inherit;
text-decoration: none;
cursor: pointer;
}
.version a:hover {
text-decoration: underline;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.image-container { .image-container {
max-width: 100vw; max-width: 100vw;
@ -134,14 +80,9 @@
} }
} }
</style> </style>
<script> {% endblock %}
setInterval(() => {
location.reload();
}, {{ reload_interval }}); // Refresh every X ms
</script>
</head>
<body> {% block content %}
{% if image %} {% if image %}
<div class="image-container"> <div class="image-container">
<img src="{{ url_for('image_routes.serve_image', filename=image) }}" alt="Latest Image" /> <img src="{{ url_for('image_routes.serve_image', filename=image) }}" alt="Latest Image" />
@ -156,11 +97,12 @@
{% else %} {% else %}
<p>No images found</p> <p>No images found</p>
{% endif %} {% endif %}
{% endblock %}
<!-- Version number at bottom right --> {% block scripts %}
<div class="version"> <script>
<a href="{{ url_for('settings_route.config_editor') }}">v{{ version }}</a> setInterval(() => {
</div> location.reload();
</body> }, {{ reload_interval }}); // Refresh every X ms
</script>
</html> {% endblock %}

View File

@ -1,25 +1,15 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}Login{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block head %}
<title>Login</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100vh; height: 100vh;
background: black;
color: white;
font-family: Arial, sans-serif;
padding: 20px;
text-align: center; text-align: center;
} }
.message { .message {
@ -59,8 +49,9 @@
background: #555; background: #555;
} }
</style> </style>
</head> {% endblock %}
<body>
{% block content %}
<div class="message">Please enter the password to continue:</div> <div class="message">Please enter the password to continue:</div>
<form method="post"> <form method="post">
<div class="prompt-text"> <div class="prompt-text">
@ -69,5 +60,4 @@
</div> </div>
<button type="submit">Login</button> <button type="submit">Login</button>
</form> </form>
</body> {% endblock %}
</html>

View File

@ -1,21 +1,10 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}Config Editor{% endblock %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> {% block head %}
<title>Config Editor</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
background: black;
color: white;
font-family: Arial, sans-serif;
min-height: 100vh; min-height: 100vh;
padding: 40px 20px; padding: 40px 20px;
display: flex; display: flex;
@ -74,21 +63,11 @@
margin-top: 20px; margin-top: 20px;
} }
.button-link { .back-button-wrapper {
background: #333; width: 100%;
height: 40px; display: flex;
color: white; justify-content: center;
text-decoration: none; margin-top: 20px;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
transition: background 0.3s;
display: inline-block;
text-align: center;
}
.button-link:hover {
background: #555;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -102,18 +81,10 @@
margin-bottom: 30px; margin-bottom: 30px;
} }
} }
.back-button-wrapper {
width: 100%;
display: flex;
justify-content: center;
margin-top: 20px;
}
</style> </style>
</head> {% endblock %}
<body>
{% block content %}
<div class="box"> <div class="box">
<h2>Topics</h2> <h2>Topics</h2>
<form method="post"> <form method="post">
@ -186,7 +157,4 @@
<div class="back-button-wrapper"> <div class="back-button-wrapper">
<a href="/" class="button-link">Back to Home</a> <a href="/" class="button-link">Back to Home</a>
</div> </div>
{% endblock %}
</body>
</html>