Compare commits

...

16 Commits
0.2.9 ... main

Author SHA1 Message Date
82f93ae557 Bump version: 0.2.15 → 0.2.16 2025-07-18 14:05:35 +01:00
651a8b364d Bump version: 0.2.14 → 0.2.15 2025-07-18 13:59:12 +01:00
43f9fcf276 add bump and push 2025-07-18 13:59:06 +01:00
095a0e66ae readme.md :) 2025-07-18 13:58:18 +01:00
aa8e1d1701 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.
2025-07-18 13:56:38 +01:00
31b373b34a Bump version: 0.2.13 → 0.2.14 2025-07-10 17:12:23 +01:00
8665ab431c rework version 2025-07-10 17:12:14 +01:00
1b528a4277 new version 2025-07-10 17:11:09 +01:00
6210b5de7d updated toml 2025-07-10 17:10:00 +01:00
ab32c8032c add and update version to docker image 2025-07-10 17:08:58 +01:00
43ce8e1b89 Bump version: 0.2.10 → 0.2.11 2025-07-10 16:59:03 +01:00
826e91e7e2 Bump version: 0.2.9 → 0.2.10 2025-07-10 16:58:56 +01:00
98f7a87005 revert to 0.2.9 2025-07-10 16:58:50 +01:00
bc933c2308 Bump version: 0.2.9 → 0.2.10 2025-07-10 16:25:14 +01:00
d5c4d54041 Bump version: 0.2.8 → 0.2.9 2025-07-10 16:25:09 +01:00
ba9b4b8bd4 fix(favorites): preserve favorite status after image rename
Previously, if a user favorited a newly generated image (named 'image.png'), the favorite status would be lost once the image was automatically renamed to its final timestamped filename.

This change modifies the renaming logic to check the `favourites.json` file. If 'image.png' is found, its entry is updated with the new filename, ensuring the favorite status is preserved.
2025-07-10 16:24:57 +01:00
13 changed files with 451 additions and 411 deletions

View File

@ -1,19 +1,24 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.2.8" current_version = "0.2.16"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)" parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"] serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
replace = "{new_version}" replace = "{new_version}"
regex = false regex = false
ignore_missing_version = false
ignore_missing_files = false
tag = true tag = true
sign_tags = false
tag_name = "{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
allow_dirty = false
commit = true commit = true
message = "Bump version: {current_version} → {new_version}" message = "Bump version: {current_version} → {new_version}"
tag_name = "{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
[[tool.bumpversion.files]]
filename = ".bumpversion.toml"
search = 'current_version = "{current_version}"'
replace = 'current_version = "{new_version}"'
[[tool.bumpversion.files]]
filename = "Dockerfile"
search = 'ARG VERSION="{current_version}"'
replace = 'ARG VERSION="{new_version}"'
moveable_tags = [] moveable_tags = []
commit_args = "" commit_args = ""
setup_hooks = [] setup_hooks = []

View File

@ -3,10 +3,14 @@ FROM python:3.11-slim
# Set the working directory in the container # Set the working directory in the container
WORKDIR /app WORKDIR /app
# Set version label
ARG VERSION="0.2.16"
LABEL version=$VERSION
# Copy project files into the container # Copy project files into the container
COPY . /app COPY . /app
# Install dependencies # Install dependencies
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt

108
README.md Normal file
View File

