Merge pull request #26 from Derpy-Leggies/unstable

Unstable
This commit is contained in:
Michał Gdula 2023-09-24 15:46:43 +01:00 committed by GitHub
commit 38a1b79ac1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 2256 additions and 2437 deletions

View file

@ -1,136 +0,0 @@
"""
Onlylegs Gallery
This is the main app file, it loads all the other files and sets up the app
"""
import os
import logging
from flask_assets import Bundle
from flask_migrate import init as migrate_init
from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash
from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
from onlylegs.models import User
from onlylegs.views import (
index as view_index,
image as view_image,
group as view_group,
settings as view_settings,
profile as view_profile,
)
from onlylegs.api import media as api_media, group as api_group, account as api_account
from onlylegs import auth as view_auth
from onlylegs import gwagwa
def create_app(): # pylint: disable=R0914
"""
Create and configure the main app
"""
app = Flask(__name__, instance_path=INSTANCE_DIR)
app.config.from_pyfile("config.py")
# DATABASE
db.init_app(app)
migrate.init_app(app, db, directory=MIGRATIONS_DIR)
# If database file doesn't exist, create it
if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
print("Creating database")
with app.app_context():
db.create_all()
register_user = User(
username=app.config["ADMIN_CONF"]["username"],
email=app.config["ADMIN_CONF"]["email"],
password=generate_password_hash("changeme!", method="sha256"),
)
db.session.add(register_user)
db.session.commit()
print(
"""
####################################################
# DEFAULY ADMIN USER GENERATED WITH GIVEN USERNAME #
# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #
# PLEASE UPDATE IT IN THE SETTINGS! #
####################################################
"""
)
# Check if migrations directory exists, if not create it
with app.app_context():
if not os.path.exists(MIGRATIONS_DIR):
print("Creating migrations directory")
migrate_init(directory=MIGRATIONS_DIR)
# LOGIN MANAGER
# can also set session_protection to "strong"
# this would protect against session hijacking
login_manager.init_app(app)
login_manager.login_view = "onlylegs.index"
@login_manager.user_loader
def load_user(user_id): # skipcq: PTC-W0065
return User.query.filter_by(alt_id=user_id).first()
@login_manager.unauthorized_handler
def unauthorized(): # skipcq: PTC-W0065
error = 401
msg = "You are not authorized to view this page!!!!"
return render_template("error.html", error=error, msg=msg), error
# ERROR HANDLERS
@app.errorhandler(Exception)
def error_page(err): # skipcq: PTC-W0065
"""
Error handlers, if the error is not a HTTP error, return 500
"""
if not isinstance(err, HTTPException):
abort(500)
return (
render_template("error.html", error=err.code, msg=err.description),
err.code,
)
# ASSETS
assets.init_app(app)
scripts = Bundle(
"js/*.js", output="gen/js.js", depends="js/*.js"
) # filter jsmin is broken :c
styles = Bundle(
"sass/style.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
)
assets.register("scripts", scripts)
assets.register("styles", styles)
# BLUEPRINTS
app.register_blueprint(view_auth.blueprint)
app.register_blueprint(view_index.blueprint)
app.register_blueprint(view_image.blueprint)
app.register_blueprint(view_group.blueprint)
app.register_blueprint(view_profile.blueprint)
app.register_blueprint(view_settings.blueprint)
# APIS
app.register_blueprint(api_media.blueprint)
app.register_blueprint(api_group.blueprint)
app.register_blueprint(api_account.blueprint)
# CACHE AND COMPRESS
cache.init_app(app)
compress.init_app(app)
# Yupee! We got there :3
print("Done!")
logging.info("Gallery started successfully!")
return app

182
onlylegs/api.py Normal file
View file

@ -0,0 +1,182 @@
"""
Onlylegs - API endpoints
"""
import os
import pathlib
import re
import logging
from uuid import uuid4
from flask import (
Blueprint,
abort,
send_from_directory,
jsonify,
request,
current_app,
)
from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import Users, Pictures
from onlylegs.utils.metadata import yoink
from onlylegs.utils.generate_image import generate_thumbnail
blueprint = Blueprint("api", __name__, url_prefix="/api")
@blueprint.route("/account/picture/<int:user_id>", methods=["POST"])
@login_required
def account_picture(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(Users, user_id)
file = request.files.get("file", None)
# If no image is uploaded, return 404 error
if not file:
return jsonify({"error": "No file uploaded"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower()
img_name = str(user.id)
img_path = os.path.join(current_app.config["PFP_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)
return jsonify({"error": "File extension not allowed"}), 403
if user.picture:
# Delete cached files and old image
os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture))
cache_name = user.picture.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
# Save file
try:
file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"error": "Error saving file"}), 500
img_colors = ColorThief(img_path).get_color()
# Save to database
user.colour = img_colors
user.picture = str(img_name + "." + img_ext)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200
@blueprint.route("/account/username/<int:user_id>", methods=["POST"])
@login_required
def account_username(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(Users, user_id)
new_name = request.form["name"]
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
# Validate the form
if not new_name or not username_regex.match(new_name):
return jsonify({"error": "Username is invalid"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Save to database
user.username = new_name
db.session.commit()
return jsonify({"message": "Username changed"}), 200
@blueprint.route("/media/<path:path>", methods=["GET"])
def media(path):
"""
Returns image from media folder
r for resolution, thumb for thumbnail etc
e for extension, jpg, png etc
"""
res = request.args.get("r", "").strip()
ext = request.args.get("e", "").strip()
# if no args are passed, return the raw file
if not res and not ext:
if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)):
abort(404)
return send_from_directory(current_app.config["MEDIA_FOLDER"], path)
# Generate thumbnail, if None is returned a server error occured
thumb = generate_thumbnail(path, res, ext)
if not thumb:
abort(500)
response = send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
response.headers["Cache-Control"] = "public, max-age=31536000"
response.headers["Expires"] = "31536000"
return response
@blueprint.route("/media/upload", methods=["POST"])
@login_required
def upload():
"""
Uploads an image to the server and saves it to the database
"""
form_file = request.files.get("file", None)
form = request.form
if not form_file:
return jsonify({"message": "No file"}), 400
# 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)
return jsonify({"message": "File extension not allowed"}), 403
# Save file
try:
form_file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"message": "Error saving file"}), 500
img_exif = yoink(img_path) # Get EXIF data
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database
query = Pictures(
author_id=current_user.id,
filename=img_name + "." + img_ext,
mimetype=img_ext,
exif=img_exif,
colours=img_colors,
description=form["description"],
alt=form["alt"],
)
db.session.add(query)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200

View file

@ -1,93 +0,0 @@
"""
Onlylegs - API endpoints
"""
import os
import pathlib
import re
import logging
from flask import Blueprint, jsonify, request, current_app
from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import User
blueprint = Blueprint("account_api", __name__, url_prefix="/api/account")
@blueprint.route("/picture/<int:user_id>", methods=["POST"])
@login_required
def account_picture(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(User, user_id)
file = request.files["file"]
# If no image is uploaded, return 404 error
if not file:
return jsonify({"error": "No file uploaded"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower()
img_name = str(user.id)
img_path = os.path.join(current_app.config["PFP_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)
return jsonify({"error": "File extension not allowed"}), 403
if user.picture:
# Delete cached files and old image
os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture))
cache_name = user.picture.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
# Save file
try:
file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"error": "Error saving file"}), 500
img_colors = ColorThief(img_path).get_color()
# Save to database
user.colour = img_colors
user.picture = str(img_name + "." + img_ext)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200
@blueprint.route("/username/<int:user_id>", methods=["POST"])
@login_required
def account_username(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(User, user_id)
new_name = request.form["name"]
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
# Validate the form
if not new_name or not username_regex.match(new_name):
return jsonify({"error": "Username is invalid"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Save to database
user.username = new_name
db.session.commit()
return jsonify({"message": "Username changed"}), 200

View file

@ -1,78 +0,0 @@
"""
Onlylegs - API endpoints
"""
from flask import Blueprint, flash, jsonify, request
from flask_login import login_required, current_user
from onlylegs.extensions import db
from onlylegs.models import Post, Group, GroupJunction
blueprint = Blueprint("group_api", __name__, url_prefix="/api/group")
@blueprint.route("/create", methods=["POST"])
@login_required
def create_group():
"""
Creates a group
"""
new_group = Group(
name=request.form["name"],
description=request.form["description"],
author_id=current_user.id,
)
db.session.add(new_group)
db.session.commit()
return jsonify({"message": "Group created", "id": new_group.id})
@blueprint.route("/modify", methods=["POST"])
@login_required
def modify_group():
"""
Changes the images in a group
"""
group_id = request.form["group"]
image_id = request.form["image"]
action = request.form["action"]
group = db.get_or_404(Group, group_id)
db.get_or_404(Post, image_id) # Check if image exists
if group.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
if (
action == "add"
and not GroupJunction.query.filter_by(
group_id=group_id, post_id=image_id
).first()
):
db.session.add(GroupJunction(group_id=group_id, post_id=image_id))
elif request.form["action"] == "remove":
GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).delete()
db.session.commit()
return jsonify({"message": "Group modified"})
@blueprint.route("/delete", methods=["POST"])
def delete_group():
"""
Deletes a group
"""
group_id = request.form["group"]
group = db.get_or_404(Group, group_id)
if group.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
GroupJunction.query.filter_by(group_id=group_id).delete()
db.session.delete(group)
db.session.commit()
flash(["Group yeeted!", "1"])
return jsonify({"message": "Group deleted"})

View file

@ -1,144 +0,0 @@
"""
Onlylegs - API endpoints
Media upload and retrieval
"""
from uuid import uuid4
import os
import pathlib
import logging
from flask import (
Blueprint,
flash,
abort,
send_from_directory,
jsonify,
request,
current_app,
)
from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import Post, GroupJunction
from onlylegs.utils import metadata as mt
from onlylegs.utils.generate_image import generate_thumbnail
blueprint = Blueprint("media_api", __name__, url_prefix="/api/media")
@blueprint.route("/<path:path>", methods=["GET"])
def media(path):
"""
Returns a file from the uploads folder
r for resolution, thumb for thumbnail etc
e for extension, jpg, png etc
"""
res = request.args.get("r", default=None, type=str)
ext = request.args.get("e", default=None, type=str)
# path = secure_filename(path)
# if no args are passed, return the raw file
if not res and not ext:
if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)):
abort(404)
return send_from_directory(current_app.config["MEDIA_FOLDER"], path)
thumb = generate_thumbnail(path, res, ext)
if not thumb:
abort(500)
return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
@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 jsonify({"message": "No file"}), 400
# 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)
return jsonify({"message": "File extension not allowed"}), 403
# Save file
try:
form_file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"message": "Error saving file"}), 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
query = Post(
author_id=current_user.id,
filename=img_name + "." + img_ext,
mimetype=img_ext,
exif=img_exif,
colours=img_colors,
description=form["description"],
alt=form["alt"],
)
db.session.add(query)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200
@blueprint.route("/delete/<int:image_id>", methods=["POST"])
@login_required
def delete_image(image_id):
"""
Deletes an image from the server and database
"""
post = db.get_or_404(Post, image_id)
# Check if image exists and if user is allowed to delete it (author)
if post.author_id != current_user.id:
logging.info("User %s tried to delete image %s", current_user.id, image_id)
return (
jsonify({"message": "You are not allowed to delete this image, heck off"}),
403,
)
# Delete file
try:
os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], post.filename))
except FileNotFoundError:
logging.warning(
"File not found: %s, already deleted or never existed", post.filename
)
# Delete cached files
cache_name = post.filename.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
GroupJunction.query.filter_by(post_id=image_id).delete()
db.session.delete(post)
db.session.commit()
logging.info("Removed image (%s) %s", image_id, post.filename)
flash(["Image was all in Le Head!", "1"])
return jsonify({"message": "Image deleted"}), 200

138
onlylegs/app.py Normal file
View file

@ -0,0 +1,138 @@
"""
Onlylegs Gallery
This is the main app file, it loads all the other files and sets up the app
"""
import os
import logging
from flask_assets import Bundle
from flask_migrate import init as migrate_init
from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash
from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
from onlylegs.models import Users
from onlylegs.views import (
index as view_index,
image as view_image,
group as view_group,
settings as view_settings,
profile as view_profile,
)
from onlylegs import api
from onlylegs import auth as view_auth
from onlylegs import filters
app = Flask(__name__, instance_path=INSTANCE_DIR)
app.config.from_pyfile("config.py")
# DATABASE
db.init_app(app)
migrate.init_app(app, db, directory=MIGRATIONS_DIR)
# If database file doesn't exist, create it
if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
print("Creating database")
with app.app_context():
db.create_all()
register_user = Users(
username=app.config["ADMIN_CONF"]["username"],
email=app.config["ADMIN_CONF"]["email"],
password=generate_password_hash("changeme!", method="sha256"),
)
db.session.add(register_user)
db.session.commit()
print(
"""
####################################################
# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #
# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #
# PLEASE UPDATE IT IN THE SETTINGS! #
####################################################
"""
)
# Check if migrations directory exists, if not create it
with app.app_context():
if not os.path.exists(MIGRATIONS_DIR):
print("Creating migrations directory")
migrate_init(directory=MIGRATIONS_DIR)
# LOGIN MANAGER
# can also set session_protection to "strong"
# this would protect against session hijacking
login_manager.init_app(app)
login_manager.login_view = "onlylegs.index"
@login_manager.user_loader
def load_user(user_id):
return Users.query.filter_by(alt_id=user_id).first()
@login_manager.unauthorized_handler
def unauthorized():
error = 401
msg = "You are not authorized to view this page!!!!"
return render_template("error.html", error=error, msg=msg), error
# ERROR HANDLERS
@app.errorhandler(Exception)
def error_page(err):
"""
Error handlers, if the error is not a HTTP error, return 500
"""
if not isinstance(err, HTTPException):
abort(500)
return (
render_template("error.html", error=err.code, msg=err.description),
err.code,
)
# ASSETS
assets.init_app(app)
scripts = Bundle(
"js/*.js", output="gen/js.js", depends="js/*.js"
) # filter jsmin is broken :c
styles = Bundle(
"sass/style.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
)
assets.register("scripts", scripts)
assets.register("styles", styles)
# BLUEPRINTS
app.register_blueprint(view_auth.blueprint)
app.register_blueprint(view_index.blueprint)
app.register_blueprint(view_image.blueprint)
app.register_blueprint(view_group.blueprint)
app.register_blueprint(view_profile.blueprint)
app.register_blueprint(view_settings.blueprint)
app.register_blueprint(api.blueprint)
# FILTERS
app.register_blueprint(filters.blueprint)
# CACHE AND COMPRESS
cache.init_app(app)
compress.init_app(app)
# Yupee! We got there :3
print("Done!")
logging.info("Gallery started successfully!")
if __name__ == "__main__":
app.run()

View file

@ -11,7 +11,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
from flask_login import login_user, logout_user, login_required from flask_login import login_user, logout_user, login_required
from onlylegs.extensions import db from onlylegs.extensions import db
from onlylegs.models import User from onlylegs.models import Users
blueprint = Blueprint("auth", __name__, url_prefix="/auth") blueprint = Blueprint("auth", __name__, url_prefix="/auth")
@ -28,7 +28,7 @@ def login():
password = request.form["password"].strip() password = request.form["password"].strip()
remember = bool(request.form["remember-me"]) remember = bool(request.form["remember-me"])
user = User.query.filter_by(username=username).first() user = Users.query.filter_by(username=username).first()
if not user or not check_password_hash(user.password, password): if not user or not check_password_hash(user.password, password):
logging.error("Login attempt from %s", request.remote_addr) logging.error("Login attempt from %s", request.remote_addr)
@ -77,7 +77,7 @@ def register():
elif password_repeat != password: elif password_repeat != password:
error.append("Passwords do not match!") error.append("Passwords do not match!")
user_exists = User.query.filter_by(username=username).first() user_exists = Users.query.filter_by(username=username).first()
if user_exists: if user_exists:
error.append("User already exists!") error.append("User already exists!")
@ -86,7 +86,7 @@ def register():
print(error) print(error)
return jsonify(error), 400 return jsonify(error), 400
register_user = User( register_user = Users(
username=username, username=username,
email=email, email=email,
password=generate_password_hash(password, method="sha256"), password=generate_password_hash(password, method="sha256"),

View file

@ -3,6 +3,7 @@ Gallery configuration file
""" """
import os import os
import platformdirs import platformdirs
import importlib.metadata
from dotenv import load_dotenv from dotenv import load_dotenv
from yaml import safe_load from yaml import safe_load
@ -41,3 +42,6 @@ MEDIA_FOLDER = os.path.join(user_dir, "media")
# Database # Database
INSTANCE_DIR = instance_dir INSTANCE_DIR = instance_dir
MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations") MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations")
# App
APP_VERSION = importlib.metadata.version("OnlyLegs")

View file

@ -13,4 +13,4 @@ migrate = Migrate()
login_manager = LoginManager() login_manager = LoginManager()
assets = Environment() assets = Environment()
compress = Compress() compress = Compress()
cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300}) cache = Cache(config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 300})

20
onlylegs/filters.py Normal file
View file

