Merge pull request #20 from Fluffy-Bean/unstable

Unstable
This commit is contained in:
Michał 2023-04-12 16:35:30 +01:00 committed by GitHub
commit a33fa2f690
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 722 additions and 734 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: Fluffy-Bean

5
.gitignore vendored
View file

@ -1,7 +1,9 @@
gallery/static/theme
gallery/static/.webassets-cache gallery/static/.webassets-cache
gallery/static/gen gallery/static/gen
instance/
migrations/
.idea .idea
.vscode .vscode
@ -11,7 +13,6 @@ venv/
*.pyc *.pyc
__pycache__/ __pycache__/
instance/
.pytest_cache/ .pytest_cache/
.coverage .coverage

View file

@ -2,79 +2,87 @@
Onlylegs Gallery Onlylegs Gallery
This is the main app file, it loads all the other files and sets up the app This is the main app file, it loads all the other files and sets up the app
""" """
# Import system modules
import os import os
import logging 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 flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash
# Configuration from gallery.extensions import db, migrate, login_manager, assets, compress, cache
import platformdirs from gallery.views import index, image, group, settings, profile
from dotenv import load_dotenv from gallery.models import User
from yaml import safe_load from gallery import api
from gallery import auth
# Import database
from sqlalchemy.orm import sessionmaker
from gallery import db
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) def create_app(): # pylint: disable=R0914
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
""" """
Create and configure the main app 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 # DATABASE
load_dotenv(os.path.join(USER_DIR, ".env")) db.init_app(app)
print("Loaded environment variables") migrate.init_app(app, db)
# Get config file # If database file doesn't exist, create it
with open(os.path.join(USER_DIR, "conf.yml"), encoding="utf-8", mode="r") as file: if not os.path.exists(os.path.join(INSTACE_DIR, "gallery.sqlite3")):
conf = safe_load(file) print("Creating database")
print("Loaded config") with app.app_context():
db.create_all()
# App configuration register_user = User(
app.config.from_mapping( username=app.config["ADMIN_CONF"]["username"],
SECRET_KEY=os.environ.get("FLASK_SECRET"), email=app.config["ADMIN_CONF"]["email"],
DATABASE=os.path.join(app.instance_path, "gallery.sqlite3"), password=generate_password_hash("changeme!", method="sha256"),
UPLOAD_FOLDER=os.path.join(USER_DIR, "uploads"), )
ALLOWED_EXTENSIONS=conf["upload"]["allowed-extensions"], db.session.add(register_user)
MAX_CONTENT_LENGTH=1024 * 1024 * conf["upload"]["max-size"], db.session.commit()
ADMIN_CONF=conf["admin"],
UPLOAD_CONF=conf["upload"],
WEBSITE_CONF=conf["website"],
)
if test_config is None: print(
app.config.from_pyfile("config.py", silent=True) """
else: ####################################################
app.config.from_mapping(test_config) # 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.init_app(app)
login_manager.login_view = "gallery.index" login_manager.login_view = "gallery.index"
login_manager.session_protection = "strong"
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): 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 @login_manager.unauthorized_handler
def unauthorized(): def unauthorized():
@ -82,23 +90,12 @@ def create_app(test_config=None): # pylint: disable=R0914
msg = "You are not authorized to view this page!!!!" msg = "You are not authorized to view this page!!!!"
return render_template("error.html", error=error, msg=msg), error return render_template("error.html", error=error, msg=msg), error
lib = Bundle( # ERROR HANDLERS
"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
@app.errorhandler(Exception) @app.errorhandler(Exception)
def error_page(err): # noqa def error_page(err): # noqa
"""
Error handlers, if the error is not a HTTP error, return 500
"""
if not isinstance(err, HTTPException): if not isinstance(err, HTTPException):
abort(500) abort(500)
return ( return (
@ -106,30 +103,34 @@ def create_app(test_config=None): # pylint: disable=R0914
err.code, err.code,
) )
# Load login, registration and logout manager # ASSETS
from gallery import auth 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) app.register_blueprint(auth.blueprint)
# Load the API
from gallery import api
app.register_blueprint(api.blueprint) 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(index.blueprint)
app.register_blueprint(image.blueprint) app.register_blueprint(image.blueprint)
app.register_blueprint(group.blueprint) app.register_blueprint(group.blueprint)
app.register_blueprint(profile.blueprint) app.register_blueprint(profile.blueprint)
app.register_blueprint(settings.blueprint) app.register_blueprint(settings.blueprint)
# Log to file that the app has started # CACHE AND COMPRESS
logging.info("Gallery started successfully!")
# Initialize extensions and return app
assets.init_app(app)
cache.init_app(app) cache.init_app(app)
compress.init_app(app) compress.init_app(app)
# Yupee! We got there :3
print("Done!")
logging.info("Gallery started successfully!")
return app return app

View file

@ -9,21 +9,17 @@ import platformdirs
from flask import Blueprint, send_from_directory, abort, flash, request, current_app from flask import Blueprint, send_from_directory, abort, flash, request, current_app
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from flask_login import login_required, current_user from flask_login import login_required, current_user
from colorthief import ColorThief from colorthief import ColorThief
from sqlalchemy.orm import sessionmaker from gallery.extensions import db
from gallery.models import Post, Group, GroupJunction
from gallery import db
from gallery.utils import metadata as mt from gallery.utils import metadata as mt
from gallery.utils.generate_image import generate_thumbnail from gallery.utils.generate_image import generate_thumbnail
blueprint = Blueprint("api", __name__, url_prefix="/api") blueprint = Blueprint("api", __name__, url_prefix="/api")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/file/<file_name>", methods=["GET"]) @blueprint.route("/file/<file_name>", methods=["GET"])
@ -87,7 +83,7 @@ def upload():
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database # Save to database
query = db.Posts( query = Post(
author_id=current_user.id, author_id=current_user.id,
filename=img_name + "." + img_ext, filename=img_name + "." + img_ext,
mimetype=img_ext, mimetype=img_ext,
@ -97,8 +93,8 @@ def upload():
alt=form["alt"], alt=form["alt"],
) )
db_session.add(query) db.session.add(query)
db_session.commit() db.session.commit()
return "Gwa Gwa" # Return something so the browser doesn't show an error 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 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) # Check if image exists and if user is allowed to delete it (author)
if img is None: if post is None:
abort(404) abort(404)
if img.author_id != current_user.id: if post.author_id != current_user.id:
abort(403) abort(403)
# Delete file # Delete file
try: 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: except FileNotFoundError:
logging.warning( 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 # Delete cached files
cache_path = os.path.join(platformdirs.user_config_dir("onlylegs"), "cache") 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 + "*"): for cache_file in pathlib.Path(cache_path).glob(cache_name + "*"):
os.remove(cache_file) os.remove(cache_file)
# Delete from database GroupJunction.query.filter_by(post_id=image_id).delete()
db_session.query(db.Posts).filter_by(id=image_id).delete() db.session.delete(post)
db.session.commit()
# Remove all entries in junction table logging.info("Removed image (%s) %s", image_id, post.filename)
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)
flash(["Image was all in Le Head!", "1"]) flash(["Image was all in Le Head!", "1"])
return "Gwa Gwa" return "Gwa Gwa"
@ -153,14 +142,14 @@ def create_group():
""" """
Creates a group Creates a group
""" """
new_group = db.Groups( new_group = Group(
name=request.form["name"], name=request.form["name"],
description=request.form["description"], description=request.form["description"],
author_id=current_user.id, author_id=current_user.id,
) )
db_session.add(new_group) db.session.add(new_group)
db_session.commit() db.session.commit()
return ":3" return ":3"
@ -175,29 +164,23 @@ def modify_group():
image_id = request.form["image"] image_id = request.form["image"]
action = request.form["action"] 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: if group.author_id != current_user.id:
abort(404)
elif group.author_id != current_user.id:
abort(403) abort(403)
if action == "add": if (
if not ( action == "add"
db_session.query(db.GroupJunction) and not GroupJunction.query.filter_by(
.filter_by(group_id=group_id, post_id=image_id) group_id=group_id, post_id=image_id
.first() ).first()
): ):
db_session.add(db.GroupJunction(group_id=group_id, post_id=image_id)) db.session.add(GroupJunction(group_id=group_id, post_id=image_id))
elif request.form["action"] == "remove": elif request.form["action"] == "remove":
( GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).delete()
db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id)
.delete()
)
db_session.commit()
db.session.commit()
return ":3" return ":3"
@ -207,17 +190,16 @@ def delete_group():
Deletes a group Deletes a group
""" """
group_id = request.form["group"] group_id = request.form["group"]
group = Group.query.filter_by(id=group_id).first()
group = db_session.query(db.Groups).filter_by(id=group_id).first()
if group is None: if group is None:
abort(404) abort(404)
elif group.author_id != current_user.id: elif group.author_id != current_user.id:
abort(403) abort(403)
db_session.query(db.Groups).filter_by(id=group_id).delete() GroupJunction.query.filter_by(group_id=group_id).delete()
db_session.query(db.GroupJunction).filter_by(group_id=group_id).delete() db.session.delete(group)
db_session.commit() db.session.commit()
flash(["Group yeeted!", "1"]) flash(["Group yeeted!", "1"])
return ":3" return ":3"

View file

@ -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 flask_login import login_user, logout_user, login_required
from sqlalchemy.orm import sessionmaker from gallery.extensions import db
from gallery import db from gallery.models import User
blueprint = Blueprint("auth", __name__, url_prefix="/auth") blueprint = Blueprint("auth", __name__, url_prefix="/auth")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/login", methods=["POST"]) @blueprint.route("/login", methods=["POST"])
@ -30,7 +28,7 @@ def login():
password = request.form["password"].strip() password = request.form["password"].strip()
remember = bool(request.form["remember-me"]) remember = bool(request.form["remember-me"])
user = 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): if not user or not check_password_hash(user.password, password):
logging.error("Login attempt from %s", request.remote_addr) logging.error("Login attempt from %s", request.remote_addr)
@ -79,7 +77,7 @@ def register():
elif password_repeat != password: elif password_repeat != password:
error.append("Passwords do not match!") error.append("Passwords do not match!")
user_exists = db_session.query(db.Users).filter_by(username=username).first() user_exists = User.query.filter_by(username=username).first()
if user_exists: if user_exists:
error.append("User already exists!") error.append("User already exists!")
@ -88,13 +86,13 @@ def register():
print(error) print(error)
return jsonify(error), 400 return jsonify(error), 400
register_user = db.Users( register_user = User(
username=username, username=username,
email=email, email=email,
password=generate_password_hash(password, method="sha256"), password=generate_password_hash(password, method="sha256"),
) )
db_session.add(register_user) db.session.add(register_user)
db_session.commit() db.session.commit()
logging.info("User %s registered", username) logging.info("User %s registered", username)
return "ok", 200 return "ok", 200

36
gallery/config.py Normal file
View file

@ -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"]

View file

@ -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")

16
gallery/extensions.py Normal file
View file

@ -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})

105
gallery/models.py Normal file
View file

@ -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)

View file

@ -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;
}

View file

@ -65,7 +65,7 @@ window.onload = function () {
} }
infoButton.onclick = function () { infoButton.onclick = function () {
popUpShow('OnlyLegs', popUpShow('OnlyLegs',
'<a href="https://github.com/Fluffy-Bean/onlylegs">V23.04.08</a> ' + '<a href="https://github.com/Fluffy-Bean/onlylegs">V23.04.10</a> ' +
'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' + 'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' +
'<br>Made by Fluffy and others with ❤️'); '<br>Made by Fluffy and others with ❤️');
} }

View file

@ -1,14 +1,14 @@
function addNotification(notificationText, notificationLevel) { function addNotification(notificationText, notificationLevel) {
let notificationContainer = document.querySelector('.notifications'); const notificationContainer = document.querySelector('.notifications');
// Set the different icons for the different notification levels // Set the different icons for the different notification levels
let successIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"></path></svg>'; const successIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"></path></svg>';
let criticalIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"></path></svg>'; const criticalIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"></path></svg>';
let warningIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>'; const warningIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>';
let infoIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>'; const infoIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>';
// Create notification element // Create notification element
let notification = document.createElement('div'); const notification = document.createElement('div');
notification.classList.add('sniffle__notification'); notification.classList.add('sniffle__notification');
notification.onclick = function() { notification.onclick = function() {
if (notification) { if (notification) {
@ -21,7 +21,7 @@ function addNotification(notificationText, notificationLevel) {
}; };
// Create icon element and append to notification // Create icon element and append to notification
let iconElement = document.createElement('span'); const iconElement = document.createElement('span');
iconElement.classList.add('sniffle__notification-icon'); iconElement.classList.add('sniffle__notification-icon');
notification.appendChild(iconElement); notification.appendChild(iconElement);
@ -41,7 +41,7 @@ function addNotification(notificationText, notificationLevel) {
} }
// Create text element and append to notification // Create text element and append to notification
let description = document.createElement('span'); const description = document.createElement('span');
description.classList.add('sniffle__notification-text'); description.classList.add('sniffle__notification-text');
description.innerHTML = notificationText; description.innerHTML = notificationText;
notification.appendChild(description); notification.appendChild(description);

View file

@ -1,19 +1,19 @@
function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) { function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) {
// Get popup elements // Get popup elements
let popupSelector = document.querySelector('.pop-up'); const popupSelector = document.querySelector('.pop-up');
let headerSelector = document.querySelector('.pop-up-header'); const headerSelector = document.querySelector('.pop-up-header');
let actionsSelector = document.querySelector('.pop-up-controlls'); const actionsSelector = document.querySelector('.pop-up-controlls');
// Clear popup elements // Clear popup elements
headerSelector.innerHTML = ''; headerSelector.innerHTML = '';
actionsSelector.innerHTML = ''; actionsSelector.innerHTML = '';
// Set popup header and subtitle // Set popup header and subtitle
let titleElement = document.createElement('h2'); const titleElement = document.createElement('h2');
titleElement.innerHTML = titleText; titleElement.innerHTML = titleText;
headerSelector.appendChild(titleElement); headerSelector.appendChild(titleElement);
let subtitleElement = document.createElement('p'); const subtitleElement = document.createElement('p');
subtitleElement.innerHTML = subtitleText; subtitleElement.innerHTML = subtitleText;
headerSelector.appendChild(subtitleElement); headerSelector.appendChild(subtitleElement);
@ -25,8 +25,7 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null)
if (userActions) { if (userActions) {
// for each user action, add the element // for each user action, add the element
for (let i = 0; i < userActions.length; i++) { for (let i = 0; i < userActions.length; i++) {
let action = userActions[i]; actionsSelector.appendChild(userActions[i]);
actionsSelector.appendChild(action);
} }
} else { } else {
actionsSelector.innerHTML = '<button class="btn-block" onclick="popupDissmiss()">Close</button>'; actionsSelector.innerHTML = '<button class="btn-block" onclick="popupDissmiss()">Close</button>';
@ -39,7 +38,7 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null)
} }
function popupDissmiss() { function popupDissmiss() {
let popupSelector = document.querySelector('.pop-up'); const popupSelector = document.querySelector('.pop-up');
document.querySelector("html").style.overflow = "auto"; document.querySelector("html").style.overflow = "auto";
popupSelector.classList.remove('active'); popupSelector.classList.remove('active');

View file

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

View file

@ -75,7 +75,6 @@
grid-template-columns: 1fr auto grid-template-columns: 1fr auto
grid-template-rows: 1fr auto auto grid-template-rows: 1fr auto auto
grid-template-areas: 'info info' 'header header' 'subtitle options' grid-template-areas: 'info info' 'header header' 'subtitle options'
gap: 0.5rem
z-index: +2 z-index: +2
@ -89,6 +88,8 @@
.banner-header .banner-header
grid-area: header grid-area: header
margin: 0.5rem 0
text-align: left text-align: left
font-size: 6.9rem font-size: 6.9rem
font-weight: 700 font-weight: 700
@ -113,7 +114,7 @@
background-color: RGB($bg-100) background-color: RGB($bg-100)
.banner-content .banner-content
padding: 0.5rem padding: 0 0.5rem
width: 100% width: 100%
height: 100% height: 100%
@ -124,7 +125,6 @@
display: flex display: flex
flex-direction: row flex-direction: row
justify-content: flex-start justify-content: flex-start
gap: 1rem
z-index: +2 z-index: +2
@ -137,7 +137,7 @@
justify-self: flex-start justify-self: flex-start
.banner-header .banner-header
padding-bottom: 0.25rem margin-right: 0.6rem
white-space: nowrap white-space: nowrap
text-overflow: ellipsis text-overflow: ellipsis
@ -149,6 +149,8 @@
color: RGB($primary) color: RGB($primary)
.banner-info .banner-info
margin-right: 0.6rem
font-size: 0.9rem font-size: 0.9rem
font-weight: 400 font-weight: 400
@ -174,9 +176,9 @@
flex-direction: column flex-direction: column
justify-content: center justify-content: center
align-items: center align-items: center
gap: 1rem
.banner-header .banner-header
margin: 1rem 0
text-align: center text-align: center
font-size: 2.5rem font-size: 2.5rem

View file

@ -1,12 +1,12 @@
@mixin btn-block($color) @mixin btn-block($color)
color: RGB($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) background-color: RGBA($color, 0.1)
color: RGB($color) 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 .btn-block
padding: 0.4rem 0.7rem padding: 0.4rem 0.7rem
@ -25,19 +25,19 @@
font-weight: 400 font-weight: 400
text-align: center text-align: center
background-color: transparent background-color: RGBA($white, 0.1)
color: RGB($white) color: RGB($white)
border: none border: none
border-radius: $rad-inner border-radius: $rad-inner
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
outline: none
cursor: pointer 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 &:hover, &:focus-visible
background-color: RGBA($white, 0.1) background-color: RGBA($white, 0.2)
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
&:focus-visible
outline: 2px solid RGBA($white, 0.3)
&.primary &.primary
@include btn-block($primary) @include btn-block($primary)
@ -91,10 +91,11 @@
background-color: RGBA($white, 0.1) background-color: RGBA($white, 0.1)
color: RGB($white) color: RGB($white)
border: none border: none
border-bottom: 3px solid RGBA($white, 0.1) border-bottom: 3px solid RGBA($white, 0.1)
border-radius: $rad-inner border-radius: $rad-inner
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
outline: none
cursor: pointer cursor: pointer
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
@ -107,7 +108,6 @@
&:focus &:focus
border-color: RGB($primary) border-color: RGB($primary)
outline: none
&.black &.black
@include btn-block($black) @include btn-block($black)
@ -132,13 +132,14 @@
background-color: RGBA($white, 0.1) background-color: RGBA($white, 0.1)
color: RGB($white) color: RGB($white)
border: none border: none
border-radius: $rad-inner border-radius: $rad-inner
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
outline: none
cursor: pointer cursor: pointer
overflow: hidden 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 input
position: absolute position: absolute
@ -153,20 +154,20 @@
text-align: center text-align: center
overflow: hidden overflow: hidden
&:hover &:hover, &:focus-visible
background-color: RGBA($white, 0.2) background-color: RGBA($white, 0.2)
color: RGB($white) color: RGB($white)
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
&:focus-visible
outline: 2px solid RGBA($white, 0.3)
&.active &.active
background-color: RGBA($primary, 0.2) background-color: RGBA($primary, 0.2)
color: RGB($primary) color: RGB($primary)
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($primary, 0.3)
&.edging &.edging
background-color: RGBA($white, 0.2) background-color: RGBA($white, 0.2)
color: RGB($white) color: RGB($white)
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
input input
display: none // So it doesnt get in the way of the drop as that breaks things display: none // So it doesnt get in the way of the drop as that breaks things
@ -174,3 +175,4 @@
&.error &.error
background-color: RGBA($critical, 0.2) background-color: RGBA($critical, 0.2)
color: RGB($critical) color: RGB($critical)
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($critical, 0.3)

View file

@ -18,10 +18,11 @@
background-color: RGB($bg-200) background-color: RGB($bg-200)
border-radius: $rad border-radius: $rad
box-shadow: 0 1px 0 RGB($bg-100), 0 -1px 0 RGB($bg-300)
.pill-text .pill-text
margin: 0 margin: 0
padding: 0.5rem padding: 0.5rem 1rem
width: auto width: auto
height: 2.5rem height: 2.5rem
@ -36,7 +37,9 @@
font-size: 1rem font-size: 1rem
font-weight: 400 font-weight: 400
background-color: RGB($bg-200)
color: RGB($fg-white) color: RGB($fg-white)
border-radius: $rad
.pill-item .pill-item
margin: 0 margin: 0

View file

@ -1,15 +1,14 @@
.gallery-grid .gallery-grid
margin: 0 margin: 0
padding: 0.65rem padding: 0.35rem
width: 100% width: 100%
display: grid display: grid
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
gap: 0.65rem
.gallery-item .gallery-item
margin: 0 margin: 0.35rem
padding: 0 padding: 0
height: auto height: auto
@ -97,7 +96,7 @@
opacity: 1 opacity: 1
.group-item .group-item
margin: 0 margin: 0.35rem
padding: 0 padding: 0
height: auto height: auto
@ -161,8 +160,6 @@
display: block display: block
background-color: RGB($bg-bright)
img img
width: 100% width: 100%
height: 100% height: 100%

View file

@ -25,7 +25,7 @@
background-color: RGB($fg-white) background-color: RGB($fg-white)
filter: blur(1rem) filter: blur(1rem) saturate(1.2)
transform: scale(1.1) transform: scale(1.1)
object-fit: cover object-fit: cover

View file

@ -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)

View file

@ -1,5 +1,5 @@
.info-container .info-container
width: 25rem width: 27rem
height: 100vh height: 100vh
position: absolute position: absolute
@ -17,7 +17,7 @@
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1) transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
&.collapsed &.collapsed
left: -25rem left: -27rem
.info-tab .info-tab
width: 100% width: 100%
@ -39,8 +39,8 @@
transform: rotate(90deg) transform: rotate(90deg)
.info-table .info-table
height: 0
padding: 0 padding: 0
opacity: 0
.collapse-indicator .collapse-indicator
margin: 0 margin: 0
@ -126,11 +126,6 @@
.link .link
margin: 0 margin: 0
padding: 0 padding: 0
font-size: 1rem
font-weight: 500
text-align: center
line-height: 1
color: RGB($primary) color: RGB($primary)
@ -150,43 +145,31 @@
border-collapse: collapse border-collapse: collapse
tr tr
margin: 0
padding: 0
width: 100%
white-space: nowrap white-space: nowrap
td td
padding-bottom: 0.5rem padding-bottom: 0.5rem
max-width: 0
font-size: 1rem
font-weight: 400
vertical-align: top
td:first-child td:first-child
padding-right: 0.5rem padding-right: 0.5rem
width: 50% width: 50%
max-width: 0
overflow: hidden overflow: hidden
text-overflow: ellipsis text-overflow: ellipsis
white-space: nowrap white-space: nowrap
font-size: 1rem
font-weight: 400
td:last-child td:last-child
padding: 0
width: 50% width: 50%
max-width: 0
overflow: hidden white-space: normal
text-overflow: ellipsis word-break: break-word
white-space: nowrap
font-size: 1rem
font-weight: 400
td.empty-table
opacity: 0.3
tr:last-of-type td tr:last-of-type td
padding-bottom: 0 padding-bottom: 0

View file

@ -1,5 +1,4 @@
@import 'background' @import 'background'
@import 'fullscreen'
@import 'info-tab' @import 'info-tab'
@import 'image' @import 'image'
@ -18,10 +17,10 @@
z-index: 3 z-index: 3
.image-block .image-block
margin: 0 0 0 25rem margin: 0 0 0 27rem
padding: 0 padding: 0
width: calc(100% - 25rem) width: calc(100% - 27rem)
height: 100vh height: 100vh
position: relative position: relative
@ -79,4 +78,4 @@
.info-tab.collapsed .info-header .info-tab.collapsed .info-header
border-radius: $rad border-radius: $rad

View file

@ -61,7 +61,7 @@
&::after &::after
content: "" content: ""
width: 450px width: 100%
height: 3px height: 3px
position: absolute position: absolute

View file

@ -94,10 +94,6 @@
vertical-align: middle vertical-align: middle
a, .link a, .link
font-size: 1rem
font-weight: 500
line-height: 1
color: RGB($primary) color: RGB($primary)
cursor: pointer cursor: pointer

View file

@ -26,7 +26,7 @@
padding: 0 padding: 0
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 400
form form
margin: 0 margin: 0
@ -57,9 +57,9 @@
position: absolute position: absolute
bottom: 0 bottom: 0
left: -25rem left: -27rem
width: 25rem width: 27rem
height: 100% height: 100%
display: flex display: flex
@ -67,11 +67,10 @@
gap: 1rem gap: 1rem
background-color: RGB($bg-200) background-color: RGB($bg-200)
opacity: 0
z-index: +2 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 #dragIndicator
display: none display: none
@ -197,7 +196,6 @@
.container .container
left: 0 left: 0
opacity: 1
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.upload-panel .upload-panel

View file

@ -4,10 +4,10 @@
@import "variables" @import "variables"
@import "animations" @import "animations"
@import "components/elements/notification" @import "components/notification"
@import "components/elements/pop-up" @import "components/pop-up"
@import "components/elements/upload-panel" @import "components/upload-panel"
@import "components/elements/tags" @import "components/tags"
@import "components/navigation" @import "components/navigation"
@import "components/banner" @import "components/banner"

View file

@ -74,13 +74,3 @@ $breakpoint: 800px
--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

View file

@ -15,7 +15,7 @@
} }
} }
{% if current_user.id == group.author_id %} {% if current_user.id == group.author.id %}
function groupDelete() { function groupDelete() {
cancelBtn = document.createElement('button'); cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block'); cancelBtn.classList.add('btn-block');
@ -223,7 +223,7 @@
<img src="{{ url_for('api.file', file_name=images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;" alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"/> <img src="{{ url_for('api.file', file_name=images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;" alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"/>
<span class="banner-filter"></span> <span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
<p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author_id) }}" class="link">By {{ group.author_username }}</a></p> <p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author.id) }}" class="link">By {{ group.author.username }}</a></p>
<h1 class="banner-header">{{ group.name }}</h1> <h1 class="banner-header">{{ group.name }}</h1>
<p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p> <p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p>
<div class="pill-row"> <div class="pill-row">
@ -232,7 +232,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button> </button>
</div> </div>
{% if current_user.id == group.author_id %} {% if current_user.id == group.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="groupDelete()"> <button class="pill-item pill__critical" onclick="groupDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
@ -249,14 +249,14 @@
<div class="banner-small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1 class="banner-header">{{ group.name }}</h1> <h1 class="banner-header">{{ group.name }}</h1>
<p class="banner-info">By {{ group.author_username }}</p> <p class="banner-info">By {{ group.author.username }}</p>
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="groupShare()"> <button class="pill-item" onclick="groupShare()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button> </button>
</div> </div>
{% if current_user.id == group.author_id %} {% if current_user.id == group.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="groupDelete()"> <button class="pill-item pill__critical" onclick="groupDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>

View file

@ -27,7 +27,7 @@
} }
} }
{% if current_user.id == image.author_id %} {% if current_user.id == image.author.id %}
function imageDelete() { function imageDelete() {
cancelBtn = document.createElement('button'); cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block'); cancelBtn.classList.add('btn-block');
@ -85,10 +85,6 @@
<span></span> <span></span>
</div> </div>
<div class="image-fullscreen" onclick="imageFullscreenOff()">
<img src="" alt="{{ image.alt }}"/>
</div>
<div class="image-grid"> <div class="image-grid">
<div class="image-block"> <div class="image-block">
<div class="image-container"> <div class="image-container">
@ -124,7 +120,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-42.34-61.66a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L120,164.69V120a8,8,0,0,1,16,0v44.69l10.34-10.35A8,8,0,0,1,157.66,154.34Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-42.34-61.66a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L120,164.69V120a8,8,0,0,1,16,0v44.69l10.34-10.35A8,8,0,0,1,157.66,154.34Z"></path></svg>
</a> </a>
</div> </div>
{% if current_user.id == image.author_id %} {% if current_user.id == image.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="imageDelete()"> <button class="pill-item pill__critical" onclick="imageDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
@ -145,22 +141,6 @@
</div> </div>
<div class="info-container"> <div class="info-container">
{% if image.post_description %}
<div class="info-tab">
<div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,160H40V56H216V200ZM184,96a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,96Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,128Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,160Z"></path></svg>
<h2>Description</h2>
<button class="collapse-indicator">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
</svg>
</button>
</div>
<div class="info-table">
<p>{{ image.post_description }}</p>
</div>
</div>
{% endif %}
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>
@ -175,12 +155,18 @@
<table> <table>
<tr> <tr>
<td>Author</td> <td>Author</td>
<td><a href="{{ url_for('profile.profile', id=image.author_id) }}" class="link">{{ image.author_username }}</a></td> <td><a href="{{ url_for('profile.profile', id=image.author.id) }}" class="link">{{ image.author.username }}</a></td>
</tr> </tr>
<tr> <tr>
<td>Upload date</td> <td>Upload date</td>
<td><span class="time">{{ image.created_at }}</span></td> <td><span class="time">{{ image.created_at }}</span></td>
</tr> </tr>
{% if image.description %}
<tr>
<td>Description</td>
<td>{{ image.description }}</td>
</tr>
{% endif %}
</table> </table>
<div class="img-colours"> <div class="img-colours">
{% for col in image.colours %} {% for col in image.colours %}

View file

@ -30,11 +30,12 @@
type="image/svg+xml" type="image/svg+xml"
media="(prefers-color-scheme: dark)"/> media="(prefers-color-scheme: dark)"/>
{% assets "lib" %} <link
<script type="text/javascript" src="{{ ASSET_URL }}"></script> rel="prefetch"
{% endassets %} href="{{url_for('static', filename='fonts/font.css')}}"
type="stylesheet"/>
{% assets "js" %} {% assets "scripts" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endassets %}
@ -69,7 +70,7 @@
<div class="navigation"> <div class="navigation">
<!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">--> <!--<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 %}"> <a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96ZM104,88a16,16,0,1,1,16,16A16,16,0,0,1,104,88Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96ZM104,88a16,16,0,1,1,16,16A16,16,0,0,1,104,88Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Home Home
@ -77,7 +78,7 @@
</span> </span>
</a> </a>
<a href="{{url_for('group.groups')}}" class="navigation-item {% block nav_groups %}{% endblock %}"> <a href="{{ url_for('group.groups') }}" class="navigation-item {% block nav_groups %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64l27.73,20.8a16.12,16.12,0,0,0,9.6,3.2H200v16H146.43a16,16,0,0,0-8.88,2.69l-20,13.31H69.42a15.94,15.94,0,0,0-14.86,10.06L40,166.46V64Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64l27.73,20.8a16.12,16.12,0,0,0,9.6,3.2H200v16H146.43a16,16,0,0,0-8.88,2.69l-20,13.31H69.42a15.94,15.94,0,0,0-14.86,10.06L40,166.46V64Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Groups Groups
@ -86,66 +87,66 @@
</a> </a>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()"> <button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,77.66a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,0l48,48a8,8,0,0,1-11.32,11.32L136,43.31V128a8,8,0,0,1-16,0V43.31L85.66,77.66A8,8,0,0,1,74.34,77.66ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16h68a4,4,0,0,1,4,4v3.46c0,13.45,11,24.79,24.46,24.54A24,24,0,0,0,152,128v-4a4,4,0,0,1,4-4h68A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,77.66a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,0l48,48a8,8,0,0,1-11.32,11.32L136,43.31V128a8,8,0,0,1-16,0V43.31L85.66,77.66A8,8,0,0,1,74.34,77.66ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16h68a4,4,0,0,1,4,4v3.46c0,13.45,11,24.79,24.46,24.54A24,24,0,0,0,152,128v-4a4,4,0,0,1,4-4h68A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Upload Upload
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span> </span>
</button> </button>
{% endif %} {% endif %}
<span class="navigation-spacer"></span> <span class="navigation-spacer"></span>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a href="{{url_for('profile.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}"> <a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Profile Profile
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span> </span>
</a> </a>
<a href="{{url_for('settings.general')}}" class="navigation-item {% block nav_settings %}{% endblock %}"> <a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Settings Settings
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span> </span>
</a> </a>
{% else %} {% else %}
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()"> <button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M141.66,133.66l-40,40A8,8,0,0,1,88,168V136H24a8,8,0,0,1,0-16H88V88a8,8,0,0,1,13.66-5.66l40,40A8,8,0,0,1,141.66,133.66ZM192,32H136a8,8,0,0,0,0,16h56V208H136a8,8,0,0,0,0,16h56a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M141.66,133.66l-40,40A8,8,0,0,1,88,168V136H24a8,8,0,0,1,0-16H88V88a8,8,0,0,1,13.66-5.66l40,40A8,8,0,0,1,141.66,133.66ZM192,32H136a8,8,0,0,0,0,16h56V208H136a8,8,0,0,0,0,16h56a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Login Login
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span> </span>
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<div class="upload-panel"> <div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span> <span class="click-off" onclick="closeUploadTab()"></span>
<div class="container"> <div class="container">
<span id="dragIndicator"></span> <span id="dragIndicator"></span>
<h3>Upload stuffs</h3> <h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p> <p>May the world see your stuff 👀</p>
<form id="uploadForm"> <form id="uploadForm">
<button class="fileDrop-block" type="button"> <button class="fileDrop-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H80a8,8,0,0,1,0,16H32v64H224V136H176a8,8,0,0,1,0-16h48A16,16,0,0,1,240,136ZM85.66,77.66,120,43.31V128a8,8,0,0,0,16,0V43.31l34.34,34.35a8,8,0,0,0,11.32-11.32l-48-48a8,8,0,0,0-11.32,0l-48,48A8,8,0,0,0,85.66,77.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H80a8,8,0,0,1,0,16H32v64H224V136H176a8,8,0,0,1,0-16h48A16,16,0,0,1,240,136ZM85.66,77.66,120,43.31V128a8,8,0,0,0,16,0V43.31l34.34,34.35a8,8,0,0,0,11.32-11.32l-48-48a8,8,0,0,0-11.32,0l-48,48A8,8,0,0,0,85.66,77.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<span class="status">Choose or Drop file</span> <span class="status">Choose or Drop file</span>
<input type="file" id="file" tab-index="-1"/> <input type="file" id="file" tab-index="-1"/>
</button> </button>
<input class="input-block" type="text" placeholder="alt" id="alt"/> <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="description" id="description"/>
<input class="input-block" type="text" placeholder="tags" id="tags"/> <input class="input-block" type="text" placeholder="tags" id="tags"/>
<button class="btn-block" type="submit">Upload</button> <button class="btn-block primary" type="submit">Upload</button>
</form> </form>
<div class="upload-jobs"></div> <div class="upload-jobs"></div>
</div>
</div> </div>
</div>
{% endif %} {% endif %}
<div class="content"> <div class="content">

View file

@ -119,9 +119,9 @@
{% if groups %} {% if groups %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for group in groups %} {% for group in groups %}
<a id="group-{{ group.id }}" class="group-item" href="{{ url_for('group.group', group_id=group.id) }}"> <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 %}>
<div class="image-filter"> <div class="image-filter">
<p class="image-subtitle">By {{ group.author_username }}</p> <p class="image-subtitle">By {{ group.author.username }}</p>
<p class="image-title">{{ group.name }}</p> <p class="image-title">{{ group.name }}</p>
</div> </div>
<div class="images size-{{ group.images|length }}"> <div class="images size-{{ group.images|length }}">

View file

@ -77,10 +77,9 @@ class Metadata:
"raw": value, "raw": value,
"formatted": ( "formatted": (
getattr( getattr(
helpers, mapping_val[key][1] # pylint: disable=E0602 helpers, # pylint: disable=E0602
)( mapping_val[key][1],
value )(value)
)
), ),
} }
else: else:

View file

@ -42,7 +42,7 @@ def shutter(value):
""" """
Formats the shutter speed into a standard format Formats the shutter speed into a standard format
""" """
return str(value) + "s" return str(value) + " s"
def focal_length(value): def focal_length(value):
@ -50,16 +50,18 @@ def focal_length(value):
Formats the focal length into a standard format Formats the focal length into a standard format
""" """
try: try:
return str(value[0] / value[1]) + "mm" calculated = value[0] / value[1]
except TypeError: except TypeError:
return str(value) + "mm" calculated = value
return str(calculated) + " mm"
def exposure(value): def exposure(value):
""" """
Formats the exposure value into a standard format Formats the exposure value into a standard format
""" """
return str(value) + "EV" return str(value) + " EV"
def color_space(value): def color_space(value):
@ -78,28 +80,28 @@ def flash(value):
Maps the value of the flash to a human readable format Maps the value of the flash to a human readable format
""" """
value_map = { value_map = {
0: "Flash did not fire", 0: "Did not fire",
1: "Flash fired", 1: "Fired",
5: "Strobe return light not detected", 5: "Strobe return light not detected",
7: "Strobe return light detected", 7: "Strobe return light detected",
9: "Flash fired, compulsory flash mode", 9: "Fired, compulsory",
13: "Flash fired, compulsory flash mode, return light not detected", 13: "Fired, compulsory, return light not detected",
15: "Flash fired, compulsory flash mode, return light detected", 15: "Fired, compulsory, return light detected",
16: "Flash did not fire, compulsory flash mode", 16: "Did not fire, compulsory",
24: "Flash did not fire, auto mode", 24: "Did not fire, auto mode",
25: "Flash fired, auto mode", 25: "Fired, auto mode",
29: "Flash fired, auto mode, return light not detected", 29: "Fired, auto mode, return light not detected",
31: "Flash fired, auto mode, return light detected", 31: "Fired, auto mode, return light detected",
32: "No flash function", 32: "No function",
65: "Flash fired, red-eye reduction mode", 65: "Fired, red-eye reduction mode",
69: "Flash fired, red-eye reduction mode, return light not detected", 69: "Fired, red-eye reduction mode, return light not detected",
71: "Flash fired, red-eye reduction mode, return light detected", 71: "Fired, red-eye reduction mode, return light detected",
73: "Flash fired, compulsory flash mode, red-eye reduction mode", 73: "Fired, compulsory, red-eye reduction mode",
77: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", 77: "Fired, compulsory, red-eye reduction mode, return light not detected",
79: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", 79: "Fired, compulsory, red-eye reduction mode, return light detected",
89: "Flash fired, auto mode, red-eye reduction mode", 89: "Fired, auto mode, red-eye reduction mode",
93: "Flash fired, auto mode, return light not detected, red-eye reduction mode", 93: "Fired, auto mode, return light not detected, red-eye reduction mode",
95: "Flash fired, auto mode, return light detected, red-eye reduction mode", 95: "Fired, auto mode, return light detected, red-eye reduction mode",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -213,8 +215,8 @@ def white_balance(value):
Maps the value of the white balance to a human readable format Maps the value of the white balance to a human readable format
""" """
value_map = { value_map = {
0: "Auto white balance", 0: "Auto",
1: "Manual white balance", 1: "Manual",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -227,8 +229,8 @@ def exposure_mode(value):
Maps the value of the exposure mode to a human readable format Maps the value of the exposure mode to a human readable format
""" """
value_map = { value_map = {
0: "Auto exposure", 0: "Auto",
1: "Manual exposure", 1: "Manual",
2: "Auto bracket", 2: "Auto bracket",
} }
try: try:
@ -386,4 +388,34 @@ def pixel_dimension(value):
""" """
Maps the value of the pixel dimension to a human readable format Maps the value of the pixel dimension to a human readable format
""" """
return str(value) + "px" return str(value) + " px"
def title(value):
"""
Maps the value of the title to a human readable format
"""
return str(value.title())
def subject_distance(value):
"""
Maps the value of the subject distance to a human readable format
"""
return str(value) + " m"
def subject_distance_range(value):
"""
Maps the value of the subject distance range to a human readable format
"""
value_map = {
0: "Unknown",
1: "Macro",
2: "Close view",
3: "Distant view",
}
try:
return value_map[int(value)]
except KeyError:
return None

View file

@ -10,22 +10,22 @@ PHOTOGRAHER_MAPPING = {
"Copyright": ["Copyright"], "Copyright": ["Copyright"],
} }
CAMERA_MAPPING = { CAMERA_MAPPING = {
"Model": ["Model"], "Model": ["Model", "title"],
"Make": ["Make"], "Make": ["Manifacturer", "title"],
"BodySerialNumber": ["Camera Type"], "BodySerialNumber": ["Camera Type"],
"LensMake": ["Lens Make"], "LensMake": ["Lens Make", "title"],
"LenseModel": ["Lens Model"], "LensModel": ["Lens Model", "title"],
"LensSpecification": ["Lens Specification", "lens_specification"], "LensSpecification": ["Lens Specification", "lens_specification"],
"ComponentsConfiguration": ["Components Configuration", "components_configuration"], "ComponentsConfiguration": ["Components Configuration", "components_configuration"],
"DateTime": ["Date Processed", "date_format"], "DateTime": ["Date and Time", "date_format"],
"DateTimeDigitized": ["Time Digitized", "date_format"], "DateTimeOriginal": ["Date and Time (Original)", "date_format"],
"DateTimeDigitized": ["Date and Time (Digitized)", "date_format"],
"OffsetTime": ["Time Offset"], "OffsetTime": ["Time Offset"],
"OffsetTimeOriginal": ["Time Offset - Original"], "OffsetTimeOriginal": ["Time Offset (Original)"],
"OffsetTimeDigitized": ["Time Offset - Digitized"], "OffsetTimeDigitized": ["Time Offset (Digitized)"],
"DateTimeOriginal": ["Date Original", "date_format"], "FNumber": ["FNumber", "fnumber"],
"FNumber": ["F-Stop", "fnumber"],
"FocalLength": ["Focal Length", "focal_length"], "FocalLength": ["Focal Length", "focal_length"],
"FocalLengthIn35mmFilm": ["Focal Length (35mm format)", "focal_length"], "FocalLengthIn35mmFilm": ["Focal Length in 35mm format", "focal_length"],
"MaxApertureValue": ["Max Aperture", "fnumber"], "MaxApertureValue": ["Max Aperture", "fnumber"],
"ApertureValue": ["Aperture", "fnumber"], "ApertureValue": ["Aperture", "fnumber"],
"ShutterSpeedValue": ["Shutter Speed", "shutter"], "ShutterSpeedValue": ["Shutter Speed", "shutter"],
@ -41,6 +41,8 @@ CAMERA_MAPPING = {
"MeteringMode": ["Metering Mode", "metering_mode"], "MeteringMode": ["Metering Mode", "metering_mode"],
"LightSource": ["Light Source", "light_source"], "LightSource": ["Light Source", "light_source"],
"SceneCaptureType": ["Scene Capture Type", "scene_capture_type"], "SceneCaptureType": ["Scene Capture Type", "scene_capture_type"],
"SubjectDistance": ["Subject Distance", "subject_distance"],
"SubjectDistanceRange": ["Subject Distance Range", "subject_distance_range"],
} }
SOFTWARE_MAPPING = { SOFTWARE_MAPPING = {
"Software": ["Software"], "Software": ["Software"],

View file

@ -3,16 +3,14 @@ Onlylegs - Image Groups
Why groups? Because I don't like calling these albums Why groups? Because I don't like calling these albums
sounds more limiting that it actually is in this gallery sounds more limiting that it actually is in this gallery
""" """
from flask import Blueprint, abort, render_template, url_for from flask import Blueprint, render_template, url_for
from sqlalchemy.orm import sessionmaker from gallery.models import Post, User, GroupJunction, Group
from gallery import db from gallery.extensions import db
from gallery.utils import contrast from gallery.utils import contrast
blueprint = Blueprint("group", __name__, url_prefix="/group") blueprint = Blueprint("group", __name__, url_prefix="/group")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/", methods=["GET"]) @blueprint.route("/", methods=["GET"])
@ -20,21 +18,21 @@ def groups():
""" """
Group overview, shows all image groups Group overview, shows all image groups
""" """
groups = db_session.query(db.Groups).all() groups = Group.query.all()
# For each group, get the 3 most recent images # For each group, get the 3 most recent images
for group in groups: for group in groups:
group.author_username = ( group.author_username = (
db_session.query(db.Users.username) User.query.with_entities(User.username)
.filter(db.Users.id == group.author_id) .filter(User.id == group.author_id)
.first()[0] .first()[0]
) )
# Get the 3 most recent images # Get the 3 most recent images
images = ( images = (
db_session.query(db.GroupJunction.post_id) GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group.id) .filter(GroupJunction.group_id == group.id)
.order_by(db.GroupJunction.date_added.desc()) .order_by(GroupJunction.date_added.desc())
.limit(3) .limit(3)
) )
@ -42,10 +40,8 @@ def groups():
group.images = [] group.images = []
for image in images: for image in images:
group.images.append( group.images.append(
db_session.query( Post.query.with_entities(Post.filename, Post.alt, Post.colours, Post.id)
db.Posts.filename, db.Posts.alt, db.Posts.colours, db.Posts.id .filter(Post.id == image[0])
)
.filter(db.Posts.id == image[0])
.first() .first()
) )
@ -58,32 +54,20 @@ def group(group_id):
Group view, shows all images in a group Group view, shows all images in a group
""" """
# Get the group, if it doesn't exist, 404 # Get the group, if it doesn't exist, 404
group = db_session.query(db.Groups).filter(db.Groups.id == group_id).first() group = db.get_or_404(Group, group_id, description="Group not found! D:")
if group is None:
abort(404, "Group not found! D:")
# Get the group's author username
group.author_username = (
db_session.query(db.Users.username)
.filter(db.Users.id == group.author_id)
.first()[0]
)
# Get all images in the group from the junction table # Get all images in the group from the junction table
junction = ( junction = (
db_session.query(db.GroupJunction.post_id) GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group_id) .filter(GroupJunction.group_id == group_id)
.order_by(db.GroupJunction.date_added.desc()) .order_by(GroupJunction.date_added.desc())
.all() .all()
) )
# Get the image data for each image in the group # Get the image data for each image in the group
images = [] images = []
for image in junction: for image in junction:
images.append( images.append(Post.query.filter(Post.id == image[0]).first())
db_session.query(db.Posts).filter(db.Posts.id == image[0]).first()
)
# Check contrast for the first image in the group for the banner # Check contrast for the first image in the group for the banner
text_colour = "rgb(var(--fg-black))" text_colour = "rgb(var(--fg-black))"
@ -103,21 +87,12 @@ def group_post(group_id, image_id):
Image view, shows the image and its metadata from a specific group Image view, shows the image and its metadata from a specific group
""" """
# Get the image, if it doesn't exist, 404 # Get the image, if it doesn't exist, 404
image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first() image = db.get_or_404(Post, image_id, description="Image not found :<")
if image is None:
abort(404, "Image not found")
# Get the image's author username
image.author_username = (
db_session.query(db.Users.username)
.filter(db.Users.id == image.author_id)
.first()[0]
)
# Get all groups the image is in # Get all groups the image is in
groups = ( groups = (
db_session.query(db.GroupJunction.group_id) GroupJunction.query.with_entities(GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id) .filter(GroupJunction.post_id == image_id)
.all() .all()
) )
@ -125,24 +100,24 @@ def group_post(group_id, image_id):
image.groups = [] image.groups = []
for group in groups: for group in groups:
image.groups.append( image.groups.append(
db_session.query(db.Groups.id, db.Groups.name) Group.query.with_entities(Group.id, Group.name)
.filter(db.Groups.id == group[0]) .filter(Group.id == group[0])
.first() .first()
) )
# Get the next and previous images in the group # Get the next and previous images in the group
next_url = ( next_url = (
db_session.query(db.GroupJunction.post_id) GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group_id) .filter(GroupJunction.group_id == group_id)
.filter(db.GroupJunction.post_id > image_id) .filter(GroupJunction.post_id > image_id)
.order_by(db.GroupJunction.date_added.asc()) .order_by(GroupJunction.date_added.asc())
.first() .first()
) )
prev_url = ( prev_url = (
db_session.query(db.GroupJunction.post_id) GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(db.GroupJunction.group_id == group_id) .filter(GroupJunction.group_id == group_id)
.filter(db.GroupJunction.post_id < image_id) .filter(GroupJunction.post_id < image_id)
.order_by(db.GroupJunction.date_added.desc()) .order_by(GroupJunction.date_added.desc())
.first() .first()
) )

View file

@ -2,16 +2,12 @@
Onlylegs - Image View Onlylegs - Image View
""" """
from math import ceil from math import ceil
from flask import Blueprint, render_template, url_for, current_app
from flask import Blueprint, abort, render_template, url_for, current_app from gallery.models import Post, GroupJunction, Group
from gallery.extensions import db
from sqlalchemy.orm import sessionmaker
from gallery import db
blueprint = Blueprint("image", __name__, url_prefix="/image") blueprint = Blueprint("image", __name__, url_prefix="/image")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/<int:image_id>") @blueprint.route("/<int:image_id>")
@ -20,45 +16,36 @@ def image(image_id):
Image view, shows the image and its metadata Image view, shows the image and its metadata
""" """
# Get the image, if it doesn't exist, 404 # Get the image, if it doesn't exist, 404
image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first() image = db.get_or_404(Post, image_id, description="Image not found :<")
if not image:
abort(404, "Image not found :<")
# Get the image's author username # Get all groups the image is in
image.author_username = (
db_session.query(db.Users.username)
.filter(db.Users.id == image.author_id)
.first()[0]
)
# Get the image's groups
groups = ( groups = (
db_session.query(db.GroupJunction.group_id) GroupJunction.query.with_entities(GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id) .filter(GroupJunction.post_id == image_id)
.all() .all()
) )
# For each group, get the group data and add it to the image item # Get the group data for each group the image is in
image.groups = [] image.groups = []
for group in groups: for group in groups:
image.groups.append( image.groups.append(
db_session.query(db.Groups.id, db.Groups.name) Group.query.with_entities(Group.id, Group.name)
.filter(db.Groups.id == group[0]) .filter(Group.id == group[0])
.first() .first()
) )
# Get the next and previous images # Get the next and previous images
# Check if there is a group ID set # Check if there is a group ID set
next_url = ( next_url = (
db_session.query(db.Posts.id) Post.query.with_entities(Post.id)
.filter(db.Posts.id > image_id) .filter(Post.id > image_id)
.order_by(db.Posts.id.asc()) .order_by(Post.id.asc())
.first() .first()
) )
prev_url = ( prev_url = (
db_session.query(db.Posts.id) Post.query.with_entities(Post.id)
.filter(db.Posts.id < image_id) .filter(Post.id < image_id)
.order_by(db.Posts.id.desc()) .order_by(Post.id.desc())
.first() .first()
) )
@ -69,7 +56,7 @@ def image(image_id):
prev_url = url_for("image.image", image_id=prev_url[0]) prev_url = url_for("image.image", image_id=prev_url[0])
# Yoink all the images in the database # Yoink all the images in the database
total_images = db_session.query(db.Posts.id).order_by(db.Posts.id.desc()).all() total_images = Post.query.with_entities(Post.id).order_by(Post.id.desc()).all()
limit = current_app.config["UPLOAD_CONF"]["max-load"] limit = current_app.config["UPLOAD_CONF"]["max-load"]
# If the number of items is less than the limit, no point of calculating the page # If the number of items is less than the limit, no point of calculating the page
@ -81,7 +68,7 @@ def image(image_id):
# Slice the list of IDs into chunks of the limit # Slice the list of IDs into chunks of the limit
for j in total_images[i * limit : (i + 1) * limit]: for j in total_images[i * limit : (i + 1) * limit]:
# Is our image in this chunk? # Is our image in this chunk?
if image_id in j: if not image_id > j[-1]:
return_page = i + 1 return_page = i + 1
break break

View file

@ -6,13 +6,10 @@ from math import ceil
from flask import Blueprint, render_template, request, current_app from flask import Blueprint, render_template, request, current_app
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from sqlalchemy.orm import sessionmaker from gallery.models import Post
from gallery import db
blueprint = Blueprint("gallery", __name__) blueprint = Blueprint("gallery", __name__)
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/") @blueprint.route("/")
@ -30,7 +27,7 @@ def index():
# get the total number of images in the database # get the total number of images in the database
# calculate the total number of pages, and make sure the page number is valid # calculate the total number of pages, and make sure the page number is valid
total_images = db_session.query(db.Posts.id).count() total_images = Post.query.with_entities(Post.id).count()
pages = ceil(max(total_images, limit) / limit) pages = ceil(max(total_images, limit) / limit)
if page > pages: if page > pages:
abort( abort(
@ -41,14 +38,10 @@ def index():
# get the images for the current page # get the images for the current page
images = ( images = (
db_session.query( Post.query.with_entities(
db.Posts.filename, Post.filename, Post.alt, Post.colours, Post.created_at, Post.id
db.Posts.alt,
db.Posts.colours,
db.Posts.created_at,
db.Posts.id,
) )
.order_by(db.Posts.id.desc()) .order_by(Post.id.desc())
.offset((page - 1) * limit) .offset((page - 1) * limit)
.limit(limit) .limit(limit)
.all() .all()

View file

@ -5,16 +5,13 @@ from flask import Blueprint, render_template, request
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from flask_login import current_user from flask_login import current_user
from sqlalchemy.orm import sessionmaker from gallery.models import Post, User
from gallery import db
blueprint = Blueprint("profile", __name__, url_prefix="/profile") blueprint = Blueprint("profile", __name__, url_prefix="/profile")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/profile") @blueprint.route("/")
def profile(): def profile():
""" """
Profile overview, shows all profiles on the onlylegs gallery Profile overview, shows all profiles on the onlylegs gallery
@ -29,11 +26,11 @@ def profile():
abort(404, "You must be logged in to view your own profile!") abort(404, "You must be logged in to view your own profile!")
# Get the user's data # Get the user's data
user = db_session.query(db.Users).filter(db.Users.id == user_id).first() user = User.query.filter(User.id == user_id).first()
if not user: if not user:
abort(404, "User not found :c") abort(404, "User not found :c")
images = db_session.query(db.Posts).filter(db.Posts.author_id == user_id).all() images = Post.query.filter(Post.author_id == user_id).all()
return render_template("profile.html", user=user, images=images) return render_template("profile.html", user=user, images=images)

200
poetry.lock generated
View file

@ -1,15 +1,37 @@
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
[[package]]
name = "alembic"
version = "1.10.3"
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"},
]
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.9\""}
importlib-resources = {version = "*", markers = "python_version < \"3.9\""}
Mako = "*"
SQLAlchemy = ">=1.3.0"
typing-extensions = ">=4"
[package.extras]
tz = ["python-dateutil"]
[[package]] [[package]]
name = "astroid" name = "astroid"
version = "2.15.1" version = "2.15.2"
description = "An abstract syntax tree for Python with inference support." description = "An abstract syntax tree for Python with inference support."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.2" python-versions = ">=3.7.2"
files = [ files = [
{file = "astroid-2.15.1-py3-none-any.whl", hash = "sha256:89860bda98fe2bbd1f5d262229be7629d778ce280de68d95d4a73d1f592ad268"}, {file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"},
{file = "astroid-2.15.1.tar.gz", hash = "sha256:af4e0aff46e2868218502789898269ed95b663fba49e65d91c1e09c966266c34"}, {file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"},
] ]
[package.dependencies] [package.dependencies]
@ -279,6 +301,39 @@ files = [
Flask = ">=1.0.4" Flask = ">=1.0.4"
Werkzeug = ">=1.0.1" Werkzeug = ">=1.0.1"
[[package]]
name = "flask-migrate"
version = "4.0.4"
description = "SQLAlchemy database migrations for Flask applications using Alembic."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"},
{file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"},
]
[package.dependencies]
alembic = ">=1.9.0"
Flask = ">=0.9"
Flask-SQLAlchemy = ">=1.0"
[[package]]
name = "flask-sqlalchemy"
version = "3.0.3"
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"},
]
[package.dependencies]
Flask = ">=2.2"
SQLAlchemy = ">=1.4.18"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "2.0.2" version = "2.0.2"
@ -376,14 +431,14 @@ tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "6.1.0" version = "6.2.1"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "importlib_metadata-6.1.0-py3-none-any.whl", hash = "sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"}, {file = "importlib_metadata-6.2.1-py3-none-any.whl", hash = "sha256:f65e478a7c2177bd19517a3a15dac094d253446d8690c5f3e71e735a04312374"},
{file = "importlib_metadata-6.1.0.tar.gz", hash = "sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20"}, {file = "importlib_metadata-6.2.1.tar.gz", hash = "sha256:5a66966b39ff1c14ef5b2d60c1d842b0141fefff0f4cc6365b4bc9446c652807"},
] ]
[package.dependencies] [package.dependencies]
@ -394,6 +449,25 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker
perf = ["ipython"] perf = ["ipython"]
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] testing = ["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)"]
[[package]]
name = "importlib-resources"
version = "5.12.0"
description = "Read resources from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
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"},
]
[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)"]
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.12.0" version = "5.12.0"
@ -514,6 +588,26 @@ files = [
{file = "libsass-0.22.0.tar.gz", hash = "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425"}, {file = "libsass-0.22.0.tar.gz", hash = "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425"},
] ]
[[package]]
name = "mako"
version = "1.2.4"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
{file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
]
[package.dependencies]
MarkupSafe = ">=0.9.2"
[package.extras]
babel = ["Babel"]
lingua = ["lingua"]
testing = ["pytest"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "2.1.2" version = "2.1.2"
@ -684,18 +778,18 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-
[[package]] [[package]]
name = "pylint" name = "pylint"
version = "2.17.1" version = "2.17.2"
description = "python code static checker" description = "python code static checker"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.2" python-versions = ">=3.7.2"
files = [ files = [
{file = "pylint-2.17.1-py3-none-any.whl", hash = "sha256:8660a54e3f696243d644fca98f79013a959c03f979992c1ab59c24d3f4ec2700"}, {file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"},
{file = "pylint-2.17.1.tar.gz", hash = "sha256:d4d009b0116e16845533bc2163493d6681846ac725eab8ca8014afb520178ddd"}, {file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"},
] ]
[package.dependencies] [package.dependencies]
astroid = ">=2.15.0,<=2.17.0-dev0" astroid = ">=2.15.2,<=2.17.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [ dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.2", markers = "python_version < \"3.11\""},
@ -796,53 +890,53 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.8" version = "2.0.9"
description = "Database Abstraction Library" description = "Database Abstraction Library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "SQLAlchemy-2.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42207a5237d8b211df1c04da4a8700bb9f366f5d7be14b1a2e39cc33d330428c"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b6a94bbb1507b3fc75a3c98491f990b80a859eb5227378f6dac908dff74396"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd405d224c0f83f267bfcc430bd275195cb48bb9576f6949911faec16bba1216"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e424a385a234e0bbaac69b6b09894ce99aa285518dd9397846e78214671ec0b4"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f005245e1cb9b8ca53df73ee85e029ac43155e062405015e49ec6187a2e3fb44"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6d82bb74a44f103d566f6c86d6db0ed944e99a7da93d6490c1e987a2b91d5613"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:34eb96c1de91d8f31e988302243357bef3f7785e1b728c7d4b98bd0c117dafeb"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c8e546c249f0d23821f4b31e5adfba8e39bc90140d39f7086d4a8422d1f0fa6"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e472e9627882f2d75b87ff91c5a2bc45b31a226efc7cc0a054a94fffef85862"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-win32.whl", hash = "sha256:32af8923cfe7b115d40b88604412450d2e4714f4b9a69b1966dfe6012e254789"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-win32.whl", hash = "sha256:0a865b5ec4ba24f57c33b633b728e43fde77b968911a6046443f581b25d29dd9"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:692a2b1e0c82c678c84ccb3a9a226e0553dd115be3d5c54b1bb6ef35bc464da9"}, {file = "SQLAlchemy-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:6e84ab63d25d8564d7a8c05dc080659931a459ee27f6ed1cf4c91f292d184038"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8a215d3e9de2af881c4eee60bf2b3576fb2eeb591a9af37ff2919cd74b68f162"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db4bd1c4792da753f914ff0b688086b9a8fd78bb9bc5ae8b6d2e65f176b81eb9"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1906c954ee7287d46877636a66da157fc83553851013e42c862653ddacb380b"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5363a1c65fde7b7466769d4261126d07d872fc2e816487ae6cec93da604b6b"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a64345a6f41c8c011df3f498cee8de39d9682ca2de14698b68a50bf3a03f6e5"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc4eeb1737a5a9bdb0c24f4c982319fa6edd23cdee27180978c29cbb026f2bd"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4223002ab7faac63384019feb31eab4ae2233753fd06ea95690d4e148b5bf80d"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbda1da8d541904ba262825a833c9f619e93cb3fd1156be0a5e43cd54d588dcd"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3a1eb8ed5dc2a7bc554a612a57253f3682aea7e4c54c742153aef6a12fde10ee"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d5327f54a9c39e7871fc532639616f3777304364a0bb9b89d6033ad34ef6c5f8"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af0683c905c856db70adf6d0f84398785f7be6cb4ffdad20d8e16fe426bdf6e8"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac6a0311fb21a99855953f84c43fcff4bdca27a2ffcc4f4d806b26b54b5cddc9"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-win32.whl", hash = "sha256:6c5e056a6dc1ce29d5e0e54bc1ce669f3035ec18cb7e5c853122b5fdfe216f8f"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-win32.whl", hash = "sha256:d209594e68bec103ad5243ecac1b40bf5770c9ebf482df7abf175748a34f4853"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:21698ac65afe477400aacb56244b607477c192892e2f7a70492cae1e13f73040"}, {file = "SQLAlchemy-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:865392a50a721445156809c1a6d6ab6437be70c1c2599f591a8849ed95d3c693"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df6722580ff89295e9d11b781d7a50ff6e48fdfe1c81362000925b4d4cbab31"}, {file = "SQLAlchemy-2.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b49f1f71d7a44329a43d3edd38cc5ee4c058dfef4487498393d16172007954b"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8707ba6e783d144209dab0324cc72147a1de34d404a61a08eef96fbb3c8909f"}, {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a019f723b6c1e6b3781be00fb9e0844bc6156f9951c836ff60787cc3938d76"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cc70331b7aed25fed5260cf30a69e42eaf3d887a23a6e4f29a51167ae8a00e1"}, {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9838bd247ee42eb74193d865e48dd62eb50e45e3fdceb0fdef3351133ee53dcf"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6f33587541eb6eb0292e82b314f5ef53159f63f14ae1c08b5ef6195732523afa"}, {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:78612edf4ba50d407d0eb3a64e9ec76e6efc2b5d9a5c63415d53e540266a230a"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0cdc65cbb95ae6813c51acffa65dd8d8272a52da42e3967fc98788fe185a0e0c"}, {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f61ab84956dc628c8dfe9d105b6aec38afb96adae3e5e7da6085b583ff6ea789"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-win32.whl", hash = "sha256:9a981a65dc0082009399eac6600b0983120bc3b850f3c1ed986f68648d9dbb4c"}, {file = "SQLAlchemy-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:07950fc82f844a2de67ddb4e535f29b65652b4d95e8b847823ce66a6d540a41d"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-win_amd64.whl", hash = "sha256:b3bdbd9a5b2788b5c51c4fffa2f875103d3c6d01b8f0dc4abbf075cea11741f5"}, {file = "SQLAlchemy-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e62c4e762d6fd2901692a093f208a6a6575b930e9458ad58c2a7f080dd6132da"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3ca04c2ccf6f70278905a371ae6eb61494e8c388e98cba909bc9458ceb73dda5"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3e5864eba71a3718236a120547e52c8da2ccb57cc96cecd0480106a0c799c92"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5b030f91dd310c0f1d7da0654fcc9d8574fe430693e29c544f5558c2426e08eb"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d06e119cf79a3d80ab069f064a07152eb9ba541d084bdaee728d8a6f03fd03d"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa72fb1be5e6704d697471eb5cc07bf9cd98689be11ffa724efe2a3666d0f6c1"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee2946042cc7851842d7a086a92b9b7b494cbe8c3e7e4627e27bc912d3a7655e"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf42183f93ad3801bc08c8a111de16802a2c32d16dcb9700daf1a8ae12d9d28"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f984a190d249769a050634b248aef8991acc035e849d02b634ea006c028fa8"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2590fd682d3b9dbecb2710ba83b28ba9df18e0c3c26c78bb950715566784723"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4780be0f19e5894c17f75fc8de2fe1ae233ab37827125239ceb593c6f6bd1e2"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d6753512214d0de6a5a1c05fe716a7d643c927eb0495341db6526577960cbb55"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:68ed381bc340b4a3d373dbfec1a8b971f6350139590c4ca3cb722fdb50035777"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-win32.whl", hash = "sha256:cf1447ec09d7c9ef0b3a65cea905690ed78ed88717beaac537271c005b0e4ea2"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-win32.whl", hash = "sha256:aa5c270ece17c0c0e0a38f2530c16b20ea05d8b794e46c79171a86b93b758891"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:b4c73a3537f928ebf687244f2af9d477b3b14640b4472fcb2456ee0193850ba1"}, {file = "SQLAlchemy-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:1b69666e25cc03c602d9d3d460e1281810109e6546739187044fc256c67941ef"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:50fa610ac1e45e9adc161227cccbd5c75f8eef25b2ad4a0e080d48c27023415d"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6e27189ff9aebfb2c02fd252c629ea58657e7a5ff1a321b7fc9c2bf6dc0b5f3"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:337ae79dc63b1c957dd2e9e7eed5235492886e2af45d8df4b28748ee4859b588"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8239ce63a90007bce479adf5460d48c1adae4b933d8e39a4eafecfc084e503c"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a091b9166ab9d8ff77c3155140105e76df1d585632fb9367a31b517960df78a"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f759eccb66e6d495fb622eb7f4ac146ae674d829942ec18b7f5a35ddf029597"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0f74c66b2d0081cee5093150407ec43d23d0d5c70aa5b41f438ead1d39bc8d"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246712af9fc761d6c13f4f065470982e175d902e77aa4218c9cb9fc9ff565a0c"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ea2976a98076f44e7e8ab9a90b6dfa357253edddf687294b1bcd852ffe20e526"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b72dccc5864ea95c93e0a9c4e397708917fb450f96737b4a8395d009f90b868"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d713d8c4b36c4f69eef33a70a4521e37b75a3ac9c86ee96626c4b052337978cc"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:93c78d42c14aa9a9e0866eacd5b48df40a50d0e2790ee377af7910d224afddcf"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-win32.whl", hash = "sha256:81ada150345ef9c709b647e2bcc8f688d62de6b8f06a7544bed76bef328c6636"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-win32.whl", hash = "sha256:f49c5d3c070a72ecb96df703966c9678dda0d4cb2e2736f88d15f5e1203b4159"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:68b328500a20f328db6af2c6839eb69e3791b2cd544f5808a4b0123b130c7571"}, {file = "SQLAlchemy-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:4c3020afb144572c7bfcba9d7cce57ad42bff6e6115dffcfe2d4ae6d444a214f"},
{file = "SQLAlchemy-2.0.8-py3-none-any.whl", hash = "sha256:f12cd526188f36f161eb71fb275b06d1879c565b07cf0ab2c2408c5059df1fed"}, {file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"},
{file = "SQLAlchemy-2.0.8.tar.gz", hash = "sha256:250e158a1f36c965dde1f949366eae9a57504a8cd7a4d968e66c2d0b3c18198d"}, {file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"},
] ]
[package.dependencies] [package.dependencies]
@ -1042,4 +1136,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "58c3430743ce1cfd8e5b89db371a0d454a478cbe79ced08c645b4628980ca9f1" content-hash = "88387c49c901feebd4685ee75f6b79c0bbc8cc9c1a64798cd0394b140e165603"

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "onlylegs" name = "onlylegs"
version = "23.04.08" version = "23.04.10"
description = "Gallery built for fast and simple image management" description = "Gallery built for fast and simple image management"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"] authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT" license = "MIT"
@ -9,19 +9,20 @@ readme = ".github/README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
Flask = "^2.2.2" Flask = "^2.2.2"
Flask-Sqlalchemy = "^3.0.3"
Flask-Migrate = "^4.0.4"
Flask-Compress = "^1.13" Flask-Compress = "^1.13"
Flask-Caching = "^2.0.2" Flask-Caching = "^2.0.2"
Flask-Assets = "^2.0" Flask-Assets = "^2.0"
Flask-Login = "^0.6.2" Flask-Login = "^0.6.2"
SQLAlchemy = "^2.0.3"
python-dotenv = "^0.21.0" python-dotenv = "^0.21.0"
gunicorn = "^20.1.0" gunicorn = "^20.1.0"
pyyaml = "^6.0" pyyaml = "^6.0"
libsass = "^0.22.0"
colorthief = "^0.2.1" colorthief = "^0.2.1"
Pillow = "^9.4.0" Pillow = "^9.4.0"
platformdirs = "^3.0.0" platformdirs = "^3.0.0"
pylint = "^2.16.3" pylint = "^2.16.3"
libsass = "^0.22.0"
jsmin = "^3.0.1" jsmin = "^3.0.1"
cssmin = "^0.2.0" cssmin = "^0.2.0"

2
run.py
View file

@ -15,7 +15,7 @@ print(
#+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+#
######## ### #### ########## ### ########## ######### ######### ######## ######## ### #### ########## ### ########## ######### ######### ########
Created by Fluffy Bean - Version 23.04.08 Created by Fluffy Bean - Version 23.04.10
""" """
) )