Merge pull request #19 from Fluffy-Bean/unstable

Unstable
This commit is contained in:
Michał 2023-04-08 20:50:02 +01:00 committed by GitHub
commit 3c96c9e807
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1324 additions and 1076 deletions

View file

@ -12,7 +12,7 @@
<img src="https://img.shields.io/badge/Gitea-34495E?style=for-the-badge&logo=gitea&logoColor=5D9425"> <img src="https://img.shields.io/badge/Gitea-34495E?style=for-the-badge&logo=gitea&logoColor=5D9425">
</a> </a>
<img src="https://img.shields.io/badge/flask-%23000.svg?style=for-the-badge&logo=flask&logoColor=white"> <img src="https://img.shields.io/badge/flask-%23000.svg?style=for-the-badge&logo=flask&logoColor=white">
<img src="https://img.shields.io/badge/sqlite-%2307405e.svg?style=for-the-badge&logo=sqlite&logoColor=white"> <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge">
<a href="https://github.com/Fluffy-Bean/onlylegs/blob/main/LICENSE"> <a href="https://github.com/Fluffy-Bean/onlylegs/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Fluffy-Bean/onlylegs?style=for-the-badge"> <img src="https://img.shields.io/github/license/Fluffy-Bean/onlylegs?style=for-the-badge">
</a> </a>

View file

@ -25,50 +25,52 @@ from sqlalchemy.orm import sessionmaker
from gallery import db from gallery import db
USER_DIR = platformdirs.user_config_dir('onlylegs') USER_DIR = platformdirs.user_config_dir("onlylegs")
db_session = sessionmaker(bind=db.engine) db_session = sessionmaker(bind=db.engine)
db_session = db_session() db_session = db_session()
login_manager = LoginManager() login_manager = LoginManager()
assets = Environment() assets = Environment()
cache = Cache(config={'CACHE_TYPE': 'SimpleCache', 'CACHE_DEFAULT_TIMEOUT': 300}) cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300})
compress = Compress() compress = Compress()
def create_app(test_config=None): def create_app(test_config=None): # pylint: disable=R0914
""" """
Create and configure the main app Create and configure the main app
""" """
app = Flask(__name__, instance_path=os.path.join(USER_DIR, 'instance')) app = Flask(__name__, instance_path=os.path.join(USER_DIR, "instance"))
# Get environment variables # Get environment variables
load_dotenv(os.path.join(USER_DIR, '.env')) load_dotenv(os.path.join(USER_DIR, ".env"))
print("Loaded environment variables") print("Loaded environment variables")
# Get config file # Get config file
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8', mode='r') as file: with open(os.path.join(USER_DIR, "conf.yml"), encoding="utf-8", mode="r") as file:
conf = safe_load(file) conf = safe_load(file)
print("Loaded config") print("Loaded config")
# App configuration # App configuration
app.config.from_mapping( app.config.from_mapping(
SECRET_KEY=os.environ.get('FLASK_SECRET'), SECRET_KEY=os.environ.get("FLASK_SECRET"),
DATABASE=os.path.join(app.instance_path, 'gallery.sqlite3'), DATABASE=os.path.join(app.instance_path, "gallery.sqlite3"),
UPLOAD_FOLDER=os.path.join(USER_DIR, 'uploads'), UPLOAD_FOLDER=os.path.join(USER_DIR, "uploads"),
ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'], ALLOWED_EXTENSIONS=conf["upload"]["allowed-extensions"],
MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'], MAX_CONTENT_LENGTH=1024 * 1024 * conf["upload"]["max-size"],
WEBSITE=conf['website'], ADMIN_CONF=conf["admin"],
UPLOAD_CONF=conf["upload"],
WEBSITE_CONF=conf["website"],
) )
if test_config is None: if test_config is None:
app.config.from_pyfile('config.py', silent=True) app.config.from_pyfile("config.py", silent=True)
else: else:
app.config.from_mapping(test_config) app.config.from_mapping(test_config)
login_manager.init_app(app) login_manager.init_app(app)
login_manager.login_view = 'gallery.index' login_manager.login_view = "gallery.index"
login_manager.session_protection = 'strong' login_manager.session_protection = "strong"
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
@ -77,50 +79,54 @@ def create_app(test_config=None):
@login_manager.unauthorized_handler @login_manager.unauthorized_handler
def unauthorized(): def unauthorized():
error = 401 error = 401
msg = 'You are not authorized to view this page!!!!' msg = "You are not authorized to view this page!!!!"
return render_template('error.html', error=error, msg=msg), error return render_template("error.html", error=error, msg=msg), error
js_pre = Bundle( lib = Bundle(
'js/pre/*.js', "lib/*.js", filters="jsmin", output="gen/lib.js", depends="lib/*.js"
output='gen/pre_packed.js',
depends='js/pre/*.js'
) )
js_post = Bundle( scripts = Bundle(
'js/post/*.js', "js/*.js", filters="jsmin", output="gen/index.js", depends="js/*.js"
output='gen/post_packed.js',
depends='js/post/*.js'
) )
styles = Bundle( styles = Bundle(
'sass/*.sass', "sass/*.sass", filters="libsass, cssmin", output="gen/styles.css", depends='sass/**/*.sass'
filters='libsass',
output='gen/styles.css',
depends='sass/**/*.sass'
) )
assets.register('js_pre', js_pre) assets.register("lib", lib)
assets.register('js_post', js_post) assets.register("js", scripts)
assets.register('styles', styles) assets.register("styles", styles)
# Error handlers, if the error is not a HTTP error, return 500 # Error handlers, if the error is not a HTTP error, return 500
@app.errorhandler(Exception) @app.errorhandler(Exception)
def error_page(err): # noqa def error_page(err): # noqa
if not isinstance(err, HTTPException): if not isinstance(err, HTTPException):
abort(500) abort(500)
return render_template('error.html', error=err.code, msg=err.description), err.code return (
render_template("error.html", error=err.code, msg=err.description),
err.code,
)
# Load login, registration and logout manager # Load login, registration and logout manager
from gallery import auth from gallery import auth
app.register_blueprint(auth.blueprint) app.register_blueprint(auth.blueprint)
# Load the different routes # Load the API
from gallery.views import api, groups, routing, settings from gallery import api
app.register_blueprint(api.blueprint) app.register_blueprint(api.blueprint)
app.register_blueprint(groups.blueprint)
app.register_blueprint(routing.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) app.register_blueprint(settings.blueprint)
# Log to file that the app has started # Log to file that the app has started
logging.info('Gallery started successfully!') logging.info("Gallery started successfully!")
# Initialize extensions and return app # Initialize extensions and return app
assets.init_app(app) assets.init_app(app)

View file

@ -21,26 +21,28 @@ from gallery.utils import metadata as mt
from gallery.utils.generate_image import generate_thumbnail from gallery.utils.generate_image import generate_thumbnail
blueprint = Blueprint('api', __name__, url_prefix='/api') blueprint = Blueprint("api", __name__, url_prefix="/api")
db_session = sessionmaker(bind=db.engine) db_session = sessionmaker(bind=db.engine)
db_session = db_session() db_session = db_session()
@blueprint.route('/file/<file_name>', methods=['GET']) @blueprint.route("/file/<file_name>", methods=["GET"])
def file(file_name): def file(file_name):
""" """
Returns a file from the uploads folder Returns a file from the uploads folder
r for resolution, 400x400 or thumb for thumbnail r for resolution, 400x400 or thumb for thumbnail
""" """
res = request.args.get('r', default=None, type=str) # Type of file (thumb, etc) res = request.args.get("r", default=None, type=str) # Type of file (thumb, etc)
ext = request.args.get('e', default=None, type=str) # File extension ext = request.args.get("e", default=None, type=str) # File extension
file_name = secure_filename(file_name) # Sanitize file name file_name = secure_filename(file_name) # Sanitize file name
# if no args are passed, return the raw file # if no args are passed, return the raw file
if not res and not ext: if not res and not ext:
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name)): if not os.path.exists(
os.path.join(current_app.config["UPLOAD_FOLDER"], file_name)
):
abort(404) abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name) return send_from_directory(current_app.config["UPLOAD_FOLDER"], file_name)
thumb = generate_thumbnail(file_name, res, ext) thumb = generate_thumbnail(file_name, res, ext)
if not thumb: if not thumb:
@ -49,13 +51,13 @@ def file(file_name):
return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb)) return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
@blueprint.route('/upload', methods=['POST']) @blueprint.route("/upload", methods=["POST"])
@login_required @login_required
def upload(): def upload():
""" """
Uploads an image to the server and saves it to the database Uploads an image to the server and saves it to the database
""" """
form_file = request.files['file'] form_file = request.files["file"]
form = request.form form = request.form
# If no image is uploaded, return 404 error # If no image is uploaded, return 404 error
@ -63,41 +65,45 @@ def upload():
return abort(404) return abort(404)
# Get file extension, generate random name and set file path # Get file extension, generate random name and set file path
img_ext = pathlib.Path(form_file.filename).suffix.replace('.', '').lower() img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower()
img_name = "GWAGWA_" + str(uuid4()) img_name = "GWAGWA_" + str(uuid4())
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img_name+'.'+img_ext) img_path = os.path.join(
current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext
)
# Check if file extension is allowed # Check if file extension is allowed
if img_ext not in current_app.config['ALLOWED_EXTENSIONS'].keys(): if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
logging.info('File extension not allowed: %s', img_ext) logging.info("File extension not allowed: %s", img_ext)
abort(403) abort(403)
# Save file # Save file
try: try:
form_file.save(img_path) form_file.save(img_path)
except OSError as err: except OSError as err:
logging.info('Error saving file %s because of %s', img_path, err) logging.info("Error saving file %s because of %s", img_path, err)
abort(500) abort(500)
img_exif = mt.Metadata(img_path).yoink() # Get EXIF data img_exif = mt.Metadata(img_path).yoink() # Get EXIF data
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database # Save to database
query = db.Posts(author_id=current_user.id, query = db.Posts(
filename=img_name + '.' + img_ext, author_id=current_user.id,
mimetype=img_ext, filename=img_name + "." + img_ext,
exif=img_exif, mimetype=img_ext,
colours=img_colors, exif=img_exif,
description=form['description'], colours=img_colors,
alt=form['alt']) description=form["description"],
alt=form["alt"],
)
db_session.add(query) db_session.add(query)
db_session.commit() db_session.commit()
return 'Gwa Gwa' # Return something so the browser doesn't show an error return "Gwa Gwa" # Return something so the browser doesn't show an error
@blueprint.route('/delete/<int:image_id>', methods=['POST']) @blueprint.route("/delete/<int:image_id>", methods=["POST"])
@login_required @login_required
def delete_image(image_id): def delete_image(image_id):
""" """
@ -113,14 +119,16 @@ def delete_image(image_id):
# Delete file # Delete file
try: try:
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'], img.filename)) os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], img.filename))
except FileNotFoundError: except FileNotFoundError:
logging.warning('File not found: %s, already deleted or never existed', img.filename) logging.warning(
"File not found: %s, already deleted or never existed", img.filename
)
# Delete cached files # Delete cached files
cache_path = os.path.join(platformdirs.user_config_dir('onlylegs'), 'cache') cache_path = os.path.join(platformdirs.user_config_dir("onlylegs"), "cache")
cache_name = img.filename.rsplit('.')[0] cache_name = img.filename.rsplit(".")[0]
for cache_file in pathlib.Path(cache_path).glob(cache_name + '*'): for cache_file in pathlib.Path(cache_path).glob(cache_name + "*"):
os.remove(cache_file) os.remove(cache_file)
# Delete from database # Delete from database
@ -134,36 +142,38 @@ def delete_image(image_id):
# Commit all changes # Commit all changes
db_session.commit() db_session.commit()
logging.info('Removed image (%s) %s', image_id, img.filename) logging.info("Removed image (%s) %s", image_id, img.filename)
flash(['Image was all in Le Head!', '1']) flash(["Image was all in Le Head!", "1"])
return 'Gwa Gwa' return "Gwa Gwa"
@blueprint.route('/group/create', methods=['POST']) @blueprint.route("/group/create", methods=["POST"])
@login_required @login_required
def create_group(): def create_group():
""" """
Creates a group Creates a group
""" """
new_group = db.Groups(name=request.form['name'], new_group = db.Groups(
description=request.form['description'], name=request.form["name"],
author_id=current_user.id) description=request.form["description"],
author_id=current_user.id,
)
db_session.add(new_group) db_session.add(new_group)
db_session.commit() db_session.commit()
return ':3' return ":3"
@blueprint.route('/group/modify', methods=['POST']) @blueprint.route("/group/modify", methods=["POST"])
@login_required @login_required
def modify_group(): def modify_group():
""" """
Changes the images in a group Changes the images in a group
""" """
group_id = request.form['group'] group_id = request.form["group"]
image_id = request.form['image'] image_id = request.form["image"]
action = request.form['action'] action = request.form["action"]
group = db_session.query(db.Groups).filter_by(id=group_id).first() group = db_session.query(db.Groups).filter_by(id=group_id).first()
@ -172,28 +182,31 @@ def modify_group():
elif group.author_id != current_user.id: elif group.author_id != current_user.id:
abort(403) abort(403)
if action == 'add': if action == "add":
if not (db_session.query(db.GroupJunction) if not (
.filter_by(group_id=group_id, post_id=image_id) db_session.query(db.GroupJunction)
.first()): .filter_by(group_id=group_id, post_id=image_id)
db_session.add(db.GroupJunction(group_id=group_id, .first()
post_id=image_id)) ):
elif request.form['action'] == 'remove': db_session.add(db.GroupJunction(group_id=group_id, post_id=image_id))
(db_session.query(db.GroupJunction) elif request.form["action"] == "remove":
.filter_by(group_id=group_id, post_id=image_id) (
.delete()) db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id)
.delete()
)
db_session.commit() db_session.commit()
return ':3' return ":3"
@blueprint.route('/group/delete', methods=['POST']) @blueprint.route("/group/delete", methods=["POST"])
def delete_group(): def delete_group():
""" """
Deletes a group Deletes a group
""" """
group_id = request.form['group'] group_id = request.form["group"]
group = db_session.query(db.Groups).filter_by(id=group_id).first() group = db_session.query(db.Groups).filter_by(id=group_id).first()
@ -206,5 +219,5 @@ def delete_group():
db_session.query(db.GroupJunction).filter_by(group_id=group_id).delete() db_session.query(db.GroupJunction).filter_by(group_id=group_id).delete()
db_session.commit() db_session.commit()
flash(['Group yeeted!', '1']) flash(["Group yeeted!", "1"])
return ':3' return ":3"