@ -0,0 +1,20 @@
"""
OnlyLegs filters
Custom Jinja2 filters
"""
from flask import Blueprint
from onlylegs.utils import colour as colour_utils
blueprint = Blueprint("filters", __name__)
@blueprint.app_template_filter()
def colour_contrast(colour):
"""
Pass in the colour of the background and will return
a css variable based on the contrast of text required to be readable
"color: var(--fg-white);" or "color: var(--fg-black);"
"""
colour_obj = colour_utils.Colour(colour)
return "rgb(var(--fg-black));" if colour_obj.is_light() else "rgb(var(--fg-white));"

View file

@ -1,4 +0,0 @@
"""
Gwa Gwa!
"""
print("Gwa Gwa!")

View file

@ -1,4 +0,0 @@
{
"IMAGES_UPLOADED": "%s images uploaded!",
"DONT USE THIS": "variable:format(data), jinja2 doesnt use the same method as Django does, odd"
}

View file

@ -6,18 +6,18 @@ from flask_login import UserMixin
from onlylegs.extensions import db from onlylegs.extensions import db
class GroupJunction(db.Model): # pylint: disable=too-few-public-methods, C0103 class AlbumJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
""" """
Junction table for posts and groups Junction table for picturess and albums
Joins with posts and groups Joins with picturess and albums
""" """
__tablename__ = "group_junction" __tablename__ = "album_junction"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
group_id = db.Column(db.Integer, db.ForeignKey("group.id")) album_id = db.Column(db.Integer, db.ForeignKey("albums.id"))
post_id = db.Column(db.Integer, db.ForeignKey("post.id")) picture_id = db.Column(db.Integer, db.ForeignKey("pictures.id"))
date_added = db.Column( date_added = db.Column(
db.DateTime, db.DateTime,
@ -26,16 +26,15 @@ class GroupJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
) )
class Post(db.Model): # pylint: disable=too-few-public-methods, C0103 class Pictures(db.Model): # pylint: disable=too-few-public-methods, C0103
""" """
Post table Pictures table
""" """
__tablename__ = "post" __tablename__ = "pictures"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
filename = db.Column(db.String, unique=True, nullable=False) filename = db.Column(db.String, unique=True, nullable=False)
mimetype = db.Column(db.String, nullable=False) mimetype = db.Column(db.String, nullable=False)
@ -51,37 +50,37 @@ class Post(db.Model): # pylint: disable=too-few-public-methods, C0103
server_default=db.func.now(), # pylint: disable=E1102 server_default=db.func.now(), # pylint: disable=E1102
) )
junction = db.relationship("GroupJunction", backref="posts") album_fk = db.relationship("AlbumJunction", backref="pictures")
class Group(db.Model): # pylint: disable=too-few-public-methods, C0103 class Albums(db.Model): # pylint: disable=too-few-public-methods, C0103
""" """
Group table albums table
""" """
__tablename__ = "group" __tablename__ = "albums"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False) name = db.Column(db.String, nullable=False)
description = db.Column(db.String, nullable=False) description = db.Column(db.String, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey("user.id")) author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
created_at = db.Column( created_at = db.Column(
db.DateTime, db.DateTime,
nullable=False, nullable=False,
server_default=db.func.now(), # pylint: disable=E1102 server_default=db.func.now(), # pylint: disable=E1102
) )
junction = db.relationship("GroupJunction", backref="groups") album_fk = db.relationship("AlbumJunction", backref="albums")
class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103 class Users(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103
""" """
User table Users table
""" """
__tablename__ = "user" __tablename__ = "users"
# Gallery used information # Gallery used information
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -100,8 +99,8 @@ class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C010
server_default=db.func.now(), # pylint: disable=E1102 server_default=db.func.now(), # pylint: disable=E1102
) )
posts = db.relationship("Post", backref="author") pictures_fk = db.relationship("Pictures", backref="author")
groups = db.relationship("Group", backref="author") albums_fk = db.relationship("Albums", backref="author")
def get_id(self): def get_id(self):
return str(self.alt_id) return str(self.alt_id)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before After
Before After

Binary file not shown.

View file

@ -1,7 +0,0 @@
@font-face {
font-family: 'Rubik';
src: url('./Rubik.ttf') format('truetype');
font-style: normal;
font-display: block;
font-weight: 300 900;
}

View file

@ -0,0 +1,8 @@
function copyToClipboard(data) {
try {
navigator.clipboard.writeText(data)
addNotification("Copied to clipboard!", 4);
} catch (err) {
addNotification("Oh noes, something when wrong D:", 2);
}
}

View file

@ -0,0 +1,11 @@
// fade in images
function imgFade(obj, time = 200) {
setTimeout(() => {
obj.style.animation = `imgFadeIn ${time}ms`;
setTimeout(() => {
obj.style.opacity = null;
obj.style.animation = null;
}, time);
}, 1);
}

View file

@ -0,0 +1,88 @@
function makeGrid() {
// Get the container and images
const container = document.querySelector('.gallery-grid');
const images = document.querySelectorAll('.gallery-item');
// Set the gap between images
const gap = 0.6 * 16;
const maxWidth = container.clientWidth - gap;
const maxHeight = 13 * 16;
if (window.innerWidth < 800) {
for (let i = 0; i < images.length; i++) {
images[i].style.height = images[i].offsetWidth + 'px';
images[i].style.width = null;
images[i].style.left = null;
images[i].style.top = null;
}
container.style.height = null;
return;
}
// Calculate the width and height of each image
let calculated = {};
for (let i = 0; i < images.length; i++) {
const image = images[i].querySelector('img');
const width = image.naturalWidth;
const height = image.naturalHeight;
let ratio = width / height;
const newWidth = maxHeight * ratio;
if (newWidth > maxWidth) {
newWidth = maxWidth / 3 - gap; // 3 images per row max
ratio = newWidth / height;
}
calculated[i] = {"width": newWidth,
"height": maxHeight,
"ratio": ratio};
}
// Next images position
let nextTop = gap;
let nextLeft = gap;
for (let i = 0; i < images.length; i++) {
let currentRow = [];
let currectLength = 0;
// While the row is not full add images to it
while (currectLength < maxWidth && i !== images.length) {
currentRow.push(i);
currectLength += calculated[i]["width"];
i++;
}
// Go back one image since the last one pushed the row over the limit
i--;
// Calculate the amount of space required to fill the row
const currentRowDiff = (currectLength - maxWidth);
// Cycle through the images in the row and adjust their width and left position
for (let j = 0; j < currentRow.length; j++) {
const image = images[currentRow[j]];
const data = calculated[currentRow[j]];
// Shrink compared to the % of the row it takes up
const shrink = currentRowDiff * (data["width"] / currectLength);
image.style.width = data["width"] - shrink - gap + 'px';
image.style.height = data["height"] + 'px';
image.style.left = nextLeft + 'px';
image.style.top = nextTop + 'px';
nextLeft += data["width"] - shrink;
}
// Reset for the next row
nextTop += maxHeight + gap;
nextLeft = gap;
}
container.style.height = nextTop + 'px';
}

View file

@ -0,0 +1,155 @@
function groupDeletePopup() {
let title = 'Yeet!';
let subtitle =
'Are you surrrre? This action is irreversible ' +
'and very final. This wont delete the images, ' +
'but it will remove them from this group.'
let body = null;
let deleteBtn = document.createElement('button');
deleteBtn.classList.add('btn-block');
deleteBtn.classList.add('critical');
deleteBtn.innerHTML = 'Dewww eeeet!';
deleteBtn.onclick = groupDeleteConfirm;
popupShow(title, subtitle, body, [popupCancelButton, deleteBtn]);
}
function groupDeleteConfirm(event) {
// AJAX takes control of subby form :3
event.preventDefault();
fetch('/group/' + group_data['id'], {
method: 'DELETE',
}).then(response => {
if (response.ok) {
window.location.href = '/group/';
} else {
addNotification('Server exploded, returned:' + response.status, 2);
}
}).catch(error => {
addNotification('Error yeeting group!' + error, 2);
});
}
function groupEditPopup() {
let title = 'Nothing stays the same';
let subtitle = 'Add, remove, or change, the power is in your hands...'
let formSubmitButton = document.createElement('button');
formSubmitButton.setAttribute('form', 'groupEditForm');
formSubmitButton.setAttribute('type', 'submit');
formSubmitButton.classList.add('btn-block');
formSubmitButton.classList.add('primary');
formSubmitButton.innerHTML = 'Saveeee';
// Create form
let body = document.createElement('form');
body.setAttribute('onsubmit', 'return groupEditConfirm(event);');
body.id = 'groupEditForm';
let formImageId = document.createElement('input');
formImageId.setAttribute('type', 'text');
formImageId.setAttribute('placeholder', 'Image ID');
formImageId.setAttribute('required', '');
formImageId.classList.add('input-block');
formImageId.id = 'groupFormImageId';
let formAction = document.createElement('input');
formAction.setAttribute('type', 'text');
formAction.setAttribute('value', 'add');
formAction.setAttribute('placeholder', '[add, remove]');
formAction.setAttribute('required', '');
formAction.classList.add('input-block');
formAction.id = 'groupFormAction';
body.appendChild(formImageId);
body.appendChild(formAction);
popupShow(title, subtitle, body, [popupCancelButton, formSubmitButton]);
}
function groupEditConfirm(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let imageId = document.querySelector("#groupFormImageId").value;
let action = document.querySelector("#groupFormAction").value;
let formData = new FormData();
formData.append("imageId", imageId);
formData.append("action", action);
fetch('/group/' + group_data['id'], {
method: 'PUT',
body: formData
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
addNotification('Server exploded, returned:' + response.status, 2);
}
}).catch(error => {
addNotification('Error!!!!! Panic!!!!' + error, 2);
});
}
function groupCreatePopup() {
let title = 'New stuff!';
let subtitle =
'Image groups are a simple way to ' +
'"group" images together, are you ready?'
let formSubmitButton = document.createElement('button');
formSubmitButton.setAttribute('form', 'groupCreateForm');
formSubmitButton.setAttribute('type', 'submit');
formSubmitButton.classList.add('btn-block');
formSubmitButton.classList.add('primary');
formSubmitButton.innerHTML = 'Huzzah!';
// Create form
let body = document.createElement('form');
body.setAttribute('onsubmit', 'return groupCreateConfirm(event);');
body.id = 'groupCreateForm';
let formName = document.createElement('input');
formName.setAttribute('type', 'text');
formName.setAttribute('placeholder', 'Group namey');
formName.setAttribute('required', '');
formName.classList.add('input-block');
formName.id = 'groupFormName';
let formDescription = document.createElement('input');
formDescription.setAttribute('type', 'text');
formDescription.setAttribute('placeholder', 'What it about????');
formDescription.classList.add('input-block');
formDescription.id = 'groupFormDescription';
body.appendChild(formName);
body.appendChild(formDescription);
popupShow(title, subtitle, body, [popupCancelButton, formSubmitButton]);
}
function groupCreateConfirm(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let name = document.querySelector("#groupFormName").value;
let description = document.querySelector("#groupFormDescription").value;
let formData = new FormData();
formData.append("name", name);
formData.append("description", description);
fetch('/group/', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
addNotification('Server exploded, returned:' + response.status, 2);
}
}).catch(error => {
addNotification('Error summoning group!' + error, 2);
});
}

View file

@ -0,0 +1,101 @@
function imageFullscreen() {
let info = document.querySelector('.info-container');
let image = document.querySelector('.image-container');
if (info.classList.contains('collapsed')) {
info.classList.remove('collapsed');
image.classList.remove('collapsed');
document.cookie = "image-info=0"
} else {
info.classList.add('collapsed');
image.classList.add('collapsed');
document.cookie = "image-info=1"
}
}
function imageDeletePopup() {
let title = 'DESTRUCTION!!!!!!';
let subtitle =
'Do you want to delete this image along with ' +
'all of its data??? This action is irreversible!';
let body = null;
let deleteBtn = document.createElement('button');
deleteBtn.classList.add('btn-block');
deleteBtn.classList.add('critical');
deleteBtn.innerHTML = 'Dewww eeeet!';
deleteBtn.onclick = imageDeleteConfirm;
popupShow(title, subtitle, body, [popupCancelButton, deleteBtn]);
}
function imageDeleteConfirm() {
popupDismiss();
fetch('/image/' + image_data["id"], {
method: 'DELETE',
}).then(response => {
if (response.ok) {
window.location.href = '/';
} else {
addNotification('Image *clings*', 2);
}
});
}
function imageEditPopup() {
let title = 'Edit image!';
let subtitle = 'Enter funny stuff here!';
let formSubmitButton = document.createElement('button');
formSubmitButton.setAttribute('form', 'imageEditForm');
formSubmitButton.setAttribute('type', 'submit');
formSubmitButton.classList.add('btn-block');
formSubmitButton.classList.add('primary');
formSubmitButton.innerHTML = 'Saveeee';
// Create form
let body = document.createElement('form');
body.setAttribute('onsubmit', 'return imageEditConfirm(event);');
body.id = 'imageEditForm';
let formAlt = document.createElement('input');
formAlt.setAttribute('type', 'text');
formAlt.setAttribute('value', image_data["alt"]);
formAlt.setAttribute('placeholder', 'Image Alt');
formAlt.classList.add('input-block');
formAlt.id = 'imageFormAlt';
let formDescription = document.createElement('input');
formDescription.setAttribute('type', 'text');
formDescription.setAttribute('value', image_data["description"]);
formDescription.setAttribute('placeholder', 'Image Description');
formDescription.classList.add('input-block');
formDescription.id = 'imageFormDescription';
body.appendChild(formAlt);
body.appendChild(formDescription);
popupShow(title, subtitle, body, [popupCancelButton, formSubmitButton]);
}
function imageEditConfirm(event) {
// Yoink subby form
event.preventDefault();
let alt = document.querySelector('#imageFormAlt').value;
let description = document.querySelector('#imageFormDescription').value;
let form = new FormData();
form.append('alt', alt);
form.append('description', description);
fetch('/image/' + image_data["id"], {
method: 'PUT',
body: form,
}).then(response => {
if (response.ok) {
popupDismiss();
window.location.reload();
} else {
addNotification('Image *clings*', 2);
}
});
}

View file

@ -1,93 +0,0 @@
// fade in images
function imgFade(obj, time = 250) {
obj.style.transition = `opacity ${time}ms`;
obj.style.opacity = 1;
}
// Lazy load images when they are in view
function loadOnView() {
const lazyLoad = document.querySelectorAll('#lazy-load');
const webpSupport = checkWebpSupport();
for (let i = 0; i < lazyLoad.length; i++) {
const image = lazyLoad[i];
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
if (!image.src && webpSupport) {
image.src = `${image.getAttribute('data-src')}&e=webp`;
} else if (!image.src) {
image.src = image.getAttribute('data-src');
}
}
}
}
window.onload = function () {
loadOnView();
const 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 and convert to UTC Date object
const dateTime = new Date(`${date[0]}/${date[1]}/${date[2]} ${time} UTC`);
// Convert to local time
times[i].innerHTML = `${dateTime.toLocaleDateString()} ${dateTime.toLocaleTimeString()}`;
}
// Top Of Page button
const 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;
}
// Info button
const infoButton = document.querySelector('.info-button');
if (infoButton) {
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.remove('show');
} else {
infoButton.classList.add('show');
}
infoButton.onclick = function () {
popUpShow('OnlyLegs',
'<a href="https://github.com/Fluffy-Bean/onlylegs">v0.1.2</a> ' +
'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' +
'<br>Made by Fluffy and others with ❤️');
}
}
};
window.onscroll = function () {
loadOnView();
// Top Of Page button
const topOfPage = document.querySelector('.top-of-page');
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
topOfPage.classList.add('show');
} else {
topOfPage.classList.remove('show');
}
// Info button
const infoButton = document.querySelector('.info-button');
if (infoButton) {
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.remove('show');
} else {
infoButton.classList.add('show');
}
}
};
window.onresize = function () {
loadOnView();
};

View file