@ -0,0 +1,108 @@
# AI Image Frame Server
This project is a Flask-based web server designed to generate and display images from various AI models, primarily interacting with ComfyUI. It can be configured to automatically generate new images at scheduled times and provides a gallery to view the generated images.
## Features
* **Web Interface:** A simple web interface to view generated images.
* **Image Generation:** Integrates with ComfyUI to generate images based on given prompts and models.
* **Scheduled Generation:** Automatically generates new images at a configurable time.
* **Docker Support:** Comes with a `Dockerfile` and `docker-compose.yml` for easy setup and deployment.
* **Configurable:** Most options can be configured through a `user_config.cfg` file.
* **Authentication:** Optional password protection for image creation.
## Prerequisites
* Python 3.11
* Docker (for containerized deployment)
* An instance of ComfyUI running and accessible from the server.
## Installation and Setup
### Manual Setup
1. **Clone the repository:**
```bash
git clone <repository_url>
cd ai_image_frame_server
```
2. **Install dependencies:**
```bash
pip install -r requirements.txt
```
3. **Configure the application:**
* Copy the `user_config.cfg.sample` to `user_config.cfg`.
* Edit `user_config.cfg` with your settings. See the [Configuration](#configuration) section for more details.
4. **Run the application:**
```bash
export SECRET_KEY='a_very_secret_key'
python ai_frame_image_server.py
```
### Docker Setup
1. **Clone the repository:**
```bash
git clone <repository_url>
cd ai_image_frame_server
```
2. **Configure the application:**
* Copy the `user_config.cfg.sample` to `user_config.cfg`.
* Edit `user_config.cfg` with your settings. The `comfyui_url` should be the address of your ComfyUI instance, accessible from within the Docker network (e.g., `http://host.docker.internal:8188` or your server's IP).
3. **Build and run with Docker Compose:**
```bash
docker-compose up --build
```
The application will be available at `http://localhost:8088`.
## Configuration
The application is configured via the `user_config.cfg` file.
| Section | Key | Description | Default (from sample) |
| :-------- | :------------------- | :-------------------------------------------------------------------------- | :-------------------- |
| `[frame]` | `reload_interval` | How often the gallery page reloads in milliseconds. | `30000` |
| `[frame]` | `auto_regen` | Enable or disable automatic image generation (`True`/`False`). | `True` |
| `[frame]` | `regen_time` | The time to automatically generate a new image (HH:MM). | `03:00` |
| `[frame]` | `port` | The port the Flask application will run on. | `5000` |
| `[frame]` | `create_requires_auth` | Require a password to create images (`True`/`False`). | `False` |
| `[frame]` | `password_for_auth` | The password to use for image creation if authentication is enabled. | `create` |
| `[comfyui]` | `comfyui_url` | The URL of your ComfyUI instance. | `http://comfyui` |
| `[comfyui]` | `models` | A comma-separated list of models to use for generation. | `zavychromaxl_v100.safetensors,ponyDiffusionV6XL_v6StartWithThisOne.safetensors` |
| `[comfyui]` | `output_dir` | The directory to save generated images to. | `./output/` |
| `[comfyui]` | `prompt` | The prompt to use for generating a random prompt for stable diffusion. | `"Generate a random detailed prompt for stable diffusion."` |
| `[comfyui]` | `width` | The width of the generated image. | `1568` |
| `[comfyui]` | `height` | The height of the generated image. | `672` |
| `[comfyui]` | `topics` | A comma-separated list of topics to generate prompts from. | |
| `[comfyui]` | `FLUX` | Enable FLUX models (`True`/`False`). | `False` |
| `[comfyui]` | `ONLY_FLUX` | Only use FLUX models (`True`/`False`). | `False` |
| `[comfyui:flux]` | `models` | A comma-separated list of FLUX models. | `flux1-dev-Q4_0.gguf,flux1-schnell-Q4_0.gguf` |
| `[openwebui]` | `base_url` | The base URL for OpenWebUI. | `https://openwebui` |
| `[openwebui]` | `api_key` | The API key for OpenWebUI. | `sk-` |
| `[openwebui]` | `models` | A comma-separated list of models for OpenWebUI. | `llama3:latest,cogito:14b,gemma3:12b` |
## Usage
* **Gallery:** Open your browser to `http://<server_ip>:<port>` to see the gallery of generated images.
* **Create Image:** Navigate to `/create` to manually trigger image generation.
## Dependencies
* Flask
* comfy_api_simplified
* APScheduler
* Pillow
* And others, see `requirements.txt`.
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## License
This project is unlicensed.

18
bump_and_push.sh Normal file
View File

@ -0,0 +1,18 @@
#!/bin/bash
set -ex
# Check if an argument is provided
if [ -z "$1" ]; then
echo "Usage: $0 <part>"
echo "Example: $0 patch"
exit 1
fi
PART=$1
# Bump the version
bump-my-version bump $PART
# Push the changes
git push
git push origin --tags

View File

@ -51,10 +51,22 @@ def load_config() -> configparser.ConfigParser:
def rename_image() -> str | None: def rename_image() -> str | None:
"""Renames 'image.png' in the output folder to a timestamped filename if it exists.""" """Renames 'image.png' in the output folder to a timestamped filename if it exists."""
old_path = os.path.join(user_config["comfyui"]["output_dir"], "image.png") old_path = os.path.join(user_config["comfyui"]["output_dir"], "image.png")
favourites_file = "./favourites.json"
if os.path.exists(old_path): if os.path.exists(old_path):
new_filename = f"{str(time.time())}.png" new_filename = f"{str(time.time())}.png"
new_path = os.path.join(user_config["comfyui"]["output_dir"], new_filename) new_path = os.path.join(user_config["comfyui"]["output_dir"], new_filename)
# Check if image.png is a favourite
if os.path.exists(favourites_file):
with open(favourites_file, 'r') as f:
favourites = json.load(f)
if "image.png" in favourites:
favourites.remove("image.png")
favourites.append(new_filename)
with open(favourites_file, 'w') as f:
json.dump(favourites, f)
os.rename(old_path, new_path) os.rename(old_path, new_path)
generate_thumbnail(new_path) generate_thumbnail(new_path)
print(f"Renamed 'image.png' to '{new_filename}'") print(f"Renamed 'image.png' to '{new_filename}'")

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>