View file

@ -14,39 +14,39 @@ from sqlalchemy.orm import sessionmaker
from gallery import db from gallery import db
blueprint = Blueprint('auth', __name__, url_prefix='/auth') blueprint = Blueprint("auth", __name__, url_prefix="/auth")
db_session = sessionmaker(bind=db.engine) db_session = sessionmaker(bind=db.engine)
db_session = db_session() db_session = db_session()
@blueprint.route('/login', methods=['POST']) @blueprint.route("/login", methods=["POST"])
def login(): def login():
""" """
Log in a registered user by adding the user id to the session Log in a registered user by adding the user id to the session
""" """
error = [] error = []
username = request.form['username'].strip() username = request.form["username"].strip()
password = request.form['password'].strip() password = request.form["password"].strip()
remember = bool(request.form['remember-me']) remember = bool(request.form["remember-me"])
user = db_session.query(db.Users).filter_by(username=username).first() user = db_session.query(db.Users).filter_by(username=username).first()
if not user or not check_password_hash(user.password, password): if not user or not check_password_hash(user.password, password):
logging.error('Login attempt from %s', request.remote_addr) logging.error("Login attempt from %s", request.remote_addr)
error.append('Username or Password is incorrect!') error.append("Username or Password is incorrect!")
if error: if error:
abort(403) abort(403)
login_user(user, remember=remember) login_user(user, remember=remember)
logging.info('User %s logged in from %s', username, request.remote_addr) logging.info("User %s logged in from %s", username, request.remote_addr)
flash(['Logged in successfully!', '4']) flash(["Logged in successfully!", "4"])
return 'ok', 200 return "ok", 200
@blueprint.route('/register', methods=['POST']) @blueprint.route("/register", methods=["POST"])
def register(): def register():
""" """
Register a new user Register a new user
@ -54,55 +54,58 @@ def register():
error = [] error = []
# Thanks Fennec for reminding me to strip out the whitespace lol # Thanks Fennec for reminding me to strip out the whitespace lol
username = request.form['username'].strip() username = request.form["username"].strip()
email = request.form['email'].strip() email = request.form["email"].strip()
password = request.form['password'].strip() password = request.form["password"].strip()
password_repeat = request.form['password-repeat'].strip() password_repeat = request.form["password-repeat"].strip()
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b') email_regex = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
username_regex = re.compile(r'\b[A-Za-z0-9._-]+\b') username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
# Validate the form # Validate the form
if not username or not username_regex.match(username): if not username or not username_regex.match(username):
error.append('Username is invalid!') error.append("Username is invalid!")
if not email or not email_regex.match(email): if not email or not email_regex.match(email):
error.append('Email is invalid!') error.append("Email is invalid!")
if not password: if not password:
error.append('Password is empty!') error.append("Password is empty!")
elif len(password) < 8: elif len(password) < 8:
error.append('Password is too short! Longer than 8 characters pls') error.append("Password is too short! Longer than 8 characters pls")
if not password_repeat: if not password_repeat:
error.append('Enter password again!') error.append("Enter password again!")
elif password_repeat != password: elif password_repeat != password:
error.append('Passwords do not match!') error.append("Passwords do not match!")
user_exists = db_session.query(db.Users).filter_by(username=username).first() user_exists = db_session.query(db.Users).filter_by(username=username).first()
if user_exists: if user_exists:
error.append('User already exists!') error.append("User already exists!")
# If there are errors, return them # If there are errors, return them
if error: if error:
print(error) print(error)
return jsonify(error), 400 return jsonify(error), 400
register_user = db.Users(username=username, email=email, register_user = db.Users(
password=generate_password_hash(password, method='sha256')) username=username,
email=email,
password=generate_password_hash(password, method="sha256"),
)
db_session.add(register_user) db_session.add(register_user)
db_session.commit() db_session.commit()
logging.info('User %s registered', username) logging.info("User %s registered", username)
return 'ok', 200 return "ok", 200
@blueprint.route('/logout') @blueprint.route("/logout")
@login_required @login_required
def logout(): def logout():
""" """
Clear the current session, including the stored user id Clear the current session, including the stored user id
""" """
logout_user() logout_user()
flash(['Goodbye!!!', '4']) flash(["Goodbye!!!", "4"])
return redirect(url_for('gallery.index')) return redirect(url_for("gallery.index"))

View file

@ -5,27 +5,36 @@ from uuid import uuid4
import os import os
import platformdirs import platformdirs
from sqlalchemy import (create_engine, Column, Integer, String, from sqlalchemy import (
DateTime, ForeignKey, PickleType, func) create_engine,
Column,
Integer,
String,
DateTime,
ForeignKey,
PickleType,
func,
)
from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm import declarative_base, relationship
from flask_login import UserMixin from flask_login import UserMixin
USER_DIR = platformdirs.user_config_dir('onlylegs') USER_DIR = platformdirs.user_config_dir("onlylegs")
DB_PATH = os.path.join(USER_DIR, 'instance', 'gallery.sqlite3') DB_PATH = os.path.join(USER_DIR, "instance", "gallery.sqlite3")
# In the future, I want to add support for other databases # In the future, I want to add support for other databases
engine = create_engine(f'sqlite:///{DB_PATH}', echo=False) engine = create_engine(f"sqlite:///{DB_PATH}", echo=False)
base = declarative_base() base = declarative_base()
class Users (base, UserMixin): # pylint: disable=too-few-public-methods, C0103 class Users(base, UserMixin): # pylint: disable=too-few-public-methods, C0103
""" """
User table User table
Joins with post, groups, session and log Joins with post, groups, session and log
""" """
__tablename__ = 'users'
__tablename__ = "users"
# Gallery used information # Gallery used information
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
@ -34,26 +43,31 @@ class Users (base, UserMixin): # pylint: disable=too-few-public-methods, C0103
username = Column(String, unique=True, nullable=False) username = Column(String, unique=True, nullable=False)
email = Column(String, unique=True, nullable=False) email = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False) password = Column(String, nullable=False)
joined_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102 joined_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
posts = relationship('Posts', backref='users') posts = relationship("Posts", backref="users")
groups = relationship('Groups', backref='users') groups = relationship("Groups", backref="users")
log = relationship('Logs', backref='users') log = relationship("Logs", backref="users")
def get_id(self): def get_id(self):
return str(self.alt_id) return str(self.alt_id)
class Posts (base): # pylint: disable=too-few-public-methods, C0103 class Posts(base): # pylint: disable=too-few-public-methods, C0103
""" """
Post table Post table
Joins with group_junction Joins with group_junction
""" """
__tablename__ = 'posts'
__tablename__ = "posts"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey('users.id')) author_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102 created_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
filename = Column(String, unique=True, nullable=False) filename = Column(String, unique=True, nullable=False)
mimetype = Column(String, nullable=False) mimetype = Column(String, nullable=False)
exif = Column(PickleType, nullable=False) exif = Column(PickleType, nullable=False)
@ -61,66 +75,79 @@ class Posts (base): # pylint: disable=too-few-public-methods, C0103
description = Column(String, nullable=False) description = Column(String, nullable=False)
alt = Column(String, nullable=False) alt = Column(String, nullable=False)
junction = relationship('GroupJunction', backref='posts') junction = relationship("GroupJunction", backref="posts")
class Groups (base): # pylint: disable=too-few-public-methods, C0103
class Groups(base): # pylint: disable=too-few-public-methods, C0103
""" """
Group table Group table
Joins with group_junction Joins with group_junction
""" """
__tablename__ = 'groups'
__tablename__ = "groups"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String, nullable=False) name = Column(String, nullable=False)
description = Column(String, nullable=False) description = Column(String, nullable=False)
author_id = Column(Integer, ForeignKey('users.id')) author_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102 created_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
junction = relationship('GroupJunction', backref='groups') junction = relationship("GroupJunction", backref="groups")
class GroupJunction (base): # pylint: disable=too-few-public-methods, C0103 class GroupJunction(base): # pylint: disable=too-few-public-methods, C0103
""" """
Junction table for posts and groups Junction table for posts and groups
Joins with posts and groups Joins with posts and groups
""" """
__tablename__ = 'group_junction'
__tablename__ = "group_junction"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
date_added = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102 date_added = Column(
group_id = Column(Integer, ForeignKey('groups.id')) DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
post_id = Column(Integer, ForeignKey('posts.id')) )
group_id = Column(Integer, ForeignKey("groups.id"))
post_id = Column(Integer, ForeignKey("posts.id"))
class Logs (base): # pylint: disable=too-few-public-methods, C0103 class Logs(base): # pylint: disable=too-few-public-methods, C0103
""" """
Log table Log table
Joins with user Joins with user
""" """
__tablename__ = 'logs'
__tablename__ = "logs"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey("users.id"))
ip_address = Column(String, nullable=False) ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False) code = Column(Integer, nullable=False)
note = Column(String, nullable=False) note = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102 created_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
class Bans (base): # pylint: disable=too-few-public-methods, C0103 class Bans(base): # pylint: disable=too-few-public-methods, C0103
""" """
Bans table Bans table
""" """
__tablename__ = 'bans'
__tablename__ = "bans"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
ip_address = Column(String, nullable=False) ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False) code = Column(Integer, nullable=False)
note = Column(String, nullable=False) note = Column(String, nullable=False)
banned_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102 banned_at = Column(
DateTime, nullable=False, server_default=func.now() # pylint: disable=E1102
)
# check if database file exists, if not create it # check if database file exists, if not create it
if not os.path.isfile(DB_PATH): if not os.path.isfile(DB_PATH):
base.metadata.create_all(engine) base.metadata.create_all(engine)
print('Database created') print("Database created")

4
gallery/langs/gb.json Normal file
View file

@ -0,0 +1,4 @@
{
"IMAGES_UPLOADED": "%s images uploaded!",
"DONT USE THIS": "variable:format(data), jinja2 doesnt use the same method as Django does, odd"
}

Binary file not shown.

View file

@ -1,11 +1,3 @@
let webpSupport = false;
try {
new Image().src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';
webpSupport = true;
} catch (e) {
webpSupport = false;
}
// fade in images // fade in images
function imgFade(obj, time = 250) { function imgFade(obj, time = 250) {
obj.style.transition = `opacity ${time}ms`; obj.style.transition = `opacity ${time}ms`;
@ -14,6 +6,7 @@ function imgFade(obj, time = 250) {
// Lazy load images when they are in view // Lazy load images when they are in view
function loadOnView() { function loadOnView() {
const lazyLoad = document.querySelectorAll('#lazy-load'); const lazyLoad = document.querySelectorAll('#lazy-load');
const webpSupport = checkWebpSupport();
for (let i = 0; i < lazyLoad.length; i++) { for (let i = 0; i < lazyLoad.length; i++) {
let image = lazyLoad[i]; let image = lazyLoad[i];
@ -71,11 +64,10 @@ window.onload = function () {
infoButton.classList.add('show'); infoButton.classList.add('show');
} }
infoButton.onclick = function () { infoButton.onclick = function () {
popUpShow('OnlyLegs on Flask', popUpShow('OnlyLegs',
'Using <a href="https://phosphoricons.com/">Phosphoricons</a> and ' + '<a href="https://github.com/Fluffy-Bean/onlylegs">V23.04.08</a> ' +
'<a href="https://www.gent.media/manrope">Manrope</a> <br>' + 'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' +
'Made by Fluffy and others with ❤️ <br>' + '<br>Made by Fluffy and others with ❤️');
'<a href="https://github.com/Fluffy-Bean/onlylegs">V23.04.05</a>');
} }
} }
}; };

View file

@ -24,6 +24,7 @@ function addNotification(notificationText, notificationLevel) {
let iconElement = document.createElement('span'); let iconElement = document.createElement('span');
iconElement.classList.add('sniffle__notification-icon'); iconElement.classList.add('sniffle__notification-icon');
notification.appendChild(iconElement); notification.appendChild(iconElement);
// Set the icon based on the notification level, not pretty but it works :3 // Set the icon based on the notification level, not pretty but it works :3
if (notificationLevel === 1) { if (notificationLevel === 1) {
notification.classList.add('success'); notification.classList.add('success');
@ -45,11 +46,6 @@ function addNotification(notificationText, notificationLevel) {
description.innerHTML = notificationText; description.innerHTML = notificationText;
notification.appendChild(description); notification.appendChild(description);
// Create span to show time remaining
let timer = document.createElement('span');
timer.classList.add('sniffle__notification-time');
notification.appendChild(timer);
// Append notification to container // Append notification to container
notificationContainer.appendChild(notification); notificationContainer.appendChild(notification);
setTimeout(function() { notification.classList.add('show'); }, 5); setTimeout(function() { notification.classList.add('show'); }, 5);
@ -65,5 +61,3 @@ function addNotification(notificationText, notificationLevel) {
} }
}, 5000); }, 5000);
} }
// uwu

View file

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

View file

