mirror of
https://github.com/karl0ss/ai_image_frame_server.git
synced 2025-07-19 19:24:59 +01:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
82f93ae557 | |||
651a8b364d | |||
43f9fcf276 | |||
095a0e66ae | |||
aa8e1d1701 | |||
31b373b34a | |||
8665ab431c | |||
1b528a4277 | |||
6210b5de7d | |||
ab32c8032c | |||
43ce8e1b89 | |||
826e91e7e2 | |||
98f7a87005 | |||
bc933c2308 | |||
d5c4d54041 | |||
ba9b4b8bd4 | |||
7f78912829 | |||
33404c7a37 |
@ -1,19 +1,24 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "0.2.7"
|
||||
current_version = "0.2.16"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
search = "{current_version}"
|
||||
replace = "{new_version}"
|
||||
regex = false
|
||||
ignore_missing_version = false
|
||||
ignore_missing_files = false
|
||||
tag = true
|
||||
sign_tags = false
|
||||
tag_name = "{new_version}"
|
||||
tag_message = "Bump version: {current_version} → {new_version}"
|
||||
allow_dirty = false
|
||||
commit = true
|
||||
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 = []
|
||||
commit_args = ""
|
||||
setup_hooks = []
|
||||
|
@ -3,10 +3,14 @@ FROM python:3.11-slim
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
# Set version label
|
||||
ARG VERSION="0.2.16"
|
||||
LABEL version=$VERSION
|
||||
|
||||
# Copy project files into the container
|
||||
COPY . /app
|
||||
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
108
README.md
Normal file
108
README.md
Normal 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
18
bump_and_push.sh
Normal 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
|
@ -51,10 +51,22 @@ def load_config() -> configparser.ConfigParser:
|
||||
def rename_image() -> str | None:
|
||||
"""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")
|
||||
favourites_file = "./favourites.json"
|
||||
|
||||
if os.path.exists(old_path):
|
||||
new_filename = f"{str(time.time())}.png"
|
||||
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)
|
||||
generate_thumbnail(new_path)
|
||||
print(f"Renamed 'image.png' to '{new_filename}'")
|
||||
|
@ -31,28 +31,20 @@ def gallery():
|
||||
def get_favourites_route():
|
||||
return jsonify(get_favourites())
|
||||
|
||||
@bp.route("/favourites/add", methods=["POST"])
|
||||
def add_favourite():
|
||||
data = request.get_json()
|
||||
filename = data.get("filename")
|
||||
if not filename:
|
||||
return jsonify({"status": "error", "message": "Filename missing"}), 400
|
||||
|
||||
favourites = get_favourites()
|
||||
if filename not in favourites:
|
||||
favourites.append(filename)
|
||||
save_favourites(favourites)
|
||||
return jsonify({"status": "success"})
|
||||
|
||||
@bp.route("/favourites/remove", methods=["POST"])
|
||||
def remove_favourite():
|
||||
@bp.route("/favourites/toggle", methods=["POST"])
|
||||
def toggle_favourite():
|
||||
data = request.get_json()
|
||||
filename = data.get("filename")
|
||||
if not filename:
|
||||
return jsonify({"status": "error", "message": "Filename missing"}), 400
|
||||
|
||||
favourites = get_favourites()
|
||||
is_favourited = False
|
||||
if filename in favourites:
|
||||
favourites.remove(filename)
|
||||
else:
|
||||
favourites.append(filename)
|
||||
is_favourited = True
|
||||
|
||||
save_favourites(favourites)
|
||||
return jsonify({"status": "success"})
|
||||
return jsonify({"status": "success", "favourited": is_favourited})
|
||||
|
51
static/css/main.css
Normal file
51
static/css/main.css
Normal 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
20
templates/base.html
Normal 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>
|
@ -1,28 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% extends "base.html" %}
|
||||
|
||||
<head>
|
||||
<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;
|
||||
}
|
||||
{% block title %}Create An Image{% endblock %}
|
||||
|
||||
/* ---------- layout ---------- */
|
||||
{% block head %}
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@ -63,7 +50,6 @@
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* ---------- spinner ---------- */
|
||||
#spinner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@ -113,15 +99,15 @@
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
<body>
|
||||
<h1 style="margin-bottom: 20px;">Create An Image</h1>
|
||||
{% block content %}
|
||||
<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="sendPrompt()">Send Prompt</button>
|
||||
@ -130,13 +116,11 @@
|
||||
|
||||
<select id="model-select">
|
||||
<option value="" selected>Random</option>
|
||||
<!-- Group: FLUX -->
|
||||
<optgroup label="FLUX">
|
||||
{% for m in models if 'flux' in m|lower %}
|
||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
<!-- Group: SDXL -->
|
||||
<optgroup label="SDXL">
|
||||
{% for m in models if 'flux' not in m|lower %}
|
||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||
@ -153,16 +137,15 @@
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- waiting overlay -->
|
||||
<div id="spinner-overlay">
|
||||
<div id="spinner-overlay">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const overlay = document.getElementById('spinner-overlay');
|
||||
|
||||
function showSpinner() { overlay.style.visibility = 'visible'; }
|
||||
@ -190,15 +173,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// wrapper for Random Prompt button so it also sends the model
|
||||
function randomPrompt() {
|
||||
showSpinner();
|
||||
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();
|
||||
formData.append('model', model);
|
||||
formData.append('topic', topic); // include topic in request
|
||||
formData.append('topic', topic);
|
||||
|
||||
fetch('/create', {
|
||||
method: 'POST',
|
||||
@ -213,7 +195,5 @@
|
||||
alert("Error requesting random prompt: " + error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,24 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% extends "base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Image Archive</title>
|
||||
{% block title %}Image Archive{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
@ -102,18 +87,14 @@
|
||||
text-align: left;
|
||||
margin-top: 20px;
|
||||
max-height: 25vh;
|
||||
/* NEW: restrict height */
|
||||
overflow-y: auto;
|
||||
/* NEW: allow vertical scroll */
|
||||
}
|
||||
|
||||
/* Back button fixed top right */
|
||||
.home-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 500;
|
||||
/* lower than lightbox (999) */
|
||||
}
|
||||
|
||||
.favourites-button {
|
||||
@ -132,24 +113,6 @@
|
||||
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) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
@ -181,9 +144,7 @@
|
||||
max-width: 90%;
|
||||
padding: 8px 16px;
|
||||
max-height: 20vh;
|
||||
/* smaller height for mobile */
|
||||
overflow-y: auto;
|
||||
/* keep scroll on mobile too */
|
||||
}
|
||||
|
||||
.button-link {
|
||||
@ -192,19 +153,17 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
{% endblock %}
|
||||
|
||||
<body>
|
||||
{% block content %}
|
||||
<a href="/" class="button-link home-button">Home</a>
|
||||
<button class="button-link favourites-button" id="favourites-button" onclick="toggleFavouritesView()">Show Favourites</button>
|
||||
|
||||
<h1 id="page-title">Image Archive</h1>
|
||||
|
||||
<!-- Empty gallery container; images will be loaded incrementally -->
|
||||
<div class="gallery" id="gallery"></div>
|
||||
|
||||
<!-- Lightbox -->
|
||||
<div class="lightbox" id="lightbox">
|
||||
<div class="lightbox" id="lightbox" tabindex="-1" onkeyup="handleLightboxKeys(event)">
|
||||
<span class="close" onclick="closeLightbox()">×</span>
|
||||
<span class="favourite-heart" id="favourite-heart" onclick="toggleFavourite()">♡</span>
|
||||
<span class="arrow left" onclick="prevImage()">❮</span>
|
||||
@ -212,8 +171,9 @@
|
||||
<p id="lightbox-prompt"></p>
|
||||
<span class="arrow right" onclick="nextImage()">❯</span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Pass image filenames from Flask to JS -->
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let allImages = JSON.parse(`[
|
||||
{% for image in images %}
|
||||
@ -224,10 +184,10 @@
|
||||
|
||||
<script>
|
||||
const gallery = document.getElementById('gallery');
|
||||
const batchSize = 9; // images to load per batch
|
||||
const batchSize = 9;
|
||||
let loadedCount = 0;
|
||||
let currentIndex = 0;
|
||||
const detailsCache = {}; // Cache for image details
|
||||
const detailsCache = {};
|
||||
let showingFavourites = false;
|
||||
let filteredImages = allImages;
|
||||
|
||||
@ -276,19 +236,16 @@
|
||||
renderGallery();
|
||||
}
|
||||
|
||||
// Load initial batch
|
||||
renderGallery();
|
||||
|
||||
// Load more images when scrolling near bottom
|
||||
window.addEventListener('scroll', () => {
|
||||
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)) {
|
||||
loadNextBatch();
|
||||
}
|
||||
});
|
||||
|
||||
// Get current images in gallery for lightbox navigation
|
||||
function getGalleryImages() {
|
||||
return Array.from(gallery.querySelectorAll('img'));
|
||||
}
|
||||
@ -297,12 +254,14 @@
|
||||
const images = getGalleryImages();
|
||||
currentIndex = images.indexOf(imgEl);
|
||||
showImageAndLoadDetails(currentIndex);
|
||||
document.getElementById("lightbox").style.display = "flex";
|
||||
const lightbox = document.getElementById("lightbox");
|
||||
lightbox.style.display = "flex";
|
||||
lightbox.focus();
|
||||
}
|
||||
|
||||
function updateFavouriteHeart(isFavourited) {
|
||||
const heart = document.getElementById('favourite-heart');
|
||||
heart.innerHTML = isFavourited ? '♥' : '♡'; // solid vs outline heart
|
||||
heart.innerHTML = isFavourited ? '♥' : '♡';
|
||||
heart.style.color = isFavourited ? 'red' : 'white';
|
||||
}
|
||||
|
||||
@ -310,10 +269,8 @@
|
||||
const images = getGalleryImages();
|
||||
const imgEl = images[currentIndex];
|
||||
const filename = imgEl.dataset.filename;
|
||||
let isFavourited = imgEl.dataset.favourited === 'true';
|
||||
|
||||
const url = isFavourited ? '/favourites/remove' : '/favourites/add';
|
||||
fetch(url, {
|
||||
fetch('/favourites/toggle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename: filename })
|
||||
@ -321,14 +278,19 @@
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
isFavourited = !isFavourited;
|
||||
const isFavourited = data.favourited;
|
||||
imgEl.dataset.favourited = isFavourited;
|
||||
updateFavouriteHeart(isFavourited);
|
||||
// Update the main image list
|
||||
|
||||
const originalImage = allImages.find(img => img.filename === filename);
|
||||
if (originalImage) {
|
||||
originalImage.favourited = isFavourited;
|
||||
}
|
||||
|
||||
if (showingFavourites) {
|
||||
filteredImages = allImages.filter(img => img.favourited);
|
||||
renderGallery();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -356,7 +318,7 @@
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
detailsCache[filename] = data; // Cache the data
|
||||
detailsCache[filename] = data;
|
||||
document.getElementById("lightbox-prompt").textContent =
|
||||
`Model:${data.model} - Created:${data.date}\n\n${data.prompt}`;
|
||||
})
|
||||
@ -371,7 +333,6 @@
|
||||
const imagesToLoad = showingFavourites ? filteredImages : allImages;
|
||||
if (currentIndex + 1 >= images.length && loadedCount < imagesToLoad.length) {
|
||||
loadNextBatch();
|
||||
// Wait briefly to ensure DOM updates
|
||||
setTimeout(() => {
|
||||
const updatedImages = getGalleryImages();
|
||||
if (currentIndex + 1 < updatedImages.length) {
|
||||
@ -391,11 +352,9 @@
|
||||
showImageAndLoadDetails(currentIndex);
|
||||
}
|
||||
|
||||
|
||||
function closeLightbox() {
|
||||
document.getElementById("lightbox").style.display = "none";
|
||||
if (showingFavourites) {
|
||||
// Refresh the gallery if a favourite was removed
|
||||
const currentImage = getGalleryImages()[currentIndex];
|
||||
const wasFavourited = currentImage.dataset.favourited === 'true';
|
||||
const originalImage = allImages.find(img => img.filename === currentImage.dataset.filename);
|
||||
@ -404,7 +363,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
function handleLightboxKeys(e) {
|
||||
if (e.key === 'f') {
|
||||
toggleFavourite();
|
||||
} else if (e.key === 'Escape') {
|
||||
closeLightbox();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
prevImage();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
nextImage();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,25 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Image Created</title>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Image Created{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
@ -50,13 +40,13 @@
|
||||
background: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="message">Image will be made with <i>{{ model }}</i> using prompt:</div>
|
||||
<div class="prompt-text">
|
||||
{{ prompt }}
|
||||
</div>
|
||||
<button onclick="location.href='/'">Home</button>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,32 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% extends "base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI Image of the Day</title>
|
||||
{% block title %}AI Image of the Day{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
position: relative;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.image-container {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
@ -54,9 +42,7 @@
|
||||
max-width: 80vw;
|
||||
text-align: left;
|
||||
max-height: 30vh;
|
||||
/* NEW: limit height */
|
||||
overflow-y: auto;
|
||||
/* NEW: allow scrolling */
|
||||
}
|
||||
|
||||
.button-group {
|
||||
@ -66,46 +52,6 @@
|
||||
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) {
|
||||
.image-container {
|
||||
max-width: 100vw;
|
||||
@ -134,14 +80,9 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, {{ reload_interval }}); // Refresh every X ms
|
||||
</script>
|
||||
</head>
|
||||
{% endblock %}
|
||||
|
||||
<body>
|
||||
{% block content %}
|
||||
{% if image %}
|
||||
<div class="image-container">
|
||||
<img src="{{ url_for('image_routes.serve_image', filename=image) }}" alt="Latest Image" />
|
||||
@ -156,11 +97,12 @@
|
||||
{% else %}
|
||||
<p>No images found</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- Version number at bottom right -->
|
||||
<div class="version">
|
||||
<a href="{{ url_for('settings_route.config_editor') }}">v{{ version }}</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{% block scripts %}
|
||||
<script>
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, {{ reload_interval }}); // Refresh every X ms
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,25 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login</title>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
@ -59,8 +49,9 @@
|
||||
background: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="message">Please enter the password to continue:</div>
|
||||
<form method="post">
|
||||
<div class="prompt-text">
|
||||
@ -69,5 +60,4 @@
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
@ -1,21 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{% extends "base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Config Editor</title>
|
||||
{% block title %}Config Editor{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
display: flex;
|
||||
@ -74,21 +63,11 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
background: #333;
|
||||
height: 40px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: background 0.3s;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-link:hover {
|
||||
background: #555;
|
||||
.back-button-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@ -102,18 +81,10 @@
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.back-button-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="box">
|
||||
<h2>Topics</h2>
|
||||
<form method="post">
|
||||
@ -186,7 +157,4 @@
|
||||
<div class="back-button-wrapper">
|
||||
<a href="/" class="button-link">Back to Home</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user