mirror of
https://github.com/karl0ss/ai_image_frame_server.git
synced 2025-07-19 19:24:59 +01:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
82f93ae557 | |||
651a8b364d | |||
43f9fcf276 | |||
095a0e66ae | |||
aa8e1d1701 | |||
31b373b34a | |||
8665ab431c | |||
1b528a4277 | |||
6210b5de7d | |||
ab32c8032c |
@ -1,19 +1,24 @@
|
|||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "0.2.11"
|
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 = []
|
||||||
|
@ -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
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
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,219 +1,199 @@
|
|||||||
<!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" />
|
{% block head %}
|
||||||
<title>Create An Image</title>
|
<style>
|
||||||
<style>
|
body {
|
||||||
/* ---------- reset ---------- */
|
display: flex;
|
||||||
* {
|
flex-direction: column;
|
||||||
margin: 0;
|
align-items: center;
|
||||||
padding: 0;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 80vw;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: monospace;
|
||||||
|
resize: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
select:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spinner-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 6px solid #555;
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- layout ---------- */
|
@media (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
display: flex;
|
min-height: 100dvh;
|
||||||
flex-direction: column;
|
height: auto;
|
||||||
align-items: center;
|
justify-content: flex-start;
|
||||||
justify-content: center;
|
padding-top: 40px;
|
||||||
height: 100vh;
|
|
||||||
background: black;
|
|
||||||
color: white;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 80vw;
|
|
||||||
height: 200px;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: monospace;
|
|
||||||
resize: none;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background: #111;
|
|
||||||
color: #eee;
|
|
||||||
border: 1px solid #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
gap: 20px;
|
align-items: stretch;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
select {
|
select {
|
||||||
background: #333;
|
width: 100%;
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover,
|
textarea {
|
||||||
select:hover {
|
height: 150px;
|
||||||
background: #555;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
/* ---------- spinner ---------- */
|
{% block content %}
|
||||||
#spinner-overlay {
|
<h1 style="margin-bottom: 20px;">Create An Image</h1>
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
visibility: hidden;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
<textarea id="prompt-box" placeholder="Enter your custom prompt here..."></textarea>
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border: 6px solid #555;
|
|
||||||
border-top-color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
<div class="button-group">
|
||||||
to {
|
<button onclick="showSpinner(); location.href='/'">Back</button>
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
<button onclick="sendPrompt()">Send Prompt</button>
|
||||||
body {
|
|
||||||
min-height: 100dvh;
|
|
||||||
height: auto;
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
<button onclick="randomPrompt()">Random Prompt</button>
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
<select id="model-select">
|
||||||
select {
|
<option value="" selected>Random</option>
|
||||||
width: 100%;
|
<optgroup label="FLUX">
|
||||||
}
|
{% for m in models if 'flux' in m|lower %}
|
||||||
|
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="SDXL">
|
||||||
|
{% for m in models if 'flux' not in m|lower %}
|
||||||
|
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
|
||||||
textarea {
|
<select id="topic-select">
|
||||||
height: 150px;
|
<option value="">No Topic</option>
|
||||||
}
|
<option value="random">Random</option>
|
||||||
}
|
<optgroup label="Topics">
|
||||||
</style>
|
{% for t in topics %}
|
||||||
</head>
|
<option value="{{ t }}">{{ t }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<body>
|
<div id="spinner-overlay">
|
||||||
<h1 style="margin-bottom: 20px;">Create An Image</h1>
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<textarea id="prompt-box" placeholder="Enter your custom prompt here..."></textarea>
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
const overlay = document.getElementById('spinner-overlay');
|
||||||
|
|
||||||
<div class="button-group">
|
function showSpinner() { overlay.style.visibility = 'visible'; }
|
||||||
<button onclick="showSpinner(); location.href='/'">Back</button>
|
|
||||||
|
|
||||||
<button onclick="sendPrompt()">Send Prompt</button>
|
function sendPrompt() {
|
||||||
|
showSpinner();
|
||||||
|
const prompt = document.getElementById('prompt-box').value;
|
||||||
|
const model = document.getElementById('model-select').value;
|
||||||
|
|
||||||
<button onclick="randomPrompt()">Random Prompt</button>
|
const formData = new URLSearchParams();
|
||||||
|
formData.append('prompt', prompt);
|
||||||
|
formData.append('model', model);
|
||||||
|
|
||||||
<select id="model-select">
|
fetch('/create', {
|
||||||
<option value="" selected>Random</option>
|
method: 'POST',
|
||||||
<!-- Group: FLUX -->
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
<optgroup label="FLUX">
|
body: formData.toString()
|
||||||
{% for m in models if 'flux' in m|lower %}
|
})
|
||||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
.then(response => {
|
||||||
{% endfor %}
|
window.location.href = response.redirected ? response.url : '/create';
|
||||||
</optgroup>
|
|
||||||
<!-- Group: SDXL -->
|
|
||||||
<optgroup label="SDXL">
|
|
||||||
{% for m in models if 'flux' not in m|lower %}
|
|
||||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="topic-select">
|
|
||||||
<option value="">No Topic</option>
|
|
||||||
<option value="random">Random</option>
|
|
||||||
<optgroup label="Topics">
|
|
||||||
{% for t in topics %}
|
|
||||||
<option value="{{ t }}">{{ t }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- waiting overlay -->
|
|
||||||
<div id="spinner-overlay">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const overlay = document.getElementById('spinner-overlay');
|
|
||||||
|
|
||||||
function showSpinner() { overlay.style.visibility = 'visible'; }
|
|
||||||
|
|
||||||
function sendPrompt() {
|
|
||||||
showSpinner();
|
|
||||||
const prompt = document.getElementById('prompt-box').value;
|
|
||||||
const model = document.getElementById('model-select').value;
|
|
||||||
|
|
||||||
const formData = new URLSearchParams();
|
|
||||||
formData.append('prompt', prompt);
|
|
||||||
formData.append('model', model);
|
|
||||||
|
|
||||||
fetch('/create', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
body: formData.toString()
|
|
||||||
})
|
})
|
||||||
.then(response => {
|
.catch(error => {
|
||||||
window.location.href = response.redirected ? response.url : '/create';
|
overlay.style.visibility = 'hidden';
|
||||||
})
|
alert("Error sending prompt: " + error);
|
||||||
.catch(error => {
|
});
|
||||||
overlay.style.visibility = 'hidden';
|
}
|
||||||
alert("Error sending prompt: " + error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
||||||
const topic = document.getElementById('topic-select').value; // this line was missing
|
|
||||||
|
|
||||||
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',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: formData.toString()
|
body: formData.toString()
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
window.location.href = response.redirected ? response.url : '/create';
|
||||||
})
|
})
|
||||||
.then(response => {
|
.catch(error => {
|
||||||
window.location.href = response.redirected ? response.url : '/create';
|
overlay.style.visibility = 'hidden';
|
||||||
})
|
alert("Error requesting random prompt: " + error);
|
||||||
.catch(error => {
|
});
|
||||||
overlay.style.visibility = 'hidden';
|
}
|
||||||
alert("Error requesting random prompt: " + error);
|
</script>
|
||||||
});
|
{% endblock %}
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -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()">×</span>
|
<span class="close" onclick="closeLightbox()">×</span>
|
||||||
<span class="favourite-heart" id="favourite-heart" onclick="toggleFavourite()">♡</span>
|
<span class="favourite-heart" id="favourite-heart" onclick="toggleFavourite()">♡</span>
|
||||||
@ -212,8 +171,9 @@
|
|||||||
<p id="lightbox-prompt"></p>
|
<p id="lightbox-prompt"></p>
|
||||||
<span class="arrow right" onclick="nextImage()">❯</span>
|
<span class="arrow right" onclick="nextImage()">❯</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 ? '♥' : '♡'; // solid vs outline heart
|
heart.innerHTML = isFavourited ? '♥' : '♡';
|
||||||
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>
|
|
@ -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>
|
|
||||||
|
|
||||||
|
@ -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 %}
|
@ -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>
|
|
||||||
|
@ -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>
|
|
Loading…
x
Reference in New Issue
Block a user