mirror of
https://github.com/Derpy-Leggies/OnlyLegs.git
synced 2025-03-04 13:46:27 +00:00
commit
9cf73a79fd
.gitignoreLICENSEREADME.md
gallery
__init__.pyapi.pyauth.pydb.py
poetry.lockpyproject.tomlrun.pyroutes
routing.pysetup.pystatic
images
js
templates
themes/default
animations.sass
components
style.sassui
variables.sassutils
setup
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,9 +1,6 @@
|
|||
# Remove all development files
|
||||
gallery/user/logs/*
|
||||
gallery/user/uploads/*
|
||||
gallery/user/conf.yml
|
||||
gallery/user/conf.json
|
||||
gallery/static/theme/*
|
||||
gallery/static/theme
|
||||
gallery/static/.webassets-cache
|
||||
gallery/static/gen
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Michal
|
||||
Copyright (c) 2023 Michal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
52
README.md
52
README.md
|
@ -22,28 +22,54 @@
|
|||
</div>
|
||||
|
||||
## Features
|
||||
### Currently implemented
|
||||
- Easy uploading and managing of a gallery of images
|
||||
- Multi user support, helping you manage a whole group of photographers
|
||||
- Custom CSS support
|
||||
- [x] Easy uploading and managing of a gallery of images
|
||||
- [x] Multi-user support, helping you manage a whole group of photographers
|
||||
- [x] Image groups, helping you sort your favourite memories
|
||||
- [x] Custom CSS support
|
||||
- [ ] Password locked images/image groups, helping you share photos only to those who you want to
|
||||
- [ ] Logging and automatic login attempt warnings and timeouts
|
||||
- [ ] Searching through tags, file names and users
|
||||
|
||||
### Coming soon tm
|
||||
- Image groups, helping you sort your favorite memories
|
||||
- Password locked images/image groups, helping you share photos only to those who you want to
|
||||
- Logging and automatic login attempt warnings and timeouts
|
||||
- Searching through tags, file names, users (and metadata maybe, no promises)
|
||||
And many more planned things!
|
||||
|
||||
## screenshots
|
||||
|
||||
Homescreen
|
||||
Home-screen
|
||||

|
||||
|
||||
Image view
|
||||

|
||||
|
||||
## Running
|
||||
Currently only for reference
|
||||
|
||||
You first need to install `python poetry`, it's best to follow their getting started guide you can find on the official website.
|
||||
|
||||
Next we need to install the required packages for the gallery to function correctly, make sure you're in the directory of the project when you run this command:
|
||||
|
||||
poetry install
|
||||
poetry run python3 -m flask --app gallery --debug run --host 0.0.0.0
|
||||
poetry run python3 -m gunicorn -w 4 -b 0.0.0.0:5000 'gallery:create_app()'
|
||||
|
||||
By default, the app runs on port 5000, 4 workers on `gunicorn` ready for you to use it. You can find more information on this using the `-h` flag. But to run the gallery, use this command.
|
||||
|
||||
poetry run python3 run.py
|
||||
|
||||
Now follow the provided prompts to fill in the information for the Admin account, and you're ready to go!
|
||||
|
||||
### Common issues
|
||||
#### App failing to create a user config folder
|
||||
|
||||
Try checking if you have `XDG_CONFIG_HOME` setup. If you don't, you can set that with this command:
|
||||
|
||||
export XDG_CONFIG_HOME="$HOME/.config"
|
||||
|
||||
## Final notes
|
||||
|
||||
Thank you to everyone who helped me test the previous and current versions of the gallery, especially critters:
|
||||
|
||||
- Carty
|
||||
- Jeetix
|
||||
- CRT
|
||||
- mrHDash
|
||||
- Verg
|
||||
- FennecBitch
|
||||
|
||||
Enjoy using OnlyLegs!
|
|
@ -1,79 +1,48 @@
|
|||
"""
|
||||
___ _ _
|
||||
/ _ \ _ __ | |_ _| | ___ __ _ ___
|
||||
| | | | '_ \| | | | | | / _ \/ _` / __|
|
||||
| |_| | | | | | |_| | |__| __/ (_| \__ \
|
||||
\___/|_| |_|_|\__, |_____\___|\__, |___/
|
||||
|___/ |___/
|
||||
Created by Fluffy Bean - Version 23.03.04
|
||||
Onlylegs Gallery
|
||||
This is the main app file, it loads all the other files and sets up the app
|
||||
"""
|
||||
|
||||
# Import system modules
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# Flask
|
||||
from flask_compress import Compress
|
||||
from flask_caching import Cache
|
||||
from flask_assets import Environment, Bundle
|
||||
from flask import Flask, render_template
|
||||
|
||||
# Configuration
|
||||
from dotenv import load_dotenv
|
||||
import platformdirs
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
from yaml import FullLoader, safe_load
|
||||
|
||||
from . import theme_manager
|
||||
# Utils
|
||||
from gallery.utils import theme_manager
|
||||
|
||||
|
||||
USER_DIR = platformdirs.user_config_dir('onlylegs')
|
||||
INSTANCE_PATH = os.path.join(USER_DIR, 'instance')
|
||||
|
||||
|
||||
# Check if any of the required files are missing
|
||||
if not os.path.exists(platformdirs.user_config_dir('onlylegs')):
|
||||
from . import setup
|
||||
setup.SetupApp()
|
||||
|
||||
# Get environment variables
|
||||
if os.path.exists(os.path.join(USER_DIR, '.env')):
|
||||
load_dotenv(os.path.join(USER_DIR, '.env'))
|
||||
print("Loaded environment variables")
|
||||
else:
|
||||
print("No environment variables found!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Get config file
|
||||
if os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
|
||||
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as f:
|
||||
conf = yaml.load(f, Loader=yaml.FullLoader)
|
||||
print("Loaded gallery config")
|
||||
else:
|
||||
print("No config file found!")
|
||||
sys.exit(1)
|
||||
|
||||
# Setup the logging config
|
||||
LOGS_PATH = os.path.join(platformdirs.user_config_dir('onlylegs'), 'logs')
|
||||
|
||||
if not os.path.isdir(LOGS_PATH):
|
||||
os.mkdir(LOGS_PATH)
|
||||
|
||||
logging.getLogger('werkzeug').disabled = True
|
||||
logging.basicConfig(
|
||||
filename=os.path.join(LOGS_PATH, 'only.log'),
|
||||
level=logging.INFO,
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',
|
||||
encoding='utf-8')
|
||||
|
||||
|
||||
def create_app(test_config=None):
|
||||
"""
|
||||
Create and configure the main app
|
||||
"""
|
||||
app = Flask(__name__,instance_path=INSTANCE_PATH)
|
||||
app = Flask(__name__, instance_path=os.path.join(USER_DIR, 'instance'))
|
||||
assets = Environment()
|
||||
cache = Cache(config={'CACHE_TYPE': 'SimpleCache', 'CACHE_DEFAULT_TIMEOUT': 300})
|
||||
compress = Compress()
|
||||
|
||||
# Get environment variables
|
||||
load_dotenv(os.path.join(USER_DIR, '.env'))
|
||||
print("Loaded environment variables")
|
||||
|
||||
# Get config file
|
||||
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as file:
|
||||
conf = safe_load(file)
|
||||
print("Loaded gallery config")
|
||||
|
||||
# App configuration
|
||||
app.config.from_mapping(
|
||||
SECRET_KEY=os.environ.get('FLASK_SECRET'),
|
||||
|
@ -94,56 +63,40 @@ def create_app(test_config=None):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# Load theme
|
||||
theme_manager.CompileTheme('default', app.root_path)
|
||||
|
||||
# Bundle JS files
|
||||
js_scripts = Bundle('js/*.js', output='gen/packed.js')
|
||||
assets.register('js_all', js_scripts)
|
||||
|
||||
@app.errorhandler(405)
|
||||
def method_not_allowed(err):
|
||||
error = '405'
|
||||
msg = err.description
|
||||
return render_template('error.html', error=error, msg=msg), 404
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(err):
|
||||
error = '404'
|
||||
msg = err.description
|
||||
return render_template('error.html', error=error, msg=msg), 404
|
||||
|
||||
# Error handlers
|
||||
@app.errorhandler(403)
|
||||
def forbidden(err):
|
||||
error = '403'
|
||||
msg = err.description
|
||||
return render_template('error.html', error=error, msg=msg), 403
|
||||
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(405)
|
||||
@app.errorhandler(410)
|
||||
def gone(err):
|
||||
error = '410'
|
||||
msg = err.description
|
||||
return render_template('error.html', error=error, msg=msg), 410
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_server_error(err):
|
||||
error = '500'
|
||||
def error_page(err):
|
||||
error = err.code
|
||||
msg = err.description
|
||||
return render_template('error.html', error=error, msg=msg), 500
|
||||
return render_template('error.html', error=error, msg=msg), err.code
|
||||
|
||||
# Load login, registration and logout manager
|
||||
from . import auth
|
||||
from gallery import auth
|
||||
app.register_blueprint(auth.blueprint)
|
||||
|
||||
# Load routes for home and images
|
||||
from . import routing
|
||||
# Load the different routes
|
||||
from .routes import api, groups, routing, settings
|
||||
app.register_blueprint(api.blueprint)
|
||||
app.register_blueprint(groups.blueprint)
|
||||
app.register_blueprint(routing.blueprint)
|
||||
app.add_url_rule('/', endpoint='index')
|
||||
|
||||
# Load routes for settings
|
||||
from . import settings
|
||||
app.register_blueprint(settings.blueprint)
|
||||
|
||||
# Load APIs
|
||||
from . import api
|
||||
app.register_blueprint(api.blueprint)
|
||||
# Log to file that the app has started
|
||||
logging.info('Gallery started successfully!')
|
||||
|
||||
# Initialize extensions and return app
|
||||
assets.init_app(app)
|
||||
cache.init_app(app)
|
||||
compress.init_app(app)
|
||||
return app
|
||||
|
|
227
gallery/api.py
227
gallery/api.py
|
@ -1,227 +0,0 @@
|
|||
"""
|
||||
Onlylegs - API endpoints
|
||||
Used intermally by the frontend and possibly by other applications
|
||||
"""
|
||||
from uuid import uuid4
|
||||
import os
|
||||
import io
|
||||
import logging
|
||||
|
||||
from flask import (
|
||||
Blueprint, current_app, send_from_directory, send_file, request, g, abort, flash, jsonify)
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from PIL import Image, ImageOps # ImageFilter
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from gallery.auth import login_required
|
||||
|
||||
from . import db # Import db to create a session
|
||||
from . import metadata as mt
|
||||
|
||||
|
||||
blueprint = Blueprint('api', __name__, url_prefix='/api')
|
||||
db_session = sessionmaker(bind=db.engine)
|
||||
db_session = db_session()
|
||||
|
||||
|
||||
@blueprint.route('/uploads/<file>', methods=['GET'])
|
||||
def uploads(file):
|
||||
"""
|
||||
Returns a file from the uploads folder
|
||||
w and h are the width and height of the image for resizing
|
||||
f is whether to apply filters to the image, such as blurring NSFW images
|
||||
"""
|
||||
# Get args
|
||||
width = request.args.get('w', default=0, type=int) # Width of image
|
||||
height = request.args.get('h', default=0, type=int) # Height of image
|
||||
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters
|
||||
|
||||
# if no args are passed, return the raw file
|
||||
if width == 0 and height == 0 and not filtered:
|
||||
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
secure_filename(file))):
|
||||
abort(404)
|
||||
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file ,as_attachment=True)
|
||||
|
||||
# Of either width or height is 0, set it to the other value to keep aspect ratio
|
||||
if width > 0 and height == 0:
|
||||
height = width
|
||||
elif width == 0 and height > 0:
|
||||
width = height
|
||||
|
||||
set_ext = current_app.config['ALLOWED_EXTENSIONS']
|
||||
buff = io.BytesIO()
|
||||
|
||||
# Open image and set extension
|
||||
try:
|
||||
img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'],file))
|
||||
except FileNotFoundError:
|
||||
logging.error('File not found: %s, possibly broken upload', file)
|
||||
abort(404)
|
||||
except Exception as err:
|
||||
logging.error('Error opening image: %s', err)
|
||||
abort(500)
|
||||
|
||||
img_ext = os.path.splitext(file)[-1].lower().replace('.', '')
|
||||
img_ext = set_ext[img_ext]
|
||||
# Get ICC profile as it alters colours when saving
|
||||
img_icc = img.info.get("icc_profile")
|
||||
|
||||
# Resize image and orientate correctly
|
||||
img.thumbnail((width, height), Image.LANCZOS)
|
||||
img = ImageOps.exif_transpose(img)
|
||||
|
||||
# If has NSFW tag, blur image, etc.
|
||||
if filtered:
|
||||
#img = img.filter(ImageFilter.GaussianBlur(20))
|
||||
pass
|
||||
|
||||
try:
|
||||
img.save(buff, img_ext, icc_profile=img_icc)
|
||||
except OSError:
|
||||
# This usually happens when saving a JPEG with an ICC profile
|
||||
# Convert to RGB and try again
|
||||
img = img.convert('RGB')
|
||||
img.save(buff, img_ext, icc_profile=img_icc)
|
||||
except Exception as err:
|
||||
logging.error('Could not resize image %s, error: %s', file, err)
|
||||
abort(500)
|
||||
|
||||
img.close()
|
||||
|
||||
# Seek to beginning of buffer and return
|
||||
buff.seek(0)
|
||||
return send_file(buff, mimetype='image/' + img_ext)
|
||||
|
||||
|
||||
@blueprint.route('/upload', methods=['POST'])
|
||||
@login_required
|
||||
def upload():
|
||||
"""
|
||||
Uploads an image to the server and saves it to the database
|
||||
"""
|
||||
form_file = request.files['file']
|
||||
form = request.form
|
||||
|
||||
if not form_file:
|
||||
return abort(404)
|
||||
|
||||
img_ext = os.path.splitext(form_file.filename)[-1].replace('.', '').lower()
|
||||
img_name = f"GWAGWA_{str(uuid4())}.{img_ext}"
|
||||
|
||||
if not img_ext in current_app.config['ALLOWED_EXTENSIONS'].keys():
|
||||
logging.info('File extension not allowed: %s', img_ext)
|
||||
abort(403)
|
||||
|
||||
if os.path.isdir(current_app.config['UPLOAD_FOLDER']) is False:
|
||||
os.mkdir(current_app.config['UPLOAD_FOLDER'])
|
||||
|
||||
# Save to database
|
||||
try:
|
||||
db_session.add(db.posts(img_name, form['description'], form['alt'], g.user.id))
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
logging.error('Could not save to database: %s', err)
|
||||
abort(500)
|
||||
|
||||
# Save file
|
||||
try:
|
||||
form_file.save(
|
||||
os.path.join(current_app.config['UPLOAD_FOLDER'], img_name))
|
||||
except Exception as err:
|
||||
logging.error('Could not save file: %s', err)
|
||||
abort(500)
|
||||
|
||||
return 'Gwa Gwa'
|
||||
|
||||
|
||||
@blueprint.route('/remove/<int:img_id>', methods=['POST'])
|
||||
@login_required
|
||||
def remove(img_id):
|
||||
"""
|
||||
Deletes an image from the server and database
|
||||
"""
|
||||
img = db_session.query(db.posts).filter_by(id=img_id).first()
|
||||
|
||||
if img is None:
|
||||
abort(404)
|
||||
if img.author_id != g.user.id:
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
|
||||
except FileNotFoundError:
|
||||
# File was already deleted or doesn't exist
|
||||
logging.warning('File not found: %s, already deleted or never existed', img.file_name)
|
||||
except Exception as err:
|
||||
logging.error('Could not remove file: %s', err)
|
||||
abort(500)
|
||||
|
||||
try:
|
||||
db_session.query(db.posts).filter_by(id=img_id).delete()
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
logging.error('Could not remove from database: %s', err)
|
||||
abort(500)
|
||||
|
||||
logging.info('Removed image (%s) %s', img_id, img.file_name)
|
||||
flash(['Image was all in Le Head!', 1])
|
||||
return 'Gwa Gwa'
|
||||
|
||||
|
||||
@blueprint.route('/metadata/<int:img_id>', methods=['GET'])
|
||||
def metadata(img_id):
|
||||
"""
|
||||
Yoinks metadata from an image
|
||||
"""
|
||||
img = db_session.query(db.posts).filter_by(id=img_id).first()
|
||||
|
||||
if img is None:
|
||||
abort(404)
|
||||
|
||||
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
|
||||
exif = mt.Metadata(img_path).yoink()
|
||||
|
||||
return jsonify(exif)
|
||||
|
||||
|
||||
@blueprint.route('/logfile')
|
||||
@login_required
|
||||
def logfile():
|
||||
"""
|
||||
Gets the log file and returns it as a JSON object
|
||||
"""
|
||||
filename = logging.getLoggerClass().root.handlers[0].baseFilename
|
||||
log_dict = {}
|
||||
i = 0
|
||||
|
||||
with open(filename, encoding='utf-8') as file:
|
||||
for line in file:
|
||||
line = line.split(' : ')
|
||||
|
||||
event = line[0].strip().split(' ')
|
||||
event_data = {
|
||||
'date': event[0],
|
||||
'time': event[1],
|
||||
'severity': event[2],
|
||||
'owner': event[3]
|
||||
}
|
||||
|
||||
message = line[1].strip()
|
||||
try:
|
||||
message_data = {
|
||||
'code': int(message[1:4]),
|
||||
'message': message[5:].strip()
|
||||
}
|
||||
except ValueError:
|
||||
message_data = {'code': 0, 'message': message}
|
||||
except Exception as err:
|
||||
logging.error('Could not parse log file: %s', err)
|
||||
abort(500)
|
||||
|
||||
log_dict[i] = {'event': event_data, 'message': message_data}
|
||||
|
||||
i += 1 # Line number, starts at 0
|
||||
|
||||
return jsonify(log_dict)
|
|
@ -1,10 +1,11 @@
|
|||
"""
|
||||
OnlyLegs - Authentification
|
||||
OnlyLegs - Authentication
|
||||
User registration, login and logout and locking access to pages behind a login
|
||||
"""
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime as dt
|
||||
|
||||
import functools
|
||||
from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify
|
||||
|
@ -28,7 +29,7 @@ def login_required(view):
|
|||
@functools.wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
if g.user is None or session.get('uuid') is None:
|
||||
logging.error('Authentification failed')
|
||||
logging.error('Authentication failed')
|
||||
session.clear()
|
||||
return redirect(url_for('gallery.index'))
|
||||
|
||||
|
@ -49,14 +50,14 @@ def load_logged_in_user():
|
|||
g.user = None
|
||||
session.clear()
|
||||
else:
|
||||
is_alive = db_session.query(db.sessions).filter_by(session_uuid=user_uuid).first()
|
||||
is_alive = db_session.query(db.Sessions).filter_by(session_uuid=user_uuid).first()
|
||||
|
||||
if is_alive is None:
|
||||
logging.info('Session expired')
|
||||
flash(['Session expired!', '3'])
|
||||
session.clear()
|
||||
else:
|
||||
g.user = db_session.query(db.users).filter_by(id=user_id).first()
|
||||
g.user = db_session.query(db.Users).filter_by(id=user_id).first()
|
||||
|
||||
|
||||
@blueprint.route('/register', methods=['POST'])
|
||||
|
@ -74,7 +75,6 @@ def register():
|
|||
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
|
||||
username_regex = re.compile(r'\b[A-Za-z0-9._%+-]+\b')
|
||||
|
||||
|
||||
if not username or not username_regex.match(username):
|
||||
error.append('Username is invalid!')
|
||||
|
||||
|
@ -94,9 +94,12 @@ def register():
|
|||
if error:
|
||||
return jsonify(error)
|
||||
|
||||
|
||||
try:
|
||||
db_session.add(db.users(username, email, generate_password_hash(password)))
|
||||
register_user = db.Users(username=username,
|
||||
email=email,
|
||||
password=generate_password_hash(password),
|
||||
created_at=dt.utcnow())
|
||||
db_session.add(register_user)
|
||||
db_session.commit()
|
||||
except exc.IntegrityError:
|
||||
return f'User {username} is already registered!'
|
||||
|
@ -116,10 +119,9 @@ def login():
|
|||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
user = db_session.query(db.users).filter_by(username=username).first()
|
||||
user = db_session.query(db.Users).filter_by(username=username).first()
|
||||
error = []
|
||||
|
||||
|
||||
if user is None:
|
||||
logging.error('User %s does not exist. Login attempt from %s',
|
||||
username, request.remote_addr)
|
||||
|
@ -132,17 +134,19 @@ def login():
|
|||
if error:
|
||||
abort(403)
|
||||
|
||||
|
||||
try:
|
||||
session.clear()
|
||||
session['user_id'] = user.id
|
||||
session['uuid'] = str(uuid.uuid4())
|
||||
|
||||
db_session.add(db.sessions(user.id,
|
||||
session.get('uuid'),
|
||||
request.remote_addr,
|
||||
request.user_agent.string,
|
||||
1))
|
||||
session_query = db.Sessions(user_id=user.id,
|
||||
session_uuid=session.get('uuid'),
|
||||
ip_address=request.remote_addr,
|
||||
user_agent=request.user_agent.string,
|
||||
active=True,
|
||||
created_at=dt.utcnow())
|
||||
|
||||
db_session.add(session_query)
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
logging.error('User %s could not be logged in: %s', username, err)
|
||||
|
@ -160,4 +164,4 @@ def logout():
|
|||
"""
|
||||
logging.info('User (%s) %s logged out', session.get('user_id'), g.user.username)
|
||||
session.clear()
|
||||
return redirect(url_for('index'))
|
||||
return redirect(url_for('gallery.index'))
|
||||
|
|
114
gallery/db.py
114
gallery/db.py
|
@ -1,21 +1,25 @@
|
|||
"""
|
||||
OnlyLegs - Database
|
||||
Database models and functions for SQLAlchemy
|
||||
OnlyLegs - Database models and functions for SQLAlchemy
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
import platformdirs
|
||||
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy import (
|
||||
create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey, PickleType)
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
|
||||
|
||||
path_to_db = os.path.join(platformdirs.user_config_dir('onlylegs'), 'gallery.sqlite')
|
||||
engine = create_engine(f'sqlite:///{path_to_db}', echo=False)
|
||||
USER_DIR = platformdirs.user_config_dir('onlylegs')
|
||||
DB_PATH = os.path.join(USER_DIR, 'gallery.sqlite')
|
||||
|
||||
|
||||
# engine = create_engine('postgresql://username:password@host:port/database_name', echo=False)
|
||||
# engine = create_engine('mysql://username:password@host:port/database_name', echo=False)
|
||||
engine = create_engine(f'sqlite:///{DB_PATH}', echo=False)
|
||||
base = declarative_base()
|
||||
|
||||
|
||||
class users (base): # pylint: disable=too-few-public-methods, C0103
|
||||
class Users (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
User table
|
||||
Joins with post, groups, session and log
|
||||
|
@ -28,19 +32,13 @@ class users (base): # pylint: disable=too-few-public-methods, C0103
|
|||
password = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
|
||||
posts = relationship('posts')
|
||||
groups = relationship('groups')
|
||||
session = relationship('sessions')
|
||||
log = relationship('logs')
|
||||
|
||||
def __init__(self, username, email, password):
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.created_at = datetime.now()
|
||||
posts = relationship('Posts', backref='users')
|
||||
groups = relationship('Groups', backref='users')
|
||||
session = relationship('Sessions', backref='users')
|
||||
log = relationship('Logs', backref='users')
|
||||
|
||||
|
||||
class posts (base): # pylint: disable=too-few-public-methods, C0103
|
||||
class Posts (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Post table
|
||||
Joins with group_junction
|
||||
|
@ -48,23 +46,34 @@ class posts (base): # pylint: disable=too-few-public-methods, C0103
|
|||
__tablename__ = 'posts'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
file_name = Column(String, unique=True, nullable=False)
|
||||
description = Column(String, nullable=False)
|
||||
alt = Column(String, nullable=False)
|
||||
author_id = Column(Integer, ForeignKey('users.id'))
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
|
||||
junction = relationship('group_junction')
|
||||
file_name = Column(String, unique=True, nullable=False)
|
||||
file_type = Column(String, nullable=False)
|
||||
|
||||
def __init__(self, file_name, description, alt, author_id):
|
||||
self.file_name = file_name
|
||||
self.description = description
|
||||
self.alt = alt
|
||||
self.author_id = author_id
|
||||
self.created_at = datetime.now()
|
||||
image_exif = Column(PickleType, nullable=False)
|
||||
image_colours = Column(PickleType, nullable=False)
|
||||
|
||||
post_description = Column(String, nullable=False)
|
||||
post_alt = Column(String, nullable=False)
|
||||
|
||||
junction = relationship('GroupJunction', backref='posts')
|
||||
|
||||
|
||||
class groups (base): # pylint: disable=too-few-public-methods, C0103
|
||||
class Thumbnails (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Thumbnail table
|
||||
"""
|
||||
__tablename__ = 'thumbnails'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
file_name = Column(String, unique=True, nullable=False)
|
||||
file_ext = Column(String, nullable=False)
|
||||
data = Column(PickleType, nullable=False)
|
||||
|
||||
|
||||
class Groups (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Group table
|
||||
Joins with group_junction
|
||||
|
@ -77,16 +86,10 @@ class groups (base): # pylint: disable=too-few-public-methods, C0103
|
|||
author_id = Column(Integer, ForeignKey('users.id'))
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
|
||||
junction = relationship('group_junction')
|
||||
|
||||
def __init__(self, name, description, author_id):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.author_id = author_id
|
||||
self.created_at = datetime.now()
|
||||
junction = relationship('GroupJunction', backref='groups')
|
||||
|
||||
|
||||
class group_junction (base): # pylint: disable=too-few-public-methods, C0103
|
||||
class GroupJunction (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Junction table for posts and groups
|
||||
Joins with posts and groups
|
||||
|
@ -94,15 +97,12 @@ class group_junction (base): # pylint: disable=too-few-public-methods, C0103
|
|||
__tablename__ = 'group_junction'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
date_added = Column(DateTime, nullable=False)
|
||||
group_id = Column(Integer, ForeignKey('groups.id'))
|
||||
post_id = Column(Integer, ForeignKey('posts.id'))
|
||||
|
||||
def __init__(self, group_id, post_id):
|
||||
self.group_id = group_id
|
||||
self.post_id = post_id
|
||||
|
||||
|
||||
class sessions (base): # pylint: disable=too-few-public-methods, C0103
|
||||
class Sessions (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Session table
|
||||
Joins with user
|
||||
|
@ -117,16 +117,8 @@ class sessions (base): # pylint: disable=too-few-public-methods, C0103
|
|||
active = Column(Boolean, nullable=False)
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
|
||||
def __init__(self, user_id, session_uuid, ip_address, user_agent, active): # pylint: disable=too-many-arguments, C0103
|
||||
self.user_id = user_id
|
||||
self.session_uuid = session_uuid
|
||||
self.ip_address = ip_address
|
||||
self.user_agent = user_agent
|
||||
self.active = active
|
||||
self.created_at = datetime.now()
|
||||
|
||||
|
||||
class logs (base): # pylint: disable=too-few-public-methods, C0103
|
||||
class Logs (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Log table
|
||||
Joins with user
|
||||
|
@ -140,15 +132,8 @@ class logs (base): # pylint: disable=too-few-public-methods, C0103
|
|||
msg = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
|
||||
def __init__(self, user_id, ip_address, code, msg):
|
||||
self.user_id = user_id
|
||||
self.ip_address = ip_address
|
||||
self.code = code
|
||||
self.msg = msg
|
||||
self.created_at = datetime.now()
|
||||
|
||||
|
||||
class bans (base): # pylint: disable=too-few-public-methods, C0103
|
||||
class Bans (base): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Bans table
|
||||
"""
|
||||
|
@ -160,11 +145,8 @@ class bans (base): # pylint: disable=too-few-public-methods, C0103
|
|||
msg = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
|
||||
def __init__(self, ip_address, code, msg):
|
||||
self.ip_address = ip_address
|
||||
self.code = code
|
||||
self.msg = msg
|
||||
self.created_at = datetime.now()
|
||||
|
||||
|
||||
base.metadata.create_all(engine)
|
||||
# check if database file exists, if not create it
|
||||
if not os.path.isfile(DB_PATH):
|
||||
base.metadata.create_all(engine)
|
||||
print('Database created')
|
||||
|
|
305
gallery/routes/api.py
Normal file
305
gallery/routes/api.py
Normal file
|
@ -0,0 +1,305 @@
|
|||
"""
|
||||
Onlylegs - API endpoints
|
||||
Used internally by the frontend and possibly by other applications
|
||||
"""
|
||||
from uuid import uuid4
|
||||
import os
|
||||
import pathlib
|
||||
import io
|
||||
import logging
|
||||
from datetime import datetime as dt
|
||||
|
||||
from flask import (Blueprint, send_from_directory, send_file,
|
||||
abort, flash, jsonify, request, g, current_app)
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from colorthief import ColorThief
|
||||
from PIL import Image, ImageOps, ImageFilter
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from gallery.auth import login_required
|
||||
|
||||
from gallery import db
|
||||
from gallery.utils import metadata as mt
|
||||
|
||||
|
||||
blueprint = Blueprint('api', __name__, url_prefix='/api')
|
||||
db_session = sessionmaker(bind=db.engine)
|
||||
db_session = db_session()
|
||||
|
||||
|
||||
@blueprint.route('/file/<file_name>', methods=['GET'])
|
||||
def get_file(file_name):
|
||||
"""
|
||||
Returns a file from the uploads folder
|
||||
r for resolution, 400x400 or thumb for thumbnail
|
||||
f is whether to apply filters to the image, such as blurring NSFW images
|
||||
b is whether to force blur the image, even if it's not NSFW
|
||||
"""
|
||||
# Get args
|
||||
res = request.args.get('r', default=None, type=str) # Type of file (thumb, etc)
|
||||
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters # pylint: disable=W0612
|
||||
blur = request.args.get('b', default=False, type=bool) # Whether to force blur
|
||||
|
||||
file_name = secure_filename(file_name) # Sanitize file name
|
||||
|
||||
# if no args are passed, return the raw file
|
||||
if not request.args:
|
||||
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name)):
|
||||
abort(404)
|
||||
|
||||
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name)
|
||||
|
||||
buff = io.BytesIO()
|
||||
img = None # Image object to be set
|
||||
|
||||
try: # Open image and set extension
|
||||
img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name))
|
||||
except FileNotFoundError: # FileNotFound is raised if the file doesn't exist
|
||||
logging.error('File not found: %s', file_name)
|
||||
abort(404)
|
||||
except OSError as err: # OSError is raised if the file is broken or corrupted
|
||||
logging.error('Possibly broken image %s, error: %s', file_name, err)
|
||||
abort(500)
|
||||
|
||||
img_ext = pathlib.Path(file_name).suffix.replace('.', '').lower() # Get file extension
|
||||
img_ext = current_app.config['ALLOWED_EXTENSIONS'][img_ext] # Convert to MIME type
|
||||
img_icc = img.info.get("icc_profile") # Get ICC profile
|
||||
|
||||
img = ImageOps.exif_transpose(img) # Rotate image based on EXIF data
|
||||
|
||||
# Todo: If type is thumb(nail), return from database instead of file system pylint: disable=W0511
|
||||
# as it's faster than generating a new thumbnail on every request
|
||||
if res:
|
||||
if res in ['thumb', 'thumbnail']:
|
||||
width, height = 400, 400
|
||||
elif res in ['prev', 'preview']:
|
||||
width, height = 1920, 1080
|
||||
else:
|
||||
try:
|
||||
width, height = res.split('x')
|
||||
width = int(width)
|
||||
height = int(height)
|
||||
except ValueError:
|
||||
abort(400)
|
||||
|
||||
img.thumbnail((width, height), Image.LANCZOS)
|
||||
|
||||
# Todo: If the image has a NSFW tag, blur image for example pylint: disable=W0511
|
||||
# if filtered:
|
||||
# pass
|
||||
|
||||
# If forced to blur, blur image
|
||||
if blur:
|
||||
img = img.filter(ImageFilter.GaussianBlur(20))
|
||||
|
||||
try:
|
||||
img.save(buff, img_ext, icc_profile=img_icc)
|
||||
except OSError:
|
||||
# This usually happens when saving a JPEG with an ICC profile,
|
||||
# so we convert to RGB and try again
|
||||
img = img.convert('RGB')
|
||||
img.save(buff, img_ext, icc_profile=img_icc)
|
||||
except Exception as err:
|
||||
logging.error('Could not resize image %s, error: %s', file_name, err)
|
||||
abort(500)
|
||||
|
||||
img.close() # Close image to free memory, learned the hard way
|
||||
buff.seek(0) # Reset buffer to start
|
||||
|
||||
return send_file(buff, mimetype='image/' + img_ext)
|
||||
|
||||
|
||||
@blueprint.route('/upload', methods=['POST'])
|
||||
@login_required
|
||||
def upload():
|
||||
"""
|
||||
Uploads an image to the server and saves it to the database
|
||||
"""
|
||||
form_file = request.files['file']
|
||||
form = request.form
|
||||
|
||||
# If no image is uploaded, return 404 error
|
||||
if not form_file:
|
||||
return abort(404)
|
||||
|
||||
# Get file extension, generate random name and set file path
|
||||
img_ext = pathlib.Path(form_file.filename).suffix.replace('.', '').lower()
|
||||
img_name = "GWAGWA_"+str(uuid4())
|
||||
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img_name+'.'+img_ext)
|
||||
|
||||
# Check if file extension is allowed
|
||||
if img_ext not in current_app.config['ALLOWED_EXTENSIONS'].keys():
|
||||
logging.info('File extension not allowed: %s', img_ext)
|
||||
abort(403)
|
||||
|
||||
# Save file
|
||||
try:
|
||||
form_file.save(img_path)
|
||||
except Exception as err:
|
||||
logging.error('Could not save file: %s', err)
|
||||
abort(500)
|
||||
|
||||
img_exif = mt.Metadata(img_path).yoink() # Get EXIF data
|
||||
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
|
||||
|
||||
# Save to database
|
||||
try:
|
||||
query = db.Posts(author_id=g.user.id,
|
||||
created_at=dt.utcnow(),
|
||||
file_name=img_name+'.'+img_ext,
|
||||
file_type=img_ext,
|
||||
image_exif=img_exif,
|
||||
image_colours=img_colors,
|
||||
post_description=form['description'],
|
||||
post_alt=form['alt'])
|
||||
|
||||
db_session.add(query)
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
logging.error('Could not save to database: %s', err)
|
||||
abort(500)
|
||||
|
||||
return 'Gwa Gwa' # Return something so the browser doesn't show an error
|
||||
|
||||
|
||||
@blueprint.route('/delete/<int:image_id>', methods=['POST'])
|
||||
@login_required
|
||||
def delete_image(image_id):
|
||||
"""
|
||||
Deletes an image from the server and database
|
||||
"""
|
||||
img = db_session.query(db.Posts).filter_by(id=image_id).first()
|
||||
|
||||
if img is None:
|
||||
abort(404)
|
||||
if img.author_id != g.user.id:
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
|
||||
except FileNotFoundError:
|
||||
# File was already deleted or doesn't exist
|
||||
logging.warning('File not found: %s, already deleted or never existed', img.file_name)
|
||||
except Exception as err:
|
||||
logging.error('Could not remove file: %s', err)
|
||||
abort(500)
|
||||
|
||||
try:
|
||||
db_session.query(db.Posts).filter_by(id=image_id).delete()
|
||||
|
||||
groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all()
|
||||
for group in groups:
|
||||
db_session.delete(group)
|
||||
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
logging.error('Could not remove from database: %s', err)
|
||||
abort(500)
|
||||
|
||||
logging.info('Removed image (%s) %s', image_id, img.file_name)
|
||||
flash(['Image was all in Le Head!', 1])
|
||||
return 'Gwa Gwa'
|
||||
|
||||
|
||||
@blueprint.route('/group/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_group():
|
||||
"""
|
||||
Creates a group
|
||||
"""
|
||||
new_group = db.Groups(name=request.form['name'],
|
||||
description=request.form['description'],
|
||||
author_id=g.user.id,
|
||||
created_at=dt.utcnow())
|
||||
|
||||
db_session.add(new_group)
|
||||
db_session.commit()
|
||||
|
||||
return ':3'
|
||||
|
||||
|
||||
@blueprint.route('/group/modify', methods=['POST'])
|
||||
@login_required
|
||||
def modify_group():
|
||||
"""
|
||||
Changes the images in a group
|
||||
"""
|
||||
group_id = request.form['group']
|
||||
image_id = request.form['image']
|
||||
|
||||
group = db_session.query(db.Groups).filter_by(id=group_id).first()
|
||||
|
||||
if group is None:
|
||||
abort(404)
|
||||
elif group.author_id != g.user.id:
|
||||
abort(403)
|
||||
|
||||
if request.form['action'] == 'add':
|
||||
if not db_session.query(db.GroupJunction)\
|
||||
.filter_by(group_id=group_id, post_id=image_id)\
|
||||
.first():
|
||||
db_session.add(db.GroupJunction(group_id=group_id,
|
||||
post_id=image_id,
|
||||
date_added=dt.utcnow()))
|
||||
elif request.form['action'] == 'remove':
|
||||
db_session.query(db.GroupJunction)\
|
||||
.filter_by(group_id=group_id, post_id=image_id)\
|
||||
.delete()
|
||||
|
||||
db_session.commit()
|
||||
|
||||
return ':3'
|
||||
|
||||
|
||||
@blueprint.route('/metadata/<int:img_id>', methods=['GET'])
|
||||
def metadata(img_id):
|
||||
"""
|
||||
Yoinks metadata from an image
|
||||
"""
|
||||
img = db_session.query(db.Posts).filter_by(id=img_id).first()
|
||||
|
||||
if not img:
|
||||
abort(404)
|
||||
|
||||
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
|
||||
exif = mt.Metadata(img_path).yoink()
|
||||
|
||||
return jsonify(exif)
|
||||
|
||||
|
||||
@blueprint.route('/logfile')
|
||||
@login_required
|
||||
def logfile():
|
||||
"""
|
||||
Gets the log file and returns it as a JSON object
|
||||
"""
|
||||
log_dict = {}
|
||||
|
||||
with open('only.log', encoding='utf-8', mode='r') as file:
|
||||
for i, line in enumerate(file):
|
||||
line = line.split(' : ')
|
||||
|
||||
event = line[0].strip().split(' ')
|
||||
event_data = {
|
||||
'date': event[0],
|
||||
'time': event[1],
|
||||
'severity': event[2],
|
||||
'owner': event[3]
|
||||
}
|
||||
|
||||
message = line[1].strip()
|
||||
try:
|
||||
message_data = {
|
||||
'code': int(message[1:4]),
|
||||
'message': message[5:].strip()
|
||||
}
|
||||
except ValueError:
|
||||
message_data = {'code': 0, 'message': message}
|
||||
except Exception as err:
|
||||
logging.error('Could not parse log file: %s', err)
|
||||
abort(500)
|
||||
|
||||
log_dict[i] = {'event': event_data, 'message': message_data}
|
||||
|
||||
return jsonify(log_dict)
|
106
gallery/routes/groups.py
Normal file
106
gallery/routes/groups.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
"""
|
||||
Onlylegs - Image Groups
|
||||
Why groups? Because I don't like calling these albums
|
||||
sounds more limiting that it actually is in this gallery
|
||||
"""
|
||||
from flask import Blueprint, abort, render_template, url_for
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from gallery import db
|
||||
|
||||
|
||||
blueprint = Blueprint('group', __name__, url_prefix='/group')
|
||||
db_session = sessionmaker(bind=db.engine)
|
||||
db_session = db_session()
|
||||
|
||||
|
||||
@blueprint.route('/', methods=['GET'])
|
||||
def groups():
|
||||
"""
|
||||
Group overview, shows all image groups
|
||||
"""
|
||||
group_list = db_session.query(db.Groups).all()
|
||||
|
||||
for group_item in group_list:
|
||||
thumbnail = db_session.query(db.GroupJunction.post_id)\
|
||||
.filter(db.GroupJunction.group_id == group_item.id)\
|
||||
.order_by(db.GroupJunction.date_added.desc())\
|
||||
.first()
|
||||
|
||||
if thumbnail:
|
||||
group_item.thumbnail = db_session.query(db.Posts.file_name, db.Posts.post_alt,
|
||||
db.Posts.image_colours, db.Posts.id)\
|
||||
.filter(db.Posts.id == thumbnail[0])\
|
||||
.first()
|
||||
|
||||
return render_template('groups/list.html', groups=group_list)
|
||||
|
||||
|
||||
@blueprint.route('/<int:group_id>')
|
||||
def group(group_id):
|
||||
"""
|
||||
Group view, shows all images in a group
|
||||
"""
|
||||
group_item = db_session.query(db.Groups).filter(db.Groups.id == group_id).first()
|
||||
|
||||
if group_item is None:
|
||||
abort(404, 'Group not found! D:')
|
||||
|
||||
group_item.author_username = db_session.query(db.Users.username)\
|
||||
.filter(db.Users.id == group_item.author_id)\
|
||||
.first()[0]
|
||||
|
||||
group_images = db_session.query(db.GroupJunction.post_id)\
|
||||
.filter(db.GroupJunction.group_id == group_id)\
|
||||
.order_by(db.GroupJunction.date_added.desc())\
|
||||
.all()
|
||||
|
||||
images = []
|
||||
for image in group_images:
|
||||
image = db_session.query(db.Posts).filter(db.Posts.id == image[0]).first()
|
||||
images.append(image)
|
||||
|
||||
return render_template('groups/group.html', group=group_item, images=images)
|
||||
|
||||
|
||||
@blueprint.route('/<int:group_id>/<int:image_id>')
|
||||
def group_post(group_id, image_id):
|
||||
"""
|
||||
Image view, shows the image and its metadata from a specific group
|
||||
"""
|
||||
img = db_session.query(db.Posts).filter(db.Posts.id == image_id).first()
|
||||
|
||||
if img is None:
|
||||
abort(404, 'Image not found')
|
||||
|
||||
img.author_username = db_session.query(db.Users.username)\
|
||||
.filter(db.Users.id == img.author_id)\
|
||||
.first()[0]
|
||||
|
||||
group_list = db_session.query(db.GroupJunction.group_id)\
|
||||
.filter(db.GroupJunction.post_id == image_id)\
|
||||
.all()
|
||||
|
||||
img.group_list = []
|
||||
for group_item in group_list:
|
||||
group_item = db_session.query(db.Groups).filter(db.Groups.id == group_item[0]).first()
|
||||
img.group_list.append(group_item)
|
||||
|
||||
next_url = db_session.query(db.GroupJunction.post_id)\
|
||||
.filter(db.GroupJunction.group_id == group_id)\
|
||||
.filter(db.GroupJunction.post_id > image_id)\
|
||||
.order_by(db.GroupJunction.date_added.asc())\
|
||||
.first()
|
||||
|
||||
prev_url = db_session.query(db.GroupJunction.post_id)\
|
||||
.filter(db.GroupJunction.group_id == group_id)\
|
||||
.filter(db.GroupJunction.post_id < image_id)\
|
||||
.order_by(db.GroupJunction.date_added.desc())\
|
||||
.first()
|
||||
|
||||
if next_url is not None:
|
||||
next_url = url_for('group.group_post', group_id=group_id, image_id=next_url[0])
|
||||
if prev_url is not None:
|
||||
prev_url = url_for('group.group_post', group_id=group_id, image_id=prev_url[0])
|
||||
|
||||
return render_template('image.html', image=img, next_url=next_url, prev_url=prev_url)
|
81
gallery/routes/routing.py
Normal file
81
gallery/routes/routing.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
"""
|
||||
Onlylegs Gallery - Routing
|
||||
"""
|
||||
from flask import Blueprint, render_template, url_for
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from gallery import db
|
||||
|
||||
|
||||
blueprint = Blueprint('gallery', __name__)
|
||||
db_session = sessionmaker(bind=db.engine)
|
||||
db_session = db_session()
|
||||
|
||||
|
||||
@blueprint.route('/')
|
||||
def index():
|
||||
"""
|
||||
Home page of the website, shows the feed of the latest images
|
||||
"""
|
||||
images = db_session.query(db.Posts.file_name,
|
||||
db.Posts.post_alt,
|
||||
db.Posts.image_colours,
|
||||
db.Posts.created_at,
|
||||
db.Posts.id).order_by(db.Posts.id.desc()).all()
|
||||
|
||||
return render_template('index.html', images=images)
|
||||
|
||||
|
||||
@blueprint.route('/image/<int:image_id>')
|
||||
def image(image_id):
|
||||
"""
|
||||
Image view, shows the image and its metadata
|
||||
"""
|
||||
img = db_session.query(db.Posts).filter(db.Posts.id == image_id).first()
|
||||
|
||||
if not img:
|
||||
abort(404, 'Image not found :<')
|
||||
|
||||
img.author_username = db_session.query(db.Users.username)\
|
||||
.filter(db.Users.id == img.author_id).first()[0]
|
||||
|
||||
groups = db_session.query(db.GroupJunction.group_id)\
|
||||
.filter(db.GroupJunction.post_id == image_id).all()
|
||||
|
||||
img.groups = []
|
||||
for group in groups:
|
||||
group = db_session.query(db.Groups).filter(db.Groups.id == group[0]).first()
|
||||
img.groups.append(group)
|
||||
|
||||
next_url = db_session.query(db.Posts.id)\
|
||||
.filter(db.Posts.id > image_id)\
|
||||
.order_by(db.Posts.id.asc())\
|
||||
.first()
|
||||
prev_url = db_session.query(db.Posts.id)\
|
||||
.filter(db.Posts.id < image_id)\
|
||||
.order_by(db.Posts.id.desc())\
|
||||
.first()
|
||||
|
||||
if next_url:
|
||||
next_url = url_for('gallery.image', image_id=next_url[0])
|
||||
if prev_url:
|
||||
prev_url = url_for('gallery.image', image_id=prev_url[0])
|
||||
|
||||
return render_template('image.html', image=img, next_url=next_url, prev_url=prev_url)
|
||||
|
||||
|
||||
@blueprint.route('/profile')
|
||||
def profile():
|
||||
"""
|
||||
Profile overview, shows all profiles on the onlylegs gallery
|
||||
"""
|
||||
return render_template('profile.html', user_id='gwa gwa')
|
||||
|
||||
|
||||
@blueprint.route('/profile/<int:user_id>')
|
||||
def profile_id(user_id):
|
||||
"""
|
||||
Shows user ofa given id, displays their uploads and other info
|
||||
"""
|
||||
return render_template('profile.html', user_id=user_id)
|
|
@ -17,6 +17,7 @@ def general():
|
|||
"""
|
||||
return render_template('settings/general.html')
|
||||
|
||||
|
||||
@blueprint.route('/server')
|
||||
@login_required
|
||||
def server():
|
||||
|
@ -25,6 +26,7 @@ def server():
|
|||
"""
|
||||
return render_template('settings/server.html')
|
||||
|
||||
|
||||
@blueprint.route('/account')
|
||||
@login_required
|
||||
def account():
|
||||
|
@ -33,6 +35,7 @@ def account():
|
|||
"""
|
||||
return render_template('settings/account.html')
|
||||
|
||||
|
||||
@blueprint.route('/logs')
|
||||
@login_required
|
||||
def logs():
|
|
@ -1,74 +0,0 @@
|
|||
"""
|
||||
Onlylegs Gallery - Routing
|
||||
"""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, render_template, current_app
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from . import db
|
||||
from . import metadata as mt
|
||||
|
||||
|
||||
blueprint = Blueprint('gallery', __name__)
|
||||
db_session = sessionmaker(bind=db.engine)
|
||||
db_session = db_session()
|
||||
|
||||
|
||||
@blueprint.route('/')
|
||||
def index():
|
||||
"""
|
||||
Home page of the website, shows the feed of latest images
|
||||
"""
|
||||
images = db_session.query(db.posts).order_by(db.posts.id.desc()).all()
|
||||
|
||||
return render_template('index.html',
|
||||
images=images,
|
||||
image_count=len(images),
|
||||
name=current_app.config['WEBSITE']['name'],
|
||||
motto=current_app.config['WEBSITE']['motto'])
|
||||
|
||||
@blueprint.route('/image/<int:image_id>')
|
||||
def image(image_id):
|
||||
"""
|
||||
Image view, shows the image and its metadata
|
||||
"""
|
||||
img = db_session.query(db.posts).filter_by(id=image_id).first()
|
||||
|
||||
if img is None:
|
||||
abort(404)
|
||||
|
||||
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
|
||||
exif = mt.Metadata(img_path).yoink()
|
||||
|
||||
return render_template('image.html', image=img, exif=exif)
|
||||
|
||||
@blueprint.route('/group')
|
||||
def groups():
|
||||
"""
|
||||
Group overview, shows all image groups
|
||||
"""
|
||||
return render_template('group.html', group_id='gwa gwa')
|
||||
|
||||
@blueprint.route('/group/<int:group_id>')
|
||||
def group(group_id):
|
||||
"""
|
||||
Group view, shows all images in a group
|
||||
"""
|
||||
return render_template('group.html', group_id=group_id)
|
||||
|
||||
@blueprint.route('/profile')
|
||||
def profile():
|
||||
"""
|
||||
Profile overview, shows all profiles on the onlylegs gallery
|
||||
"""
|
||||
return render_template('profile.html', user_id='gwa gwa')
|
||||
|
||||
@blueprint.route('/profile/<int:user_id>')
|
||||
def profile_id(user_id):
|
||||
"""
|
||||
Shows user ofa given id, displays their uploads and other info
|
||||
"""
|
||||
return render_template('profile.html', user_id=user_id)
|
|
@ -1,98 +0,0 @@
|
|||
"""
|
||||
OnlyLegs - Setup
|
||||
Runs when the app detects that there is no user directory
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import platformdirs
|
||||
import yaml
|
||||
|
||||
USER_DIR = platformdirs.user_config_dir('onlylegs')
|
||||
|
||||
class SetupApp:
|
||||
"""
|
||||
Setup the application on first run
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Main setup function
|
||||
"""
|
||||
print("Running setup...")
|
||||
|
||||
if not os.path.exists(USER_DIR):
|
||||
self.make_dir()
|
||||
if not os.path.exists(os.path.join(USER_DIR, '.env')):
|
||||
self.make_env()
|
||||
if not os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
|
||||
self.make_yaml()
|
||||
|
||||
def make_dir(self):
|
||||
"""
|
||||
Create the user directory
|
||||
"""
|
||||
try:
|
||||
os.makedirs(USER_DIR)
|
||||
os.makedirs(os.path.join(USER_DIR, 'instance'))
|
||||
|
||||
print("Created user directory at:", USER_DIR)
|
||||
except Exception as err:
|
||||
print("Error creating user directory:", err)
|
||||
sys.exit(1) # exit with error code
|
||||
|
||||
def make_env(self):
|
||||
"""
|
||||
Create the .env file with default values
|
||||
"""
|
||||
env_conf = {
|
||||
'FLASK_SECRETE': 'dev',
|
||||
}
|
||||
try:
|
||||
with open(os.path.join(USER_DIR, '.env'), encoding='utf-8') as file:
|
||||
for key, value in env_conf.items():
|
||||
file.write(f"{key}={value}\n")
|
||||
print("Created environment variables")
|
||||
except Exception as err:
|
||||
print("Error creating environment variables:", err)
|
||||
sys.exit(1)
|
||||
|
||||
print("Generated default .env file. EDIT IT BEFORE RUNNING THE APP AGAIN!")
|
||||
|
||||
def make_yaml(self):
|
||||
"""
|
||||
Create the YAML config file with default values
|
||||
"""
|
||||
yaml_conf = {
|
||||
'admin': {
|
||||
'name': 'Real Person',
|
||||
'username': 'User',
|
||||
'email': 'real-email@some.place'
|
||||
},
|
||||
'upload': {
|
||||
'allowed-extensions': {
|
||||
'jpg': 'jpeg',
|
||||
'jpeg': 'jpeg',
|
||||
'png': 'png',
|
||||
'webp': 'webp'
|
||||
},
|
||||
'max-size': 69,
|
||||
'rename': 'GWA_\{\{username\}\}_\{\{time\}\}'
|
||||
},
|
||||
'website': {
|
||||
'name': 'OnlyLegs',
|
||||
'motto': 'Gwa Gwa',
|
||||
'language': 'english'
|
||||
},
|
||||
'server': {
|
||||
'host': '0.0.0.0',
|
||||
'port': 5000
|
||||
},
|
||||
}
|
||||
try:
|
||||
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as file:
|
||||
yaml.dump(yaml_conf, file, default_flow_style=False)
|
||||
print("Created default gallery config")
|
||||
except Exception as err:
|
||||
print("Error creating default gallery config:", err)
|
||||
sys.exit(1)
|
||||
|
||||
print("Generated default YAML config. EDIT IT BEFORE RUNNING THE APP AGAIN!")
|
File diff suppressed because one or more lines are too long
Before (image error) Size: 19 KiB |
1
gallery/static/images/bg.svg
Normal file
1
gallery/static/images/bg.svg
Normal file
File diff suppressed because one or more lines are too long
After (image error) Size: 225 KiB |
Binary file not shown.
Before ![]() (image error) Size: 1.1 MiB |
60
gallery/static/images/logo-black.svg
Normal file
60
gallery/static/images/logo-black.svg
Normal file
File diff suppressed because one or more lines are too long
After (image error) Size: 7.9 KiB |
62
gallery/static/images/logo-white.svg
Normal file
62
gallery/static/images/logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
After (image error) Size: 8.1 KiB |
|
@ -1,108 +1,123 @@
|
|||
// Function to show login
|
||||
function showLogin() {
|
||||
popUpShow(
|
||||
'idk what to put here, just login please',
|
||||
'Login!',
|
||||
'Need an account? <span class="pop-up__link" onclick="showRegister()">Register!</span>',
|
||||
'<button class="pop-up__btn pop-up__btn-primary-fill" form="loginForm" type="submit">Login</button>',
|
||||
'<button class="btn-block" onclick="popupDissmiss()">Cancelee</button>\
|
||||
<button class="btn-block primary" form="loginForm" type="submit">Login</button>',
|
||||
'<form id="loginForm" onsubmit="return login(event)">\
|
||||
<input class="pop-up__input" type="text" placeholder="Namey" id="username"/>\
|
||||
<input class="pop-up__input" type="password" placeholder="Passywassy" id="password"/>\
|
||||
<input class="input-block" type="text" placeholder="Namey" id="username"/>\
|
||||
<input class="input-block" type="password" placeholder="Passywassy" id="password"/>\
|
||||
</form>'
|
||||
);
|
||||
};
|
||||
// Function to login
|
||||
function login(event) {
|
||||
// AJAX takes control of subby form :3
|
||||
event.preventDefault();
|
||||
|
||||
let formUsername = document.querySelector("#username").value;
|
||||
let formPassword = document.querySelector("#password").value;
|
||||
|
||||
if (formUsername === "" || formPassword === "") {
|
||||
addNotification("Please fill in all fields!!!!", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
var formData = new FormData();
|
||||
formData.append("username", formUsername);
|
||||
formData.append("password", formPassword);
|
||||
|
||||
$.ajax({
|
||||
url: '/auth/login',
|
||||
type: 'post',
|
||||
data: formData,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
location.reload();
|
||||
},
|
||||
error: function (response) {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here... Wrong information', 2);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Function to show register
|
||||
function showRegister() {
|
||||
popUpShow(
|
||||
'Who are you?',
|
||||
'Already have an account? <span class="pop-up__link" onclick="showLogin()">Login!</span>',
|
||||
'<button class="pop-up__btn pop-up__btn-primary-fill" form="registerForm" type="submit">Register</button>',
|
||||
'<button class="btn-block" onclick="popupDissmiss()">Canceleee</button>\
|
||||
<button class="btn-block primary" form="registerForm" type="submit">Register</button>',
|
||||
'<form id="registerForm" onsubmit="return register(event)">\
|
||||
<input class="pop-up__input" type="text" placeholder="Namey" id="username"/>\
|
||||
<input class="pop-up__input" type="text" placeholder="E mail!" id="email"/>\
|
||||
<input class="pop-up__input" type="password" placeholder="Passywassy" id="password"/>\
|
||||
<input class="pop-up__input" type="password" placeholder="Passywassy again!" id="password-repeat"/>\
|
||||
<input class="input-block" type="text" placeholder="Namey" id="username"/>\
|
||||
<input class="input-block" type="text" placeholder="E mail!" id="email"/>\
|
||||
<input class="input-block" type="password" placeholder="Passywassy" id="password"/>\
|
||||
<input class="input-block" type="password" placeholder="Passywassy again!" id="password-repeat"/>\
|
||||
</form>'
|
||||
);
|
||||
};
|
||||
|
||||
function login(event) {
|
||||
// AJAX takes control of subby form
|
||||
event.preventDefault();
|
||||
|
||||
if ($("#username").val() === "" || $("#password").val() === "") {
|
||||
addNotification("Please fill in all fields", 3);
|
||||
} else {
|
||||
// Make form
|
||||
var formData = new FormData();
|
||||
formData.append("username", $("#username").val());
|
||||
formData.append("password", $("#password").val());
|
||||
|
||||
$.ajax({
|
||||
url: '/auth/login',
|
||||
type: 'post',
|
||||
data: formData,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
location.reload();
|
||||
},
|
||||
error: function (response) {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here... Wrong information', 2);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Function to register
|
||||
function register(obj) {
|
||||
// AJAX takes control of subby form
|
||||
event.preventDefault();
|
||||
|
||||
if ($("#username").val() === "" || $("#email").val() === "" || $("#password").val() === "" || $("#password-repeat").val() === "") {
|
||||
addNotification("Please fill in all fields", 3);
|
||||
} else {
|
||||
// Make form
|
||||
var formData = new FormData();
|
||||
formData.append("username", $("#username").val());
|
||||
formData.append("email", $("#email").val());
|
||||
formData.append("password", $("#password").val());
|
||||
formData.append("password-repeat", $("#password-repeat").val());
|
||||
let formUsername = document.querySelector("#username").value;
|
||||
let formEmail = document.querySelector("#email").value;
|
||||
let formPassword = document.querySelector("#password").value;
|
||||
let formPasswordRepeat = document.querySelector("#password-repeat").value;
|
||||
|
||||
$.ajax({
|
||||
url: '/auth/register',
|
||||
type: 'post',
|
||||
data: formData,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
if (response === "gwa gwa") {
|
||||
addNotification('Registered successfully! Now please login to continue', 1);
|
||||
showLogin();
|
||||
} else {
|
||||
for (var i = 0; i < response.length; i++) {
|
||||
addNotification(response[i], 2);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function (response) {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here...', 2);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
break;
|
||||
if (formUsername === "" || formEmail === "" || formPassword === "" || formPasswordRepeat === "") {
|
||||
addNotification("Please fill in all fields!!!!", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
var formData = new FormData();
|
||||
formData.append("username", formUsername);
|
||||
formData.append("email", formEmail);
|
||||
formData.append("password", formPassword);
|
||||
formData.append("password-repeat", formPasswordRepeat);
|
||||
|
||||
$.ajax({
|
||||
url: '/auth/register',
|
||||
type: 'post',
|
||||
data: formData,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
if (response === "gwa gwa") {
|
||||
addNotification('Registered successfully! Now please login to continue', 1);
|
||||
showLogin();
|
||||
} else {
|
||||
for (var i = 0; i < response.length; i++) {
|
||||
addNotification(response[i], 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (response) {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here...', 2);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,168 +1,92 @@
|
|||
let navToggle = true;
|
||||
|
||||
document.onscroll = function() {
|
||||
try {
|
||||
document.querySelector('.background-decoration').style.opacity = `${1 - window.scrollY / 621}`;
|
||||
document.querySelector('.background-decoration').style.top = `-${window.scrollY / 5}px`;
|
||||
} catch (e) {
|
||||
console.log('No background decoration found');
|
||||
}
|
||||
|
||||
try {
|
||||
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
|
||||
document.querySelector('.banner').classList = 'banner banner-scrolled';
|
||||
} else {
|
||||
document.querySelector('.banner').classList = 'banner';
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log('No banner found');
|
||||
}
|
||||
|
||||
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
|
||||
document.querySelector('.jumpUp').classList = 'jumpUp jumpUp--show';
|
||||
// fade in images
|
||||
function imgFade(obj, time = 250) {
|
||||
$(obj).animate({ opacity: 1 }, time);
|
||||
}
|
||||
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
|
||||
function colourContrast(bgColor, lightColor, darkColor, threshold = 0.179) {
|
||||
// if color is in hex format then convert to rgb else parese rgb
|
||||
if (bgColor.charAt(0) === '#') {
|
||||
var color = (bgColor.charAt(0) === '#') ? bgColor.substring(1, 7) : bgColor;
|
||||
var r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||
var g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||
var b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||
} else {
|
||||
document.querySelector('.jumpUp').classList = 'jumpUp';
|
||||
var color = bgColor.replace('rgb(', '').replace(')', '').split(',');
|
||||
var r = color[0];
|
||||
var g = color[1];
|
||||
var b = color[2];
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('.jumpUp').onclick = function() {
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
}
|
||||
|
||||
function imgFade(obj) {
|
||||
$(obj).animate({opacity: 1}, 250);
|
||||
}
|
||||
|
||||
var times = document.getElementsByClassName('time');
|
||||
for (var i = 0; i < times.length; i++) {
|
||||
var time = times[i].innerHTML;
|
||||
var date = new Date(time);
|
||||
times[i].innerHTML = date.toLocaleString('en-GB');
|
||||
}
|
||||
|
||||
function addNotification(text='Sample notification', type=4) {
|
||||
var container = document.querySelector('.notifications');
|
||||
|
||||
// Create notification element
|
||||
var div = document.createElement('div');
|
||||
div.classList.add('sniffle__notification');
|
||||
div.onclick = function() {
|
||||
if (div.parentNode) {
|
||||
div.classList.add('sniffle__notification--hide');
|
||||
|
||||
setTimeout(function() {
|
||||
container.removeChild(div);
|
||||
}, 500);
|
||||
|
||||
var uicolors = [r / 255, g / 255, b / 255];
|
||||
var c = uicolors.map((col) => {
|
||||
if (col <= 0.03928) {
|
||||
return col / 12.92;
|
||||
}
|
||||
};
|
||||
|
||||
// Create icon element and append to notification
|
||||
var icon = document.createElement('span');
|
||||
icon.classList.add('sniffle__notification-icon');
|
||||
switch (type) {
|
||||
case 1:
|
||||
div.classList.add('sniffle__notification--success');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -7 24 24" fill="currentColor">\
|
||||
<path d="M5.486 9.73a.997.997 0 0 1-.707-.292L.537 5.195A1 1 0 1 1 1.95 3.78l3.535 3.535L11.85.952a1 1 0 0 1 1.415 1.414L6.193 9.438a.997.997 0 0 1-.707.292z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
case 2:
|
||||
div.classList.add('sniffle__notification--error');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6 -6 24 24" fill="currentColor">\
|
||||
<path d="M7.314 5.9l3.535-3.536A1 1 0 1 0 9.435.95L5.899 4.485 2.364.95A1 1 0 1 0 .95 2.364l3.535 3.535L.95 9.435a1 1 0 1 0 1.414 1.414l3.535-3.535 3.536 3.535a1 1 0 1 0 1.414-1.414L7.314 5.899z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
case 3:
|
||||
div.classList.add('sniffle__notification--warning');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -3 24 24" fill="currentColor">\
|
||||
<path d="M12.8 1.613l6.701 11.161c.963 1.603.49 3.712-1.057 4.71a3.213 3.213 0 0 1-1.743.516H3.298C1.477 18 0 16.47 0 14.581c0-.639.173-1.264.498-1.807L7.2 1.613C8.162.01 10.196-.481 11.743.517c.428.276.79.651 1.057 1.096zm-2.22.839a1.077 1.077 0 0 0-1.514.365L2.365 13.98a1.17 1.17 0 0 0-.166.602c0 .63.492 1.14 1.1 1.14H16.7c.206 0 .407-.06.581-.172a1.164 1.164 0 0 0 .353-1.57L10.933 2.817a1.12 1.12 0 0 0-.352-.365zM10 14a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0-9a1 1 0 0 1 1 1v4a1 1 0 0 1-2 0V6a1 1 0 0 1 1-1z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
default:
|
||||
div.classList.add('sniffle__notification--info');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor">\
|
||||
<path d="M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-10a1 1 0 0 1 1 1v5a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1zm0-1a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
}
|
||||
div.appendChild(icon);
|
||||
|
||||
// Create text element and append to notification
|
||||
var description = document.createElement('span');
|
||||
description.classList.add('sniffle__notification-text');
|
||||
description.innerHTML = text;
|
||||
div.appendChild(description);
|
||||
|
||||
// Create span to show time remaining
|
||||
var timer = document.createElement('span');
|
||||
timer.classList.add('sniffle__notification-time');
|
||||
div.appendChild(timer);
|
||||
|
||||
// Append notification to container
|
||||
container.appendChild(div);
|
||||
setTimeout(function() {
|
||||
div.classList.add('sniffle__notification-show');
|
||||
}, 100);
|
||||
|
||||
// Remove notification after 5 seconds
|
||||
setTimeout(function() {
|
||||
if (div.parentNode) {
|
||||
div.classList.add('sniffle__notification--hide');
|
||||
|
||||
setTimeout(function() {
|
||||
container.removeChild(div);
|
||||
}, 500);
|
||||
}
|
||||
}, 5000);
|
||||
return Math.pow((col + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
var L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
|
||||
return (L > threshold) ? darkColor : lightColor;
|
||||
}
|
||||
// Lazy load images when they are in view
|
||||
function loadOnView() {
|
||||
let lazyLoad = document.querySelectorAll('#lazy-load');
|
||||
|
||||
function popUpShow(title, body, actions, content) {
|
||||
var popup = document.querySelector('.pop-up');
|
||||
var popupContent = document.querySelector('.pop-up-content');
|
||||
var popupActions = document.querySelector('.pop-up-controlls');
|
||||
|
||||
// Set tile and description
|
||||
h3 = document.createElement('h3');
|
||||
h3.innerHTML = title;
|
||||
p = document.createElement('p');
|
||||
p.innerHTML = body;
|
||||
|
||||
popupContent.innerHTML = '';
|
||||
popupContent.appendChild(h3);
|
||||
popupContent.appendChild(p);
|
||||
|
||||
// Set content
|
||||
if (content != '') {
|
||||
popupContent.innerHTML += content;
|
||||
}
|
||||
|
||||
// Set buttons that will be displayed
|
||||
popupActions.innerHTML = '';
|
||||
if (actions != '') {
|
||||
popupActions.innerHTML += actions;
|
||||
}
|
||||
popupActions.innerHTML += '<button class="pop-up__btn pop-up__btn-fill" onclick="popupDissmiss()">Nooooooo</button>';
|
||||
|
||||
// Show popup
|
||||
popup.classList.add('pop-up__active');
|
||||
}
|
||||
|
||||
function popupDissmiss() {
|
||||
var popup = document.querySelector('.pop-up');
|
||||
|
||||
popup.classList.add('pop-up__hide');
|
||||
|
||||
setTimeout(function() {
|
||||
popup.classList = 'pop-up';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
if (document.querySelector('.pop-up').classList.contains('pop-up__active')) {
|
||||
popupDissmiss();
|
||||
for (let i = 0; i < lazyLoad.length; i++) {
|
||||
let image = lazyLoad[i];
|
||||
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
|
||||
if (!image.src) {
|
||||
image.src = `/api/file/${image.getAttribute('data-src')}?r=thumb`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
loadOnView();
|
||||
|
||||
const darkColor = '#151515';
|
||||
const lightColor = '#E8E3E3';
|
||||
let contrastCheck = document.querySelectorAll('#contrast-check');
|
||||
for (let i = 0; i < contrastCheck.length; i++) {
|
||||
console.log(contrastCheck[i].getAttribute('data-color'));
|
||||
bgColor = contrastCheck[i].getAttribute('data-color');
|
||||
contrastCheck[i].style.color = colourContrast(bgColor, lightColor, darkColor);
|
||||
}
|
||||
|
||||
let times = document.querySelectorAll('.time');
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
// Remove milliseconds
|
||||
const raw = times[i].innerHTML.split('.')[0];
|
||||
|
||||
// Parse YYYY-MM-DD HH:MM:SS to Date object
|
||||
const time = raw.split(' ')[1]
|
||||
const date = raw.split(' ')[0].split('-');
|
||||
|
||||
// Format to YYYY/MM/DD HH:MM:SS
|
||||
let formatted = date[0] + '/' + date[1] + '/' + date[2] + ' ' + time + ' UTC';
|
||||
|
||||
// Convert to UTC Date object
|
||||
let dateTime = new Date(formatted);
|
||||
|
||||
// Convert to local time
|
||||
times[i].innerHTML = dateTime.toLocaleDateString() + ' ' + dateTime.toLocaleTimeString();
|
||||
}
|
||||
};
|
||||
window.onscroll = function () {
|
||||
loadOnView();
|
||||
|
||||
// Jump to top button
|
||||
let topOfPage = document.querySelector('.top-of-page');
|
||||
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
|
||||
topOfPage.classList.add('show');
|
||||
} else {
|
||||
topOfPage.classList.remove('show');
|
||||
}
|
||||
topOfPage.onclick = function () {
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
window.onresize = function () {
|
||||
loadOnView();
|
||||
};
|
75
gallery/static/js/notifications.js
Normal file
75
gallery/static/js/notifications.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
function addNotification(text='Sample notification', type=4) {
|
||||
var container = document.querySelector('.notifications');
|
||||
|
||||
// Create notification element
|
||||
var div = document.createElement('div');
|
||||
div.classList.add('sniffle__notification');
|
||||
div.onclick = function() {
|
||||
if (div.parentNode) {
|
||||
div.classList.add('sniffle__notification--hide');
|
||||
|
||||
setTimeout(function() {
|
||||
container.removeChild(div);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Create icon element and append to notification
|
||||
var icon = document.createElement('span');
|
||||
icon.classList.add('sniffle__notification-icon');
|
||||
switch (type) {
|
||||
case 1:
|
||||
div.classList.add('sniffle__notification--success');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -7 24 24" fill="currentColor">\
|
||||
<path d="M5.486 9.73a.997.997 0 0 1-.707-.292L.537 5.195A1 1 0 1 1 1.95 3.78l3.535 3.535L11.85.952a1 1 0 0 1 1.415 1.414L6.193 9.438a.997.997 0 0 1-.707.292z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
case 2:
|
||||
div.classList.add('sniffle__notification--error');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6 -6 24 24" fill="currentColor">\
|
||||
<path d="M7.314 5.9l3.535-3.536A1 1 0 1 0 9.435.95L5.899 4.485 2.364.95A1 1 0 1 0 .95 2.364l3.535 3.535L.95 9.435a1 1 0 1 0 1.414 1.414l3.535-3.535 3.536 3.535a1 1 0 1 0 1.414-1.414L7.314 5.899z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
case 3:
|
||||
div.classList.add('sniffle__notification--warning');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -3 24 24" fill="currentColor">\
|
||||
<path d="M12.8 1.613l6.701 11.161c.963 1.603.49 3.712-1.057 4.71a3.213 3.213 0 0 1-1.743.516H3.298C1.477 18 0 16.47 0 14.581c0-.639.173-1.264.498-1.807L7.2 1.613C8.162.01 10.196-.481 11.743.517c.428.276.79.651 1.057 1.096zm-2.22.839a1.077 1.077 0 0 0-1.514.365L2.365 13.98a1.17 1.17 0 0 0-.166.602c0 .63.492 1.14 1.1 1.14H16.7c.206 0 .407-.06.581-.172a1.164 1.164 0 0 0 .353-1.57L10.933 2.817a1.12 1.12 0 0 0-.352-.365zM10 14a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0-9a1 1 0 0 1 1 1v4a1 1 0 0 1-2 0V6a1 1 0 0 1 1-1z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
default:
|
||||
div.classList.add('sniffle__notification--info');
|
||||
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor">\
|
||||
<path d="M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-10a1 1 0 0 1 1 1v5a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1zm0-1a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"></path>\
|
||||
</svg>';
|
||||
break;
|
||||
}
|
||||
div.appendChild(icon);
|
||||
|
||||
// Create text element and append to notification
|
||||
var description = document.createElement('span');
|
||||
description.classList.add('sniffle__notification-text');
|
||||
description.innerHTML = text;
|
||||
div.appendChild(description);
|
||||
|
||||
// Create span to show time remaining
|
||||
var timer = document.createElement('span');
|
||||
timer.classList.add('sniffle__notification-time');
|
||||
div.appendChild(timer);
|
||||
|
||||
// Append notification to container
|
||||
container.appendChild(div);
|
||||
setTimeout(function() {
|
||||
div.classList.add('sniffle__notification-show');
|
||||
}, 100);
|
||||
|
||||
// Remove notification after 5 seconds
|
||||
setTimeout(function() {
|
||||
if (div.parentNode) {
|
||||
div.classList.add('sniffle__notification--hide');
|
||||
|
||||
setTimeout(function() {
|
||||
container.removeChild(div);
|
||||
}, 500);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
42
gallery/static/js/popup.js
Normal file
42
gallery/static/js/popup.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
function popUpShow(title, body, actions, content) {
|
||||
// Stop scrolling
|
||||
document.querySelector("html").style.overflow = "hidden";
|
||||
|
||||
// Get popup elements
|
||||
var popup = document.querySelector('.pop-up');
|
||||
var popupContent = document.querySelector('.pop-up-content');
|
||||
var popupActions = document.querySelector('.pop-up-controlls');
|
||||
|
||||
// Set popup content
|
||||
popupContent.innerHTML = `<h3>${title}</h3><p>${body}</p>${content}`;
|
||||
|
||||
// Set buttons that will be displayed
|
||||
popupActions.innerHTML = actions;
|
||||
|
||||
// Show popup
|
||||
popup.style.display = 'block';
|
||||
setTimeout(function() {
|
||||
popup.classList.add('active')
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function popupDissmiss() {
|
||||
// un-Stop scrolling
|
||||
document.querySelector("html").style.overflow = "auto";
|
||||
|
||||
var popup = document.querySelector('.pop-up');
|
||||
|
||||
popup.classList.remove('active');
|
||||
|
||||
setTimeout(function() {
|
||||
popup.style.display = 'none';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
if (document.querySelector('.pop-up').classList.contains('active')) {
|
||||
popupDissmiss();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
function showUpload() {
|
||||
popUpShow(
|
||||
'Upload funny stuff',
|
||||
'May the world see your stuff 👀',
|
||||
'<button class="pop-up__btn pop-up__btn-primary-fill" form="uploadForm" type"submit">Upload</button>',
|
||||
'<form id="uploadForm" onsubmit="return uploadFile(event)">\
|
||||
<input class="pop-up__input" type="file" id="file"/>\
|
||||
<input class="pop-up__input" type="text" placeholder="alt" id="alt"/>\
|
||||
<input class="pop-up__input" type="text" placeholder="description" id="description"/>\
|
||||
<input class="pop-up__input" type="text" placeholder="tags" id="tags"/>\
|
||||
</form>'
|
||||
);
|
||||
};
|
||||
function uploadFile(){
|
||||
// AJAX takes control of subby form
|
||||
event.preventDefault();
|
||||
|
||||
// Check for empty upload
|
||||
if ($("#file").val() === "") {
|
||||
addNotification("Please select a file to upload", 2);
|
||||
} else {
|
||||
// Make form
|
||||
var formData = new FormData();
|
||||
formData.append("file", $("#file").prop("files")[0]);
|
||||
formData.append("alt", $("#alt").val());
|
||||
formData.append("description", $("#description").val());
|
||||
formData.append("tags", $("#tags").val());
|
||||
formData.append("submit", $("#submit").val());
|
||||
|
||||
// Upload the information
|
||||
$.ajax({
|
||||
url: '/api/upload',
|
||||
type: 'post',
|
||||
data: formData,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (response) {
|
||||
addNotification("File uploaded successfully!", 1);
|
||||
// popupDissmiss(); // Close popup
|
||||
},
|
||||
error: function (response) {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 400:
|
||||
case 404:
|
||||
addNotification('Error uploading. Blame yourself', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here...', 2);
|
||||
break;
|
||||
case 413:
|
||||
addNotification('File too large!!!!!!', 3);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error uploading file, blame someone', 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Empty values
|
||||
$("#file").val("");
|
||||
$("#alt").val("");
|
||||
$("#description").val("");
|
||||
$("#tags").val("");
|
||||
}
|
||||
};
|
128
gallery/static/js/uploadTab.js
Normal file
128
gallery/static/js/uploadTab.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
// Function to upload images
|
||||
function uploadFile() {
|
||||
// AJAX takes control of subby form
|
||||
event.preventDefault();
|
||||
|
||||
const jobList = document.querySelector(".upload-jobs");
|
||||
|
||||
// Check for empty upload
|
||||
if ($("#file").val() === "") {
|
||||
addNotification("Please select a file to upload", 2);
|
||||
} else {
|
||||
// Make form
|
||||
let formData = new FormData();
|
||||
formData.append("file", $("#file").prop("files")[0]);
|
||||
formData.append("alt", $("#alt").val());
|
||||
formData.append("description", $("#description").val());
|
||||
formData.append("tags", $("#tags").val());
|
||||
formData.append("submit", $("#submit").val());
|
||||
|
||||
// Upload the information
|
||||
$.ajax({
|
||||
url: '/api/upload',
|
||||
type: 'post',
|
||||
data: formData,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
beforeSend: function () {
|
||||
jobContainer = document.createElement("div");
|
||||
jobContainer.classList.add("job");
|
||||
|
||||
jobStatus = document.createElement("span");
|
||||
jobStatus.classList.add("job__status");
|
||||
jobStatus.innerHTML = "Uploading...";
|
||||
|
||||
jobProgress = document.createElement("span");
|
||||
jobProgress.classList.add("progress");
|
||||
|
||||
jobImg = document.createElement("img");
|
||||
jobImg.src = URL.createObjectURL($("#file").prop("files")[0]);
|
||||
|
||||
jobImgFilter = document.createElement("span");
|
||||
jobImgFilter.classList.add("img-filter");
|
||||
|
||||
jobContainer.appendChild(jobStatus);
|
||||
jobContainer.appendChild(jobProgress);
|
||||
jobContainer.appendChild(jobImg);
|
||||
jobContainer.appendChild(jobImgFilter);
|
||||
jobList.appendChild(jobContainer);
|
||||
},
|
||||
success: function (response) {
|
||||
jobContainer.classList.add("success");
|
||||
jobStatus.innerHTML = "Uploaded!";
|
||||
if (!document.querySelector(".upload-panel").classList.contains("open")) {
|
||||
addNotification("Image uploaded successfully", 1);
|
||||
}
|
||||
},
|
||||
error: function (response) {
|
||||
jobContainer.classList.add("critical");
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
jobStatus.innerHTML = "Server exploded, F's in chat";
|
||||
break;
|
||||
case 400:
|
||||
case 404:
|
||||
jobStatus.innerHTML = "Error uploading. Blame yourself";
|
||||
break;
|
||||
case 403:
|
||||
jobStatus.innerHTML = "None but devils play past here...";
|
||||
break;
|
||||
case 413:
|
||||
jobStatus.innerHTML = "File too large!!!!!!";
|
||||
break;
|
||||
default:
|
||||
jobStatus.innerHTML = "Error uploading file, blame someone";
|
||||
break;
|
||||
}
|
||||
if (!document.querySelector(".upload-panel").classList.contains("open")) {
|
||||
addNotification("Error uploading file", 2);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Empty values
|
||||
$("#file").val("");
|
||||
$("#alt").val("");
|
||||
$("#description").val("");
|
||||
$("#tags").val("");
|
||||
}
|
||||
};
|
||||
|
||||
// open upload tab
|
||||
function openUploadTab() {
|
||||
// Stop scrolling
|
||||
document.querySelector("html").style.overflow = "hidden";
|
||||
document.querySelector(".content").tabIndex = "-1";
|
||||
|
||||
// Open upload tab
|
||||
const uploadTab = document.querySelector(".upload-panel");
|
||||
uploadTab.style.display = "block";
|
||||
|
||||
setTimeout(function () {
|
||||
uploadTab.classList.add("open");
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// close upload tab
|
||||
function closeUploadTab() {
|
||||
// un-Stop scrolling
|
||||
document.querySelector("html").style.overflow = "auto";
|
||||
document.querySelector(".content").tabIndex = "";
|
||||
|
||||
// Close upload tab
|
||||
const uploadTab = document.querySelector(".upload-panel");
|
||||
uploadTab.classList.remove("open");
|
||||
|
||||
setTimeout(function () {
|
||||
uploadTab.style.display = "none";
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// toggle upload tab
|
||||
function toggleUploadTab() {
|
||||
if (document.querySelector(".upload-panel").classList.contains("open")) {
|
||||
closeUploadTab();
|
||||
} else {
|
||||
openUploadTab();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block wrapper_class %}error-wrapper{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{error}}</h1>
|
||||
<p>{{msg}}</p>
|
||||
<span class="error-page">
|
||||
<h1>{{error}}</h1>
|
||||
<p>{{msg}}</p>
|
||||
</span>
|
||||
{% endblock %}
|
|
@ -1,15 +0,0 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block header %}
|
||||
<div class="background-decoration">
|
||||
<img src="{{ url_for('static', filename='images/background.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span></span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block nav_groups %}navigation-item__selected{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Image Group</h1>
|
||||
<p>{{group_id}}</p>
|
||||
{% endblock %}
|
91
gallery/templates/groups/group.html
Normal file
91
gallery/templates/groups/group.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block nav_groups %}navigation-item__selected{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="banner">
|
||||
{% if images %}
|
||||
<img
|
||||
src="/api/file/{{ images.0.file_name }}?w=1920&h=1080"
|
||||
onload="imgFade(this)"
|
||||
style="opacity:0; background-color:rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})"
|
||||
/>
|
||||
<span
|
||||
class="banner-filter"
|
||||
style="background: linear-gradient(to right, rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}), transparent)"
|
||||
></span>
|
||||
<div class="banner-content">
|
||||
<p><span id="contrast-check" data-color="rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})">{{ group.description }}</span></p>
|
||||
<h1><span id="contrast-check" data-color="rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})">{{ group.name }}</span></h1>
|
||||
<p><span id="contrast-check" data-color="rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})">By {{ group.author_username }} - {{ images|length }} Images</span></p>
|
||||
</div>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span></span>
|
||||
<div class="banner-content">
|
||||
<p>{{ group.description }}</p>
|
||||
<h1>{{ group.name }}</h1>
|
||||
<p>By {{ group.author_username }} - {{ images|length }} Images</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form id="modifyGroup">
|
||||
<input type="text" name="group" placeholder="group id" value="{{ group.id }}">
|
||||
<input type="text" name="image" placeholder="image id">
|
||||
<input type="text" name="action" placeholder="add/remove" value="add">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
{% if images %}
|
||||
<div class="gallery-grid">
|
||||
{% for image in images %}
|
||||
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('group.group_post', group_id=group.id, image_id=image.id) }}" style="background-color: rgb({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }})">
|
||||
<div class="image-filter">
|
||||
<p class="image-subtitle"></p>
|
||||
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
|
||||
</div>
|
||||
<img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="imgFade(this)" style="opacity:0;" id="lazy-load"/>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-text">
|
||||
<h1>No image!</h1>
|
||||
{% if g.user %}
|
||||
<p>You can get started by uploading an image!</p>
|
||||
{% else %}
|
||||
<p>Login to start uploading images!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
// /api/group/modify
|
||||
|
||||
modForm = document.querySelector('#modifyGroup');
|
||||
|
||||
modForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('group', modForm.group.value);
|
||||
formData.append('image', modForm.image.value);
|
||||
formData.append('action', modForm.action.value);
|
||||
|
||||
$.ajax({
|
||||
url: '/api/group/modify',
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (data) {
|
||||
addNotification('Image added to group', 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
59
gallery/templates/groups/list.html
Normal file
59
gallery/templates/groups/list.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block nav_groups %}navigation-item__selected{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="banner">
|
||||
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span class="banner-filter"></span>
|
||||
|
||||
<div class="banner-content">
|
||||
<p>{{ config.WEBSITE.motto }}</p>
|
||||
<h1>Groups</h1>
|
||||
<p>{{ groups|length }} Groups</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/api/group/create" method="post" enctype="multipart/form-data">
|
||||
<input type="text" name="name" placeholder="name">
|
||||
<input type="text" name="description" placeholder="description">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
{% if groups %}
|
||||
<div class="gallery-grid">
|
||||
{% for group in groups %}
|
||||
{% if group.thumbnail %}
|
||||
<a id="group-{{ group.id }}" class="gallery-item" href="{{ url_for('group.group', group_id=group.id) }}" style="background-color: rgb({{ group.thumbnail.image_colours.0.0 }}, {{ group.thumbnail.image_colours.0.1 }}, {{ group.thumbnail.image_colours.0.2 }})">
|
||||
<div class="image-filter">
|
||||
<p class="image-subtitle"></p>
|
||||
<p class="image-title">{{ group.name }}</p>
|
||||
</div>
|
||||
<img data-src="{{ group.thumbnail.file_name }}" onload="imgFade(this)" style="opacity:0;" id="lazy-load"/>
|
||||
</a>
|
||||
{% else %}
|
||||
<a id="group-{{ group.id }}" class="gallery-item" href="{{ url_for('group.group', group_id=group.id) }}">
|
||||
<div class="image-filter">
|
||||
<p class="image-subtitle"></p>
|
||||
<p class="image-title">{{ group.name }}</p>
|
||||
</div>
|
||||
<img src="{{ url_for('static', filename='images/error.png') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-text">
|
||||
<h1>No image groups!</h1>
|
||||
{% if g.user %}
|
||||
<p>You can get started by creating a new image group!</p>
|
||||
{% else %}
|
||||
<p>Login to get started!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script></script>
|
||||
{% endblock %}
|
|
@ -1,238 +1,253 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block header %}
|
||||
<div class="background-decoration">
|
||||
<img src="/api/uploads/{{ image['file_name'] }}?w=1000&h=1000" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span></span>
|
||||
</div>
|
||||
{% block head %}
|
||||
<meta property="og:image" content="/api/file/{{ image.file_name }}"/>
|
||||
<meta name="theme-color" content="#{{ image.image_colours.0.0 }}{{ image.image_colours.0.1 }}{{ image.image_colours.0.2 }}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block wrapper_class %}image-wrapper{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="background">
|
||||
<img src="/api/file/{{ image.file_name }}?r=prev" alt="{{ image.post_alt }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span style="background-image: linear-gradient(to top, rgba({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }}, 1), transparent);"></span>
|
||||
</div>
|
||||
|
||||
<div class="image-fullscreen">
|
||||
<img src="" onload="imgFade(this);" style="opacity:0;" />
|
||||
<img src="" alt="{{ image.post_alt }}" onload="imgFade(this);" style="opacity:0;" />
|
||||
</div>
|
||||
|
||||
<div class="image-container">
|
||||
<img
|
||||
src="/api/uploads/{{ image['file_name'] }}?w=1000&h=1000"
|
||||
onload="imgFade(this)" style="opacity:0;"
|
||||
onerror="this.src='/static/images/error.png'"
|
||||
width="{{ exif['File']['Width']['raw'] }}"
|
||||
height="{{ exif['File']['Height']['raw'] }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pill-row" id="image-tools">
|
||||
<div>
|
||||
<button class="pill-item" id="img-fullscreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 24 24" fill="currentColor">
|
||||
<path d="M12.586 2H11a1 1 0 0 1 0-2h4a1 1 0 0 1 1 1v4a1 1 0 0 1-2 0V3.414L9.414 8 14 12.586V11a1 1 0 0 1 2 0v4a1 1 0 0 1-1 1h-4a1 1 0 0 1 0-2h1.586L8 9.414 3.414 14H5a1 1 0 0 1 0 2H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 2 0v1.586L6.586 8 2 3.414V5a1 1 0 1 1-2 0V1a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2H3.414L8 6.586 12.586 2z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">Fullscreen</span>
|
||||
</button>
|
||||
<button class="pill-item" id="img-share">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 24 24" fill="currentColor">
|
||||
<path d="M3.19 9.345a.97.97 0 0 1 1.37 0 .966.966 0 0 1 0 1.367l-2.055 2.052a1.932 1.932 0 0 0 0 2.735 1.94 1.94 0 0 0 2.74 0l4.794-4.787a.966.966 0 0 0 0-1.367.966.966 0 0 1 0-1.368.97.97 0 0 1 1.37 0 2.898 2.898 0 0 1 0 4.103l-4.795 4.787a3.879 3.879 0 0 1-5.48 0 3.864 3.864 0 0 1 0-5.47L3.19 9.344zm11.62-.69a.97.97 0 0 1-1.37 0 .966.966 0 0 1 0-1.367l2.055-2.052a1.932 1.932 0 0 0 0-2.735 1.94 1.94 0 0 0-2.74 0L7.962 7.288a.966.966 0 0 0 0 1.367.966.966 0 0 1 0 1.368.97.97 0 0 1-1.37 0 2.898 2.898 0 0 1 0-4.103l4.795-4.787a3.879 3.879 0 0 1 5.48 0 3.864 3.864 0 0 1 0 5.47L14.81 8.656z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">Share</span>
|
||||
</button>
|
||||
<a class="pill-item" id="img-download" href="/api/uploads/{{ image['file_name'] }}/0" download>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 24 24" fill="currentColor">
|
||||
<path d="M8 6.641l1.121-1.12a1 1 0 0 1 1.415 1.413L7.707 9.763a.997.997 0 0 1-1.414 0L3.464 6.934A1 1 0 1 1 4.88 5.52L6 6.641V1a1 1 0 1 1 2 0v5.641zM1 12h12a1 1 0 0 1 0 2H1a1 1 0 0 1 0-2z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">Download</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if g.user['id'] == image['author_id'] %}
|
||||
<div>
|
||||
<button class="pill-item pill__critical" id="img-delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -2 24 24" fill="currentColor">
|
||||
<path d="M6 2V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2h-.133l-.68 10.2a3 3 0 0 1-2.993 2.8H5.826a3 3 0 0 1-2.993-2.796L2.137 7H2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h4zm10 2H2v1h14V4zM4.141 7l.687 10.068a1 1 0 0 0 .998.932h6.368a1 1 0 0 0 .998-.934L13.862 7h-9.72zM7 8a1 1 0 0 1 1 1v7a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1zm4 0a1 1 0 0 1 1 1v7a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">Delete</span>
|
||||
</button>
|
||||
<button class="pill-item pill__critical" id="img-edit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2.5 -2.5 24 24" fill="currentColor">
|
||||
<path d="M12.238 5.472L3.2 14.51l-.591 2.016 1.975-.571 9.068-9.068-1.414-1.415zM13.78 3.93l1.414 1.414 1.318-1.318a.5.5 0 0 0 0-.707l-.708-.707a.5.5 0 0 0-.707 0L13.781 3.93zm3.439-2.732l.707.707a2.5 2.5 0 0 1 0 3.535L5.634 17.733l-4.22 1.22a1 1 0 0 1-1.237-1.241l1.248-4.255 12.26-12.26a2.5 2.5 0 0 1 3.535 0z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!--
|
||||
<div>
|
||||
<button class="tool-btn" id="img-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor">
|
||||
<path d="M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-10a1 1 0 0 1 1 1v5a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1zm0-1a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">Info</span>
|
||||
</button>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="image-info__container">
|
||||
{% if image['alt'] != '' %}
|
||||
<div class="image-info">
|
||||
<span class="image-info__collapse" id="collapse-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
|
||||
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2.5 24 24" fill="currentColor">
|
||||
<path d="M3.656 17.979A1 1 0 0 1 2 17.243V15a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H8.003l-4.347 2.979zm.844-3.093a.536.536 0 0 0 .26-.069l2.355-1.638A1 1 0 0 1 7.686 13H12a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v5c0 .54.429.982 1 1 .41.016.707.083.844.226.128.134.135.36.156.79.003.063.003.177 0 .37a.5.5 0 0 0 .5.5z"></path><path d="M16 10.017a7.136 7.136 0 0 0 0 .369v-.37c.02-.43.028-.656.156-.79.137-.143.434-.21.844-.226.571-.018 1-.46 1-1V3a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1H5V2a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2v2.243a1 1 0 0 1-1.656.736L16 13.743v-3.726z"></path>
|
||||
</svg>
|
||||
<h2>Alt</h2>
|
||||
</div>
|
||||
<div class="image-info__content">
|
||||
<p>{{ image['alt'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if image['description'] != '' %}
|
||||
<div class="image-info">
|
||||
<span class="image-info__collapse" id="collapse-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
|
||||
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -2 24 24" fill="currentColor">
|
||||
<path d="M3 0h10a3 3 0 0 1 3 3v14a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm2 1h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 12h2a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zm0-4h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zm0-4h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"></path>
|
||||
</svg>
|
||||
<h2>Description</h2>
|
||||
</div>
|
||||
<div class="image-info__content">
|
||||
<p>{{ image['description'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="image-info">
|
||||
<span class="image-info__collapse" id="collapse-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
|
||||
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor">
|
||||
<path d="M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-10a1 1 0 0 1 1 1v5a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1zm0-1a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"></path>
|
||||
</svg>
|
||||
<h2>Info</h2>
|
||||
</div>
|
||||
<div class="image-info__content">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Image ID</td>
|
||||
<td>{{ image['id'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author</td>
|
||||
<td>{{ image['author_id'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upload date</td>
|
||||
<td><span class="time">{{ image['created_at'] }}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% for tag in exif %}
|
||||
<div class="image-info">
|
||||
<span class="image-info__collapse" id="collapse-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
|
||||
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{% if tag == 'Photographer' %}
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -2 24 24" fill="currentColor">
|
||||
<path d="M3.534 10.07a1 1 0 1 1 .733 1.86A3.579 3.579 0 0 0 2 15.26V17a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1.647a3.658 3.658 0 0 0-2.356-3.419 1 1 0 1 1 .712-1.868A5.658 5.658 0 0 1 14 15.353V17a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-1.74a5.579 5.579 0 0 1 3.534-5.19zM7 0a4 4 0 0 1 4 4v2a4 4 0 1 1-8 0V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0V4a2 2 0 0 0-2-2z"></path>
|
||||
</svg>
|
||||
<h2>Photographer</h2>
|
||||
</div>
|
||||
{% elif tag == 'Camera' %}
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -4 24 24" fill="currentColor">
|
||||
<path d="M5.676 5H4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-1.676l-.387-1.501A2.002 2.002 0 0 0 12 2H8a2 2 0 0 0-1.937 1.499L5.676 5zm-1.55-2C4.57 1.275 6.136 0 8 0h4a4.002 4.002 0 0 1 3.874 3H16a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h.126zM10 13a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm6-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
|
||||
</svg>
|
||||
<h2>Camera</h2>
|
||||
</div>
|
||||
{% elif tag == 'Software' %}
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -4 24 24" fill="currentColor">
|
||||
<path d="M2 13v1h3V2H2v9h1v2H2zM1 0h5a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm9 3h8a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm0 2v6h8V5h-8zm2 9h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2z"></path>
|
||||
</svg>
|
||||
<h2>Software</h2>
|
||||
</div>
|
||||
{% elif tag == 'File' %}
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -2 24 24" fill="currentColor">
|
||||
<path d="M14 8.322V2H2v12h3.576l3.97-5.292A3 3 0 0 1 14 8.322zm0 3.753l-1.188-2.066a1 1 0 0 0-1.667-.101L8.076 14H14v-1.925zM14 16H2v2h12v-2zM2 0h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm4 9a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
|
||||
</svg>
|
||||
<h2>File</h2>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="image-info__header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor">
|
||||
<path d="M14.95 7.879l-.707-.707a1 1 0 0 1 1.414-1.415l.707.707 1.414-1.414-2.828-2.828L2.222 14.95l2.828 2.828 1.414-1.414L5.05 14.95a1 1 0 0 1 1.414-1.414L7.88 14.95l1.414-1.414-.707-.708A1 1 0 0 1 10 11.414l.707.707 1.414-1.414-1.414-1.414a1 1 0 0 1 1.414-1.414l1.415 1.414 1.414-1.414zM.808 13.536L13.536.808a2 2 0 0 1 2.828 0l2.828 2.828a2 2 0 0 1 0 2.828L6.464 19.192a2 2 0 0 1-2.828 0L.808 16.364a2 2 0 0 1 0-2.828z"></path>
|
||||
</svg>
|
||||
<h2>{{tag}}</h2>
|
||||
</div>
|
||||
<div class="image-grid">
|
||||
<div class="image-container" id="image-container">
|
||||
<img
|
||||
src="/api/file/{{ image.file_name }}?r=prev"
|
||||
alt="{{ image.post_alt }}"
|
||||
onload="imgFade(this)" style="opacity:0;"
|
||||
onerror="this.src='/static/images/error.png'"
|
||||
{% if "File" in image.image_exif %}
|
||||
width="{{ image.image_exif.File.Width.raw }}"
|
||||
height="{{ image.image_exif.File.Height.raw }}"
|
||||
{% endif %}
|
||||
<div class="image-info__content">
|
||||
<table>
|
||||
{% for subtag in exif[tag] %}
|
||||
<tr>
|
||||
<td>{{subtag}}</td>
|
||||
{% if exif[tag][subtag]['formatted'] %}
|
||||
{% if exif[tag][subtag]['type'] == 'date' %}
|
||||
<td><span class="time">{{exif[tag][subtag]['formatted']}}</span></td>
|
||||
{% else %}
|
||||
<td>{{exif[tag][subtag]['formatted']}}</td>
|
||||
{% endif %}
|
||||
{% elif exif[tag][subtag]['raw'] %}
|
||||
<td>{{exif[tag][subtag]['raw']}}</td>
|
||||
{% else %}
|
||||
<td class="empty-table">Oops, an error</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pill-row" id="image-tools">
|
||||
{% if next_url %}
|
||||
<div>
|
||||
<a class="pill-item" href="{{ next_url }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 24 24" width="24" fill="currentColor">
|
||||
<path d="M3.414 7.657l3.95 3.95A1 1 0 0 1 5.95 13.02L.293 7.364a.997.997 0 0 1 0-1.414L5.95.293a1 1 0 1 1 1.414 1.414l-3.95 3.95H13a1 1 0 0 1 0 2H3.414z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">
|
||||
Previous
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<button class="pill-item" id="img-fullscreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,48V96L160,48ZM48,208H96L48,160Zm160,0V160l-48,48ZM48,96,96,48H48Z" opacity="0.2"></path><path d="M98.34,146.34,72,172.69,53.66,154.34A8,8,0,0,0,40,160v48a8,8,0,0,0,8,8H96a8,8,0,0,0,5.66-13.66L83.31,184l26.35-26.34a8,8,0,0,0-11.32-11.32ZM56,200V179.31L76.69,200ZM83.31,72l18.35-18.34A8,8,0,0,0,96,40H48a8,8,0,0,0-8,8V96a8,8,0,0,0,13.66,5.66L72,83.31l26.34,26.35a8,8,0,0,0,11.32-11.32ZM56,76.69V56H76.69ZM208,40H160a8,8,0,0,0-5.66,13.66L172.69,72,146.34,98.34a8,8,0,0,0,11.32,11.32L184,83.31l18.34,18.35A8,8,0,0,0,216,96V48A8,8,0,0,0,208,40Zm-8,36.69L179.31,56H200Zm11.06,75.92a8,8,0,0,0-8.72,1.73L184,172.69l-26.34-26.35a8,8,0,0,0-11.32,11.32L172.69,184l-18.35,18.34A8,8,0,0,0,160,216h48a8,8,0,0,0,8-8V160A8,8,0,0,0,211.06,152.61ZM200,200H179.31L200,179.31Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Fullscreen
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="pill-item" id="img-share">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M209.94,113.94l-28,28a47.76,47.76,0,0,1-26.52,13.48,47.76,47.76,0,0,1-13.48,26.52l-28,28a48,48,0,0,1-67.88-67.88l28-28a47.76,47.76,0,0,1,26.52-13.48,47.76,47.76,0,0,1,13.48-26.52l28-28a48,48,0,0,1,67.88,67.88Z" opacity="0.2"></path><path d="M137.54,186.36a8,8,0,0,1,0,11.31l-17.94,18A56,56,0,0,1,40.38,136.4L68.5,108.29A56,56,0,0,1,145.31,106a8,8,0,1,1-10.64,12,40,40,0,0,0-54.85,1.63L51.7,147.72a40,40,0,1,0,56.58,56.58l17.94-17.94A8,8,0,0,1,137.54,186.36Zm78.08-146a56.08,56.08,0,0,0-79.22,0L118.46,58.33a8,8,0,0,0,11.32,11.31L147.72,51.7a40,40,0,0,1,56.58,56.58L176.18,136.4A40,40,0,0,1,121.33,138,8,8,0,1,0,110.69,150a56,56,0,0,0,76.81-2.27l28.12-28.11A56.08,56.08,0,0,0,215.62,40.38Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Share
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
<a class="pill-item" href="/api/file/{{ image.file_name }}" download onclick="addNotification('Download started!', 4)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M232,136v64a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V136a8,8,0,0,1,8-8H224A8,8,0,0,1,232,136Z" opacity="0.2"></path><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Download
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if image.author_id == g.user.id %}
|
||||
<div>
|
||||
<button class="pill-item pill__critical" id="img-delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56Z" opacity="0.2"></path><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Delete
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="pill-item pill__critical" id="img-edit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,90.34,192,120,136,64l29.66-29.66a8,8,0,0,1,11.31,0L221.66,79A8,8,0,0,1,221.66,90.34Z" opacity="0.2"></path><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Edit
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if prev_url %}
|
||||
<div>
|
||||
<a class="pill-item" href="{{ prev_url }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 24 24" width="24" fill="currentColor">
|
||||
<path d="M10.586 5.657l-3.95-3.95A1 1 0 0 1 8.05.293l5.657 5.657a.997.997 0 0 1 0 1.414L8.05 13.021a1 1 0 1 1-1.414-1.414l3.95-3.95H1a1 1 0 1 1 0-2h9.586z"></path>
|
||||
</svg>
|
||||
<span class="tool-tip">
|
||||
Next
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="info-container" id="image-info">
|
||||
{% if image.post_description %}
|
||||
<div class="info-tab">
|
||||
<div class="info-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,56V200a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H216A8,8,0,0,1,224,56Z" opacity="0.2"></path><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,160H40V56H216V200ZM184,96a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,96Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,128Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,160Z"></path></svg>
|
||||
<h2>Description</h2>
|
||||
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
|
||||
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<p>{{ image.post_description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="info-tab">
|
||||
<div class="info-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2"></path><path d="M144,176a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176Zm88-48A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128ZM124,96a12,12,0,1,0-12-12A12,12,0,0,0,124,96Z"></path></svg>
|
||||
<h2>Info</h2>
|
||||
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
|
||||
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<div class="img-colours">
|
||||
{% for col in image.image_colours %}
|
||||
<span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Image ID</td>
|
||||
<td>{{ image['id'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author</td>
|
||||
<td>{{ image.author_username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upload date</td>
|
||||
<td><span class="time">{{ image.created_at }}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if group and image.author_id == g.user.id %}
|
||||
<div class="img-groups">
|
||||
{% for group in image.groups %}
|
||||
<a href="/group/{{ group.id }}" class="tag-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,88V200.89a7.11,7.11,0,0,1-7.11,7.11H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6l27.74,20.8a8,8,0,0,0,4.8,1.6H216A8,8,0,0,1,224,88Z" opacity="0.2"></path><path d="M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216Z"></path></svg>
|
||||
{{ group['name'] }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
<button class="tag-icon" id="#img-group">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path></svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{% elif image.author_id == g.user.id %}
|
||||
<div class="img-groups">
|
||||
<button class="tag-icon" id="#img-group">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path></svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% for tag in image.image_exif %}
|
||||
<div class="info-tab">
|
||||
<div class="info-header">
|
||||
{% if tag == 'Photographer' %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M104,40a24,24,0,1,1,24,24A24,24,0,0,1,104,40Zm108.49,99.51L167.17,88.13a24,24,0,0,0-18-8.13H106.83a24,24,0,0,0-18,8.13L43.51,139.51a12,12,0,0,0,17,17L96,128,73.13,214.93a12,12,0,0,0,21.75,10.14L128,168l33.12,57.07a12,12,0,0,0,21.75-10.14L160,128l35.51,28.49a12,12,0,0,0,17-17Z" opacity="0.2"></path><path d="M160,40a32,32,0,1,0-32,32A32,32,0,0,0,160,40ZM128,56a16,16,0,1,1,16-16A16,16,0,0,1,128,56Zm90.34,78.05L173.17,82.83a32,32,0,0,0-24-10.83H106.83a32,32,0,0,0-24,10.83L37.66,134.05a20,20,0,0,0,28.13,28.43l16.3-13.08L65.55,212.28A20,20,0,0,0,102,228.8l26-44.87,26,44.87a20,20,0,0,0,36.41-16.52L173.91,149.4l16.3,13.08a20,20,0,0,0,28.13-28.43Zm-11.51,16.77a4,4,0,0,1-5.66,0c-.21-.2-.42-.4-.65-.58L165,121.76A8,8,0,0,0,152.26,130L175.14,217a7.72,7.72,0,0,0,.48,1.35,4,4,0,1,1-7.25,3.38,6.25,6.25,0,0,0-.33-.63L134.92,164a8,8,0,0,0-13.84,0L88,221.05a6.25,6.25,0,0,0-.33.63,4,4,0,0,1-2.26,2.07,4,4,0,0,1-5-5.45,7.72,7.72,0,0,0,.48-1.35L103.74,130A8,8,0,0,0,91,121.76L55.48,150.24c-.23.18-.44.38-.65.58a4,4,0,1,1-5.66-5.65c.12-.12.23-.24.34-.37L94.83,93.41a16,16,0,0,1,12-5.41h42.34a16,16,0,0,1,12,5.41l45.32,51.39c.11.13.22.25.34.37A4,4,0,0,1,206.83,150.82Z"></path></svg>
|
||||
<h2>Photographer</h2>
|
||||
{% elif tag == 'Camera' %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,64H176L160,40H96L80,64H48A16,16,0,0,0,32,80V192a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V80A16,16,0,0,0,208,64ZM128,168a36,36,0,1,1,36-36A36,36,0,0,1,128,168Z" opacity="0.2"></path><path d="M208,56H180.28L166.65,35.56A8,8,0,0,0,160,32H96a8,8,0,0,0-6.65,3.56L75.71,56H48A24,24,0,0,0,24,80V192a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V80A24,24,0,0,0,208,56Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H80a8,8,0,0,0,6.66-3.56L100.28,48h55.43l13.63,20.44A8,8,0,0,0,176,72h32a8,8,0,0,1,8,8ZM128,88a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,88Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,128,160Z"></path></svg>
|
||||
<h2>Camera</h2>
|
||||
{% elif tag == 'Software' %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,48V208a8,8,0,0,1-8,8H152a8,8,0,0,1-8-8V48a8,8,0,0,1,8-8h80A8,8,0,0,1,240,48Z" opacity="0.2"></path><path d="M24,96v72a8,8,0,0,0,8,8h80a8,8,0,0,1,0,16H96v16h16a8,8,0,0,1,0,16H64a8,8,0,0,1,0-16H80V192H32A24,24,0,0,1,8,168V96A24,24,0,0,1,32,72h80a8,8,0,0,1,0,16H32A8,8,0,0,0,24,96ZM208,64H176a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16Zm0,32H176a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16Zm40-48V208a16,16,0,0,1-16,16H152a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h80A16,16,0,0,1,248,48ZM232,208V48H152V208h80Zm-40-40a12,12,0,1,0,12,12A12,12,0,0,0,192,168Z"></path></svg>
|
||||
<h2>Software</h2>
|
||||
{% elif tag == 'File' %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M104,152l48,72H24l36-56,16.36,25.45ZM152,32V88h56Z" opacity="0.2"></path><path d="M110.66,147.56a8,8,0,0,0-13.32,0L76.49,178.85l-9.76-15.18a8,8,0,0,0-13.46,0l-36,56A8,8,0,0,0,24,232H152a8,8,0,0,0,6.66-12.44ZM38.65,216,60,182.79l9.63,15a8,8,0,0,0,6.67,3.67A7.91,7.91,0,0,0,83,197.89l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg>
|
||||
<h2>File</h2>
|
||||
{% else %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M104,152l48,72H24l36-56,16.36,25.45ZM152,32V88h56Z" opacity="0.2"></path><path d="M110.66,147.56a8,8,0,0,0-13.32,0L76.49,178.85l-9.76-15.18a8,8,0,0,0-13.46,0l-36,56A8,8,0,0,0,24,232H152a8,8,0,0,0,6.66-12.44ZM38.65,216,60,182.79l9.63,15a8,8,0,0,0,6.67,3.67A7.91,7.91,0,0,0,83,197.89l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg>
|
||||
<h2>{{ tag }}</h2>
|
||||
{% endif %}
|
||||
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
|
||||
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<table>
|
||||
{% for subtag in image.image_exif[tag] %}
|
||||
<tr>
|
||||
<td>{{ subtag }}</td>
|
||||
{% if image.image_exif[tag][subtag]['formatted'] %}
|
||||
{% if image.image_exif[tag][subtag]['type'] == 'date' %}
|
||||
<td><span class="time">{{ image.image_exif[tag][subtag]['formatted'] }}</span></td>
|
||||
{% else %}
|
||||
<td>{{ image.image_exif[tag][subtag]['formatted'] }}</td>
|
||||
{% endif %}
|
||||
{% elif image.image_exif[tag][subtag]['raw'] %}
|
||||
<td>{{ image.image_exif[tag][subtag]['raw'] }}</td>
|
||||
{% else %}
|
||||
<td class="empty-table">Oops, an error</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
var infoCollapse = document.querySelectorAll('#collapse-info');
|
||||
for (var i = 0; i < infoCollapse.length; i++) {
|
||||
infoCollapse[i].addEventListener('click', function() {
|
||||
this.parentNode.classList.toggle('image-info__collapsed');
|
||||
});
|
||||
}
|
||||
var infoHeader = document.querySelectorAll('.image-info__header');
|
||||
for (var i = 0; i < infoHeader.length; i++) {
|
||||
infoHeader[i].addEventListener('click', function() {
|
||||
this.parentNode.classList.toggle('image-info__collapsed');
|
||||
var infoTab = document.querySelectorAll('.info-tab');
|
||||
for (var i = 0; i < infoTab.length; i++) {
|
||||
infoTab[i].querySelector('.info-header').addEventListener('click', function() {
|
||||
this.parentNode.classList.toggle('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
$('.image-fullscreen').click(function() {
|
||||
$('.image-fullscreen').addClass('image-fullscreen__hide');
|
||||
// un-Stop scrolling
|
||||
document.querySelector("html").style.overflow = "auto";
|
||||
let fullscreen = document.querySelector('.image-fullscreen')
|
||||
|
||||
fullscreen.classList.remove('active');
|
||||
|
||||
setTimeout(function() {
|
||||
$('.image-fullscreen').removeClass('image-fullscreen__active image-fullscreen__hide');
|
||||
fullscreen.style.display = 'none';
|
||||
}, 200);
|
||||
});
|
||||
$('#img-fullscreen').click(function() {
|
||||
$('.image-fullscreen').addClass('image-fullscreen__active');
|
||||
|
||||
if ($('.image-fullscreen img').attr('src') == '') {
|
||||
$('.image-fullscreen img').attr('src', '/api/uploads/{{ image['file_name'] }}');
|
||||
}
|
||||
// Stop scrolling
|
||||
document.querySelector("html").style.overflow = "hidden";
|
||||
let fullscreen = document.querySelector('.image-fullscreen')
|
||||
|
||||
fullscreen.querySelector('img').src = '/api/file/{{ image.file_name }}';
|
||||
fullscreen.style.display = 'flex';
|
||||
|
||||
setTimeout(function() {
|
||||
fullscreen.classList.add('active');
|
||||
}, 10);
|
||||
});
|
||||
|
||||
$('#img-share').click(function() {
|
||||
|
@ -243,28 +258,22 @@
|
|||
addNotification("Failed to copy link! Are you on HTTP?", 2);
|
||||
}
|
||||
});
|
||||
$('#img-download').click(function() {
|
||||
addNotification("Download started!", 4);
|
||||
});
|
||||
|
||||
{% if g.user['id'] == image['author_id'] %}
|
||||
$('#img-delete').click(function() {
|
||||
popUpShow(
|
||||
'DESTRUCTION!!!!!!',
|
||||
'This will delete the image and all of its data!!! This action is irreversible!!!!! Are you sure you want to do this?????',
|
||||
'<button class="pop-up__btn pop-up__btn-critical-fill" onclick="deleteImage()">Dewww eeeet!</button>',
|
||||
'<img src="/api/uploads/{{ image['file_name'] }}?w=1000&h=1000" />'
|
||||
'<button class="btn-block" onclick="popupDissmiss()">Nooooooo</button>\
|
||||
<button class="btn-block critical" onclick="deleteImage()">Dewww eeeet!</button>',
|
||||
'<img src="/api/file/{{ image.file_name }}?w=1920&h=1080"/>'
|
||||
);
|
||||
});
|
||||
$('#img-edit').click(function() {
|
||||
window.location.href = '/image/{{ image['id'] }}/edit';
|
||||
});
|
||||
|
||||
function deleteImage() {
|
||||
popupDissmiss();
|
||||
|
||||
$.ajax({
|
||||
url: '/api/remove/{{ image['id'] }}',
|
||||
url: '{{ url_for('api.delete_image', image_id=image['id']) }}',
|
||||
type: 'post',
|
||||
data: {
|
||||
action: 'delete'
|
||||
|
@ -277,6 +286,10 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#img-edit').click(function() {
|
||||
window.location.href = '/image/{{ image.id }}/edit';
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,73 +1,59 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block header %}
|
||||
<div class="banner">
|
||||
<img src="{{ url_for('static', filename='images/leaves.jpg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span></span>
|
||||
|
||||
<div class="banner__content">
|
||||
{% block banner_subtitle%}{% endblock %}
|
||||
<p>{{ motto }}</p>
|
||||
<h1>{{ name }}</h1>
|
||||
<p>Serving {{ image_count }} images</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block nav_home %}navigation-item__selected{% endblock %}
|
||||
{% block wrapper_class %}index-wrapper{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="gallery">
|
||||
{% for image in images %}
|
||||
<a id="image-{{ image['id'] }}" class="gallery__item" href="/image/{{ image['id'] }}">
|
||||
<span class="gallery__item-info">
|
||||
<p>{{ image['id'] }}</p>
|
||||
<h2><span class="time">{{ image['created_at'] }}</span></h2>
|
||||
</span>
|
||||
<img
|
||||
class="gallery__item-image"
|
||||
data-src="{{ image['file_name'] }}"
|
||||
onload="imgFade(this)"
|
||||
style="opacity:0;"
|
||||
/>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="banner">
|
||||
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span class="banner-filter"></span>
|
||||
|
||||
<div class="banner-content">
|
||||
<p>{{ config.WEBSITE.motto }}</p>
|
||||
<h1>{{ config.WEBSITE.name }}</h1>
|
||||
<p>Serving {{ images|length }} images</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if images %}
|
||||
<div class="gallery-grid">
|
||||
{% for image in images %}
|
||||
<a id="image-{{ image.id }}" class="gallery-item" href="/image/{{ image.id }}" style="background-color: rgb({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }})">
|
||||
<div class="image-filter">
|
||||
<p class="image-subtitle"></p>
|
||||
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
|
||||
</div>
|
||||
<img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="imgFade(this)" style="opacity:0;" id="lazy-load"/>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-text">
|
||||
<h1>No images</h1>
|
||||
{% if g.user %}
|
||||
<p>You can get started by uploading an image!</p>
|
||||
{% else %}
|
||||
<p>Login to start uploading images!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
function loadOnView() {
|
||||
var images = document.querySelectorAll('.gallery__item-image');
|
||||
for (var i = 0; i < images.length; i++) {
|
||||
var image = images[i];
|
||||
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
|
||||
if (!image.src) {
|
||||
image.src = `/api/uploads/${image.getAttribute('data-src')}?w=500&h=500`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
window.onload = function() {
|
||||
loadOnView();
|
||||
};
|
||||
window.onscroll = function() {
|
||||
loadOnView();
|
||||
};
|
||||
window.onresize = function() {
|
||||
loadOnView();
|
||||
};
|
||||
|
||||
if (document.referrer.includes('image')) {
|
||||
var referrerId = document.referrer.split('/').pop();
|
||||
|
||||
var imgOffset = document.getElementById('image-' + referrerId).offsetTop;
|
||||
var imgHeight = document.getElementById('image-' + referrerId).offsetHeight;
|
||||
var windowHeight = window.innerHeight;
|
||||
|
||||
document.querySelector('html').style.scrollBehavior = 'auto';
|
||||
window.scrollTo(0, imgOffset + (imgHeight / 2) - (windowHeight / 2));
|
||||
document.querySelector('html').style.scrollBehavior = 'smooth';
|
||||
try {
|
||||
var referrerId = document.referrer.split('/').pop();
|
||||
|
||||
var imgOffset = document.getElementById('image-' + referrerId).offsetTop;
|
||||
var imgHeight = document.getElementById('image-' + referrerId).offsetHeight;
|
||||
var windowHeight = window.innerHeight;
|
||||
|
||||
document.querySelector('html').style.scrollBehavior = 'auto';
|
||||
window.scrollTo(0, imgOffset + (imgHeight / 2) - (windowHeight / 2));
|
||||
document.querySelector('html').style.scrollBehavior = 'smooth';
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -3,42 +3,84 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gallery</title>
|
||||
<link rel="icon" href="{{url_for('static', filename='images/icon.png')}}">
|
||||
|
||||
<title>{{ config.WEBSITE.name }}</title>
|
||||
<meta name="description" content="{{ config.WEBSITE.motto }}"/>
|
||||
<meta name="author" content="{{ config.WEBSITE.author }}"/>
|
||||
|
||||
<meta property="og:title" content="{{ config.WEBSITE.name }}"/>
|
||||
<meta property="og:description" content="{{ config.WEBSITE.motto }}"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
|
||||
<meta name="twitter:title" content="{{ config.WEBSITE.name }}"/>
|
||||
<meta name="twitter:description" content="{{ config.WEBSITE.motto }}"/>
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<link
|
||||
href="{{url_for('static', filename='images/logo-black.svg')}}"
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
media="(prefers-color-scheme: light)"/>
|
||||
<link
|
||||
href="{{url_for('static', filename='images/logo-white.svg')}}"
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
media="(prefers-color-scheme: dark)"/>
|
||||
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='theme/style.css')}}" defer>
|
||||
<script src="{{url_for('static', filename='js/jquery-3.6.3.min.js')}}"></script>
|
||||
|
||||
{% assets "js_all" %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="notifications"></div>
|
||||
|
||||
<svg class="top-of-page" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,120H176v88a8,8,0,0,1-8,8H88a8,8,0,0,1-8-8V120H32l96-96Z" opacity="0.2"></path><path d="M229.66,114.34l-96-96a8,8,0,0,0-11.32,0l-96,96A8,8,0,0,0,32,128H72v80a16,16,0,0,0,16,16h80a16,16,0,0,0,16-16V128h40a8,8,0,0,0,5.66-13.66ZM176,112a8,8,0,0,0-8,8v88H88V120a8,8,0,0,0-8-8H51.31L128,35.31,204.69,112Z"></path></svg>
|
||||
|
||||
<div class="pop-up">
|
||||
<span class="pop-up__click-off" onclick="popupDissmiss()"></span>
|
||||
<div class="pop-up-wrapper">
|
||||
<div class="pop-up-content">
|
||||
<h3>Title</h3>
|
||||
<p>Very very very drawn out example description</p>
|
||||
</div>
|
||||
<div class="pop-up-controlls">
|
||||
<button class="pop-up__btn" onclick="popupDissmiss()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="notifications"></div>
|
||||
|
||||
<svg class="jumpUp" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor">
|
||||
<path d="M11 8.414V14a1 1 0 0 1-2 0V8.414L6.464 10.95A1 1 0 1 1 5.05 9.536l4.243-4.243a.997.997 0 0 1 1.414 0l4.243 4.243a1 1 0 1 1-1.414 1.414L11 8.414zM10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"></path>
|
||||
</svg>
|
||||
|
||||
{% block header %}{% endblock %}
|
||||
|
||||
<div class="navigation">
|
||||
<img src="{{url_for('static', filename='images/icon.png')}}" alt="Logo" class="logo" onload="imgFade(this)" style="opacity:0;">
|
||||
|
||||
<a href="{{url_for('gallery.index')}}" class="navigation-item {% block nav_home %}{% endblock %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" width="24" fill="currentColor">
|
||||
<path d="M2 8v10h12V8H2zm2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-2v4a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2zm2 0h8a2 2 0 0 1 2 2v4h2V2H6v4zm0 9a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path><path d="M7 6a3 3 0 1 1 6 0h-2a1 1 0 0 0-2 0H7zm1.864 13.518l2.725-4.672a1 1 0 0 1 1.6-.174l1.087 1.184 1.473-1.354-1.088-1.183a3 3 0 0 0-4.8.52L7.136 18.51l1.728 1.007zm6.512-12.969a2.994 2.994 0 0 1 3.285.77l1.088 1.183-1.473 1.354-1.087-1.184A1 1 0 0 0 16 8.457V8c0-.571-.24-1.087-.624-1.451z"></path>
|
||||
</svg>
|
||||
<span>Home</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,40H80a8,8,0,0,0-8,8V176a8,8,0,0,0,8,8H96.69l77.65-77.66a8,8,0,0,1,11.32,0L216,136.69V48A8,8,0,0,0,208,40Zm-88,64a16,16,0,1,1,16-16A16,16,0,0,1,120,104Z" opacity="0.2"></path><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96Zm32-32H116l64-64,28,28v36Zm-88-64A24,24,0,1,0,96,88,24,24,0,0,0,120,112Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,120,80Z"></path></svg>
|
||||
<span>
|
||||
Home
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{url_for('gallery.groups')}}" class="navigation-item {% block nav_groups %}{% endblock %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -4 24 24" width="24" fill="currentColor">
|
||||
<path d="M17 4H9.415l-.471-1.334A1.001 1.001 0 0 0 8 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm-6.17-2H17a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5c1.306 0 2.417.835 2.83 2z"></path>
|
||||
</svg>
|
||||
<span>Groups</span>
|
||||
<a href="{{url_for('group.groups')}}" class="navigation-item {% block nav_groups %}{% endblock %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,88v24H146.42a8.07,8.07,0,0,0-4.44,1.34l-20,13.32a8.07,8.07,0,0,1-4.44,1.34H69.42A8,8,0,0,0,62,133L32,208V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6l27.74,20.8a8,8,0,0,0,4.8,1.6H200A8,8,0,0,1,208,88Z" opacity="0.2"></path><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64l27.73,20.8a16.12,16.12,0,0,0,9.6,3.2H200v16H146.43a16,16,0,0,0-8.88,2.69l-20,13.31H69.42a15.94,15.94,0,0,0-14.86,10.06L40,166.46V64Zm112,136H43.82l25.6-64h48.16a16,16,0,0,0,8.88-2.69l20-13.31H232Z"></path></svg>
|
||||
<span>
|
||||
Groups
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{% if g.user %}
|
||||
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="showUpload()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 24 24" width="24" fill="currentColor">
|
||||
<path d="M8 3.414v5.642a1 1 0 1 1-2 0V3.414L4.879 4.536A1 1 0 0 1 3.464 3.12L6.293.293a1 1 0 0 1 1.414 0l2.829 2.828A1 1 0 1 1 9.12 4.536L8 3.414zM1 12h12a1 1 0 0 1 0 2H1a1 1 0 0 1 0-2z"></path>
|
||||
</svg>
|
||||
<span>Upload</span>
|
||||
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,80H88l40-40Z" opacity="0.2"></path><path d="M224,152v56a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V152a8,8,0,0,1,16,0v56H208V152a8,8,0,0,1,16,0ZM80.61,83.06a8,8,0,0,1,1.73-8.72l40-40a8,8,0,0,1,11.32,0l40,40A8,8,0,0,1,168,88H136v64a8,8,0,0,1-16,0V88H88A8,8,0,0,1,80.61,83.06ZM107.31,72h41.38L128,51.31Z"></path></svg>
|
||||
<span>
|
||||
Upload
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
|
@ -46,53 +88,55 @@
|
|||
|
||||
{% if g.user %}
|
||||
<a href="{{url_for('gallery.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" width="24" fill="currentColor">
|
||||
<path d="M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-14a4 4 0 0 1 4 4v2a4 4 0 1 1-8 0V8a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0V8a2 2 0 0 0-2-2zM5.91 16.876a8.033 8.033 0 0 1-1.58-1.232 5.57 5.57 0 0 1 2.204-1.574 1 1 0 1 1 .733 1.86c-.532.21-.993.538-1.358.946zm8.144.022a3.652 3.652 0 0 0-1.41-.964 1 1 0 1 1 .712-1.868 5.65 5.65 0 0 1 2.284 1.607 8.032 8.032 0 0 1-1.586 1.225z"></path>
|
||||
</svg>
|
||||
<span>Profile</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,176a24,24,0,1,1-24-24A24,24,0,0,1,216,176Z" opacity="0.2"></path><path d="M214.61,198.62a32,32,0,1,0-45.23,0,40,40,0,0,0-17.11,23.32,8,8,0,0,0,5.67,9.79A8.15,8.15,0,0,0,160,232a8,8,0,0,0,7.73-5.95C170.56,215.42,180.54,208,192,208s21.44,7.42,24.27,18.05a8,8,0,1,0,15.46-4.11A40,40,0,0,0,214.61,198.62ZM192,160a16,16,0,1,1-16,16A16,16,0,0,1,192,160Zm24-88H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.61A15.4,15.4,0,0,0,39.38,216h81.18a8,8,0,0,0,0-16H40V88H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72ZM92.69,56l16,16H40V56Z"></path></svg>
|
||||
<span>
|
||||
Profile
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{url_for('settings.general')}}" class="navigation-item {% block nav_settings %}{% endblock %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -2 24 24" width="24" fill="currentColor">
|
||||
<path d="M9.815 3.094a3.467 3.467 0 0 1-2.78-1.09l-.084-.001a3.467 3.467 0 0 1-2.781 1.09 3.477 3.477 0 0 1-1.727 2.51 3.471 3.471 0 0 1 0 2.794 3.477 3.477 0 0 1 1.727 2.51 3.467 3.467 0 0 1 2.78 1.09h.084a3.467 3.467 0 0 1 2.78-1.09 3.477 3.477 0 0 1 1.727-2.51 3.471 3.471 0 0 1 0-2.794 3.477 3.477 0 0 1-1.726-2.51zM14 5.714a1.474 1.474 0 0 0 0 2.572l-.502 1.684a1.473 1.473 0 0 0-1.553 2.14l-1.443 1.122A1.473 1.473 0 0 0 8.143 14l-2.304-.006a1.473 1.473 0 0 0-2.352-.765l-1.442-1.131A1.473 1.473 0 0 0 .5 9.968L0 8.278a1.474 1.474 0 0 0 0-2.555l.5-1.69a1.473 1.473 0 0 0 1.545-2.13L3.487.77A1.473 1.473 0 0 0 5.84.005L8.143 0a1.473 1.473 0 0 0 2.358.768l1.444 1.122a1.473 1.473 0 0 0 1.553 2.14L14 5.714zm-5.812 9.198a7.943 7.943 0 0 0 2.342-.73 3.468 3.468 0 0 1-.087.215 3.477 3.477 0 0 1 1.727 2.51 3.467 3.467 0 0 1 2.78 1.09h.084a3.467 3.467 0 0 1 2.78-1.09 3.477 3.477 0 0 1 1.727-2.51 3.471 3.471 0 0 1 0-2.794 3.477 3.477 0 0 1-1.726-2.51 3.467 3.467 0 0 1-2.78-1.09h-.084l-.015.016a8.077 8.077 0 0 0 .002-2.016L16.144 6a1.473 1.473 0 0 0 2.358.768l1.444 1.122a1.473 1.473 0 0 0 1.553 2.14L22 11.714a1.474 1.474 0 0 0 0 2.572l-.502 1.684a1.473 1.473 0 0 0-1.553 2.14l-1.443 1.122a1.473 1.473 0 0 0-2.359.768l-2.304-.006a1.473 1.473 0 0 0-2.352-.765l-1.442-1.131a1.473 1.473 0 0 0-1.545-2.13l-.312-1.056zM7 10a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M207.86,123.18l16.78-21a99.14,99.14,0,0,0-10.07-24.29l-26.7-3a81,81,0,0,0-6.81-6.81l-3-26.71a99.43,99.43,0,0,0-24.3-10l-21,16.77a81.59,81.59,0,0,0-9.64,0l-21-16.78A99.14,99.14,0,0,0,77.91,41.43l-3,26.7a81,81,0,0,0-6.81,6.81l-26.71,3a99.43,99.43,0,0,0-10,24.3l16.77,21a81.59,81.59,0,0,0,0,9.64l-16.78,21a99.14,99.14,0,0,0,10.07,24.29l26.7,3a81,81,0,0,0,6.81,6.81l3,26.71a99.43,99.43,0,0,0,24.3,10l21-16.77a81.59,81.59,0,0,0,9.64,0l21,16.78a99.14,99.14,0,0,0,24.29-10.07l3-26.7a81,81,0,0,0,6.81-6.81l26.71-3a99.43,99.43,0,0,0,10-24.3l-16.77-21A81.59,81.59,0,0,0,207.86,123.18ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z" opacity="0.2"></path><path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8.06,8.06,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8.06,8.06,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z"></path></svg>
|
||||
<span>
|
||||
Settings
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -3 24 24" fill="currentColor">
|
||||
<path d="M6.641 9.828H1a1 1 0 1 1 0-2h5.641l-1.12-1.12a1 1 0 0 1 1.413-1.415L9.763 8.12a.997.997 0 0 1 0 1.415l-2.829 2.828A1 1 0 0 1 5.52 10.95l1.121-1.122zM13 0a1 1 0 0 1 1 1v16a1 1 0 0 1-2 0V1a1 1 0 0 1 1-1z"></path>
|
||||
</svg>
|
||||
<span>Login</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M136,128,96,168V88Z" opacity="0.2"></path><path d="M141.66,122.34l-40-40A8,8,0,0,0,88,88v32H24a8,8,0,0,0,0,16H88v32a8,8,0,0,0,13.66,5.66l40-40A8,8,0,0,0,141.66,122.34ZM104,148.69V107.31L124.69,128ZM208,48V208a16,16,0,0,1-16,16H136a8,8,0,0,1,0-16h56V48H136a8,8,0,0,1,0-16h56A16,16,0,0,1,208,48Z"></path></svg>
|
||||
<span>
|
||||
Login
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="content {% block wrapper_class %}{% endblock %}">
|
||||
{% if g.user %}
|
||||
<div class="upload-panel">
|
||||
<span class="click-off" onclick="closeUploadTab()"></span>
|
||||
<div class="container">
|
||||
<h3>Upload stuffs</h3>
|
||||
<p>May the world see your stuff 👀</p>
|
||||
<form id="uploadForm" onsubmit="return uploadFile(event)">
|
||||
<input class="input-block file" type="file" id="file"/>
|
||||
<input class="input-block" type="text" placeholder="alt" id="alt"/>
|
||||
<input class="input-block" type="text" placeholder="description" id="description"/>
|
||||
<input class="input-block" type="text" placeholder="tags" id="tags"/>
|
||||
<button class="btn-block primary" type="submit">Upload</button>
|
||||
</form>
|
||||
<div class="upload-jobs"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="pop-up">
|
||||
<span class="pop-up__click-off" onclick="popupDissmiss()"></span>
|
||||
<div class="pop-up-wrapper">
|
||||
<div class="pop-up-content">
|
||||
<h3>Title</h3>
|
||||
<p>Very very very drawn out example description</p>
|
||||
</div>
|
||||
<div class="pop-up-controlls">
|
||||
<button class="pop-up__btn" onclick="popupDissmiss()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
{% if g.user %}
|
||||
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
|
||||
{% else %}
|
||||
<script src="{{ url_for('static', filename='js/login.js') }}"></script>
|
||||
{% endif %}
|
||||
<script>
|
||||
{% for message in get_flashed_messages() %}
|
||||
// Show notifications on page load
|
||||
|
|
|
@ -1,25 +1,18 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block header %}
|
||||
<div class="background-decoration">
|
||||
<img src="{{ url_for('static', filename='images/background.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span></span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block nav_profile %}navigation-item__selected{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="banner">
|
||||
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span></span>
|
||||
|
||||
<div class="banner-content">
|
||||
<h1>Profile</h1>
|
||||
<p>Hello {{ g.user['username'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>User</h1>
|
||||
<p>{{user_id}}</p>
|
||||
<ul>
|
||||
{% if g.user %}
|
||||
<li><span>{{ g.user['username'] }}</span>
|
||||
<li><a href="{{ url_for('auth.logout') }}">Log Out</a>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for('auth.register') }}">Register</a>
|
||||
<li><a href="{{ url_for('auth.login') }}">Log In</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
{% block settings_account %}settings-nav__item-selected{% endblock %}
|
||||
{% block settings_content %}
|
||||
<h2>Account</h2>
|
||||
<a href="{{ url_for( 'auth.logout' ) }}">Logout</a>
|
||||
{% endblock %}
|
|
@ -1,21 +1,18 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block header %}
|
||||
{% block nav_settings %}navigation-item__selected{% endblock %}
|
||||
{% block content %}
|
||||
<div class="banner">
|
||||
<img src="{{ url_for('static', filename='images/leaves.jpg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<span></span>
|
||||
|
||||
<div class="banner__content">
|
||||
<div class="banner-content">
|
||||
{% block banner_subtitle%}{% endblock %}
|
||||
<h1>Settings</h1>
|
||||
<p>All the red buttons in one place, what could go wrong?</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block nav_settings %}navigation-item__selected{% endblock %}
|
||||
{% block wrapper_class %}settings-wrapper{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings-nav">
|
||||
<a href="{{ url_for('settings.general') }}" class="settings-nav__item {% block settings_general %}{% endblock %}">General</a>
|
||||
<a href="{{ url_for('settings.server') }}" class="settings-nav__item {% block settings_server %}{% endblock %}">Server</a>
|
||||
|
|
|
@ -17,3 +17,9 @@
|
|||
100%
|
||||
left: 0%
|
||||
height: 0
|
||||
|
||||
@keyframes uploadingLoop
|
||||
0%
|
||||
left: -100%
|
||||
100%
|
||||
left: 100%
|
93
gallery/themes/default/components/banner.sass
Normal file
93
gallery/themes/default/components/banner.sass
Normal file
|
@ -0,0 +1,93 @@
|
|||
.banner
|
||||
width: 100%
|
||||
height: 40vh
|
||||
|
||||
position: relative
|
||||
|
||||
background-color: $black
|
||||
color: $black
|
||||
|
||||
overflow: hidden
|
||||
transition: opacity 0.3s ease-in-out
|
||||
|
||||
img
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-color: $black
|
||||
|
||||
object-fit: cover
|
||||
object-position: center center
|
||||
|
||||
.banner-filter
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background: linear-gradient(to right, rgba($primary, 1), rgba($primary, 0))
|
||||
|
||||
z-index: +1
|
||||
|
||||
.banner-content
|
||||
padding: 1rem
|
||||
|
||||
width: 100%
|
||||
height: inherit
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
gap: 0.5rem
|
||||
|
||||
z-index: +2
|
||||
|
||||
h1
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 6.9rem
|
||||
font-weight: 700
|
||||
line-height: 1
|
||||
text-align: left
|
||||
|
||||
color: $black
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
line-height: 1
|
||||
text-align: left
|
||||
|
||||
color: $black
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.banner
|
||||
width: 100vw
|
||||
height: 25vh
|
||||
|
||||
.banner-content
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
h1
|
||||
font-size: 3.5rem
|
||||
text-align: center
|
||||
|
||||
p
|
||||
font-size: 1.1rem
|
||||
text-align: center
|
120
gallery/themes/default/components/buttons/block.sass
Normal file
120
gallery/themes/default/components/buttons/block.sass
Normal file
|
@ -0,0 +1,120 @@
|
|||
@mixin btn-block($color)
|
||||
background-color: transparent
|
||||
color: $color
|
||||
|
||||
&:hover
|
||||
background-color: $color
|
||||
color: $black
|
||||
|
||||
&:focus-visible
|
||||
outline: 2px solid rgba($color, 0.5)
|
||||
|
||||
.btn-block
|
||||
padding: 0.5rem 1rem
|
||||
|
||||
width: auto
|
||||
min-height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
position: relative
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
|
||||
background-color: transparent
|
||||
color: $white
|
||||
border: none
|
||||
border-radius: $rad-inner
|
||||
|
||||
cursor: pointer
|
||||
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
|
||||
|
||||
&:hover
|
||||
background-color: $black
|
||||
|
||||
&:focus-visible
|
||||
outline: 2px solid rgba($black, 0.5)
|
||||
|
||||
&.primary
|
||||
@include btn-block($primary)
|
||||
|
||||
&.critical
|
||||
@include btn-block($critical)
|
||||
|
||||
&.warning
|
||||
@include btn-block($warning)
|
||||
|
||||
&.success
|
||||
@include btn-block($success)
|
||||
|
||||
&.info
|
||||
@include btn-block($info)
|
||||
|
||||
|
||||
.input-block
|
||||
padding: 0.5rem 1rem
|
||||
|
||||
width: auto
|
||||
min-height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
|
||||
position: relative
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
text-align: left
|
||||
|
||||
background-color: transparent
|
||||
color: $white
|
||||
|
||||
border: none
|
||||
border-bottom: 3px solid $gray
|
||||
border-radius: $rad-inner
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
|
||||
|
||||
&:not(:focus):not([value=""]):not(:placeholder-shown)
|
||||
border-color: $white
|
||||
|
||||
&:hover
|
||||
border-color: $white
|
||||
|
||||
&:focus
|
||||
border-color: $primary
|
||||
outline: none
|
||||
|
||||
&.file
|
||||
padding: 0 1rem 0 0
|
||||
border: none
|
||||
background-color: $black2
|
||||
|
||||
&:focus-visible
|
||||
outline: 2px solid rgba($white, 0.5)
|
||||
|
||||
&::file-selector-button
|
||||
margin-right: 1rem
|
||||
padding: 0.5rem 1rem
|
||||
|
||||
width: auto
|
||||
height: 100%
|
||||
|
||||
font-size: 1rem
|
||||
text-decoration: none
|
||||
|
||||
background-color: $white
|
||||
color: $black
|
||||
border: none
|
||||
border-radius: $rad-inner
|
||||
|
||||
&:not([value=""])
|
||||
display: none
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
.tool-tip
|
||||
opacity: 1
|
||||
top: -2.5rem
|
||||
top: -2.7rem
|
||||
transform: translateX(calc(-50% + 1.25rem ))
|
||||
|
||||
.pill__critical
|
||||
|
@ -84,7 +84,7 @@
|
|||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
background-color: $black2
|
||||
background-color: #000000
|
||||
color: $white
|
||||
opacity: 0
|
||||
border-radius: $rad-inner
|
||||
|
@ -92,3 +92,19 @@
|
|||
transition: opacity 0.2s cubic-bezier(.76,0,.17,1), top 0.2s cubic-bezier(.76,0,.17,1)
|
||||
|
||||
pointer-events: none
|
||||
|
||||
svg
|
||||
margin: 0
|
||||
font-size: 1rem
|
||||
|
||||
width: 0.75rem
|
||||
height: 0.75rem
|
||||
|
||||
display: block
|
||||
|
||||
position: absolute
|
||||
left: 50%
|
||||
bottom: -0.46rem
|
||||
transform: translateX(-50%)
|
||||
|
||||
color: #000000
|
|
@ -1,6 +1,6 @@
|
|||
.jumpUp
|
||||
.top-of-page
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
padding: 0.55rem
|
||||
|
||||
width: 2.5rem
|
||||
height: 2.5rem
|
||||
|
@ -28,10 +28,10 @@
|
|||
background-color: $black
|
||||
color: $primary
|
||||
|
||||
.jumpUp--show
|
||||
right: 0.75rem
|
||||
opacity: 1
|
||||
&.show
|
||||
right: 0.75rem
|
||||
opacity: 1
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.jumpUp
|
||||
.top-of-page
|
||||
bottom: 4.25rem
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
animation: notificationTimeout 5.1s linear
|
||||
|
||||
@each $name, $colour in (success: $succes, error: $critical, warning: $warning, info: $info)
|
||||
@each $name, $colour in (success: $success, error: $critical, warning: $warning, info: $info)
|
||||
.sniffle__notification--#{$name}
|
||||
@include notification($colour)
|
||||
|
179
gallery/themes/default/components/elements/pop-up.sass
Normal file
179
gallery/themes/default/components/elements/pop-up.sass
Normal file
|
@ -0,0 +1,179 @@
|
|||
.pop-up
|
||||
width: calc(100% - 3.5rem)
|
||||
height: 100vh
|
||||
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 3.5rem
|
||||
|
||||
display: none
|
||||
|
||||
background-color: rgba($black, 0.8)
|
||||
opacity: 0
|
||||
|
||||
z-index: 101
|
||||
transition: opacity 0.2s ease
|
||||
|
||||
.pop-up__click-off
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
z-index: +1
|
||||
|
||||
.pop-up-wrapper
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 621px
|
||||
height: auto
|
||||
|
||||
position: absolute
|
||||
bottom: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, 50%) scale(0.8)
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
background-color: $black
|
||||
border-radius: $rad
|
||||
overflow: hidden
|
||||
|
||||
z-index: +2
|
||||
transition: transform 0.2s $animation-smooth
|
||||
|
||||
.pop-up-content
|
||||
margin: 0
|
||||
padding: 1rem
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
text-size-adjust: auto;
|
||||
text-overflow: ellipsis
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
position: sticky
|
||||
top: 0
|
||||
|
||||
font-size: 2.25rem
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
line-height: 1
|
||||
|
||||
color: $white
|
||||
background-color: $black
|
||||
|
||||
p
|
||||
margin: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
text-align: center
|
||||
line-height: 1
|
||||
|
||||
color: $white
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 40vh
|
||||
|
||||
border-radius: $rad-inner
|
||||
|
||||
form
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
justify-content: center
|
||||
|
||||
.pop-up-controlls
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: flex-end
|
||||
gap: 0.5rem
|
||||
|
||||
background-color: $black2
|
||||
|
||||
&.active
|
||||
opacity: 1
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translate(-50%, 50%) scale(1)
|
||||
|
||||
.pop-up__link
|
||||
color: $primary
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
cursor: pointer
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.pop-up
|
||||
width: 100%
|
||||
height: 100vh
|
||||
height: 100dvh
|
||||
|
||||
position: fixed
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
.pop-up-wrapper
|
||||
width: calc(100vw - 1rem)
|
||||
max-height: 99vh
|
||||
|
||||
left: 0.5rem
|
||||
bottom: 0.5rem
|
||||
|
||||
border-radius: $rad
|
||||
transform: translateY(5rem)
|
||||
|
||||
.pop-up-content
|
||||
max-height: 100%
|
||||
|
||||
img
|
||||
max-height: 50vh
|
||||
|
||||
.pop-up-controlls
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
|
||||
&.active
|
||||
opacity: 1
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translateY(0)
|
24
gallery/themes/default/components/elements/tags.sass
Normal file
24
gallery/themes/default/components/elements/tags.sass
Normal file
|
@ -0,0 +1,24 @@
|
|||
.tag-icon
|
||||
margin: 0
|
||||
padding: 0.25rem 0.5rem
|
||||
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
gap: 0.25rem
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
text-decoration: none
|
||||
|
||||
border-radius: $rad-inner
|
||||
background-color: $primary
|
||||
color: $black
|
||||
border: none
|
||||
|
||||
cursor: pointer
|
||||
|
||||
svg
|
||||
width: 1.15rem
|
||||
height: 1.15rem
|
202
gallery/themes/default/components/elements/upload-panel.sass
Normal file
202
gallery/themes/default/components/elements/upload-panel.sass
Normal file
|
@ -0,0 +1,202 @@
|
|||
.upload-panel
|
||||
position: fixed
|
||||
left: 3.5rem
|
||||
bottom: 0
|
||||
|
||||
display: none
|
||||
|
||||
width: calc(100% - 3.5rem)
|
||||
height: 100vh
|
||||
|
||||
background-color: rgba($black, 0)
|
||||
|
||||
overflow: hidden
|
||||
z-index: 68
|
||||
transition: background-color 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1.5rem
|
||||
font-weight: 600
|
||||
|
||||
color: $white
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
|
||||
color: $white
|
||||
|
||||
form
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
input, button
|
||||
width: 100%
|
||||
|
||||
.click-off
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
z-index: +1
|
||||
|
||||
|
||||
.container
|
||||
padding: 1rem
|
||||
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: -400px
|
||||
|
||||
width: 400px
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
background-color: $black
|
||||
opacity: 0
|
||||
|
||||
z-index: +2
|
||||
|
||||
transition: left 0.25s cubic-bezier(0.76, 0, 0.17, 1), bottom 0.25s cubic-bezier(0.76, 0, 0.17, 1), opacity 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
.upload-jobs
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
border-radius: $rad
|
||||
|
||||
overflow-y: auto
|
||||
|
||||
.job
|
||||
width: 100%
|
||||
height: 5rem
|
||||
min-height: 5rem
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
background-color: $black2
|
||||
border-radius: $rad
|
||||
|
||||
overflow: hidden
|
||||
|
||||
img
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 5rem
|
||||
|
||||
object-fit: cover
|
||||
|
||||
.img-filter
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-image: linear-gradient(to right, rgba($black, 0.8), rgba($black, 0))
|
||||
|
||||
.job__status
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
position: absolute
|
||||
top: 0.5rem
|
||||
left: 0.5rem
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
color: $white
|
||||
|
||||
z-index: +3
|
||||
|
||||
transition: color 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
.progress
|
||||
width: 100%
|
||||
height: $rad-inner
|
||||
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: -100%
|
||||
|
||||
background-color: $primary
|
||||
|
||||
animation: uploadingLoop 1s cubic-bezier(0.76, 0, 0.17, 1) infinite
|
||||
|
||||
z-index: +5
|
||||
transition: left 1s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
&.critical
|
||||
.job__status, .progress
|
||||
color: $critical
|
||||
&.success
|
||||
.job__status
|
||||
color: $success
|
||||
.progress
|
||||
height: 0
|
||||
animation: none
|
||||
&.warning
|
||||
.job__status, .progress
|
||||
color: $warning
|
||||
|
||||
&.critical, &.success, &.warning
|
||||
.progress
|
||||
height: 0
|
||||
|
||||
&.open
|
||||
background-color: rgba($black, 0.5)
|
||||
|
||||
.container
|
||||
left: 0
|
||||
opacity: 1
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.upload-panel
|
||||
width: 100%
|
||||
height: calc(100vh - 3.5rem)
|
||||
|
||||
left: 0
|
||||
bottom: 3.5rem
|
||||
|
||||
.container
|
||||
width: 100%
|
||||
height: calc(100% - 10rem)
|
||||
|
||||
left: 0
|
||||
bottom: calc(-100vh + 3.5rem)
|
||||
|
||||
border-radius: $rad $rad 0 0
|
||||
|
||||
&.open
|
||||
.container
|
||||
left: 0
|
||||
bottom: 0
|
110
gallery/themes/default/components/gallery.sass
Normal file
110
gallery/themes/default/components/gallery.sass
Normal file
|
@ -0,0 +1,110 @@
|
|||
.gallery-grid
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
|
||||
gap: 0.5rem
|
||||
|
||||
.gallery-item
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
height: auto
|
||||
|
||||
position: relative
|
||||
|
||||
border-radius: $rad
|
||||
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
|
||||
.image-filter
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
left: 0
|
||||
bottom: -1rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
|
||||
background-image: linear-gradient(to bottom, rgba($black, 0), rgba($black, 0.8))
|
||||
|
||||
z-index: +1
|
||||
|
||||
opacity: 0 // hide
|
||||
transform: scale(1.05) // scale up
|
||||
transition: all 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
.image-title
|
||||
margin: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
color: $white
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
opacity: 0 // hide
|
||||
transition: all 0.2s ease-in-out
|
||||
|
||||
.image-subtitle
|
||||
margin: 0
|
||||
|
||||
font-size: 0.8rem
|
||||
font-weight: 500
|
||||
|
||||
color: $white
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
opacity: 0 // hide
|
||||
transition: all 0.2s ease-in-out
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
object-fit: cover
|
||||
object-position: center
|
||||
|
||||
background-color: $white
|
||||
transform: scale(1.05)
|
||||
|
||||
transition: all 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
&:after
|
||||
content: ""
|
||||
display: block
|
||||
padding-bottom: 100%
|
||||
|
||||
&:hover
|
||||
.image-filter
|
||||
bottom: 0
|
||||
opacity: 1
|
||||
transform: scale(1)
|
||||
|
||||
.image-title,
|
||||
.image-subtitle
|
||||
opacity: 1
|
||||
|
||||
img
|
||||
transform: scale(1)
|
||||
|
||||
@media (max-width: 800px)
|
||||
.gallery-grid
|
||||
grid-template-columns: auto auto auto
|
|
@ -1,7 +1,4 @@
|
|||
.background-decoration
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
.background
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
|
@ -17,6 +14,7 @@
|
|||
|
||||
user-select: none
|
||||
overflow: hidden
|
||||
z-index: 1
|
||||
|
||||
img
|
||||
position: absolute
|
||||
|
@ -42,6 +40,4 @@
|
|||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-image: linear-gradient(to bottom, rgba($white, 0), rgba($white, 1))
|
||||
|
||||
z-index: +1
|
||||
z-index: +1
|
42
gallery/themes/default/components/image-view/fullscreen.sass
Normal file
42
gallery/themes/default/components/image-view/fullscreen.sass
Normal file
|
@ -0,0 +1,42 @@
|
|||
.image-fullscreen
|
||||
margin: 0
|
||||
padding: 0 0 0 3.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
height: 100dvh
|
||||
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
display: none
|
||||
opacity: 0 // hide
|
||||
|
||||
background-color: rgba($black, 0.8)
|
||||
z-index: 21
|
||||
|
||||
box-sizing: border-box
|
||||
|
||||
transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 100%
|
||||
|
||||
object-fit: contain
|
||||
object-position: center
|
||||
|
||||
transform: scale(0.8)
|
||||
transition: transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
|
||||
|
||||
&.active
|
||||
opacity: 1 // show
|
||||
|
||||
img
|
||||
transform: scale(1)
|
21
gallery/themes/default/components/image-view/image.sass
Normal file
21
gallery/themes/default/components/image-view/image.sass
Normal file
|
@ -0,0 +1,21 @@
|
|||
.image-container
|
||||
margin: auto
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
overflow: hidden
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 100%
|
||||
|
||||
object-fit: contain
|
||||
object-position: center
|
190
gallery/themes/default/components/image-view/info-tab.sass
Normal file
190
gallery/themes/default/components/image-view/info-tab.sass
Normal file
|
@ -0,0 +1,190 @@
|
|||
.info-container
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
background-color: $black
|
||||
|
||||
overflow-y: auto
|
||||
|
||||
.info-tab
|
||||
width: 100%
|
||||
max-height: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
position: relative
|
||||
|
||||
background-color: $black
|
||||
border-radius: $rad
|
||||
|
||||
transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
&.collapsed
|
||||
height: 2.5rem
|
||||
|
||||
.collapse-indicator
|
||||
transform: rotate(90deg)
|
||||
|
||||
.info-table
|
||||
padding: 0
|
||||
opacity: 0
|
||||
|
||||
.collapse-indicator
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
position: absolute
|
||||
top: 0.6rem
|
||||
right: 0.6rem
|
||||
|
||||
color: $primary
|
||||
|
||||
z-index: +2
|
||||
|
||||
transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
|
||||
user-select: none
|
||||
|
||||
.info-header
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: start
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
position: sticky
|
||||
top: 0
|
||||
z-index: +1
|
||||
|
||||
background: $black2
|
||||
|
||||
svg
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
fill: $primary
|
||||
|
||||
h2
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1.25rem
|
||||
font-weight: 600
|
||||
|
||||
color: $primary
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
.info-table
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
color: $white
|
||||
|
||||
overflow-x: hidden
|
||||
|
||||
transition: opacity 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
table
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
max-width: 100%
|
||||
|
||||
overflow-x: hidden
|
||||
border-collapse: collapse
|
||||
|
||||
tr
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
white-space: nowrap
|
||||
|
||||
td
|
||||
padding-bottom: 0.5rem
|
||||
|
||||
td:first-child
|
||||
padding-right: 0.5rem
|
||||
|
||||
width: 50%
|
||||
max-width: 0
|
||||
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
td:last-child
|
||||
padding: 0
|
||||
|
||||
width: 50%
|
||||
max-width: 0
|
||||
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
|
||||
td.empty-table
|
||||
opacity: 0.3
|
||||
|
||||
tr:last-of-type td
|
||||
padding-bottom: 0
|
||||
|
||||
.img-colours
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
gap: 0.5rem
|
||||
|
||||
span
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 1.5rem
|
||||
height: 1.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
border-radius: 50%
|
||||
// border: 1px solid $white
|
||||
|
||||
.img-groups
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
gap: 0.5rem
|
65
gallery/themes/default/components/image-view/view.sass
Normal file
65
gallery/themes/default/components/image-view/view.sass
Normal file
|
@ -0,0 +1,65 @@
|
|||
@import 'background'
|
||||
@import 'fullscreen'
|
||||
@import 'info-tab'
|
||||
@import 'image'
|
||||
|
||||
|
||||
.image-grid
|
||||
padding: 0
|
||||
|
||||
position: relative
|
||||
|
||||
display: grid
|
||||
grid-template-areas: 'info image' 'info tools'
|
||||
grid-template-columns: 25rem 1fr
|
||||
grid-template-rows: 1fr auto
|
||||
gap: 0
|
||||
|
||||
height: 100vh
|
||||
|
||||
z-index: 3
|
||||
|
||||
#image-info
|
||||
grid-area: info
|
||||
#image-tools
|
||||
grid-area: tools
|
||||
padding: 0 0 0.5rem 0
|
||||
#image-container
|
||||
grid-area: image
|
||||
|
||||
|
||||
@media (max-width: 1100px)
|
||||
.image-grid
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
height: auto
|
||||
|
||||
.image-container
|
||||
margin: 0 auto
|
||||
padding: 0
|
||||
|
||||
max-height: 69vh
|
||||
|
||||
img
|
||||
max-height: 69vh
|
||||
|
||||
#image-tools
|
||||
padding: 0
|
||||
|
||||
.info-container
|
||||
background: transparent
|
||||
|
||||
.info-header
|
||||
border-radius: $rad $rad 0 0
|
||||
|
||||
.info-tab.collapsed .info-header
|
||||
border-radius: $rad
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.image-fullscreen
|
||||
padding: 0 0 3.5rem 0
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
padding: 0
|
||||
|
||||
width: 3.5rem
|
||||
height: 100%
|
||||
height: 100dvh
|
||||
|
||||
display: flex
|
||||
|
@ -22,6 +23,18 @@
|
|||
> span
|
||||
height: 100%
|
||||
|
||||
.logo
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 3.5rem
|
||||
height: 3.5rem
|
||||
min-height: 3.5rem
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
|
||||
.navigation-item
|
||||
margin: 0
|
||||
padding: 1rem
|
||||
|
@ -40,7 +53,7 @@
|
|||
|
||||
text-decoration: none
|
||||
|
||||
svg
|
||||
> svg
|
||||
margin: 0
|
||||
font-size: 1.5rem
|
||||
color: $white
|
||||
|
@ -60,7 +73,7 @@
|
|||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
background-color: $black2
|
||||
background-color: #000000
|
||||
color: $white
|
||||
opacity: 0
|
||||
border-radius: $rad-inner
|
||||
|
@ -69,26 +82,42 @@
|
|||
|
||||
pointer-events: none
|
||||
|
||||
svg
|
||||
margin: 0
|
||||
font-size: 1rem
|
||||
|
||||
width: 0.75rem
|
||||
height: 0.75rem
|
||||
|
||||
display: block
|
||||
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: -0.45rem
|
||||
transform: translateY(-50%)
|
||||
|
||||
color: #000000
|
||||
|
||||
&:hover
|
||||
background-color: $black2
|
||||
|
||||
svg
|
||||
> svg
|
||||
color: $primary
|
||||
|
||||
span
|
||||
opacity: 1
|
||||
left: 3.8rem
|
||||
left: 3.9rem
|
||||
|
||||
.navigation-item__selected
|
||||
background: $primary
|
||||
|
||||
svg
|
||||
> svg
|
||||
color: $black
|
||||
|
||||
&:hover
|
||||
background: $primary
|
||||
|
||||
svg
|
||||
> svg
|
||||
color: $white
|
||||
|
||||
|
||||
|
@ -108,6 +137,9 @@
|
|||
> span
|
||||
display: none
|
||||
|
||||
.logo
|
||||
display: none
|
||||
|
||||
.navigation-item
|
||||
margin: 0.25rem
|
||||
padding: 0
|
|
@ -4,16 +4,20 @@
|
|||
@import "variables"
|
||||
@import "animations"
|
||||
|
||||
@import "ui/notification"
|
||||
@import "ui/pop-up"
|
||||
@import "ui/navigation"
|
||||
@import "ui/content"
|
||||
@import "ui/background"
|
||||
@import "ui/banner"
|
||||
@import "ui/gallery"
|
||||
@import "components/elements/notification"
|
||||
@import "components/elements/pop-up"
|
||||
@import "components/elements/upload-panel"
|
||||
@import "components/elements/tags"
|
||||
|
||||
@import "buttons/jumpUp"
|
||||
@import "buttons/pill"
|
||||
@import "components/navigation"
|
||||
@import "components/banner"
|
||||
@import "components/gallery"
|
||||
|
||||
@import "components/buttons/top-of-page"
|
||||
@import "components/buttons/pill"
|
||||
@import "components/buttons/block"
|
||||
|
||||
@import "components/image-view/view"
|
||||
|
||||
// Reset
|
||||
*
|
||||
|
@ -34,9 +38,91 @@ html, body
|
|||
|
||||
.wrapper
|
||||
margin: 0
|
||||
padding: 0
|
||||
padding: 0 0 0 3.5rem
|
||||
|
||||
min-height: 100vh
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
background-color: $white
|
||||
color: $black
|
||||
|
||||
.big-text
|
||||
height: 20rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
color: $black
|
||||
|
||||
h1
|
||||
margin: 0 2rem
|
||||
|
||||
font-size: 4rem
|
||||
font-weight: 900
|
||||
text-align: center
|
||||
|
||||
p
|
||||
margin: 0 2rem
|
||||
|
||||
max-width: 40rem
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
.error-page
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: $black
|
||||
|
||||
h1
|
||||
margin: 0 2rem
|
||||
|
||||
font-size: 6.9rem
|
||||
font-weight: 900
|
||||
text-align: center
|
||||
|
||||
color: $primary
|
||||
|
||||
p
|
||||
margin: 0 2rem
|
||||
|
||||
max-width: 40rem
|
||||
font-size: 1.25rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
color: $white
|
||||
|
||||
#contrast-check
|
||||
transition: color 0.15s ease-in-out
|
||||
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.wrapper
|
||||
padding: 0 0 3.5rem 0
|
||||
|
||||
.big-text
|
||||
height: calc(75vh - 3.5rem)
|
||||
|
||||
h1
|
||||
font-size: 3.5rem
|
||||
|
||||
.error-page
|
||||
height: calc(100vh - 3.5rem)
|
||||
|
||||
h1
|
||||
font-size: 4.5rem
|
||||
|
||||
p
|
||||
max-width: 100%
|
||||
font-size: 1rem
|
|
@ -1,108 +0,0 @@
|
|||
.banner
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: calc(100vw - 3.5rem)
|
||||
height: 40vh
|
||||
|
||||
position: relative
|
||||
top: 0
|
||||
left: 3.5rem
|
||||
|
||||
background-color: $white
|
||||
color: $black
|
||||
|
||||
background-image: linear-gradient(to right, darken($white, 1%) 15%, darken($white, 10%) 35%, darken($white, 1%) 50%)
|
||||
background-size: 1000px 640px
|
||||
animation: imgLoading 1.8s linear infinite forwards
|
||||
|
||||
overflow: hidden
|
||||
transition: opacity 0.3s ease-in-out
|
||||
|
||||
img
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-color: $white
|
||||
|
||||
object-fit: cover
|
||||
object-position: center center
|
||||
|
||||
span
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background: linear-gradient(to right, rgba($primary, 1), rgba($primary, 0))
|
||||
|
||||
z-index: +1
|
||||
|
||||
.banner__content
|
||||
margin: 0
|
||||
padding: 1rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
gap: 0.5rem
|
||||
|
||||
z-index: +2
|
||||
|
||||
h1
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 6.9rem
|
||||
font-weight: 700
|
||||
line-height: 1
|
||||
text-align: left
|
||||
|
||||
color: $black
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
line-height: 1
|
||||
text-align: left
|
||||
|
||||
color: $black
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.banner
|
||||
width: 100vw
|
||||
height: 25vh
|
||||
|
||||
left: 0
|
||||
|
||||
span
|
||||
background-image: linear-gradient(to bottom, rgba($primary, 1), rgba($primary, 0))
|
||||
|
||||
.banner__content
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
h1
|
||||
font-size: 3.5rem
|
||||
text-align: center
|
||||
|
||||
p
|
||||
font-size: 1.1rem
|
||||
text-align: center
|
|
@ -1,16 +0,0 @@
|
|||
@import "wrappers/index"
|
||||
@import "wrappers/image"
|
||||
@import "wrappers/settings"
|
||||
@import "wrappers/error"
|
||||
|
||||
.content
|
||||
width: calc(100% - 3.5rem)
|
||||
min-height: 100vh
|
||||
|
||||
position: relative
|
||||
left: 3.5rem
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.content
|
||||
width: 100%
|
||||
left: 0
|
|
@ -1,115 +0,0 @@
|
|||
.gallery
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
|
||||
gap: 0.5rem
|
||||
|
||||
@media (max-width: 800px)
|
||||
.gallery
|
||||
grid-template-columns: auto auto auto
|
||||
|
||||
.gallery__item
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
height: auto
|
||||
|
||||
position: relative
|
||||
|
||||
background: linear-gradient(to right, darken($white, 1%) 15%, darken($white, 10%) 35%, darken($white, 1%) 50%)
|
||||
background-size: 1000px 640px
|
||||
animation: imgLoading 1.8s linear infinite forwards
|
||||
border-radius: $rad
|
||||
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
|
||||
&:after
|
||||
content: ""
|
||||
display: block
|
||||
padding-bottom: 100%
|
||||
|
||||
&:hover
|
||||
.gallery__item-info
|
||||
opacity: 1
|
||||
transform: scale(1)
|
||||
|
||||
h2, p
|
||||
opacity: 1
|
||||
|
||||
.gallery__item-image
|
||||
transform: scale(1.1)
|
||||
|
||||
.gallery__item-info
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
|
||||
background-image: linear-gradient(to bottom, rgba($black, 0), rgba($black, 0.8))
|
||||
|
||||
z-index: +1
|
||||
|
||||
opacity: 0 // hide
|
||||
transform: scale(1.05) // scale up
|
||||
transition: all 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
h2
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
color: $primary
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
opacity: 0 // hide
|
||||
transition: all 0.2s ease-in-out
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 0.8rem
|
||||
font-weight: 500
|
||||
|
||||
color: $white
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
opacity: 0 // hide
|
||||
transition: all 0.2s ease-in-out
|
||||
|
||||
.gallery__item-image
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
|
||||
object-fit: cover
|
||||
object-position: center
|
||||
|
||||
background-color: $white
|
||||
|
||||
transition: all 0.3s cubic-bezier(.79, .14, .15, .86)
|
|
@ -1,277 +0,0 @@
|
|||
@mixin pop-up-btn($color, $fill: false)
|
||||
@if $fill
|
||||
color: $white
|
||||
background-color: $color
|
||||
border: 2px solid $color
|
||||
|
||||
&:hover
|
||||
background-color: $white
|
||||
color: $color
|
||||
@else
|
||||
color: $color
|
||||
background-color: $white
|
||||
border: 2px solid $color
|
||||
|
||||
&:hover
|
||||
background-color: $color
|
||||
color: $white
|
||||
|
||||
.pop-up
|
||||
width: calc(100% - 3.5rem)
|
||||
height: 100vh
|
||||
|
||||
position: fixed
|
||||
top: 100%
|
||||
left: 3.5rem
|
||||
|
||||
background-color: rgba($black, 0.8)
|
||||
backdrop-filter: blur(1rem)
|
||||
|
||||
opacity: 0
|
||||
z-index: 101
|
||||
|
||||
transition: opacity 0.2s ease
|
||||
|
||||
.pop-up__click-off
|
||||
width: 100vw
|
||||
height: 100vh
|
||||
height: 100dvh
|
||||
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
z-index: +1
|
||||
|
||||
.pop-up-wrapper
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 621px
|
||||
height: auto
|
||||
|
||||
position: absolute
|
||||
bottom: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, 50%) scale(0.8)
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
background-color: $white
|
||||
border-radius: $rad
|
||||
|
||||
overflow: hidden
|
||||
|
||||
transition: transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
|
||||
z-index: +2
|
||||
|
||||
.pop-up-content
|
||||
margin: 0
|
||||
padding: 0.5rem 0.5rem 0
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
max-height: 50vh
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
text-size-adjust: auto;
|
||||
text-overflow: ellipsis
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
position: sticky
|
||||
top: 0
|
||||
|
||||
font-size: 2.5rem
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
line-height: 1
|
||||
|
||||
color: $black
|
||||
background-color: $white
|
||||
|
||||
p
|
||||
margin: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
text-align: center
|
||||
line-height: 1
|
||||
|
||||
color: $black
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 40vh
|
||||
|
||||
border-radius: $rad-inner
|
||||
|
||||
form
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
justify-content: center
|
||||
|
||||
.pop-up-controlls
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: flex-end
|
||||
gap: 0.5rem
|
||||
|
||||
.pop-up__btn
|
||||
margin: 0
|
||||
padding: 0.5rem 1rem
|
||||
|
||||
width: auto
|
||||
height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
line-height: 1
|
||||
|
||||
border-radius: $rad-inner
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s ease, color 0.2s ease
|
||||
|
||||
@include pop-up-btn($black)
|
||||
|
||||
&:focus
|
||||
outline: none
|
||||
.pop-up__btn-fill
|
||||
@include pop-up-btn($black, true)
|
||||
|
||||
@each $name, $colour in (primary: $primary, info: $info, warning: $warning, critical: $critical)
|
||||
.pop-up__btn-#{$name}
|
||||
@include pop-up-btn($colour)
|
||||
.pop-up__btn-#{$name}-fill
|
||||
@include pop-up-btn($colour, true)
|
||||
|
||||
.pop-up__input
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 2.5rem
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
text-align: left
|
||||
line-height: 1
|
||||
|
||||
border-radius: $rad-inner
|
||||
|
||||
background-color: $white
|
||||
border: 2px solid $black
|
||||
|
||||
&:focus
|
||||
outline: none
|
||||
border-color: $primary
|
||||
|
||||
.pop-up__link
|
||||
color: $primary
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
cursor: pointer
|
||||
|
||||
.pop-up__active
|
||||
opacity: 1
|
||||
top: 0
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translate(-50%, 50%) scale(1)
|
||||
|
||||
.pop-up__hide
|
||||
opacity: 0
|
||||
transition: opacity 0.2s ease
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translate(-50%, 50%) scaleY(0)
|
||||
transition: transform 0.2s ease
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.pop-up
|
||||
width: 100%
|
||||
height: 100vh
|
||||
height: 100dvh
|
||||
|
||||
position: fixed
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
backdrop-filter: blur(0.5rem)
|
||||
|
||||
.pop-up-wrapper
|
||||
width: calc(100vw - 1rem)
|
||||
max-height: calc(100vh - 1rem)
|
||||
max-height: calc(100dvh - 1rem)
|
||||
|
||||
left: 0.5rem
|
||||
bottom: 0.5rem
|
||||
|
||||
border-radius: $rad
|
||||
transform: translateY(5rem)
|
||||
|
||||
.pop-up-content
|
||||
max-height: 100%
|
||||
|
||||
img
|
||||
max-height: 50vh
|
||||
|
||||
.pop-up-controlls
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
|
||||
.pop-up__btn
|
||||
width: 100%
|
||||
|
||||
.pop-up__active
|
||||
opacity: 1
|
||||
top: unset
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translateY(0)
|
||||
|
||||
.pop-up__hide
|
||||
opacity: 0
|
||||
transition: opacity 0.2s ease
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translateY(5rem)
|
||||
transition: transform 0.2s ease
|
|
@ -1,35 +0,0 @@
|
|||
.error-wrapper
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: $black
|
||||
|
||||
h1
|
||||
margin: 0 2rem
|
||||
|
||||
font-size: 6.9rem
|
||||
font-weight: 900
|
||||
text-align: center
|
||||
|
||||
color: $primary
|
||||
|
||||
p
|
||||
margin: 0 2rem
|
||||
|
||||
max-width: 40rem
|
||||
font-size: 1.25rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
color: $white
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.error-wrapper
|
||||
h1
|
||||
font-size: 4.5rem
|
||||
|
||||
p
|
||||
max-width: 100%
|
||||
font-size: 1rem
|
|
@ -1,328 +0,0 @@
|
|||
.image-wrapper
|
||||
padding: 0
|
||||
|
||||
display: grid
|
||||
grid-template-areas: 'info image' 'info tools'
|
||||
grid-template-columns: 25rem 1fr
|
||||
grid-template-rows: 1fr auto
|
||||
gap: 0
|
||||
|
||||
height: 100vh
|
||||
|
||||
.image-fullscreen
|
||||
margin: 0
|
||||
padding: 0 0 0 3.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100dvh
|
||||
|
||||
position: fixed
|
||||
top: -100%
|
||||
left: 0
|
||||
|
||||
display: flex
|
||||
opacity: 0 // hide
|
||||
|
||||
background-color: rgba($black, 0.8)
|
||||
backdrop-filter: blur(1rem)
|
||||
z-index: 21
|
||||
|
||||
box-sizing: border-box
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 100%
|
||||
|
||||
object-fit: contain
|
||||
object-position: center
|
||||
|
||||
transform: scale(0.8)
|
||||
|
||||
.image-fullscreen__active
|
||||
top: 0
|
||||
|
||||
opacity: 1 // show
|
||||
|
||||
transition: opacity 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
img
|
||||
transform: scale(1)
|
||||
transition: transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
|
||||
.image-fullscreen__hide
|
||||
opacity: 0 // hide
|
||||
|
||||
transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
img
|
||||
transform: scaleY(0) // scale(0.8)
|
||||
transition: transform 0.2s ease
|
||||
|
||||
.image-container
|
||||
margin: auto
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
overflow: hidden
|
||||
|
||||
grid-area: image
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 100%
|
||||
|
||||
object-fit: contain
|
||||
object-position: center
|
||||
|
||||
.image-info__container
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
background-color: $black
|
||||
|
||||
overflow-y: auto
|
||||
|
||||
grid-area: info
|
||||
|
||||
.image-info
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
position: relative
|
||||
|
||||
background-color: $black
|
||||
border-radius: $rad
|
||||
|
||||
.image-info__collapse
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
position: absolute
|
||||
top: 0.6rem
|
||||
right: 0.6rem
|
||||
|
||||
cursor: pointer
|
||||
z-index: +2
|
||||
|
||||
transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
svg
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
fill: $primary
|
||||
|
||||
.image-info__header
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: start
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
position: sticky
|
||||
top: 0
|
||||
z-index: +1
|
||||
|
||||
background-image: linear-gradient(to right, rgba($black2, 1), rgba($black, 1))
|
||||
|
||||
svg
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
fill: $primary
|
||||
|
||||
h2
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1.25rem
|
||||
font-weight: 600
|
||||
|
||||
color: $primary
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
.image-info__content
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
color: $white
|
||||
|
||||
overflow-x: hidden
|
||||
|
||||
transition: opacity 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
table
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
max-width: 100%
|
||||
|
||||
overflow-x: hidden
|
||||
border-collapse: collapse
|
||||
|
||||
tr
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
white-space: nowrap
|
||||
|
||||
td
|
||||
padding-bottom: 0.5rem
|
||||
|
||||
td:first-child
|
||||
padding-right: 0.5rem
|
||||
|
||||
width: 50%
|
||||
max-width: 0
|
||||
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
td:last-child
|
||||
padding: 0
|
||||
|
||||
width: 50%
|
||||
max-width: 0
|
||||
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 500
|
||||
|
||||
td.empty-table
|
||||
opacity: 0.3
|
||||
|
||||
tr:last-of-type td
|
||||
padding-bottom: 0
|
||||
|
||||
.image-info__collapsed
|
||||
height: 2.5rem
|
||||
|
||||
.image-info__collapse
|
||||
transform: rotate(90deg)
|
||||
|
||||
.image-info__content
|
||||
padding: 0
|
||||
opacity: 0
|
||||
|
||||
#image-tools
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
grid-area: tools
|
||||
|
||||
@media (max-width: 1100px)
|
||||
.image-wrapper
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex !important
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
height: auto
|
||||
|
||||
.image-container
|
||||
margin: 0 auto
|
||||
padding: 0
|
||||
|
||||
max-height: 69vh
|
||||
|
||||
img
|
||||
max-height: 69vh
|
||||
|
||||
#image-tools
|
||||
margin: 0
|
||||
|
||||
.image-info__container
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
background: none
|
||||
|
||||
.image-info__container
|
||||
border-radius: $rad
|
||||
|
||||
.image-info__header
|
||||
border-radius: $rad $rad 0 0
|
||||
|
||||
.image-info__collapsed
|
||||
.image-info__header
|
||||
border-radius: $rad
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.image-fullscreen
|
||||
padding: 0 0 3.5rem 0
|
||||
|
||||
.image-wrapper
|
||||
padding-bottom: 4rem
|
||||
|
||||
.image-info__header
|
||||
background-image: none
|
||||
background-color: $black2
|
|
@ -1,12 +0,0 @@
|
|||
.index-wrapper
|
||||
padding: 0.5rem
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.index-wrapper
|
||||
padding-bottom: 4rem
|
|
@ -1,115 +0,0 @@
|
|||
@mixin settings-btn($color, $fill: false)
|
||||
@if $fill
|
||||
color: $white
|
||||
background-color: $color
|
||||
border: 2px solid $color
|
||||
|
||||
&:hover
|
||||
background-color: $white
|
||||
color: $color
|
||||
@else
|
||||
color: $color
|
||||
background-color: $white
|
||||
border: 2px solid $color
|
||||
|
||||
&:hover
|
||||
background-color: $color
|
||||
color: $white
|
||||
|
||||
@mixin settings-log($color)
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
color: $white
|
||||
background-color: $black
|
||||
background-image: linear-gradient(120deg, rgba($color, 0.3), rgba($color, 0));
|
||||
//border-left: 3px solid $color
|
||||
|
||||
|
||||
.settings-wrapper
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
|
||||
.settings-nav
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
position: sticky
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: center
|
||||
gap: 0.5rem
|
||||
|
||||
background-color: $white
|
||||
|
||||
.settings-nav__item
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
line-height: 1
|
||||
text-decoration: none
|
||||
|
||||
border-radius: $rad
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s ease, color 0.2s ease
|
||||
|
||||
@include settings-btn($black)
|
||||
|
||||
&:focus
|
||||
outline: none
|
||||
|
||||
.settings-nav__item-selected
|
||||
@include settings-btn($black, true)
|
||||
|
||||
.settings-list
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
.log
|
||||
margin: 0
|
||||
padding: 1rem
|
||||
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
border-radius: $rad
|
||||
|
||||
@include settings-log($critical)
|
||||
|
||||
@media (max-width: 450px)
|
||||
.settings-nav
|
||||
position: relative
|
||||
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
.settings-wrapper
|
||||
padding-bottom: 4rem
|
|
@ -1,5 +1,6 @@
|
|||
$black: #151515
|
||||
$black2: #101010
|
||||
$gray: #666
|
||||
$white: #E8E3E3
|
||||
|
||||
$red: #B66467
|
||||
|
@ -12,18 +13,18 @@ $purple: #A988B0
|
|||
$primary: $green
|
||||
$warning: $orange
|
||||
$critical: $red
|
||||
$succes: $green
|
||||
$success: $green
|
||||
$info: $blue
|
||||
|
||||
$rad: 6px
|
||||
$rad-inner: 3px
|
||||
|
||||
//$font: "Work Sans", sans-serif
|
||||
$animation-smooth: cubic-bezier(0.76, 0, 0.17, 1)
|
||||
$animation-bounce: cubic-bezier(.68,-0.55,.27,1.55)
|
||||
|
||||
$font: "Work Sans", sans-serif
|
||||
$breakpoint: 800px
|
||||
|
||||
$breakpoint: 800px // responsive breakpoint for mobile
|
||||
|
||||
// Work Sans
|
||||
@font-face
|
||||
font-family: 'Work Sans'
|
||||
src: url('fonts/worksans-regular.woff2')
|
||||
|
|
0
gallery/utils/__init__.py
Normal file
0
gallery/utils/__init__.py
Normal file
|
@ -1,17 +1,17 @@
|
|||
"""
|
||||
OnlyLegs - Metatada Parser
|
||||
OnlyLegs - Metadata Parser
|
||||
Parse metadata from images if available
|
||||
otherwise get some basic information from the file
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import TAGS, GPSTAGS
|
||||
from PIL.ExifTags import TAGS
|
||||
|
||||
from .helpers import *
|
||||
from .mapping import *
|
||||
|
||||
|
||||
class Metadata:
|
||||
"""
|
||||
Metadata parser
|
||||
|
@ -54,7 +54,8 @@ class Metadata:
|
|||
return None
|
||||
return self.format_data(self.encoded)
|
||||
|
||||
def format_data(self, encoded_exif): # pylint: disable=R0912 # For now, this is fine
|
||||
@staticmethod
|
||||
def format_data(encoded_exif):
|
||||
"""
|
||||
Formats the data into a dictionary
|
||||
"""
|
||||
|
@ -65,43 +66,18 @@ class Metadata:
|
|||
'File': {},
|
||||
}
|
||||
|
||||
for data in encoded_exif:
|
||||
if data in PHOTOGRAHER_MAPPING:
|
||||
exif['Photographer'][PHOTOGRAHER_MAPPING[data][0]] = {
|
||||
'raw': encoded_exif[data],
|
||||
}
|
||||
elif data in CAMERA_MAPPING:
|
||||
if len(CAMERA_MAPPING[data]) == 2:
|
||||
exif['Camera'][CAMERA_MAPPING[data][0]] = {
|
||||
'raw': encoded_exif[data],
|
||||
'formatted':
|
||||
getattr(helpers, CAMERA_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
|
||||
# Thanks chatGPT xP
|
||||
for key, value in encoded_exif.items():
|
||||
for mapping_name, mapping_val in EXIF_MAPPING:
|
||||
if key in mapping_val:
|
||||
if len(mapping_val[key]) == 2:
|
||||
exif[mapping_name][mapping_val[key][0]] = {
|
||||
'raw': value,
|
||||
'formatted': getattr(helpers, mapping_val[key][1])(value), # pylint: disable=E0602
|
||||
}
|
||||
else:
|
||||
exif['Camera'][CAMERA_MAPPING[data][0]] = {
|
||||
'raw': encoded_exif[data],
|
||||
}
|
||||
elif data in SOFTWARE_MAPPING:
|
||||
if len(SOFTWARE_MAPPING[data]) == 2:
|
||||
exif['Software'][SOFTWARE_MAPPING[data][0]] = {
|
||||
'raw': encoded_exif[data],
|
||||
'formatted':
|
||||
getattr(helpers, SOFTWARE_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
|
||||
}
|
||||
else:
|
||||
exif['Software'][SOFTWARE_MAPPING[data][0]] = {
|
||||
'raw': encoded_exif[data],
|
||||
}
|
||||
elif data in FILE_MAPPING:
|
||||
if len(FILE_MAPPING[data]) == 2:
|
||||
exif['File'][FILE_MAPPING[data][0]] = {
|
||||
'raw': encoded_exif[data],
|
||||
'formatted':
|
||||
getattr(helpers, FILE_MAPPING[data][1])(encoded_exif[data]), # pylint: disable=E0602
|
||||
}
|
||||
else:
|
||||
exif['File'][FILE_MAPPING[data][0]] = {
|
||||
'raw': encoded_exif[data]
|
||||
else:
|
||||
exif[mapping_name][mapping_val[key][0]] = {
|
||||
'raw': value,
|
||||
}
|
||||
|
||||
# Remove empty keys
|
|
@ -4,6 +4,7 @@ Metadata formatting helpers
|
|||
"""
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def human_size(value):
|
||||
"""
|
||||
Formats the size of a file in a human readable format
|
||||
|
@ -215,13 +216,6 @@ def scene_capture_type(value):
|
|||
return None
|
||||
|
||||
|
||||
def scene_type(value): # pylint: disable=W0613 # Itss fiiiineeee
|
||||
"""
|
||||
Maps the value of the scene type to a human readable format
|
||||
"""
|
||||
return 'Directly photographed image'
|
||||
|
||||
|
||||
def white_balance(value):
|
||||
"""
|
||||
Maps the value of the white balance to a human readable format
|
||||
|
@ -283,7 +277,10 @@ def lens_specification(value):
|
|||
"""
|
||||
Maps the value of the lens specification to a human readable format
|
||||
"""
|
||||
return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm'
|
||||
try:
|
||||
return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm'
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def compression_type(value):
|
||||
|
@ -352,6 +349,7 @@ def orientation(value):
|
|||
Maps the value of the orientation to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: 'Undefined',
|
||||
1: 'Horizontal (normal)',
|
||||
2: 'Mirror horizontal',
|
||||
3: 'Rotate 180',
|
|
@ -2,6 +2,7 @@
|
|||
OnlyLegs - Metatada Parser
|
||||
Mapping for metadata
|
||||
"""
|
||||
|
||||
PHOTOGRAHER_MAPPING = {
|
||||
'Artist': ['Artist'],
|
||||
'UserComment': ['Comment'],
|
||||
|
@ -31,7 +32,7 @@ CAMERA_MAPPING = {
|
|||
'ISOSpeedRatings': ['ISO Speed Ratings', 'iso'],
|
||||
'ISOSpeed': ['ISO Speed', 'iso'],
|
||||
'SensitivityType': ['Sensitivity Type', 'sensitivity_type'],
|
||||
'ExposureBiasValue': ['Exposure Bias', 'ev'],
|
||||
'ExposureBiasValue': ['Exposure Bias', 'exposure'],
|
||||
'ExposureTime': ['Exposure Time', 'shutter'],
|
||||
'ExposureMode': ['Exposure Mode', 'exposure_mode'],
|
||||
'ExposureProgram': ['Exposure Program', 'exposure_program'],
|
||||
|
@ -40,9 +41,6 @@ CAMERA_MAPPING = {
|
|||
'MeteringMode': ['Metering Mode', 'metering_mode'],
|
||||
'LightSource': ['Light Source', 'light_source'],
|
||||
'SceneCaptureType': ['Scene Capture Type', 'scene_capture_type'],
|
||||
'SceneType': ['Scene Type', 'scene_type'],
|
||||
'Rating': ['Rating', 'rating'],
|
||||
'RatingPercent': ['Rating Percent', 'rating_percent'],
|
||||
}
|
||||
SOFTWARE_MAPPING = {
|
||||
'Software': ['Software'],
|
||||
|
@ -59,4 +57,13 @@ FILE_MAPPING = {
|
|||
'XResolution': ['X-resolution'],
|
||||
'YResolution': ['Y-resolution'],
|
||||
'ResolutionUnit': ['Resolution Units', 'resolution_unit'],
|
||||
'Rating': ['Rating', 'rating'],
|
||||
'RatingPercent': ['Rating Percent', 'rating_percent'],
|
||||
}
|
||||
|
||||
EXIF_MAPPING = [
|
||||
('Photographer', PHOTOGRAHER_MAPPING),
|
||||
('Camera', CAMERA_MAPPING),
|
||||
('Software', SOFTWARE_MAPPING),
|
||||
('File', FILE_MAPPING)
|
||||
]
|
|
@ -8,7 +8,7 @@ from datetime import datetime
|
|||
import sass
|
||||
|
||||
|
||||
class CompileTheme():
|
||||
class CompileTheme:
|
||||
"""
|
||||
Compiles the theme into the static folder
|
||||
"""
|
||||
|
@ -27,13 +27,16 @@ class CompileTheme():
|
|||
print("Theme does not exist!")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.exists(theme_dest):
|
||||
os.makedirs(theme_dest)
|
||||
|
||||
self.load_sass(theme_path, theme_dest)
|
||||
self.load_fonts(theme_path, theme_dest)
|
||||
|
||||
now = datetime.now()
|
||||
print(f"{now.hour}:{now.minute}:{now.second} - Done!\n")
|
||||
print(f"{datetime.now().hour}:{datetime.now().minute}:{datetime.now().second} - Done!\n")
|
||||
|
||||
def load_sass(self, source_path, css_dest):
|
||||
@staticmethod
|
||||
def load_sass(source_path, css_dest):
|
||||
"""
|
||||
Compile the sass (or scss) file into css and save it to the static folder
|
||||
"""
|
||||
|
@ -45,7 +48,7 @@ class CompileTheme():
|
|||
print("No sass file found!")
|
||||
sys.exit(1)
|
||||
|
||||
with open(os.path.join(css_dest, 'style.css'), 'w', encoding='utf-8') as file:
|
||||
with open(os.path.join(css_dest, 'style.css'), encoding='utf-8', mode='w+') as file:
|
||||
try:
|
||||
file.write(sass.compile(filename=sass_path,output_style='compressed'))
|
||||
except sass.CompileError as err:
|
||||
|
@ -54,7 +57,8 @@ class CompileTheme():
|
|||
|
||||
print("Compiled successfully!")
|
||||
|
||||
def load_fonts(self, source_path, font_dest):
|
||||
@staticmethod
|
||||
def load_fonts(source_path, font_dest):
|
||||
"""
|
||||
Copy the fonts folder to the static folder
|
||||
"""
|
||||
|
@ -63,7 +67,6 @@ class CompileTheme():
|
|||
font_dest = os.path.join(font_dest, 'fonts')
|
||||
|
||||
if os.path.exists(font_dest):
|
||||
print("Updating fonts...")
|
||||
try:
|
||||
shutil.rmtree(font_dest)
|
||||
except Exception as err:
|
||||
|
@ -72,7 +75,8 @@ class CompileTheme():
|
|||
|
||||
try:
|
||||
shutil.copytree(source_path, font_dest)
|
||||
print("Copied new fonts!")
|
||||
except Exception as err:
|
||||
print("Failed to copy fonts!\n", err)
|
||||
sys.exit(1)
|
||||
|
||||
print("Fonts copied successfully!")
|
176
poetry.lock
generated
176
poetry.lock
generated
|
@ -2,14 +2,14 @@
|
|||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "2.14.2"
|
||||
version = "2.15.0"
|
||||
description = "An abstract syntax tree for Python with inference support."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7.2"
|
||||
files = [
|
||||
{file = "astroid-2.14.2-py3-none-any.whl", hash = "sha256:0e0e3709d64fbffd3037e4ff403580550f14471fd3eaae9fa11cc9a5c7901153"},
|
||||
{file = "astroid-2.14.2.tar.gz", hash = "sha256:a3cf9f02c53dd259144a7e8f3ccd75d67c9a8c716ef183e0c1f291bc5d7bb3cf"},
|
||||
{file = "astroid-2.15.0-py3-none-any.whl", hash = "sha256:e3e4d0ffc2d15d954065579689c36aac57a339a4679a679579af6401db4d3fdb"},
|
||||
{file = "astroid-2.15.0.tar.gz", hash = "sha256:525f126d5dc1b8b0b6ee398b33159105615d92dc4a17f2cd064125d57f6186fa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -112,6 +112,18 @@ files = [
|
|||
{file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachelib"
|
||||
version = "0.9.0"
|
||||
description = "A collection of cache libraries in the same API interface."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cachelib-0.9.0-py3-none-any.whl", hash = "sha256:811ceeb1209d2fe51cd2b62810bd1eccf70feba5c52641532498be5c675493b3"},
|
||||
{file = "cachelib-0.9.0.tar.gz", hash = "sha256:38222cc7c1b79a23606de5c2607f4925779e37cdcea1c2ad21b8bae94b5425a5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
|
@ -192,6 +204,38 @@ Werkzeug = ">=2.2.2"
|
|||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-assets"
|
||||
version = "2.0"
|
||||
description = "Asset management for Flask, to compress and merge CSS and Javascript files."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Flask-Assets-2.0.tar.gz", hash = "sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2"},
|
||||
{file = "Flask_Assets-2.0-py3-none-any.whl", hash = "sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=0.8"
|
||||
webassets = ">=2.0"
|
||||
|
||||
[[package]]
|
||||
name = "flask-caching"
|
||||
version = "2.0.2"
|
||||
description = "Adds caching support to Flask applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Flask-Caching-2.0.2.tar.gz", hash = "sha256:24b60c552d59a9605cc1b6a42c56cdb39a82a28dab4532bbedb9222ae54ecb4e"},
|
||||
{file = "Flask_Caching-2.0.2-py3-none-any.whl", hash = "sha256:19571f2570e9b8dd9dd9d2f49d7cbee69c14ebe8cc001100b1eb98c379dd80ad"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cachelib = ">=0.9.0,<0.10.0"
|
||||
Flask = "<3"
|
||||
|
||||
[[package]]
|
||||
name = "flask-compress"
|
||||
version = "1.13"
|
||||
|
@ -305,14 +349,14 @@ tornado = ["tornado (>=0.2)"]
|
|||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "6.0.0"
|
||||
version = "6.1.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"},
|
||||
{file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"},
|
||||
{file = "importlib_metadata-6.1.0-py3-none-any.whl", hash = "sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"},
|
||||
{file = "importlib_metadata-6.1.0.tar.gz", hash = "sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -597,14 +641,14 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
|
|||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "3.1.0"
|
||||
version = "3.1.1"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
|
||||
{file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
|
||||
{file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"},
|
||||
{file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -613,18 +657,18 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytes
|
|||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "2.16.3"
|
||||
version = "2.17.0"
|
||||
description = "python code static checker"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7.2"
|
||||
files = [
|
||||
{file = "pylint-2.16.3-py3-none-any.whl", hash = "sha256:3e803be66e3a34c76b0aa1a3cf4714b538335e79bd69718d34fcf36d8fff2a2b"},
|
||||
{file = "pylint-2.16.3.tar.gz", hash = "sha256:0decdf8dfe30298cd9f8d82e9a1542da464db47da60e03641631086671a03621"},
|
||||
{file = "pylint-2.17.0-py3-none-any.whl", hash = "sha256:e097d8325f8c88e14ad12844e3fe2d963d3de871ea9a8f8ad25ab1c109889ddc"},
|
||||
{file = "pylint-2.17.0.tar.gz", hash = "sha256:1460829b6397cb5eb0cdb0b4fc4b556348e515cdca32115f74a1eb7c20b896b4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
astroid = ">=2.14.2,<=2.16.0-dev0"
|
||||
astroid = ">=2.15.0,<=2.17.0-dev0"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
dill = [
|
||||
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
||||
|
@ -708,14 +752,14 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "67.4.0"
|
||||
version = "67.6.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"},
|
||||
{file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"},
|
||||
{file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"},
|
||||
{file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -725,53 +769,53 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
|
|||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.4"
|
||||
version = "2.0.7"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b67d6e626caa571fb53accaac2fba003ef4f7317cb3481e9ab99dad6e89a70d6"},
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b01dce097cf6f145da131a53d4cce7f42e0bfa9ae161dd171a423f7970d296d0"},
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738c80705e11c1268827dbe22c01162a9cdc98fc6f7901b429a1459db2593060"},
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6363697c938b9a13e07f1bc2cd433502a7aa07efd55b946b31d25b9449890621"},
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a42e6831e82dfa6d16b45f0c98c69e7b0defc64d76213173456355034450c414"},
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:011ef3c33f30bae5637c575f30647e0add98686642d237f0c3a1e3d9b35747fa"},
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-win32.whl", hash = "sha256:c1e8edc49b32483cd5d2d015f343e16be7dfab89f4aaf66b0fa6827ab356880d"},
|
||||
{file = "SQLAlchemy-2.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:77a380bf8721b416782c763e0ff66f80f3b05aee83db33ddfc0eac20bcb6791f"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a2f9120eb32190bdba31d1022181ef08f257aed4f984f3368aa4e838de72bc0"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:679b9bd10bb32b8d3befed4aad4356799b6ec1bdddc0f930a79e41ba5b084124"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:582053571125895d008d4b8d9687d12d4bd209c076cdbab3504da307e2a0a2bd"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c82395e2925639e6d320592943608070678e7157bd1db2672a63be9c7889434"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:25e4e54575f9d2af1eab82d3a470fca27062191c48ee57b6386fe09a3c0a6a33"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9946ee503962859f1a9e1ad17dff0859269b0cb453686747fe87f00b0e030b34"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-win32.whl", hash = "sha256:c621f05859caed5c0aab032888a3d3bde2cae3988ca151113cbecf262adad976"},
|
||||
{file = "SQLAlchemy-2.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:662a79e80f3e9fe33b7861c19fedf3d8389fab2413c04bba787e3f1139c22188"},
|
||||
{file = "SQLAlchemy-2.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3f927340b37fe65ec42e19af7ce15260a73e11c6b456febb59009bfdfec29a35"},
|
||||
{file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67901b91bf5821482fcbe9da988cb16897809624ddf0fde339cd62365cc50032"},
|
||||
{file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1644c603558590f465b3fa16e4557d87d3962bc2c81fd7ea85b582ecf4676b31"},
|
||||
{file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9a7ecaf90fe9ec8e45c86828f4f183564b33c9514e08667ca59e526fea63893a"},
|
||||
{file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8a88b32ce5b69d18507ffc9f10401833934ebc353c7b30d1e056023c64f0a736"},
|
||||
{file = "SQLAlchemy-2.0.4-cp37-cp37m-win32.whl", hash = "sha256:2267c004e78e291bba0dc766a9711c389649cf3e662cd46eec2bc2c238c637bd"},
|
||||
{file = "SQLAlchemy-2.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:59cf0cdb29baec4e074c7520d7226646a8a8f856b87d8300f3e4494901d55235"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd801375f19a6e1f021dabd8b1714f2fdb91cbc835cd13b5dd0bd7e9860392d7"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8efdda920988bcade542f53a2890751ff680474d548f32df919a35a21404e3f"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:918c2b553e3c78268b187f70983c9bc6f91e451a4f934827e9c919e03d258bd7"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d05773d5c79f2d3371d81697d54ee1b2c32085ad434ce9de4482e457ecb018"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fdb2686eb01f670cdc6c43f092e333ff08c1cf0b646da5256c1237dc4ceef4ae"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ff0a7c669ec7cdb899eae7e622211c2dd8725b82655db2b41740d39e3cda466"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-win32.whl", hash = "sha256:57dcd9eed52413f7270b22797aa83c71b698db153d1541c1e83d45ecdf8e95e7"},
|
||||
{file = "SQLAlchemy-2.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:54aa9f40d88728dd058e951eeb5ecc55241831ba4011e60c641738c1da0146b7"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:817aab80f7e8fe581696dae7aaeb2ceb0b7ea70ad03c95483c9115970d2a9b00"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc7b9f55c2f72c13b2328b8a870ff585c993ba1b5c155ece5c9d3216fa4b18f6"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f696828784ab2c07b127bfd2f2d513f47ec58924c29cff5b19806ac37acee31c"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce54965a94673a0ebda25e7c3a05bf1aa74fd78cc452a1a710b704bf73fb8402"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f342057422d6bcfdd4996e34cd5c7f78f7e500112f64b113f334cdfc6a0c593d"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5deafb4901618b3f98e8df7099cd11edd0d1e6856912647e28968b803de0dae"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-win32.whl", hash = "sha256:81f1ea264278fcbe113b9a5840f13a356cb0186e55b52168334124f1cd1bc495"},
|
||||
{file = "SQLAlchemy-2.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:954f1ad73b78ea5ba5a35c89c4a5dfd0f3a06c17926503de19510eb9b3857bde"},
|
||||
{file = "SQLAlchemy-2.0.4-py3-none-any.whl", hash = "sha256:0adca8a3ca77234a142c5afed29322fb501921f13d1d5e9fa4253450d786c160"},
|
||||
{file = "SQLAlchemy-2.0.4.tar.gz", hash = "sha256:95a18e1a6af2114dbd9ee4f168ad33070d6317e11bafa28d983cc7b585fe900b"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7917632606fc5d4be661dcde45cc415df835e594e2c50cc999a44f24b6bf6d92"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32f508fef9c5a7d19411d94ef64cf5405e42c4689e51ddbb81ac9a7be045cce8"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0995b92612979d208189245bf87349ad9243b97b49652347a28ddee0803225a"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cebd161f964af58290596523c65e41a5a161a99f7212b1ae675e288a4b5e0a7c"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c38641f5c3714505d65dbbd8fb1350408b9ad8461769ec8e440e1177f9c92d1d"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:921485d1f69ed016e1f756de67d02ad4f143eb6b92b9776bfff78786d8978ab5"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-win32.whl", hash = "sha256:a65a8fd09bdffd63fa23b39cd902e6a4ca23d86ecfe129513e43767a1f3e91fb"},
|
||||
{file = "SQLAlchemy-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:d2e7411d5ea164c6f4d003f5d4f5e72e202956aaa7496b95bb4a4c39669e001c"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:432cfd77642771ee7ea0dd0f3fb664f18506a3625eab6e6d5d1d771569171270"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce076e25f1170000b4ecdc57a1ff8a70dbe4a5648ec3da0563ef3064e8db4f15"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14854bdb2a35af536d14f77dfa8dbc20e1bb1972996d64c4147e0d3165c9aaf5"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9020125e3be677c64d4dda7048e247343f1663089cf268a4cc98c957adb7dbe0"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fb649c5473f79c9a7b6133f53a31f4d87de14755c79224007eb7ec76e628551e"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33f73cc45ffa050f5c3b60ff4490e0ae9e02701461c1600d5ede1b008076b1b9"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-win32.whl", hash = "sha256:0789e199fbce8cb1775337afc631ed12bcc5463dd77d7a06b8dafd758cde51f8"},
|
||||
{file = "SQLAlchemy-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:013f4f330001e84a2b0ef1f2c9bd73169c79d582e54e1a144be1be1dbc911711"},
|
||||
{file = "SQLAlchemy-2.0.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4339110be209fea37a2bb4f35f1127c7562a0393e9e6df5d9a65cc4f5c167cb6"},
|
||||
{file = "SQLAlchemy-2.0.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e61e2e4dfe175dc3510889e44eda1c32f55870d6950ef40519640cb266704d"},
|
||||
{file = "SQLAlchemy-2.0.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d44ff7573016fc26311b5a5c54d5656fb9e0c39e138bc8b81cb7c8667485203"},
|
||||
{file = "SQLAlchemy-2.0.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:57b80e877eb6ec63295835f8a3b86ca3a44829f80c4748e1b019e03adea550fc"},
|
||||
{file = "SQLAlchemy-2.0.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e90f0be674e0845c5c1ccfa5e31c9ee28fd406546a61afc734355cc7ea1f8f8b"},
|
||||
{file = "SQLAlchemy-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:e735a635126b2338dfd3a0863b675437cb53d85885a7602b8cffb24345df33ed"},
|
||||
{file = "SQLAlchemy-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ea1c63e61b5c13161c8468305f0a5837c80aae2070e33654c68dd12572b638eb"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cc337b96ec59ef29907eeadc2ac11188739281568f14c719e61550ca6d201a41"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0eac488be90dd3f7a655d2e34fa59e1305fccabc4abfbd002e3a72ae10bd2f89"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8ab8f90f4a13c979e6c41c9f011b655c1b9ae2df6cffa8fa2c7c4d740f3512e"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc370d53fee7408330099c4bcc2573a107757b203bc61f114467dfe586a0c7bd"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:494db0026918e3f707466a1200a5dedbf254a4bce01a3115fd95f04ba8258f09"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:486015a58c9a67f65a15b4f19468b35b97cee074ae55386a9c240f1da308fbfe"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-win32.whl", hash = "sha256:5f7c40ec2e3b31293184020daba95850832bea523a08496ac89b27a5276ec804"},
|
||||
{file = "SQLAlchemy-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:3da3dff8d9833a7d7f66a3c45a79a3955f775c79f47bb7eea266d0b4c267b17a"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:774965c41b71c8ebe3c5728bf5b9a948231fc3a0422d9fdace0686f5bb689ad6"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94556a2a7fc3de094ea056b62845e2e6e271e26d1e1b2540a1cd2d2506257a10"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f15c54713a8dd57a01c974c9f96476688f6f6374d348819ed7e459535844b614"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea9461f6955f3cf9eff6eeec271686caed7792c76f5b966886a36a42ea46e6b2"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18795e87601b4244fd08b542cd6bff9ef674b17bcd34e4a3c9935398e2cc762c"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0b698440c477c00bdedff87348b19a79630a235864a8f4378098d61079c16ce9"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-win32.whl", hash = "sha256:38e26cf6b9b4c6c37846f7e31b42e4d664b35f055691265f07e06aeb6167c494"},
|
||||
{file = "SQLAlchemy-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:a6f7d1debb233f1567d700ebcdde0781a0b63db0ef266246dfbf75ae41bfdf85"},
|
||||
{file = "SQLAlchemy-2.0.7-py3-none-any.whl", hash = "sha256:fc67667c8e8c04e5c3250ab2cd51df40bc7c28c7c253d0475b377eff86fe4bb0"},
|
||||
{file = "SQLAlchemy-2.0.7.tar.gz", hash = "sha256:a4c1e1582492c66dfacc9eab52738f3e64d9a2a380e412668f75aa06e540f649"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -837,6 +881,18 @@ files = [
|
|||
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webassets"
|
||||
version = "2.0"
|
||||
description = "Media asset management for Python, with glue code for various web frameworks"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "webassets-2.0-py3-none-any.whl", hash = "sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"},
|
||||
{file = "webassets-2.0.tar.gz", hash = "sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "2.2.3"
|
||||
|
@ -958,5 +1014,5 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
|
|||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "de131da70fd04213714611f747ff9102979dbc6855e68645ea93fa83a6d433d8"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "28f7d599f912b9385b4a497f89da59cf2b087ac1ec12c3582e603f47f8e11492"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "onlylegs"
|
||||
version = "23.03.03"
|
||||
version = "23.03.20"
|
||||
description = "Gallery built for fast and simple image management"
|
||||
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
|
||||
license = "MIT"
|
||||
|
@ -9,15 +9,17 @@ readme = ".github/README.md"
|
|||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
Flask = "^2.2.2"
|
||||
flask-compress = "^1.13"
|
||||
gunicorn = "^20.1.0"
|
||||
Flask-Compress = "^1.13"
|
||||
Flask-Caching = "^2.0.2"
|
||||
Flask-Assets = "^2.0"
|
||||
SQLAlchemy = "^2.0.3"
|
||||
python-dotenv = "^0.21.0"
|
||||
gunicorn = "^20.1.0"
|
||||
pyyaml = "^6.0"
|
||||
libsass = "^0.22.0"
|
||||
colorthief = "^0.2.1"
|
||||
Pillow = "^9.4.0"
|
||||
platformdirs = "^3.0.0"
|
||||
SQLAlchemy = "^2.0.3"
|
||||
pylint = "^2.16.3"
|
||||
|
||||
[build-system]
|
||||
|
|
42
run.py
Normal file
42
run.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
Run script for OnlyLegs
|
||||
"""
|
||||
from setup.args import PORT, ADDRESS, WORKERS, DEBUG
|
||||
from setup.configuration import Configuration
|
||||
|
||||
|
||||
print("""
|
||||
___ _ _
|
||||
/ _ \ _ __ | |_ _| | ___ __ _ ___
|
||||
| | | | '_ \| | | | | | / _ \/ _` / __|
|
||||
| |_| | | | | | |_| | |__| __/ (_| \__ \
|
||||
\___/|_| |_|_|\__, |_____\___|\__, |___/
|
||||
|___/ |___/
|
||||
Created by Fluffy Bean - Version 23.03.20
|
||||
""")
|
||||
|
||||
|
||||
Configuration() # Run pre-checks
|
||||
|
||||
|
||||
if DEBUG:
|
||||
from gallery import create_app
|
||||
|
||||
# If no address is specified, use localhost
|
||||
if not ADDRESS:
|
||||
ADDRESS = 'localhost'
|
||||
|
||||
create_app().run(host=ADDRESS, port=PORT, debug=True, threaded=True)
|
||||
else:
|
||||
from setup.runner import OnlyLegs # pylint: disable=C0412
|
||||
|
||||
# If no address is specified, bind the server to all interfaces
|
||||
if not ADDRESS:
|
||||
ADDRESS = '0.0.0.0'
|
||||
|
||||
options = {
|
||||
'bind': f'{ADDRESS}:{PORT}',
|
||||
'workers': WORKERS,
|
||||
}
|
||||
|
||||
OnlyLegs(options).run()
|
27
setup/args.py
Normal file
27
setup/args.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""
|
||||
Startup arguments for the OnlyLegs gallery
|
||||
|
||||
-p, --port: Port to run on (default: 5000)
|
||||
-a, --address: Address to run on (default: For Debug: localhost, For Production: 0.0.0.0)
|
||||
-w, --workers: Number of workers to run (default: 4)
|
||||
|
||||
-d, --debug: Run as Flask app in debug mode (default: False)
|
||||
-S, --scream: Show verbose output (default: False)
|
||||
-h, --help: Show a help message
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='Run the OnlyLegs gallery')
|
||||
parser.add_argument('-p', '--port', type=int, default=5000, help='Port to run on')
|
||||
parser.add_argument('-a', '--address', type=str, default=None, help='Address to run on')
|
||||
parser.add_argument('-w', '--workers', type=int, default=4, help='Number of workers to run')
|
||||
parser.add_argument('-d', '--debug', action='store_true', help='Run as Flask app in debug mode')
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
PORT = args.port
|
||||
ADDRESS = args.address
|
||||
WORKERS = args.workers
|
||||
DEBUG = args.debug
|
163
setup/configuration.py
Normal file
163
setup/configuration.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
"""
|
||||
OnlyLegs - Setup
|
||||
Runs when the app detects that there is no user directory
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import re
|
||||
import platformdirs
|
||||
import yaml
|
||||
|
||||
|
||||
USER_DIR = platformdirs.user_config_dir('onlylegs')
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Setup the application on first run
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Main setup function
|
||||
"""
|
||||
print("Running startup checks...")
|
||||
|
||||
# Check if the user directory exists
|
||||
if not os.path.exists(USER_DIR):
|
||||
self.make_dir()
|
||||
|
||||
# Check if the .env file exists
|
||||
if not os.path.exists(os.path.join(USER_DIR, '.env')):
|
||||
self.make_env()
|
||||
|
||||
# Check if the conf.yml file exists
|
||||
if not os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
|
||||
self.make_yaml()
|
||||
|
||||
# Load the config files
|
||||
self.logging_config()
|
||||
|
||||
@staticmethod
|
||||
def make_dir():
|
||||
"""
|
||||
Create the user directory
|
||||
"""
|
||||
try:
|
||||
os.makedirs(USER_DIR)
|
||||
os.makedirs(os.path.join(USER_DIR, 'instance'))
|
||||
os.makedirs(os.path.join(USER_DIR, 'uploads'))
|
||||
except Exception as err:
|
||||
print("Error creating user directory:", err)
|
||||
sys.exit(1)
|
||||
|
||||
print("Created user directory at:", USER_DIR)
|
||||
|
||||
@staticmethod
|
||||
def make_env():
|
||||
"""
|
||||
Create the .env file with default values
|
||||
"""
|
||||
env_conf = {
|
||||
'FLASK_SECRET': os.urandom(32).hex(),
|
||||
}
|
||||
|
||||
try:
|
||||
with open(os.path.join(USER_DIR, '.env'), encoding='utf-8', mode='w+') as file:
|
||||
for key, value in env_conf.items():
|
||||
file.write(f"{key}={value}\n")
|
||||
except Exception as err:
|
||||
print("Error creating environment variables:", err)
|
||||
sys.exit(1)
|
||||
|
||||
print("""
|
||||
####################################################
|
||||
# A NEW KEY WAS GENERATED FOR YOU! PLEASE NOTE #
|
||||
# DOWN THE FLASK_SECRET KEY LOCATED IN YOUR #
|
||||
# .config/onlylegs/.env FOLDER! LOOSING THIS KEY #
|
||||
# WILL RESULT IN YOU BEING UNABLE TO LOG IN! #
|
||||
####################################################
|
||||
""")
|
||||
|
||||
@staticmethod
|
||||
def make_yaml():
|
||||
"""
|
||||
Create the YAML config file with default values
|
||||
"""
|
||||
is_correct = False
|
||||
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
|
||||
username_regex = re.compile(r'\b[A-Za-z0-9._%+-]+\b')
|
||||
|
||||
print("\nNo config file found, please enter the following information:")
|
||||
while not is_correct:
|
||||
username = input("Admin username: ")
|
||||
name = input("Admin name: ")
|
||||
email = input("Admin email: ")
|
||||
|
||||
# Check if the values are valid
|
||||
if not username or not username_regex.match(username):
|
||||
print("Username is invalid!")
|
||||
continue
|
||||
|
||||
if not name:
|
||||
print("Name is invalid!")
|
||||
continue
|
||||
|
||||
if not email or not email_regex.match(email):
|
||||
print("Email is invalid!")
|
||||
continue
|
||||
|
||||
# Check if user is happy with the values
|
||||
if input("Is this correct? (y/n): ").lower() == 'y':
|
||||
is_correct = True
|
||||
|
||||
yaml_conf = {
|
||||
'admin': {
|
||||
'name': name,
|
||||
'username': username,
|
||||
'email': email,
|
||||
},
|
||||
'upload': {
|
||||
'allowed-extensions': {
|
||||
'jpg': 'jpeg',
|
||||
'jpeg': 'jpeg',
|
||||
'png': 'png',
|
||||
'webp': 'webp',
|
||||
},
|
||||
'max-size': 69,
|
||||
'rename': 'GWA_{{username}}_{{time}}',
|
||||
},
|
||||
'website': {
|
||||
'name': 'OnlyLegs',
|
||||
'motto': 'A gallery built for fast and simple image management!',
|
||||
'language': 'en',
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8', mode='w+') as file:
|
||||
yaml.dump(yaml_conf, file, default_flow_style=False)
|
||||
except Exception as err:
|
||||
print("Error creating default gallery config:", err)
|
||||
sys.exit(1)
|
||||
|
||||
print("Generated config file, you can change these values in the settings of the app")
|
||||
|
||||
@staticmethod
|
||||
def logging_config():
|
||||
"""
|
||||
Set the logging config
|
||||
"""
|
||||
logs_path = os.path.join(platformdirs.user_config_dir('onlylegs'), 'logs')
|
||||
|
||||
if not os.path.isdir(logs_path):
|
||||
os.mkdir(logs_path)
|
||||
print("Created logs directory at:", logs_path)
|
||||
|
||||
logging.getLogger('werkzeug').disabled = True
|
||||
logging.basicConfig(
|
||||
filename=os.path.join(logs_path, 'only.log'),
|
||||
level=logging.INFO,
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',
|
||||
encoding='utf-8')
|
32
setup/runner.py
Normal file
32
setup/runner.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""
|
||||
Gunicorn configuration file
|
||||
"""
|
||||
from gunicorn.app.base import Application
|
||||
from gunicorn import util
|
||||
|
||||
|
||||
class OnlyLegs(Application):
|
||||
"""
|
||||
Gunicorn application
|
||||
"""
|
||||
def __init__(self, options={}): # pylint: disable=W0102, W0231
|
||||
self.usage = None
|
||||
self.callable = None
|
||||
self.options = options
|
||||
self.do_load_config()
|
||||
|
||||
def init(self, *args):
|
||||
"""
|
||||
Initialize the application
|
||||
"""
|
||||
cfg = {}
|
||||
for setting, value in self.options.items():
|
||||
if setting.lower() in self.cfg.settings and value is not None:
|
||||
cfg[setting.lower()] = value
|
||||
return cfg
|
||||
|
||||
def prog(self): # pylint: disable=C0116, E0202
|
||||
return 'OnlyLegs'
|
||||
|
||||
def load(self):
|
||||
return util.import_app('gallery:create_app()')
|
Loading…
Reference in a new issue