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/gen
instance/
migrations/
.idea
.vscode
@ -11,7 +13,6 @@ venv/
*.pyc
__pycache__/
instance/
.pytest_cache/
.coverage

View file

@ -2,79 +2,87 @@
Onlylegs Gallery
This is the main app file, it loads all the other files and sets up the app
"""
# Import system modules
import os
import logging
import platformdirs
from flask_assets import Bundle
from flask_migrate import init as migrate_init
from flask_migrate import upgrade as migrate_upgrade
from flask_migrate import migrate as migrate_migrate
# Flask
from flask_compress import Compress
from flask_caching import Cache
from flask_assets import Environment, Bundle
from flask_login import LoginManager
from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash
# Configuration
import platformdirs
from dotenv import load_dotenv
from yaml import safe_load
# Import database
from sqlalchemy.orm import sessionmaker
from gallery import db
from gallery.extensions import db, migrate, login_manager, assets, compress, cache
from gallery.views import index, image, group, settings, profile
from gallery.models import User
from gallery import api
from gallery import auth
USER_DIR = platformdirs.user_config_dir("onlylegs")
INSTACE_DIR = os.path.join(platformdirs.user_config_dir("onlylegs"), "instance")
MIGRATIONS_DIR = os.path.join(INSTACE_DIR, "migrations")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
login_manager = LoginManager()
assets = Environment()
cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300})
compress = Compress()
def create_app(test_config=None): # pylint: disable=R0914
def create_app(): # pylint: disable=R0914
"""
Create and configure the main app
"""
app = Flask(__name__, instance_path=os.path.join(USER_DIR, "instance"))
app = Flask(__name__, instance_path=INSTACE_DIR)
app.config.from_pyfile("config.py")
# Get environment variables
load_dotenv(os.path.join(USER_DIR, ".env"))
print("Loaded environment variables")
# DATABASE
db.init_app(app)
migrate.init_app(app, db)
# Get config file
with open(os.path.join(USER_DIR, "conf.yml"), encoding="utf-8", mode="r") as file:
conf = safe_load(file)
print("Loaded config")
# If database file doesn't exist, create it
if not os.path.exists(os.path.join(INSTACE_DIR, "gallery.sqlite3")):
print("Creating database")
with app.app_context():
db.create_all()
# App configuration
app.config.from_mapping(
SECRET_KEY=os.environ.get("FLASK_SECRET"),
DATABASE=os.path.join(app.instance_path, "gallery.sqlite3"),
UPLOAD_FOLDER=os.path.join(USER_DIR, "uploads"),
ALLOWED_EXTENSIONS=conf["upload"]["allowed-extensions"],
MAX_CONTENT_LENGTH=1024 * 1024 * conf["upload"]["max-size"],
ADMIN_CONF=conf["admin"],
UPLOAD_CONF=conf["upload"],
WEBSITE_CONF=conf["website"],
)
register_user = User(
username=app.config["ADMIN_CONF"]["username"],
email=app.config["ADMIN_CONF"]["email"],
password=generate_password_hash("changeme!", method="sha256"),
)
db.session.add(register_user)
db.session.commit()
if test_config is None:
app.config.from_pyfile("config.py", silent=True)
else:
app.config.from_mapping(test_config)
print(
"""
####################################################
# DEFAULY ADMIN USER GENERATED WITH GIVEN USERNAME #
# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #
# PLEASE UPDATE IT IN THE SETTINGS! #
####################################################
"""
)
# Check if migrations directory exists, if not create it
with app.app_context():
if not os.path.exists(MIGRATIONS_DIR):
print("Creating migrations directory")
migrate_init(directory=MIGRATIONS_DIR)
# Check if migrations are up to date
with app.app_context():
print("Checking for schema changes...")
migrate_migrate(directory=MIGRATIONS_DIR)
migrate_upgrade(directory=MIGRATIONS_DIR)
# LOGIN MANAGER
# can also set session_protection to "strong"
# this would protect against session hijacking
login_manager.init_app(app)
login_manager.login_view = "gallery.index"
login_manager.session_protection = "strong"
@login_manager.user_loader
def load_user(user_id):
return db_session.query(db.Users).filter_by(alt_id=user_id).first()
return User.query.filter_by(alt_id=user_id).first()
@login_manager.unauthorized_handler
def unauthorized():
@ -82,23 +90,12 @@ def create_app(test_config=None): # pylint: disable=R0914
msg = "You are not authorized to view this page!!!!"
return render_template("error.html", error=error, msg=msg), error
lib = Bundle(
"lib/*.js", filters="jsmin", output="gen/lib.js", depends="lib/*.js"
)
scripts = Bundle(
"js/*.js", filters="jsmin", output="gen/index.js", depends="js/*.js"
)
styles = Bundle(
"sass/*.sass", filters="libsass, cssmin", output="gen/styles.css", depends='sass/**/*.sass'
)
assets.register("lib", lib)
assets.register("js", scripts)
assets.register("styles", styles)
# Error handlers, if the error is not a HTTP error, return 500
# ERROR HANDLERS
@app.errorhandler(Exception)
def error_page(err): # noqa
"""
Error handlers, if the error is not a HTTP error, return 500
"""
if not isinstance(err, HTTPException):
abort(500)
return (
@ -106,30 +103,34 @@ def create_app(test_config=None): # pylint: disable=R0914
err.code,
)
# Load login, registration and logout manager
from gallery import auth
# ASSETS
assets.init_app(app)
scripts = Bundle("js/*.js", filters="jsmin", output="gen/js.js", depends="js/*.js")
styles = Bundle(
"sass/*.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
)
assets.register("scripts", scripts)
assets.register("styles", styles)
# BLUEPRINTS
app.register_blueprint(auth.blueprint)
# Load the API
from gallery import api
app.register_blueprint(api.blueprint)
# Load the different views
from gallery.views import index, image, group, settings, profile
app.register_blueprint(index.blueprint)
app.register_blueprint(image.blueprint)
app.register_blueprint(group.blueprint)
app.register_blueprint(profile.blueprint)
app.register_blueprint(settings.blueprint)
# Log to file that the app has started
logging.info("Gallery started successfully!")
# Initialize extensions and return app
assets.init_app(app)
# CACHE AND COMPRESS
cache.init_app(app)
compress.init_app(app)
# Yupee! We got there :3
print("Done!")
logging.info("Gallery started successfully!")
return app

View file

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

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

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 () {
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.' +
'<br>Made by Fluffy and others with ❤️');
}

View file

@ -1,14 +1,14 @@
function addNotification(notificationText, notificationLevel) {
let notificationContainer = document.querySelector('.notifications');
const notificationContainer = document.querySelector('.notifications');
// Set the different icons for the different notification levels
let successIcon = '<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>';
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>';
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 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 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 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 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
let notification = document.createElement('div');
const notification = document.createElement('div');
notification.classList.add('sniffle__notification');
notification.onclick = function() {
if (notification) {
@ -21,7 +21,7 @@ function addNotification(notificationText, notificationLevel) {
};
// Create icon element and append to notification
let iconElement = document.createElement('span');
const iconElement = document.createElement('span');
iconElement.classList.add('sniffle__notification-icon');
notification.appendChild(iconElement);
@ -41,7 +41,7 @@ function addNotification(notificationText, notificationLevel) {
}
// Create text element and append to notification
let description = document.createElement('span');
const description = document.createElement('span');
description.classList.add('sniffle__notification-text');
description.innerHTML = notificationText;
notification.appendChild(description);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -74,13 +74,3 @@ $breakpoint: 800px
--breakpoint: 800px
// I have no clue if its webassets or libsass thats doing this shit
// But one of them is trying to "correct" the path, and 404-ing the
// font, so enjoy this path fuckery
@font-face
font-family: 'Rubik'
src: url('../../../../static/fonts/Rubik.ttf') format('truetype')
font-style: normal
font-display: swap
font-weight: 300 900

View file

@ -15,7 +15,7 @@
}
}
{% if current_user.id == group.author_id %}
{% if current_user.id == group.author.id %}
function groupDelete() {
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
@ -223,7 +223,7 @@
<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>
<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>
<p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p>
<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>
</button>
</div>
{% if current_user.id == group.author_id %}
{% if current_user.id == group.author.id %}
<div>
<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>
@ -249,14 +249,14 @@
<div class="banner-small">
<div class="banner-content">
<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>
<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>
</button>
</div>
{% if current_user.id == group.author_id %}
{% if current_user.id == group.author.id %}
<div>
<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>

View file

@ -27,7 +27,7 @@
}
}
{% if current_user.id == image.author_id %}
{% if current_user.id == image.author.id %}
function imageDelete() {
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
@ -85,10 +85,6 @@
<span></span>
</div>
<div class="image-fullscreen" onclick="imageFullscreenOff()">
<img src="" alt="{{ image.alt }}"/>
</div>
<div class="image-grid">
<div class="image-block">
<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>
</a>
</div>
{% if current_user.id == image.author_id %}
{% if current_user.id == image.author.id %}
<div>
<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>
@ -145,22 +141,6 @@
</div>
<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-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>
@ -175,12 +155,18 @@
<table>
<tr>
<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>
<td>Upload date</td>
<td><span class="time">{{ image.created_at }}</span></td>
</tr>
{% if image.description %}
<tr>
<td>Description</td>
<td>{{ image.description }}</td>
</tr>
{% endif %}
</table>
<div class="img-colours">
{% for col in image.colours %}

View file

@ -30,11 +30,12 @@
type="image/svg+xml"
media="(prefers-color-scheme: dark)"/>
{% assets "lib" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
<link
rel="prefetch"
href="{{url_for('static', filename='fonts/font.css')}}"
type="stylesheet"/>
{% assets "js" %}
{% assets "scripts" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
@ -69,7 +70,7 @@
<div class="navigation">
<!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">-->
<a href="{{url_for('gallery.index')}}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}">
<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>
<span class="tool-tip">
Home
@ -77,7 +78,7 @@
</span>
</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>
<span class="tool-tip">
Groups
@ -86,66 +87,66 @@
</a>
{% if current_user.is_authenticated %}
<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>
<span class="tool-tip">
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>
</span>
</button>
<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>
<span class="tool-tip">
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>
</span>
</button>
{% endif %}
<span class="navigation-spacer"></span>
{% if current_user.is_authenticated %}
<a href="{{url_for('profile.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}">
<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">
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>
</span>
</a>
<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>
<span class="tool-tip">
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>
</span>
</a>
<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>
<span class="tool-tip">
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>
</span>
</a>
<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>
<span class="tool-tip">
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>
</span>
</a>
{% else %}
<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>
<span class="tool-tip">
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>
</span>
</button>
<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>
<span class="tool-tip">
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>
</span>
</button>
{% endif %}
</div>
{% if current_user.is_authenticated %}
<div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span>
<div class="container">
<span id="dragIndicator"></span>
<h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p>
<form id="uploadForm">
<button class="fileDrop-block" type="button">
<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>
<input type="file" id="file" tab-index="-1"/>
</button>
<div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span>
<div class="container">
<span id="dragIndicator"></span>
<h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p>
<form id="uploadForm">
<button class="fileDrop-block" type="button">
<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>
<input type="file" id="file" tab-index="-1"/>
</button>
<input class="input-block" type="text" placeholder="alt" id="alt"/>
<input class="input-block" type="text" placeholder="description" id="description"/>
<input class="input-block" type="text" placeholder="tags" id="tags"/>
<button class="btn-block" type="submit">Upload</button>
</form>
<div class="upload-jobs"></div>
<input class="input-block" type="text" placeholder="alt" id="alt"/>
<input class="input-block" type="text" placeholder="description" id="description"/>
<input class="input-block" type="text" placeholder="tags" id="tags"/>
<button class="btn-block primary" type="submit">Upload</button>
</form>
<div class="upload-jobs"></div>
</div>
</div>
</div>
{% endif %}
<div class="content">

View file

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

View file

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

View file

@ -42,7 +42,7 @@ def shutter(value):
"""
Formats the shutter speed into a standard format
"""
return str(value) + "s"
return str(value) + " s"
def focal_length(value):
@ -50,16 +50,18 @@ def focal_length(value):
Formats the focal length into a standard format
"""
try:
return str(value[0] / value[1]) + "mm"
calculated = value[0] / value[1]
except TypeError:
return str(value) + "mm"
calculated = value
return str(calculated) + " mm"
def exposure(value):
"""
Formats the exposure value into a standard format
"""
return str(value) + "EV"
return str(value) + " EV"
def color_space(value):
@ -78,28 +80,28 @@ def flash(value):
Maps the value of the flash to a human readable format
"""
value_map = {
0: "Flash did not fire",
1: "Flash fired",
0: "Did not fire",
1: "Fired",
5: "Strobe return light not detected",
7: "Strobe return light detected",
9: "Flash fired, compulsory flash mode",
13: "Flash fired, compulsory flash mode, return light not detected",
15: "Flash fired, compulsory flash mode, return light detected",
16: "Flash did not fire, compulsory flash mode",
24: "Flash did not fire, auto mode",
25: "Flash fired, auto mode",
29: "Flash fired, auto mode, return light not detected",
31: "Flash fired, auto mode, return light detected",
32: "No flash function",
65: "Flash fired, red-eye reduction mode",
69: "Flash fired, red-eye reduction mode, return light not detected",
71: "Flash fired, red-eye reduction mode, return light detected",
73: "Flash fired, compulsory flash mode, red-eye reduction mode",
77: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",
79: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",
89: "Flash fired, auto mode, red-eye reduction mode",
93: "Flash fired, auto mode, return light not detected, red-eye reduction mode",
95: "Flash fired, auto mode, return light detected, red-eye reduction mode",
9: "Fired, compulsory",
13: "Fired, compulsory, return light not detected",
15: "Fired, compulsory, return light detected",
16: "Did not fire, compulsory",
24: "Did not fire, auto mode",
25: "Fired, auto mode",
29: "Fired, auto mode, return light not detected",
31: "Fired, auto mode, return light detected",
32: "No function",
65: "Fired, red-eye reduction mode",
69: "Fired, red-eye reduction mode, return light not detected",
71: "Fired, red-eye reduction mode, return light detected",
73: "Fired, compulsory, red-eye reduction mode",
77: "Fired, compulsory, red-eye reduction mode, return light not detected",
79: "Fired, compulsory, red-eye reduction mode, return light detected",
89: "Fired, auto mode, red-eye reduction mode",
93: "Fired, auto mode, return light not detected, red-eye reduction mode",
95: "Fired, auto mode, return light detected, red-eye reduction mode",
}
try:
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
"""
value_map = {
0: "Auto white balance",
1: "Manual white balance",
0: "Auto",
1: "Manual",
}
try:
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
"""
value_map = {
0: "Auto exposure",
1: "Manual exposure",
0: "Auto",
1: "Manual",
2: "Auto bracket",
}
try:
@ -386,4 +388,34 @@ def pixel_dimension(value):
"""
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"],
}
CAMERA_MAPPING = {
"Model": ["Model"],
"Make": ["Make"],
"Model": ["Model", "title"],
"Make": ["Manifacturer", "title"],
"BodySerialNumber": ["Camera Type"],
"LensMake": ["Lens Make"],
"LenseModel": ["Lens Model"],
"LensMake": ["Lens Make", "title"],
"LensModel": ["Lens Model", "title"],
"LensSpecification": ["Lens Specification", "lens_specification"],
"ComponentsConfiguration": ["Components Configuration", "components_configuration"],
"DateTime": ["Date Processed", "date_format"],
"DateTimeDigitized": ["Time Digitized", "date_format"],
"DateTime": ["Date and Time", "date_format"],
"DateTimeOriginal": ["Date and Time (Original)", "date_format"],
"DateTimeDigitized": ["Date and Time (Digitized)", "date_format"],
"OffsetTime": ["Time Offset"],
"OffsetTimeOriginal": ["Time Offset - Original"],
"OffsetTimeDigitized": ["Time Offset - Digitized"],
"DateTimeOriginal": ["Date Original", "date_format"],
"FNumber": ["F-Stop", "fnumber"],
"OffsetTimeOriginal": ["Time Offset (Original)"],
"OffsetTimeDigitized": ["Time Offset (Digitized)"],
"FNumber": ["FNumber", "fnumber"],
"FocalLength": ["Focal Length", "focal_length"],
"FocalLengthIn35mmFilm": ["Focal Length (35mm format)", "focal_length"],
"FocalLengthIn35mmFilm": ["Focal Length in 35mm format", "focal_length"],
"MaxApertureValue": ["Max Aperture", "fnumber"],
"ApertureValue": ["Aperture", "fnumber"],
"ShutterSpeedValue": ["Shutter Speed", "shutter"],
@ -41,6 +41,8 @@ CAMERA_MAPPING = {
"MeteringMode": ["Metering Mode", "metering_mode"],
"LightSource": ["Light Source", "light_source"],
"SceneCaptureType": ["Scene Capture Type", "scene_capture_type"],
"SubjectDistance": ["Subject Distance", "subject_distance"],
"SubjectDistanceRange": ["Subject Distance Range", "subject_distance_range"],
}
SOFTWARE_MAPPING = {
"Software": ["Software"],

View file

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

View file

@ -2,16 +2,12 @@
Onlylegs - Image View
"""
from math import ceil
from flask import Blueprint, abort, render_template, url_for, current_app
from sqlalchemy.orm import sessionmaker
from gallery import db
from flask import Blueprint, render_template, url_for, current_app
from gallery.models import Post, GroupJunction, Group
from gallery.extensions import db
blueprint = Blueprint("image", __name__, url_prefix="/image")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/<int:image_id>")
@ -20,45 +16,36 @@ def image(image_id):
Image view, shows the image and its metadata
"""
# Get the image, if it doesn't exist, 404
image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first()
if not image:
abort(404, "Image not found :<")
image = db.get_or_404(Post, image_id, description="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 the image's groups
# Get all groups the image is in
groups = (
db_session.query(db.GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id)
GroupJunction.query.with_entities(GroupJunction.group_id)
.filter(GroupJunction.post_id == image_id)
.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 = []
for group in groups:
image.groups.append(
db_session.query(db.Groups.id, db.Groups.name)
.filter(db.Groups.id == group[0])
Group.query.with_entities(Group.id, Group.name)
.filter(Group.id == group[0])
.first()
)
# Get the next and previous images
# Check if there is a group ID set
next_url = (
db_session.query(db.Posts.id)
.filter(db.Posts.id > image_id)
.order_by(db.Posts.id.asc())
Post.query.with_entities(Post.id)
.filter(Post.id > image_id)
.order_by(Post.id.asc())
.first()
)
prev_url = (
db_session.query(db.Posts.id)
.filter(db.Posts.id < image_id)
.order_by(db.Posts.id.desc())
Post.query.with_entities(Post.id)
.filter(Post.id < image_id)
.order_by(Post.id.desc())
.first()
)
@ -69,7 +56,7 @@ def image(image_id):
prev_url = url_for("image.image", image_id=prev_url[0])
# 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"]
# 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
for j in total_images[i * limit : (i + 1) * limit]:
# Is our image in this chunk?
if image_id in j:
if not image_id > j[-1]:
return_page = i + 1
break

View file

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

View file

@ -5,16 +5,13 @@ from flask import Blueprint, render_template, request
from werkzeug.exceptions import abort
from flask_login import current_user
from sqlalchemy.orm import sessionmaker
from gallery import db
from gallery.models import Post, User
blueprint = Blueprint("profile", __name__, url_prefix="/profile")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/profile")
@blueprint.route("/")
def profile():
"""
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!")
# 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:
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)

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.
[[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]]
name = "astroid"
version = "2.15.1"
version = "2.15.2"
description = "An abstract syntax tree for Python with inference support."
category = "main"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "astroid-2.15.1-py3-none-any.whl", hash = "sha256:89860bda98fe2bbd1f5d262229be7629d778ce280de68d95d4a73d1f592ad268"},
{file = "astroid-2.15.1.tar.gz", hash = "sha256:af4e0aff46e2868218502789898269ed95b663fba49e65d91c1e09c966266c34"},
{file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"},
{file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"},
]
[package.dependencies]
@ -279,6 +301,39 @@ files = [
Flask = ">=1.0.4"
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]]
name = "greenlet"
version = "2.0.2"
@ -376,14 +431,14 @@ tornado = ["tornado (>=0.2)"]
[[package]]
name = "importlib-metadata"
version = "6.1.0"
version = "6.2.1"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_metadata-6.1.0-py3-none-any.whl", hash = "sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"},
{file = "importlib_metadata-6.1.0.tar.gz", hash = "sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20"},
{file = "importlib_metadata-6.2.1-py3-none-any.whl", hash = "sha256:f65e478a7c2177bd19517a3a15dac094d253446d8690c5f3e71e735a04312374"},
{file = "importlib_metadata-6.2.1.tar.gz", hash = "sha256:5a66966b39ff1c14ef5b2d60c1d842b0141fefff0f4cc6365b4bc9446c652807"},
]
[package.dependencies]
@ -394,6 +449,25 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker
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)"]
[[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]]
name = "isort"
version = "5.12.0"
@ -514,6 +588,26 @@ files = [
{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]]
name = "markupsafe"
version = "2.1.2"
@ -684,18 +778,18 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-
[[package]]
name = "pylint"
version = "2.17.1"
version = "2.17.2"
description = "python code static checker"
category = "main"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "pylint-2.17.1-py3-none-any.whl", hash = "sha256:8660a54e3f696243d644fca98f79013a959c03f979992c1ab59c24d3f4ec2700"},
{file = "pylint-2.17.1.tar.gz", hash = "sha256:d4d009b0116e16845533bc2163493d6681846ac725eab8ca8014afb520178ddd"},
{file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"},
{file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"},
]
[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\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
@ -796,53 +890,53 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
[[package]]
name = "sqlalchemy"
version = "2.0.8"
version = "2.0.9"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "SQLAlchemy-2.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42207a5237d8b211df1c04da4a8700bb9f366f5d7be14b1a2e39cc33d330428c"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b6a94bbb1507b3fc75a3c98491f990b80a859eb5227378f6dac908dff74396"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd405d224c0f83f267bfcc430bd275195cb48bb9576f6949911faec16bba1216"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e424a385a234e0bbaac69b6b09894ce99aa285518dd9397846e78214671ec0b4"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6d82bb74a44f103d566f6c86d6db0ed944e99a7da93d6490c1e987a2b91d5613"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c8e546c249f0d23821f4b31e5adfba8e39bc90140d39f7086d4a8422d1f0fa6"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-win32.whl", hash = "sha256:32af8923cfe7b115d40b88604412450d2e4714f4b9a69b1966dfe6012e254789"},
{file = "SQLAlchemy-2.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:692a2b1e0c82c678c84ccb3a9a226e0553dd115be3d5c54b1bb6ef35bc464da9"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8a215d3e9de2af881c4eee60bf2b3576fb2eeb591a9af37ff2919cd74b68f162"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1906c954ee7287d46877636a66da157fc83553851013e42c862653ddacb380b"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a64345a6f41c8c011df3f498cee8de39d9682ca2de14698b68a50bf3a03f6e5"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4223002ab7faac63384019feb31eab4ae2233753fd06ea95690d4e148b5bf80d"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3a1eb8ed5dc2a7bc554a612a57253f3682aea7e4c54c742153aef6a12fde10ee"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af0683c905c856db70adf6d0f84398785f7be6cb4ffdad20d8e16fe426bdf6e8"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-win32.whl", hash = "sha256:6c5e056a6dc1ce29d5e0e54bc1ce669f3035ec18cb7e5c853122b5fdfe216f8f"},
{file = "SQLAlchemy-2.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:21698ac65afe477400aacb56244b607477c192892e2f7a70492cae1e13f73040"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df6722580ff89295e9d11b781d7a50ff6e48fdfe1c81362000925b4d4cbab31"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8707ba6e783d144209dab0324cc72147a1de34d404a61a08eef96fbb3c8909f"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cc70331b7aed25fed5260cf30a69e42eaf3d887a23a6e4f29a51167ae8a00e1"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6f33587541eb6eb0292e82b314f5ef53159f63f14ae1c08b5ef6195732523afa"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0cdc65cbb95ae6813c51acffa65dd8d8272a52da42e3967fc98788fe185a0e0c"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-win32.whl", hash = "sha256:9a981a65dc0082009399eac6600b0983120bc3b850f3c1ed986f68648d9dbb4c"},
{file = "SQLAlchemy-2.0.8-cp37-cp37m-win_amd64.whl", hash = "sha256:b3bdbd9a5b2788b5c51c4fffa2f875103d3c6d01b8f0dc4abbf075cea11741f5"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3ca04c2ccf6f70278905a371ae6eb61494e8c388e98cba909bc9458ceb73dda5"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5b030f91dd310c0f1d7da0654fcc9d8574fe430693e29c544f5558c2426e08eb"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa72fb1be5e6704d697471eb5cc07bf9cd98689be11ffa724efe2a3666d0f6c1"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf42183f93ad3801bc08c8a111de16802a2c32d16dcb9700daf1a8ae12d9d28"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2590fd682d3b9dbecb2710ba83b28ba9df18e0c3c26c78bb950715566784723"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d6753512214d0de6a5a1c05fe716a7d643c927eb0495341db6526577960cbb55"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-win32.whl", hash = "sha256:cf1447ec09d7c9ef0b3a65cea905690ed78ed88717beaac537271c005b0e4ea2"},
{file = "SQLAlchemy-2.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:b4c73a3537f928ebf687244f2af9d477b3b14640b4472fcb2456ee0193850ba1"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:50fa610ac1e45e9adc161227cccbd5c75f8eef25b2ad4a0e080d48c27023415d"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:337ae79dc63b1c957dd2e9e7eed5235492886e2af45d8df4b28748ee4859b588"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a091b9166ab9d8ff77c3155140105e76df1d585632fb9367a31b517960df78a"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0f74c66b2d0081cee5093150407ec43d23d0d5c70aa5b41f438ead1d39bc8d"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ea2976a98076f44e7e8ab9a90b6dfa357253edddf687294b1bcd852ffe20e526"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d713d8c4b36c4f69eef33a70a4521e37b75a3ac9c86ee96626c4b052337978cc"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-win32.whl", hash = "sha256:81ada150345ef9c709b647e2bcc8f688d62de6b8f06a7544bed76bef328c6636"},
{file = "SQLAlchemy-2.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:68b328500a20f328db6af2c6839eb69e3791b2cd544f5808a4b0123b130c7571"},
{file = "SQLAlchemy-2.0.8-py3-none-any.whl", hash = "sha256:f12cd526188f36f161eb71fb275b06d1879c565b07cf0ab2c2408c5059df1fed"},
{file = "SQLAlchemy-2.0.8.tar.gz", hash = "sha256:250e158a1f36c965dde1f949366eae9a57504a8cd7a4d968e66c2d0b3c18198d"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f005245e1cb9b8ca53df73ee85e029ac43155e062405015e49ec6187a2e3fb44"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:34eb96c1de91d8f31e988302243357bef3f7785e1b728c7d4b98bd0c117dafeb"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e472e9627882f2d75b87ff91c5a2bc45b31a226efc7cc0a054a94fffef85862"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-win32.whl", hash = "sha256:0a865b5ec4ba24f57c33b633b728e43fde77b968911a6046443f581b25d29dd9"},
{file = "SQLAlchemy-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:6e84ab63d25d8564d7a8c05dc080659931a459ee27f6ed1cf4c91f292d184038"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db4bd1c4792da753f914ff0b688086b9a8fd78bb9bc5ae8b6d2e65f176b81eb9"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5363a1c65fde7b7466769d4261126d07d872fc2e816487ae6cec93da604b6b"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc4eeb1737a5a9bdb0c24f4c982319fa6edd23cdee27180978c29cbb026f2bd"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbda1da8d541904ba262825a833c9f619e93cb3fd1156be0a5e43cd54d588dcd"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d5327f54a9c39e7871fc532639616f3777304364a0bb9b89d6033ad34ef6c5f8"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac6a0311fb21a99855953f84c43fcff4bdca27a2ffcc4f4d806b26b54b5cddc9"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-win32.whl", hash = "sha256:d209594e68bec103ad5243ecac1b40bf5770c9ebf482df7abf175748a34f4853"},
{file = "SQLAlchemy-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:865392a50a721445156809c1a6d6ab6437be70c1c2599f591a8849ed95d3c693"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b49f1f71d7a44329a43d3edd38cc5ee4c058dfef4487498393d16172007954b"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a019f723b6c1e6b3781be00fb9e0844bc6156f9951c836ff60787cc3938d76"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9838bd247ee42eb74193d865e48dd62eb50e45e3fdceb0fdef3351133ee53dcf"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:78612edf4ba50d407d0eb3a64e9ec76e6efc2b5d9a5c63415d53e540266a230a"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f61ab84956dc628c8dfe9d105b6aec38afb96adae3e5e7da6085b583ff6ea789"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:07950fc82f844a2de67ddb4e535f29b65652b4d95e8b847823ce66a6d540a41d"},
{file = "SQLAlchemy-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e62c4e762d6fd2901692a093f208a6a6575b930e9458ad58c2a7f080dd6132da"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3e5864eba71a3718236a120547e52c8da2ccb57cc96cecd0480106a0c799c92"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d06e119cf79a3d80ab069f064a07152eb9ba541d084bdaee728d8a6f03fd03d"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee2946042cc7851842d7a086a92b9b7b494cbe8c3e7e4627e27bc912d3a7655e"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f984a190d249769a050634b248aef8991acc035e849d02b634ea006c028fa8"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4780be0f19e5894c17f75fc8de2fe1ae233ab37827125239ceb593c6f6bd1e2"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:68ed381bc340b4a3d373dbfec1a8b971f6350139590c4ca3cb722fdb50035777"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-win32.whl", hash = "sha256:aa5c270ece17c0c0e0a38f2530c16b20ea05d8b794e46c79171a86b93b758891"},
{file = "SQLAlchemy-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:1b69666e25cc03c602d9d3d460e1281810109e6546739187044fc256c67941ef"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6e27189ff9aebfb2c02fd252c629ea58657e7a5ff1a321b7fc9c2bf6dc0b5f3"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8239ce63a90007bce479adf5460d48c1adae4b933d8e39a4eafecfc084e503c"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f759eccb66e6d495fb622eb7f4ac146ae674d829942ec18b7f5a35ddf029597"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246712af9fc761d6c13f4f065470982e175d902e77aa4218c9cb9fc9ff565a0c"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b72dccc5864ea95c93e0a9c4e397708917fb450f96737b4a8395d009f90b868"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:93c78d42c14aa9a9e0866eacd5b48df40a50d0e2790ee377af7910d224afddcf"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-win32.whl", hash = "sha256:f49c5d3c070a72ecb96df703966c9678dda0d4cb2e2736f88d15f5e1203b4159"},
{file = "SQLAlchemy-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:4c3020afb144572c7bfcba9d7cce57ad42bff6e6115dffcfe2d4ae6d444a214f"},
{file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"},
{file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"},
]
[package.dependencies]
@ -1042,4 +1136,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "58c3430743ce1cfd8e5b89db371a0d454a478cbe79ced08c645b4628980ca9f1"
content-hash = "88387c49c901feebd4685ee75f6b79c0bbc8cc9c1a64798cd0394b140e165603"

View file

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