@ -0,0 +1,18 @@
{
"name": "OnlyLegs",
"short_name": "OnlyLegs",
"start_url": "/",
"display": "standalone",
"background_color": "#151515",
"theme_color": "#151515",
"description": "A gallery built for fast and simple image management!",
"icons": [
{
"src": "icon.png",
"sizes": "621x621",
"type": "image/png"
}
],
"splash_pages": null
}

View file

@ -4,20 +4,6 @@
100% 100%
background-position: 468px 0 background-position: 468px 0
@keyframes notificationTimeout
0%
left: -100%
height: 3px
90%
left: 0%
height: 3px
95%
left: 0%
height: 0
100%
left: 0%
height: 0
@keyframes uploadingLoop @keyframes uploadingLoop
0% 0%
left: -100% left: -100%

View file

@ -4,6 +4,22 @@
position: relative position: relative
color: RGB($fg-white) color: RGB($fg-white)
.link
padding: 0.1rem 0.3rem
text-decoration: none
font-weight: 500
background-color: RGB($fg-white)
color: RGB($fg-black)
border-radius: $rad-inner
cursor: pointer
&:hover
background-color: RGB($fg-black)
color: RGB($fg-white)
&::after &::after
content: '' content: ''
@ -73,23 +89,20 @@
.banner-header .banner-header
grid-area: header grid-area: header
white-space: nowrap
text-overflow: ellipsis
overflow: hidden
text-align: left text-align: left
font-size: 6.9rem font-size: 6.9rem
font-weight: 800 font-weight: 700
color: RGB($primary) color: RGB($primary)
.banner-info .banner-info
grid-area: info grid-area: info
font-size: 1rem font-size: 1rem
font-weight: 600 font-weight: 400
.banner-subtitle .banner-subtitle
grid-area: subtitle grid-area: subtitle
font-size: 1rem font-size: 1rem
font-weight: 600 font-weight: 400
.pill-row .pill-row
margin-top: auto margin-top: auto
@ -130,14 +143,14 @@
text-overflow: ellipsis text-overflow: ellipsis
overflow: hidden overflow: hidden
text-align: left text-align: left
font-weight: 800 font-weight: 700
font-size: 1.5rem font-size: 1.5rem
color: RGB($primary) color: RGB($primary)
.banner-info .banner-info
font-size: 0.9rem font-size: 0.9rem
font-weight: 600 font-weight: 400
.pill-row .pill-row
margin-left: auto margin-left: auto
@ -161,19 +174,21 @@
flex-direction: column flex-direction: column
justify-content: center justify-content: center
align-items: center align-items: center
gap: 0.25rem gap: 1rem
.banner-header .banner-header
font-size: 3rem
text-align: center text-align: center
font-size: 2.5rem
.banner-info, .banner-info
.banner-subtitle
font-size: 1.1rem font-size: 1.1rem
text-align: center text-align: center
.banner-subtitle
display: none
.pill-row .pill-row
margin-top: 1rem margin-top: 0rem
.banner-small .banner-small
.banner-content .banner-content

View file

@ -9,10 +9,10 @@
outline: 2px solid RGBA($color, 0.3) outline: 2px solid RGBA($color, 0.3)
.btn-block .btn-block
padding: 0.5rem 1rem padding: 0.4rem 0.7rem
width: auto width: auto
min-height: 2.5rem min-height: 2.3rem
display: flex display: flex
justify-content: center justify-content: center
@ -22,7 +22,7 @@
position: relative position: relative
font-size: 1rem font-size: 1rem
font-weight: 600 font-weight: 400
text-align: center text-align: center
background-color: transparent background-color: transparent
@ -68,16 +68,16 @@
label label
font-size: 1rem font-size: 1rem
font-weight: 600 font-weight: 400
text-align: left text-align: left
color: RGB($fg-white) color: RGB($fg-white)
.input-block .input-block
padding: 0.5rem 1rem padding: 0.4rem 0.7rem
width: auto width: auto
min-height: 2.5rem min-height: 2.3rem
display: flex display: flex
justify-content: flex-start justify-content: flex-start
@ -86,7 +86,7 @@
position: relative position: relative
font-size: 1rem font-size: 1rem
font-weight: 600 font-weight: 400
text-align: left text-align: left
background-color: RGBA($white, 0.1) background-color: RGBA($white, 0.1)
@ -116,7 +116,7 @@
padding: 1rem 1.25rem padding: 1rem 1.25rem
width: 100% width: 100%
min-height: 2.5rem min-height: 2.3rem
display: flex display: flex
flex-direction: column flex-direction: column
@ -127,7 +127,7 @@
position: relative position: relative
font-size: 1rem font-size: 1rem
font-weight: 600 font-weight: 400
text-align: center text-align: center
background-color: RGBA($white, 0.1) background-color: RGBA($white, 0.1)

View file

@ -16,9 +16,28 @@
display: flex display: flex
background-color: RGB($bg-100) background-color: RGB($bg-200)
border-radius: $rad border-radius: $rad
.pill-text
margin: 0
padding: 0.5rem
width: auto
height: 2.5rem
display: flex
justify-content: center
align-items: center
position: relative
text-align: center
font-size: 1rem
font-weight: 400
color: RGB($fg-white)
.pill-item .pill-item
margin: 0 margin: 0
padding: 0.5rem padding: 0.5rem
@ -45,11 +64,6 @@
color: RGB($primary) color: RGB($primary)
.tool-tip
opacity: 1
top: -2.3rem
transform: translateX(calc(-50% + 1.25rem ))
.pill__critical .pill__critical
color: RGB($critical) color: RGB($critical)
@ -72,47 +86,6 @@
&:hover &:hover
color: RGB($fg-white) color: RGB($fg-white)
.tool-tip
margin: 0
padding: 0.35rem 0.7rem
width: auto
display: block
position: absolute
top: -1.7rem
left: 0
transform: translateX(calc(-50% + 1.25rem ))
font-size: 0.9rem
font-weight: 700
background-color: #000000
color: RGB($fg-white)
opacity: 0
border-radius: $rad-inner
transition: opacity 0.2s cubic-bezier(.76,0,.17,1), top 0.2s cubic-bezier(.76,0,.17,1)
pointer-events: none
svg
margin: 0
font-size: 1rem
width: 0.75rem
height: 0.75rem
display: block
position: absolute
left: 50%
bottom: -0.46rem
transform: translateX(-50%)
color: #000000
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.tool-tip .tool-tip
display: none display: none

View file

@ -1,7 +1,21 @@
@keyframes notificationTimeout
0%
left: -100%
height: 3px
90%
left: 0%
height: 3px
95%
left: 0%
height: 0
100%
left: 0%
height: 0
@mixin notification($color) @mixin notification($color)
color: RGB($color) color: RGB($color)
.sniffle__notification-time &::after
background-color: RGB($color) background-color: RGB($color)
.notifications .notifications
@ -44,6 +58,21 @@
transition: all 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55) transition: all 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
&::after
content: ""
width: 450px
height: 3px
position: absolute
bottom: 0px
left: 0px
background-color: RGB($fg-white)
z-index: +2
animation: notificationTimeout 5.1s linear
&.success &.success
@include notification($success) @include notification($success)
&.warning &.warning
@ -100,21 +129,6 @@
line-height: 1 line-height: 1
text-align: left text-align: left
.sniffle__notification-time
margin: 0
padding: 0
width: 450px
height: 3px
position: absolute
bottom: 0px
left: 0px
background-color: RGB($fg-white)
animation: notificationTimeout 5.1s linear
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.notifications .notifications
width: calc(100vw - 0.6rem) width: calc(100vw - 0.6rem)

View file

@ -70,8 +70,8 @@
top: 0 top: 0
font-size: 1.5rem font-size: 1.5rem
font-weight: 800 font-weight: 700
text-align: center text-align: left
color: RGB($fg-white) color: RGB($fg-white)
@ -81,8 +81,8 @@
width: 100% width: 100%
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 400
text-align: center text-align: left
color: RGB($fg-white) color: RGB($fg-white)
@ -96,7 +96,6 @@
a, .link a, .link
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 500
text-align: center
line-height: 1 line-height: 1
color: RGB($primary) color: RGB($primary)

View file

@ -57,9 +57,9 @@
position: absolute position: absolute
bottom: 0 bottom: 0
left: -400px left: -25rem
width: 400px width: 25rem
height: 100% height: 100%
display: flex display: flex
@ -213,7 +213,7 @@
height: 95% height: 95%
left: 0 left: 0
bottom: calc(-100vh + 3.5rem) bottom: -100vh
border-radius: $rad $rad 0 0 border-radius: $rad $rad 0 0

View file

@ -1,12 +1,12 @@
.gallery-grid .gallery-grid
margin: 0 margin: 0
padding: 0.5rem padding: 0.65rem
width: 100% width: 100%
display: grid display: grid
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
gap: 0.5rem gap: 0.65rem
.gallery-item .gallery-item
margin: 0 margin: 0
@ -17,9 +17,11 @@
position: relative position: relative
border-radius: $rad-inner border-radius: $rad-inner
box-shadow: 0 0.15rem 0.4rem 0.1rem RGBA($bg-100, 0.4)
box-sizing: border-box box-sizing: border-box
overflow: hidden overflow: hidden
transition: box-shadow 0.2s cubic-bezier(.79, .14, .15, .86)
.image-filter .image-filter
margin: 0 margin: 0
@ -37,11 +39,11 @@
flex-direction: column flex-direction: column
justify-content: flex-end justify-content: flex-end
background-image: linear-gradient(to top, rgba($bg-100, 0.5), transparent) background-image: linear-gradient(to top, rgba($bg-100, 0.69), transparent)
opacity: 0 // hide opacity: 0 // hide
z-index: +4 z-index: +4
transition: background-image 0.3s cubic-bezier(.79, .14, .15, .86), opacity 0.3s cubic-bezier(.79, .14, .15, .86) transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86)
.image-title, .image-title,
.image-subtitle .image-subtitle
@ -57,11 +59,11 @@
.image-title .image-title
font-size: 0.9rem font-size: 0.9rem
font-weight: 800 font-weight: 700
.image-subtitle .image-subtitle
font-size: 0.8rem font-size: 0.8rem
font-weight: 600 font-weight: 400
img img
width: 100% width: 100%
@ -72,7 +74,6 @@
object-fit: cover object-fit: cover
object-position: center object-position: center
transform: scale(1.05)
background-color: RGB($bg-bright) background-color: RGB($bg-bright)
filter: blur(0.5rem) filter: blur(0.5rem)
@ -90,12 +91,10 @@
padding-bottom: 100% padding-bottom: 100%
&:hover &:hover
.image-filter box-shadow: 0 0.2rem 0.4rem 0.1rem RGBA($bg-100, 0.6)
background-image: linear-gradient(to top, rgba($bg-100, 0.69), transparent)
opacity: 1
img .image-filter
transform: scale(1) opacity: 1
.group-item .group-item
margin: 0 margin: 0
@ -144,11 +143,11 @@
.image-title .image-title
font-size: 0.9rem font-size: 0.9rem
font-weight: 800 font-weight: 700
.image-subtitle .image-subtitle
font-size: 0.8rem font-size: 0.8rem
font-weight: 600 font-weight: 400
.images .images
margin: 0 margin: 0

View file

@ -1,6 +1,6 @@
.image-fullscreen .image-fullscreen
margin: 0 margin: 0
padding: 0 0 0 3.5rem padding: 0
width: 100% width: 100%
height: 100% height: 100%
@ -14,7 +14,7 @@
opacity: 0 // hide opacity: 0 // hide
background-color: $bg-transparent background-color: $bg-transparent
z-index: 21 z-index: 100
box-sizing: border-box box-sizing: border-box

View file

@ -1,6 +1,10 @@
.info-container .info-container
width: 100% width: 25rem
height: 100% height: 100vh
position: absolute
top: 0
left: 0
display: flex display: flex
flex-direction: column flex-direction: column
@ -9,6 +13,11 @@
background-color: RGB($bg-200) background-color: RGB($bg-200)
overflow-y: auto overflow-y: auto
z-index: +4
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
&.collapsed
left: -25rem
.info-tab .info-tab
width: 100% width: 100%
@ -34,6 +43,9 @@
opacity: 0 opacity: 0
.collapse-indicator .collapse-indicator
margin: 0
padding: 0
width: 1.25rem width: 1.25rem
height: 1.25rem height: 1.25rem
@ -41,12 +53,14 @@
top: 0.6rem top: 0.6rem
right: 0.6rem right: 0.6rem
background-color: transparent
color: RGB($primary) color: RGB($primary)
border: none
z-index: +2 z-index: +2
transition: transform 0.15s cubic-bezier(.79, .14, .15, .86) transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
user-select: none cursor: pointer
.info-header .info-header
margin: 0 margin: 0
@ -80,7 +94,7 @@
padding: 0 padding: 0
font-size: 1.1rem font-size: 1.1rem
font-weight: 600 font-weight: 500
color: RGB($primary) color: RGB($primary)
@ -104,7 +118,7 @@
padding: 0 padding: 0
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 400
text-overflow: ellipsis text-overflow: ellipsis
overflow: hidden overflow: hidden
@ -157,7 +171,7 @@
white-space: nowrap white-space: nowrap
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 400
td:last-child td:last-child
padding: 0 padding: 0
@ -169,7 +183,7 @@
white-space: nowrap white-space: nowrap
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 400
td.empty-table td.empty-table
opacity: 0.3 opacity: 0.3
@ -205,4 +219,14 @@
@media (max-width: 1100px) @media (max-width: 1100px)
.info-container .info-container
width: 100%
height: 100%
position: relative
display: flex
flex-direction: column
gap: 0.5rem gap: 0.5rem
&.collapsed
left: unset

View file