@ -5,7 +5,7 @@ function showLogin() {
cancelBtn.classList.add('btn-block'); cancelBtn.classList.add('btn-block');
cancelBtn.classList.add('transparent'); cancelBtn.classList.add('transparent');
cancelBtn.innerHTML = 'nuuuuuuuu'; cancelBtn.innerHTML = 'nuuuuuuuu';
cancelBtn.onclick = popupDissmiss; cancelBtn.onclick = popupDismiss;
loginBtn = document.createElement('button'); loginBtn = document.createElement('button');
loginBtn.classList.add('btn-block'); loginBtn.classList.add('btn-block');
@ -50,7 +50,7 @@ function showLogin() {
loginForm.appendChild(passwordInput); loginForm.appendChild(passwordInput);
loginForm.appendChild(rememberMeSpan); loginForm.appendChild(rememberMeSpan);
popUpShow( popupShow(
'Login!', 'Login!',
'Need an account? <span class="link" onclick="showRegister()">Register!</span>', 'Need an account? <span class="link" onclick="showRegister()">Register!</span>',
loginForm, loginForm,
@ -103,7 +103,7 @@ function showRegister() {
cancelBtn.classList.add('btn-block'); cancelBtn.classList.add('btn-block');
cancelBtn.classList.add('transparent'); cancelBtn.classList.add('transparent');
cancelBtn.innerHTML = 'nuuuuuuuu'; cancelBtn.innerHTML = 'nuuuuuuuu';
cancelBtn.onclick = popupDissmiss; cancelBtn.onclick = popupDismiss;
registerBtn = document.createElement('button'); registerBtn = document.createElement('button');
registerBtn.classList.add('btn-block'); registerBtn.classList.add('btn-block');
@ -146,7 +146,7 @@ function showRegister() {
registerForm.appendChild(passwordInput); registerForm.appendChild(passwordInput);
registerForm.appendChild(passwordInputRepeat); registerForm.appendChild(passwordInputRepeat);
popUpShow( popupShow(
'Who are you?', 'Who are you?',
'Already have an account? <span class="link" onclick="showLogin()">Login!</span>', 'Already have an account? <span class="link" onclick="showLogin()">Login!</span>',
registerForm, registerForm,

View file

@ -1,4 +1,4 @@
function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) { function popupShow(titleText, subtitleText, bodyContent=null, userActions=null) {
// Get popup elements // Get popup elements
const popupSelector = document.querySelector('.pop-up'); const popupSelector = document.querySelector('.pop-up');
const headerSelector = document.querySelector('.pop-up-header'); const headerSelector = document.querySelector('.pop-up-header');
@ -9,38 +9,47 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null)
actionsSelector.innerHTML = ''; actionsSelector.innerHTML = '';
// Set popup header and subtitle // Set popup header and subtitle
const titleElement = document.createElement('h2'); let titleElement = document.createElement('h2');
titleElement.innerHTML = titleText; titleElement.innerHTML = titleText;
headerSelector.appendChild(titleElement); headerSelector.appendChild(titleElement);
const subtitleElement = document.createElement('p'); let subtitleElement = document.createElement('p');
subtitleElement.innerHTML = subtitleText; subtitleElement.innerHTML = subtitleText;
headerSelector.appendChild(subtitleElement); headerSelector.appendChild(subtitleElement);
if (bodyContent) { if (bodyContent) { headerSelector.appendChild(bodyContent) }
headerSelector.appendChild(bodyContent);
}
// Set buttons that will be displayed // Set buttons that will be displayed
if (userActions) { if (userActions) {
// for each user action, add the element userActions.forEach((action) => {
for (let i = 0; i < userActions.length; i++) { actionsSelector.appendChild(action);
actionsSelector.appendChild(userActions[i]); });
}
} else { } else {
actionsSelector.innerHTML = '<button class="btn-block transparent" onclick="popupDissmiss()">Close</button>'; let closeButton = document.createElement('button');
closeButton.classList.add('btn-block');
closeButton.classList.add('transparent');
closeButton.innerHTML = 'Yeet!';
closeButton.onclick = popupDismiss;
actionsSelector.appendChild(closeButton);
} }
// Stop scrolling and show popup // Stop scrolling and show popup
document.querySelector("html").style.overflow = "hidden"; document.querySelector("html").style.overflow = "hidden";
popupSelector.style.display = 'block'; popupSelector.style.display = 'block';
setTimeout(() => { popupSelector.classList.add('active') }, 5); // 2ms delay to allow for css transition >:C
// 5ms delay to allow for css transition >:C
setTimeout(() => { popupSelector.classList.add('active') }, 5);
} }
function popupDissmiss() { function popupDismiss() {
const popupSelector = document.querySelector('.pop-up'); const popupSelector = document.querySelector('.pop-up');
document.querySelector("html").style.overflow = "auto"; document.querySelector("html").style.overflow = "auto";
popupSelector.classList.remove('active'); popupSelector.classList.remove('active');
setTimeout(() => { popupSelector.style.display = 'none'; }, 200); setTimeout(() => { popupSelector.style.display = 'none'; }, 200);
} }
const popupCancelButton = document.createElement('button');
popupCancelButton.classList.add('btn-block');
popupCancelButton.classList.add('transparent');
popupCancelButton.innerHTML = 'nuuuuuuuu';
popupCancelButton.onclick = popupDismiss;

View file

@ -0,0 +1,6 @@
function keepSquare() {
let square = document.getElementsByClassName('square')
for (let i = 0; i < square.length; i++) {
square[i].style.height = square[i].offsetWidth + 'px';
}
}

View file

@ -1,10 +0,0 @@
function checkWebpSupport() {
let webpSupport = false;
try {
webpSupport = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0;
} catch (e) {
webpSupport = false;
}
return webpSupport;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -1,18 +0,0 @@
{
"name": "OnlyLegs",
"short_name": "OnlyLegs",
"start_url": "/",
"display": "standalone",
"background_color": "#151515",
"theme_color": "#151515",
"description": "A gallery built for fast and simple image management!",
"icons": [
{
"src": "icon.png",
"sizes": "621x621",
"type": "image/png"
}
],
"splash_pages": null
}

View file

@ -9,3 +9,11 @@
left: -100% left: -100%
100% 100%
left: 100% left: 100%
@keyframes imgFadeIn
0%
opacity: 0
// filter: blur(0.5rem)
100%
opacity: 1
// filter: blur(0)

View file

@ -20,26 +20,10 @@
background-color: RGB($fg-black) background-color: RGB($fg-black)
color: RGB($fg-white) color: RGB($fg-white)
&::after
content: ''
width: $rad
height: calc(#{$rad} * 2)
position: absolute
bottom: calc(#{$rad} * -2)
left: 0
background-color: RGB($bg-bright)
border-radius: $rad 0 0 0
box-shadow: 0 calc(#{$rad} * -1) 0 0 RGB($bg-100)
.banner .banner
height: 30rem height: 30rem
max-height: 69vh max-height: 69vh
background-color: RGB($bg-300)
img img
position: absolute position: absolute
inset: 0 inset: 0
@ -122,10 +106,10 @@
background-color: RGB($primary) background-color: RGB($primary)
border-radius: $rad border-radius: $rad
overflow: hidden
.banner-small .banner-small
height: 3.5rem height: 3.5rem
background-color: RGB($bg-100)
.banner-content .banner-content
padding: 0 0.5rem padding: 0 0.5rem

View file

@ -59,6 +59,10 @@
&.black &.black
@include btn-block($black) @include btn-block($black)
&.disabled, &:disabled
color: RGB($fg-dim)
cursor: unset
.input-checkbox .input-checkbox
padding: 0 padding: 0
display: flex display: flex

View file

@ -68,6 +68,10 @@
color: RGB($primary) color: RGB($primary)
&.disabled, &:disabled
color: RGB($fg-dim)
cursor: unset
.pill__critical .pill__critical
color: RGB($critical) color: RGB($critical)

View file

@ -24,8 +24,6 @@
margin: 0.35rem margin: 0.35rem
padding: 0 padding: 0
height: auto
position: relative position: relative
border-radius: $rad-inner border-radius: $rad-inner
@ -44,8 +42,7 @@
height: auto height: auto
position: absolute position: absolute
left: 0 inset: 0
bottom: 0
display: flex display: flex
flex-direction: column flex-direction: column
@ -88,19 +85,6 @@
object-position: center object-position: center
background-color: RGB($bg-bright) background-color: RGB($bg-bright)
filter: blur(0.5rem)
opacity: 0
transition: all 0.2s cubic-bezier(.79, .14, .15, .86)
&.loaded
filter: blur(0)
opacity: 1
&:after
content: ""
display: block
padding-bottom: 100%
&:hover &:hover
box-shadow: 0 0.2rem 0.4rem 0.1rem RGBA($bg-100, 0.6) box-shadow: 0 0.2rem 0.4rem 0.1rem RGBA($bg-100, 0.6)
@ -112,8 +96,6 @@
margin: 0.35rem margin: 0.35rem
padding: 0 padding: 0
height: auto
position: relative position: relative
border-radius: $rad-inner border-radius: $rad-inner
@ -178,8 +160,7 @@
height: 100% height: 100%
position: absolute position: absolute
top: 0 inset: 0
left: 0
object-fit: cover object-fit: cover
object-position: center object-position: center
@ -187,14 +168,8 @@
background-color: RGB($bg-bright) background-color: RGB($bg-bright)
border-radius: $rad-inner border-radius: $rad-inner
box-shadow: 0 0 0.4rem 0.25rem RGBA($bg-100, 0.1) box-shadow: 0 0 0.4rem 0.25rem RGBA($bg-100, 0.1)
filter: blur(0.5rem)
opacity: 0
transition: all 0.2s cubic-bezier(.79, .14, .15, .86) transition: transform 0.2s cubic-bezier(.79, .14, .15, .86)
&.loaded
filter: blur(0)
opacity: 1
&.size-1 &.size-1
.data-1 .data-1
@ -219,11 +194,6 @@
transform: scale(0.6) rotate(-1deg) translate(-15%, -23%) transform: scale(0.6) rotate(-1deg) translate(-15%, -23%)
z-index: +1 z-index: +1
&:after
content: ""
display: block
padding-bottom: 100%
&:hover &:hover
.images .images
&.size-1 &.size-1
@ -252,3 +222,7 @@
@media (max-width: 800px) @media (max-width: 800px)
.gallery-grid .gallery-grid
grid-template-columns: auto auto auto grid-template-columns: auto auto auto
.gallery-item
margin: 0.35rem
position: relative

View file

@ -0,0 +1,259 @@
.info-container
padding: 0.5rem 0 0 0.5rem
width: 27rem
position: absolute
top: 0
left: 0
bottom: 0
background-image: linear-gradient(90deg, $bg-transparent, transparent)
overflow-y: auto
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
z-index: 2
-ms-overflow-style: none
scrollbar-width: none
&::-webkit-scrollbar
display: none
&.collapsed
left: -27rem
@media (max-width: 1100px)
.info-container
padding: 0 0.5rem 0 0.5rem
width: 100%
position: relative
background: none
// While probably not the best way of doing this
// Not bothered to fight with CSS today
&.collapsed
left: 0
details
margin-bottom: 0.5rem
padding: 0.5rem
display: flex
flex-direction: column
background-color: RGB($bg-300)
color: RGB($fg-white)
border-radius: $rad
overflow: hidden
summary
height: 1.5rem
display: flex
flex-direction: row
justify-content: flex-start
align-items: center
position: relative
color: RGB($primary)
> i
margin-right: 0
font-size: 1.25rem
&.collapse-indicator
transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
h2
margin: 0 0.5rem
font-size: 1.1rem
font-weight: 500
&[open]
summary
margin-bottom: 0.5rem
> i.collapse-indicator
transform: rotate(90deg)
p
margin: 0
padding: 0
font-size: 1rem
font-weight: 400
text-overflow: ellipsis
overflow: hidden
.link
margin: 0
padding: 0
color: RGB($primary)
cursor: pointer
text-decoration: none
&:hover
text-decoration: underline
.pfp
width: 1.1rem
height: 1.1rem
border-radius: $rad-inner
object-fit: cover
table
margin: 0
padding: 0
width: 100%
overflow-x: hidden
border-collapse: collapse
tr
white-space: nowrap
td
padding-bottom: 0.5rem
max-width: 0
font-size: 1rem
font-weight: 400
vertical-align: top
> *
vertical-align: top
td:first-child
padding-right: 0.5rem
width: 50%
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
td:last-child
width: 50%
white-space: normal
word-break: break-word
tr:last-of-type td
padding-bottom: 0
.img-colours
width: 100%
display: flex
gap: 0.5rem
button
margin: 0
padding: 0
width: 1.6rem
height: 1.6rem
display: flex
justify-content: center
align-items: center
border-radius: $rad-inner
border: none
i
font-size: 1rem
opacity: 0
transition: opacity 0.15s ease-in-out
&:hover i
opacity: 1
.img-groups
width: 100%
display: flex
flex-wrap: wrap
gap: 0.5rem
.image-container
padding: 0.5rem
position: absolute
top: 0
left: 27rem
right: 0
bottom: 0
z-index: 2
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1), padding 0.3s cubic-bezier(0.76, 0, 0.17, 1)
picture
margin: auto
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
border-radius: $rad
&.collapsed
padding: 0
left: 0
picture img
border-radius: 0
@media (max-width: 1100px)
.image-container
position: relative
left: 0
picture
margin: 0 auto
max-height: 69vh
img
max-height: 69vh
&.collapsed
padding: 0.5rem
left: 0
picture img
border-radius: $rad
.background
position: absolute
inset: 0
background-color: RGB($bg-300)
background-image: linear-gradient(to right, RGB($bg-400) 15%, RGB($bg-200) 35%, RGB($bg-400) 50%)
background-size: 1000px 640px
animation: imgLoading 1.8s linear infinite forwards
user-select: none
overflow: hidden
z-index: 1
img
position: absolute
inset: 0
width: 100%
height: 100%
background-color: RGB($fg-white)
filter: blur(3rem) saturate(1.2) brightness(0.7)
transform: scale(1.1)
object-fit: cover
object-position: center center
&::after
content: ''
position: absolute
inset: 0
width: 100%
height: 100%
z-index: +1
@media (max-width: 1100px)
#fullscreenImage
display: none

View file

@ -1,42 +0,0 @@
.background
width: 100%
height: 100vh
position: fixed
top: 0
left: 0
background-color: RGB($bg-300)
background-image: linear-gradient(to right, RGB($bg-400) 15%, RGB($bg-200) 35%, RGB($bg-400) 50%)
background-size: 1000px 640px
animation: imgLoading 1.8s linear infinite forwards
user-select: none
overflow: hidden
z-index: 1
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
background-color: RGB($fg-white)
filter: blur(1rem) saturate(1.2)
transform: scale(1.1)
object-fit: cover
object-position: center center
span
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: +1

View file

@ -1,28 +0,0 @@
.image-container
margin: auto
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
@media (max-width: 1100px)
.image-container
margin: 0 auto
max-height: 69vh
img
max-height: 69vh

View file

@ -1,215 +0,0 @@
.info-container
padding: 0.5rem 0 0.5rem 0.5rem
width: 27rem
height: 100vh
position: absolute
top: 0
left: 0
display: flex
flex-direction: column
gap: 0.5rem
background-image: linear-gradient(90deg, $bg-transparent, transparent)
overflow-y: auto
z-index: +4
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
-ms-overflow-style: none
scrollbar-width: none
&::-webkit-scrollbar
display: none
&.collapsed
left: -27rem
.info-tab
width: 100%
display: flex
flex-direction: column
position: relative
background-color: RGB($bg-300)
border-radius: $rad
transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86)
&.collapsed
height: 2.5rem
.collapse-indicator
transform: rotate(90deg)
.info-header
border-radius: $rad
.info-table
display: none
.collapse-indicator
margin: 0
padding: 0
position: absolute
top: 0.6rem
right: 0.6rem
background-color: transparent
color: RGB($primary)
border: none
z-index: +2
transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
cursor: pointer
> i
font-size: 1.1rem
color: RGB($primary)
.info-header
padding: 0.5rem
width: 100%
height: 2.5rem
display: flex
justify-content: start
align-items: center
gap: 0.5rem
background-color: RGB($bg-200)
border-radius: $rad $rad 0 0
> i
font-size: 1.25rem
color: RGB($primary)
h2
margin: 0
font-size: 1.1rem
font-weight: 500
color: RGB($primary)
text-overflow: ellipsis
overflow: hidden
.info-table
margin: 0
padding: 0.5rem
display: flex
flex-direction: column
gap: 1rem
color: RGB($fg-white)
overflow-x: hidden
p
margin: 0
padding: 0
font-size: 1rem
font-weight: 400
text-overflow: ellipsis
overflow: hidden
.link
margin: 0
padding: 0
color: RGB($primary)
cursor: pointer
text-decoration: none
&:hover
text-decoration: underline
table
margin: 0
padding: 0
width: 100%
overflow-x: hidden
border-collapse: collapse
tr
white-space: nowrap
td
padding-bottom: 0.5rem
max-width: 0
font-size: 1rem
font-weight: 400
vertical-align: top
td:first-child
padding-right: 0.5rem
width: 50%
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
td:last-child
width: 50%
white-space: normal
word-break: break-word
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: $rad-inner
// border: 1px solid RGB($white)
.img-groups
width: 100%
display: flex
flex-wrap: wrap
gap: 0.5rem
@media (max-width: 1100px)
.info-container
padding: 0.5rem
width: 100%
height: 100%
position: relative
&.collapsed
left: unset
.info-container
background: transparent

View file

@ -1,59 +0,0 @@
@import 'background'
@import 'info-tab'
@import 'image'
.image-grid
padding: 0
width: 100%
height: 100vh
position: relative
display: flex
flex-direction: column
z-index: 3
.image-block
margin: 0 0 0 27rem
padding: 0.5rem
width: calc(100% - 27rem)
height: 100vh
position: relative
display: flex
flex-direction: column
gap: 0
z-index: 3
transition: margin 0.3s cubic-bezier(0.76, 0, 0.17, 1), width 0.3s cubic-bezier(0.76, 0, 0.17, 1)
.pill-row
margin-top: 0.5rem
&.collapsed
.image-block
margin: 0
width: 100%
@media (max-width: 1100px)
.image-grid
height: auto
.image-block
margin: 0
padding: 0.5rem 0.5rem 0 0.5rem
width: 100%
height: auto
transition: margin 0s, width 0s
.pill-row
#fullscreenImage
display: none

View file

@ -1,36 +1,20 @@
.navigation nav
margin: 0
padding: 0
width: 3.5rem width: 3.5rem
height: 100% height: 100%
height: 100dvh height: 100dvh
display: flex display: flex
flex-direction: column flex-direction: column
justify-content: space-between
position: fixed position: fixed
top: 0 top: 0
left: 0 left: 0
background-color: RGB($bg-100) background-color: transparent
color: RGB($fg-white) color: inherit
z-index: 69 z-index: 69
.logo
margin: 0
padding: 0
width: 3.5rem
height: 3.5rem
min-height: 3.5rem
display: flex
flex-direction: row
align-items: center
.navigation-spacer .navigation-spacer
height: 100% height: 100%
@ -50,6 +34,7 @@
align-items: center align-items: center
background-color: transparent background-color: transparent
color: inherit
border: none border: none
text-decoration: none text-decoration: none
@ -58,7 +43,7 @@
padding: 0.5rem padding: 0.5rem
font-size: 1.3rem font-size: 1.3rem
border-radius: $rad-inner border-radius: $rad-inner
color: RGB($fg-white) color: inherit
> .nav-pfp > .nav-pfp
padding: 0.4rem padding: 0.4rem
@ -72,68 +57,29 @@
object-fit: cover object-fit: cover
border-radius: $rad-inner border-radius: $rad-inner
.tool-tip
padding: 0.4rem 0.7rem
display: block
position: absolute
top: 50%
left: 3rem
transform: translateY(-50%)
font-size: 0.9rem
font-weight: 500
background-color: RGB($bg-100)
color: RGB($fg-white)
opacity: 0
border-radius: $rad-inner
transition: opacity 0.2s cubic-bezier(.76,0,.17,1), left 0.2s cubic-bezier(.76,0,.17,1)
pointer-events: none
> i
display: block
position: absolute
top: 50%
left: -0.45rem
transform: translateY(-50%)
font-size: 0.75rem
color: RGB($bg-100)
&:hover &:hover
> i, .nav-pfp > i, .nav-pfp
background: RGBA($fg-white, 0.1) background: RGBA($fg-white, 0.1)
span
opacity: 1
left: 3.9rem
&.selected &.selected
> i color: RGB($primary)
color: RGB($primary)
&::before &::before
content: '' content: ''
display: block display: block
position: absolute position: absolute
top: 3px top: 0.5rem
left: 0 left: 0.2rem
width: 3px width: 3px
height: calc(100% - 6px) height: calc(100% - 1rem)
background-color: RGB($primary) background-color: currentColor
border-radius: $rad-inner border-radius: $rad-inner
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.navigation nav
width: 100vw width: 100vw
height: 3.5rem height: 3.5rem
@ -145,6 +91,8 @@
bottom: 0 bottom: 0
left: 0 left: 0
background-color: RGB($background)
> span > span
display: none display: none

View file

@ -22,23 +22,22 @@
margin: 0 margin: 0
padding: 0 padding: 0
width: 450px width: 24rem
height: auto height: auto
position: fixed position: fixed
top: 0.3rem bottom: 0.3rem
right: 0.3rem right: 0.3rem
display: flex display: flex
flex-direction: column flex-direction: column-reverse
z-index: 621 z-index: 621
.sniffle__notification .sniffle__notification
margin: 0 0 0.3rem 0 margin-top: 0.3rem
padding: 0 padding: 0
width: 450px
height: auto height: auto
max-height: 100px max-height: 100px
@ -56,7 +55,7 @@
box-sizing: border-box box-sizing: border-box
overflow: hidden overflow: hidden
transition: all 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55) transition: opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
&::after &::after
content: "" content: ""
@ -89,10 +88,8 @@
&.hide &.hide
margin: 0 margin: 0
max-height: 0 max-height: 0
opacity: 0 opacity: 0
transform: translateX(100%) transform: translateX(100%)
transition: all 0.4s ease-in-out, max-height 0.2s ease-in-out transition: all 0.4s ease-in-out, max-height 0.2s ease-in-out
.sniffle__notification-icon .sniffle__notification-icon
@ -130,6 +127,7 @@
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.notifications .notifications
bottom: 3.8rem
width: calc(100vw - 0.6rem) width: calc(100vw - 0.6rem)
height: auto height: auto
@ -138,7 +136,7 @@
&.hide &.hide
opacity: 0 opacity: 0
transform: translateY(-5rem) transform: translateY(1rem)
.sniffle__notification-time .sniffle__notification-time
width: 100% width: 100%

View file

@ -1,11 +1,11 @@
.tag-icon .tag-icon
margin: 0 margin: 0
padding: 0.25rem 0.5rem padding: 0.3rem 0.5rem
display: flex display: flex
align-items: center align-items: flex-end
justify-content: center justify-content: center
gap: 0.25rem gap: 0.3rem
font-size: 0.9rem font-size: 0.9rem
font-weight: 500 font-weight: 500
@ -19,9 +19,8 @@
cursor: pointer cursor: pointer
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
svg i
width: 1.15rem font-size: 1.15rem
height: 1.15rem
&:hover &:hover
background-color: RGBA($primary, 0.3) background-color: RGBA($primary, 0.2)

View file

@ -18,15 +18,13 @@
@import "components/buttons/pill" @import "components/buttons/pill"
@import "components/buttons/block" @import "components/buttons/block"
@import "components/image-view/view" @import "components/image-view"
@import "components/settings" @import "components/settings"
// Reset
* *
box-sizing: border-box box-sizing: border-box
font-family: $font
scrollbar-color: RGB($primary) transparent scrollbar-color: RGB($primary) transparent
font-family: $font
::-webkit-scrollbar ::-webkit-scrollbar
width: 0.5rem width: 0.5rem
@ -37,66 +35,49 @@
::-webkit-scrollbar-thumb:hover ::-webkit-scrollbar-thumb:hover
background: RGB($fg-white) background: RGB($fg-white)
html, body html
margin: 0 margin: 0
padding: 0 padding: 0
min-height: 100vh
max-width: 100vw
background-color: RGB($fg-white)
scroll-behavior: smooth scroll-behavior: smooth
overflow-x: hidden
.wrapper body
margin: 0 margin: 0
padding: 0 0 0 3.5rem padding: 0 0 0 3.5rem
max-width: 100%
min-height: 100vh min-height: 100vh
display: grid
grid-template-rows: auto 1fr auto
background-color: RGB($background)
color: RGB($foreground)
overflow-x: hidden
@media (max-width: $breakpoint)
body
padding: 0 0 3.5rem 0
main
display: flex display: flex
flex-direction: column flex-direction: column
position: relative
background-color: RGB($bg-bright) background: RGBA($white, 1)
color: RGB($bg-100) color: RGB($fg-black)
border-top-left-radius: $rad
.big-text overflow: hidden
height: 20rem @media (max-width: $breakpoint)
main
display: flex border-top-left-radius: 0
flex-direction: column
justify-content: center
align-items: center
color: RGB($bg-100)
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 .error-page
width: 100% min-height: 100%
height: 100vh
display: flex display: flex
flex-direction: column flex-direction: column
justify-content: center justify-content: center
align-items: center align-items: center
background-color: RGB($bg-bright)
h1 h1
margin: 0 2rem margin: 0 2rem
@ -113,23 +94,8 @@ html, body
font-size: 1.25rem font-size: 1.25rem
font-weight: 400 font-weight: 400
text-align: center text-align: center
color: $fg-black
@media (max-width: $breakpoint) @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 .error-page
height: calc(100vh - 3.5rem)
h1 h1
font-size: 4.5rem font-size: 4.5rem

View file

@ -37,6 +37,11 @@ $font: 'Rubik', sans-serif
$breakpoint: 800px $breakpoint: 800px
// New variables, Slowly moving over to them because I suck at planning ahead and coding reeee
$background: var(--bg-100)
$foreground: var(--fg-white)
\:root \:root
--bg-dim: 16, 16, 16 --bg-dim: 16, 16, 16
--bg-bright: 232, 227, 227 --bg-bright: 232, 227, 227
@ -66,7 +71,7 @@ $breakpoint: 800px
--success: var(--green) --success: var(--green)
--info: var(--blue) --info: var(--blue)
--rad: 8px --rad: 0.5rem
--rad-inner: calc(var(--rad) / 2) --rad-inner: calc(var(--rad) / 2)
--animation-smooth: cubic-bezier(0.76, 0, 0.17, 1) --animation-smooth: cubic-bezier(0.76, 0, 0.17, 1)

View file

@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ config.WEBSITE_CONF.name }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ config.WEBSITE_CONF.motto }}">
<meta name="author" content="{{ config.WEBSITE_CONF.author }}">
<meta property="og:title" content="{{ config.WEBSITE_CONF.name }}">
<meta property="og:description" content="{{ config.WEBSITE_CONF.motto }}">
<meta property="og:type" content="website">
<meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}">
<meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap">
<!-- phosphor icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<!-- Favicon -->
<link rel="icon" href="{{url_for('static', filename='icon.png')}}" type="image/png">
{% assets "scripts" %} <script type="text/javascript" src="{{ ASSET_URL }}"></script> {% endassets %}
{% assets "styles" %} <link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer> {% endassets %}
{% block head %}{% endblock %}
</head>
<body>
<div class="notifications"></div>
<button class="top-of-page" aria-label="Jump to top of page"><i class="ph ph-arrow-up"></i></button>
<div class="pop-up">
<span class="pop-up__click-off" onclick="popupDismiss()"></span>
<div class="pop-up-wrapper">
<div class="pop-up-header"></div>
<div class="pop-up-controlls"></div>
</div>
</div>
<nav>
<a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}" aria-label="Home Page">
<i class="ph-fill ph-images-square"></i>
</a>
<a href="{{ url_for('group.groups') }}" class="navigation-item {% block nav_groups %}{% endblock %}" aria-label="Photo Groups">
<i class="ph-fill ph-package"></i>
</a>
{% if current_user.is_authenticated %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()" aria-label="Upload Photo">
<i class="ph-fill ph-upload"></i>
</button>
{% endif %}
<span class="navigation-spacer"></span>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}" aria-label="Profile Page">
{% if current_user.picture %}
<span class="nav-pfp">
<picture>
<source srcset="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=webp">
<source srcset="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=png">
<img
src="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=icon"
alt="Profile picture"
onload="imgFade(this)"
style="opacity:0;"
>
</picture>
</span>
{% else %}
<i class="ph-fill ph-folder-simple-user"></i>
{% endif %}
</a>
<a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}" aria-label="Gallery Settings">
<i class="ph-fill ph-gear-fine"></i>
</a>
{% else %}
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()" aria-label="Sign up or Login">
<i class="ph-fill ph-sign-in"></i>
</button>
{% endif %}
</nav>
<header>{% block header %}{% endblock %}</header>
<main>
{% if current_user.is_authenticated %}
<div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span>
<div class="container">
<span id="dragIndicator"></span>
<h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p>
<form id="uploadForm">
<button class="fileDrop-block" type="button">
<i class="ph ph-upload"></i>
<span class="status">Choose or Drop file</span>
<input type="file" id="file" tab-index="-1">
</button>
<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 %}
{% block content %}{% endblock %}
</main>
<script type="text/javascript">
keepSquare();
const 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 and convert to UTC Date object
const dateTime = new Date(`${date[0]}/${date[1]}/${date[2]} ${time} UTC`);
// Convert to local time
times[i].innerHTML = `${dateTime.toLocaleDateString()} ${dateTime.toLocaleTimeString()}`;
}
// Top Of Page button
const 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 = () => {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
// Info button
const infoButton = document.querySelector('.info-button');
if (infoButton) {
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.remove('show');
} else {
infoButton.classList.add('show');
}
infoButton.onclick = () => {
popupShow('OnlyLegs',
'<a href="https://github.com/Fluffy-Bean/onlylegs">v{{ config['APP_VERSION'] }}</a> ' +
'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' +
'<br>Made by Fluffy and others with ❤️');
}
}
window.onload = () => { keepSquare(); }
window.onresize = () => { keepSquare(); }
window.onscroll = () => {
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
topOfPage.classList.add('show');
} else {
topOfPage.classList.remove('show');
}
if (infoButton) {
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.remove('show');
} else {
infoButton.classList.add('show');
}
}
}
{% for message in get_flashed_messages() %}
addNotification('{{ message[0] }}', {{ message[1] }});
{% endfor %}
</script>
{% block script %}{% endblock %}
</body>
</html>

