diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b973d7c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: Fluffy-Bean diff --git a/.gitignore b/.gitignore index 209a447..c411669 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ -gallery/static/theme gallery/static/.webassets-cache gallery/static/gen +instance/ +migrations/ + .idea .vscode @@ -11,7 +13,6 @@ venv/ *.pyc __pycache__/ -instance/ .pytest_cache/ .coverage diff --git a/gallery/__init__.py b/gallery/__init__.py index 1793a69..42ff00b 100644 --- a/gallery/__init__.py +++ b/gallery/__init__.py @@ -2,79 +2,87 @@ Onlylegs Gallery This is the main app file, it loads all the other files and sets up the app """ - -# Import system modules import os import 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 -# Flask -from flask_compress import Compress -from flask_caching import Cache -from flask_assets import Environment, Bundle -from flask_login import LoginManager from flask import Flask, render_template, abort from werkzeug.exceptions import HTTPException +from werkzeug.security import generate_password_hash -# Configuration -import platformdirs -from dotenv import load_dotenv -from yaml import safe_load - -# Import database -from sqlalchemy.orm import sessionmaker -from gallery import db +from gallery.extensions import db, migrate, login_manager, assets, compress, cache +from gallery.views import index, image, group, settings, profile +from gallery.models import User +from gallery import api +from gallery import auth -USER_DIR = platformdirs.user_config_dir("onlylegs") +INSTACE_DIR = os.path.join(platformdirs.user_config_dir("onlylegs"), "instance") +MIGRATIONS_DIR = os.path.join(INSTACE_DIR, "migrations") -db_session = sessionmaker(bind=db.engine) -db_session = db_session() -login_manager = LoginManager() -assets = Environment() -cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300}) -compress = Compress() - - -def create_app(test_config=None): # pylint: disable=R0914 +def create_app(): # pylint: disable=R0914 """ Create and configure the main app """ - app = Flask(__name__, instance_path=os.path.join(USER_DIR, "instance")) + app = Flask(__name__, instance_path=INSTACE_DIR) + app.config.from_pyfile("config.py") - # Get environment variables - load_dotenv(os.path.join(USER_DIR, ".env")) - print("Loaded environment variables") + # DATABASE + db.init_app(app) + migrate.init_app(app, db) - # Get config file - with open(os.path.join(USER_DIR, "conf.yml"), encoding="utf-8", mode="r") as file: - conf = safe_load(file) - print("Loaded config") + # If database file doesn't exist, create it + if not os.path.exists(os.path.join(INSTACE_DIR, "gallery.sqlite3")): + print("Creating database") + with app.app_context(): + db.create_all() - # App configuration - app.config.from_mapping( - SECRET_KEY=os.environ.get("FLASK_SECRET"), - DATABASE=os.path.join(app.instance_path, "gallery.sqlite3"), - UPLOAD_FOLDER=os.path.join(USER_DIR, "uploads"), - ALLOWED_EXTENSIONS=conf["upload"]["allowed-extensions"], - MAX_CONTENT_LENGTH=1024 * 1024 * conf["upload"]["max-size"], - ADMIN_CONF=conf["admin"], - UPLOAD_CONF=conf["upload"], - WEBSITE_CONF=conf["website"], - ) + 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() - if test_config is None: - app.config.from_pyfile("config.py", silent=True) - else: - app.config.from_mapping(test_config) + 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) + + # 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 login_manager.init_app(app) login_manager.login_view = "gallery.index" - login_manager.session_protection = "strong" @login_manager.user_loader def load_user(user_id): - return db_session.query(db.Users).filter_by(alt_id=user_id).first() + return User.query.filter_by(alt_id=user_id).first() @login_manager.unauthorized_handler def unauthorized(): @@ -82,23 +90,12 @@ def create_app(test_config=None): # pylint: disable=R0914 msg = "You are not authorized to view this page!!!!" return render_template("error.html", error=error, msg=msg), error - lib = Bundle( - "lib/*.js", filters="jsmin", output="gen/lib.js", depends="lib/*.js" - ) - scripts = Bundle( - "js/*.js", filters="jsmin", output="gen/index.js", depends="js/*.js" - ) - styles = Bundle( - "sass/*.sass", filters="libsass, cssmin", output="gen/styles.css", depends='sass/**/*.sass' - ) - - assets.register("lib", lib) - assets.register("js", scripts) - assets.register("styles", styles) - - # Error handlers, if the error is not a HTTP error, return 500 + # ERROR HANDLERS @app.errorhandler(Exception) def error_page(err): # noqa + """ + Error handlers, if the error is not a HTTP error, return 500 + """ if not isinstance(err, HTTPException): abort(500) return ( @@ -106,30 +103,34 @@ def create_app(test_config=None): # pylint: disable=R0914 err.code, ) - # Load login, registration and logout manager - from gallery import auth + # ASSETS + assets.init_app(app) + scripts = Bundle("js/*.js", filters="jsmin", output="gen/js.js", depends="js/*.js") + styles = Bundle( + "sass/*.sass", + filters="libsass, cssmin", + output="gen/styles.css", + depends="sass/**/*.sass", + ) + + assets.register("scripts", scripts) + assets.register("styles", styles) + + # BLUEPRINTS app.register_blueprint(auth.blueprint) - - # Load the API - from gallery import api - app.register_blueprint(api.blueprint) - - # Load the different views - from gallery.views import index, image, group, settings, profile - 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) - # Log to file that the app has started - logging.info("Gallery started successfully!") - - # Initialize extensions and return app - assets.init_app(app) + # 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/gallery/api.py b/gallery/api.py index 7515d41..379e5b1 100644 --- a/gallery/api.py +++ b/gallery/api.py @@ -9,21 +9,17 @@ 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 sqlalchemy.orm import sessionmaker - -from gallery import db +from gallery.extensions import db +from gallery.models import Post, Group, GroupJunction from gallery.utils import metadata as mt from gallery.utils.generate_image import generate_thumbnail blueprint = Blueprint("api", __name__, url_prefix="/api") -db_session = sessionmaker(bind=db.engine) -db_session = db_session() @blueprint.route("/file/", methods=["GET"]) @@ -87,7 +83,7 @@ def upload(): img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette # Save to database - query = db.Posts( + query = Post( author_id=current_user.id, filename=img_name + "." + img_ext, mimetype=img_ext, @@ -97,8 +93,8 @@ def upload(): alt=form["alt"], ) - db_session.add(query) - db_session.commit() + db.session.add(query) + db.session.commit() return "Gwa Gwa" # Return something so the browser doesn't show an error @@ -109,40 +105,33 @@ def delete_image(image_id): """ Deletes an image from the server and database """ - img = db_session.query(db.Posts).filter_by(id=image_id).first() + post = Post.query.filter_by(id=image_id).first() # Check if image exists and if user is allowed to delete it (author) - if img is None: + if post is None: abort(404) - if img.author_id != current_user.id: + if post.author_id != current_user.id: abort(403) # Delete file try: - os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], img.filename)) + 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", img.filename + "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 = img.filename.rsplit(".")[0] + cache_name = post.filename.rsplit(".")[0] for cache_file in pathlib.Path(cache_path).glob(cache_name + "*"): os.remove(cache_file) - # Delete from database - db_session.query(db.Posts).filter_by(id=image_id).delete() + GroupJunction.query.filter_by(post_id=image_id).delete() + db.session.delete(post) + db.session.commit() - # Remove all entries in junction table - groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all() - for group in groups: - db_session.delete(group) - - # Commit all changes - db_session.commit() - - logging.info("Removed image (%s) %s", image_id, img.filename) + logging.info("Removed image (%s) %s", image_id, post.filename) flash(["Image was all in Le Head!", "1"]) return "Gwa Gwa" @@ -153,14 +142,14 @@ def create_group(): """ Creates a group """ - new_group = db.Groups( + new_group = Group( name=request.form["name"], description=request.form["description"], author_id=current_user.id, ) - db_session.add(new_group) - db_session.commit() + db.session.add(new_group) + db.session.commit() return ":3" @@ -175,29 +164,23 @@ def modify_group(): image_id = request.form["image"] action = request.form["action"] - group = db_session.query(db.Groups).filter_by(id=group_id).first() + group = db.get_or_404(Group, group_id) + db.get_or_404(Post, image_id) # Check if image exists - if group is None: - abort(404) - elif group.author_id != current_user.id: + if group.author_id != current_user.id: abort(403) - if action == "add": - if not ( - db_session.query(db.GroupJunction) - .filter_by(group_id=group_id, post_id=image_id) - .first() - ): - db_session.add(db.GroupJunction(group_id=group_id, post_id=image_id)) + 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": - ( - db_session.query(db.GroupJunction) - .filter_by(group_id=group_id, post_id=image_id) - .delete() - ) - - db_session.commit() + GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).delete() + db.session.commit() return ":3" @@ -207,17 +190,16 @@ def delete_group(): Deletes a group """ group_id = request.form["group"] - - group = db_session.query(db.Groups).filter_by(id=group_id).first() + group = Group.query.filter_by(id=group_id).first() if group is None: abort(404) elif group.author_id != current_user.id: abort(403) - db_session.query(db.Groups).filter_by(id=group_id).delete() - db_session.query(db.GroupJunction).filter_by(group_id=group_id).delete() - db_session.commit() + 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/gallery/auth.py b/gallery/auth.py index 163c3c4..0973384 100644 --- a/gallery/auth.py +++ b/gallery/auth.py @@ -10,13 +10,11 @@ from werkzeug.security import check_password_hash, generate_password_hash from flask_login import login_user, logout_user, login_required -from sqlalchemy.orm import sessionmaker -from gallery import db +from gallery.extensions import db +from gallery.models import User blueprint = Blueprint("auth", __name__, url_prefix="/auth") -db_session = sessionmaker(bind=db.engine) -db_session = db_session() @blueprint.route("/login", methods=["POST"]) @@ -30,7 +28,7 @@ def login(): password = request.form["password"].strip() remember = bool(request.form["remember-me"]) - user = db_session.query(db.Users).filter_by(username=username).first() + user = User.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) @@ -79,7 +77,7 @@ def register(): elif password_repeat != password: error.append("Passwords do not match!") - user_exists = db_session.query(db.Users).filter_by(username=username).first() + user_exists = User.query.filter_by(username=username).first() if user_exists: error.append("User already exists!") @@ -88,13 +86,13 @@ def register(): print(error) return jsonify(error), 400 - register_user = db.Users( + register_user = User( username=username, email=email, password=generate_password_hash(password, method="sha256"), ) - db_session.add(register_user) - db_session.commit() + db.session.add(register_user) + db.session.commit() logging.info("User %s registered", username) return "ok", 200 diff --git a/gallery/config.py b/gallery/config.py new file mode 100644 index 0000000..57f42da --- /dev/null +++ b/gallery/config.py @@ -0,0 +1,36 @@ +""" +Gallery configuration file +""" +import os +import platformdirs +from dotenv import load_dotenv +from yaml import safe_load + + +# Set dirs +user_dir = platformdirs.user_config_dir("onlylegs") +instance_dir = os.path.join(user_dir, "instance") + +# Load environment variables +print("Loading environment variables...") +load_dotenv(os.path.join(user_dir, ".env")) + +# Load config from user dir +print("Loading config...") +with open(os.path.join(user_dir, "conf.yml"), encoding="utf-8", mode="r") as file: + conf = safe_load(file) + + +# 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"] diff --git a/gallery/db.py b/gallery/db.py deleted file mode 100644 index a6b2638..0000000 --- a/gallery/db.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -OnlyLegs - Database models and ions for SQLAlchemy -""" -from uuid import uuid4 -import os -import platformdirs - -from sqlalchemy import ( - create_engine, - Column, - Integer, - String, - DateTime, - ForeignKey, - PickleType, - func, -) -from sqlalchemy.orm import declarative_base, relationship -from flask_login import UserMixin - - -USER_DIR = platformdirs.user_config_dir("onlylegs") -DB_PATH = os.path.join(USER_DIR, "instance", "gallery.sqlite3") - - -# In the future, I want to add support for other databases -engine = create_engine(f"sqlite:///{DB_PATH}", echo=False) -base = declarative_base() - - -class Users(base, UserMixin): # pylint: disable=too-few-public-methods, C0103 - """ - User table - Joins with post, groups, session and log - """ - - __tablename__ = "users" - - # Gallery used information - id = Column(Integer, primary_key=True) - alt_id = Column(String, unique=True, nullable=False, default=str(uuid4())) - profile_picture = Column(String, nullable=True, default=None) - username = Column(String, unique=True, nullable=False) - email = Column(String, unique=True, nullable=False) - password = Column(String, nullable=False) - joined_at = Column( - DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102 - ) - - posts = relationship("Posts", backref="users") - groups = relationship("Groups", backref="users") - log = relationship("Logs", backref="users") - - def get_id(self): - return str(self.alt_id) - - -class Posts(base): # pylint: disable=too-few-public-methods, C0103 - """ - Post table - Joins with group_junction - """ - - __tablename__ = "posts" - - id = Column(Integer, primary_key=True) - author_id = Column(Integer, ForeignKey("users.id")) - created_at = Column( - DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102 - ) - filename = Column(String, unique=True, nullable=False) - mimetype = Column(String, nullable=False) - exif = Column(PickleType, nullable=False) - colours = Column(PickleType, nullable=False) - description = Column(String, nullable=False) - alt = Column(String, nullable=False) - - junction = relationship("GroupJunction", backref="posts") - - -class Groups(base): # pylint: disable=too-few-public-methods, C0103 - """ - Group table - Joins with group_junction - """ - - __tablename__ = "groups" - - id = Column(Integer, primary_key=True) - name = Column(String, nullable=False) - description = Column(String, nullable=False) - author_id = Column(Integer, ForeignKey("users.id")) - created_at = Column( - DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102 - ) - - junction = relationship("GroupJunction", backref="groups") - - -class GroupJunction(base): # pylint: disable=too-few-public-methods, C0103 - """ - Junction table for posts and groups - Joins with posts and groups - """ - - __tablename__ = "group_junction" - - id = Column(Integer, primary_key=True) - date_added = Column( - DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102 - ) - group_id = Column(Integer, ForeignKey("groups.id")) - post_id = Column(Integer, ForeignKey("posts.id")) - - -class Logs(base): # pylint: disable=too-few-public-methods, C0103 - """ - Log table - Joins with user - """ - - __tablename__ = "logs" - - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - ip_address = Column(String, nullable=False) - code = Column(Integer, nullable=False) - note = Column(String, nullable=False) - created_at = Column( - DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102 - ) - - -class Bans(base): # pylint: disable=too-few-public-methods, C0103 - """ - Bans table - """ - - __tablename__ = "bans" - - id = Column(Integer, primary_key=True) - ip_address = Column(String, nullable=False) - code = Column(Integer, nullable=False) - note = Column(String, nullable=False) - banned_at = Column( - DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102 - ) - - -# check if database file exists, if not create it -if not os.path.isfile(DB_PATH): - base.metadata.create_all(engine) - print("Database created") diff --git a/gallery/extensions.py b/gallery/extensions.py new file mode 100644 index 0000000..0b27ca4 --- /dev/null +++ b/gallery/extensions.py @@ -0,0 +1,16 @@ +""" +Extensions used by the application +""" +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager +from flask_assets import Environment +from flask_compress import Compress +from flask_caching import Cache + +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() +assets = Environment() +compress = Compress() +cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300}) diff --git a/gallery/models.py b/gallery/models.py new file mode 100644 index 0000000..3958f9c --- /dev/null +++ b/gallery/models.py @@ -0,0 +1,105 @@ +""" +OnlyLegs - Database models and ions for SQLAlchemy +""" +from uuid import uuid4 +from flask_login import UserMixin +from .extensions import db + + +class GroupJunction(db.Model): # pylint: disable=too-few-public-methods, C0103 + """ + Junction table for posts and groups + Joins with posts and groups + """ + + __tablename__ = "group_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")) + + date_added = db.Column( + db.DateTime, + nullable=False, + server_default=db.func.now(), # pylint: disable=E1102 + ) + + +class Post(db.Model): # pylint: disable=too-few-public-methods, C0103 + """ + Post table + """ + + __tablename__ = "post" + + id = db.Column(db.Integer, primary_key=True) + + author_id = db.Column(db.Integer, db.ForeignKey("user.id")) + + filename = db.Column(db.String, unique=True, nullable=False) + mimetype = db.Column(db.String, nullable=False) + exif = db.Column(db.PickleType, nullable=False) + colours = db.Column(db.PickleType, nullable=False) + + description = db.Column(db.String, nullable=False) + alt = db.Column(db.String, nullable=False) + + created_at = db.Column( + db.DateTime, + nullable=False, + server_default=db.func.now(), # pylint: disable=E1102 + ) + + junction = db.relationship("GroupJunction", backref="posts") + + +class Group(db.Model): # pylint: disable=too-few-public-methods, C0103 + """ + Group table + """ + + __tablename__ = "group" + + 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")) + created_at = db.Column( + db.DateTime, + nullable=False, + server_default=db.func.now(), # pylint: disable=E1102 + ) + + junction = db.relationship("GroupJunction", backref="groups") + + +class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103 + """ + User table + """ + + __tablename__ = "user" + + # Gallery used information + 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) + + email = db.Column(db.String, unique=True, nullable=False) + password = db.Column(db.String, nullable=False) + joined_at = db.Column( + db.DateTime, + nullable=False, + server_default=db.func.now(), # pylint: disable=E1102 + ) + + posts = db.relationship("Post", backref="author") + groups = db.relationship("Group", backref="author") + + def get_id(self): + return str(self.alt_id) diff --git a/gallery/static/fonts/font.css b/gallery/static/fonts/font.css new file mode 100644 index 0000000..bb8f61b --- /dev/null +++ b/gallery/static/fonts/font.css @@ -0,0 +1,7 @@ +@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/gallery/static/js/index.js b/gallery/static/js/index.js index 5d55656..fdff447 100644 --- a/gallery/static/js/index.js +++ b/gallery/static/js/index.js @@ -65,7 +65,7 @@ window.onload = function () { } infoButton.onclick = function () { popUpShow('OnlyLegs', - 'V23.04.08 ' + + 'V23.04.10 ' + 'using Phosphoricons and Flask.' + '
Made by Fluffy and others with โค๏ธ'); } diff --git a/gallery/static/lib/notifications.js b/gallery/static/js/notifications.js similarity index 50% rename from gallery/static/lib/notifications.js rename to gallery/static/js/notifications.js index 1cda690..3674625 100644 --- a/gallery/static/lib/notifications.js +++ b/gallery/static/js/notifications.js @@ -1,14 +1,14 @@ function addNotification(notificationText, notificationLevel) { - let notificationContainer = document.querySelector('.notifications'); + const notificationContainer = document.querySelector('.notifications'); // Set the different icons for the different notification levels - let successIcon = ''; - let criticalIcon = ''; - let warningIcon = ''; - let infoIcon = ''; + const successIcon = ''; + const criticalIcon = ''; + const warningIcon = ''; + const infoIcon = ''; // Create notification element - let notification = document.createElement('div'); + const notification = document.createElement('div'); notification.classList.add('sniffle__notification'); notification.onclick = function() { if (notification) { @@ -21,7 +21,7 @@ function addNotification(notificationText, notificationLevel) { }; // Create icon element and append to notification - let iconElement = document.createElement('span'); + const iconElement = document.createElement('span'); iconElement.classList.add('sniffle__notification-icon'); notification.appendChild(iconElement); @@ -41,7 +41,7 @@ function addNotification(notificationText, notificationLevel) { } // Create text element and append to notification - let description = document.createElement('span'); + const description = document.createElement('span'); description.classList.add('sniffle__notification-text'); description.innerHTML = notificationText; notification.appendChild(description); diff --git a/gallery/static/lib/popup.js b/gallery/static/js/popup.js similarity index 73% rename from gallery/static/lib/popup.js rename to gallery/static/js/popup.js index 301c5b2..bda748c 100644 --- a/gallery/static/lib/popup.js +++ b/gallery/static/js/popup.js @@ -1,19 +1,19 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) { // Get popup elements - let popupSelector = document.querySelector('.pop-up'); - let headerSelector = document.querySelector('.pop-up-header'); - let actionsSelector = document.querySelector('.pop-up-controlls'); + const popupSelector = document.querySelector('.pop-up'); + const headerSelector = document.querySelector('.pop-up-header'); + const actionsSelector = document.querySelector('.pop-up-controlls'); // Clear popup elements headerSelector.innerHTML = ''; actionsSelector.innerHTML = ''; // Set popup header and subtitle - let titleElement = document.createElement('h2'); + const titleElement = document.createElement('h2'); titleElement.innerHTML = titleText; headerSelector.appendChild(titleElement); - let subtitleElement = document.createElement('p'); + const subtitleElement = document.createElement('p'); subtitleElement.innerHTML = subtitleText; headerSelector.appendChild(subtitleElement); @@ -25,8 +25,7 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) if (userActions) { // for each user action, add the element for (let i = 0; i < userActions.length; i++) { - let action = userActions[i]; - actionsSelector.appendChild(action); + actionsSelector.appendChild(userActions[i]); } } else { actionsSelector.innerHTML = ''; @@ -39,7 +38,7 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) } function popupDissmiss() { - let popupSelector = document.querySelector('.pop-up'); + const popupSelector = document.querySelector('.pop-up'); document.querySelector("html").style.overflow = "auto"; popupSelector.classList.remove('active'); diff --git a/gallery/static/lib/webp.js b/gallery/static/js/webp.js similarity index 88% rename from gallery/static/lib/webp.js rename to gallery/static/js/webp.js index c39c4ff..93a4ade 100644 --- a/gallery/static/lib/webp.js +++ b/gallery/static/js/webp.js @@ -1,5 +1,5 @@ function checkWebpSupport() { - var webpSupport = false; + let webpSupport = false; try { webpSupport = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0; } catch (e) { diff --git a/gallery/static/sass/components/banner.sass b/gallery/static/sass/components/banner.sass index e9d713f..c9b4aca 100644 --- a/gallery/static/sass/components/banner.sass +++ b/gallery/static/sass/components/banner.sass @@ -75,7 +75,6 @@ grid-template-columns: 1fr auto grid-template-rows: 1fr auto auto grid-template-areas: 'info info' 'header header' 'subtitle options' - gap: 0.5rem z-index: +2 @@ -89,6 +88,8 @@ .banner-header grid-area: header + margin: 0.5rem 0 + text-align: left font-size: 6.9rem font-weight: 700 @@ -113,7 +114,7 @@ background-color: RGB($bg-100) .banner-content - padding: 0.5rem + padding: 0 0.5rem width: 100% height: 100% @@ -124,7 +125,6 @@ display: flex flex-direction: row justify-content: flex-start - gap: 1rem z-index: +2 @@ -137,7 +137,7 @@ justify-self: flex-start .banner-header - padding-bottom: 0.25rem + margin-right: 0.6rem white-space: nowrap text-overflow: ellipsis @@ -149,6 +149,8 @@ color: RGB($primary) .banner-info + margin-right: 0.6rem + font-size: 0.9rem font-weight: 400 @@ -174,9 +176,9 @@ flex-direction: column justify-content: center align-items: center - gap: 1rem .banner-header + margin: 1rem 0 text-align: center font-size: 2.5rem diff --git a/gallery/static/sass/components/buttons/block.sass b/gallery/static/sass/components/buttons/block.sass index 2b767ee..6dbe3c8 100644 --- a/gallery/static/sass/components/buttons/block.sass +++ b/gallery/static/sass/components/buttons/block.sass @@ -1,12 +1,12 @@ @mixin btn-block($color) color: RGB($color) + box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2) - &:hover + &:hover, &:focus-visible background-color: RGBA($color, 0.1) color: RGB($color) + box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($color, 0.2) - &:focus-visible - outline: 2px solid RGBA($color, 0.3) .btn-block padding: 0.4rem 0.7rem @@ -25,19 +25,19 @@ font-weight: 400 text-align: center - background-color: transparent + background-color: RGBA($white, 0.1) color: RGB($white) border: none border-radius: $rad-inner + box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2) + outline: none cursor: pointer - transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out + transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, box-shadow 0.15s ease-in-out - &:hover - background-color: RGBA($white, 0.1) - - &:focus-visible - outline: 2px solid RGBA($white, 0.3) + &:hover, &:focus-visible + background-color: RGBA($white, 0.2) + box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3) &.primary @include btn-block($primary) @@ -91,10 +91,11 @@ background-color: RGBA($white, 0.1) color: RGB($white) - border: none border-bottom: 3px solid RGBA($white, 0.1) border-radius: $rad-inner + box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2) + outline: none cursor: pointer transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out @@ -107,7 +108,6 @@ &:focus border-color: RGB($primary) - outline: none &.black @include btn-block($black) @@ -132,13 +132,14 @@ background-color: RGBA($white, 0.1) color: RGB($white) - border: none border-radius: $rad-inner + box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2) + outline: none cursor: pointer overflow: hidden - transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, box-shadow 0.15s ease-in-out input position: absolute @@ -153,20 +154,20 @@ text-align: center overflow: hidden - &:hover + &:hover, &:focus-visible background-color: RGBA($white, 0.2) color: RGB($white) - - &:focus-visible - outline: 2px solid RGBA($white, 0.3) + box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3) &.active background-color: RGBA($primary, 0.2) color: RGB($primary) + box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($primary, 0.3) &.edging background-color: RGBA($white, 0.2) color: RGB($white) + box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3) input display: none // So it doesnt get in the way of the drop as that breaks things @@ -174,3 +175,4 @@ &.error background-color: RGBA($critical, 0.2) color: RGB($critical) + box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($critical, 0.3) diff --git a/gallery/static/sass/components/buttons/pill.sass b/gallery/static/sass/components/buttons/pill.sass index d0b7ee6..cc30fc5 100644 --- a/gallery/static/sass/components/buttons/pill.sass +++ b/gallery/static/sass/components/buttons/pill.sass @@ -18,10 +18,11 @@ background-color: RGB($bg-200) border-radius: $rad + box-shadow: 0 1px 0 RGB($bg-100), 0 -1px 0 RGB($bg-300) .pill-text margin: 0 - padding: 0.5rem + padding: 0.5rem 1rem width: auto height: 2.5rem @@ -36,7 +37,9 @@ font-size: 1rem font-weight: 400 + background-color: RGB($bg-200) color: RGB($fg-white) + border-radius: $rad .pill-item margin: 0 diff --git a/gallery/static/sass/components/gallery.sass b/gallery/static/sass/components/gallery.sass index 69ee691..e714e23 100644 --- a/gallery/static/sass/components/gallery.sass +++ b/gallery/static/sass/components/gallery.sass @@ -1,15 +1,14 @@ .gallery-grid margin: 0 - padding: 0.65rem + padding: 0.35rem width: 100% display: grid grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) - gap: 0.65rem .gallery-item - margin: 0 + margin: 0.35rem padding: 0 height: auto @@ -97,7 +96,7 @@ opacity: 1 .group-item - margin: 0 + margin: 0.35rem padding: 0 height: auto @@ -161,8 +160,6 @@ display: block - background-color: RGB($bg-bright) - img width: 100% height: 100% diff --git a/gallery/static/sass/components/image-view/background.sass b/gallery/static/sass/components/image-view/background.sass index f157561..e7478b5 100644 --- a/gallery/static/sass/components/image-view/background.sass +++ b/gallery/static/sass/components/image-view/background.sass @@ -25,7 +25,7 @@ background-color: RGB($fg-white) - filter: blur(1rem) + filter: blur(1rem) saturate(1.2) transform: scale(1.1) object-fit: cover diff --git a/gallery/static/sass/components/image-view/fullscreen.sass b/gallery/static/sass/components/image-view/fullscreen.sass deleted file mode 100644 index f8e0fbc..0000000 --- a/gallery/static/sass/components/image-view/fullscreen.sass +++ /dev/null @@ -1,42 +0,0 @@ -.image-fullscreen - margin: 0 - padding: 0 - - width: 100% - height: 100% - height: 100dvh - - position: fixed - top: 0 - left: 0 - - display: none - opacity: 0 // hide - - background-color: $bg-transparent - z-index: 100 - - box-sizing: border-box - - transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86) - - img - margin: auto - padding: 0 - - width: auto - height: auto - max-width: 100% - max-height: 100% - - object-fit: contain - object-position: center - - transform: scale(0.8) - transition: transform 0.2s cubic-bezier(.68,-0.55,.27,1.55) - - &.active - opacity: 1 // show - - img - transform: scale(1) \ No newline at end of file diff --git a/gallery/static/sass/components/image-view/info-tab.sass b/gallery/static/sass/components/image-view/info-tab.sass index 47c9d44..ab599e2 100644 --- a/gallery/static/sass/components/image-view/info-tab.sass +++ b/gallery/static/sass/components/image-view/info-tab.sass @@ -1,5 +1,5 @@ .info-container - width: 25rem + width: 27rem height: 100vh position: absolute @@ -17,7 +17,7 @@ transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1) &.collapsed - left: -25rem + left: -27rem .info-tab width: 100% @@ -39,8 +39,8 @@ transform: rotate(90deg) .info-table + height: 0 padding: 0 - opacity: 0 .collapse-indicator margin: 0 @@ -126,11 +126,6 @@ .link margin: 0 padding: 0 - - font-size: 1rem - font-weight: 500 - text-align: center - line-height: 1 color: RGB($primary) @@ -150,43 +145,31 @@ border-collapse: collapse tr - margin: 0 - padding: 0 - - width: 100% - 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% - max-width: 0 overflow: hidden text-overflow: ellipsis white-space: nowrap - - font-size: 1rem - font-weight: 400 td:last-child - padding: 0 - width: 50% - max-width: 0 - overflow: hidden - text-overflow: ellipsis - white-space: nowrap - - font-size: 1rem - font-weight: 400 - - td.empty-table - opacity: 0.3 + white-space: normal + word-break: break-word tr:last-of-type td padding-bottom: 0 diff --git a/gallery/static/sass/components/image-view/view.sass b/gallery/static/sass/components/image-view/view.sass index 22e47fe..297560a 100644 --- a/gallery/static/sass/components/image-view/view.sass +++ b/gallery/static/sass/components/image-view/view.sass @@ -1,5 +1,4 @@ @import 'background' -@import 'fullscreen' @import 'info-tab' @import 'image' @@ -18,10 +17,10 @@ z-index: 3 .image-block - margin: 0 0 0 25rem + margin: 0 0 0 27rem padding: 0 - width: calc(100% - 25rem) + width: calc(100% - 27rem) height: 100vh position: relative @@ -79,4 +78,4 @@ .info-tab.collapsed .info-header border-radius: $rad - \ No newline at end of file + diff --git a/gallery/static/sass/components/elements/notification.sass b/gallery/static/sass/components/notification.sass similarity index 99% rename from gallery/static/sass/components/elements/notification.sass rename to gallery/static/sass/components/notification.sass index 078ae97..294c695 100644 --- a/gallery/static/sass/components/elements/notification.sass +++ b/gallery/static/sass/components/notification.sass @@ -61,7 +61,7 @@ &::after content: "" - width: 450px + width: 100% height: 3px position: absolute diff --git a/gallery/static/sass/components/elements/pop-up.sass b/gallery/static/sass/components/pop-up.sass similarity index 96% rename from gallery/static/sass/components/elements/pop-up.sass rename to gallery/static/sass/components/pop-up.sass index 8af0dc8..2af9246 100644 --- a/gallery/static/sass/components/elements/pop-up.sass +++ b/gallery/static/sass/components/pop-up.sass @@ -94,10 +94,6 @@ vertical-align: middle a, .link - font-size: 1rem - font-weight: 500 - line-height: 1 - color: RGB($primary) cursor: pointer diff --git a/gallery/static/sass/components/elements/tags.sass b/gallery/static/sass/components/tags.sass similarity index 100% rename from gallery/static/sass/components/elements/tags.sass rename to gallery/static/sass/components/tags.sass diff --git a/gallery/static/sass/components/elements/upload-panel.sass b/gallery/static/sass/components/upload-panel.sass similarity index 95% rename from gallery/static/sass/components/elements/upload-panel.sass rename to gallery/static/sass/components/upload-panel.sass index 19325bb..69e34db 100644 --- a/gallery/static/sass/components/elements/upload-panel.sass +++ b/gallery/static/sass/components/upload-panel.sass @@ -26,7 +26,7 @@ padding: 0 font-size: 1rem - font-weight: 500 + font-weight: 400 form margin: 0 @@ -57,9 +57,9 @@ position: absolute bottom: 0 - left: -25rem + left: -27rem - width: 25rem + width: 27rem height: 100% display: flex @@ -67,11 +67,10 @@ gap: 1rem background-color: RGB($bg-200) - opacity: 0 z-index: +2 - transition: left 0.25s cubic-bezier(0.76, 0, 0.17, 1), bottom 0.25s cubic-bezier(0.76, 0, 0.17, 1), opacity 0.25s cubic-bezier(0.76, 0, 0.17, 1) + transition: left 0.25s cubic-bezier(0.76, 0, 0.17, 1), bottom 0.25s cubic-bezier(0.76, 0, 0.17, 1) #dragIndicator display: none @@ -197,7 +196,6 @@ .container left: 0 - opacity: 1 @media (max-width: $breakpoint) .upload-panel diff --git a/gallery/static/sass/style.sass b/gallery/static/sass/style.sass index 0d5fc4f..763b3ea 100644 --- a/gallery/static/sass/style.sass +++ b/gallery/static/sass/style.sass @@ -4,10 +4,10 @@ @import "variables" @import "animations" -@import "components/elements/notification" -@import "components/elements/pop-up" -@import "components/elements/upload-panel" -@import "components/elements/tags" +@import "components/notification" +@import "components/pop-up" +@import "components/upload-panel" +@import "components/tags" @import "components/navigation" @import "components/banner" diff --git a/gallery/static/sass/variables.sass b/gallery/static/sass/variables.sass index 321a03d..537fab9 100644 --- a/gallery/static/sass/variables.sass +++ b/gallery/static/sass/variables.sass @@ -74,13 +74,3 @@ $breakpoint: 800px --breakpoint: 800px -// I have no clue if its webassets or libsass thats doing this shit -// But one of them is trying to "correct" the path, and 404-ing the -// font, so enjoy this path fuckery - -@font-face - font-family: 'Rubik' - src: url('../../../../static/fonts/Rubik.ttf') format('truetype') - font-style: normal - font-display: swap - font-weight: 300 900 diff --git a/gallery/templates/group.html b/gallery/templates/group.html index caa5313..a05a4b8 100644 --- a/gallery/templates/group.html +++ b/gallery/templates/group.html @@ -15,7 +15,7 @@ } } - {% if current_user.id == group.author_id %} + {% if current_user.id == group.author.id %} function groupDelete() { cancelBtn = document.createElement('button'); cancelBtn.classList.add('btn-block'); @@ -223,7 +223,7 @@ {% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}