diff --git a/onlylegs/__init__.py b/onlylegs/__init__.py index a0853e2..e15e3e1 100644 --- a/onlylegs/__init__.py +++ b/onlylegs/__init__.py @@ -4,42 +4,42 @@ This is the main app file, it loads all the other files and sets up the app """ import os import logging -import platformdirs from flask_assets import Bundle - from flask_migrate import init as migrate_init -from flask_migrate import upgrade as migrate_upgrade -from flask_migrate import migrate as migrate_migrate 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.views import index, image, group, settings, profile -from onlylegs import api -from onlylegs import auth +from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR from onlylegs.models import User - - -INSTACE_DIR = os.path.join(platformdirs.user_config_dir("onlylegs"), "instance") -MIGRATIONS_DIR = os.path.join(INSTACE_DIR, "migrations") +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=INSTACE_DIR) + app = Flask(__name__, instance_path=INSTANCE_DIR) app.config.from_pyfile("config.py") # DATABASE db.init_app(app) - migrate.init_app(app, db) + migrate.init_app(app, db, directory=MIGRATIONS_DIR) # If database file doesn't exist, create it - if not os.path.exists(os.path.join(INSTACE_DIR, "gallery.sqlite3")): + if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")): print("Creating database") with app.app_context(): db.create_all() @@ -68,12 +68,6 @@ def create_app(): # pylint: disable=R0914 print("Creating migrations directory") migrate_init(directory=MIGRATIONS_DIR) - # Check if migrations are up to date - with app.app_context(): - print("Checking for schema changes...") - migrate_migrate(directory=MIGRATIONS_DIR) - migrate_upgrade(directory=MIGRATIONS_DIR) - # LOGIN MANAGER # can also set session_protection to "strong" # this would protect against session hijacking @@ -106,9 +100,11 @@ def create_app(): # pylint: disable=R0914 # ASSETS assets.init_app(app) - scripts = Bundle("js/*.js", output="gen/js.js", depends="js/*.js") # filter jsmin is broken :c + scripts = Bundle( + "js/*.js", output="gen/js.js", depends="js/*.js" + ) # filter jsmin is broken :c styles = Bundle( - "sass/*.sass", + "sass/style.sass", filters="libsass, cssmin", output="gen/styles.css", depends="sass/**/*.sass", @@ -118,13 +114,17 @@ def create_app(): # pylint: disable=R0914 assets.register("styles", styles) # BLUEPRINTS - app.register_blueprint(auth.blueprint) - app.register_blueprint(api.blueprint) - app.register_blueprint(index.blueprint) - app.register_blueprint(image.blueprint) - app.register_blueprint(group.blueprint) - app.register_blueprint(profile.blueprint) - app.register_blueprint(settings.blueprint) + 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) diff --git a/onlylegs/api.py b/onlylegs/api.py deleted file mode 100644 index 3ddfc3b..0000000 --- a/onlylegs/api.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -Onlylegs - API endpoints -""" -from uuid import uuid4 -import os -import pathlib -import logging -import platformdirs - -from flask import Blueprint, send_from_directory, abort, flash, request, current_app -from werkzeug.utils import secure_filename -from flask_login import login_required, current_user - -from colorthief import ColorThief - -from onlylegs.extensions import db -from onlylegs.models import Post, Group, GroupJunction -from onlylegs.utils import metadata as mt -from onlylegs.utils.generate_image import generate_thumbnail - - -blueprint = Blueprint("api", __name__, url_prefix="/api") - - -@blueprint.route("/file/", methods=["GET"]) -def file(file_name): - """ - Returns a file from the uploads folder - r for resolution, 400x400 or thumb for thumbnail - """ - res = request.args.get("r", default=None, type=str) # Type of file (thumb, etc) - ext = request.args.get("e", default=None, type=str) # File extension - file_name = secure_filename(file_name) # Sanitize file name - - # 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["UPLOAD_FOLDER"], file_name) - ): - abort(404) - return send_from_directory(current_app.config["UPLOAD_FOLDER"], file_name) - - thumb = generate_thumbnail(file_name, res, ext) - if not thumb: - abort(404) - - 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 no image is uploaded, return 404 error - if not form_file: - return abort(404) - - # Get file extension, generate random name and set file path - img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower() - img_name = "GWAGWA_" + str(uuid4()) - img_path = os.path.join( - current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext - ) - - # Check if file extension is allowed - if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys(): - logging.info("File extension not allowed: %s", img_ext) - abort(403) - - # Save file - try: - form_file.save(img_path) - except OSError as err: - logging.info("Error saving file %s because of %s", img_path, err) - abort(500) - - img_exif = mt.Metadata(img_path).yoink() # Get EXIF data - img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette - - # Save to database - 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 "Gwa Gwa" # Return something so the browser doesn't show an error - - -@blueprint.route("/delete/", methods=["POST"]) -@login_required -def delete_image(image_id): - """ - Deletes an image from the server and database - """ - post = Post.query.filter_by(id=image_id).first() - - # Check if image exists and if user is allowed to delete it (author) - if post is None: - abort(404) - if post.author_id != current_user.id: - abort(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_path = os.path.join(platformdirs.user_config_dir("onlylegs"), "cache") - cache_name = post.filename.rsplit(".")[0] - for cache_file in pathlib.Path(cache_path).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 "Gwa Gwa" - - -@blueprint.route("/group/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 ":3" - - -@blueprint.route("/group/modify", methods=["POST"]) -@login_required -def modify_group(): - """ - Changes the images in a group - """ - group_id = request.form["group"] - image_id = request.form["image"] - 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: - abort(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 ":3" - - -@blueprint.route("/group/delete", methods=["POST"]) -def delete_group(): - """ - Deletes a group - """ - group_id = request.form["group"] - group = Group.query.filter_by(id=group_id).first() - - if group is None: - abort(404) - elif group.author_id != current_user.id: - abort(403) - - GroupJunction.query.filter_by(group_id=group_id).delete() - db.session.delete(group) - db.session.commit() - - flash(["Group yeeted!", "1"]) - return ":3" diff --git a/onlylegs/api/__init__.py b/onlylegs/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/onlylegs/api/account.py b/onlylegs/api/account.py new file mode 100644 index 0000000..fb54c25 --- /dev/null +++ b/onlylegs/api/account.py @@ -0,0 +1,93 @@ +""" +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/", 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/", 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 new file mode 100644 index 0000000..9be39a5 --- /dev/null +++ b/onlylegs/api/group.py @@ -0,0 +1,78 @@ +""" +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 new file mode 100644 index 0000000..2ce774b --- /dev/null +++ b/onlylegs/api/media.py @@ -0,0 +1,144 @@ +""" +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("/", 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/", 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/config.py b/onlylegs/config.py index 57f42da..58e7a80 100644 --- a/onlylegs/config.py +++ b/onlylegs/config.py @@ -12,11 +12,11 @@ user_dir = platformdirs.user_config_dir("onlylegs") instance_dir = os.path.join(user_dir, "instance") # Load environment variables -print("Loading environment variables...") +# print("Loading environment variables...") load_dotenv(os.path.join(user_dir, ".env")) # Load config from user dir -print("Loading config...") +# print("Loading config...") with open(os.path.join(user_dir, "conf.yml"), encoding="utf-8", mode="r") as file: conf = safe_load(file) @@ -24,13 +24,20 @@ with open(os.path.join(user_dir, "conf.yml"), encoding="utf-8", mode="r") as fil # Flask config SECRET_KEY = os.environ.get("FLASK_SECRET") SQLALCHEMY_DATABASE_URI = "sqlite:///gallery.sqlite3" - -# Upload config MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"] -UPLOAD_FOLDER = os.path.join(user_dir, "uploads") ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"] # Pass YAML config to app ADMIN_CONF = conf["admin"] UPLOAD_CONF = conf["upload"] WEBSITE_CONF = conf["website"] + +# Directories +UPLOAD_FOLDER = os.path.join(user_dir, "media", "uploads") +CACHE_FOLDER = os.path.join(user_dir, "media", "cache") +PFP_FOLDER = os.path.join(user_dir, "media", "pfp") +MEDIA_FOLDER = os.path.join(user_dir, "media") + +# Database +INSTANCE_DIR = instance_dir +MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations") diff --git a/onlylegs/gwagwa.py b/onlylegs/gwagwa.py new file mode 100644 index 0000000..6fb58f6 --- /dev/null +++ b/onlylegs/gwagwa.py @@ -0,0 +1,4 @@ +""" +Gwa Gwa! +""" +print("Gwa Gwa!") diff --git a/onlylegs/models.py b/onlylegs/models.py index ace0b67..af3eb6f 100644 --- a/onlylegs/models.py +++ b/onlylegs/models.py @@ -87,9 +87,11 @@ class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C010 id = db.Column(db.Integer, primary_key=True) alt_id = db.Column(db.String, unique=True, nullable=False, default=str(uuid4())) - profile_picture = db.Column(db.String, nullable=True, default=None) - username = db.Column(db.String, unique=True, nullable=False) + picture = db.Column(db.String, default=None) + colour = db.Column(db.PickleType, default=None) + banner = db.Column(db.String, default=None) + username = db.Column(db.String, unique=True, nullable=False) email = db.Column(db.String, unique=True, nullable=False) password = db.Column(db.String, nullable=False) joined_at = db.Column( diff --git a/onlylegs/static/banner.png b/onlylegs/static/banner.png new file mode 100644 index 0000000..179ed20 Binary files /dev/null and b/onlylegs/static/banner.png differ diff --git a/onlylegs/static/js/index.js b/onlylegs/static/js/index.js index a46cbe4..d9d0aa5 100644 --- a/onlylegs/static/js/index.js +++ b/onlylegs/static/js/index.js @@ -61,7 +61,7 @@ window.onload = function () { } infoButton.onclick = function () { popUpShow('OnlyLegs', - 'v0.1.0 ' + + 'v0.1.2 ' + 'using Phosphoricons and Flask.' + '
Made by Fluffy and others with ❤️'); } diff --git a/onlylegs/static/js/notifications.js b/onlylegs/static/js/notifications.js index 94309fa..a93e170 100644 --- a/onlylegs/static/js/notifications.js +++ b/onlylegs/static/js/notifications.js @@ -1,12 +1,6 @@ function addNotification(notificationText, notificationLevel) { const notificationContainer = document.querySelector('.notifications'); - // Set the different icons for the different notification levels - const successIcon = ''; - const criticalIcon = ''; - const warningIcon = ''; - const infoIcon = ''; - // Create notification element const notification = document.createElement('div'); notification.classList.add('sniffle__notification'); @@ -28,16 +22,16 @@ function addNotification(notificationText, notificationLevel) { // Set the icon based on the notification level, not pretty but it works :3 if (notificationLevel === 1) { notification.classList.add('success'); - iconElement.innerHTML = successIcon; + iconElement.innerHTML = ''; } else if (notificationLevel === 2) { notification.classList.add('critical'); - iconElement.innerHTML = criticalIcon; + iconElement.innerHTML = ''; } else if (notificationLevel === 3) { notification.classList.add('warning'); - iconElement.innerHTML = warningIcon; + iconElement.innerHTML = ''; } else { notification.classList.add('info'); - iconElement.innerHTML = infoIcon; + iconElement.innerHTML = ''; } // Create text element and append to notification diff --git a/onlylegs/static/js/uploadTab.js b/onlylegs/static/js/uploadTab.js index 1769fec..a27e57f 100644 --- a/onlylegs/static/js/uploadTab.js +++ b/onlylegs/static/js/uploadTab.js @@ -276,12 +276,14 @@ document.addEventListener('DOMContentLoaded', () => { // }); - fetch('/api/upload', { + fetch('/api/media/upload', { method: 'POST', body: formData }) // .then(response => response.json()) - .then(data => { addNotification("Image uploaded successfully", 1); }) + .then(data => { + addNotification("Image uploaded successfully", 1); + }) .catch(error => { switch (response.status) { case 500: @@ -303,7 +305,6 @@ document.addEventListener('DOMContentLoaded', () => { } }); - clearUpload(); // Reset drop diff --git a/onlylegs/static/sass/components/banner.sass b/onlylegs/static/sass/components/banner.sass index f835868..483c071 100644 --- a/onlylegs/static/sass/components/banner.sass +++ b/onlylegs/static/sass/components/banner.sass @@ -36,6 +36,8 @@ .banner height: 30rem + max-height: 69vh + background-color: RGB($bg-300) img @@ -72,9 +74,8 @@ bottom: 0 display: grid - grid-template-columns: 1fr auto - grid-template-rows: 1fr auto auto - grid-template-areas: 'info info' 'header header' 'subtitle options' + grid-template-columns: auto 1fr auto + grid-template-areas: 'info info info' 'image header header' 'subtitle subtitle options' z-index: +2 @@ -109,6 +110,19 @@ margin-top: auto grid-area: options + .banner-picture + grid-area: image + + margin: auto 1rem auto 0 + + position: relative + + width: 6.9rem + height: 6.9rem + + background-color: RGB($primary) + border-radius: $rad + .banner-small height: 3.5rem background-color: RGB($bg-100) @@ -165,8 +179,12 @@ display: none .banner - min-height: 17rem + min-height: 15rem height: auto + max-height: 30vh + + .banner-filter + background: linear-gradient(to bottom, RGB($bg-100), transparent) .banner-content padding: 0.5rem @@ -178,7 +196,7 @@ align-items: center .banner-header - margin: 1rem 0 + margin: 0.7rem 0 text-align: center font-size: 2.5rem @@ -192,6 +210,12 @@ .pill-row margin-top: 0rem + .banner-picture + margin: 0 auto + width: 4rem + height: 4rem + display: flex + .banner-small .banner-content .banner-info diff --git a/onlylegs/static/sass/components/buttons/block.sass b/onlylegs/static/sass/components/buttons/block.sass index 0fee205..3891edd 100644 --- a/onlylegs/static/sass/components/buttons/block.sass +++ b/onlylegs/static/sass/components/buttons/block.sass @@ -149,6 +149,9 @@ opacity: 0 cursor: pointer + i + font-size: 1.2rem + .status width: 100% white-space: nowrap diff --git a/onlylegs/static/sass/components/buttons/info-button.sass b/onlylegs/static/sass/components/buttons/info-button.sass index 89e44ce..4805c59 100644 --- a/onlylegs/static/sass/components/buttons/info-button.sass +++ b/onlylegs/static/sass/components/buttons/info-button.sass @@ -23,15 +23,13 @@ cursor: pointer transition: all 0.2s cubic-bezier(.86, 0, .07, 1) + i + margin: 0.5rem + font-size: 1.25rem + &:hover color: RGB($info) - svg - margin: 0.5rem - - width: 1.25rem - height: 1.25rem - &.show right: 0.75rem opacity: 1 diff --git a/onlylegs/static/sass/components/buttons/pill.sass b/onlylegs/static/sass/components/buttons/pill.sass index 574476d..e9723f3 100644 --- a/onlylegs/static/sass/components/buttons/pill.sass +++ b/onlylegs/static/sass/components/buttons/pill.sass @@ -54,13 +54,14 @@ position: relative + text-decoration: none + border: none background-color: transparent color: RGB($fg-white) - svg - width: 1.25rem - height: 1.25rem + i + font-size: 1.25rem &:hover cursor: pointer @@ -74,7 +75,7 @@ background: RGB($critical) color: RGB($fg-white) - svg + i color: RGB($critical) &:hover diff --git a/onlylegs/static/sass/components/buttons/top-of-page.sass b/onlylegs/static/sass/components/buttons/top-of-page.sass index 05110d4..918356d 100644 --- a/onlylegs/static/sass/components/buttons/top-of-page.sass +++ b/onlylegs/static/sass/components/buttons/top-of-page.sass @@ -23,15 +23,13 @@ cursor: pointer transition: all 0.2s cubic-bezier(.86, 0, .07, 1) + i + margin: 0.5rem + font-size: 1.25rem + &:hover color: RGB($primary) - svg - margin: 0.5rem - - width: 1.25rem - height: 1.25rem - &.show right: 0.75rem opacity: 1 diff --git a/onlylegs/static/sass/components/gallery.sass b/onlylegs/static/sass/components/gallery.sass index e714e23..9b980ce 100644 --- a/onlylegs/static/sass/components/gallery.sass +++ b/onlylegs/static/sass/components/gallery.sass @@ -1,3 +1,16 @@ +.gallery-header + margin: 0.5rem + padding: 0 + + width: 100% + + display: flex + flex-direction: row + justify-content: flex-start + + font-size: 2rem + font-weight: 700 + .gallery-grid margin: 0 padding: 0.35rem diff --git a/onlylegs/static/sass/components/image-view/image.sass b/onlylegs/static/sass/components/image-view/image.sass index 6436e14..99fd1ac 100644 --- a/onlylegs/static/sass/components/image-view/image.sass +++ b/onlylegs/static/sass/components/image-view/image.sass @@ -1,6 +1,5 @@ .image-container margin: auto - padding: 0.5rem width: 100% height: 100% @@ -18,4 +17,12 @@ max-height: 100% object-fit: contain - object-position: center \ No newline at end of file + 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 index ab599e2..1b42c1e 100644 --- a/onlylegs/static/sass/components/image-view/info-tab.sass +++ b/onlylegs/static/sass/components/image-view/info-tab.sass @@ -1,4 +1,6 @@ .info-container + padding: 0.5rem 0 0.5rem 0.5rem + width: 27rem height: 100vh @@ -8,13 +10,17 @@ display: flex flex-direction: column - gap: 0 + gap: 0.5rem - background-color: RGB($bg-200) + 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 @@ -27,7 +33,7 @@ position: relative - background-color: RGB($bg-200) + background-color: RGB($bg-300) border-radius: $rad transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86) @@ -38,17 +44,16 @@ .collapse-indicator transform: rotate(90deg) + .info-header + border-radius: $rad + .info-table - height: 0 - padding: 0 + display: none .collapse-indicator margin: 0 padding: 0 - - width: 1.25rem - height: 1.25rem - + position: absolute top: 0.6rem right: 0.6rem @@ -62,8 +67,11 @@ transition: transform 0.15s cubic-bezier(.79, .14, .15, .86) cursor: pointer + > i + font-size: 1.1rem + color: RGB($primary) + .info-header - margin: 0 padding: 0.5rem width: 100% @@ -74,24 +82,15 @@ align-items: center gap: 0.5rem - position: sticky - top: 0 - z-index: +1 - background-color: RGB($bg-200) + border-radius: $rad $rad 0 0 - svg - margin: 0 - padding: 0 - - width: 1.25rem - height: 1.25rem - - fill: RGB($primary) + > i + font-size: 1.25rem + color: RGB($primary) h2 margin: 0 - padding: 0 font-size: 1.1rem font-weight: 500 @@ -202,14 +201,15 @@ @media (max-width: 1100px) .info-container + padding: 0.5rem + width: 100% height: 100% - position: relative - - display: flex - flex-direction: column - gap: 0.5rem + 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 index 297560a..8103d88 100644 --- a/onlylegs/static/sass/components/image-view/view.sass +++ b/onlylegs/static/sass/components/image-view/view.sass @@ -13,12 +13,11 @@ display: flex flex-direction: column - gap: 0.5rem z-index: 3 .image-block margin: 0 0 0 27rem - padding: 0 + padding: 0.5rem width: calc(100% - 27rem) height: 100vh @@ -33,49 +32,28 @@ 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-bottom: 0.5rem + margin-top: 0.5rem &.collapsed .image-block margin: 0 width: 100% - @media (max-width: 1100px) .image-grid - padding: 0.5rem height: auto .image-block margin: 0 + padding: 0.5rem 0.5rem 0 0.5rem + width: 100% height: auto - gap: 0.5rem - transition: margin 0s, width 0s - .image-container - margin: 0 auto - padding: 0 - max-height: 69vh - - img - max-height: 69vh - .pill-row - margin-bottom: 0 - #fullscreenImage display: none - .info-container - background: transparent - - .info-header - border-radius: $rad $rad 0 0 - - .info-tab.collapsed .info-header - border-radius: $rad - diff --git a/onlylegs/static/sass/components/navigation.sass b/onlylegs/static/sass/components/navigation.sass index 5235e34..c58805e 100644 --- a/onlylegs/static/sass/components/navigation.sass +++ b/onlylegs/static/sass/components/navigation.sass @@ -54,20 +54,25 @@ text-decoration: none - > svg - margin: 0 + > i padding: 0.5rem - - width: 2.5rem - height: 2.5rem - + font-size: 1.3rem border-radius: $rad-inner color: RGB($fg-white) - - transition: color 0.2s ease-out, transform 0.2s ease-out + + > .nav-pfp + padding: 0.4rem + width: 2.3rem + height: 2.3rem + border-radius: $rad-inner + + img + width: 100% + height: 100% + object-fit: cover + border-radius: $rad-inner .tool-tip - margin: 0 padding: 0.4rem 0.7rem display: block @@ -89,24 +94,20 @@ pointer-events: none - > svg - margin: 0 - font-size: 1rem - - width: 0.75rem - height: 0.75rem - + > i display: block position: absolute top: 50% left: -0.45rem transform: translateY(-50%) + + font-size: 0.75rem color: RGB($bg-100) &:hover - > svg + > i, .nav-pfp background: RGBA($fg-white, 0.1) span @@ -114,7 +115,7 @@ left: 3.9rem &.selected - > svg + > i color: RGB($primary) &::before diff --git a/onlylegs/static/sass/components/notification.sass b/onlylegs/static/sass/components/notification.sass index 294c695..3a468a2 100644 --- a/onlylegs/static/sass/components/notification.sass +++ b/onlylegs/static/sass/components/notification.sass @@ -108,9 +108,8 @@ background-color: RGB($bg-200) - svg - width: 1.25rem - height: 1.25rem + i + font-size: 1.25rem .sniffle__notification-text margin: 0 diff --git a/onlylegs/static/sass/components/pop-up.sass b/onlylegs/static/sass/components/pop-up.sass index 6a7de6e..0e6974b 100644 --- a/onlylegs/static/sass/components/pop-up.sass +++ b/onlylegs/static/sass/components/pop-up.sass @@ -138,7 +138,7 @@ @media (max-width: $breakpoint) .pop-up .pop-up-wrapper - width: calc(100% - 0.75rem) + max-width: calc(100% - 0.75rem) max-height: 95vh .pop-up-content diff --git a/onlylegs/static/sass/components/settings.sass b/onlylegs/static/sass/components/settings.sass new file mode 100644 index 0000000..cce4ac3 --- /dev/null +++ b/onlylegs/static/sass/components/settings.sass @@ -0,0 +1,29 @@ +.settings-content + margin: 0.5rem + padding: 1rem + + position: relative + + display: flex + flex-direction: column + justify-content: center + gap: 1rem + + background-color: RGB($bg-400) + color: RGB($fg-white) + border: 2px solid RGB($bg-200) + border-radius: $rad-inner + + h2 + margin: 0 + padding: 0 + + font-size: 1.5rem + font-weight: 700 + + h3 + margin: 0 + padding: 0 + + font-size: 1.25rem + font-weight: 700 \ No newline at end of file diff --git a/onlylegs/static/sass/components/upload-panel.sass b/onlylegs/static/sass/components/upload-panel.sass index 69e34db..ed090f0 100644 --- a/onlylegs/static/sass/components/upload-panel.sass +++ b/onlylegs/static/sass/components/upload-panel.sass @@ -86,7 +86,7 @@ &::after content: '' - width: 8rem + width: 6rem height: 3px position: absolute @@ -97,6 +97,12 @@ background-color: RGB($bg-400) border-radius: $rad-inner + transition: width 0.25s $animation-bounce + + &.dragging #dragIndicator::after + width: 9rem + background-color: RGB($primary) + .upload-jobs display: flex flex-direction: column diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass index 763b3ea..097346c 100644 --- a/onlylegs/static/sass/style.sass +++ b/onlylegs/static/sass/style.sass @@ -19,6 +19,7 @@ @import "components/buttons/block" @import "components/image-view/view" +@import "components/settings" // Reset * diff --git a/onlylegs/templates/group.html b/onlylegs/templates/group.html index c2cc311..bebe8ec 100644 --- a/onlylegs/templates/group.html +++ b/onlylegs/templates/group.html @@ -51,7 +51,7 @@ const formData = new FormData(); formData.append("group", formID); - fetch('{{ url_for('api.delete_group') }}', { + fetch('{{ url_for('group_api.delete_group') }}', { method: 'POST', body: formData }).then(response => { @@ -147,7 +147,7 @@ formData.append("image", formImage); formData.append("action", formAction); - fetch('{{ url_for('api.modify_group') }}', { + fetch('{{ url_for('group_api.modify_group') }}', { method: 'POST', body: formData }).then(response => { @@ -209,8 +209,7 @@ .navigation { background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important; } - .navigation-item > svg { - fill: {{ text_colour }} !important; + .navigation-item > i { color: {{ text_colour }} !important; } .navigation-item.selected::before { @@ -222,7 +221,7 @@ {% block content %} {% if images %}