View file

@ -1,7 +1,7 @@
{% extends 'layout.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<span class="error-page"> <div class="error-page">
<h1>{{error}}</h1> <h1>{{error}}</h1>
<p>{{msg}}</p> <p>{{msg}}</p>
</span> </div>
{% endblock %} {% endblock %}

View file

@ -1,227 +1,76 @@
{% extends 'layout.html' %} {% extends 'base.html' %}
{% from 'macros/image.html' import gallery_item %}
{% block nav_groups %}selected{% endblock %} {% block nav_groups %}selected{% endblock %}
{% block head %} {% block head %}
{% if images %} {% if images %}
<meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/> <meta property="og:image" content="{{ url_for('api.media', path='uploads/' + images.0.filename) }}"/>
<meta name="twitter:image" content="{{ url_for('api.media', path='uploads/' + images.0.filename) }}">
<meta name="theme-color" content="rgb{{ images.0.colours.0 }}"/>
<meta name="twitter:card" content="summary_large_image">
{% endif %} {% endif %}
<script type="text/javascript"> <script type="text/javascript">
function groupShare() { group_data = {
try { 'id': {{ group.id }},
navigator.clipboard.writeText(window.location.href) 'name': "{{ group.name }}",
addNotification("Copied link!", 4); 'description': "{{ group.description }}",
} catch (err) {
addNotification("Failed to copy link! Are you on HTTP?", 2);
}
} }
{% if current_user.id == group.author.id %}
function groupDelete() {
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.classList.add('transparent');
cancelBtn.innerHTML = 'AAAAAAAAAA';
cancelBtn.onclick = popupDissmiss;
deleteBtn = document.createElement('button');
deleteBtn.classList.add('btn-block');
deleteBtn.classList.add('critical');
deleteBtn.innerHTML = 'No ragrats!';
deleteBtn.onclick = deleteConfirm;
popUpShow('Yeet!',
'Are you surrrre? This action is irreversible and very final.' +
' This wont delete the images, but it will remove them from this group.',
null,
[cancelBtn, deleteBtn]);
}
function deleteConfirm(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let formID = {{ group.id }};
if (!formID) {
addNotification("Dont tamper with the JavaScript pls!", 3);
return;
}
// Make form
const formData = new FormData();
formData.append("group", formID);
fetch('{{ url_for('group_api.delete_group') }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
// Redirect to groups page
window.location.href = '{{ url_for('group.groups') }}';
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Bad information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
}
}
}).catch(error => {
addNotification('Error yeeting group!', 2);
});
}
function groupEdit() {
// Create elements
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.classList.add('transparent');
cancelBtn.innerHTML = 'go baaaaack';
cancelBtn.onclick = popupDissmiss;
submitBtn = document.createElement('button');
submitBtn.classList.add('btn-block');
submitBtn.classList.add('primary');
submitBtn.innerHTML = 'Saveeee';
submitBtn.type = 'submit';
submitBtn.setAttribute('form', 'editForm');
// Create form
editForm = document.createElement('form');
editForm.id = 'editForm';
editForm.setAttribute('onsubmit', 'return edit(event);');
groupInput = document.createElement('input');
groupInput.classList.add('input-block');
groupInput.type = 'text';
groupInput.placeholder = 'Group ID';
groupInput.value = {{ group.id }};
groupInput.id = 'group';
imageInput = document.createElement('input');
imageInput.classList.add('input-block');
imageInput.type = 'text';
imageInput.placeholder = 'Image ID';
imageInput.id = 'image';
actionInput = document.createElement('input');
actionInput.classList.add('input-block');
actionInput.type = 'text';
actionInput.placeholder = 'add/remove';
actionInput.value = 'add';
actionInput.id = 'action';
editForm.appendChild(groupInput);
editForm.appendChild(imageInput);
editForm.appendChild(actionInput);
popUpShow(
'Nothing stays the same',
'Add, remove, or change, the power is in your hands...',
editForm,
[cancelBtn, submitBtn]
);
}
function edit(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let formGroup = document.querySelector("#group").value;
let formImage = document.querySelector("#image").value;
let formAction = document.querySelector("#action").value;
if (!formGroup || !formImage || !formAction) {
addNotification("All values must be set!", 3);
return;
}
// Make form
const formData = new FormData();
formData.append("group", formGroup);
formData.append("image", formImage);
formData.append("action", formAction);
fetch('{{ url_for('group_api.modify_group') }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
addNotification('Group edited!!!', 1);
popupDissmiss();
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Bad information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
}
}
}).catch(error => {
addNotification('Error!!!!! Panic!!!!', 2);
});
}
{% endif %}
</script> </script>
<style> <style>
{% if images %} {% if images %}
.banner::after { :root { --bg-100: {{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }} }
box-shadow: 0 calc(var(--rad) * -1) 0 0 rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }});
} body {
.banner-content p { background: rgb{{ images.0.colours.0 }} !important;
color: {{ text_colour }} !important; color: {{ text_colour }} !important;
} }
.banner-content h1 { main {
color: {{ text_colour }} !important; background: rgba(var(--white), 0.6) !important;
} }
.navigation-item.selected { color: {{ text_colour }} !important; }
.banner .banner-content .banner-header,
.banner .banner-content .banner-info,
.banner .banner-content .banner-subtitle {
color: {{ text_colour }} !important;
}
.banner-content .link { .banner-content .link {
background-color: {{ text_colour }} !important; background-color: {{ text_colour }} !important;
color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important; color: rgb{{ images.0.colours.0 }} !important;
} }
.banner-content .link:hover { .banner-content .link:hover {
background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important; background-color: rgb{{ images.0.colours.0 }} !important;
color: {{ text_colour }} !important; color: {{ text_colour }} !important;
} }
.banner-filter { .banner-filter {
background: linear-gradient(90deg, rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}), background: linear-gradient(90deg, rgb{{ images.0.colours.0 }}, rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important;
rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important;
} }
@media (max-width: 800px) { @media (max-width: 800px) {
.banner-filter { .banner-filter {
background: linear-gradient(180deg, rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 1), background: linear-gradient(180deg, rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.4), rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.3)) !important;
rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.5)) !important;
} }
} }
.navigation {
background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
}
.navigation-item > i {
color: {{ text_colour }} !important;
}
.navigation-item.selected::before {
background-color: {{ text_colour }} !important;
}
{% endif %} {% endif %}
</style> </style>
{% endblock %} {% endblock %}
{% block content %}
{% block header %}
{% if images %} {% if images %}
<div class="banner"> <div class="banner">
<img src="{{ url_for('media_api.media', path='uploads/' + images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;" alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"/> <picture>
<source srcset="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev&e=png">
<img
src="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev"
alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"
onload="imgFade(this)" style="opacity:0;"
/>
</picture>
<span class="banner-filter"></span> <span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
<p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author.id) }}" class="link">By {{ group.author.username }}</a></p> <p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author.id) }}" class="link">By {{ group.author.username }}</a></p>
@ -229,12 +78,12 @@
<p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p> <p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p>
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button> <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
</div> </div>
{% if current_user.id == group.author.id %} {% if current_user.id == group.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button> <button class="pill-item pill__critical" onclick="groupDeletePopup()"><i class="ph ph-trash"></i></button>
<button class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button> <button class="pill-item pill__critical" onclick="groupEditPopup()"><i class="ph ph-pencil-simple"></i></button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -247,29 +96,24 @@
<p class="banner-info">By {{ group.author.username }}</p> <p class="banner-info">By {{ group.author.username }}</p>
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button> <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
</div> </div>
{% if current_user.id == group.author.id %} {% if current_user.id == group.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button> <button class="pill-item pill__critical" onclick="groupDeletePopup()"><i class="ph ph-trash"></i></button>
<button class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button> <button class="pill-item pill__critical" onclick="groupEditPopup()"><i class="ph ph-pencil-simple"></i></button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endblock %}
{% block content %}
{% if images %} {% if images %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% for image in images %}{{ gallery_item(image) }}{% endfor %}
<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.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter">
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<img alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a>
{% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">

View file

@ -1,215 +1,157 @@
{% extends 'layout.html' %} {% extends 'base.html' %}
{% block page_index %} {% block page_index %}
{% if return_page %}?page={{ return_page }}{% endif %}{% endblock %} {% if return_page %}?page={{ return_page }}{% endif %}{% endblock %}
{% block head %} {% block head %}
<meta property="og:image" content="{{ url_for('media_api.media', path='uploads/' + image.filename) }}"/> <meta property="og:image" content="{{ url_for('api.media', path='uploads/' + image.filename) }}"/>
<meta name="theme-color" content="rgb({{ image.colours.0.0 }}{{ image.colours.0.1 }}{{ image.colours.0.2 }})"/> <meta name="twitter:image" content="{{ url_for('api.media', path='uploads/' + image.filename) }}">
<meta name="theme-color" content="rgb{{ image.colours.0 }}"/>
<meta name="twitter:card" content="summary_large_image">
<script type="text/javascript"> <script type="text/javascript">
function imageShare() { const image_data = {
try { 'id': {{ image.id }},
navigator.clipboard.writeText(window.location.href) 'description': '{{ image.description }}',
addNotification("Copied link!", 4); 'alt': '{{ image.alt }}',
} catch (err) { };
addNotification("Failed to copy link! Are you on HTTP?", 2);
}
}
function fullscreen() {
let info = document.querySelector('.info-container');
let wrapper = document.querySelector('.image-grid');
if (info.classList.contains('collapsed')) {
info.classList.remove('collapsed');
wrapper.classList.remove('collapsed');
} else {
info.classList.add('collapsed');
wrapper.classList.add('collapsed');
}
}
{% if current_user.id == image.author.id %}
function imageDelete() {
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.classList.add('transparent');
cancelBtn.innerHTML = 'nuuuuuuuu';
cancelBtn.onclick = popupDissmiss;
deleteBtn = document.createElement('button');
deleteBtn.classList.add('btn-block');
deleteBtn.classList.add('critical');
deleteBtn.innerHTML = 'Dewww eeeet!';
deleteBtn.onclick = deleteConfirm;
popUpShow('DESTRUCTION!!!!!!',
'Do you want to delete this image along with all of its data??? ' +
'This action is irreversible!',
null,
[cancelBtn, deleteBtn]);
}
function deleteConfirm() {
popupDissmiss();
fetch('{{ url_for('media_api.delete_image', image_id=image['id']) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'delete'
})
}).then(function(response) {
if (response.ok) {
window.location.href = '/';
} else {
addNotification(`Image *clings*`, 2);
}
});
}
function imageEdit() {
addNotification("Not an option, oops!", 3);
}
{% endif %}
</script> </script>
<style> <style>
.background span { .background::after {
background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 0.8), background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 0.8),
rgba({{ image.colours.1.0 }}, {{ image.colours.1.1 }}, {{ image.colours.1.2 }}, 0.2)); rgba({{ image.colours.1.0 }}, {{ image.colours.1.1 }}, {{ image.colours.1.2 }}, 0.2));
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %}
<div class="background">
<img src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
</div>
<div class="image-grid">
<div class="image-block">
<div class="image-container">
<img
src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev"
alt="{{ image.alt }}"
onload="imgFade(this)"
style="opacity: 0;"
onerror="this.src='{{ url_for('static', filename='error.png')}}'"
{% if "File" in image.exif %}
width="{{ image.exif.File.Width.raw }}"
height="{{ image.exif.File.Height.raw }}"
{% endif %}
/>
</div>
{% block header %}
<div class="banner-small">
<div class="banner-content">
<h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
<div class="pill-row"> <div class="pill-row">
{% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %} {% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %}
<div> <div>
<button class="pill-item" onclick="fullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button> <button class="pill-item" onclick="imageFullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button>
<button class="pill-item" onclick="imageShare()"><i class="ph ph-export"></i></button> <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
<a class="pill-item" href="{{ url_for('media_api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a> <a class="pill-item" href="{{ url_for('api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a>
</div> </div>
{% if current_user.id == image.author.id %} {% if current_user.id == image.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="imageDelete()"><i class="ph ph-trash"></i></button> <button class="pill-item pill__critical" onclick="imageDeletePopup()"><i class="ph ph-trash"></i></button>
<button class="pill-item pill__critical" onclick="imageEdit()"><i class="ph ph-pencil-simple"></i></button> <button class="pill-item pill__critical" onclick="imageEditPopup()"><i class="ph ph-pencil-simple"></i></button>
</div> </div>
{% endif %} {% endif %}
{% if prev_url %}<div><a class="pill-item" href="{{ prev_url }}"><i class="ph ph-arrow-right"></i></a></div>{% endif %} {% if prev_url %}<div><a class="pill-item" href="{{ prev_url }}"><i class="ph ph-arrow-right"></i></a></div>{% endif %}
</div> </div>
</div> </div>
<div class="info-container">
<div class="info-tab">
<div class="info-header">
<i class="ph ph-info"></i>
<h2>Info</h2>
<button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
</div>
<div class="info-table">
<table>
<tr>
<td>Author</td>
<td><a href="{{ url_for('profile.profile', id=image.author.id) }}" class="link">{{ image.author.username }}</a></td>
</tr>
<tr>
<td>Upload date</td>
<td><span class="time">{{ image.created_at }}</span></td>
</tr>
{% if image.description %}
<tr>
<td>Description</td>
<td>{{ image.description }}</td>
</tr>
{% endif %}
</table>
<div class="img-colours">
{% for col in image.colours %}
<span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
{% endfor %}
</div>
{% if image.groups %}
<div class="img-groups">
{% for group in image.groups %}
<a href="{{ url_for('group.group', group_id=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="M223.68,66.15,135.68,18a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM128,32l80.34,44-29.77,16.3-80.35-44ZM128,120,47.66,76l33.9-18.56,80.34,44ZM40,90l80,43.78v85.79L40,175.82Zm176,85.78h0l-80,43.79V133.82l32-17.51V152a8,8,0,0,0,16,0V107.55L216,90v85.77Z"></path></svg>
{{ group['name'] }}
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% for tag in image.exif %}
<div class="info-tab">
<div class="info-header">
{% if tag == 'Photographer' %}
<i class="ph ph-person"></i><h2>Photographer</h2>
{% elif tag == 'Camera' %}
<i class="ph ph-camera"></i><h2>Camera</h2>
{% elif tag == 'Software' %}
<i class="ph ph-desktop-tower"></i><h2>Software</h2>
{% elif tag == 'File' %}
<i class="ph ph-file-image"></i><h2>File</h2>
{% else %}
<i class="ph ph-file-image"></i><h2>{{ tag }}</h2>
{% endif %}
<button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
</div>
<div class="info-table">
<table>
{% for subtag in image.exif[tag] %}
<tr>
<td>{{ subtag }}</td>
{% if image.exif[tag][subtag]['formatted'] %}
{% if image.exif[tag][subtag]['type'] == 'date' %}
<td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
{% else %}
<td>{{ image.exif[tag][subtag]['formatted'] }}</td>
{% endif %}
{% elif image.exif[tag][subtag]['raw'] %}
<td>{{ image.exif[tag][subtag]['raw'] }}</td>
{% else %}
<td class="empty-table">Oops, an error</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
</div>
{% endfor %}
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block script %} {% block content %}
<script type="text/javascript"> <div class="background">
let infoTab = document.querySelectorAll('.info-tab'); <picture>
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
<img src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
</picture>
</div>
for (let i = 0; i < infoTab.length; i++) { <div class="image-container {% if close_tab %}collapsed{% endif %}">
infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() { <picture>
infoTab[i].classList.toggle('collapsed'); <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
}); <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
} <img
</script> src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev"
alt="{{ image.alt }}"
onload="imgFade(this)"
style="opacity:0;"
{% if "File" in image.exif %}
width="{{ image.exif.File.Width.raw }}"
height="{{ image.exif.File.Height.raw }}"
{% endif %}
/>
</picture>
</div>
<div class="info-container {% if close_tab %}collapsed{% endif %}">
<details open>
<summary>
<i class="ph ph-info"></i><h2>Info</h2><span style="width: 100%"></span>
<i class="ph ph-caret-down collapse-indicator"></i>
</summary>
<table>
<tr>
<td>Author</td>
<td>
{% if image.author.picture %}
<img src="{{ url_for('api.media', path='pfp/' + image.author.picture) }}" alt="Profile Picture" class="pfp" onload="imgFade(this)" style="opacity: 0;"/>
{% endif %}
<a href="{{ url_for('profile.profile', id=image.author.id) }}" class="link">{{ image.author.username }}</a>
</td>
</tr>
<tr>
<td>Upload date</td>
<td><span class="time">{{ image.created_at }}</span></td>
</tr>
{% if image.description %}
<tr>
<td>Description</td>
<td>{{ image.description }}</td>
</tr>
{% endif %}
</table>
<div class="img-colours">
{% for col in image.colours %}
<button style="background-color: rgb{{ col }}" onclick="copyToClipboard('rgb{{ col }}')">
<i class="ph-fill ph-paint-bucket" style="color:{{ col|colour_contrast }};"></i>
</button>
{% endfor %}
</div>
{% if image.groups %}
<div class="img-groups">
{% for group in image.groups %}
<a href="{{ url_for('group.group', group_id=group.id) }}" class="tag-icon"><i class="ph ph-package"></i>{{ group['name'] }}</a>
{% endfor %}
</div>
{% endif %}
</details>
{% for tag in image.exif %}
<details open>
<summary>
{% if tag == 'Photographer' %}
<i class="ph ph-person"></i><h2>Photographer</h2>
{% elif tag == 'Camera' %}
<i class="ph ph-camera"></i><h2>Camera</h2>
{% elif tag == 'Software' %}
<i class="ph ph-desktop-tower"></i><h2>Software</h2>
{% elif tag == 'File' %}
<i class="ph ph-file-image"></i><h2>File</h2>
{% else %}
<i class="ph ph-file-image"></i><h2>{{ tag }}</h2>
{% endif %}
<span style="width: 100%"></span>
<i class="ph ph-caret-down collapse-indicator"></i>
</summary>
<table>
{% for subtag in image.exif[tag] %}
<tr>
<td>{{ subtag }}</td>
{% if image.exif[tag][subtag]['formatted'] %}
{% if image.exif[tag][subtag]['type'] == 'date' %}
<td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
{% else %}
<td>{{ image.exif[tag][subtag]['formatted'] }}</td>
{% endif %}
{% elif image.exif[tag][subtag]['raw'] %}
<td>{{ image.exif[tag][subtag]['raw'] }}</td>
{% else %}
<td class="empty-table">Oops, an error</td>
{% endif %}
</tr>
{% endfor %}
</table>
</details>
{% endfor %}
</div>
{% endblock %} {% endblock %}

View file

@ -1,11 +1,17 @@
{% extends 'layout.html' %} {% extends 'base.html' %}
{% block nav_home %}selected{% endblock %} {% from 'macros/image.html' import gallery_item %}
{% block content %} {% block head %}
<meta property="og:image" content="{{ url_for('static', filename='icon.png') }}"/>
<meta name="twitter:image" content="{{ url_for('static', filename='icon.png') }}"/>
<meta name="twitter:card" content="summary"/>
{% endblock %}
{% block header %}
<div class="banner-small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1> <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
{% if total_images == 0 %} {% if not total_images %}
<p class="banner-info">0 images D:</p> <p class="banner-info">0 images!</p>
{% elif total_images == 69 %} {% elif total_images == 69 %}
<p class="banner-info">{{ total_images }} images, nice</p> <p class="banner-info">{{ total_images }} images, nice</p>
{% else %} {% else %}
@ -15,29 +21,33 @@
{% if pages > 1 %} {% if pages > 1 %}
<div class="pill-row"> <div class="pill-row">
<div> <div>
{% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index') }}"><i class="ph ph-arrow-line-left"></i></a>{% endif %} {% if pages > 4 %}
<a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page-1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}"><i class="ph ph-arrow-left"></i></a> <a class="pill-item" href="{{ url_for('gallery.index') }}"><i class="ph ph-caret-double-left"></i></a>
{% else %}
<button class="pill-item disabled"><i class="ph ph-caret-double-left"></i></button>
{% endif %}
<a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page - 1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}"><i class="ph ph-caret-left"></i></a>
</div> </div>
<span class="pill-text">{{ page }} / {{ pages }}</span> <span class="pill-text">{{ page }} / {{ pages }}</span>
<div> <div>
<a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page+1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}"><i class="ph ph-arrow-right"></i></a> <a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page + 1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}"><i class="ph ph-caret-right"></i></a>
{% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}"><i class="ph ph-arrow-line-right"></i></a>{% endif %} {% if pages > 4 %}
<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}"><i class="ph ph-caret-double-right"></i></a>
{% else %}
<button class="pill-item disabled"><i class="ph ph-caret-double-right"></i></button>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %}
{% block nav_home %}selected{% endblock %}
{% block content %}
{% if images %} {% if images %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% for image in images %}{{ gallery_item(image) }}{% endfor %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('image.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter">
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<img fetchpriority="low" alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a>
{% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">

View file

@ -1,157 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ config.WEBSITE_CONF.name }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta name="author" content="{{ config.WEBSITE_CONF.author }}"/>
<meta property="og:title" content="{{ config.WEBSITE_CONF.name }}"/>
<meta property="og:description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta property="og:type" content="website"/>
<meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}"/>
<meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta name="twitter:card" content="summary_large_image">
<link rel="manifest" href="static/manifest.json"/>
<!-- phosphor icons!!! -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<link
href="{{url_for('static', filename='logo-black.svg')}}"
rel="icon"
type="image/svg+xml"
media="(prefers-color-scheme: light)"/>
<link
href="{{url_for('static', filename='logo-white.svg')}}"
rel="icon"
type="image/svg+xml"
media="(prefers-color-scheme: dark)"/>
<link
rel="prefetch"
href="{{url_for('static', filename='fonts/font.css')}}"
type="stylesheet"/>
{% assets "scripts" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets "styles" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer>
{% endassets %}
{% block head %}{% endblock %}
</head>
<body>
<div class="notifications"></div>
<button class="top-of-page" aria-label="Jump to top of page"><i class="ph ph-arrow-up"></i></button>
{% if request.path == "/" %}<button class="info-button" aria-label="Show info on gallery"><i class="ph ph-question"></i></button>{% endif %}
<div class="pop-up">
<span class="pop-up__click-off" onclick="popupDissmiss()"></span>
<div class="pop-up-wrapper">
<div class="pop-up-header"></div>
<div class="pop-up-controlls"></div>
</div>
</div>
<div class="wrapper">
<div class="navigation">
<!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">-->
<a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}">
<i class="ph-fill ph-images-square"></i>
<span class="tool-tip">Home<i class="ph-fill ph-caret-left"></i></span>
</a>
<a href="{{ url_for('group.groups') }}" class="navigation-item {% block nav_groups %}{% endblock %}">
<i class="ph-fill ph-package"></i>
<span class="tool-tip">Groups<i class="ph-fill ph-caret-left"></i></span>
</a>
{% if current_user.is_authenticated %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
<i class="ph-fill ph-upload"></i>
<span class="tool-tip">Upload<i class="ph-fill ph-caret-left"></i></span>
</button>
{% endif %}
<span class="navigation-spacer"></span>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}">
{% if current_user.picture %}
<span class="nav-pfp">
<img
src="{{ url_for('media_api.media', path='pfp/' + current_user.picture) }}?r=icon"
alt="Profile picture"
onload="imgFade(this)"
style="opacity:0;"
/>
</span>
{% else %}
<i class="ph-fill ph-folder-simple-user"></i>
{% endif %}
<span class="tool-tip">Profile<i class="ph-fill ph-caret-left"></i></span>
</a>
<a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}">
<i class="ph-fill ph-gear-fine"></i>
<span class="tool-tip">Settings<i class="ph-fill ph-caret-left"></i></span>
</a>
{% else %}
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
<i class="ph-fill ph-sign-in"></i>
<span class="tool-tip">Login<i class="ph-fill ph-caret-left"></i></span>
</button>
{% endif %}
</div>
{% if current_user.is_authenticated %}
<div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span>
<div class="container">
<span id="dragIndicator"></span>
<h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p>
<form id="uploadForm">
<button class="fileDrop-block" type="button">
<i class="ph ph-upload"></i>
<span class="status">Choose or Drop file</span>
<input type="file" id="file" tab-index="-1"/>
</button>
<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>
<script type="text/javascript">
// Show notifications on page load
{% for message in get_flashed_messages() %}
addNotification('{{ message[0] }}', {{ message[1] }});
{% endfor %}
</script>
{% block script %}{% endblock %}
</body>
</html>

