major styling pass, artist list, and import processing improvements
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# Ignore test data
|
||||||
|
images/
|
||||||
|
import/
|
||||||
|
|
||||||
|
# Ignore Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# Ignore environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Ignore virtual environments
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Ignore Git files
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Ignore DB files
|
||||||
|
app.db
|
||||||
|
|
||||||
|
# Ignore Docker related files
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
+1
-1
@@ -45,7 +45,7 @@ def login():
|
|||||||
user = User.query.filter_by(email=form.email.data).first()
|
user = User.query.filter_by(email=form.email.data).first()
|
||||||
if user and check_password_hash(user.password_hash, form.password.data):
|
if user and check_password_hash(user.password_hash, form.password.data):
|
||||||
login_user(user)
|
login_user(user)
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.gallery'))
|
||||||
else:
|
else:
|
||||||
flash('Login failed. Check username/password.', 'danger')
|
flash('Login failed. Check username/password.', 'danger')
|
||||||
return render_template('login.html', form=form)
|
return render_template('login.html', form=form)
|
||||||
|
|||||||
+41
-9
@@ -14,7 +14,18 @@ main = Blueprint('main', __name__)
|
|||||||
|
|
||||||
@main.route('/')
|
@main.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
from random import choice
|
||||||
|
from app.models import ImageRecord
|
||||||
|
|
||||||
|
image = ImageRecord.query.order_by(func.random()).first()
|
||||||
|
background_url = None
|
||||||
|
|
||||||
|
if image:
|
||||||
|
filename = image.filepath.replace('/images/', '')
|
||||||
|
background_url = url_for('main.serve_image', filename=filename)
|
||||||
|
|
||||||
|
return render_template('index.html', background_url=background_url)
|
||||||
|
|
||||||
|
|
||||||
@main.route('/gallery')
|
@main.route('/gallery')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -25,11 +36,12 @@ def gallery():
|
|||||||
).limit(recent_images).all()
|
).limit(recent_images).all()
|
||||||
return render_template('gallery.html', images=images)
|
return render_template('gallery.html', images=images)
|
||||||
|
|
||||||
@main.route('/static/images/<path:filename>')
|
@main.route('/images/<path:filename>')
|
||||||
def serve_image(filename):
|
def serve_image(filename):
|
||||||
return send_from_directory('/images', filename)
|
return send_from_directory('/images', filename)
|
||||||
|
|
||||||
@main.route('/artist/<artist_name>')
|
@main.route('/artist/<artist_name>')
|
||||||
|
@login_required
|
||||||
def artist_gallery(artist_name):
|
def artist_gallery(artist_name):
|
||||||
per_page=49
|
per_page=49
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
@@ -38,6 +50,30 @@ def artist_gallery(artist_name):
|
|||||||
).paginate(page=page, per_page=per_page)
|
).paginate(page=page, per_page=per_page)
|
||||||
return render_template('artist_gallery.html', images=images, artist=artist_name)
|
return render_template('artist_gallery.html', images=images, artist=artist_name)
|
||||||
|
|
||||||
|
@main.route('/artists')
|
||||||
|
@login_required
|
||||||
|
def artist_list():
|
||||||
|
from app.models import ImageRecord
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
# Define how many images to show per artist card
|
||||||
|
images_per_artist = 3 # Change this value to tweak
|
||||||
|
|
||||||
|
# Get all unique artist names
|
||||||
|
artists = db.session.query(ImageRecord.artist).distinct().all()
|
||||||
|
artist_data = []
|
||||||
|
|
||||||
|
for (artist,) in artists:
|
||||||
|
images = (
|
||||||
|
ImageRecord.query
|
||||||
|
.filter_by(artist=artist)
|
||||||
|
.order_by(ImageRecord.taken_at.desc().nullslast(), ImageRecord.imported_at.desc())
|
||||||
|
.limit(images_per_artist)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
artist_data.append({'name': artist, 'images': images})
|
||||||
|
|
||||||
|
return render_template('artist_list.html', artist_data=artist_data, images_per_artist=images_per_artist)
|
||||||
|
|
||||||
@main.route('/settings')
|
@main.route('/settings')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -49,13 +85,9 @@ def settings():
|
|||||||
@main.route('/import-images')
|
@main.route('/import-images')
|
||||||
@login_required
|
@login_required
|
||||||
def trigger_image_import():
|
def trigger_image_import():
|
||||||
if not current_user.is_admin:
|
with open('/import/trigger.flag', 'w') as f:
|
||||||
abort(403)
|
f.write('start')
|
||||||
source_dir = '/import'
|
flash("Image import triggered.")
|
||||||
dest_dir = '/images'
|
|
||||||
|
|
||||||
result = import_images_task(source_dir, dest_dir)
|
|
||||||
flash(f"Image import completed: {result}")
|
|
||||||
return redirect(url_for('main.settings'))
|
return redirect(url_for('main.settings'))
|
||||||
|
|
||||||
@main.route('/reset-db', methods=['POST'])
|
@main.route('/reset-db', methods=['POST'])
|
||||||
|
|||||||
+204
-1
@@ -223,4 +223,207 @@ body {
|
|||||||
transform: none;
|
transform: none;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Artist Styling */
|
||||||
|
.artist-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-card {
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-card h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-preview {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-thumb {
|
||||||
|
flex: 1;
|
||||||
|
height: 300px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-thumb:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-card-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-card-link:hover .artist-card {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover {
|
||||||
|
background-color: #004d99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
background-color: #cc0033;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn:hover {
|
||||||
|
background-color: #990024;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-info {
|
||||||
|
margin-top: 2rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #444;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-hero {
|
||||||
|
position: relative;
|
||||||
|
height: 100vh;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(0px); /* Required to initialize */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(20, 20, 20, 0.5);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-message {
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-message h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-message p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-message .btn {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styling */
|
||||||
|
.form-container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: #ffffffdd;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
padding: 0.6rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-button {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #5a67d8;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-button:hover {
|
||||||
|
background-color: #434190;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer-text {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block title %}{{ artist }}'s Gallery{% endblock %}
|
{% block title %}{{ artist }}'s Gallery{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 style="text-align:center; margin-top: 1rem;">{{ artist }}</h2>
|
<h1 style="text-align:center; margin-top: 1rem;">{{ artist }}</h1>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<!-- Pagination Controls -->
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
@@ -21,23 +21,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gallery Grid -->
|
<!-- Gallery Grid -->
|
||||||
<div class="gallery-grid">
|
<div class="gallery-grid">
|
||||||
{% for image in images %}
|
{% for image in images.items %}
|
||||||
<div class="gallery-item">
|
<div class="gallery-item">
|
||||||
<div class="gallery-thumb img-clickable"
|
<div class="gallery-thumb img-clickable"
|
||||||
data-full="{{ url_for('static', filename=image.filepath) }}"
|
data-full="{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}"
|
||||||
style="background-image: url('{{ url_for('static', filename=image.filepath) }}');"
|
style="background-image: url('{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}');"
|
||||||
data-full="{{ url_for('static', filename=image.filepath) }}"
|
data-filename="{{ image.filename }}">
|
||||||
data-filename="{{ image.filename }}">
|
</div>
|
||||||
</div>
|
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
||||||
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
<span class="image-date">
|
||||||
<span class="image-date">
|
{{ (image.taken_at or image.imported_at) | datetimeformat }}
|
||||||
{{ image.taken_at or image.imported_at | datetimeformat }}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<!-- Pagination Controls -->
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Artists{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 style="text-align:center; margin-top: 1rem;">Artists</h1>
|
||||||
|
|
||||||
|
<div class="artist-grid">
|
||||||
|
{% for artist in artist_data %}
|
||||||
|
<a href="{{ url_for('main.artist_gallery', artist_name=artist.name) }}" class="artist-card-link">
|
||||||
|
<div class="artist-card">
|
||||||
|
<h3>{{ artist.name }}</h3>
|
||||||
|
<div class="artist-preview">
|
||||||
|
{% for image in artist.images %}
|
||||||
|
<div class="artist-thumb"
|
||||||
|
style="background-image: url('{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}');">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -8,18 +8,15 @@
|
|||||||
{% for image in images %}
|
{% for image in images %}
|
||||||
<div class="gallery-item">
|
<div class="gallery-item">
|
||||||
<div class="gallery-thumb img-clickable"
|
<div class="gallery-thumb img-clickable"
|
||||||
data-full="{{ url_for('static', filename=image.filepath) }}"
|
data-full="{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}"
|
||||||
style="background-image: url('{{ url_for('static', filename=image.filepath) }}');"
|
style="background-image: url('{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}');"
|
||||||
data-full="{{ url_for('static', filename=image.filepath) }}"
|
|
||||||
data-filename="{{ image.filename }}">
|
data-filename="{{ image.filename }}">
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
||||||
<span class="image-date">
|
<span class="image-date">
|
||||||
{{ image.taken_at or image.imported_at | datetimeformat }}
|
{{ (image.taken_at or image.imported_at) | datetimeformat }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Welcome{% endblock %}
|
||||||
{% block title %}Home{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Welcome to the Flask Template App!</h1>
|
<div class="index-hero" style="background-image: url('{{ background_url }}');">
|
||||||
<p>This is the index page.</p>
|
<div class="index-overlay">
|
||||||
|
<div class="index-message">
|
||||||
|
<h1>Private Image Repository</h1>
|
||||||
|
<p>This is a private image collection. You must register for access to browse and contribute.</p>
|
||||||
|
<a href="{{ url_for('auth.register') }}" class="btn primary-btn">Register</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Flask Template App</title>
|
<title>ImageRepo - {% block title %}{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
<a class="nav-button" href="{{ url_for('main.artist_list') }}">Artists</a>
|
||||||
<a class="nav-button" href="{{ url_for('auth.logout') }}">Logout</a>
|
<a class="nav-button" href="{{ url_for('auth.logout') }}">Logout</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="nav-button" href="{{ url_for('auth.login') }}">Login</a>
|
<a class="nav-button" href="{{ url_for('auth.login') }}">Login</a>
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Login</h2>
|
<div class="form-container">
|
||||||
<form method="POST">
|
<h2>Login</h2>
|
||||||
|
<form method="POST" class="form-card">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ form.email(size=32, placeholder=form.email.label.text) }}<br>
|
|
||||||
{{ form.password(size=32, placeholder=form.password.label.text) }}<br>
|
<div class="form-group">
|
||||||
{{ form.submit() }}
|
{{ form.email.label(class="form-label") }}
|
||||||
</form>
|
{{ form.email(class="form-input", placeholder=form.email.label.text) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
{{ form.password.label(class="form-label") }}
|
||||||
|
{{ form.password(class="form-input", placeholder=form.password.label.text) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
{{ form.submit(class="form-button") }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="form-footer-text">
|
||||||
|
Don’t have an account? <a href="{{ url_for('auth.register') }}">Register here</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+43
-30
@@ -1,39 +1,52 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Register{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Register</h2>
|
<div class="form-container">
|
||||||
<form method="POST">
|
<h2>Register</h2>
|
||||||
|
<form method="POST" class="form-card">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<p>
|
<div class="form-group">
|
||||||
{{ form.username(size=32, placeholder=form.username.label.text) }}
|
{{ form.username.label(class="form-label") }}
|
||||||
{% for error in form.username.errors %}
|
{{ form.username(class="form-input", placeholder=form.username.label.text) }}
|
||||||
<span class="error">{{ error }}</span>
|
{% for error in form.username.errors %}
|
||||||
{% endfor %}
|
<span class="error">{{ error }}</span>
|
||||||
</p>
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<div class="form-group">
|
||||||
{{ form.email(size=32, placeholder=form.email.label.text) }}
|
{{ form.email.label(class="form-label") }}
|
||||||
{% for error in form.email.errors %}
|
{{ form.email(class="form-input", placeholder=form.email.label.text) }}
|
||||||
<span class="error">{{ error }}</span>
|
{% for error in form.email.errors %}
|
||||||
{% endfor %}
|
<span class="error">{{ error }}</span>
|
||||||
</p>
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<div class="form-group">
|
||||||
{{ form.password(size=32, placeholder=form.password.label.text) }}
|
{{ form.password.label(class="form-label") }}
|
||||||
{% for error in form.password.errors %}
|
{{ form.password(class="form-input", placeholder=form.password.label.text) }}
|
||||||
<span class="error">{{ error }}</span>
|
{% for error in form.password.errors %}
|
||||||
{% endfor %}
|
<span class="error">{{ error }}</span>
|
||||||
</p>
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<div class="form-group">
|
||||||
{{ form.confirm_password(size=32, placeholder=form.confirm_password.label.text) }}
|
{{ form.confirm_password.label(class="form-label") }}
|
||||||
{% for error in form.confirm_password.errors %}
|
{{ form.confirm_password(class="form-input", placeholder=form.confirm_password.label.text) }}
|
||||||
<span class="error">{{ error }}</span>
|
{% for error in form.confirm_password.errors %}
|
||||||
{% endfor %}
|
<span class="error">{{ error }}</span>
|
||||||
</p>
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<div class="form-actions">
|
||||||
{{ form.submit() }}
|
{{ form.submit(class="form-button") }}
|
||||||
</p>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p class="form-footer-text">
|
||||||
|
Already have an account? <a href="{{ url_for('auth.login') }}">Login here</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+15
-15
@@ -1,25 +1,25 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block title %}Settings{% endblock %}
|
{% block title %}Settings{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mt-4">
|
<h1 style="text-align:center; margin-top: 1rem;">Settings</h1>
|
||||||
<h2>Image Management</h2>
|
|
||||||
<p>Use the options below to manage image data.</p>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="settings-container">
|
||||||
<a href="{{ url_for('main.trigger_image_import') }}" class="btn btn-primary">
|
<div class="settings-section">
|
||||||
📥 Import Images
|
<h2>Admin Actions</h2>
|
||||||
</a>
|
<div class="settings-actions">
|
||||||
</div>
|
<form action="{{ url_for('main.trigger_image_import') }}" method="get">
|
||||||
|
<button type="submit" class="btn primary-btn">Trigger Image Import</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="mb-3">
|
<form action="{{ url_for('main.reset_db') }}" method="post" onsubmit="return confirm('Are you sure you want to delete all image records? This action cannot be undone.');">
|
||||||
<form method="POST" action="{{ url_for('main.reset_db') }}"
|
<button type="submit" class="btn danger-btn">Reset Image Database</button>
|
||||||
onsubmit="return confirm('Are you sure you want to delete all image records?');">
|
|
||||||
<button type="submit" class="btn btn-danger">
|
|
||||||
🗑️ Reset Database
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-info">
|
||||||
|
<p>Use these tools to manually trigger an import or reset the image database. Resetting the database does not delete the actual image files.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# app/utils/image_importer.py
|
# app/utils/image_importer.py
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os, shutil, hashlib
|
import os, shutil, hashlib, time
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import exifread
|
import exifread
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@@ -60,6 +60,9 @@ def import_images_task(source_dir, dest_dir):
|
|||||||
db.session.add(record)
|
db.session.add(record)
|
||||||
imported.append(filename)
|
imported.append(filename)
|
||||||
|
|
||||||
|
# Add delay between each image
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return f"Imported {len(imported)} images."
|
return f"Imported {len(imported)} images."
|
||||||
|
|
||||||
|
|||||||
@@ -5,26 +5,26 @@ class Config:
|
|||||||
DB_TYPE = os.environ.get('DB_TYPE', 'sqlite').lower() # 'sqlite' or 'postgresql'
|
DB_TYPE = os.environ.get('DB_TYPE', 'sqlite').lower() # 'sqlite' or 'postgresql'
|
||||||
|
|
||||||
if DB_TYPE == 'postgresql':
|
if DB_TYPE == 'postgresql':
|
||||||
DB_USER = os.environ.get('DB_USER', 'postgres')
|
DB_USER = os.environ.get('DB_USER', 'imagerepo')
|
||||||
DB_PASS = os.environ.get('DB_PASS', 'postgres')
|
DB_PASS = os.environ.get('DB_PASS', 'postgres')
|
||||||
DB_HOST = os.environ.get('DB_HOST', 'localhost')
|
DB_HOST = os.environ.get('DB_HOST', 'postgres')
|
||||||
DB_PORT = os.environ.get('DB_PORT', '5432')
|
DB_PORT = os.environ.get('DB_PORT', '5432')
|
||||||
DB_NAME = os.environ.get('DB_NAME', 'postgres')
|
DB_NAME = os.environ.get('DB_NAME', 'imagerepo')
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = (
|
SQLALCHEMY_DATABASE_URI = (
|
||||||
f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
||||||
)
|
)
|
||||||
|
|
||||||
elif DB_TYPE == 'sqlite':
|
elif DB_TYPE == 'sqlite':
|
||||||
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
DB_DIR = '/db'
|
||||||
|
os.makedirs(DB_DIR, exist_ok=True) # only works if /db is writable
|
||||||
DB_NAME = os.environ.get('DB_NAME', 'app.db')
|
DB_NAME = os.environ.get('DB_NAME', 'app.db')
|
||||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(BASEDIR, DB_NAME)}'
|
SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(DB_DIR, DB_NAME)}'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported DB_TYPE '{DB_TYPE}'. Use 'sqlite' or 'postgresql'.")
|
raise ValueError(f"Unsupported DB_TYPE '{DB_TYPE}'. Use 'sqlite' or 'postgresql'.")
|
||||||
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
|
||||||
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
|
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -4,7 +4,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/app
|
- ./imagerepo/db/:/db # SQLite db storage
|
||||||
- ./import:/import
|
- ./imagerepo/images:/images # where the app will store it's imported images
|
||||||
- ./images:/images
|
- ./import:/import # where it will import images from
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ echo "Applying database migrations..."
|
|||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
|
||||||
# Start periodic image import in background
|
# Start periodic image import in background
|
||||||
echo "Starting 2h import loop"
|
echo "import worker waiting for flag"
|
||||||
python image_import_worker.py &
|
python image_import_worker.py &
|
||||||
|
|
||||||
# Start the Flask app
|
# Start the Flask app
|
||||||
|
|||||||
+28
-10
@@ -1,20 +1,38 @@
|
|||||||
# image_import_loop.py
|
# image_import_loop.py
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
from app import create_app
|
from app import create_app
|
||||||
from app.utils.image_importer import import_images_task
|
from app.utils.image_importer import import_images_task
|
||||||
|
|
||||||
|
TRIGGER_FLAG = "/import/trigger.flag"
|
||||||
|
SOURCE_DIR = "/import"
|
||||||
|
DEST_DIR = "/images"
|
||||||
|
CHECK_INTERVAL = 10 # seconds
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
def looped_import(source_dir='/import', dest_dir='/images', delay_seconds=7200):
|
def monitor_and_import():
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
print("[Image Importer] Starting import task...")
|
print("[Image Importer] Monitoring for trigger flag...")
|
||||||
try:
|
|
||||||
result = import_images_task(source_dir, dest_dir)
|
while True:
|
||||||
print(f"[Image Importer] {result}")
|
if os.path.exists(TRIGGER_FLAG):
|
||||||
except Exception as e:
|
print("[Image Importer] Trigger flag found. Starting import...")
|
||||||
print(f"[Image Importer] Error: {e}")
|
try:
|
||||||
print(f"[Image Importer] Sleeping for {delay_seconds / 3600:.1f} hours...\n")
|
result = import_images_task(SOURCE_DIR, DEST_DIR)
|
||||||
time.sleep(delay_seconds)
|
print(f"[Image Importer] {result}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Image Importer] Error during import: {e}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove(TRIGGER_FLAG)
|
||||||
|
print("[Image Importer] Trigger flag cleared.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Image Importer] Failed to remove trigger flag: {e}")
|
||||||
|
else:
|
||||||
|
print("[Image Importer] No trigger flag. Sleeping...")
|
||||||
|
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
looped_import()
|
monitor_and_import()
|
||||||
|
|||||||
Reference in New Issue
Block a user