@ -7,48 +7,68 @@
.image-grid .image-grid
padding: 0 padding: 0
position: relative width: 100%
display: grid
grid-template-areas: 'info image' 'info tools'
grid-template-columns: 25rem 1fr
grid-template-rows: 1fr auto
gap: 0
height: 100vh height: 100vh
position: relative
display: flex
flex-direction: column
gap: 0.5rem
z-index: 3 z-index: 3
#image-info .image-block
grid-area: info margin: 0 0 0 25rem
#image-tools padding: 0
grid-area: tools
padding: 0 0 0.5rem 0 width: calc(100% - 25rem)
#image-container height: 100vh
grid-area: image
position: relative
display: flex
flex-direction: column
gap: 0
z-index: 3
transition: margin 0.3s cubic-bezier(0.76, 0, 0.17, 1), width 0.3s cubic-bezier(0.76, 0, 0.17, 1)
.pill-row
margin-bottom: 0.5rem
&.collapsed
.image-block
margin: 0
width: 100%
@media (max-width: 1100px) @media (max-width: 1100px)
.image-grid .image-grid
padding: 0.5rem padding: 0.5rem
display: flex
flex-direction: column
gap: 0.5rem
height: auto height: auto
.image-container .image-block
margin: 0 auto margin: 0
padding: 0 width: 100%
height: auto
max-height: 69vh gap: 0.5rem
img transition: margin 0s, width 0s
max-height: 69vh
#image-tools .image-container
padding: 0 margin: 0 auto
padding: 0
max-height: 69vh
img
max-height: 69vh
.pill-row
margin-bottom: 0
#fullscreenImage
display: none
.info-container .info-container
background: transparent background: transparent
@ -59,7 +79,4 @@
.info-tab.collapsed .info-header .info-tab.collapsed .info-header
border-radius: $rad border-radius: $rad
@media (max-width: $breakpoint)
.image-fullscreen
padding: 0 0 3.5rem 0

View file

@ -68,7 +68,7 @@
.tool-tip .tool-tip
margin: 0 margin: 0
padding: 0.35rem 0.7rem padding: 0.4rem 0.7rem
display: block display: block
@ -78,9 +78,9 @@
transform: translateY(-50%) transform: translateY(-50%)
font-size: 0.9rem font-size: 0.9rem
font-weight: 700 font-weight: 500
background-color: #000000 background-color: RGB($bg-100)
color: RGB($fg-white) color: RGB($fg-white)
opacity: 0 opacity: 0
border-radius: $rad-inner border-radius: $rad-inner
@ -103,7 +103,7 @@
left: -0.45rem left: -0.45rem
transform: translateY(-50%) transform: translateY(-50%)
color: #000000 color: RGB($bg-100)
&:hover &:hover
> svg > svg

View file

@ -25,6 +25,8 @@
box-sizing: border-box box-sizing: border-box
font-family: $font font-family: $font
scrollbar-color: RGB($primary) transparent
::-webkit-scrollbar ::-webkit-scrollbar
width: 0.5rem width: 0.5rem
::-webkit-scrollbar-track ::-webkit-scrollbar-track

View file

@ -33,7 +33,7 @@ $rad-inner: var(--rad-inner)
$animation-smooth: var(--animation-smooth) $animation-smooth: var(--animation-smooth)
$animation-bounce: var(--animation-bounce) $animation-bounce: var(--animation-bounce)
$font: 'Manrope', sans-serif $font: 'Rubik', sans-serif
$breakpoint: 800px $breakpoint: 800px
@ -77,8 +77,10 @@ $breakpoint: 800px
// I have no clue if its webassets or libsass thats doing this shit // 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 // But one of them is trying to "correct" the path, and 404-ing the
// font, so enjoy this path fuckery // font, so enjoy this path fuckery
@font-face @font-face
font-family: 'Manrope' font-family: 'Rubik'
src: url('../../../../static/fonts/Manrope[wght].woff2') format('woff2') src: url('../../../../static/fonts/Rubik.ttf') format('truetype')
font-style: normal font-style: normal
font-display: swap font-display: swap
font-weight: 300 900

View file

@ -184,12 +184,23 @@
color: {{ text_colour }} !important; color: {{ text_colour }} !important;
} }
.banner-content .link {
background-color: {{ text_colour }} !important;
color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
}
.banner-content .link:hover {
background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
color: {{ text_colour }} !important;
}
.banner-filter { .banner-filter {
background: linear-gradient(90deg, rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}), rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.3)) !important; background: linear-gradient(90deg, rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}),
rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important;
} }
@media (max-width: 800px) { @media (max-width: 800px) {
.banner-filter { .banner-filter {
background: linear-gradient(180deg, rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.8), rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.5)) !important; background: linear-gradient(180deg, rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 1),
rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.5)) !important;
} }
} }
@ -209,12 +220,12 @@
{% block content %} {% block content %}
{% if images %} {% if images %}
<div class="banner"> <div class="banner">
<img src="{{ url_for('api.file', file_name=images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;"/> <img src="{{ url_for('api.file', file_name=images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;" alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"/>
<span class="banner-filter"></span> <span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
<p class="banner-info">By {{ group.author_username }} - {{ images|length }} Images</p> <p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author_id) }}" class="link">By {{ group.author_username }}</a></p>
<h1 class="banner-header">{{ group.name }}</h1> <h1 class="banner-header">{{ group.name }}</h1>
<p class="banner-subtitle">{{ group.description }}</p> <p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p>
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="groupShare()"> <button class="pill-item" onclick="groupShare()">
@ -268,7 +279,7 @@
<p class="image-subtitle"></p> <p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p> <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div> </div>
<img alt="{{ image.alt }}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/> <img alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -1,29 +1,11 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block wrapper_class %}image-wrapper{% endblock %} {% block page_index %}
{% if return_page %}?page={{ return_page }}{% endif %}{% endblock %}
{% block head %} {% block head %}
<meta property="og:image" content="{{ url_for('api.file', file_name=image.filename) }}"/> <meta property="og:image" content="{{ url_for('api.file', file_name=image.filename) }}"/>
<meta name="theme-color" content="rgb({{ image.colours.0.0 }}{{ image.colours.0.1 }}{{ image.colours.0.2 }})"/> <meta name="theme-color" content="rgb({{ image.colours.0.0 }}{{ image.colours.0.1 }}{{ image.colours.0.2 }})"/>
<script type="text/javascript"> <script type="text/javascript">
function imageFullscreenOff() {
document.querySelector("html").style.overflow = "auto";
let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.classList.remove('active');
setTimeout(function() {
fullscreen.style.display = 'none';
}, 200);
}
function imageFullscreenOn() {
let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.querySelector('img').src = '{{ url_for('api.file', file_name=image.filename) }}';
document.querySelector("html").style.overflow = "hidden";
fullscreen.style.display = 'flex';
setTimeout(function() { fullscreen.classList.add('active'); }, 5);
}
function imageShare() { function imageShare() {
try { try {
navigator.clipboard.writeText(window.location.href) navigator.clipboard.writeText(window.location.href)
@ -32,6 +14,18 @@
addNotification("Failed to copy link! Are you on HTTP?", 2); addNotification("Failed to copy link! Are you on HTTP?", 2);
} }
} }
function fullscreen() {
let info = document.querySelector('.info-container');
let wrapper = document.querySelector('.image-grid');
if (info.classList.contains('collapsed')) {
info.classList.remove('collapsed');
wrapper.classList.remove('collapsed');
} else {
info.classList.add('collapsed');
wrapper.classList.add('collapsed');
}
}
{% if current_user.id == image.author_id %} {% if current_user.id == image.author_id %}
function imageDelete() { function imageDelete() {
@ -67,7 +61,7 @@
if (response.ok) { if (response.ok) {
window.location.href = '/'; window.location.href = '/';
} else { } else {
addNotification(`Image *clings*: ${response}`, 2); addNotification(`Image *clings*`, 2);
} }
}); });
} }
@ -80,7 +74,8 @@
<style> <style>
.background span { .background span {
background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 1), transparent); background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 0.8),
rgba({{ image.colours.1.0 }}, {{ image.colours.1.1 }}, {{ image.colours.1.2 }}, 0.2));
} }
</style> </style>
{% endblock %} {% endblock %}
@ -95,95 +90,71 @@
</div> </div>
<div class="image-grid"> <div class="image-grid">
<div class="image-container" id="image-container"> <div class="image-block">
<img <div class="image-container">
src="{{ url_for('api.file', file_name=image.filename) }}?r=prev" <img
alt="{{ image.alt }}" src="{{ url_for('api.file', file_name=image.filename) }}?r=prev"
onload="imgFade(this)" alt="{{ image.alt }}"
style="opacity: 0;" onload="imgFade(this)"
onerror="this.src='{{ url_for('static', filename='error.png')}}'" style="opacity: 0;"
{% if "File" in image.exif %} onerror="this.src='{{ url_for('static', filename='error.png')}}'"
width="{{ image.exif.File.Width.raw }}" {% if "File" in image.exif %}
height="{{ image.exif.File.Height.raw }}" width="{{ image.exif.File.Width.raw }}"
{% endif %} height="{{ image.exif.File.Height.raw }}"
/> {% endif %}
</div> />
<div class="pill-row" id="image-tools">
{% if next_url %}
<div>
<a class="pill-item" href="{{ next_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
<span class="tool-tip">
Previous
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</a>
</div>
{% endif %}
<div>
<button class="pill-item" onclick="imageFullscreenOn()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48V96a8,8,0,0,1-16,0V67.31l-50.34,50.35a8,8,0,0,1-11.32-11.32L188.69,56H160a8,8,0,0,1,0-16h48A8,8,0,0,1,216,48ZM106.34,138.34,56,188.69V160a8,8,0,0,0-16,0v48a8,8,0,0,0,8,8H96a8,8,0,0,0,0-16H67.31l50.35-50.34a8,8,0,0,0-11.32-11.32Z"></path></svg>
<span class="tool-tip">
Fullscreen
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</button>
<button class="pill-item" onclick="imageShare()">
<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>
<span class="tool-tip">
Share
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</button>
<a class="pill-item" href="/api/file/{{ image.filename }}" download onclick="addNotification('Download started!', 4)">
<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>
<span class="tool-tip">
Download
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</a>
</div> </div>
{% if current_user.id == image.author_id %}
<div class="pill-row">
{% if next_url %}
<div>
<a class="pill-item" href="{{ next_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
</a>
</div>
{% endif %}
<div> <div>
<button class="pill-item pill__critical" onclick="imageDelete()"> <button class="pill-item" onclick="fullscreen()" id="fullscreenImage">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="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>
<span class="tool-tip">
Delete
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</button> </button>
<button class="pill-item pill__critical" onclick="imageEdit()"> <button class="pill-item" onclick="imageShare()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
<span class="tool-tip">
Edit
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</button> </button>
</div> <a class="pill-item" href="/api/file/{{ image.filename }}" download onclick="addNotification('Download started!', 4)">
{% endif %} <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>
{% if prev_url %}
<div>
<a class="pill-item" href="{{ prev_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>
<span class="tool-tip">
Next
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</a> </a>
</div> </div>
{% endif %} {% 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>
</button>
<button class="pill-item pill__critical" onclick="imageEdit()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg>
</button>
</div>
{% endif %}
{% if prev_url %}
<div>
<a class="pill-item" href="{{ prev_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>
</a>
</div>
{% endif %}
</div>
</div> </div>
<div class="info-container" id="image-info"> <div class="info-container">
{% if image.post_description %} {% if image.post_description %}
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="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> <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> <h2>Description</h2>
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor"> <button class="collapse-indicator">
<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 xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
</svg> <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>
<div class="info-table"> <div class="info-table">
<p>{{ image.post_description }}</p> <p>{{ image.post_description }}</p>
@ -194,15 +165,17 @@
<div class="info-header"> <div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>
<h2>Info</h2> <h2>Info</h2>
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor"> <button class="collapse-indicator">
<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 xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
</svg> <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>
<div class="info-table"> <div class="info-table">
<table> <table>
<tr> <tr>
<td>Author</td> <td>Author</td>
<td><a href="{{ url_for('gallery.profile', id=image.author_id) }}" class="link">{{ image.author_username }}</a></td> <td><a href="{{ url_for('profile.profile', id=image.author_id) }}" class="link">{{ image.author_username }}</a></td>
</tr> </tr>
<tr> <tr>
<td>Upload date</td> <td>Upload date</td>
@ -245,9 +218,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M110.66,147.56a8,8,0,0,0-13.32,0L76.49,178.85l-9.76-15.18a8,8,0,0,0-13.46,0l-36,56A8,8,0,0,0,24,232H152a8,8,0,0,0,6.66-12.44ZM38.65,216,60,182.79l9.63,15a8,8,0,0,0,13.39.11l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M110.66,147.56a8,8,0,0,0-13.32,0L76.49,178.85l-9.76-15.18a8,8,0,0,0-13.46,0l-36,56A8,8,0,0,0,24,232H152a8,8,0,0,0,6.66-12.44ZM38.65,216,60,182.79l9.63,15a8,8,0,0,0,13.39.11l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg>
<h2>{{ tag }}</h2> <h2>{{ tag }}</h2>
{% endif %} {% endif %}
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor"> <button class="collapse-indicator">
<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 xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
</svg> <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>
<div class="info-table"> <div class="info-table">
<table> <table>
@ -278,9 +253,10 @@
{% block script %} {% block script %}
<script type="text/javascript"> <script type="text/javascript">
let infoTab = document.querySelectorAll('.info-tab'); let infoTab = document.querySelectorAll('.info-tab');
for (let i = 0; i < infoTab.length; i++) { for (let i = 0; i < infoTab.length; i++) {
infoTab[i].querySelector('.info-header').addEventListener('click', function() { infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() {
this.parentNode.classList.toggle('collapsed'); infoTab[i].classList.toggle('collapsed');
}); });
} }
</script> </script>

View file