View file

@ -1,100 +1,11 @@
{% extends 'layout.html' %} {% extends 'base.html' %}
{% block nav_groups %}selected{% endblock %} {% block nav_groups %}selected{% endblock %}
{% block head %} {% block head %}
{% if images %} {% if images %}<meta name="theme-color" content="rgb{{ images.0.colours.0 }}"/>{% endif %}
<meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/>
{% endif %}
{% if current_user.is_authenticated %}
<script type="text/javascript">
function showCreate() {
// Create elements
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.classList.add('transparent');
cancelBtn.innerHTML = 'nuuuuuuuu';
cancelBtn.onclick = popupDissmiss;
submitBtn = document.createElement('button');
submitBtn.classList.add('btn-block');
submitBtn.classList.add('primary');
submitBtn.innerHTML = 'Submit!!';
submitBtn.type = 'submit';
submitBtn.setAttribute('form', 'createForm');
// Create form
createForm = document.createElement('form');
createForm.id = 'createForm';
createForm.setAttribute('onsubmit', 'return create(event);');
titleInput = document.createElement('input');
titleInput.classList.add('input-block');
titleInput.type = 'text';
titleInput.placeholder = 'Group namey';
titleInput.id = 'name';
descriptionInput = document.createElement('input');
descriptionInput.classList.add('input-block');
descriptionInput.type = 'text';
descriptionInput.placeholder = 'What it about????';
descriptionInput.id = 'description';
createForm.appendChild(titleInput);
createForm.appendChild(descriptionInput);
popUpShow(
'New stuff!',
'Image groups are a simple way to "group" images together, are you ready?',
createForm,
[cancelBtn, submitBtn]
);
}
function create(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let formName = document.querySelector("#name").value;
let formDescription = document.querySelector("#description").value;
if (!formName) {
addNotification("Group name must be set!", 3);
return;
}
// Make form
const formData = new FormData();
formData.append("name", formName);
formData.append("description", formDescription);
fetch('{{ url_for('group_api.create_group') }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
addNotification('Group created!', 1);
popupDissmiss();
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Bad information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
}
}
}).catch(error => {
addNotification('Error making group! :c', 2);
});
}
</script>
{% endif %}
{% endblock %} {% endblock %}
{% block content %}
{% block header %}
<div class="banner-small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1> <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
@ -108,17 +19,24 @@
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="showCreate()"><i class="ph ph-plus"></i></button> <button class="pill-item" onclick="groupCreatePopup()"><i class="ph ph-plus"></i></button>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %}
{% block content %}
{% if groups %} {% if groups %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for group in groups %} {% for group in groups %}
<a id="group-{{ group.id }}" class="group-item" href="{{ url_for('group.group', group_id=group.id) }}" {% if group.images|length > 0 %} style="background-color: rgba({{ group.images.0.colours.0.0 }}, {{ group.images.0.colours.0.1 }}, {{ group.images.0.colours.0.2 }}, 0.4);" {% endif %}> <a
class="group-item square"
id="group-{{ group.id }}"
href="{{ url_for('group.group', group_id=group.id) }}"
{% if group.images|length > 0 %} style="background-color: rgba{{ group.images.0.colours.0 }};"{% endif %}
>
<div class="image-filter"> <div class="image-filter">
<p class="image-subtitle">By {{ group.author.username }}</p> <p class="image-subtitle">By {{ group.author.username }}</p>
<p class="image-title">{{ group.name }}</p> <p class="image-title">{{ group.name }}</p>
@ -126,7 +44,18 @@
<div class="images size-{{ group.images|length }}"> <div class="images size-{{ group.images|length }}">
{% if group.images|length > 0 %} {% if group.images|length > 0 %}
{% for image in group.images %} {% for image in group.images %}
<img data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load" class="data-{{ loop.index }}" {% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}/> <picture>
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
<img
src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb"
alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}"
class="data-{{ loop.index }}"
onload="imgFade(this)"
style="opacity:0;"
fetchpriority="low"
/>
</picture>
{% endfor %} {% endfor %}
{% else %} {% else %}
<img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/> <img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/>

