diff --git a/onlylegs/__init__.py b/onlylegs/__init__.py deleted file mode 100644 index e15e3e1..0000000 --- a/onlylegs/__init__.py +++ /dev/null @@ -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 diff --git a/onlylegs/api.py b/onlylegs/api.py new file mode 100644 index 0000000..fffeea5 --- /dev/null +++ b/onlylegs/api.py @@ -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 diff --git a/onlylegs/api/__init__.py b/onlylegs/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/onlylegs/api/account.py b/onlylegs/api/account.py deleted file mode 100644 index fb54c25..0000000 --- a/onlylegs/api/account.py +++ /dev/null @@ -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 diff --git a/onlylegs/api/group.py b/onlylegs/api/group.py deleted file mode 100644 index 9be39a5..0000000 --- a/onlylegs/api/group.py +++ /dev/null @@ -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"}) diff --git a/onlylegs/api/media.py b/onlylegs/api/media.py deleted file mode 100644 index 2ce774b..0000000 --- a/onlylegs/api/media.py +++ /dev/null @@ -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 diff --git a/onlylegs/app.py b/onlylegs/app.py new file mode 100644 index 0000000..0dd4349 --- /dev/null +++ b/onlylegs/app.py @@ -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() diff --git a/onlylegs/auth.py b/onlylegs/auth.py index 6b5884f..c3d0698 100644 --- a/onlylegs/auth.py +++ b/onlylegs/auth.py @@ -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 onlylegs.extensions import db -from onlylegs.models import User +from onlylegs.models import Users blueprint = Blueprint("auth", __name__, url_prefix="/auth") @@ -28,7 +28,7 @@ def login(): password = request.form["password"].strip() 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): logging.error("Login attempt from %s", request.remote_addr) @@ -77,7 +77,7 @@ def register(): elif password_repeat != password: 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: error.append("User already exists!") @@ -86,7 +86,7 @@ def register(): print(error) return jsonify(error), 400 - register_user = User( + register_user = Users( username=username, email=email, password=generate_password_hash(password, method="sha256"), diff --git a/onlylegs/config.py b/onlylegs/config.py index 58e7a80..06cd271 100644 --- a/onlylegs/config.py +++ b/onlylegs/config.py @@ -3,6 +3,7 @@ Gallery configuration file """ import os import platformdirs +import importlib.metadata from dotenv import load_dotenv from yaml import safe_load @@ -41,3 +42,6 @@ MEDIA_FOLDER = os.path.join(user_dir, "media") # Database INSTANCE_DIR = instance_dir MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations") + +# App +APP_VERSION = importlib.metadata.version("OnlyLegs") diff --git a/onlylegs/extensions.py b/onlylegs/extensions.py index 0b27ca4..9e3eb84 100644 --- a/onlylegs/extensions.py +++ b/onlylegs/extensions.py @@ -13,4 +13,4 @@ migrate = Migrate() login_manager = LoginManager() assets = Environment() compress = Compress() -cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300}) +cache = Cache(config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 300}) diff --git a/onlylegs/filters.py b/onlylegs/filters.py new file mode 100644 index 0000000..765003b --- /dev/null +++ b/onlylegs/filters.py @@ -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));" diff --git a/onlylegs/gwagwa.py b/onlylegs/gwagwa.py deleted file mode 100644 index 6fb58f6..0000000 --- a/onlylegs/gwagwa.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Gwa Gwa! -""" -print("Gwa Gwa!") diff --git a/onlylegs/langs/gb.json b/onlylegs/langs/gb.json deleted file mode 100644 index d3f0895..0000000 --- a/onlylegs/langs/gb.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/onlylegs/models.py b/onlylegs/models.py index af3eb6f..6970a9b 100644 --- a/onlylegs/models.py +++ b/onlylegs/models.py @@ -6,18 +6,18 @@ from flask_login import UserMixin 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 - Joins with posts and groups + Junction table for picturess and albums + Joins with picturess and albums """ - __tablename__ = "group_junction" + __tablename__ = "album_junction" id = db.Column(db.Integer, primary_key=True) - group_id = db.Column(db.Integer, db.ForeignKey("group.id")) - post_id = db.Column(db.Integer, db.ForeignKey("post.id")) + album_id = db.Column(db.Integer, db.ForeignKey("albums.id")) + picture_id = db.Column(db.Integer, db.ForeignKey("pictures.id")) date_added = db.Column( 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) - - author_id = db.Column(db.Integer, db.ForeignKey("user.id")) + author_id = db.Column(db.Integer, db.ForeignKey("users.id")) filename = db.Column(db.String, unique=True, 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 ) - 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) name = 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( db.DateTime, nullable=False, 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 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 ) - posts = db.relationship("Post", backref="author") - groups = db.relationship("Group", backref="author") + pictures_fk = db.relationship("Pictures", backref="author") + albums_fk = db.relationship("Albums", backref="author") def get_id(self): return str(self.alt_id) diff --git a/onlylegs/static/banner.png b/onlylegs/static/banner.png index 179ed20..67aacfc 100644 Binary files a/onlylegs/static/banner.png and b/onlylegs/static/banner.png differ diff --git a/onlylegs/static/fonts/Rubik.ttf b/onlylegs/static/fonts/Rubik.ttf deleted file mode 100644 index 547f069..0000000 Binary files a/onlylegs/static/fonts/Rubik.ttf and /dev/null differ diff --git a/onlylegs/static/fonts/font.css b/onlylegs/static/fonts/font.css deleted file mode 100644 index bb8f61b..0000000 --- a/onlylegs/static/fonts/font.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/onlylegs/static/js/clipboard.js b/onlylegs/static/js/clipboard.js new file mode 100644 index 0000000..0882f56 --- /dev/null +++ b/onlylegs/static/js/clipboard.js @@ -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); + } +} \ No newline at end of file diff --git a/onlylegs/static/js/fade.js b/onlylegs/static/js/fade.js new file mode 100644 index 0000000..56749ba --- /dev/null +++ b/onlylegs/static/js/fade.js @@ -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); +} diff --git a/onlylegs/static/js/grid.js b/onlylegs/static/js/grid.js new file mode 100644 index 0000000..54aae09 --- /dev/null +++ b/onlylegs/static/js/grid.js @@ -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'; +} \ No newline at end of file diff --git a/onlylegs/static/js/groupPage.js b/onlylegs/static/js/groupPage.js new file mode 100644 index 0000000..193dccc --- /dev/null +++ b/onlylegs/static/js/groupPage.js @@ -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); + }); +} \ No newline at end of file diff --git a/onlylegs/static/js/imagePage.js b/onlylegs/static/js/imagePage.js new file mode 100644 index 0000000..b98d46d --- /dev/null +++ b/onlylegs/static/js/imagePage.js @@ -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); + } + }); +} diff --git a/onlylegs/static/js/index.js b/onlylegs/static/js/index.js deleted file mode 100644 index d9d0aa5..0000000 --- a/onlylegs/static/js/index.js +++ /dev/null @@ -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(); -}; diff --git a/onlylegs/static/js/login.js b/onlylegs/static/js/login.js index 1e2287a..ed95512 100644 --- a/onlylegs/static/js/login.js +++ b/onlylegs/static/js/login.js @@ -5,7 +5,7 @@ function showLogin() { cancelBtn.classList.add('btn-block'); cancelBtn.classList.add('transparent'); cancelBtn.innerHTML = 'nuuuuuuuu'; - cancelBtn.onclick = popupDissmiss; + cancelBtn.onclick = popupDismiss; loginBtn = document.createElement('button'); loginBtn.classList.add('btn-block'); @@ -50,7 +50,7 @@ function showLogin() { loginForm.appendChild(passwordInput); loginForm.appendChild(rememberMeSpan); - popUpShow( + popupShow( 'Login!', 'Need an account? <span class="link" onclick="showRegister()">Register!</span>', loginForm, @@ -103,7 +103,7 @@ function showRegister() { cancelBtn.classList.add('btn-block'); cancelBtn.classList.add('transparent'); cancelBtn.innerHTML = 'nuuuuuuuu'; - cancelBtn.onclick = popupDissmiss; + cancelBtn.onclick = popupDismiss; registerBtn = document.createElement('button'); registerBtn.classList.add('btn-block'); @@ -146,7 +146,7 @@ function showRegister() { registerForm.appendChild(passwordInput); registerForm.appendChild(passwordInputRepeat); - popUpShow( + popupShow( 'Who are you?', 'Already have an account? <span class="link" onclick="showLogin()">Login!</span>', registerForm, diff --git a/onlylegs/static/js/popup.js b/onlylegs/static/js/popup.js index b0b19ac..0ef8cc4 100644 --- a/onlylegs/static/js/popup.js +++ b/onlylegs/static/js/popup.js @@ -1,4 +1,4 @@ -function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) { +function popupShow(titleText, subtitleText, bodyContent=null, userActions=null) { // Get popup elements const popupSelector = document.querySelector('.pop-up'); const headerSelector = document.querySelector('.pop-up-header'); @@ -9,38 +9,47 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) actionsSelector.innerHTML = ''; // Set popup header and subtitle - const titleElement = document.createElement('h2'); - titleElement.innerHTML = titleText; - headerSelector.appendChild(titleElement); + let titleElement = document.createElement('h2'); + titleElement.innerHTML = titleText; + headerSelector.appendChild(titleElement); - const subtitleElement = document.createElement('p'); - subtitleElement.innerHTML = subtitleText; - headerSelector.appendChild(subtitleElement); + let subtitleElement = document.createElement('p'); + subtitleElement.innerHTML = subtitleText; + headerSelector.appendChild(subtitleElement); - if (bodyContent) { - headerSelector.appendChild(bodyContent); - } + if (bodyContent) { headerSelector.appendChild(bodyContent) } // Set buttons that will be displayed if (userActions) { - // for each user action, add the element - for (let i = 0; i < userActions.length; i++) { - actionsSelector.appendChild(userActions[i]); - } + userActions.forEach((action) => { + actionsSelector.appendChild(action); + }); } 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 document.querySelector("html").style.overflow = "hidden"; 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'); - document.querySelector("html").style.overflow = "auto"; popupSelector.classList.remove('active'); 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; diff --git a/onlylegs/static/js/square.js b/onlylegs/static/js/square.js new file mode 100644 index 0000000..ff9bfb4 --- /dev/null +++ b/onlylegs/static/js/square.js @@ -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'; + } +} \ No newline at end of file diff --git a/onlylegs/static/js/webp.js b/onlylegs/static/js/webp.js deleted file mode 100644 index 93a4ade..0000000 --- a/onlylegs/static/js/webp.js +++ /dev/null @@ -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; -} diff --git a/onlylegs/static/logo-black.svg b/onlylegs/static/logo-black.svg deleted file mode 100644 index 559ad4d..0000000 --- a/onlylegs/static/logo-black.svg +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - version="1.0" - width="621.000000pt" - height="621.000000pt" - viewBox="0 0 621.000000 621.000000" - preserveAspectRatio="xMidYMid meet" - id="svg12" - sodipodi:docname="OnlyLegs.svg" - inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"> - <defs - id="defs16" /> - <sodipodi:namedview - id="namedview14" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - inkscape:document-units="pt" - showgrid="false" - inkscape:zoom="0.75493043" - inkscape:cx="207.30387" - inkscape:cy="449.048" - inkscape:window-width="1856" - inkscape:window-height="1064" - inkscape:window-x="56" - inkscape:window-y="8" - inkscape:window-maximized="1" - inkscape:current-layer="g10" /> - <g - transform="translate(0.000000,621.000000) scale(0.100000,-0.100000)" - fill="#000000" - stroke="none" - id="g10"> - <path - d="m 1650,6136 c -28,-16 -61,-77 -113,-206 -107,-264 -114,-362 -48,-634 23,-93 46,-204 52,-245 6,-42 17,-104 25,-137 7,-34 19,-97 24,-140 6,-44 20,-124 30,-179 11,-55 27,-138 35,-185 8,-47 24,-125 35,-175 12,-49 29,-132 39,-183 18,-91 25.8809,-120.7949 21.7873,-134.9298 -10.1829,-12.5128 -43.6012,-16.8559 -57.7873,-18.0702 -299,-22 -419,-42 -480,-80 -57,-36 -69,-86 -32,-133 24,-30 137,-88 215,-111 27,-7 74,-22 104,-33 63,-23 67,-21 -90,-47 -107,-18 -264,-59 -327,-86 -71,-30 -97,-116 -49,-164 36,-36 84,-48 364,-91 29,-4 32,-8 32,-39 0,-55 20,-85 74,-112 141.9762,-39.3457 220.3199,-36.183 366,-42 -52.2181,-55.8508 -428.3582,-414.6946 -490.4542,-467.477 -65.1534,47.6205 -260.6727,12.2815 -311.2588,-90.0814 -50.3326,-175.9923 22.0801,-322.4949 253.1708,-312.892 82.63,4.7723 122.7958,22.9511 163.5422,72.4504 110.0316,28.4289 1032.7396,235.9943 1114,257 14,0 79,-71 100,-109 148.2757,-235.4969 289.4861,-410.3884 407,-631 31,-68 62,-134 70,-145 7,-11 32,-74 54,-140 45,-128 68,-283 69,-451 1,-104 16,-126 136,-211 144,-101 193,-121 222,-92 15,15 17,0 55,-493 L 3724,0 h 79 80 l -7,123 c -4,67 -11,151 -16,187 -5,36 -14,137 -20,225 -6,88 -15,176 -21,196 -5,20 -20,112 -33,203 -26,188 -35,206 -97,206 -29,0 -42,-6 -58,-26 -25,-32 -26,-50 -6,-180 23,-156 25,-152 -44,-102 -33,24 -73,51 -90,60 -30,16 -31,20 -32,85 -1,166 -25,313 -73,458 -27,77 -54,150 -62,162 -8,13 -98.9126,185.5819 -130.9126,255.5819 C 3078.1155,2027.0841 3018.0028,2099.9913 2958,2188 c -60.9556,77.2514 -67.3555,103.6675 -109.5679,174.1092 L 2791,2467 l 87,26 c 92,28 219,53 342,69 82,10 115,26 132,64 14,30 0,66 -37,90 -23,15 -37,16 -108,7 -112,-14 -286,-50 -382,-78 -44,-13 -96,-26 -115,-30 -19,-4 -114,-28 -210,-54 -66.7812,-17.1766 -734.4826,-161.2693 -877.9113,-194.6921 -61.4817,-10.1244 -101.9911,25.0397 -69.4824,79.572 30.1454,40.2606 436.3495,409.3445 464.3937,438.1201 18,31 16,60 -8,90 -17.2726,26.1357 -4.8315,31.9291 30,50 53.754,58.4845 37.1768,154.6092 -116,123 -43,-9 -112,-16 -153,-16 -64,1 -69,2 -36,9 77,15 100,87 47,141 -44.2079,53.7356 -129.2499,29.5932 -198,30 -221,1 -246,9 -107,33 231,40 352,84 379,136 45,87 -15,141 -205,185 -41,10 -93,25 -115,34 l -40,16 50,8 c 28,4 122,12 210,18 187,12 208,21 213,86 4,54 -30,83 -97,83 -43,0 -45,1 -21,13 13,7 31,24 40,39 13,24 13,37 -2,120 -9,51 -26,131 -38,178 -21,84 -53,254 -85,445 -9,55 -27,156 -40,225 -13,69 -26,148 -30,175 -7,49 -42,209 -70,315 -19,74 -26,158 -14,165 5,4 22,-7 38,-24 37,-39 81,-42 116,-6 14,13 25,32 25,41 0,12 5,14 18,9 47,-21 71,-17 102,13 21,21 30,40 30,62 0,30 6,24 88,-79 89,-114 175,-258 286,-486 71,-143 131,-332 170,-531 27,-141 35,-158 76,-173 33,-11 55,-7 81,16 15,12 20,10 47,-17 36,-35 82,-39 172,-15 60,16 283,20 405,6 129,-14 389,-90 542,-157 36,-16 74,-10 97,14 8,10 41,69 74,132 33,63 76,141 95,174 20,32 42,74 48,93 11,33 103,189 175,298 95,143 226,309 330,417 l 83,88 26,-26 c 20,-20 34,-25 60,-22 29,3 35,0 40,-19 8,-32 49,-56 86,-51 27,4 34,0 51,-29 22,-37 46,-53 77,-53 12,0 21,-3 21,-7 0,-5 -18,-69 -40,-143 -29,-95 -71,-197 -142,-345 -55,-115 -115,-246 -133,-290 -40,-101 -44,-108 -125,-235 -57.6611,-76.9253 -108.8483,-155.9466 -158,-245 -41.8198,-72.4341 -151.3026,-225.4907 -108,-300 65.0126,-70.4405 142.2339,-55.61 217,-79 24,-8 70,-20 103,-26 32,-7 54,-16 50,-20 -5,-5 -54.4462,-13.6036 -109,-25 -55.5759,-11.6099 -107.6919,-23.3581 -190,-41 -86.0096,-13.9667 -223.7414,-29.6671 -275,-106 -59,-97 -1,-143 260,-211 l 55,-14 -55,-13 c -165,-38 -232,-110 -185,-199 14,-28 13,-29 -13,-39 -15,-6 -49,-15 -77,-22 -65,-14 -153,-72 -161,-105 -13,-51 31,-105 84,-105 10,0 34,11 54,25 20,14 64,31 96,39 l 59,14 17,-83 c 9,-46 16,-107 16,-135 0,-44 4,-56 29,-81 37,-37 65,-37 102,0 27,27 29,35 29,105 0,41 -4,85 -9,98 -4,13 -11,47 -15,76 l -7,52 c 180.2886,6.4837 207.2871,-4.1989 355.8174,23.0309 66,12 305.1826,43.9691 335.1826,52.9691 85,26 170,34 170,16 0,-8 15,-29 34,-48 59,-59 104,-45 271,80 117,89 129,91 178,35 21,-24 59,-74 83,-111 30,-44 62,-77 97,-100 48,-31 58,-33 149,-36 l 98,-4 v 85 85 l -66,-6 c -82,-6 -102,4 -150,77 -160,243 -270,269 -476,113 l -66,-50 -13,29 c -12,31 -54,59 -86,59 -10,0 -34,-8 -53,-18 -19,-11 -62,-22 -95,-25 -33,-3 -80,-13 -104,-22 -25,-8 -97,-24 -160,-36 -150.6695,-31.5585 -331.7946,-51.6255 -476.3702,-37.9738 -66,9 53.3702,64.9738 155.3702,82.9738 221,38 324,64 342,86 31,38 17,107 -26,129 -33,17 -144,40 -226,46 -41,4 -95,10 -120,15 -25,5 -65,12 -90,16 -39,6 -42,8 -22,15 12,5 53,13 90,19 138,23 319,62 386,83 77,25 136,78 136,123 0,35 -47,80 -101,94 -24,7 -80,23 -124,36 -44,13 -136,38 -205,55 -69,17 -128,33 -133,36 -4,2 18,32 49,66 30,34 78,103 106,152 28,50 67,115 88,145 45,68 103,173 120,220 7,19 66,148 131,285 216,458 279,758 208,1000 -12,39 -27,81 -34,95 -7,14 -21,48 -30,75 -26,78 -35,93 -68,106 -58,24 -139,-13 -239,-108 -311,-298 -478,-484 -659,-735 -65,-90 -214,-337 -228,-377 -6,-19 -35,-75 -64,-125 -30,-50 -62,-110 -72,-134 -10,-24 -23,-42 -29,-40 -6,2 -69,22 -141,45 -163,52 -290,84 -380,94 -124,14 -394,10 -455,-6 -30,-8 -67,-15 -81,-15 -15,0 -38,-9 -52,-20 -14,-11 -28,-17 -31,-12 -2,4 -14,55 -25,112 -54,270 -139,504 -262,723 -28,51 -71,127 -95,169 -54,95 -275,380 -364,468 -36,36 -93,89 -128,118 -67,55 -96,63 -142,38 z" - id="path2" - sodipodi:nodetypes="cccccccsccccccccccscscccccccccccccscccccssccscccccccccccccccccscccccccccccccccccccscccccscccccsccsccccccscccscccccsccccssccccccccsscccccccccsscccsssscccccsscscccccccccccscscccccccsccccscsscccccccsscccccsscccssccccccc" /> - <path - d="m 2500.8313,4134.78 c -87.3956,-37.9843 -97.6852,-154.4873 -98,-194.7871 0,-44.3118 -4,-131.089 -9,-192.0177 -5,-60.9286 -5,-159.707 -2,-219.7125 6,-105.2404 7,-110.7794 34,-132.0121 79,-62.775 164,3.6926 217,168.9386 55,175.4007 49,403.4217 -15,519.7401 -35,64.6213 -71,78.4687 -127,49.8507 z" - id="path4" - sodipodi:nodetypes="ccsccccc" - style="stroke-width:1" /> - <path - d="m 3344.4273,3996.6247 c -48.5583,-50.0953 -37.4868,-100.9153 -40.8178,-215.7915 -2.7697,-95.5212 29.3536,-310.9299 34.084,-408.9796 38.3413,-101.4275 148.4445,-115.5653 198.0212,-37.0586 31.0725,92.512 58.8307,202.0641 62.5807,328.6811 2.6164,88.3394 -12.9222,191.6407 -19.8758,275.1699 -22.7001,117.379 -144.8343,172.9775 -233.9923,57.9787 z" - id="path6" - sodipodi:nodetypes="csccscc" - style="stroke-width:1" /> - <path - d="M4633 1040 c-60 -25 -78 -94 -158 -605 -20 -126 -45 -273 -56 -327 -10 -53 -19 -99 -19 -102 0 -3 41 -6 91 -6 85 0 90 1 84 19 -3 11 3 66 14 123 10 57 24 142 31 190 l12 87 46 -5 c111 -12 107 -10 141 -74 37 -68 81 -231 81 -296 l0 -44 85 0 85 0 -6 42 c-10 75 -44 237 -60 280 -8 22 -12 43 -9 46 2 2 46 -1 97 -7 181 -22 397 -31 756 -31 l362 0 0 80 0 80 -362 0 c-388 0 -565 9 -828 41 -183 21 -351 46 -357 52 -10 11 46 278 73 342 16 39 11 76 -14 98 -23 21 -62 28 -89 17z" - id="path8" /> - </g> -</svg> diff --git a/onlylegs/static/logo-white.svg b/onlylegs/static/logo-white.svg deleted file mode 100644 index a50b3f3..0000000 --- a/onlylegs/static/logo-white.svg +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - version="1.0" - width="621.000000pt" - height="621.000000pt" - viewBox="0 0 621.000000 621.000000" - preserveAspectRatio="xMidYMid meet" - id="svg12" - sodipodi:docname="OnlyLegs-white.svg" - inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"> - <defs - id="defs16" /> - <sodipodi:namedview - id="namedview14" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - inkscape:document-units="pt" - showgrid="false" - inkscape:zoom="0.75493043" - inkscape:cx="207.30387" - inkscape:cy="449.048" - inkscape:window-width="1856" - inkscape:window-height="1064" - inkscape:window-x="56" - inkscape:window-y="8" - inkscape:window-maximized="1" - inkscape:current-layer="g10" /> - <g - transform="translate(0.000000,621.000000) scale(0.100000,-0.100000)" - fill="#000000" - stroke="none" - id="g10"> - <path - d="m 1650,6136 c -28,-16 -61,-77 -113,-206 -107,-264 -114,-362 -48,-634 23,-93 46,-204 52,-245 6,-42 17,-104 25,-137 7,-34 19,-97 24,-140 6,-44 20,-124 30,-179 11,-55 27,-138 35,-185 8,-47 24,-125 35,-175 12,-49 29,-132 39,-183 18,-91 25.8809,-120.7949 21.7873,-134.9298 -10.1829,-12.5128 -43.6012,-16.8559 -57.7873,-18.0702 -299,-22 -419,-42 -480,-80 -57,-36 -69,-86 -32,-133 24,-30 137,-88 215,-111 27,-7 74,-22 104,-33 63,-23 67,-21 -90,-47 -107,-18 -264,-59 -327,-86 -71,-30 -97,-116 -49,-164 36,-36 84,-48 364,-91 29,-4 32,-8 32,-39 0,-55 20,-85 74,-112 141.9762,-39.3457 220.3199,-36.183 366,-42 -52.2181,-55.8508 -428.3582,-414.6946 -490.4542,-467.477 -65.1534,47.6205 -260.6727,12.2815 -311.2588,-90.0814 -50.3326,-175.9923 22.0801,-322.4949 253.1708,-312.892 82.63,4.7723 122.7958,22.9511 163.5422,72.4504 110.0316,28.4289 1032.7396,235.9943 1114,257 14,0 79,-71 100,-109 148.2757,-235.4969 289.4861,-410.3884 407,-631 31,-68 62,-134 70,-145 7,-11 32,-74 54,-140 45,-128 68,-283 69,-451 1,-104 16,-126 136,-211 144,-101 193,-121 222,-92 15,15 17,0 55,-493 L 3724,0 h 79 80 l -7,123 c -4,67 -11,151 -16,187 -5,36 -14,137 -20,225 -6,88 -15,176 -21,196 -5,20 -20,112 -33,203 -26,188 -35,206 -97,206 -29,0 -42,-6 -58,-26 -25,-32 -26,-50 -6,-180 23,-156 25,-152 -44,-102 -33,24 -73,51 -90,60 -30,16 -31,20 -32,85 -1,166 -25,313 -73,458 -27,77 -54,150 -62,162 -8,13 -98.9126,185.5819 -130.9126,255.5819 C 3078.1155,2027.0841 3018.0028,2099.9913 2958,2188 c -60.9556,77.2514 -67.3555,103.6675 -109.5679,174.1092 L 2791,2467 l 87,26 c 92,28 219,53 342,69 82,10 115,26 132,64 14,30 0,66 -37,90 -23,15 -37,16 -108,7 -112,-14 -286,-50 -382,-78 -44,-13 -96,-26 -115,-30 -19,-4 -114,-28 -210,-54 -66.7812,-17.1766 -734.4826,-161.2693 -877.9113,-194.6921 -61.4817,-10.1244 -101.9911,25.0397 -69.4824,79.572 30.1454,40.2606 436.3495,409.3445 464.3937,438.1201 18,31 16,60 -8,90 -17.2726,26.1357 -4.8315,31.9291 30,50 53.754,58.4845 37.1768,154.6092 -116,123 -43,-9 -112,-16 -153,-16 -64,1 -69,2 -36,9 77,15 100,87 47,141 -44.2079,53.7356 -129.2499,29.5932 -198,30 -221,1 -246,9 -107,33 231,40 352,84 379,136 45,87 -15,141 -205,185 -41,10 -93,25 -115,34 l -40,16 50,8 c 28,4 122,12 210,18 187,12 208,21 213,86 4,54 -30,83 -97,83 -43,0 -45,1 -21,13 13,7 31,24 40,39 13,24 13,37 -2,120 -9,51 -26,131 -38,178 -21,84 -53,254 -85,445 -9,55 -27,156 -40,225 -13,69 -26,148 -30,175 -7,49 -42,209 -70,315 -19,74 -26,158 -14,165 5,4 22,-7 38,-24 37,-39 81,-42 116,-6 14,13 25,32 25,41 0,12 5,14 18,9 47,-21 71,-17 102,13 21,21 30,40 30,62 0,30 6,24 88,-79 89,-114 175,-258 286,-486 71,-143 131,-332 170,-531 27,-141 35,-158 76,-173 33,-11 55,-7 81,16 15,12 20,10 47,-17 36,-35 82,-39 172,-15 60,16 283,20 405,6 129,-14 389,-90 542,-157 36,-16 74,-10 97,14 8,10 41,69 74,132 33,63 76,141 95,174 20,32 42,74 48,93 11,33 103,189 175,298 95,143 226,309 330,417 l 83,88 26,-26 c 20,-20 34,-25 60,-22 29,3 35,0 40,-19 8,-32 49,-56 86,-51 27,4 34,0 51,-29 22,-37 46,-53 77,-53 12,0 21,-3 21,-7 0,-5 -18,-69 -40,-143 -29,-95 -71,-197 -142,-345 -55,-115 -115,-246 -133,-290 -40,-101 -44,-108 -125,-235 -57.6611,-76.9253 -108.8483,-155.9466 -158,-245 -41.8198,-72.4341 -151.3026,-225.4907 -108,-300 65.0126,-70.4405 142.2339,-55.61 217,-79 24,-8 70,-20 103,-26 32,-7 54,-16 50,-20 -5,-5 -54.4462,-13.6036 -109,-25 -55.5759,-11.6099 -107.6919,-23.3581 -190,-41 -86.0096,-13.9667 -223.7414,-29.6671 -275,-106 -59,-97 -1,-143 260,-211 l 55,-14 -55,-13 c -165,-38 -232,-110 -185,-199 14,-28 13,-29 -13,-39 -15,-6 -49,-15 -77,-22 -65,-14 -153,-72 -161,-105 -13,-51 31,-105 84,-105 10,0 34,11 54,25 20,14 64,31 96,39 l 59,14 17,-83 c 9,-46 16,-107 16,-135 0,-44 4,-56 29,-81 37,-37 65,-37 102,0 27,27 29,35 29,105 0,41 -4,85 -9,98 -4,13 -11,47 -15,76 l -7,52 c 180.2886,6.4837 207.2871,-4.1989 355.8174,23.0309 66,12 305.1826,43.9691 335.1826,52.9691 85,26 170,34 170,16 0,-8 15,-29 34,-48 59,-59 104,-45 271,80 117,89 129,91 178,35 21,-24 59,-74 83,-111 30,-44 62,-77 97,-100 48,-31 58,-33 149,-36 l 98,-4 v 85 85 l -66,-6 c -82,-6 -102,4 -150,77 -160,243 -270,269 -476,113 l -66,-50 -13,29 c -12,31 -54,59 -86,59 -10,0 -34,-8 -53,-18 -19,-11 -62,-22 -95,-25 -33,-3 -80,-13 -104,-22 -25,-8 -97,-24 -160,-36 -150.6695,-31.5585 -331.7946,-51.6255 -476.3702,-37.9738 -66,9 53.3702,64.9738 155.3702,82.9738 221,38 324,64 342,86 31,38 17,107 -26,129 -33,17 -144,40 -226,46 -41,4 -95,10 -120,15 -25,5 -65,12 -90,16 -39,6 -42,8 -22,15 12,5 53,13 90,19 138,23 319,62 386,83 77,25 136,78 136,123 0,35 -47,80 -101,94 -24,7 -80,23 -124,36 -44,13 -136,38 -205,55 -69,17 -128,33 -133,36 -4,2 18,32 49,66 30,34 78,103 106,152 28,50 67,115 88,145 45,68 103,173 120,220 7,19 66,148 131,285 216,458 279,758 208,1000 -12,39 -27,81 -34,95 -7,14 -21,48 -30,75 -26,78 -35,93 -68,106 -58,24 -139,-13 -239,-108 -311,-298 -478,-484 -659,-735 -65,-90 -214,-337 -228,-377 -6,-19 -35,-75 -64,-125 -30,-50 -62,-110 -72,-134 -10,-24 -23,-42 -29,-40 -6,2 -69,22 -141,45 -163,52 -290,84 -380,94 -124,14 -394,10 -455,-6 -30,-8 -67,-15 -81,-15 -15,0 -38,-9 -52,-20 -14,-11 -28,-17 -31,-12 -2,4 -14,55 -25,112 -54,270 -139,504 -262,723 -28,51 -71,127 -95,169 -54,95 -275,380 -364,468 -36,36 -93,89 -128,118 -67,55 -96,63 -142,38 z" - id="path2" - sodipodi:nodetypes="cccccccsccccccccccscscccccccccccccscccccssccscccccccccccccccccscccccccccccccccccccscccccscccccsccsccccccscccscccccsccccssccccccccsscccccccccsscccsssscccccsscscccccccccccscscccccccsccccscsscccccccsscccccsscccssccccccc" - style="fill:#ffffff;fill-opacity:1" /> - <path - d="m 2500.8313,4134.78 c -87.3956,-37.9843 -97.6852,-154.4873 -98,-194.7871 0,-44.3118 -4,-131.089 -9,-192.0177 -5,-60.9286 -5,-159.707 -2,-219.7125 6,-105.2404 7,-110.7794 34,-132.0121 79,-62.775 164,3.6926 217,168.9386 55,175.4007 49,403.4217 -15,519.7401 -35,64.6213 -71,78.4687 -127,49.8507 z" - id="path4" - sodipodi:nodetypes="ccsccccc" - style="stroke-width:1;fill:#ffffff;fill-opacity:1" /> - <path - d="m 3344.4273,3996.6247 c -48.5583,-50.0953 -37.4868,-100.9153 -40.8178,-215.7915 -2.7697,-95.5212 29.3536,-310.9299 34.084,-408.9796 38.3413,-101.4275 148.4445,-115.5653 198.0212,-37.0586 31.0725,92.512 58.8307,202.0641 62.5807,328.6811 2.6164,88.3394 -12.9222,191.6407 -19.8758,275.1699 -22.7001,117.379 -144.8343,172.9775 -233.9923,57.9787 z" - id="path6" - sodipodi:nodetypes="csccscc" - style="stroke-width:1;fill:#ffffff;fill-opacity:1" /> - <path - d="M4633 1040 c-60 -25 -78 -94 -158 -605 -20 -126 -45 -273 -56 -327 -10 -53 -19 -99 -19 -102 0 -3 41 -6 91 -6 85 0 90 1 84 19 -3 11 3 66 14 123 10 57 24 142 31 190 l12 87 46 -5 c111 -12 107 -10 141 -74 37 -68 81 -231 81 -296 l0 -44 85 0 85 0 -6 42 c-10 75 -44 237 -60 280 -8 22 -12 43 -9 46 2 2 46 -1 97 -7 181 -22 397 -31 756 -31 l362 0 0 80 0 80 -362 0 c-388 0 -565 9 -828 41 -183 21 -351 46 -357 52 -10 11 46 278 73 342 16 39 11 76 -14 98 -23 21 -62 28 -89 17z" - id="path8" - style="fill:#ffffff;fill-opacity:1" /> - </g> -</svg> diff --git a/onlylegs/static/manifest.json b/onlylegs/static/manifest.json deleted file mode 100644 index ab9009a..0000000 --- a/onlylegs/static/manifest.json +++ /dev/null @@ -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 - } - \ No newline at end of file diff --git a/onlylegs/static/sass/animations.sass b/onlylegs/static/sass/animations.sass index 9fc623e..d85be1b 100644 --- a/onlylegs/static/sass/animations.sass +++ b/onlylegs/static/sass/animations.sass @@ -8,4 +8,12 @@ 0% left: -100% 100% - left: 100% \ No newline at end of file + left: 100% + +@keyframes imgFadeIn + 0% + opacity: 0 + // filter: blur(0.5rem) + 100% + opacity: 1 + // filter: blur(0) diff --git a/onlylegs/static/sass/components/banner.sass b/onlylegs/static/sass/components/banner.sass index 483c071..037e38d 100644 --- a/onlylegs/static/sass/components/banner.sass +++ b/onlylegs/static/sass/components/banner.sass @@ -20,26 +20,10 @@ background-color: RGB($fg-black) 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 height: 30rem max-height: 69vh - background-color: RGB($bg-300) - img position: absolute inset: 0 @@ -122,10 +106,10 @@ background-color: RGB($primary) border-radius: $rad + overflow: hidden .banner-small height: 3.5rem - background-color: RGB($bg-100) .banner-content padding: 0 0.5rem diff --git a/onlylegs/static/sass/components/buttons/block.sass b/onlylegs/static/sass/components/buttons/block.sass index 3891edd..f16f96e 100644 --- a/onlylegs/static/sass/components/buttons/block.sass +++ b/onlylegs/static/sass/components/buttons/block.sass @@ -59,6 +59,10 @@ &.black @include btn-block($black) + &.disabled, &:disabled + color: RGB($fg-dim) + cursor: unset + .input-checkbox padding: 0 display: flex diff --git a/onlylegs/static/sass/components/buttons/pill.sass b/onlylegs/static/sass/components/buttons/pill.sass index e9723f3..3767c01 100644 --- a/onlylegs/static/sass/components/buttons/pill.sass +++ b/onlylegs/static/sass/components/buttons/pill.sass @@ -68,6 +68,10 @@ color: RGB($primary) + &.disabled, &:disabled + color: RGB($fg-dim) + cursor: unset + .pill__critical color: RGB($critical) diff --git a/onlylegs/static/sass/components/gallery.sass b/onlylegs/static/sass/components/gallery.sass index 9b980ce..87d63ca 100644 --- a/onlylegs/static/sass/components/gallery.sass +++ b/onlylegs/static/sass/components/gallery.sass @@ -24,8 +24,6 @@ margin: 0.35rem padding: 0 - height: auto - position: relative border-radius: $rad-inner @@ -44,8 +42,7 @@ height: auto position: absolute - left: 0 - bottom: 0 + inset: 0 display: flex flex-direction: column @@ -88,19 +85,6 @@ object-position: center 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 box-shadow: 0 0.2rem 0.4rem 0.1rem RGBA($bg-100, 0.6) @@ -112,8 +96,6 @@ margin: 0.35rem padding: 0 - height: auto - position: relative border-radius: $rad-inner @@ -178,8 +160,7 @@ height: 100% position: absolute - top: 0 - left: 0 + inset: 0 object-fit: cover object-position: center @@ -187,14 +168,8 @@ background-color: RGB($bg-bright) border-radius: $rad-inner 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) - - &.loaded - filter: blur(0) - opacity: 1 + transition: transform 0.2s cubic-bezier(.79, .14, .15, .86) &.size-1 .data-1 @@ -219,11 +194,6 @@ transform: scale(0.6) rotate(-1deg) translate(-15%, -23%) z-index: +1 - &:after - content: "" - display: block - padding-bottom: 100% - &:hover .images &.size-1 @@ -252,3 +222,7 @@ @media (max-width: 800px) .gallery-grid grid-template-columns: auto auto auto + + .gallery-item + margin: 0.35rem + position: relative \ No newline at end of file diff --git a/onlylegs/static/sass/components/image-view.sass b/onlylegs/static/sass/components/image-view.sass new file mode 100644 index 0000000..1439670 --- /dev/null +++ b/onlylegs/static/sass/components/image-view.sass @@ -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 \ No newline at end of file diff --git a/onlylegs/static/sass/components/image-view/background.sass b/onlylegs/static/sass/components/image-view/background.sass deleted file mode 100644 index e7478b5..0000000 --- a/onlylegs/static/sass/components/image-view/background.sass +++ /dev/null @@ -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 \ No newline at end of file diff --git a/onlylegs/static/sass/components/image-view/image.sass b/onlylegs/static/sass/components/image-view/image.sass deleted file mode 100644 index 99fd1ac..0000000 --- a/onlylegs/static/sass/components/image-view/image.sass +++ /dev/null @@ -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 \ No newline at end of file diff --git a/onlylegs/static/sass/components/image-view/info-tab.sass b/onlylegs/static/sass/components/image-view/info-tab.sass deleted file mode 100644 index 1b42c1e..0000000 --- a/onlylegs/static/sass/components/image-view/info-tab.sass +++ /dev/null @@ -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 diff --git a/onlylegs/static/sass/components/image-view/view.sass b/onlylegs/static/sass/components/image-view/view.sass deleted file mode 100644 index 8103d88..0000000 --- a/onlylegs/static/sass/components/image-view/view.sass +++ /dev/null @@ -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 - - diff --git a/onlylegs/static/sass/components/navigation.sass b/onlylegs/static/sass/components/navigation.sass index c58805e..760a7d3 100644 --- a/onlylegs/static/sass/components/navigation.sass +++ b/onlylegs/static/sass/components/navigation.sass @@ -1,36 +1,20 @@ -.navigation - margin: 0 - padding: 0 - +nav width: 3.5rem height: 100% height: 100dvh display: flex flex-direction: column - justify-content: space-between position: fixed top: 0 left: 0 - background-color: RGB($bg-100) - color: RGB($fg-white) + background-color: transparent + color: inherit 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 height: 100% @@ -50,6 +34,7 @@ align-items: center background-color: transparent + color: inherit border: none text-decoration: none @@ -58,7 +43,7 @@ padding: 0.5rem font-size: 1.3rem border-radius: $rad-inner - color: RGB($fg-white) + color: inherit > .nav-pfp padding: 0.4rem @@ -72,68 +57,29 @@ object-fit: cover 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 > i, .nav-pfp background: RGBA($fg-white, 0.1) - span - opacity: 1 - left: 3.9rem - &.selected - > i - color: RGB($primary) + color: RGB($primary) &::before content: '' display: block position: absolute - top: 3px - left: 0 + top: 0.5rem + left: 0.2rem width: 3px - height: calc(100% - 6px) + height: calc(100% - 1rem) - background-color: RGB($primary) + background-color: currentColor border-radius: $rad-inner @media (max-width: $breakpoint) - .navigation + nav width: 100vw height: 3.5rem @@ -145,6 +91,8 @@ bottom: 0 left: 0 + background-color: RGB($background) + > span display: none diff --git a/onlylegs/static/sass/components/notification.sass b/onlylegs/static/sass/components/notification.sass index 3a468a2..feea589 100644 --- a/onlylegs/static/sass/components/notification.sass +++ b/onlylegs/static/sass/components/notification.sass @@ -22,23 +22,22 @@ margin: 0 padding: 0 - width: 450px + width: 24rem height: auto position: fixed - top: 0.3rem + bottom: 0.3rem right: 0.3rem display: flex - flex-direction: column + flex-direction: column-reverse z-index: 621 .sniffle__notification - margin: 0 0 0.3rem 0 + margin-top: 0.3rem padding: 0 - width: 450px height: auto max-height: 100px @@ -56,7 +55,7 @@ box-sizing: border-box 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 content: "" @@ -89,10 +88,8 @@ &.hide margin: 0 max-height: 0 - opacity: 0 transform: translateX(100%) - transition: all 0.4s ease-in-out, max-height 0.2s ease-in-out .sniffle__notification-icon @@ -130,6 +127,7 @@ @media (max-width: $breakpoint) .notifications + bottom: 3.8rem width: calc(100vw - 0.6rem) height: auto @@ -138,7 +136,7 @@ &.hide opacity: 0 - transform: translateY(-5rem) + transform: translateY(1rem) .sniffle__notification-time width: 100% diff --git a/onlylegs/static/sass/components/settings.sass b/onlylegs/static/sass/components/settings.sass index cce4ac3..8587a76 100644 --- a/onlylegs/static/sass/components/settings.sass +++ b/onlylegs/static/sass/components/settings.sass @@ -26,4 +26,4 @@ padding: 0 font-size: 1.25rem - font-weight: 700 \ No newline at end of file + font-weight: 700 diff --git a/onlylegs/static/sass/components/tags.sass b/onlylegs/static/sass/components/tags.sass index 4901829..6da6e67 100644 --- a/onlylegs/static/sass/components/tags.sass +++ b/onlylegs/static/sass/components/tags.sass @@ -1,11 +1,11 @@ .tag-icon margin: 0 - padding: 0.25rem 0.5rem + padding: 0.3rem 0.5rem display: flex - align-items: center + align-items: flex-end justify-content: center - gap: 0.25rem + gap: 0.3rem font-size: 0.9rem font-weight: 500 @@ -19,9 +19,8 @@ cursor: pointer transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out - svg - width: 1.15rem - height: 1.15rem + i + font-size: 1.15rem &:hover - background-color: RGBA($primary, 0.3) + background-color: RGBA($primary, 0.2) diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass index 097346c..daf7094 100644 --- a/onlylegs/static/sass/style.sass +++ b/onlylegs/static/sass/style.sass @@ -18,15 +18,13 @@ @import "components/buttons/pill" @import "components/buttons/block" -@import "components/image-view/view" +@import "components/image-view" @import "components/settings" -// Reset * box-sizing: border-box - font-family: $font - scrollbar-color: RGB($primary) transparent + font-family: $font ::-webkit-scrollbar width: 0.5rem @@ -37,66 +35,49 @@ ::-webkit-scrollbar-thumb:hover background: RGB($fg-white) -html, body +html margin: 0 padding: 0 - - min-height: 100vh - max-width: 100vw - - background-color: RGB($fg-white) - scroll-behavior: smooth - overflow-x: hidden -.wrapper +body margin: 0 padding: 0 0 0 3.5rem + max-width: 100% 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 flex-direction: column - - background-color: RGB($bg-bright) - color: RGB($bg-100) - -.big-text - height: 20rem - - display: flex - 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 + position: relative + background: RGBA($white, 1) + color: RGB($fg-black) + border-top-left-radius: $rad + overflow: hidden +@media (max-width: $breakpoint) + main + border-top-left-radius: 0 .error-page - width: 100% - height: 100vh + min-height: 100% display: flex flex-direction: column justify-content: center align-items: center - background-color: RGB($bg-bright) - h1 margin: 0 2rem @@ -113,23 +94,8 @@ html, body font-size: 1.25rem font-weight: 400 text-align: center - - color: $fg-black - - @media (max-width: $breakpoint) - .wrapper - padding: 0 0 3.5rem 0 - - .big-text - height: calc(75vh - 3.5rem) - - h1 - font-size: 3.5rem - .error-page - height: calc(100vh - 3.5rem) - h1 font-size: 4.5rem diff --git a/onlylegs/static/sass/variables.sass b/onlylegs/static/sass/variables.sass index bc1bd9f..e3fcd83 100644 --- a/onlylegs/static/sass/variables.sass +++ b/onlylegs/static/sass/variables.sass @@ -37,6 +37,11 @@ $font: 'Rubik', sans-serif $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 --bg-dim: 16, 16, 16 --bg-bright: 232, 227, 227 @@ -66,7 +71,7 @@ $breakpoint: 800px --success: var(--green) --info: var(--blue) - --rad: 8px + --rad: 0.5rem --rad-inner: calc(var(--rad) / 2) --animation-smooth: cubic-bezier(0.76, 0, 0.17, 1) diff --git a/onlylegs/templates/base.html b/onlylegs/templates/base.html new file mode 100644 index 0000000..b580c7f --- /dev/null +++ b/onlylegs/templates/base.html @@ -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> \ No newline at end of file diff --git a/onlylegs/templates/error.html b/onlylegs/templates/error.html index 9d76c79..7e9c20c 100644 --- a/onlylegs/templates/error.html +++ b/onlylegs/templates/error.html @@ -1,7 +1,7 @@ -{% extends 'layout.html' %} +{% extends 'base.html' %} {% block content %} - <span class="error-page"> + <div class="error-page"> <h1>{{error}}</h1> <p>{{msg}}</p> - </span> + </div> {% endblock %} diff --git a/onlylegs/templates/group.html b/onlylegs/templates/group.html index bebe8ec..67cfb1f 100644 --- a/onlylegs/templates/group.html +++ b/onlylegs/templates/group.html @@ -1,227 +1,76 @@ -{% extends 'layout.html' %} +{% extends 'base.html' %} +{% from 'macros/image.html' import gallery_item %} {% block nav_groups %}selected{% endblock %} + {% block head %} {% 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 %} <script type="text/javascript"> - function groupShare() { - try { - navigator.clipboard.writeText(window.location.href) - addNotification("Copied link!", 4); - } catch (err) { - addNotification("Failed to copy link! Are you on HTTP?", 2); - } + group_data = { + 'id': {{ group.id }}, + 'name': "{{ group.name }}", + 'description': "{{ group.description }}", } - - {% 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> <style> {% if images %} - .banner::after { - 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 }}); - } - .banner-content p { + :root { --bg-100: {{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }} } + + body { + background: rgb{{ images.0.colours.0 }} !important; color: {{ text_colour }} !important; } - .banner-content h1 { - color: {{ text_colour }} !important; + main { + 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 { 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 { - 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; } .banner-filter { - background: linear-gradient(90deg, rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}), - rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important; + 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; } @media (max-width: 800px) { .banner-filter { - background: linear-gradient(180deg, rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 1), - rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.5)) !important; + 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; } } - - .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 %} </style> {% endblock %} -{% block content %} + +{% block header %} {% if images %} <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> <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> @@ -229,12 +78,12 @@ <p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p> <div class="pill-row"> <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> {% if current_user.id == group.author.id %} <div> - <button class="pill-item pill__critical" onclick="groupDelete()"><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="groupDeletePopup()"><i class="ph ph-trash"></i></button> + <button class="pill-item pill__critical" onclick="groupEditPopup()"><i class="ph ph-pencil-simple"></i></button> </div> {% endif %} </div> @@ -247,29 +96,24 @@ <p class="banner-info">By {{ group.author.username }}</p> <div class="pill-row"> <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> {% if current_user.id == group.author.id %} <div> - <button class="pill-item pill__critical" onclick="groupDelete()"><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="groupDeletePopup()"><i class="ph ph-trash"></i></button> + <button class="pill-item pill__critical" onclick="groupEditPopup()"><i class="ph ph-pencil-simple"></i></button> </div> {% endif %} </div> </div> </div> {% endif %} +{% endblock %} +{% block content %} {% if images %} <div class="gallery-grid"> - {% for image in images %} - <a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('group.group_post', group_id=group.id, image_id=image.id) }}" style="background-color: rgb({{ image.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 %} + {% for image in images %}{{ gallery_item(image) }}{% endfor %} </div> {% else %} <div class="big-text"> diff --git a/onlylegs/templates/image.html b/onlylegs/templates/image.html index 5cf33d9..2a76cf0 100644 --- a/onlylegs/templates/image.html +++ b/onlylegs/templates/image.html @@ -1,215 +1,157 @@ -{% extends 'layout.html' %} +{% extends 'base.html' %} {% block page_index %} {% if return_page %}?page={{ return_page }}{% endif %}{% endblock %} + {% block head %} - <meta property="og:image" content="{{ url_for('media_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 property="og:image" content="{{ url_for('api.media', path='uploads/' + image.filename) }}"/> + <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"> - function imageShare() { - try { - navigator.clipboard.writeText(window.location.href) - addNotification("Copied link!", 4); - } 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 %} + const image_data = { + 'id': {{ image.id }}, + 'description': '{{ image.description }}', + 'alt': '{{ image.alt }}', + }; </script> <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), rgba({{ image.colours.1.0 }}, {{ image.colours.1.1 }}, {{ image.colours.1.2 }}, 0.2)); } </style> {% 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"> {% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %} <div> - <button class="pill-item" onclick="fullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button> - <button class="pill-item" onclick="imageShare()"><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> + <button class="pill-item" onclick="imageFullscreen()" id="fullscreenImage"><i class="ph ph-info"></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('api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a> </div> {% if current_user.id == image.author.id %} <div> - <button class="pill-item pill__critical" onclick="imageDelete()"><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="imageDeletePopup()"><i class="ph ph-trash"></i></button> + <button class="pill-item pill__critical" onclick="imageEditPopup()"><i class="ph ph-pencil-simple"></i></button> </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 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> {% endblock %} -{% block script %} - <script type="text/javascript"> - let infoTab = document.querySelectorAll('.info-tab'); +{% block content %} + <div class="background"> + <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++) { - infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() { - infoTab[i].classList.toggle('collapsed'); - }); - } - </script> -{% endblock %} \ No newline at end of file + <div class="image-container {% if close_tab %}collapsed{% endif %}"> + <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;" + {% 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 %} diff --git a/onlylegs/templates/index.html b/onlylegs/templates/index.html index 340313f..eead3a8 100644 --- a/onlylegs/templates/index.html +++ b/onlylegs/templates/index.html @@ -1,11 +1,17 @@ -{% extends 'layout.html' %} -{% block nav_home %}selected{% endblock %} -{% block content %} +{% extends 'base.html' %} +{% from 'macros/image.html' import gallery_item %} +{% 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-content"> <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1> - {% if total_images == 0 %} - <p class="banner-info">0 images D:</p> + {% if not total_images %} + <p class="banner-info">0 images!</p> {% elif total_images == 69 %} <p class="banner-info">{{ total_images }} images, nice</p> {% else %} @@ -15,29 +21,33 @@ {% if pages > 1 %} <div class="pill-row"> <div> - {% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index') }}"><i class="ph ph-arrow-line-left"></i></a>{% 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-arrow-left"></i></a> + {% if pages > 4 %} + <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> <span class="pill-text">{{ page }} / {{ pages }}</span> <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> - {% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}"><i class="ph ph-arrow-line-right"></i></a>{% endif %} + <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-caret-double-right"></i></a> + {% else %} + <button class="pill-item disabled"><i class="ph ph-caret-double-right"></i></button> + {% endif %} </div> </div> {% endif %} </div> </div> +{% endblock %} +{% block nav_home %}selected{% endblock %} +{% block content %} {% if images %} <div class="gallery-grid"> - {% for image in images %} - <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 %} + {% for image in images %}{{ gallery_item(image) }}{% endfor %} </div> {% else %} <div class="big-text"> diff --git a/onlylegs/templates/layout.html b/onlylegs/templates/layout.html deleted file mode 100644 index e01adb2..0000000 --- a/onlylegs/templates/layout.html +++ /dev/null @@ -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> \ No newline at end of file diff --git a/onlylegs/templates/list.html b/onlylegs/templates/list.html index af2edaa..21a7c9b 100644 --- a/onlylegs/templates/list.html +++ b/onlylegs/templates/list.html @@ -1,100 +1,11 @@ -{% extends 'layout.html' %} +{% extends 'base.html' %} {% block nav_groups %}selected{% endblock %} + {% block head %} - {% if images %} - <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 %} + {% if images %}<meta name="theme-color" content="rgb{{ images.0.colours.0 }}"/>{% endif %} {% endblock %} -{% block content %} + +{% block header %} <div class="banner-small"> <div class="banner-content"> <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1> @@ -108,17 +19,24 @@ {% if current_user.is_authenticated %} <div class="pill-row"> <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> {% endif %} </div> </div> +{% endblock %} +{% block content %} {% if groups %} <div class="gallery-grid"> {% 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"> <p class="image-subtitle">By {{ group.author.username }}</p> <p class="image-title">{{ group.name }}</p> @@ -126,7 +44,18 @@ <div class="images size-{{ group.images|length }}"> {% if group.images|length > 0 %} {% 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 %} {% else %} <img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/> diff --git a/onlylegs/templates/macros/header.html b/onlylegs/templates/macros/header.html new file mode 100644 index 0000000..4433fd0 --- /dev/null +++ b/onlylegs/templates/macros/header.html @@ -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 %} diff --git a/onlylegs/templates/macros/image.html b/onlylegs/templates/macros/image.html new file mode 100644 index 0000000..ed66b28 --- /dev/null +++ b/onlylegs/templates/macros/image.html @@ -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 %} diff --git a/onlylegs/templates/profile.html b/onlylegs/templates/profile.html index 7b4ae81..7880339 100644 --- a/onlylegs/templates/profile.html +++ b/onlylegs/templates/profile.html @@ -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 %} {% if user.picture %} - <meta property="og:image" content="{{ url_for('media_api.media', path='pfp/' + user.picture) }}"/> - {% endif %} - {% if user.colour %} - <meta name="theme-color" content="rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }})"/> + <meta property="og:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}"/> + <meta name="twitter:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}"> {% endif %} + {% if user.colour %}<meta name="theme-color" content="rgb{{ user.colour }}"/>{% endif %} + <meta name="twitter:card" content="summary"> <script type="text/javascript"> function moreInfo() { - popUpShow('{{ user.username }}', + popupShow('{{ user.username }}', '<p>Joined: {{ user.joined_at }}</p><br>' + '<p>Images: {{ images|length }}</p><br>' + '<p>Groups: {{ groups|length }}</p>'); @@ -18,12 +21,12 @@ <style> .banner-picture { - background-color: rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }}) !important; + background-color: rgb{{ user.colour }} !important; } </style> {% endblock %} -{% block nav_profile %}{% if user.id == current_user.id %}selected{% endif %}{% endblock %} -{% block content %} + +{% block header %} <div class="banner"> {% if user.banner %} <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> <div class="banner-content"> {% if user.picture %} - <img - class="banner-picture" - src="{{ url_for('media_api.media', path='pfp/' + user.picture) }}?r=pfp" - alt="Profile picture" - onload="imgFade(this)" - style="opacity:0;" - /> + <picture class="banner-picture"> + <source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=webp"> + <source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=png"> + <img + src="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp" + alt="Profile picture" + onload="imgFade(this)" + style="opacity:0;" + /> + </picture> {% else %} <img class="banner-picture" @@ -53,7 +59,7 @@ <p class="banner-subtitle">{{ images|length }} Images · {{ groups|length }} Groups</p> <div class="pill-row"> <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> </div> {% if user.id == current_user.id %} @@ -64,18 +70,13 @@ </div> </div> </div> +{% endblock %} +{% block content %} {% if images %} <h1 class="gallery-header">Images</h1> <div class="gallery-grid"> - {% for image in images %} - <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 %} + {% for image in images %}{{ gallery_item(image) }}{% endfor %} </div> {% else %} <div class="big-text"> @@ -84,4 +85,3 @@ </div> {% endif %} {% endblock %} - diff --git a/onlylegs/templates/settings.html b/onlylegs/templates/settings.html index b25e209..10840cb 100644 --- a/onlylegs/templates/settings.html +++ b/onlylegs/templates/settings.html @@ -1,7 +1,7 @@ -{% extends 'layout.html' %} - +{% extends 'base.html' %} {% block nav_settings %}selected{% endblock %} -{% block content %} + +{% block header %} <div class="banner-small"> <div class="banner-content"> <h1 class="banner-header">Settings</h1> @@ -13,28 +13,53 @@ </div> </div> </div> +{% endblock %} - <div class="settings-content" id="profileSettings"> - <h2>Profile Settings</h2> - <form method="POST" action="{{ url_for('account_api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data"> - <h3>Profile Picture</h3> - <input type="file" name="file" tab-index="-1"/> - <input type="submit" value="Upload" class="btn-block"> - </form> - - <form method="POST" action="{{ url_for('account_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> +{% block content %} + <div class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem"> + <div class="info-header"> + <i class="ph ph-info"></i> + <h2>Profile Settings</h2> + <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button> + </div> + <div class="info-table"> + <form method="POST" action="{{ url_for('api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data"> + <h3>Profile Picture</h3> + <input type="file" name="file" tab-index="-1"/> + <input type="submit" value="Upload" class="btn-block"> + </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 class="settings-content" id="profileSettings"> - <h2>Account Settings</h2> - <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 class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem"> + <div class="info-header"> + <i class="ph ph-info"></i> + <h2>Account Settings</h2> + <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button> + </div> + <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> -{% endblock %} \ No newline at end of file +{% 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 %} diff --git a/onlylegs/utils/colour.py b/onlylegs/utils/colour.py new file mode 100644 index 0000000..4c9c8b0 --- /dev/null +++ b/onlylegs/utils/colour.py @@ -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 diff --git a/onlylegs/utils/contrast.py b/onlylegs/utils/contrast.py deleted file mode 100644 index 2872914..0000000 --- a/onlylegs/utils/contrast.py +++ /dev/null @@ -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 diff --git a/onlylegs/utils/generate_image.py b/onlylegs/utils/generate_image.py index fd86d28..c597d3d 100644 --- a/onlylegs/utils/generate_image.py +++ b/onlylegs/utils/generate_image.py @@ -7,7 +7,7 @@ from werkzeug.utils import secure_filename 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 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: ext = file_ext.strip(".") - # PIL doesnt like jpg so we convert it to jpeg - if ext.lower() == "jpg": - ext = "jpeg" + ext = "jpeg" if ext.lower() == "jpg" else ext.lower() # Set resolution based on preset resolutions - if resolution in ["prev", "preview"]: + if resolution in ("prev", "preview"): res_x, res_y = (1920, 1080) - elif resolution in ["thumb", "thumbnail"]: - res_x, res_y = (400, 400) - elif resolution in ["pfp", "profile"]: - res_x, res_y = (200, 200) - elif resolution in ["icon", "favicon"]: - res_x, res_y = (25, 25) + elif resolution in ("thumb", "thumbnail"): + res_x, res_y = (300, 300) + elif resolution in ("pfp", "profile"): + res_x, res_y = (150, 150) + elif resolution in ("icon", "favicon"): + res_x, res_y = (30, 30) else: 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 os.path.exists(os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}")): - return 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, cache_file_name) # Check if image exists in the uploads directory 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 try: 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, ) except OSError: @@ -69,11 +69,11 @@ def generate_thumbnail(file_path, resolution, ext=None): # so we convert to RGB and try again image = image.convert("RGB") 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, ) # No need to keep the image in memory, learned the hard way 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) diff --git a/onlylegs/utils/metadata/__init__.py b/onlylegs/utils/metadata/__init__.py index 302116a..ace9e76 100644 --- a/onlylegs/utils/metadata/__init__.py +++ b/onlylegs/utils/metadata/__init__.py @@ -12,90 +12,74 @@ from .helpers 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): - """ - Initialize the metadata parser - """ - self.file_path = file_path - img_exif = {} + img_exif = {} + file = Image.open(file_path) - try: - file = Image.open(file_path) - tags = file._getexif() - img_exif = {} + 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 - for tag, value in TAGS.items(): - if tag in tags: - img_exif[value] = tags[tag] + try: + tags = file._getexif() + 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) - 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() - file.close() - 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 + return _format_data(img_exif) - self.encoded = img_exif - def yoink(self): - """ - Yoinks the metadata from the image - """ - if not os.path.isfile(self.file_path): - return None - return self.format_data(self.encoded) +def _format_data(encoded): + """ + Formats the data into a dictionary + """ + exif = { + "Photographer": {}, + "Camera": {}, + "Software": {}, + "File": {}, + } - @staticmethod - def format_data(encoded_exif): - """ - Formats the data into a dictionary - """ - exif = { - "Photographer": {}, - "Camera": {}, - "Software": {}, - "File": {}, - } + # Thanks chatGPT xP + # the helper function works, so not sure why it triggers pylint + for key, value in encoded.items(): + for mapping_name, mapping_val in EXIF_MAPPING: + if key in mapping_val: + if len(mapping_val[key]) == 2: + exif[mapping_name][mapping_val[key][0]] = { + "raw": value, + "formatted": ( + getattr( + helpers, # pylint: disable=E0602 + mapping_val[key][1], + )(value) + ), + } + else: + exif[mapping_name][mapping_val[key][0]] = { + "raw": value, + } + continue - # Thanks chatGPT xP - # the helper function works, so not sure why it triggers pylint - for key, value in encoded_exif.items(): - for mapping_name, mapping_val in EXIF_MAPPING: - if key in mapping_val: - if len(mapping_val[key]) == 2: - exif[mapping_name][mapping_val[key][0]] = { - "raw": value, - "formatted": ( - getattr( - helpers, # pylint: disable=E0602 - mapping_val[key][1], - )(value) - ), - } - else: - exif[mapping_name][mapping_val[key][0]] = { - "raw": value, - } - continue + # Remove empty keys + 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"] - # Remove empty keys - 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 + return exif diff --git a/onlylegs/views/group.py b/onlylegs/views/group.py index 7fe1bdc..56f39f8 100644 --- a/onlylegs/views/group.py +++ b/onlylegs/views/group.py @@ -3,11 +3,11 @@ Onlylegs - Image Groups Why groups? Because I don't like calling these albums sounds more limiting that it actually is in this gallery """ -from flask import Blueprint, render_template, url_for - -from onlylegs.models import Post, User, GroupJunction, Group +from flask import Blueprint, render_template, url_for, request, flash, jsonify +from flask_login import login_required, current_user +from onlylegs.models import Pictures, Users, AlbumJunction, Albums from onlylegs.extensions import db -from onlylegs.utils import contrast +from onlylegs.utils import colour blueprint = Blueprint("group", __name__, url_prefix="/group") @@ -18,21 +18,21 @@ def 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 group in groups: group.author_username = ( - User.query.with_entities(User.username) - .filter(User.id == group.author_id) + Users.query.with_entities(Users.username) + .filter(Users.id == group.author_id) .first()[0] ) # Get the 3 most recent images images = ( - GroupJunction.query.with_entities(GroupJunction.post_id) - .filter(GroupJunction.group_id == group.id) - .order_by(GroupJunction.date_added.desc()) + AlbumJunction.query.with_entities(AlbumJunction.picture_id) + .filter(AlbumJunction.album_id == group.id) + .order_by(AlbumJunction.date_added.desc()) .limit(3) ) @@ -40,40 +40,67 @@ def groups(): group.images = [] for image in images: group.images.append( - Post.query.with_entities(Post.filename, Post.alt, Post.colours, Post.id) - .filter(Post.id == image[0]) + Pictures.query.with_entities( + Pictures.filename, Pictures.alt, Pictures.colours, Pictures.id + ) + .filter(Pictures.id == image[0]) .first() ) 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): """ Group view, shows all images in a group """ # 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 junction = ( - GroupJunction.query.with_entities(GroupJunction.post_id) - .filter(GroupJunction.group_id == group_id) - .order_by(GroupJunction.date_added.desc()) + AlbumJunction.query.with_entities(AlbumJunction.picture_id) + .filter(AlbumJunction.album_id == group_id) + .order_by(AlbumJunction.date_added.desc()) .all() ) # Get the image data for each image in the group images = [] 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 text_colour = "rgb(var(--fg-black))" if images: - text_colour = contrast.contrast( - images[0].colours[0], "rgb(var(--fg-black))", "rgb(var(--fg-white))" + colour_obj = colour.Colour(images[0].colours[0]) + text_colour = ( + "rgb(var(--fg-black));" + if colour_obj.is_light() + else "rgb(var(--fg-white));" ) 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>") def group_post(group_id, image_id): """ Image view, shows the image and its metadata from a specific group """ # 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 groups = ( - GroupJunction.query.with_entities(GroupJunction.group_id) - .filter(GroupJunction.post_id == image_id) + AlbumJunction.query.with_entities(AlbumJunction.album_id) + .filter(AlbumJunction.picture_id == image_id) .all() ) @@ -100,24 +175,24 @@ def group_post(group_id, image_id): image.groups = [] for group in groups: image.groups.append( - Group.query.with_entities(Group.id, Group.name) - .filter(Group.id == group[0]) + Albums.query.with_entities(Albums.id, Albums.name) + .filter(Albums.id == group[0]) .first() ) # Get the next and previous images in the group next_url = ( - GroupJunction.query.with_entities(GroupJunction.post_id) - .filter(GroupJunction.group_id == group_id) - .filter(GroupJunction.post_id > image_id) - .order_by(GroupJunction.date_added.asc()) + AlbumJunction.query.with_entities(AlbumJunction.picture_id) + .filter(AlbumJunction.album_id == group_id) + .filter(AlbumJunction.picture_id > image_id) + .order_by(AlbumJunction.date_added.asc()) .first() ) prev_url = ( - GroupJunction.query.with_entities(GroupJunction.post_id) - .filter(GroupJunction.group_id == group_id) - .filter(GroupJunction.post_id < image_id) - .order_by(GroupJunction.date_added.desc()) + AlbumJunction.query.with_entities(AlbumJunction.picture_id) + .filter(AlbumJunction.album_id == group_id) + .filter(AlbumJunction.picture_id < image_id) + .order_by(AlbumJunction.date_added.desc()) .first() ) @@ -127,6 +202,14 @@ def group_post(group_id, image_id): if prev_url: 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( - "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, ) diff --git a/onlylegs/views/image.py b/onlylegs/views/image.py index a192f15..75a70fd 100644 --- a/onlylegs/views/image.py +++ b/onlylegs/views/image.py @@ -1,27 +1,39 @@ """ Onlylegs - Image View """ +import os +import logging +import pathlib from math import ceil -from flask import Blueprint, render_template, url_for, current_app -from onlylegs.models import Post, GroupJunction, Group +from flask import ( + 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 blueprint = Blueprint("image", __name__, url_prefix="/image") -@blueprint.route("/<int:image_id>") +@blueprint.route("/<int:image_id>", methods=["GET"]) def image(image_id): """ Image view, shows the image and its metadata """ # 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 groups = ( - GroupJunction.query.with_entities(GroupJunction.group_id) - .filter(GroupJunction.post_id == image_id) + AlbumJunction.query.with_entities(AlbumJunction.album_id) + .filter(AlbumJunction.picture_id == image_id) .all() ) @@ -29,34 +41,34 @@ def image(image_id): image.groups = [] for group in groups: image.groups.append( - Group.query.with_entities(Group.id, Group.name) - .filter(Group.id == group[0]) + Albums.query.with_entities(Albums.id, Albums.name) + .filter(Albums.id == group[0]) .first() ) # Get the next and previous images # Check if there is a group ID set next_url = ( - Post.query.with_entities(Post.id) - .filter(Post.id > image_id) - .order_by(Post.id.asc()) + Pictures.query.with_entities(Pictures.id) + .filter(Pictures.id > image_id) + .order_by(Pictures.id.asc()) .first() ) prev_url = ( - Post.query.with_entities(Post.id) - .filter(Post.id < image_id) - .order_by(Post.id.desc()) + Pictures.query.with_entities(Pictures.id) + .filter(Pictures.id < image_id) + .order_by(Pictures.id.desc()) .first() ) # 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 prev_url: - prev_url = url_for("image.image", image_id=prev_url[0]) + next_url = url_for("image.image", image_id=next_url[0]) if next_url else None + prev_url = url_for("image.image", image_id=prev_url[0]) if prev_url else None # 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"] # 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 break + close_tab = True + if request.cookies.get("image-info") == "0": + close_tab = False + return render_template( "image.html", image=image, next_url=next_url, prev_url=prev_url, 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 diff --git a/onlylegs/views/index.py b/onlylegs/views/index.py index 3cc0155..943507a 100644 --- a/onlylegs/views/index.py +++ b/onlylegs/views/index.py @@ -2,11 +2,9 @@ Onlylegs Gallery - Index view """ from math import ceil - from flask import Blueprint, render_template, request, current_app from werkzeug.exceptions import abort - -from onlylegs.models import Post +from onlylegs.models import Pictures, Users blueprint = Blueprint("gallery", __name__) @@ -17,31 +15,32 @@ def index(): """ 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 page = request.args.get("page", default=1, type=int) limit = current_app.config["UPLOAD_CONF"]["max-load"] # get the total number of images in the database # 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) if page > pages: - abort( + return abort( 404, - "You have reached the far and beyond, " - + "but you will not find your answers here.", + "You have reached the far and beyond, but you will not find your answers here.", ) # get the images for the current page images = ( - Post.query.with_entities( - Post.filename, Post.alt, Post.colours, Post.created_at, Post.id + Pictures.query.with_entities( + 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) .limit(limit) .all() diff --git a/onlylegs/views/profile.py b/onlylegs/views/profile.py index b83399f..22457d7 100644 --- a/onlylegs/views/profile.py +++ b/onlylegs/views/profile.py @@ -5,7 +5,7 @@ from flask import Blueprint, render_template, request from werkzeug.exceptions import abort 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 @@ -27,9 +27,9 @@ def profile(): abort(404, "You must be logged in to view your own profile!") # 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() - groups = Group.query.filter(Group.author_id == user_id).all() + images = Pictures.query.filter(Pictures.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) diff --git a/poetry.lock b/poetry.lock index ff854af..8c1c926 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,14 +2,14 @@ [[package]] name = "alembic" -version = "1.10.3" +version = "1.11.2" description = "A database migration tool for SQLAlchemy." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, - {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, + {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"}, + {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"}, ] [package.dependencies] @@ -24,14 +24,14 @@ tz = ["python-dateutil"] [[package]] name = "astroid" -version = "2.15.3" +version = "2.15.6" description = "An abstract syntax tree for Python with inference support." category = "main" optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.15.3-py3-none-any.whl", hash = "sha256:f11e74658da0f2a14a8d19776a8647900870a63de71db83713a8e77a6af52662"}, - {file = "astroid-2.15.3.tar.gz", hash = "sha256:44224ad27c54d770233751315fa7f74c46fa3ee0fab7beef1065f99f09897efe"}, + {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, + {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, ] [package.dependencies] @@ -44,37 +44,34 @@ wrapt = [ [[package]] name = "black" -version = "23.3.0" +version = "23.7.0" description = "The uncompromising code formatter." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {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"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, ] [package.dependencies] @@ -197,27 +194,27 @@ files = [ ] [[package]] -name = "cachelib" -version = "0.9.0" -description = "A collection of cache libraries in the same API interface." +name = "cachetools" +version = "5.3.1" +description = "Extensible memoizing collections and decorators" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cachelib-0.9.0-py3-none-any.whl", hash = "sha256:811ceeb1209d2fe51cd2b62810bd1eccf70feba5c52641532498be5c675493b3"}, - {file = "cachelib-0.9.0.tar.gz", hash = "sha256:38222cc7c1b79a23606de5c2607f4925779e37cdcea1c2ad21b8bae94b5425a5"}, + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, ] [[package]] name = "click" -version = "8.1.3" +version = "8.1.6" description = "Composable command line interface toolkit" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, ] [package.dependencies] @@ -263,14 +260,14 @@ files = [ [[package]] name = "dill" -version = "0.3.6" -description = "serialize all of python" +version = "0.3.7" +description = "serialize all of Python" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, - {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, ] [package.extras] @@ -318,19 +315,18 @@ webassets = ">=2.0" [[package]] name = "flask-caching" -version = "2.0.2" -description = "Adds caching support to Flask applications." +version = "1.10.1" +description = "Adds caching support to your Flask application" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.5" files = [ - {file = "Flask-Caching-2.0.2.tar.gz", hash = "sha256:24b60c552d59a9605cc1b6a42c56cdb39a82a28dab4532bbedb9222ae54ecb4e"}, - {file = "Flask_Caching-2.0.2-py3-none-any.whl", hash = "sha256:19571f2570e9b8dd9dd9d2f49d7cbee69c14ebe8cc001100b1eb98c379dd80ad"}, + {file = "Flask-Caching-1.10.1.tar.gz", hash = "sha256:cf19b722fcebc2ba03e4ae7c55b532ed53f0cbf683ce36fafe5e881789a01c00"}, + {file = "Flask_Caching-1.10.1-py3-none-any.whl", hash = "sha256:bcda8acbc7508e31e50f63e9b1ab83185b446f6b6318bd9dd1d45626fba2e903"}, ] [package.dependencies] -cachelib = ">=0.9.0,<0.10.0" -Flask = "<3" +Flask = "*" [[package]] name = "flask-compress" @@ -383,19 +379,19 @@ Flask-SQLAlchemy = ">=1.0" [[package]] name = "flask-sqlalchemy" -version = "3.0.3" +version = "3.0.5" description = "Add SQLAlchemy support to your Flask application." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"}, - {file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"}, + {file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"}, + {file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"}, ] [package.dependencies] -Flask = ">=2.2" -SQLAlchemy = ">=1.4.18" +flask = ">=2.2.5" +sqlalchemy = ">=1.4.18" [[package]] name = "greenlet" @@ -494,14 +490,14 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "importlib-metadata" -version = "6.5.0" +version = "6.8.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.5.0-py3-none-any.whl", hash = "sha256:03ba783c3a2c69d751b109fc0c94a62c51f581b3d6acf8ed1331b6d5729321ff"}, - {file = "importlib_metadata-6.5.0.tar.gz", hash = "sha256:7a8bdf1bc3a726297f5cfbc999e6e7ff6b4fa41b26bba4afc580448624460045"}, + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, ] [package.dependencies] @@ -510,26 +506,26 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 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]] name = "importlib-resources" -version = "5.12.0" +version = "6.0.0" description = "Read resources from Python packages" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, + {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"}, + {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "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)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +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]] name = "isort" @@ -673,62 +669,62 @@ testing = ["pytest"] [[package]] name = "markupsafe" -version = "2.1.2" +version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {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.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {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.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {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.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {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.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {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.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {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.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {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.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {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.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {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.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {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.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] [[package]] @@ -769,14 +765,14 @@ files = [ [[package]] name = "pathspec" -version = "0.11.1" +version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] [[package]] @@ -861,34 +857,34 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] 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\"." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, - {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +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.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pylint" -version = "2.17.2" +version = "2.17.5" description = "python code static checker" category = "main" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"}, - {file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"}, + {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"}, + {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"}, ] [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\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -922,120 +918,120 @@ cli = ["click (>=5.0)"] [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.1" description = "YAML parser and emitter for Python" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {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-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {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-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {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-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {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-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {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-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] name = "setuptools" -version = "67.7.0" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.7.0-py3-none-any.whl", hash = "sha256:888be97fde8cc3afd60f7784e678fa29ee13c4e5362daa7104a93bba33646c50"}, - {file = "setuptools-67.7.0.tar.gz", hash = "sha256:b7e53a01c6c654d26d2999ee033d8c6125e5fa55f03b7b193f937ae7ac999f22"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [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"] -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"] [[package]] name = "sqlalchemy" -version = "2.0.9" +version = "2.0.19" description = "Database Abstraction Library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"}, - {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"}, - {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"}, - {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f005245e1cb9b8ca53df73ee85e029ac43155e062405015e49ec6187a2e3fb44"}, - {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:34eb96c1de91d8f31e988302243357bef3f7785e1b728c7d4b98bd0c117dafeb"}, - {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e472e9627882f2d75b87ff91c5a2bc45b31a226efc7cc0a054a94fffef85862"}, - {file = "SQLAlchemy-2.0.9-cp310-cp310-win32.whl", hash = "sha256:0a865b5ec4ba24f57c33b633b728e43fde77b968911a6046443f581b25d29dd9"}, - {file = "SQLAlchemy-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:6e84ab63d25d8564d7a8c05dc080659931a459ee27f6ed1cf4c91f292d184038"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db4bd1c4792da753f914ff0b688086b9a8fd78bb9bc5ae8b6d2e65f176b81eb9"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5363a1c65fde7b7466769d4261126d07d872fc2e816487ae6cec93da604b6b"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc4eeb1737a5a9bdb0c24f4c982319fa6edd23cdee27180978c29cbb026f2bd"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbda1da8d541904ba262825a833c9f619e93cb3fd1156be0a5e43cd54d588dcd"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d5327f54a9c39e7871fc532639616f3777304364a0bb9b89d6033ad34ef6c5f8"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac6a0311fb21a99855953f84c43fcff4bdca27a2ffcc4f4d806b26b54b5cddc9"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-win32.whl", hash = "sha256:d209594e68bec103ad5243ecac1b40bf5770c9ebf482df7abf175748a34f4853"}, - {file = "SQLAlchemy-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:865392a50a721445156809c1a6d6ab6437be70c1c2599f591a8849ed95d3c693"}, - {file = "SQLAlchemy-2.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b49f1f71d7a44329a43d3edd38cc5ee4c058dfef4487498393d16172007954b"}, - {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a019f723b6c1e6b3781be00fb9e0844bc6156f9951c836ff60787cc3938d76"}, - {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9838bd247ee42eb74193d865e48dd62eb50e45e3fdceb0fdef3351133ee53dcf"}, - {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:78612edf4ba50d407d0eb3a64e9ec76e6efc2b5d9a5c63415d53e540266a230a"}, - {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f61ab84956dc628c8dfe9d105b6aec38afb96adae3e5e7da6085b583ff6ea789"}, - {file = "SQLAlchemy-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:07950fc82f844a2de67ddb4e535f29b65652b4d95e8b847823ce66a6d540a41d"}, - {file = "SQLAlchemy-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e62c4e762d6fd2901692a093f208a6a6575b930e9458ad58c2a7f080dd6132da"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3e5864eba71a3718236a120547e52c8da2ccb57cc96cecd0480106a0c799c92"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d06e119cf79a3d80ab069f064a07152eb9ba541d084bdaee728d8a6f03fd03d"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee2946042cc7851842d7a086a92b9b7b494cbe8c3e7e4627e27bc912d3a7655e"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f984a190d249769a050634b248aef8991acc035e849d02b634ea006c028fa8"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4780be0f19e5894c17f75fc8de2fe1ae233ab37827125239ceb593c6f6bd1e2"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:68ed381bc340b4a3d373dbfec1a8b971f6350139590c4ca3cb722fdb50035777"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-win32.whl", hash = "sha256:aa5c270ece17c0c0e0a38f2530c16b20ea05d8b794e46c79171a86b93b758891"}, - {file = "SQLAlchemy-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:1b69666e25cc03c602d9d3d460e1281810109e6546739187044fc256c67941ef"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6e27189ff9aebfb2c02fd252c629ea58657e7a5ff1a321b7fc9c2bf6dc0b5f3"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8239ce63a90007bce479adf5460d48c1adae4b933d8e39a4eafecfc084e503c"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f759eccb66e6d495fb622eb7f4ac146ae674d829942ec18b7f5a35ddf029597"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246712af9fc761d6c13f4f065470982e175d902e77aa4218c9cb9fc9ff565a0c"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b72dccc5864ea95c93e0a9c4e397708917fb450f96737b4a8395d009f90b868"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:93c78d42c14aa9a9e0866eacd5b48df40a50d0e2790ee377af7910d224afddcf"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-win32.whl", hash = "sha256:f49c5d3c070a72ecb96df703966c9678dda0d4cb2e2736f88d15f5e1203b4159"}, - {file = "SQLAlchemy-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:4c3020afb144572c7bfcba9d7cce57ad42bff6e6115dffcfe2d4ae6d444a214f"}, - {file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"}, - {file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-win32.whl", hash = "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-win32.whl", hash = "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-win32.whl", hash = "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-win_amd64.whl", hash = "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-win32.whl", hash = "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-win_amd64.whl", hash = "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-win32.whl", hash = "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-win_amd64.whl", hash = "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0"}, + {file = "SQLAlchemy-2.0.19-py3-none-any.whl", hash = "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972"}, + {file = "SQLAlchemy-2.0.19.tar.gz", hash = "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f"}, ] [package.dependencies] @@ -1062,6 +1058,7 @@ postgresql-pg8000 = ["pg8000 (>=1.29.1)"] postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3-binary"] @@ -1079,26 +1076,26 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.7" +version = "0.12.1" description = "Style preserving TOML library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, - {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, + {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, + {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, ] [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] @@ -1115,14 +1112,14 @@ files = [ [[package]] name = "werkzeug" -version = "2.3.3" +version = "2.3.6" description = "The comprehensive WSGI web application library." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.3-py3-none-any.whl", hash = "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a"}, - {file = "Werkzeug-2.3.3.tar.gz", hash = "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"}, + {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, + {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, ] [package.dependencies] @@ -1218,21 +1215,21 @@ files = [ [[package]] name = "zipp" -version = "3.15.0" +version = "3.16.2" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, + {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "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)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +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] lock-version = "2.0" python-versions = "^3.8" -content-hash = "15ba5eebb3e5385a9e2ab48b5d6156bd59df6b7a1d431c54d95bba5a8ec53004" +content-hash = "96ec0d1f7b512afb05455262fa2de8c4f862bf68fdae513f8552dc30c6e5ab49" diff --git a/pyproject.toml b/pyproject.toml index f32f3b0..3752e8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OnlyLegs" -version = "0.1.2" +version = "0.1.5" repository = "https://github.com/Fluffy-Bean/onlylegs" license = "MIT" readme = "README.md" @@ -13,7 +13,7 @@ Flask = "^2.3.2" Flask-Sqlalchemy = "^3.0.3" Flask-Migrate = "^4.0.4" Flask-Compress = "^1.13" -Flask-Caching = "^2.0.2" +Flask-Caching = "1.10.1" Flask-Assets = "^2.0" Flask-Login = "^0.6.2" python-dotenv = "^0.21.0" @@ -27,6 +27,7 @@ jsmin = "^3.0.1" cssmin = "^0.2.0" pylint = "^2.16.3" black = "^23.3.0" +cachetools = "^5.3.0" [build-system] requires = ["poetry-core"] diff --git a/run.py b/run.py index 8653117..a10fa90 100644 --- a/run.py +++ b/run.py @@ -25,9 +25,9 @@ Configuration() 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: from setup.runner import OnlyLegs # pylint: disable=C0412 import sys diff --git a/setup/runner.py b/setup/runner.py index 76a8a32..94cd827 100644 --- a/setup/runner.py +++ b/setup/runner.py @@ -32,4 +32,4 @@ class OnlyLegs(Application): return "OnlyLegs" def load(self): - return util.import_app("onlylegs:create_app()") + return util.import_app("onlylegs.app:app")