@ -3,25 +3,56 @@
{% block content %} {% block content %}
<div class="banner-small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1 class="banner-header">{{ config.WEBSITE.name }}</h1> <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
{% if images|length == 0 %} {% if total_images == 0 %}
<p class="banner-info">0 images D:</p> <p class="banner-info">0 images D:</p>
{% elif images|length == 69 %} {% elif total_images == 69 %}
<p class="banner-info">{{ images|length }} images, nice</p> <p class="banner-info">{{ total_images }} images, nice</p>
{% else %} {% else %}
<p class="banner-info">{{ images|length }} images</p> <p class="banner-info">{{ total_images }} images</p>
{% endif %}
{% if pages > 1 %}
<div class="pill-row">
<div>
{% if pages > 4 %}
<a class="pill-item" href="{{ url_for('gallery.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M232,128a8,8,0,0,1-8,8H91.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L91.31,120H224A8,8,0,0,1,232,128ZM40,32a8,8,0,0,0-8,8V216a8,8,0,0,0,16,0V40A8,8,0,0,0,40,32Z"></path></svg>
</a>
{% endif %}
<a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page-1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
</a>
</div>
<span class="pill-text">
{{ page }} / {{ pages }}
</span>
<div>
<a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page+1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>
</a>
{% if pages > 4 %}
<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M189.66,122.34a8,8,0,0,1,0,11.32l-72,72a8,8,0,0,1-11.32-11.32L164.69,136H32a8,8,0,0,1,0-16H164.69L106.34,61.66a8,8,0,0,1,11.32-11.32ZM216,32a8,8,0,0,0-8,8V216a8,8,0,0,0,16,0V40A8,8,0,0,0,216,32Z"></path></svg>
</a>
{% endif %}
</div>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if images %} {% if images %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% for image in images %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('gallery.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})"> <a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('image.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter"> <div class="image-filter">
<p class="image-subtitle"></p> <p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p> <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div> </div>
<img fetchpriority="low" alt="{{ image.alt }}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/> <img fetchpriority="low" alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
@ -32,22 +63,3 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block script %}
<script type="text/javascript">
if (document.referrer.includes('image')) {
try {
let referrerId = document.referrer.split('/').pop();
let imgOffset = document.getElementById('image-' + referrerId).offsetTop;
let imgHeight = document.getElementById('image-' + referrerId).offsetHeight;
let windowHeight = window.innerHeight;
document.querySelector('html').style.scrollBehavior = 'auto';
window.scrollTo(0, imgOffset + (imgHeight / 2) - (windowHeight / 2));
document.querySelector('html').style.scrollBehavior = 'smooth';
} catch (e) {
console.log(e);
}
}
</script>
{% endblock %}

View file

@ -1,18 +1,23 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ config.WEBSITE.name }}</title> <title>{{ config.WEBSITE_CONF.name }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ config.WEBSITE.motto }}"/> <meta name="description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta name="author" content="{{ config.WEBSITE.author }}"/> <meta name="author" content="{{ config.WEBSITE_CONF.author }}"/>
<meta property="og:title" content="{{ config.WEBSITE.name }}"/>
<meta property="og:description" content="{{ config.WEBSITE.motto }}"/> <meta property="og:title" content="{{ config.WEBSITE_CONF.name }}"/>
<meta property="og:description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta name="twitter:title" content="{{ config.WEBSITE.name }}"/>
<meta name="twitter:description" content="{{ config.WEBSITE.motto }}"/> <meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}"/>
<meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<link rel="manifest" href="static/manifest.json"/>
<link <link
href="{{url_for('static', filename='logo-black.svg')}}" href="{{url_for('static', filename='logo-black.svg')}}"
@ -25,12 +30,12 @@
type="image/svg+xml" type="image/svg+xml"
media="(prefers-color-scheme: dark)"/> media="(prefers-color-scheme: dark)"/>
{% assets "js_pre" %} {% assets "lib" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endassets %}
{% assets "js_post" %} {% assets "js" %}
<script type="text/javascript" src="{{ ASSET_URL }}" defer></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endassets %}
{% assets "styles" %} {% assets "styles" %}
@ -42,12 +47,12 @@
<body> <body>
<div class="notifications"></div> <div class="notifications"></div>
<button class="top-of-page"> <button class="top-of-page" aria-label="Jump to top of page">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M184,216a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,216Zm45.66-101.66-96-96a8,8,0,0,0-11.32,0l-96,96A8,8,0,0,0,32,128H72v24a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8V128h40a8,8,0,0,0,5.66-13.66ZM176,176H80a8,8,0,0,0,0,16h96a8,8,0,0,0,0-16Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M184,216a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,216Zm45.66-101.66-96-96a8,8,0,0,0-11.32,0l-96,96A8,8,0,0,0,32,128H72v24a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8V128h40a8,8,0,0,0,5.66-13.66ZM176,176H80a8,8,0,0,0,0,16h96a8,8,0,0,0,0-16Z"></path></svg>
</button> </button>
{% if request.path == "/" %} {% if request.path == "/" %}
<button class="info-button"> <button class="info-button" aria-label="Show info on gallery">
<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,24Zm-4,48a12,12,0,1,1-12,12A12,12,0,0,1,124,72Zm12,112a16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40a8,8,0,0,1,0,16Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
</button> </button>
{% endif %} {% endif %}
@ -64,7 +69,7 @@
<div class="navigation"> <div class="navigation">
<!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">--> <!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">-->
<a href="{{url_for('gallery.index')}}" class="navigation-item {% block nav_home %}{% endblock %}"> <a href="{{url_for('gallery.index')}}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96ZM104,88a16,16,0,1,1,16,16A16,16,0,0,1,104,88Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96ZM104,88a16,16,0,1,1,16,16A16,16,0,0,1,104,88Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Home Home
@ -93,7 +98,7 @@
<span class="navigation-spacer"></span> <span class="navigation-spacer"></span>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a href="{{url_for('gallery.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}"> <a href="{{url_for('profile.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Profile Profile

View file

@ -96,7 +96,7 @@
{% block content %} {% block content %}
<div class="banner-small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1 class="banner-header">{{ config.WEBSITE.name }}</h1> <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
{% if groups|length == 0 %} {% if groups|length == 0 %}
<p class="banner-info">No groups!!!!</p> <p class="banner-info">No groups!!!!</p>
{% elif groups|length == 69 %} {% elif groups|length == 69 %}
@ -127,10 +127,10 @@
<div class="images size-{{ group.images|length }}"> <div class="images size-{{ group.images|length }}">
{% if group.images|length > 0 %} {% if group.images|length > 0 %}
{% for image in group.images %} {% for image in group.images %}
<img data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load" class="data-{{ loop.index }}"/> <img data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load" class="data-{{ loop.index }}" {% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}/>
{% endfor %} {% endfor %}
{% else %} {% else %}
<img src="{{ url_for('static', filename='error.png') }}" class="loaded"/> <img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/>
{% endif %} {% endif %}
</div> </div>
</a> </a>

View file

@ -11,7 +11,7 @@
{% if images %} {% if images %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% for image in images %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('gallery.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})"> <a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('image.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter"> <div class="image-filter">
<p class="image-subtitle"></p> <p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p> <p class="image-title"><span class="time">{{ image.created_at }}</span></p>

View file

@ -16,7 +16,10 @@ def contrast(background, light, dark, threshold=0.179):
# Calculate contrast # Calculate contrast
uicolors = [red / 255, green / 255, blue / 255] uicolors = [red / 255, green / 255, blue / 255]
cont = [col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4 for col in uicolors] cont = [
col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4
for col in uicolors
]
lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2]) lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
return light if lightness > threshold else dark return light if lightness > threshold else dark

View file

@ -8,8 +8,8 @@ from PIL import Image, ImageOps
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
CACHE_PATH = platformdirs.user_config_dir('onlylegs') + '/cache' CACHE_PATH = os.path.join(platformdirs.user_config_dir("onlylegs"), "cache")
UPLOAD_PATH = platformdirs.user_config_dir('onlylegs') + '/uploads' UPLOAD_PATH = os.path.join(platformdirs.user_config_dir("onlylegs"), "uploads")
def generate_thumbnail(file_name, resolution, ext=None): def generate_thumbnail(file_name, resolution, ext=None):
@ -25,34 +25,34 @@ def generate_thumbnail(file_name, resolution, ext=None):
os.makedirs(CACHE_PATH) os.makedirs(CACHE_PATH)
# no sussy business # no sussy business
file_name, file_ext = secure_filename(file_name).rsplit('.') file_name, file_ext = secure_filename(file_name).rsplit(".")
if not ext: if not ext:
ext = file_ext.strip('.') ext = file_ext.strip(".")
# PIL doesnt like jpg so we convert it to jpeg # PIL doesnt like jpg so we convert it to jpeg
if ext.lower() == "jpg": if ext.lower() == "jpg":
ext = "jpeg" ext = "jpeg"
# Set resolution based on preset resolutions # Set resolution based on preset resolutions
if resolution in ['prev', 'preview']: if resolution in ["prev", "preview"]:
res_x, res_y = (1920, 1080) res_x, res_y = (1920, 1080)
elif resolution in ['thumb', 'thumbnail']: elif resolution in ["thumb", "thumbnail"]:
res_x, res_y = (350, 350) res_x, res_y = (400, 400)
elif resolution in ['icon', 'favicon']: elif resolution in ["icon", "favicon"]:
res_x, res_y = (10, 10) res_x, res_y = (10, 10)
else: else:
return None return None
# If image has been already generated, return it from the cache # If image has been already generated, return it from the cache
if os.path.exists(os.path.join(CACHE_PATH, f'{file_name}_{res_x}x{res_y}.{ext}')): if os.path.exists(os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}")):
return os.path.join(CACHE_PATH, f'{file_name}_{res_x}x{res_y}.{ext}') return os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}")
# Check if image exists in the uploads directory # Check if image exists in the uploads directory
if not os.path.exists(os.path.join(UPLOAD_PATH, f'{file_name}.{file_ext}')): if not os.path.exists(os.path.join(UPLOAD_PATH, f"{file_name}.{file_ext}")):
return None return None
# Open image and rotate it based on EXIF data and get ICC profile so colors are correct # Open image and rotate it based on EXIF data and get ICC profile so colors are correct
image = Image.open(os.path.join(UPLOAD_PATH, f'{file_name}.{file_ext}')) image = Image.open(os.path.join(UPLOAD_PATH, f"{file_name}.{file_ext}"))
image_icc = image.info.get("icc_profile") image_icc = image.info.get("icc_profile")
img_x, img_y = image.size img_x, img_y = image.size
@ -62,16 +62,20 @@ def generate_thumbnail(file_name, resolution, ext=None):
# Save image to cache directory # Save image to cache directory
try: try:
image.save(os.path.join(CACHE_PATH, f'{file_name}_{res_x}x{res_y}.{ext}'), image.save(
icc_profile=image_icc) os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}"),
icc_profile=image_icc,
)
except OSError: except OSError:
# This usually happens when saving a JPEG with an ICC profile, # This usually happens when saving a JPEG with an ICC profile,
# so we convert to RGB and try again # so we convert to RGB and try again
image = image.convert('RGB') image = image.convert("RGB")
image.save(os.path.join(CACHE_PATH, f'{file_name}_{res_x}x{res_y}.{ext}'), image.save(
icc_profile=image_icc) os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}"),
icc_profile=image_icc,
)
# No need to keep the image in memory, learned the hard way # No need to keep the image in memory, learned the hard way
image.close() image.close()
return os.path.join(CACHE_PATH, f'{file_name}_{res_x}x{res_y}.{ext}') return os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}")

View file

@ -16,6 +16,7 @@ class Metadata:
""" """
Metadata parser Metadata parser
""" """
def __init__(self, file_path): def __init__(self, file_path):
""" """
Initialize the metadata parser Initialize the metadata parser
@ -32,17 +33,17 @@ class Metadata:
if tag in tags: if tag in tags:
img_exif[value] = tags[tag] img_exif[value] = tags[tag]
img_exif['FileName'] = os.path.basename(file_path) img_exif["FileName"] = os.path.basename(file_path)
img_exif['FileSize'] = os.path.getsize(file_path) img_exif["FileSize"] = os.path.getsize(file_path)
img_exif['FileFormat'] = img_exif['FileName'].split('.')[-1] img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
img_exif['FileWidth'], img_exif['FileHeight'] = file.size img_exif["FileWidth"], img_exif["FileHeight"] = file.size
file.close() file.close()
except TypeError: except TypeError:
img_exif['FileName'] = os.path.basename(file_path) img_exif["FileName"] = os.path.basename(file_path)
img_exif['FileSize'] = os.path.getsize(file_path) img_exif["FileSize"] = os.path.getsize(file_path)
img_exif['FileFormat'] = img_exif['FileName'].split('.')[-1] img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
img_exif['FileWidth'], img_exif['FileHeight'] = file.size img_exif["FileWidth"], img_exif["FileHeight"] = file.size
self.encoded = img_exif self.encoded = img_exif
@ -60,39 +61,42 @@ class Metadata:
Formats the data into a dictionary Formats the data into a dictionary
""" """
exif = { exif = {
'Photographer': {}, "Photographer": {},
'Camera': {}, "Camera": {},
'Software': {}, "Software": {},
'File': {}, "File": {},
} }
# Thanks chatGPT xP # Thanks chatGPT xP
# the helper function works, so not sure why it triggers pylint
for key, value in encoded_exif.items(): for key, value in encoded_exif.items():
for mapping_name, mapping_val in EXIF_MAPPING: for mapping_name, mapping_val in EXIF_MAPPING:
if key in mapping_val: if key in mapping_val:
if len(mapping_val[key]) == 2: if len(mapping_val[key]) == 2:
# the helper function works, so not sure why it triggers pylint
exif[mapping_name][mapping_val[key][0]] = { exif[mapping_name][mapping_val[key][0]] = {
'raw': value, "raw": value,
'formatted': ( "formatted": (
getattr(helpers, mapping_val[key][1]) # pylint: disable=E0602 getattr(
(value) helpers, mapping_val[key][1] # pylint: disable=E0602
), )(
value
)
),
} }
else: else:
exif[mapping_name][mapping_val[key][0]] = { exif[mapping_name][mapping_val[key][0]] = {
'raw': value, "raw": value,
} }
continue continue
# Remove empty keys # Remove empty keys
if not exif['Photographer']: if not exif["Photographer"]:
del exif['Photographer'] del exif["Photographer"]
if not exif['Camera']: if not exif["Camera"]:
del exif['Camera'] del exif["Camera"]
if not exif['Software']: if not exif["Software"]:
del exif['Software'] del exif["Software"]
if not exif['File']: if not exif["File"]:
del exif['File'] del exif["File"]
return exif return exif