View file

@ -0,0 +1,9 @@
{% macro header_small(title, subtitle, buttons) %}
<div class="banner-small">
<div class="banner-content">
<h1 class="banner-header">{{ title }}</h1>
<p class="banner-info">{{ subtitle }}</p>
<div class="pill-row">{{ buttons }}</div>
</div>
</div>
{% endmacro %}

View file

@ -0,0 +1,23 @@
{% macro gallery_item(image) %}
<a
id="image-{{ image.id }}"
class="gallery-item square"
href="{{ url_for('image.image', image_id=image.id) }}"
style="background-color: rgb{{ image.colours.0 }}"
draggable="false">
<div class="image-filter">
<p class="image-subtitle">By {{ image.username }}</p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<picture>
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
<img
src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb"
alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}"
onload="imgFade(this)"
style="opacity:0;"
/>
</picture>
</a>
{% endmacro %}

View file

@ -1,15 +1,18 @@
{% extends 'layout.html' %} {% extends 'base.html' %}
{% from 'macros/image.html' import gallery_item %}
{% block nav_profile %}{% if user.id == current_user.id %}selected{% endif %}{% endblock %}
{% block head %} {% block head %}
{% if user.picture %} {% if user.picture %}
<meta property="og:image" content="{{ url_for('media_api.media', path='pfp/' + user.picture) }}"/> <meta property="og:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}"/>
{% endif %} <meta name="twitter:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}">
{% if user.colour %}
<meta name="theme-color" content="rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }})"/>
{% endif %} {% endif %}
{% if user.colour %}<meta name="theme-color" content="rgb{{ user.colour }}"/>{% endif %}
<meta name="twitter:card" content="summary">
<script type="text/javascript"> <script type="text/javascript">
function moreInfo() { function moreInfo() {
popUpShow('{{ user.username }}', popupShow('{{ user.username }}',
'<p>Joined: {{ user.joined_at }}</p><br>' + '<p>Joined: {{ user.joined_at }}</p><br>' +
'<p>Images: {{ images|length }}</p><br>' + '<p>Images: {{ images|length }}</p><br>' +
'<p>Groups: {{ groups|length }}</p>'); '<p>Groups: {{ groups|length }}</p>');
@ -18,12 +21,12 @@
<style> <style>
.banner-picture { .banner-picture {
background-color: rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }}) !important; background-color: rgb{{ user.colour }} !important;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block nav_profile %}{% if user.id == current_user.id %}selected{% endif %}{% endblock %}
{% block content %} {% block header %}
<div class="banner"> <div class="banner">
{% if user.banner %} {% if user.banner %}
<img src="{{ url_for('static', filename='icon.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/> <img src="{{ url_for('static', filename='icon.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/>
@ -33,13 +36,16 @@
<span class="banner-filter"></span> <span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
{% if user.picture %} {% if user.picture %}
<img <picture class="banner-picture">
class="banner-picture" <source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=webp">
src="{{ url_for('media_api.media', path='pfp/' + user.picture) }}?r=pfp" <source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=png">
alt="Profile picture" <img
onload="imgFade(this)" src="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp"
style="opacity:0;" alt="Profile picture"
/> onload="imgFade(this)"
style="opacity:0;"
/>
</picture>
{% else %} {% else %}
<img <img
class="banner-picture" class="banner-picture"
@ -53,7 +59,7 @@
<p class="banner-subtitle">{{ images|length }} Images · {{ groups|length }} Groups</p> <p class="banner-subtitle">{{ images|length }} Images · {{ groups|length }} Groups</p>
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="profileShare()"><i class="ph ph-export"></i></button> <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
<button class="pill-item" onclick="moreInfo()"><i class="ph ph-info"></i></button> <button class="pill-item" onclick="moreInfo()"><i class="ph ph-info"></i></button>
</div> </div>
{% if user.id == current_user.id %} {% if user.id == current_user.id %}
@ -64,18 +70,13 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block content %}
{% if images %} {% if images %}
<h1 class="gallery-header">Images</h1> <h1 class="gallery-header">Images</h1>
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% for image in images %}{{ gallery_item(image) }}{% endfor %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('image.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter">
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<img fetchpriority="low" alt="{{ image.alt }}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a>
{% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">
@ -84,4 +85,3 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,7 +1,7 @@
{% extends 'layout.html' %} {% extends 'base.html' %}
{% block nav_settings %}selected{% endblock %} {% block nav_settings %}selected{% endblock %}
{% block content %}
{% block header %}
<div class="banner-small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1 class="banner-header">Settings</h1> <h1 class="banner-header">Settings</h1>
@ -13,28 +13,53 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<div class="settings-content" id="profileSettings"> {% block content %}
<h2>Profile Settings</h2> <div class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem">
<form method="POST" action="{{ url_for('account_api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data"> <div class="info-header">
<h3>Profile Picture</h3> <i class="ph ph-info"></i>
<input type="file" name="file" tab-index="-1"/> <h2>Profile Settings</h2>
<input type="submit" value="Upload" class="btn-block"> <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
</form> </div>
<div class="info-table">
<form method="POST" action="{{ url_for('account_api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data"> <form method="POST" action="{{ url_for('api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data">
<h3>Username</h3> <h3>Profile Picture</h3>
<input type="text" name="name" class="input-block" value="{{ current_user.username }}" /> <input type="file" name="file" tab-index="-1"/>
<input type="submit" value="Upload" class="btn-block"/> <input type="submit" value="Upload" class="btn-block">
</form> </form>
<form method="POST" action="{{ url_for('api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data">
<h3>Username</h3>
<input type="text" name="name" class="input-block" value="{{ current_user.username }}" />
<input type="submit" value="Upload" class="btn-block"/>
</form>
</div>
</div> </div>
<div class="settings-content" id="profileSettings"> <div class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem">
<h2>Account Settings</h2> <div class="info-header">
<form method="POST" action="" enctype="multipart/form-data"> <i class="ph ph-info"></i>
<h3>Email</h3> <h2>Account Settings</h2>
<input type="text" name="email" class="input-block" value="{{ current_user.email }}" /> <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
<input type="submit" value="Upload" class="btn-block"/> </div>
</form> <div class="info-table">
<form method="POST" action="" enctype="multipart/form-data">
<h3>Email</h3>
<input type="text" name="email" class="input-block" value="{{ current_user.email }}" />
<input type="submit" value="Upload" class="btn-block"/>
</form>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block script %}
<script type="text/javascript">
let infoTab = document.querySelectorAll('.info-tab');
for (let i = 0; i < infoTab.length; i++) {
infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() {
infoTab[i].classList.toggle('collapsed');
});
}
</script>
{% endblock %}

69
onlylegs/utils/colour.py Normal file
View file

@ -0,0 +1,69 @@
"""
Colour tools used by OnlyLegs
Source 1: https://gist.github.com/mathebox/e0805f72e7db3269ec22
"""
class Colour:
def __init__(self, rgb):
self.rgb = rgb
def is_light(self, threshold=0.179):
"""
returns True if background is light, False if dark
threshold: the threshold to use for determining lightness, the default is w3 recommended
"""
red, green, blue = self.rgb
# Calculate contrast
colors = [red / 255, green / 255, blue / 255]
cont = [
col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4
for col in colors
]
lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
return True if lightness > threshold else False
def to_hsv(self):
r, g, b = self.rgb
high = max(r, g, b)
low = min(r, g, b)
h, s, v = high, high, high
d = high - low
s = 0 if high == 0 else d / high
if high == low:
h = 0.0
else:
h = {
r: (g - b) / d + (6 if g < b else 0),
g: (b - r) / d + 2,
b: (r - g) / d + 4,
}[high]
h /= 6
return h, s, v
def to_hsl(self):
r, g, b = self.rgb
high = max(r, g, b)
low = min(r, g, b)
h, s, v = ((high + low) / 2,) * 3
if high == low:
h = 0.0
s = 0.0
else:
d = high - low
s = d / (2 - high - low) if l > 0.5 else d / (high + low)
h = {
r: (g - b) / d + (6 if g < b else 0),
g: (b - r) / d + 2,
b: (r - g) / d + 4,
}[high]
h /= 6
return h, s, v

View file

@ -1,25 +0,0 @@
"""
Calculate the contrast between two colors
"""
def contrast(background, light, dark, threshold=0.179):
"""
background: tuple of (r, g, b) values
light: color to use if the background is light
dark: color to use if the background is dark
threshold: the threshold to use for determining lightness, the default is w3 recommended
"""
red = background[0]
green = background[1]
blue = background[2]
# Calculate contrast
uicolors = [red / 255, green / 255, blue / 255]
cont = [
col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4
for col in uicolors
]
lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
return light if lightness > threshold else dark

View file

@ -7,7 +7,7 @@ from werkzeug.utils import secure_filename
from onlylegs.config import MEDIA_FOLDER, CACHE_FOLDER from onlylegs.config import MEDIA_FOLDER, CACHE_FOLDER
def generate_thumbnail(file_path, resolution, ext=None): def generate_thumbnail(file_path, resolution, ext=""):
""" """
Image thumbnail generator Image thumbnail generator
Uses PIL to generate a thumbnail of the image and saves it to the cache directory Uses PIL to generate a thumbnail of the image and saves it to the cache directory
@ -25,25 +25,25 @@ def generate_thumbnail(file_path, resolution, ext=None):
if not ext: if not ext:
ext = file_ext.strip(".") ext = file_ext.strip(".")
# PIL doesnt like jpg so we convert it to jpeg ext = "jpeg" if ext.lower() == "jpg" else ext.lower()
if ext.lower() == "jpg":
ext = "jpeg"
# Set resolution based on preset resolutions # Set resolution based on preset resolutions
if resolution in ["prev", "preview"]: if resolution in ("prev", "preview"):
res_x, res_y = (1920, 1080) res_x, res_y = (1920, 1080)
elif resolution in ["thumb", "thumbnail"]: elif resolution in ("thumb", "thumbnail"):
res_x, res_y = (400, 400) res_x, res_y = (300, 300)
elif resolution in ["pfp", "profile"]: elif resolution in ("pfp", "profile"):
res_x, res_y = (200, 200) res_x, res_y = (150, 150)
elif resolution in ["icon", "favicon"]: elif resolution in ("icon", "favicon"):
res_x, res_y = (25, 25) res_x, res_y = (30, 30)
else: else:
return None return None
cache_file_name = "{}_{}x{}.{}".format(file_name, res_x, res_y, ext).lower()
# If image has been already generated, return it from the cache # If image has been already generated, return it from the cache
if os.path.exists(os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}")): if os.path.exists(os.path.join(CACHE_FOLDER, cache_file_name)):
return os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}") return os.path.join(CACHE_FOLDER, cache_file_name)
# Check if image exists in the uploads directory # Check if image exists in the uploads directory
if not os.path.exists(os.path.join(MEDIA_FOLDER, file_path)): if not os.path.exists(os.path.join(MEDIA_FOLDER, file_path)):
@ -61,7 +61,7 @@ def generate_thumbnail(file_path, resolution, ext=None):
# Save image to cache directory # Save image to cache directory
try: try:
image.save( image.save(
os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}"), os.path.join(CACHE_FOLDER, cache_file_name),
icc_profile=image_icc, icc_profile=image_icc,
) )
except OSError: except OSError:
@ -69,11 +69,11 @@ def generate_thumbnail(file_path, resolution, ext=None):
# so we convert to RGB and try again # so we convert to RGB and try again
image = image.convert("RGB") image = image.convert("RGB")
image.save( image.save(
os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}"), os.path.join(CACHE_FOLDER, cache_file_name),
icc_profile=image_icc, icc_profile=image_icc,
) )
# No need to keep the image in memory, learned the hard way # No need to keep the image in memory, learned the hard way
image.close() image.close()
return os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}") return os.path.join(CACHE_FOLDER, cache_file_name)

