should be done
|
@ -160,3 +160,6 @@ cython_debug/
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
db/
|
||||||
|
images/
|
||||||
|
cache/
|
||||||
|
|
Before Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,107 @@
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
from duckduckgo_search import DDGS
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def set_login_info(username, password):
|
||||||
|
salt = secrets.token_hex(16)
|
||||||
|
password_hash = generate_password_hash(password + salt)
|
||||||
|
key = secrets.token_hex(32)
|
||||||
|
|
||||||
|
login_info = {
|
||||||
|
"login_info": {
|
||||||
|
"username": username,
|
||||||
|
"password_hash": password_hash,
|
||||||
|
"salt": salt
|
||||||
|
},
|
||||||
|
"key": key
|
||||||
|
}
|
||||||
|
|
||||||
|
os.makedirs('db', exist_ok=True)
|
||||||
|
with open('db/keys.json', 'w') as f:
|
||||||
|
json.dump(login_info, f, indent=4)
|
||||||
|
|
||||||
|
def get_next_batch_number():
|
||||||
|
existing_files = os.listdir('images')
|
||||||
|
batch_numbers = [int(f.split('_')[0]) for f in existing_files if '_' in f]
|
||||||
|
return max(batch_numbers, default=0) + 1
|
||||||
|
|
||||||
|
def download_images(query, total_images=100, batch_size=10):
|
||||||
|
os.makedirs('images', exist_ok=True)
|
||||||
|
ddgs = DDGS()
|
||||||
|
results = ddgs.images(query, max_results=total_images)
|
||||||
|
batch_number = get_next_batch_number()
|
||||||
|
downloaded_images = 0
|
||||||
|
valid_extensions = {'jpg', 'jpeg', 'png', 'gif'}
|
||||||
|
|
||||||
|
for i, result in enumerate(results):
|
||||||
|
image_url = result['image']
|
||||||
|
try:
|
||||||
|
response = requests.get(image_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
ext = image_url.split('.')[-1].lower()
|
||||||
|
if ext not in valid_extensions:
|
||||||
|
ext = 'jpg'
|
||||||
|
filename = f'{batch_number:04d}_{(downloaded_images % batch_size) + 1:05d}.{ext}'
|
||||||
|
with open(f'images/{filename}', 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
downloaded_images += 1
|
||||||
|
else:
|
||||||
|
print(f"Failed to download image from {image_url}. Status code: {response.status_code}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"An error occurred while downloading image from {image_url}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
if downloaded_images > 0 and downloaded_images % batch_size == 0:
|
||||||
|
batch_number += 1
|
||||||
|
|
||||||
|
print(f"Downloaded {downloaded_images} images for query '{query}'.")
|
||||||
|
|
||||||
|
def clear_cache():
|
||||||
|
cache_folder = 'cache'
|
||||||
|
if os.path.exists(cache_folder):
|
||||||
|
shutil.rmtree(cache_folder)
|
||||||
|
print(f"Cache directory '{cache_folder}' removed successfully.")
|
||||||
|
else:
|
||||||
|
print(f"Cache directory '{cache_folder}' does not exist.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Set username and password for the application and optionally download images using DuckDuckGo.',
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add an argument to enter user setting mode
|
||||||
|
parser.add_argument('--user-setting', action='store_true', help='Enter user setting mode to set username and password.')
|
||||||
|
|
||||||
|
# Create a userset argument group
|
||||||
|
userset_group = parser.add_argument_group('userset', 'Set username and password')
|
||||||
|
userset_group.add_argument('-u', '--username', type=str, help='The username to set.')
|
||||||
|
userset_group.add_argument('-p', '--password', type=str, help='The password to set.')
|
||||||
|
|
||||||
|
parser.add_argument('-q', '--query', type=str, help='The search query to download images for testing.')
|
||||||
|
parser.add_argument('-t', '--total_images', type=int, default=10, help='The total number of images to download.')
|
||||||
|
parser.add_argument('-b', '--batch_size', type=int, default=5, help='The number of images to download in each batch.')
|
||||||
|
parser.add_argument('-c', '--clear-cache', action='store_true', help='Remove the cache directory.')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.user_setting:
|
||||||
|
if not args.username or not args.password:
|
||||||
|
parser.error('--user-setting requires --username and --password.')
|
||||||
|
set_login_info(args.username, args.password)
|
||||||
|
print(f"Username and password set successfully. Key generated and saved in db/keys.json.")
|
||||||
|
elif args.clear_cache:
|
||||||
|
clear_cache()
|
||||||
|
elif args.query:
|
||||||
|
download_images(args.query, args.total_images, args.batch_size)
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
138
app.py
|
@ -1,31 +1,66 @@
|
||||||
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, session
|
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, session, jsonify
|
||||||
from flask_socketio import SocketIO, emit
|
from flask_socketio import SocketIO, emit, disconnect
|
||||||
import os
|
import os
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
import json
|
||||||
from PIL import Image
|
from werkzeug.security import check_password_hash
|
||||||
|
from PIL import Image, UnidentifiedImageError
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SECRET_KEY'] = 'your_secret_key' # Replace with a strong secret key
|
|
||||||
|
# Load configuration from keys.json
|
||||||
|
with open('db/keys.json', 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
app.config['SECRET_KEY'] = config['key']
|
||||||
socketio = SocketIO(app)
|
socketio = SocketIO(app)
|
||||||
|
|
||||||
app.config['UPLOAD_FOLDER'] = 'images' # Folder to store images
|
app.config['UPLOAD_FOLDER'] = 'images' # Folder to store images
|
||||||
app.config['CACHE_FOLDER'] = 'cache' # Folder to store cached resized images
|
app.config['CACHE_FOLDER'] = 'cache' # Folder to store cached resized images
|
||||||
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}
|
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
|
||||||
# Fixed username and password
|
USERNAME = config['login_info']['username']
|
||||||
USERNAME = 'user'
|
PASSWORD_HASH = config['login_info']['password_hash']
|
||||||
PASSWORD = generate_password_hash('password') # Hashed password
|
SALT = config['login_info']['salt']
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
return '.' in filename and \
|
return '.' in filename and \
|
||||||
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
||||||
|
|
||||||
|
def is_image_valid(image_path):
|
||||||
|
try:
|
||||||
|
with Image.open(image_path) as img:
|
||||||
|
img.verify() # Verify that it is, in fact, an image
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def resize_image(image_path, cache_path, size=(800, 450)):
|
def resize_image(image_path, cache_path, size=(800, 450)):
|
||||||
with Image.open(image_path) as img:
|
with Image.open(image_path) as img:
|
||||||
img.thumbnail(size)
|
img.thumbnail(size)
|
||||||
img.save(cache_path)
|
if img.mode in ("RGBA", "P"): # Convert to RGB if necessary
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img.save(cache_path, format="JPEG")
|
||||||
|
|
||||||
|
def create_cache():
|
||||||
|
images = os.listdir(app.config['UPLOAD_FOLDER'])
|
||||||
|
cached_images = []
|
||||||
|
|
||||||
|
if not os.path.exists(app.config['CACHE_FOLDER']):
|
||||||
|
os.makedirs(app.config['CACHE_FOLDER'])
|
||||||
|
|
||||||
|
for image in images:
|
||||||
|
image_path = os.path.join(app.config['UPLOAD_FOLDER'], image)
|
||||||
|
cache_path = os.path.join(app.config['CACHE_FOLDER'], image)
|
||||||
|
|
||||||
|
if not os.path.exists(cache_path) and is_image_valid(image_path):
|
||||||
|
resize_image(image_path, cache_path)
|
||||||
|
|
||||||
|
if is_image_valid(image_path):
|
||||||
|
cached_images.append(image)
|
||||||
|
|
||||||
|
return cached_images
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
|
@ -36,7 +71,7 @@ def login():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
password = request.form['password']
|
password = request.form['password']
|
||||||
if username == USERNAME and check_password_hash(PASSWORD, password):
|
if username == USERNAME and check_password_hash(PASSWORD_HASH, password + SALT):
|
||||||
session['logged_in'] = True
|
session['logged_in'] = True
|
||||||
return redirect(url_for('gallery'))
|
return redirect(url_for('gallery'))
|
||||||
else:
|
else:
|
||||||
|
@ -54,43 +89,92 @@ def gallery():
|
||||||
if not session.get('logged_in'):
|
if not session.get('logged_in'):
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
images = os.listdir(app.config['UPLOAD_FOLDER'])
|
cached_images = create_cache()
|
||||||
cached_images = []
|
batches = []
|
||||||
|
current_batch = []
|
||||||
|
current_batch_number = None
|
||||||
|
unstructured_images = []
|
||||||
|
|
||||||
if not os.path.exists(app.config['CACHE_FOLDER']):
|
for image in cached_images:
|
||||||
os.makedirs(app.config['CACHE_FOLDER'])
|
parts = image.split('_')
|
||||||
|
if len(parts) == 2 and parts[1].split('.')[0].isdigit():
|
||||||
|
batch_number = int(parts[0])
|
||||||
|
if current_batch_number is None:
|
||||||
|
current_batch_number = batch_number
|
||||||
|
if batch_number != current_batch_number:
|
||||||
|
batches.append(current_batch)
|
||||||
|
current_batch = []
|
||||||
|
current_batch_number = batch_number
|
||||||
|
current_batch.append(image)
|
||||||
|
else:
|
||||||
|
unstructured_images.append(image)
|
||||||
|
|
||||||
for image in images:
|
if current_batch:
|
||||||
image_path = os.path.join(app.config['UPLOAD_FOLDER'], image)
|
batches.append(current_batch)
|
||||||
cache_path = os.path.join(app.config['CACHE_FOLDER'], image)
|
if unstructured_images:
|
||||||
|
batches.append(unstructured_images)
|
||||||
|
|
||||||
if not os.path.exists(cache_path):
|
return render_template('gallery.html', batches=batches)
|
||||||
resize_image(image_path, cache_path)
|
|
||||||
|
|
||||||
cached_images.append(image)
|
|
||||||
|
|
||||||
return render_template('gallery.html', images=cached_images)
|
|
||||||
|
|
||||||
@app.route('/images/<filename>')
|
@app.route('/images/<filename>')
|
||||||
def uploaded_file(filename):
|
def uploaded_file(filename):
|
||||||
return send_from_directory(app.config['CACHE_FOLDER'], filename)
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
if is_image_valid(image_path):
|
||||||
|
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
else:
|
||||||
|
return "Invalid image", 404
|
||||||
|
|
||||||
|
@app.route('/cached/<filename>')
|
||||||
|
def cached_file(filename):
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
image_path = os.path.join(app.config['CACHE_FOLDER'], filename)
|
||||||
|
if is_image_valid(image_path):
|
||||||
|
return send_from_directory(app.config['CACHE_FOLDER'], filename)
|
||||||
|
else:
|
||||||
|
return "Invalid image", 404
|
||||||
|
|
||||||
|
@app.route('/api/images')
|
||||||
|
def api_images():
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return jsonify([])
|
||||||
|
|
||||||
|
images = sorted(
|
||||||
|
os.listdir(app.config['UPLOAD_FOLDER']),
|
||||||
|
key=lambda x: os.path.getctime(os.path.join(app.config['UPLOAD_FOLDER'], x)),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
valid_images = [img for img in images if is_image_valid(os.path.join(app.config['UPLOAD_FOLDER'], img))]
|
||||||
|
return jsonify(valid_images)
|
||||||
|
|
||||||
def emit_gallery_update():
|
def emit_gallery_update():
|
||||||
socketio.emit('update_gallery')
|
socketio.emit('update_gallery')
|
||||||
|
|
||||||
class Watcher(FileSystemEventHandler):
|
class Watcher(FileSystemEventHandler):
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
if not event.is_directory:
|
if not event.is_directory and event.src_path.startswith(app.config['UPLOAD_FOLDER']):
|
||||||
|
create_cache()
|
||||||
emit_gallery_update()
|
emit_gallery_update()
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
if not event.is_directory:
|
if not event.is_directory and event.src_path.startswith(app.config['UPLOAD_FOLDER']):
|
||||||
|
create_cache()
|
||||||
emit_gallery_update()
|
emit_gallery_update()
|
||||||
|
|
||||||
def on_deleted(self, event):
|
def on_deleted(self, event):
|
||||||
if not event.is_directory:
|
if not event.is_directory and event.src_path.startswith(app.config['UPLOAD_FOLDER']):
|
||||||
|
create_cache()
|
||||||
emit_gallery_update()
|
emit_gallery_update()
|
||||||
|
|
||||||
|
@socketio.on('connect')
|
||||||
|
def handle_connect():
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
disconnect()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
observer = Observer()
|
observer = Observer()
|
||||||
observer.schedule(Watcher(), path=app.config['UPLOAD_FOLDER'], recursive=False)
|
observer.schedule(Watcher(), path=app.config['UPLOAD_FOLDER'], recursive=False)
|
||||||
|
|
Before Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 520 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 13 MiB |
|
@ -1,2 +0,0 @@
|
||||||
Flask
|
|
||||||
Werkzeug
|
|
|
@ -1,6 +1,7 @@
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%);
|
background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%);
|
||||||
|
background-attachment: fixed;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -15,71 +16,62 @@ h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.gallery-container {
|
.gallery-container {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 10px;
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
.batch-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid #ddd;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.responsive-img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: opacity 0.5s ease-in-out, transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.responsive-img.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.responsive-img:not(.loaded) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.responsive-img:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.added {
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
.removed {
|
||||||
|
animation: fadeOut 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Media queries for responsive padding */
|
/* Media queries for responsive padding */
|
||||||
@media (max-width: 1199px) {
|
@media (max-width: 1199px) {
|
||||||
.gallery-container {
|
.gallery-container {
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.gallery-container {
|
|
||||||
padding-left: 30px;
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.gallery-container {
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: transform 0.2s;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
object-fit: cover;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease-in-out, transform 0.2s;
|
|
||||||
}
|
|
||||||
.responsive-img.loaded {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.responsive-img:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Media queries for responsive columns */
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.gallery-container {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) and (max-width: 1199px) {
|
|
||||||
.gallery-container {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) and (max-width: 991px) {
|
|
||||||
.gallery-container {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.gallery-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -5,6 +5,8 @@ body {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%);
|
||||||
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
form h1 {
|
form h1 {
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%);
|
background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%);
|
||||||
|
background-attachment: fixed;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -2,20 +2,140 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Image Gallery</title>
|
<title>Image Gallery</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='gallery_styles.css') }}">
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='gallery_styles.css') }}">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Image Gallery</h1>
|
<h1>Image Gallery</h1>
|
||||||
<div class="gallery-container" id="gallery-container">
|
<div class="gallery-container" id="gallery-container">
|
||||||
{% for image in images %}
|
{% for batch in batches %}
|
||||||
<img class="responsive-img" src="{{ url_for('uploaded_file', filename=image) }}" alt="{{ image }}">
|
{% set batch_number = batch[0].split('_')[0] if batch and '_' in batch[0] else 'unstructured' %}
|
||||||
|
<div class="batch-container batch-{{ batch_number }}">
|
||||||
|
{% for image in batch %}
|
||||||
|
{% if '_' in image and '.' in image.split('_')[1] %}
|
||||||
|
{% set index = image.split('_')[1].split('.')[0] %}
|
||||||
|
{% else %}
|
||||||
|
{% set index = 'unstructured' %}
|
||||||
|
{% endif %}
|
||||||
|
<a href="#" data-original="{{ url_for('uploaded_file', filename=image) }}" target="_blank">
|
||||||
|
<img class="responsive-img index-{{ index }}" src="{{ url_for('cached_file', filename=image) }}" alt="{{ image }}">
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if not loop.last %}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port);
|
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port);
|
||||||
|
|
||||||
|
function parseImageName(image) {
|
||||||
|
var parts = image.split('_');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
var batchNumber = parseInt(parts[0], 10);
|
||||||
|
var index = parseInt(parts[1].split('.')[0], 10);
|
||||||
|
return { batchNumber: batchNumber, index: index };
|
||||||
|
}
|
||||||
|
return { batchNumber: 0, index: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNonExistentImages(existingImages, images) {
|
||||||
|
var galleryContainer = document.getElementById('gallery-container');
|
||||||
|
existingImages.forEach(function(image) {
|
||||||
|
if (!images.includes(image)) {
|
||||||
|
var imgElement = galleryContainer.querySelector(`img[alt="${image}"]`);
|
||||||
|
if (imgElement) {
|
||||||
|
imgElement.parentElement.classList.add('removed');
|
||||||
|
setTimeout(() => {
|
||||||
|
imgElement.parentElement.remove();
|
||||||
|
}, 500); // Match the duration of the fadeOut animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewImages(existingImages, images) {
|
||||||
|
var galleryContainer = document.getElementById('gallery-container');
|
||||||
|
var currentBatchContainer = null;
|
||||||
|
var currentBatchNumber = null;
|
||||||
|
|
||||||
|
images.forEach(function(image) {
|
||||||
|
if (!existingImages.includes(image)) {
|
||||||
|
var parsedImage = parseImageName(image);
|
||||||
|
var batchNumber = parsedImage.batchNumber;
|
||||||
|
var index = parsedImage.index;
|
||||||
|
|
||||||
|
if (currentBatchNumber !== batchNumber) {
|
||||||
|
currentBatchNumber = batchNumber;
|
||||||
|
currentBatchContainer = document.querySelector(`.batch-${batchNumber}`);
|
||||||
|
if (!currentBatchContainer) {
|
||||||
|
currentBatchContainer = document.createElement('div');
|
||||||
|
currentBatchContainer.className = `batch-container batch-${batchNumber}`;
|
||||||
|
galleryContainer.appendChild(currentBatchContainer);
|
||||||
|
|
||||||
|
if (galleryContainer.children.length > 1) {
|
||||||
|
var divider = document.createElement('hr');
|
||||||
|
galleryContainer.insertBefore(divider, currentBatchContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchor = document.createElement('a');
|
||||||
|
anchor.href = '#';
|
||||||
|
anchor.setAttribute('data-original', `/images/${image}`);
|
||||||
|
anchor.target = '_blank';
|
||||||
|
anchor.classList.add('added');
|
||||||
|
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.className = `responsive-img index-${index}`;
|
||||||
|
img.alt = image;
|
||||||
|
img.src = `/cached/${image}`;
|
||||||
|
|
||||||
|
anchor.appendChild(img);
|
||||||
|
|
||||||
|
// Insert the image in the correct location based on the index
|
||||||
|
var inserted = false;
|
||||||
|
var children = currentBatchContainer.children;
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
var childIndex = parseInt(children[i].querySelector('img').className.split('index-')[1], 10);
|
||||||
|
if (index < childIndex) {
|
||||||
|
currentBatchContainer.insertBefore(anchor, children[i]);
|
||||||
|
inserted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inserted) {
|
||||||
|
currentBatchContainer.appendChild(anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.addEventListener('load', function() {
|
||||||
|
img.classList.add('loaded');
|
||||||
|
});
|
||||||
|
if (img.complete) {
|
||||||
|
img.classList.add('loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var originalUrl = anchor.getAttribute('data-original');
|
||||||
|
window.open(originalUrl, '_blank');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('update_gallery', function() {
|
socket.on('update_gallery', function() {
|
||||||
location.reload();
|
fetch('/api/images')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(images => {
|
||||||
|
var galleryContainer = document.getElementById('gallery-container');
|
||||||
|
var existingImages = Array.from(galleryContainer.getElementsByTagName('img')).map(img => img.alt);
|
||||||
|
|
||||||
|
removeNonExistentImages(existingImages, images);
|
||||||
|
addNewImages(existingImages, images);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
@ -28,6 +148,15 @@
|
||||||
img.classList.add('loaded');
|
img.classList.add('loaded');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var links = document.querySelectorAll('.gallery-container a');
|
||||||
|
links.forEach(function(link) {
|
||||||
|
link.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var originalUrl = link.getAttribute('data-original');
|
||||||
|
window.open(originalUrl, '_blank');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Login</title>
|
<title>Login</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='login_styles.css') }}">
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='login_styles.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|