View file

@ -21,28 +21,28 @@ def date_format(value):
""" """
Formats the date into a standard format Formats the date into a standard format
""" """
return str(datetime.strptime(value, '%Y:%m:%d %H:%M:%S')) return str(datetime.strptime(value, "%Y:%m:%d %H:%M:%S"))
def fnumber(value): def fnumber(value):
""" """
Formats the f-number into a standard format Formats the f-number into a standard format
""" """
return 'ƒ/' + str(value) return "ƒ/" + str(value)
def iso(value): def iso(value):
""" """
Formats the ISO into a standard format Formats the ISO into a standard format
""" """
return 'ISO ' + str(value) return "ISO " + str(value)
def shutter(value): def shutter(value):
""" """
Formats the shutter speed into a standard format Formats the shutter speed into a standard format
""" """
return str(value) + 's' return str(value) + "s"
def focal_length(value): def focal_length(value):
@ -50,27 +50,23 @@ def focal_length(value):
Formats the focal length into a standard format Formats the focal length into a standard format
""" """
try: try:
return str(value[0] / value[1]) + 'mm' return str(value[0] / value[1]) + "mm"
except TypeError: except TypeError:
return str(value) + 'mm' return str(value) + "mm"
def exposure(value): def exposure(value):
""" """
Formats the exposure value into a standard format Formats the exposure value into a standard format
""" """
return str(value) + 'EV' return str(value) + "EV"
def color_space(value): def color_space(value):
""" """
Maps the value of the color space to a human readable format Maps the value of the color space to a human readable format
""" """
value_map = { value_map = {0: "Reserved", 1: "sRGB", 65535: "Uncalibrated"}
0: 'Reserved',
1: 'sRGB',
65535: 'Uncalibrated'
}
try: try:
return value_map[int(value)] return value_map[int(value)]
except KeyError: except KeyError:
@ -82,28 +78,28 @@ def flash(value):
Maps the value of the flash to a human readable format Maps the value of the flash to a human readable format
""" """
value_map = { value_map = {
0: 'Flash did not fire', 0: "Flash did not fire",
1: 'Flash fired', 1: "Flash fired",
5: 'Strobe return light not detected', 5: "Strobe return light not detected",
7: 'Strobe return light detected', 7: "Strobe return light detected",
9: 'Flash fired, compulsory flash mode', 9: "Flash fired, compulsory flash mode",
13: 'Flash fired, compulsory flash mode, return light not detected', 13: "Flash fired, compulsory flash mode, return light not detected",
15: 'Flash fired, compulsory flash mode, return light detected', 15: "Flash fired, compulsory flash mode, return light detected",
16: 'Flash did not fire, compulsory flash mode', 16: "Flash did not fire, compulsory flash mode",
24: 'Flash did not fire, auto mode', 24: "Flash did not fire, auto mode",
25: 'Flash fired, auto mode', 25: "Flash fired, auto mode",
29: 'Flash fired, auto mode, return light not detected', 29: "Flash fired, auto mode, return light not detected",
31: 'Flash fired, auto mode, return light detected', 31: "Flash fired, auto mode, return light detected",
32: 'No flash function', 32: "No flash function",
65: 'Flash fired, red-eye reduction mode', 65: "Flash fired, red-eye reduction mode",
69: 'Flash fired, red-eye reduction mode, return light not detected', 69: "Flash fired, red-eye reduction mode, return light not detected",
71: 'Flash fired, red-eye reduction mode, return light detected', 71: "Flash fired, red-eye reduction mode, return light detected",
73: 'Flash fired, compulsory flash mode, red-eye reduction mode', 73: "Flash fired, compulsory flash mode, red-eye reduction mode",
77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', 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', 79: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",
89: 'Flash fired, auto mode, red-eye reduction mode', 89: "Flash fired, auto mode, red-eye reduction mode",
93: 'Flash fired, auto mode, return light not detected, 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' 95: "Flash fired, auto mode, return light detected, red-eye reduction mode",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -116,15 +112,15 @@ def exposure_program(value):
Maps the value of the exposure program to a human readable format Maps the value of the exposure program to a human readable format
""" """
value_map = { value_map = {
0: 'Not defined', 0: "Not defined",
1: 'Manual', 1: "Manual",
2: 'Normal program', 2: "Normal program",
3: 'Aperture priority', 3: "Aperture priority",
4: 'Shutter priority', 4: "Shutter priority",
5: 'Creative program', 5: "Creative program",
6: 'Action program', 6: "Action program",
7: 'Portrait mode', 7: "Portrait mode",
8: 'Landscape mode' 8: "Landscape mode",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -137,14 +133,14 @@ def metering_mode(value):
Maps the value of the metering mode to a human readable format Maps the value of the metering mode to a human readable format
""" """
value_map = { value_map = {
0: 'Unknown', 0: "Unknown",
1: 'Average', 1: "Average",
2: 'Center-Weighted Average', 2: "Center-Weighted Average",
3: 'Spot', 3: "Spot",
4: 'Multi-Spot', 4: "Multi-Spot",
5: 'Pattern', 5: "Pattern",
6: 'Partial', 6: "Partial",
255: 'Other' 255: "Other",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -156,11 +152,7 @@ def resolution_unit(value):
""" """
Maps the value of the resolution unit to a human readable format Maps the value of the resolution unit to a human readable format
""" """
value_map = { value_map = {1: "No absolute unit of measurement", 2: "Inch", 3: "Centimeter"}
1: 'No absolute unit of measurement',
2: 'Inch',
3: 'Centimeter'
}
try: try:
return value_map[int(value)] return value_map[int(value)]
except KeyError: except KeyError:
@ -172,27 +164,27 @@ def light_source(value):
Maps the value of the light source to a human readable format Maps the value of the light source to a human readable format
""" """
value_map = { value_map = {
0: 'Unknown', 0: "Unknown",
1: 'Daylight', 1: "Daylight",
2: 'Fluorescent', 2: "Fluorescent",
3: 'Tungsten (incandescent light)', 3: "Tungsten (incandescent light)",
4: 'Flash', 4: "Flash",
9: 'Fine weather', 9: "Fine weather",
10: 'Cloudy weather', 10: "Cloudy weather",
11: 'Shade', 11: "Shade",
12: 'Daylight fluorescent (D 5700 - 7100K)', 12: "Daylight fluorescent (D 5700 - 7100K)",
13: 'Day white fluorescent (N 4600 - 5400K)', 13: "Day white fluorescent (N 4600 - 5400K)",
14: 'Cool white fluorescent (W 3900 - 4500K)', 14: "Cool white fluorescent (W 3900 - 4500K)",
15: 'White fluorescent (WW 3200 - 3700K)', 15: "White fluorescent (WW 3200 - 3700K)",
17: 'Standard light A', 17: "Standard light A",
18: 'Standard light B', 18: "Standard light B",
19: 'Standard light C', 19: "Standard light C",
20: 'D55', 20: "D55",
21: 'D65', 21: "D65",
22: 'D75', 22: "D75",
23: 'D50', 23: "D50",
24: 'ISO studio tungsten', 24: "ISO studio tungsten",
255: 'Other light source', 255: "Other light source",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -205,10 +197,10 @@ def scene_capture_type(value):
Maps the value of the scene capture type to a human readable format Maps the value of the scene capture type to a human readable format
""" """
value_map = { value_map = {
0: 'Standard', 0: "Standard",
1: 'Landscape', 1: "Landscape",
2: 'Portrait', 2: "Portrait",
3: 'Night scene', 3: "Night scene",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -221,8 +213,8 @@ def white_balance(value):
Maps the value of the white balance to a human readable format Maps the value of the white balance to a human readable format
""" """
value_map = { value_map = {
0: 'Auto white balance', 0: "Auto white balance",
1: 'Manual white balance', 1: "Manual white balance",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -235,9 +227,9 @@ def exposure_mode(value):
Maps the value of the exposure mode to a human readable format Maps the value of the exposure mode to a human readable format
""" """
value_map = { value_map = {
0: 'Auto exposure', 0: "Auto exposure",
1: 'Manual exposure', 1: "Manual exposure",
2: 'Auto bracket', 2: "Auto bracket",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -250,22 +242,14 @@ def sensitivity_type(value):
Maps the value of the sensitivity type to a human readable format Maps the value of the sensitivity type to a human readable format
""" """
value_map = { value_map = {
0: 0: "Unknown",
'Unknown', 1: "Standard Output Sensitivity",
1: 2: "Recommended Exposure Index",
'Standard Output Sensitivity', 3: "ISO Speed",
2: 4: "Standard Output Sensitivity and Recommended Exposure Index",
'Recommended Exposure Index', 5: "Standard Output Sensitivity and ISO Speed",
3: 6: "Recommended Exposure Index and ISO Speed",
'ISO Speed', 7: "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed",
4:
'Standard Output Sensitivity and Recommended Exposure Index',
5:
'Standard Output Sensitivity and ISO Speed',
6:
'Recommended Exposure Index and ISO Speed',
7:
'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed',
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -278,7 +262,7 @@ def lens_specification(value):
Maps the value of the lens specification to a human readable format Maps the value of the lens specification to a human readable format
""" """
try: try:
return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm' return str(value[0] / value[1]) + "mm - " + str(value[2] / value[3]) + "mm"
except TypeError: except TypeError:
return None return None
@ -288,55 +272,55 @@ def compression_type(value):
Maps the value of the compression type to a human readable format Maps the value of the compression type to a human readable format
""" """
value_map = { value_map = {
1: 'Uncompressed', 1: "Uncompressed",
2: 'CCITT 1D', 2: "CCITT 1D",
3: 'T4/Group 3 Fax', 3: "T4/Group 3 Fax",
4: 'T6/Group 4 Fax', 4: "T6/Group 4 Fax",
5: 'LZW', 5: "LZW",
6: 'JPEG (old-style)', 6: "JPEG (old-style)",
7: 'JPEG', 7: "JPEG",
8: 'Adobe Deflate', 8: "Adobe Deflate",
9: 'JBIG B&W', 9: "JBIG B&W",
10: 'JBIG Color', 10: "JBIG Color",
99: 'JPEG', 99: "JPEG",
262: 'Kodak 262', 262: "Kodak 262",
32766: 'Next', 32766: "Next",
32767: 'Sony ARW Compressed', 32767: "Sony ARW Compressed",
32769: 'Packed RAW', 32769: "Packed RAW",
32770: 'Samsung SRW Compressed', 32770: "Samsung SRW Compressed",
32771: 'CCIRLEW', 32771: "CCIRLEW",
32772: 'Samsung SRW Compressed 2', 32772: "Samsung SRW Compressed 2",
32773: 'PackBits', 32773: "PackBits",
32809: 'Thunderscan', 32809: "Thunderscan",
32867: 'Kodak KDC Compressed', 32867: "Kodak KDC Compressed",
32895: 'IT8CTPAD', 32895: "IT8CTPAD",
32896: 'IT8LW', 32896: "IT8LW",
32897: 'IT8MP', 32897: "IT8MP",
32898: 'IT8BL', 32898: "IT8BL",
32908: 'PixarFilm', 32908: "PixarFilm",
32909: 'PixarLog', 32909: "PixarLog",
32946: 'Deflate', 32946: "Deflate",
32947: 'DCS', 32947: "DCS",
33003: 'Aperio JPEG 2000 YCbCr', 33003: "Aperio JPEG 2000 YCbCr",
33005: 'Aperio JPEG 2000 RGB', 33005: "Aperio JPEG 2000 RGB",
34661: 'JBIG', 34661: "JBIG",
34676: 'SGILog', 34676: "SGILog",
34677: 'SGILog24', 34677: "SGILog24",
34712: 'JPEG 2000', 34712: "JPEG 2000",
34713: 'Nikon NEF Compressed', 34713: "Nikon NEF Compressed",
34715: 'JBIG2 TIFF FX', 34715: "JBIG2 TIFF FX",
34718: '(MDI) Binary Level Codec', 34718: "(MDI) Binary Level Codec",
34719: '(MDI) Progressive Transform Codec', 34719: "(MDI) Progressive Transform Codec",
34720: '(MDI) Vector', 34720: "(MDI) Vector",
34887: 'ESRI Lerc', 34887: "ESRI Lerc",
34892: 'Lossy JPEG', 34892: "Lossy JPEG",
34925: 'LZMA2', 34925: "LZMA2",
34926: 'Zstd', 34926: "Zstd",
34927: 'WebP', 34927: "WebP",
34933: 'PNG', 34933: "PNG",
34934: 'JPEG XR', 34934: "JPEG XR",
65000: 'Kodak DCR Compressed', 65000: "Kodak DCR Compressed",
65535: 'Pentax PEF Compressed', 65535: "Pentax PEF Compressed",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -349,15 +333,15 @@ def orientation(value):
Maps the value of the orientation to a human readable format Maps the value of the orientation to a human readable format
""" """
value_map = { value_map = {
0: 'Undefined', 0: "Undefined",
1: 'Horizontal (normal)', 1: "Horizontal (normal)",
2: 'Mirror horizontal', 2: "Mirror horizontal",
3: 'Rotate 180', 3: "Rotate 180",
4: 'Mirror vertical', 4: "Mirror vertical",
5: 'Mirror horizontal and rotate 270 CW', 5: "Mirror horizontal and rotate 270 CW",
6: 'Rotate 90 CW', 6: "Rotate 90 CW",
7: 'Mirror horizontal and rotate 90 CW', 7: "Mirror horizontal and rotate 90 CW",
8: 'Rotate 270 CW', 8: "Rotate 270 CW",
} }
try: try:
return value_map[int(value)] return value_map[int(value)]
@ -370,16 +354,16 @@ def components_configuration(value):
Maps the value of the components configuration to a human readable format Maps the value of the components configuration to a human readable format
""" """
value_map = { value_map = {
0: '', 0: "",
1: 'Y', 1: "Y",
2: 'Cb', 2: "Cb",
3: 'Cr', 3: "Cr",
4: 'R', 4: "R",
5: 'G', 5: "G",
6: 'B', 6: "B",
} }
try: try:
return ''.join([value_map[int(x)] for x in value]) return "".join([value_map[int(x)] for x in value])
except KeyError: except KeyError:
return None return None
@ -388,18 +372,18 @@ def rating(value):
""" """
Maps the value of the rating to a human readable format Maps the value of the rating to a human readable format
""" """
return str(value) + ' stars' return str(value) + " stars"
def rating_percent(value): def rating_percent(value):
""" """
Maps the value of the rating to a human readable format Maps the value of the rating to a human readable format
""" """
return str(value) + '%' return str(value) + "%"
def pixel_dimension(value): def pixel_dimension(value):
""" """
Maps the value of the pixel dimension to a human readable format Maps the value of the pixel dimension to a human readable format
""" """
return str(value) + 'px' return str(value) + "px"

View file

@ -4,66 +4,66 @@ Mapping for metadata
""" """
PHOTOGRAHER_MAPPING = { PHOTOGRAHER_MAPPING = {
'Artist': ['Artist'], "Artist": ["Artist"],
'UserComment': ['Comment'], "UserComment": ["Comment"],
'ImageDescription': ['Description'], "ImageDescription": ["Description"],
'Copyright': ['Copyright'], "Copyright": ["Copyright"],
} }
CAMERA_MAPPING = { CAMERA_MAPPING = {
'Model': ['Model'], "Model": ["Model"],
'Make': ['Make'], "Make": ["Make"],
'BodySerialNumber': ['Camera Type'], "BodySerialNumber": ["Camera Type"],
'LensMake': ['Lens Make'], "LensMake": ["Lens Make"],
'LenseModel': ['Lens Model'], "LenseModel": ["Lens Model"],
'LensSpecification': ['Lens Specification', 'lens_specification'], "LensSpecification": ["Lens Specification", "lens_specification"],
'ComponentsConfiguration': ['Components Configuration', 'components_configuration'], "ComponentsConfiguration": ["Components Configuration", "components_configuration"],
'DateTime': ['Date Processed', 'date_format'], "DateTime": ["Date Processed", "date_format"],
'DateTimeDigitized': ['Time Digitized', 'date_format'], "DateTimeDigitized": ["Time Digitized", "date_format"],
'OffsetTime': ['Time Offset'], "OffsetTime": ["Time Offset"],
'OffsetTimeOriginal': ['Time Offset - Original'], "OffsetTimeOriginal": ["Time Offset - Original"],
'OffsetTimeDigitized': ['Time Offset - Digitized'], "OffsetTimeDigitized": ["Time Offset - Digitized"],
'DateTimeOriginal': ['Date Original', 'date_format'], "DateTimeOriginal": ["Date Original", "date_format"],
'FNumber': ['F-Stop', 'fnumber'], "FNumber": ["F-Stop", "fnumber"],
'FocalLength': ['Focal Length', 'focal_length'], "FocalLength": ["Focal Length", "focal_length"],
'FocalLengthIn35mmFilm': ['Focal Length (35mm format)', 'focal_length'], "FocalLengthIn35mmFilm": ["Focal Length (35mm format)", "focal_length"],
'MaxApertureValue': ['Max Aperture', 'fnumber'], "MaxApertureValue": ["Max Aperture", "fnumber"],
'ApertureValue': ['Aperture', 'fnumber'], "ApertureValue": ["Aperture", "fnumber"],
'ShutterSpeedValue': ['Shutter Speed', 'shutter'], "ShutterSpeedValue": ["Shutter Speed", "shutter"],
'ISOSpeedRatings': ['ISO Speed Ratings', 'iso'], "ISOSpeedRatings": ["ISO Speed Ratings", "iso"],
'ISOSpeed': ['ISO Speed', 'iso'], "ISOSpeed": ["ISO Speed", "iso"],
'SensitivityType': ['Sensitivity Type', 'sensitivity_type'], "SensitivityType": ["Sensitivity Type", "sensitivity_type"],
'ExposureBiasValue': ['Exposure Bias', 'exposure'], "ExposureBiasValue": ["Exposure Bias", "exposure"],
'ExposureTime': ['Exposure Time', 'shutter'], "ExposureTime": ["Exposure Time", "shutter"],
'ExposureMode': ['Exposure Mode', 'exposure_mode'], "ExposureMode": ["Exposure Mode", "exposure_mode"],
'ExposureProgram': ['Exposure Program', 'exposure_program'], "ExposureProgram": ["Exposure Program", "exposure_program"],
'WhiteBalance': ['White Balance', 'white_balance'], "WhiteBalance": ["White Balance", "white_balance"],
'Flash': ['Flash', 'flash'], "Flash": ["Flash", "flash"],
'MeteringMode': ['Metering Mode', 'metering_mode'], "MeteringMode": ["Metering Mode", "metering_mode"],
'LightSource': ['Light Source', 'light_source'], "LightSource": ["Light Source", "light_source"],
'SceneCaptureType': ['Scene Capture Type', 'scene_capture_type'], "SceneCaptureType": ["Scene Capture Type", "scene_capture_type"],
} }
SOFTWARE_MAPPING = { SOFTWARE_MAPPING = {
'Software': ['Software'], "Software": ["Software"],
'ColorSpace': ['Colour Space', 'color_space'], "ColorSpace": ["Colour Space", "color_space"],
'Compression': ['Compression', 'compression_type'], "Compression": ["Compression", "compression_type"],
} }
FILE_MAPPING = { FILE_MAPPING = {
'FileName': ['Name'], "FileName": ["Name"],
'FileSize': ['Size', 'human_size'], "FileSize": ["Size", "human_size"],
'FileFormat': ['Format'], "FileFormat": ["Format"],
'FileWidth': ['Width', 'pixel_dimension'], "FileWidth": ["Width", "pixel_dimension"],
'FileHeight': ['Height', 'pixel_dimension'], "FileHeight": ["Height", "pixel_dimension"],
'Orientation': ['Orientation', 'orientation'], "Orientation": ["Orientation", "orientation"],
'XResolution': ['X-resolution'], "XResolution": ["X-resolution"],
'YResolution': ['Y-resolution'], "YResolution": ["Y-resolution"],
'ResolutionUnit': ['Resolution Units', 'resolution_unit'], "ResolutionUnit": ["Resolution Units", "resolution_unit"],
'Rating': ['Rating', 'rating'], "Rating": ["Rating", "rating"],
'RatingPercent': ['Rating Percent', 'rating_percent'], "RatingPercent": ["Rating Percent", "rating_percent"],
} }
EXIF_MAPPING = [ EXIF_MAPPING = [
('Photographer', PHOTOGRAHER_MAPPING), ("Photographer", PHOTOGRAHER_MAPPING),
('Camera', CAMERA_MAPPING), ("Camera", CAMERA_MAPPING),
('Software', SOFTWARE_MAPPING), ("Software", SOFTWARE_MAPPING),
('File', FILE_MAPPING) ("File", FILE_MAPPING),
] ]

157
gallery/views/group.py Normal file
View file

@ -0,0 +1,157 @@
"""
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 sqlalchemy.orm import sessionmaker
from gallery 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"])
def groups():
"""
Group overview, shows all image groups
"""
groups = db_session.query(db.Groups).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)
.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())
.limit(3)
)
# For each image, get the image data and add it to the group item
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])
.first()
)
return render_template("list.html", groups=groups)
@blueprint.route("/<int:group_id>")
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]
)
# 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())
.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()
)
# Check contrast for the first image in the group for the banner
text_colour = "rgb(var(--fg-black))"
if images:
text_colour = contrast.contrast(
images[0].colours[0], "rgb(var(--fg-black))", "rgb(var(--fg-white))"
)
return render_template(
"group.html", group=group, images=images, text_colour=text_colour
)
@blueprint.route("/<int:group_id>/<int:image_id>")
def group_post(group_id, image_id):
"""
Image view, shows the image and its metadata from a specific group
"""
# Get the image, if it doesn't exist, 404
image = db_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]
)
# Get all groups the image is in
groups = (
db_session.query(db.GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id)
.all()
)
# 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])
.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())
.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())
.first()
)
# If there is a next or previous image, get the URL for it
if next_url:
next_url = url_for("group.group_post", group_id=group_id, image_id=next_url[0])
if prev_url:
prev_url = url_for("group.group_post", group_id=group_id, image_id=prev_url[0])
return render_template(
"image.html", image=image, next_url=next_url, prev_url=prev_url
)

View file

@ -1,139 +0,0 @@
"""
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 sqlalchemy.orm import sessionmaker
from gallery 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'])
def groups():
"""
Group overview, shows all image groups
"""
groups = db_session.query(db.Groups).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)
.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())
.limit(3))
# For each image, get the image data and add it to the group item
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])
.first())
return render_template('groups/list.html', groups=groups)
@blueprint.route('/<int:group_id>')
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])
# 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())
.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())
# Check contrast for the first image in the group for the banner
text_colour = 'rgb(var(--fg-black))'
if images:
text_colour = contrast.contrast(images[0].colours[0],
'rgb(var(--fg-black))',
'rgb(var(--fg-white))')
return render_template('groups/group.html',
group=group,
images=images,
text_colour=text_colour)
@blueprint.route('/<int:group_id>/<int:image_id>')
def group_post(group_id, image_id):
"""
Image view, shows the image and its metadata from a specific group
"""
# Get the image, if it doesn't exist, 404
image = (db_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])
# Get all groups the image is in
groups = (db_session.query(db.GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id)
.all())
# 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])
.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())
.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())
.first())
# If there is a next or previous image, get the URL for it
if next_url:
next_url = url_for('group.group_post', group_id=group_id, image_id=next_url[0])
if prev_url:
prev_url = url_for('group.group_post', group_id=group_id, image_id=prev_url[0])
return render_template('image.html', image=image, next_url=next_url, prev_url=prev_url)

94
gallery/views/image.py Normal file
View file

@ -0,0 +1,94 @@
"""
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
blueprint = Blueprint("image", __name__, url_prefix="/image")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/<int:image_id>")
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 :<")
# 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
groups = (
db_session.query(db.GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id)
.all()
)
# For each group, get the group data and add it to the image item
image.groups = []
for group in groups:
image.groups.append(
db_session.query(db.Groups.id, db.Groups.name)
.filter(db.Groups.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())
.first()
)
prev_url = (
db_session.query(db.Posts.id)
.filter(db.Posts.id < image_id)
.order_by(db.Posts.id.desc())
.first()
)
# If there is a next or previous image, get the url
if next_url:
next_url = url_for("image.image", image_id=next_url[0])
if prev_url:
prev_url = url_for("image.image", image_id=prev_url[0])
# Yoink all the images in the database
total_images = db_session.query(db.Posts.id).order_by(db.Posts.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
if len(total_images) <= limit:
return_page = None
else:
# How many pages should there be
for i in range(ceil(len(total_images) / limit)):
# 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:
return_page = i + 1
break
return render_template(
"image.html",
image=image,
next_url=next_url,
prev_url=prev_url,
return_page=return_page,
)

59
gallery/views/index.py Normal file
View file

@ -0,0 +1,59 @@
"""
Onlylegs Gallery - Index view
"""
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
blueprint = Blueprint("gallery", __name__)
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/")
def index():
"""
Home page of the website, shows the feed of the latest images
"""
# meme
if request.args.get("coffee") == "please":
abort(418)
# pagination, defaults to page 1 if no page is specified
page = request.args.get("page", default=1, type=int)
limit = current_app.config["UPLOAD_CONF"]["max-load"]
# get the total number of images in the database
# calculate the total number of pages, and make sure the page number is valid
total_images = db_session.query(db.Posts.id).count()
pages = ceil(max(total_images, limit) / limit)
if page > pages:
abort(
404,
"You have reached the far and beyond, "
+ "but you will not find your answers here.",
)
# 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,
)
.order_by(db.Posts.id.desc())
.offset((page - 1) * limit)
.limit(limit)
.all()
)
return render_template(
"index.html", images=images, total_images=total_images, pages=pages, page=page
)

39
gallery/views/profile.py Normal file
View file

@ -0,0 +1,39 @@
"""
Onlylegs Gallery - Profile view
"""
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
blueprint = Blueprint("profile", __name__, url_prefix="/profile")
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route("/profile")
def profile():
"""
Profile overview, shows all profiles on the onlylegs gallery
"""
user_id = request.args.get("id", default=None, type=int)
# If there is no userID set, check if the user is logged in and display their profile
if not user_id:
if current_user.is_authenticated:
user_id = current_user.id
else:
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()
if not user:
abort(404, "User not found :c")
images = db_session.query(db.Posts).filter(db.Posts.author_id == user_id).all()
return render_template("profile.html", user=user, images=images)

View file

@ -1,102 +0,0 @@
"""
Onlylegs Gallery - Routing
"""
from flask import Blueprint, render_template, url_for, request
from werkzeug.exceptions import abort
from flask_login import current_user
from sqlalchemy.orm import sessionmaker
from gallery import db
blueprint = Blueprint('gallery', __name__)
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route('/')
def index():
"""
Home page of the website, shows the feed of the latest images
"""
images = db_session.query(db.Posts.filename,
db.Posts.alt,
db.Posts.colours,
db.Posts.created_at,
db.Posts.id).order_by(db.Posts.id.desc()).all()
if request.args.get('coffee') == 'please':
abort(418)
return render_template('index.html', images=images)
@blueprint.route('/image/<int:image_id>')
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 :<')
# 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
groups = (db_session.query(db.GroupJunction.group_id)
.filter(db.GroupJunction.post_id == image_id)
.all())
# For each group, get the group data and add it to the image item
image.groups = []
for group in groups:
image.groups.append(db_session.query(db.Groups.id, db.Groups.name)
.filter(db.Groups.id == group[0])
.first())
# Get the next and previous images
next_url = (db_session.query(db.Posts.id)
.filter(db.Posts.id > image_id)
.order_by(db.Posts.id.asc())
.first())
prev_url = (db_session.query(db.Posts.id)
.filter(db.Posts.id < image_id)
.order_by(db.Posts.id.desc())
.first())
# If there is a next or previous image, get the url
if next_url:
next_url = url_for('gallery.image', image_id=next_url[0])
if prev_url:
prev_url = url_for('gallery.image', image_id=prev_url[0])
return render_template('image.html', image=image, next_url=next_url, prev_url=prev_url)
@blueprint.route('/profile')
def profile():
"""
Profile overview, shows all profiles on the onlylegs gallery
"""
user_id = request.args.get('id', default=None, type=int)
# If there is no userID set, check if the user is logged in and display their profile
if not user_id:
if current_user.is_authenticated:
user_id = current_user.id
else:
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()
if not user:
abort(404, 'User not found :c')
images = db_session.query(db.Posts).filter(db.Posts.author_id == user_id).all()
return render_template('profile.html', user=user, images=images)

View file

@ -4,40 +4,40 @@ OnlyLegs - Settings page
from flask import Blueprint, render_template from flask import Blueprint, render_template
from flask_login import login_required from flask_login import login_required
blueprint = Blueprint('settings', __name__, url_prefix='/settings') blueprint = Blueprint("settings", __name__, url_prefix="/settings")
@blueprint.route('/') @blueprint.route("/")
@login_required @login_required
def general(): def general():
""" """
General settings page General settings page
""" """
return render_template('settings/general.html') return render_template("settings/general.html")
@blueprint.route('/server') @blueprint.route("/server")
@login_required @login_required
def server(): def server():
""" """
Server settings page Server settings page
""" """
return render_template('settings/server.html') return render_template("settings/server.html")
@blueprint.route('/account') @blueprint.route("/account")
@login_required @login_required
def account(): def account():
""" """
Account settings page Account settings page
""" """
return render_template('settings/account.html') return render_template("settings/account.html")
@blueprint.route('/logs') @blueprint.route("/logs")
@login_required @login_required
def logs(): def logs():
""" """
Logs settings page Logs settings page
""" """
return render_template('settings/logs.html') return render_template("settings/logs.html")

24
poetry.lock generated
View file

@ -166,6 +166,17 @@ files = [
[package.dependencies] [package.dependencies]
Pillow = "*" Pillow = "*"
[[package]]
name = "cssmin"
version = "0.2.0"
description = "A Python port of the YUI CSS compression algorithm."
category = "main"
optional = false
python-versions = "*"
files = [
{file = "cssmin-0.2.0.tar.gz", hash = "sha256:e012f0cc8401efcf2620332339011564738ae32be8c84b2e43ce8beaec1067b6"},
]
[[package]] [[package]]
name = "dill" name = "dill"
version = "0.3.6" version = "0.3.6"
@ -431,6 +442,17 @@ MarkupSafe = ">=2.0"
[package.extras] [package.extras]
i18n = ["Babel (>=2.7)"] i18n = ["Babel (>=2.7)"]
[[package]]
name = "jsmin"
version = "3.0.1"
description = "JavaScript minifier."
category = "main"
optional = false
python-versions = "*"
files = [
{file = "jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc"},
]
[[package]] [[package]]
name = "lazy-object-proxy" name = "lazy-object-proxy"
version = "1.9.0" version = "1.9.0"
@ -1020,4 +1042,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "431b58579dc3ebde52c8f3905c851556a465d5a82796a8a26718d69cb4915959" content-hash = "58c3430743ce1cfd8e5b89db371a0d454a478cbe79ced08c645b4628980ca9f1"

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "onlylegs" name = "onlylegs"
version = "23.04.05" version = "23.04.08"
description = "Gallery built for fast and simple image management" description = "Gallery built for fast and simple image management"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"] authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT" license = "MIT"
@ -22,6 +22,8 @@ colorthief = "^0.2.1"
Pillow = "^9.4.0" Pillow = "^9.4.0"
platformdirs = "^3.0.0" platformdirs = "^3.0.0"
pylint = "^2.16.3" pylint = "^2.16.3"
jsmin = "^3.0.1"
cssmin = "^0.2.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

13
run.py
View file

@ -5,7 +5,8 @@ from setup.args import PORT, ADDRESS, WORKERS, DEBUG
from setup.configuration import Configuration from setup.configuration import Configuration
print(""" print(
"""
:::::::: :::: ::: ::: ::: ::: ::: ::::::::: ::::::::: :::::::: :::::::: :::: ::: ::: ::: ::: ::: ::::::::: ::::::::: ::::::::
:+: :+: :+:+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
+:+ +:+ :+:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ :+:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
@ -14,8 +15,9 @@ print("""
#+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+#
######## ### #### ########## ### ########## ######### ######### ######## ######## ### #### ########## ### ########## ######### ######### ########
Created by Fluffy Bean - Version 23.04.05 Created by Fluffy Bean - Version 23.04.08
""") """
)
# Run pre-startup checks and load configuration # Run pre-startup checks and load configuration
@ -24,6 +26,7 @@ Configuration()
if DEBUG: if DEBUG:
from gallery import create_app from gallery import create_app
create_app().run(host=ADDRESS, port=PORT, debug=True, threaded=True) create_app().run(host=ADDRESS, port=PORT, debug=True, threaded=True)
else: else:
from setup.runner import OnlyLegs # pylint: disable=C0412 from setup.runner import OnlyLegs # pylint: disable=C0412
@ -33,8 +36,8 @@ else:
sys.argv = [sys.argv[0]] sys.argv = [sys.argv[0]]
options = { options = {
'bind': f'{ADDRESS}:{PORT}', "bind": f"{ADDRESS}:{PORT}",
'workers': WORKERS, "workers": WORKERS,
} }
OnlyLegs(options).run() OnlyLegs(options).run()

View file

@ -13,11 +13,17 @@ Startup arguments for the OnlyLegs gallery
import argparse import argparse
parser = argparse.ArgumentParser(description='Run the OnlyLegs gallery') parser = argparse.ArgumentParser(description="Run the OnlyLegs gallery")
parser.add_argument('-p', '--port', type=int, default=5000, help='Port to run on') parser.add_argument("-p", "--port", type=int, default=5000, help="Port to run on")
parser.add_argument('-a', '--address', type=str, default='127.0.0.0', help='Address to run on') parser.add_argument(
parser.add_argument('-w', '--workers', type=int, default=4, help='Number of workers to run') "-a", "--address", type=str, default="127.0.0.0", help="Address to run on"
parser.add_argument('-d', '--debug', action='store_true', help='Run as Flask app in debug mode') )
parser.add_argument(
"-w", "--workers", type=int, default=4, help="Number of workers to run"
)
parser.add_argument(
"-d", "--debug", action="store_true", help="Run as Flask app in debug mode"
)
args = parser.parse_args() args = parser.parse_args()

View file

@ -9,13 +9,14 @@ import platformdirs
import yaml import yaml
USER_DIR = platformdirs.user_config_dir('onlylegs') USER_DIR = platformdirs.user_config_dir("onlylegs")
class Configuration: class Configuration:
""" """
Setup the application on first run Setup the application on first run
""" """
def __init__(self): def __init__(self):
""" """
Main setup function Main setup function
@ -27,11 +28,11 @@ class Configuration:
self.make_dir() self.make_dir()
# Check if the .env file exists # Check if the .env file exists
if not os.path.exists(os.path.join(USER_DIR, '.env')): if not os.path.exists(os.path.join(USER_DIR, ".env")):
self.make_env() self.make_env()
# Check if the conf.yml file exists # Check if the conf.yml file exists
if not os.path.exists(os.path.join(USER_DIR, 'conf.yml')): if not os.path.exists(os.path.join(USER_DIR, "conf.yml")):
self.make_yaml() self.make_yaml()
# Load the config files # Load the config files
@ -43,8 +44,8 @@ class Configuration:
Create the user directory Create the user directory
""" """
os.makedirs(USER_DIR) os.makedirs(USER_DIR)
os.makedirs(os.path.join(USER_DIR, 'instance')) os.makedirs(os.path.join(USER_DIR, "instance"))
os.makedirs(os.path.join(USER_DIR, 'uploads')) os.makedirs(os.path.join(USER_DIR, "uploads"))
print("Created user directory at:", USER_DIR) print("Created user directory at:", USER_DIR)
@ -54,21 +55,23 @@ class Configuration:
Create the .env file with default values Create the .env file with default values
""" """
env_conf = { env_conf = {
'FLASK_SECRET': os.urandom(32).hex(), "FLASK_SECRET": os.urandom(32).hex(),
} }
with open(os.path.join(USER_DIR, '.env'), encoding='utf-8', mode='w+') as file: with open(os.path.join(USER_DIR, ".env"), encoding="utf-8", mode="w+") as file:
for key, value in env_conf.items(): for key, value in env_conf.items():
file.write(f"{key}={value}\n") file.write(f"{key}={value}\n")
print(""" print(
"""
#################################################### ####################################################
# A NEW KEY WAS GENERATED FOR YOU! PLEASE NOTE # # A NEW KEY WAS GENERATED FOR YOU! PLEASE NOTE #
# DOWN THE FLASK_SECRET KEY LOCATED IN YOUR # # DOWN THE FLASK_SECRET KEY LOCATED IN YOUR #
# .config/onlylegs/.env FOLDER! LOOSING THIS KEY # # .config/onlylegs/.env FOLDER! LOOSING THIS KEY #
# WILL RESULT IN YOU BEING UNABLE TO LOG IN! # # WILL RESULT IN YOU BEING UNABLE TO LOG IN! #
#################################################### ####################################################
""") """
)
@staticmethod @staticmethod
def make_yaml(): def make_yaml():
@ -76,8 +79,8 @@ class Configuration:
Create the YAML config file with default values Create the YAML config file with default values
""" """
is_correct = False is_correct = False
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b') email_regex = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
username_regex = re.compile(r'\b[A-Za-z0-9._%+-]+\b') username_regex = re.compile(r"\b[A-Za-z0-9._%+-]+\b")
print("\nNo config file found, please enter the following information:") print("\nNo config file found, please enter the following information:")
while not is_correct: while not is_correct:
@ -99,46 +102,52 @@ class Configuration:
continue continue
# Check if user is happy with the values # Check if user is happy with the values
if input("Is this correct? (y/n): ").lower() == 'y': if input("Is this correct? (y/n): ").lower() == "y":
is_correct = True is_correct = True
yaml_conf = { yaml_conf = {
'admin': { "admin": {
'name': name, "name": name,
'username': username, "username": username,
'email': email, "email": email,
}, },
'upload': { "upload": {
'allowed-extensions': { "allowed-extensions": {
'jpg': 'jpeg', "jpg": "jpeg",
'jpeg': 'jpeg', "jpeg": "jpeg",
'png': 'png', "png": "png",
'webp': 'webp', "webp": "webp",
}, },
'max-size': 69, "max-size": 69,
'rename': 'GWA_{{username}}_{{time}}', "max-load": 50,
"rename": "GWA_{{username}}_{{time}}",
},
"website": {
"name": "OnlyLegs",
"motto": "A gallery built for fast and simple image management!",
"language": "en",
}, },
'website': {
'name': 'OnlyLegs',
'motto': 'A gallery built for fast and simple image management!',
'language': 'en',
}
} }
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8', mode='w+') as file: with open(
os.path.join(USER_DIR, "conf.yml"), encoding="utf-8", mode="w+"
) as file:
yaml.dump(yaml_conf, file, default_flow_style=False) yaml.dump(yaml_conf, file, default_flow_style=False)
print("Generated config file, you can change these values in the settings of the app") print(
"Generated config file, you can change these values in the settings of the app"
)
@staticmethod @staticmethod
def logging_config(): def logging_config():
""" """
Set the logging config Set the logging config
""" """
logging.getLogger('werkzeug').disabled = True logging.getLogger("werkzeug").disabled = True
logging.basicConfig( logging.basicConfig(
filename=os.path.join(USER_DIR, 'only.log'), filename=os.path.join(USER_DIR, "only.log"),
level=logging.INFO, level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S', datefmt="%Y-%m-%d %H:%M:%S",
format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s', format="%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s",
encoding='utf-8') encoding="utf-8",
)

View file

@ -9,6 +9,7 @@ class OnlyLegs(Application):
""" """
Gunicorn application Gunicorn application
""" """
def __init__(self, options={}): # pylint: disable=W0102, W0231 def __init__(self, options={}): # pylint: disable=W0102, W0231
self.usage = None self.usage = None
self.callable = None self.callable = None
@ -27,7 +28,7 @@ class OnlyLegs(Application):
@staticmethod @staticmethod
def prog(): # pylint: disable=C0116, E0202 def prog(): # pylint: disable=C0116, E0202
return 'OnlyLegs' return "OnlyLegs"
def load(self): def load(self):
return util.import_app('gallery:create_app()') return util.import_app("gallery:create_app()")