View file

@ -12,90 +12,74 @@ from .helpers import *
from .mapping import * from .mapping import *
class Metadata: def yoink(file_path):
""" """
Metadata parser Initialize the metadata parser
""" """
if not os.path.isfile(file_path):
return None
def __init__(self, file_path): img_exif = {}
""" file = Image.open(file_path)
Initialize the metadata parser
"""
self.file_path = file_path
img_exif = {}
try: img_exif["FileName"] = os.path.basename(file_path)
file = Image.open(file_path) img_exif["FileSize"] = os.path.getsize(file_path)
tags = file._getexif() img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
img_exif = {} img_exif["FileWidth"], img_exif["FileHeight"] = file.size
for tag, value in TAGS.items(): try:
if tag in tags: tags = file._getexif()
img_exif[value] = tags[tag] for tag, value in TAGS.items():
if tag in tags:
img_exif[value] = tags[tag]
except TypeError:
pass
img_exif["FileName"] = os.path.basename(file_path) file.close()
img_exif["FileSize"] = os.path.getsize(file_path)
img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
img_exif["FileWidth"], img_exif["FileHeight"] = file.size
file.close() return _format_data(img_exif)
except TypeError:
img_exif["FileName"] = os.path.basename(file_path)
img_exif["FileSize"] = os.path.getsize(file_path)
img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
img_exif["FileWidth"], img_exif["FileHeight"] = file.size
self.encoded = img_exif
def yoink(self): def _format_data(encoded):
""" """
Yoinks the metadata from the image Formats the data into a dictionary
""" """
if not os.path.isfile(self.file_path): exif = {
return None "Photographer": {},
return self.format_data(self.encoded) "Camera": {},
"Software": {},
"File": {},
}
@staticmethod # Thanks chatGPT xP
def format_data(encoded_exif): # the helper function works, so not sure why it triggers pylint
""" for key, value in encoded.items():
Formats the data into a dictionary for mapping_name, mapping_val in EXIF_MAPPING:
""" if key in mapping_val:
exif = { if len(mapping_val[key]) == 2:
"Photographer": {}, exif[mapping_name][mapping_val[key][0]] = {
"Camera": {}, "raw": value,
"Software": {}, "formatted": (
"File": {}, getattr(
} helpers, # pylint: disable=E0602
mapping_val[key][1],
)(value)
),
}
else:
exif[mapping_name][mapping_val[key][0]] = {
"raw": value,
}
continue
# Thanks chatGPT xP # Remove empty keys
# the helper function works, so not sure why it triggers pylint if not exif["Photographer"]:
for key, value in encoded_exif.items(): del exif["Photographer"]
for mapping_name, mapping_val in EXIF_MAPPING: if not exif["Camera"]:
if key in mapping_val: del exif["Camera"]
if len(mapping_val[key]) == 2: if not exif["Software"]:
exif[mapping_name][mapping_val[key][0]] = { del exif["Software"]
"raw": value, if not exif["File"]:
"formatted": ( del exif["File"]
getattr(
helpers, # pylint: disable=E0602
mapping_val[key][1],
)(value)
),
}
else:
exif[mapping_name][mapping_val[key][0]] = {
"raw": value,
}
continue
# Remove empty keys return exif
if not exif["Photographer"]:
del exif["Photographer"]
if not exif["Camera"]:
del exif["Camera"]
if not exif["Software"]:
del exif["Software"]
if not exif["File"]:
del exif["File"]
return exif

View file

@ -3,11 +3,11 @@ Onlylegs - Image Groups
Why groups? Because I don't like calling these albums Why groups? Because I don't like calling these albums
sounds more limiting that it actually is in this gallery sounds more limiting that it actually is in this gallery
""" """
from flask import Blueprint, render_template, url_for from flask import Blueprint, render_template, url_for, request, flash, jsonify
from flask_login import login_required, current_user
from onlylegs.models import Post, User, GroupJunction, Group from onlylegs.models import Pictures, Users, AlbumJunction, Albums
from onlylegs.extensions import db from onlylegs.extensions import db
from onlylegs.utils import contrast from onlylegs.utils import colour
blueprint = Blueprint("group", __name__, url_prefix="/group") blueprint = Blueprint("group", __name__, url_prefix="/group")
@ -18,21 +18,21 @@ def groups():
""" """
Group overview, shows all image groups Group overview, shows all image groups
""" """
groups = Group.query.all() groups = Albums.query.all()
# For each group, get the 3 most recent images # For each group, get the 3 most recent images
for group in groups: for group in groups:
group.author_username = ( group.author_username = (
User.query.with_entities(User.username) Users.query.with_entities(Users.username)
.filter(User.id == group.author_id) .filter(Users.id == group.author_id)
.first()[0] .first()[0]
) )
# Get the 3 most recent images # Get the 3 most recent images
images = ( images = (
GroupJunction.query.with_entities(GroupJunction.post_id) AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(GroupJunction.group_id == group.id) .filter(AlbumJunction.album_id == group.id)
.order_by(GroupJunction.date_added.desc()) .order_by(AlbumJunction.date_added.desc())
.limit(3) .limit(3)
) )
@ -40,40 +40,67 @@ def groups():
group.images = [] group.images = []
for image in images: for image in images:
group.images.append( group.images.append(
Post.query.with_entities(Post.filename, Post.alt, Post.colours, Post.id) Pictures.query.with_entities(
.filter(Post.id == image[0]) Pictures.filename, Pictures.alt, Pictures.colours, Pictures.id
)
.filter(Pictures.id == image[0])
.first() .first()
) )
return render_template("list.html", groups=groups) return render_template("list.html", groups=groups)
@blueprint.route("/<int:group_id>") @blueprint.route("/", methods=["POST"])
@login_required
def groups_post():
"""
Creates a group
"""
group_name = request.form.get("name", "").strip()
group_description = request.form.get("description", "").strip()
new_group = Albums(
name=group_name,
description=group_description,
author_id=current_user.id,
)
db.session.add(new_group)
db.session.commit()
flash(["Group created!", "1"])
return jsonify({"message": "Group created", "id": new_group.id})
@blueprint.route("/<int:group_id>", methods=["GET"])
def group(group_id): def group(group_id):
""" """
Group view, shows all images in a group Group view, shows all images in a group
""" """
# Get the group, if it doesn't exist, 404 # Get the group, if it doesn't exist, 404
group = db.get_or_404(Group, group_id, description="Group not found! D:") group = db.get_or_404(Albums, group_id, description="Group not found! D:")
# Get all images in the group from the junction table # Get all images in the group from the junction table
junction = ( junction = (
GroupJunction.query.with_entities(GroupJunction.post_id) AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(GroupJunction.group_id == group_id) .filter(AlbumJunction.album_id == group_id)
.order_by(GroupJunction.date_added.desc()) .order_by(AlbumJunction.date_added.desc())
.all() .all()
) )
# Get the image data for each image in the group # Get the image data for each image in the group
images = [] images = []
for image in junction: for image in junction:
images.append(Post.query.filter(Post.id == image[0]).first()) images.append(Pictures.query.filter(Pictures.id == image[0]).first())
# Check contrast for the first image in the group for the banner # Check contrast for the first image in the group for the banner
text_colour = "rgb(var(--fg-black))" text_colour = "rgb(var(--fg-black))"
if images: if images:
text_colour = contrast.contrast( colour_obj = colour.Colour(images[0].colours[0])
images[0].colours[0], "rgb(var(--fg-black))", "rgb(var(--fg-white))" text_colour = (
"rgb(var(--fg-black));"
if colour_obj.is_light()
else "rgb(var(--fg-white));"
) )
return render_template( return render_template(
@ -81,18 +108,66 @@ def group(group_id):
) )
@blueprint.route("/<int:group_id>", methods=["PUT"])
@login_required
def group_put(group_id):
"""
Changes the images in a group
"""
image_id = request.form.get("imageId", "").strip()
action = request.form.get("action", "").strip()
group_record = db.get_or_404(Albums, group_id)
db.get_or_404(Pictures, image_id) # Check if image exists
if group_record.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
junction_exist = AlbumJunction.query.filter_by(
album_id=group_id, picture_id=image_id
).first()
if action == "add" and not junction_exist:
db.session.add(AlbumJunction(album_id=group_id, picture_id=image_id))
elif request.form["action"] == "remove":
AlbumJunction.query.filter_by(album_id=group_id, picture_id=image_id).delete()
db.session.commit()
flash(["Group modified!", "1"])
return jsonify({"message": "Group modified"})
@blueprint.route("/<int:group_id>", methods=["DELETE"])
@login_required
def group_delete(group_id):
"""
Deletes a group
"""
group_record = db.get_or_404(Albums, group_id)
if group_record.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
AlbumJunction.query.filter_by(album_id=group_id).delete()
db.session.delete(group_record)
db.session.commit()
flash(["Group yeeted!", "1"])
return jsonify({"message": "Group deleted"})
@blueprint.route("/<int:group_id>/<int:image_id>") @blueprint.route("/<int:group_id>/<int:image_id>")
def group_post(group_id, image_id): def group_post(group_id, image_id):
""" """
Image view, shows the image and its metadata from a specific group Image view, shows the image and its metadata from a specific group
""" """
# Get the image, if it doesn't exist, 404 # Get the image, if it doesn't exist, 404
image = db.get_or_404(Post, image_id, description="Image not found :<") image = db.get_or_404(Pictures, image_id, description="Image not found :<")
# Get all groups the image is in # Get all groups the image is in
groups = ( groups = (
GroupJunction.query.with_entities(GroupJunction.group_id) AlbumJunction.query.with_entities(AlbumJunction.album_id)
.filter(GroupJunction.post_id == image_id) .filter(AlbumJunction.picture_id == image_id)
.all() .all()
) )
@ -100,24 +175,24 @@ def group_post(group_id, image_id):
image.groups = [] image.groups = []
for group in groups: for group in groups:
image.groups.append( image.groups.append(
Group.query.with_entities(Group.id, Group.name) Albums.query.with_entities(Albums.id, Albums.name)
.filter(Group.id == group[0]) .filter(Albums.id == group[0])
.first() .first()
) )
# Get the next and previous images in the group # Get the next and previous images in the group
next_url = ( next_url = (
GroupJunction.query.with_entities(GroupJunction.post_id) AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(GroupJunction.group_id == group_id) .filter(AlbumJunction.album_id == group_id)
.filter(GroupJunction.post_id > image_id) .filter(AlbumJunction.picture_id > image_id)
.order_by(GroupJunction.date_added.asc()) .order_by(AlbumJunction.date_added.asc())
.first() .first()
) )
prev_url = ( prev_url = (
GroupJunction.query.with_entities(GroupJunction.post_id) AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(GroupJunction.group_id == group_id) .filter(AlbumJunction.album_id == group_id)
.filter(GroupJunction.post_id < image_id) .filter(AlbumJunction.picture_id < image_id)
.order_by(GroupJunction.date_added.desc()) .order_by(AlbumJunction.date_added.desc())
.first() .first()
) )
@ -127,6 +202,14 @@ def group_post(group_id, image_id):
if prev_url: if prev_url:
prev_url = url_for("group.group_post", group_id=group_id, image_id=prev_url[0]) prev_url = url_for("group.group_post", group_id=group_id, image_id=prev_url[0])
close_tab = True
if request.cookies.get("image-info") == "0":
close_tab = False
return render_template( return render_template(
"image.html", image=image, next_url=next_url, prev_url=prev_url "image.html",
image=image,
next_url=next_url,
prev_url=prev_url,
close_tab=close_tab,
) )

View file

@ -1,27 +1,39 @@
""" """
Onlylegs - Image View Onlylegs - Image View
""" """
import os
import logging
import pathlib
from math import ceil from math import ceil
from flask import Blueprint, render_template, url_for, current_app from flask import (
from onlylegs.models import Post, GroupJunction, Group Blueprint,
render_template,
url_for,
current_app,
request,
flash,
jsonify,
)
from flask_login import current_user
from onlylegs.models import Pictures, AlbumJunction, Albums
from onlylegs.extensions import db from onlylegs.extensions import db
blueprint = Blueprint("image", __name__, url_prefix="/image") blueprint = Blueprint("image", __name__, url_prefix="/image")
@blueprint.route("/<int:image_id>") @blueprint.route("/<int:image_id>", methods=["GET"])
def image(image_id): def image(image_id):
""" """
Image view, shows the image and its metadata Image view, shows the image and its metadata
""" """
# Get the image, if it doesn't exist, 404 # Get the image, if it doesn't exist, 404
image = db.get_or_404(Post, image_id, description="Image not found :<") image = db.get_or_404(Pictures, image_id, description="Image not found :<")
# Get all groups the image is in # Get all groups the image is in
groups = ( groups = (
GroupJunction.query.with_entities(GroupJunction.group_id) AlbumJunction.query.with_entities(AlbumJunction.album_id)
.filter(GroupJunction.post_id == image_id) .filter(AlbumJunction.picture_id == image_id)
.all() .all()
) )
@ -29,34 +41,34 @@ def image(image_id):
image.groups = [] image.groups = []
for group in groups: for group in groups:
image.groups.append( image.groups.append(
Group.query.with_entities(Group.id, Group.name) Albums.query.with_entities(Albums.id, Albums.name)
.filter(Group.id == group[0]) .filter(Albums.id == group[0])
.first() .first()
) )
# Get the next and previous images # Get the next and previous images
# Check if there is a group ID set # Check if there is a group ID set
next_url = ( next_url = (
Post.query.with_entities(Post.id) Pictures.query.with_entities(Pictures.id)
.filter(Post.id > image_id) .filter(Pictures.id > image_id)
.order_by(Post.id.asc()) .order_by(Pictures.id.asc())
.first() .first()
) )
prev_url = ( prev_url = (
Post.query.with_entities(Post.id) Pictures.query.with_entities(Pictures.id)
.filter(Post.id < image_id) .filter(Pictures.id < image_id)
.order_by(Post.id.desc()) .order_by(Pictures.id.desc())
.first() .first()
) )
# If there is a next or previous image, get the url # If there is a next or previous image, get the url
if next_url: next_url = url_for("image.image", image_id=next_url[0]) if next_url else None
next_url = url_for("image.image", image_id=next_url[0]) prev_url = url_for("image.image", image_id=prev_url[0]) if prev_url else None
if prev_url:
prev_url = url_for("image.image", image_id=prev_url[0])
# Yoink all the images in the database # Yoink all the images in the database
total_images = Post.query.with_entities(Post.id).order_by(Post.id.desc()).all() total_images = (
Pictures.query.with_entities(Pictures.id).order_by(Pictures.id.desc()).all()
)
limit = current_app.config["UPLOAD_CONF"]["max-load"] limit = current_app.config["UPLOAD_CONF"]["max-load"]
# If the number of items is less than the limit, no point of calculating the page # If the number of items is less than the limit, no point of calculating the page
@ -72,10 +84,72 @@ def image(image_id):
return_page = i + 1 return_page = i + 1
break break
close_tab = True
if request.cookies.get("image-info") == "0":
close_tab = False
return render_template( return render_template(
"image.html", "image.html",
image=image, image=image,
next_url=next_url, next_url=next_url,
prev_url=prev_url, prev_url=prev_url,
return_page=return_page, return_page=return_page,
close_tab=close_tab,
) )
@blueprint.route("/<int:image_id>", methods=["PUT"])
def image_put(image_id):
"""
Update the image metadata
"""
image_record = db.get_or_404(Pictures, image_id, description="Image not found :<")
image_record.description = request.form.get("description", image_record.description)
image_record.alt = request.form.get("alt", image_record.alt)
print(request.form.get("description"))
db.session.commit()
flash(["Image updated!", "1"])
return "OK", 200
@blueprint.route("/<int:image_id>", methods=["DELETE"])
def image_delete(image_id):
image_record = db.get_or_404(Pictures, image_id)
# Check if image exists and if user is allowed to delete it (author)
if image_record.author_id != current_user.id:
logging.info("User %s tried to delete image %s", current_user.id, image_id)
return (
jsonify({"message": "You are not allowed to delete this image, heck off"}),
403,
)
# Delete file
try:
os.remove(
os.path.join(current_app.config["UPLOAD_FOLDER"], image_record.filename)
)
except FileNotFoundError:
logging.warning(
"File not found: %s, already deleted or never existed",
image_record.filename,
)
# Delete cached files
cache_name = image_record.filename.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
AlbumJunction.query.filter_by(picture_id=image_id).delete()
db.session.delete(image_record)
db.session.commit()
logging.info("Removed image (%s) %s", image_id, image_record.filename)
flash(["Image was all in Le Head!", "1"])
return jsonify({"message": "Image deleted"}), 200

View file

@ -2,11 +2,9 @@
Onlylegs Gallery - Index view Onlylegs Gallery - Index view
""" """
from math import ceil from math import ceil
from flask import Blueprint, render_template, request, current_app from flask import Blueprint, render_template, request, current_app
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from onlylegs.models import Pictures, Users
from onlylegs.models import Post
blueprint = Blueprint("gallery", __name__) blueprint = Blueprint("gallery", __name__)
@ -17,31 +15,32 @@ def index():
""" """
Home page of the website, shows the feed of the latest images Home page of the website, shows the feed of the latest images
""" """
# meme
if request.args.get("coffee") == "please":
abort(418)
# pagination, defaults to page 1 if no page is specified # pagination, defaults to page 1 if no page is specified
page = request.args.get("page", default=1, type=int) page = request.args.get("page", default=1, type=int)
limit = current_app.config["UPLOAD_CONF"]["max-load"] limit = current_app.config["UPLOAD_CONF"]["max-load"]
# get the total number of images in the database # get the total number of images in the database
# calculate the total number of pages, and make sure the page number is valid # calculate the total number of pages, and make sure the page number is valid
total_images = Post.query.with_entities(Post.id).count() total_images = Pictures.query.with_entities(Pictures.id).count()
pages = ceil(max(total_images, limit) / limit) pages = ceil(max(total_images, limit) / limit)
if page > pages: if page > pages:
abort( return abort(
404, 404,
"You have reached the far and beyond, " "You have reached the far and beyond, but you will not find your answers here.",
+ "but you will not find your answers here.",
) )
# get the images for the current page # get the images for the current page
images = ( images = (
Post.query.with_entities( Pictures.query.with_entities(
Post.filename, Post.alt, Post.colours, Post.created_at, Post.id Pictures.filename,
Pictures.alt,
Pictures.colours,
Pictures.created_at,
Pictures.id,
Users.username,
) )
.order_by(Post.id.desc()) .join(Users)
.order_by(Pictures.id.desc())
.offset((page - 1) * limit) .offset((page - 1) * limit)
.limit(limit) .limit(limit)
.all() .all()

View file

@ -5,7 +5,7 @@ from flask import Blueprint, render_template, request
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from flask_login import current_user from flask_login import current_user
from onlylegs.models import Post, User, Group from onlylegs.models import Pictures, Users, Albums
from onlylegs.extensions import db from onlylegs.extensions import db
@ -27,9 +27,9 @@ def profile():
abort(404, "You must be logged in to view your own profile!") abort(404, "You must be logged in to view your own profile!")
# Get the user's data # Get the user's data
user = db.get_or_404(User, user_id, description="User not found :<") user = db.get_or_404(Users, user_id, description="User not found :<")
images = Post.query.filter(Post.author_id == user_id).all() images = Pictures.query.filter(Pictures.author_id == user_id).all()
groups = Group.query.filter(Group.author_id == user_id).all() groups = Albums.query.filter(Albums.author_id == user_id).all()
return render_template("profile.html", user=user, images=images, groups=groups) return render_template("profile.html", user=user, images=images, groups=groups)

465
poetry.lock generated
View file

@ -2,14 +2,14 @@
[[package]] [[package]]
name = "alembic" name = "alembic"
version = "1.10.3" version = "1.11.2"
description = "A database migration tool for SQLAlchemy." description = "A database migration tool for SQLAlchemy."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"},
{file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"},
] ]
[package.dependencies] [package.dependencies]
@ -24,14 +24,14 @@ tz = ["python-dateutil"]
[[package]] [[package]]
name = "astroid" name = "astroid"
version = "2.15.3" version = "2.15.6"
description = "An abstract syntax tree for Python with inference support." description = "An abstract syntax tree for Python with inference support."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.2" python-versions = ">=3.7.2"
files = [ files = [
{file = "astroid-2.15.3-py3-none-any.whl", hash = "sha256:f11e74658da0f2a14a8d19776a8647900870a63de71db83713a8e77a6af52662"}, {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"},
{file = "astroid-2.15.3.tar.gz", hash = "sha256:44224ad27c54d770233751315fa7f74c46fa3ee0fab7beef1065f99f09897efe"}, {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"},
] ]
[package.dependencies] [package.dependencies]
@ -44,37 +44,34 @@ wrapt = [
[[package]] [[package]]
name = "black" name = "black"
version = "23.3.0" version = "23.7.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"},
{file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"},
{file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"},
{file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"},
{file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"},
{file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"},
{file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"},
{file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"},
{file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"},
{file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"},
{file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"},
{file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"},
{file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"},
{file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"},
{file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"},
{file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"},
{file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"},
{file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"},
{file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"},
{file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"},
{file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"},
{file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"},
{file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
{file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
{file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
] ]
[package.dependencies] [package.dependencies]
@ -197,27 +194,27 @@ files = [
] ]
[[package]] [[package]]
name = "cachelib" name = "cachetools"
version = "0.9.0" version = "5.3.1"
description = "A collection of cache libraries in the same API interface." description = "Extensible memoizing collections and decorators"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "cachelib-0.9.0-py3-none-any.whl", hash = "sha256:811ceeb1209d2fe51cd2b62810bd1eccf70feba5c52641532498be5c675493b3"}, {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"},
{file = "cachelib-0.9.0.tar.gz", hash = "sha256:38222cc7c1b79a23606de5c2607f4925779e37cdcea1c2ad21b8bae94b5425a5"}, {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"},
] ]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.3" version = "8.1.6"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
] ]
[package.dependencies] [package.dependencies]
@ -263,14 +260,14 @@ files = [
[[package]] [[package]]
name = "dill" name = "dill"
version = "0.3.6" version = "0.3.7"
description = "serialize all of python" description = "serialize all of Python"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"},
{file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"},
] ]
[package.extras] [package.extras]
@ -318,19 +315,18 @@ webassets = ">=2.0"
[[package]] [[package]]
name = "flask-caching" name = "flask-caching"
version = "2.0.2" version = "1.10.1"
description = "Adds caching support to Flask applications." description = "Adds caching support to your Flask application"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.5"
files = [ files = [
{file = "Flask-Caching-2.0.2.tar.gz", hash = "sha256:24b60c552d59a9605cc1b6a42c56cdb39a82a28dab4532bbedb9222ae54ecb4e"}, {file = "Flask-Caching-1.10.1.tar.gz", hash = "sha256:cf19b722fcebc2ba03e4ae7c55b532ed53f0cbf683ce36fafe5e881789a01c00"},
{file = "Flask_Caching-2.0.2-py3-none-any.whl", hash = "sha256:19571f2570e9b8dd9dd9d2f49d7cbee69c14ebe8cc001100b1eb98c379dd80ad"}, {file = "Flask_Caching-1.10.1-py3-none-any.whl", hash = "sha256:bcda8acbc7508e31e50f63e9b1ab83185b446f6b6318bd9dd1d45626fba2e903"},
] ]
[package.dependencies] [package.dependencies]
cachelib = ">=0.9.0,<0.10.0" Flask = "*"
Flask = "<3"
[[package]] [[package]]
name = "flask-compress" name = "flask-compress"
@ -383,19 +379,19 @@ Flask-SQLAlchemy = ">=1.0"
[[package]] [[package]]
name = "flask-sqlalchemy" name = "flask-sqlalchemy"
version = "3.0.3" version = "3.0.5"
description = "Add SQLAlchemy support to your Flask application." description = "Add SQLAlchemy support to your Flask application."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"}, {file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"},
{file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"}, {file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"},
] ]
[package.dependencies] [package.dependencies]
Flask = ">=2.2" flask = ">=2.2.5"
SQLAlchemy = ">=1.4.18" sqlalchemy = ">=1.4.18"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
@ -494,14 +490,14 @@ tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "6.5.0" version = "6.8.0"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "importlib_metadata-6.5.0-py3-none-any.whl", hash = "sha256:03ba783c3a2c69d751b109fc0c94a62c51f581b3d6acf8ed1331b6d5729321ff"}, {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
{file = "importlib_metadata-6.5.0.tar.gz", hash = "sha256:7a8bdf1bc3a726297f5cfbc999e6e7ff6b4fa41b26bba4afc580448624460045"}, {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
] ]
[package.dependencies] [package.dependencies]
@ -510,26 +506,26 @@ zipp = ">=0.5"
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"] perf = ["ipython"]
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
[[package]] [[package]]
name = "importlib-resources" name = "importlib-resources"
version = "5.12.0" version = "6.0.0"
description = "Read resources from Python packages" description = "Read resources from Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"},
{file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"},
] ]
[package.dependencies] [package.dependencies]
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[[package]] [[package]]
name = "isort" name = "isort"
@ -673,62 +669,62 @@ testing = ["pytest"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "2.1.2" version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
] ]
[[package]] [[package]]
@ -769,14 +765,14 @@ files = [
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.11.1" version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
] ]
[[package]] [[package]]
@ -861,34 +857,34 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "3.2.0" version = "3.10.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
{file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]] [[package]]
name = "pylint" name = "pylint"
version = "2.17.2" version = "2.17.5"
description = "python code static checker" description = "python code static checker"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.2" python-versions = ">=3.7.2"
files = [ files = [
{file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"}, {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"},
{file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"}, {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"},
] ]
[package.dependencies] [package.dependencies]
astroid = ">=2.15.2,<=2.17.0-dev0" astroid = ">=2.15.6,<=2.17.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [ dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.2", markers = "python_version < \"3.11\""},
@ -922,120 +918,120 @@ cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0" version = "6.0.1"
description = "YAML parser and emitter for Python" description = "YAML parser and emitter for Python"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
] ]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "67.7.0" version = "68.0.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "setuptools-67.7.0-py3-none-any.whl", hash = "sha256:888be97fde8cc3afd60f7784e678fa29ee13c4e5362daa7104a93bba33646c50"}, {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
{file = "setuptools-67.7.0.tar.gz", hash = "sha256:b7e53a01c6c654d26d2999ee033d8c6125e5fa55f03b7b193f937ae7ac999f22"}, {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
] ]
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.9" version = "2.0.19"
description = "Database Abstraction Library" description = "Database Abstraction Library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f005245e1cb9b8ca53df73ee85e029ac43155e062405015e49ec6187a2e3fb44"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:34eb96c1de91d8f31e988302243357bef3f7785e1b728c7d4b98bd0c117dafeb"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e472e9627882f2d75b87ff91c5a2bc45b31a226efc7cc0a054a94fffef85862"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-win32.whl", hash = "sha256:0a865b5ec4ba24f57c33b633b728e43fde77b968911a6046443f581b25d29dd9"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-win32.whl", hash = "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:6e84ab63d25d8564d7a8c05dc080659931a459ee27f6ed1cf4c91f292d184038"}, {file = "SQLAlchemy-2.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db4bd1c4792da753f914ff0b688086b9a8fd78bb9bc5ae8b6d2e65f176b81eb9"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5363a1c65fde7b7466769d4261126d07d872fc2e816487ae6cec93da604b6b"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc4eeb1737a5a9bdb0c24f4c982319fa6edd23cdee27180978c29cbb026f2bd"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbda1da8d541904ba262825a833c9f619e93cb3fd1156be0a5e43cd54d588dcd"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d5327f54a9c39e7871fc532639616f3777304364a0bb9b89d6033ad34ef6c5f8"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac6a0311fb21a99855953f84c43fcff4bdca27a2ffcc4f4d806b26b54b5cddc9"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-win32.whl", hash = "sha256:d209594e68bec103ad5243ecac1b40bf5770c9ebf482df7abf175748a34f4853"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-win32.whl", hash = "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:865392a50a721445156809c1a6d6ab6437be70c1c2599f591a8849ed95d3c693"}, {file = "SQLAlchemy-2.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b49f1f71d7a44329a43d3edd38cc5ee4c058dfef4487498393d16172007954b"}, {file = "SQLAlchemy-2.0.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a019f723b6c1e6b3781be00fb9e0844bc6156f9951c836ff60787cc3938d76"}, {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9838bd247ee42eb74193d865e48dd62eb50e45e3fdceb0fdef3351133ee53dcf"}, {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:78612edf4ba50d407d0eb3a64e9ec76e6efc2b5d9a5c63415d53e540266a230a"}, {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f61ab84956dc628c8dfe9d105b6aec38afb96adae3e5e7da6085b583ff6ea789"}, {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:07950fc82f844a2de67ddb4e535f29b65652b4d95e8b847823ce66a6d540a41d"}, {file = "SQLAlchemy-2.0.19-cp37-cp37m-win32.whl", hash = "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e62c4e762d6fd2901692a093f208a6a6575b930e9458ad58c2a7f080dd6132da"}, {file = "SQLAlchemy-2.0.19-cp37-cp37m-win_amd64.whl", hash = "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3e5864eba71a3718236a120547e52c8da2ccb57cc96cecd0480106a0c799c92"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d06e119cf79a3d80ab069f064a07152eb9ba541d084bdaee728d8a6f03fd03d"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee2946042cc7851842d7a086a92b9b7b494cbe8c3e7e4627e27bc912d3a7655e"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f984a190d249769a050634b248aef8991acc035e849d02b634ea006c028fa8"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4780be0f19e5894c17f75fc8de2fe1ae233ab37827125239ceb593c6f6bd1e2"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:68ed381bc340b4a3d373dbfec1a8b971f6350139590c4ca3cb722fdb50035777"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-win32.whl", hash = "sha256:aa5c270ece17c0c0e0a38f2530c16b20ea05d8b794e46c79171a86b93b758891"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-win32.whl", hash = "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:1b69666e25cc03c602d9d3d460e1281810109e6546739187044fc256c67941ef"}, {file = "SQLAlchemy-2.0.19-cp38-cp38-win_amd64.whl", hash = "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6e27189ff9aebfb2c02fd252c629ea58657e7a5ff1a321b7fc9c2bf6dc0b5f3"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8239ce63a90007bce479adf5460d48c1adae4b933d8e39a4eafecfc084e503c"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f759eccb66e6d495fb622eb7f4ac146ae674d829942ec18b7f5a35ddf029597"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246712af9fc761d6c13f4f065470982e175d902e77aa4218c9cb9fc9ff565a0c"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b72dccc5864ea95c93e0a9c4e397708917fb450f96737b4a8395d009f90b868"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:93c78d42c14aa9a9e0866eacd5b48df40a50d0e2790ee377af7910d224afddcf"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-win32.whl", hash = "sha256:f49c5d3c070a72ecb96df703966c9678dda0d4cb2e2736f88d15f5e1203b4159"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-win32.whl", hash = "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:4c3020afb144572c7bfcba9d7cce57ad42bff6e6115dffcfe2d4ae6d444a214f"}, {file = "SQLAlchemy-2.0.19-cp39-cp39-win_amd64.whl", hash = "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0"},
{file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"}, {file = "SQLAlchemy-2.0.19-py3-none-any.whl", hash = "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972"},
{file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"}, {file = "SQLAlchemy-2.0.19.tar.gz", hash = "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f"},
] ]
[package.dependencies] [package.dependencies]
@ -1062,6 +1058,7 @@ postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopg2cffi = ["psycopg2cffi"]
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"] pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3-binary"] sqlcipher = ["sqlcipher3-binary"]
@ -1079,26 +1076,26 @@ files = [
[[package]] [[package]]
name = "tomlkit" name = "tomlkit"
version = "0.11.7" version = "0.12.1"
description = "Style preserving TOML library" description = "Style preserving TOML library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"},
{file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"},
] ]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.5.0" version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+" description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
] ]
[[package]] [[package]]
@ -1115,14 +1112,14 @@ files = [
[[package]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "2.3.3" version = "2.3.6"
description = "The comprehensive WSGI web application library." description = "The comprehensive WSGI web application library."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "Werkzeug-2.3.3-py3-none-any.whl", hash = "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a"}, {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"},
{file = "Werkzeug-2.3.3.tar.gz", hash = "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"}, {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"},
] ]
[package.dependencies] [package.dependencies]
@ -1218,21 +1215,21 @@ files = [
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.15.0" version = "3.16.2"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"},
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"},
] ]
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "15ba5eebb3e5385a9e2ab48b5d6156bd59df6b7a1d431c54d95bba5a8ec53004" content-hash = "96ec0d1f7b512afb05455262fa2de8c4f862bf68fdae513f8552dc30c6e5ab49"

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "OnlyLegs" name = "OnlyLegs"
version = "0.1.2" version = "0.1.5"
repository = "https://github.com/Fluffy-Bean/onlylegs" repository = "https://github.com/Fluffy-Bean/onlylegs"
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
@ -13,7 +13,7 @@ Flask = "^2.3.2"
Flask-Sqlalchemy = "^3.0.3" Flask-Sqlalchemy = "^3.0.3"
Flask-Migrate = "^4.0.4" Flask-Migrate = "^4.0.4"
Flask-Compress = "^1.13" Flask-Compress = "^1.13"
Flask-Caching = "^2.0.2" Flask-Caching = "1.10.1"
Flask-Assets = "^2.0" Flask-Assets = "^2.0"
Flask-Login = "^0.6.2" Flask-Login = "^0.6.2"
python-dotenv = "^0.21.0" python-dotenv = "^0.21.0"
@ -27,6 +27,7 @@ jsmin = "^3.0.1"
cssmin = "^0.2.0" cssmin = "^0.2.0"
pylint = "^2.16.3" pylint = "^2.16.3"
black = "^23.3.0" black = "^23.3.0"
cachetools = "^5.3.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

4
run.py
View file

@ -25,9 +25,9 @@ Configuration()
if DEBUG: if DEBUG:
from onlylegs import create_app from onlylegs.app import app
create_app().run(host=ADDRESS, port=PORT, debug=True, threaded=True) app.run(host=ADDRESS, port=PORT, debug=True, threaded=True)
else: else:
from setup.runner import OnlyLegs # pylint: disable=C0412 from setup.runner import OnlyLegs # pylint: disable=C0412
import sys import sys

View file

@ -32,4 +32,4 @@ class OnlyLegs(Application):
return "OnlyLegs" return "OnlyLegs"
def load(self): def load(self):
return util.import_app("onlylegs:create_app()") return util.import_app("onlylegs.app:app")