This commit is contained in:
Michał Gdula 2023-08-04 17:34:08 +00:00
parent 4c7bf9706f
commit d19a33501a
36 changed files with 808 additions and 1052 deletions

View file

@ -1,140 +0,0 @@
"""
Onlylegs Gallery
This is the main app file, it loads all the other files and sets up the app
"""
import os
import logging
from flask_assets import Bundle
from flask_migrate import init as migrate_init
from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash
from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
from onlylegs.models import User
from onlylegs.views import (
index as view_index,
image as view_image,
group as view_group,
settings as view_settings,
profile as view_profile,
)
from onlylegs.api import media as api_media, group as api_group, account as api_account
from onlylegs import auth as view_auth
from onlylegs import filters
from onlylegs import gwagwa
def create_app(): # pylint: disable=R0914
"""
Create and configure the main app
"""
app = Flask(__name__, instance_path=INSTANCE_DIR)
app.config.from_pyfile("config.py")
# DATABASE
db.init_app(app)
migrate.init_app(app, db, directory=MIGRATIONS_DIR)
# If database file doesn't exist, create it
if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
print("Creating database")
with app.app_context():
db.create_all()
register_user = User(
username=app.config["ADMIN_CONF"]["username"],
email=app.config["ADMIN_CONF"]["email"],
password=generate_password_hash("changeme!", method="sha256"),
)
db.session.add(register_user)
db.session.commit()
print(
"""
####################################################
# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #
# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #
# PLEASE UPDATE IT IN THE SETTINGS! #
####################################################
"""
)
# Check if migrations directory exists, if not create it
with app.app_context():
if not os.path.exists(MIGRATIONS_DIR):
print("Creating migrations directory")
migrate_init(directory=MIGRATIONS_DIR)
# LOGIN MANAGER
# can also set session_protection to "strong"
# this would protect against session hijacking
login_manager.init_app(app)
login_manager.login_view = "onlylegs.index"
@login_manager.user_loader
def load_user(user_id):
return User.query.filter_by(alt_id=user_id).first()
@login_manager.unauthorized_handler
def unauthorized():
error = 401
msg = "You are not authorized to view this page!!!!"
return render_template("error.html", error=error, msg=msg), error
# ERROR HANDLERS
@app.errorhandler(Exception)
def error_page(err):
"""
Error handlers, if the error is not a HTTP error, return 500
"""
if not isinstance(err, HTTPException):
abort(500)
return (
render_template("error.html", error=err.code, msg=err.description),
err.code,
)
# ASSETS
assets.init_app(app)
scripts = Bundle(
"js/*.js", output="gen/js.js", depends="js/*.js"
) # filter jsmin is broken :c
styles = Bundle(
"sass/style.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
)
assets.register("scripts", scripts)
assets.register("styles", styles)
# BLUEPRINTS
app.register_blueprint(view_auth.blueprint)
app.register_blueprint(view_index.blueprint)
app.register_blueprint(view_image.blueprint)
app.register_blueprint(view_group.blueprint)
app.register_blueprint(view_profile.blueprint)
app.register_blueprint(view_settings.blueprint)
# APIS
app.register_blueprint(api_media.blueprint)
app.register_blueprint(api_group.blueprint)
app.register_blueprint(api_account.blueprint)
# FILTERS
app.register_blueprint(filters.blueprint)
# CACHE AND COMPRESS
cache.init_app(app)
compress.init_app(app)
# Yupee! We got there :3
print("Done!")
logging.info("Gallery started successfully!")
return app

293
onlylegs/api.py Normal file
View file

@ -0,0 +1,293 @@
"""
Onlylegs - API endpoints
"""
import os
import pathlib
import re
import logging
from uuid import uuid4
from flask import (
Blueprint,
flash,
abort,
send_from_directory,
jsonify,
request,
current_app,
)
from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import Users, Pictures, Albums, AlbumJunction
from onlylegs.utils.metadata import yoink
from onlylegs.utils.generate_image import generate_thumbnail
blueprint = Blueprint("api", __name__, url_prefix="/api")
@blueprint.route("/account/picture/<int:user_id>", methods=["POST"])
@login_required
def account_picture(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(Users, user_id)
file = request.files.get("file", None)
# If no image is uploaded, return 404 error
if not file:
return jsonify({"error": "No file uploaded"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower()
img_name = str(user.id)
img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext)
# Check if file extension is allowed
if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
logging.info("File extension not allowed: %s", img_ext)
return jsonify({"error": "File extension not allowed"}), 403
if user.picture:
# Delete cached files and old image
os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture))
cache_name = user.picture.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
# Save file
try:
file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"error": "Error saving file"}), 500
img_colors = ColorThief(img_path).get_color()
# Save to database
user.colour = img_colors
user.picture = str(img_name + "." + img_ext)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200
@blueprint.route("/account/username/<int:user_id>", methods=["POST"])
@login_required
def account_username(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(Users, user_id)
new_name = request.form["name"]
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
# Validate the form
if not new_name or not username_regex.match(new_name):
return jsonify({"error": "Username is invalid"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Save to database
user.username = new_name
db.session.commit()
return jsonify({"message": "Username changed"}), 200
@blueprint.route("/media/<path:path>", methods=["GET"])
def media(path):
"""
Returns image from media folder
r for resolution, thumb for thumbnail etc
e for extension, jpg, png etc
"""
res = request.args.get("r", default=None).strip()
ext = request.args.get("e", default=None).strip()
# if no args are passed, return the raw file
if not res and not ext:
if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)):
abort(404)
return send_from_directory(current_app.config["MEDIA_FOLDER"], path)
# Generate thumbnail, if None is returned a server error occured
thumb = generate_thumbnail(path, res, ext)
if not thumb:
abort(500)
response = send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
response.headers["Cache-Control"] = "public, max-age=31536000"
response.headers["Expires"] = "31536000"
return response
@blueprint.route("/media/upload", methods=["POST"])
@login_required
def upload():
"""
Uploads an image to the server and saves it to the database
"""
form_file = request.files.get("file", None)
form = request.form
if not form_file:
return jsonify({"message": "No file"}), 400
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower()
img_name = "GWAGWA_" + str(uuid4())
img_path = os.path.join(
current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext
)
# Check if file extension is allowed
if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
logging.info("File extension not allowed: %s", img_ext)
return jsonify({"message": "File extension not allowed"}), 403
# Save file
try:
form_file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"message": "Error saving file"}), 500
img_exif = yoink(img_path) # Get EXIF data
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database
query = Pictures(
author_id=current_user.id,
filename=img_name + "." + img_ext,
mimetype=img_ext,
exif=img_exif,
colours=img_colors,
description=form["description"],
alt=form["alt"],
)
db.session.add(query)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200
@blueprint.route("/media/delete/<int:image_id>", methods=["POST"])
@login_required
def delete_image(image_id):
"""
Deletes an image from the server and database
"""
post = db.get_or_404(Pictures, image_id)
# Check if image exists and if user is allowed to delete it (author)
if post.author_id != current_user.id:
logging.info("User %s tried to delete image %s", current_user.id, image_id)
return (
jsonify({"message": "You are not allowed to delete this image, heck off"}),
403,
)
# Delete file
try:
os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], post.filename))
except FileNotFoundError:
logging.warning(
"File not found: %s, already deleted or never existed", post.filename
)
# Delete cached files
cache_name = post.filename.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
AlbumJunction.query.filter_by(picture_id=image_id).delete()
db.session.delete(post)
db.session.commit()
logging.info("Removed image (%s) %s", image_id, post.filename)
flash(["Image was all in Le Head!", "1"])
return jsonify({"message": "Image deleted"}), 200
@blueprint.route("/group/create", methods=["POST"])
@login_required
def create_group():
"""
Creates a group
"""
group_name = request.form.get("name", "").strip()
group_description = request.form.get("description", "").strip()
group_author = current_user.id
new_group = Albums(
name=group_name,
description=group_description,
author_id=group_author,
)
db.session.add(new_group)
db.session.commit()
return jsonify({"message": "Group created", "id": new_group.id})
@blueprint.route("/group/modify", methods=["POST"])
@login_required
def modify_group():
"""
Changes the images in a group
"""
group_id = request.form.get("group", "").strip()
image_id = request.form.get("image", "").strip()
action = request.form.get("action", "").strip()
group = db.get_or_404(Albums, group_id)
db.get_or_404(Pictures, image_id) # Check if image exists
if group.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
junction_exist = AlbumJunction.query.filter_by(
album_id=group_id, picture_id=image_id
).first()
if action == "add" and not junction_exist:
db.session.add(AlbumJunction(album_id=group_id, picture_id=image_id))
elif request.form["action"] == "remove":
AlbumJunction.query.filter_by(album_id=group_id, picture_id=image_id).delete()
db.session.commit()
return jsonify({"message": "Group modified"})
@blueprint.route("/group/delete", methods=["POST"])
def delete_group():
"""
Deletes a group
"""
group_id = request.form.get("group", "").strip()
group = db.get_or_404(Albums, group_id)
if group.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
AlbumJunction.query.filter_by(album_id=group_id).delete()
db.session.delete(group)
db.session.commit()
flash(["Group yeeted!", "1"])
return jsonify({"message": "Group deleted"})

View file

@ -1,93 +0,0 @@
"""
Onlylegs - API endpoints
"""
import os
import pathlib
import re
import logging
from flask import Blueprint, jsonify, request, current_app
from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import User
blueprint = Blueprint("account_api", __name__, url_prefix="/api/account")
@blueprint.route("/picture/<int:user_id>", methods=["POST"])
@login_required
def account_picture(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(User, user_id)
file = request.files["file"]
# If no image is uploaded, return 404 error
if not file:
return jsonify({"error": "No file uploaded"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower()
img_name = str(user.id)
img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext)
# Check if file extension is allowed
if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
logging.info("File extension not allowed: %s", img_ext)
return jsonify({"error": "File extension not allowed"}), 403
if user.picture:
# Delete cached files and old image
os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture))
cache_name = user.picture.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
# Save file
try:
file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"error": "Error saving file"}), 500
img_colors = ColorThief(img_path).get_color()
# Save to database
user.colour = img_colors
user.picture = str(img_name + "." + img_ext)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200
@blueprint.route("/username/<int:user_id>", methods=["POST"])
@login_required
def account_username(user_id):
"""
Returns the profile of a user
"""
user = db.get_or_404(User, user_id)
new_name = request.form["name"]
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
# Validate the form
if not new_name or not username_regex.match(new_name):
return jsonify({"error": "Username is invalid"}), 400
if user.id != current_user.id:
return jsonify({"error": "You are not allowed to do this, go away"}), 403
# Save to database
user.username = new_name
db.session.commit()
return jsonify({"message": "Username changed"}), 200

View file

@ -1,78 +0,0 @@
"""
Onlylegs - API endpoints
"""
from flask import Blueprint, flash, jsonify, request
from flask_login import login_required, current_user
from onlylegs.extensions import db
from onlylegs.models import Post, Group, GroupJunction
blueprint = Blueprint("group_api", __name__, url_prefix="/api/group")
@blueprint.route("/create", methods=["POST"])
@login_required
def create_group():
"""
Creates a group
"""
new_group = Group(
name=request.form["name"],
description=request.form["description"],
author_id=current_user.id,
)
db.session.add(new_group)
db.session.commit()
return jsonify({"message": "Group created", "id": new_group.id})
@blueprint.route("/modify", methods=["POST"])
@login_required
def modify_group():
"""
Changes the images in a group
"""
group_id = request.form["group"]
image_id = request.form["image"]
action = request.form["action"]
group = db.get_or_404(Group, group_id)
db.get_or_404(Post, image_id) # Check if image exists
if group.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
if (
action == "add"
and not GroupJunction.query.filter_by(
group_id=group_id, post_id=image_id
).first()
):
db.session.add(GroupJunction(group_id=group_id, post_id=image_id))
elif request.form["action"] == "remove":
GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).delete()
db.session.commit()
return jsonify({"message": "Group modified"})
@blueprint.route("/delete", methods=["POST"])
def delete_group():
"""
Deletes a group
"""
group_id = request.form["group"]
group = db.get_or_404(Group, group_id)
if group.author_id != current_user.id:
return jsonify({"message": "You are not the owner of this group"}), 403
GroupJunction.query.filter_by(group_id=group_id).delete()
db.session.delete(group)
db.session.commit()
flash(["Group yeeted!", "1"])
return jsonify({"message": "Group deleted"})

View file

@ -1,148 +0,0 @@
"""
Onlylegs - API endpoints
Media upload and retrieval
"""
from uuid import uuid4
import os
import pathlib
import logging
from flask import (
Blueprint,
flash,
abort,
send_from_directory,
jsonify,
request,
current_app,
)
from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import Post, GroupJunction
from onlylegs.utils.metadata import yoink
from onlylegs.utils.generate_image import generate_thumbnail
blueprint = Blueprint("media_api", __name__, url_prefix="/api/media")
@blueprint.route("/<path:path>", methods=["GET"])
def media(path):
"""
Returns image from media folder
r for resolution, thumb for thumbnail etc
e for extension, jpg, png etc
"""
res = request.args.get("r", default=None, type=str)
ext = request.args.get("e", default=None, type=str)
# if no args are passed, return the raw file
if not res and not ext:
if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)):
abort(404)
return send_from_directory(current_app.config["MEDIA_FOLDER"], path)
# Generate thumbnail, if None is returned a server error occured
thumb = generate_thumbnail(path, res, ext)
if not thumb:
abort(500)
response = send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
response.headers["Cache-Control"] = "public, max-age=31536000"
response.headers["Expires"] = "31536000"
return response
@blueprint.route("/upload", methods=["POST"])
@login_required
def upload():
"""
Uploads an image to the server and saves it to the database
"""
form_file = request.files["file"]
form = request.form
if not form_file:
return jsonify({"message": "No file"}), 400
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower()
img_name = "GWAGWA_" + str(uuid4())
img_path = os.path.join(
current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext
)
# Check if file extension is allowed
if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
logging.info("File extension not allowed: %s", img_ext)
return jsonify({"message": "File extension not allowed"}), 403
# Save file
try:
form_file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"message": "Error saving file"}), 500
img_exif = yoink(img_path) # Get EXIF data
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database
query = Post(
author_id=current_user.id,
filename=img_name + "." + img_ext,
mimetype=img_ext,
exif=img_exif,
colours=img_colors,
description=form["description"],
alt=form["alt"],
)
db.session.add(query)
db.session.commit()
return jsonify({"message": "File uploaded"}), 200
@blueprint.route("/delete/<int:image_id>", methods=["POST"])
@login_required
def delete_image(image_id):
"""
Deletes an image from the server and database
"""
post = db.get_or_404(Post, image_id)
# Check if image exists and if user is allowed to delete it (author)
if post.author_id != current_user.id:
logging.info("User %s tried to delete image %s", current_user.id, image_id)
return (
jsonify({"message": "You are not allowed to delete this image, heck off"}),
403,
)
# Delete file
try:
os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], post.filename))
except FileNotFoundError:
logging.warning(
"File not found: %s, already deleted or never existed", post.filename
)
# Delete cached files
cache_name = post.filename.rsplit(".")[0]
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
cache_name + "*"
):
os.remove(cache_file)
GroupJunction.query.filter_by(post_id=image_id).delete()
db.session.delete(post)
db.session.commit()
logging.info("Removed image (%s) %s", image_id, post.filename)
flash(["Image was all in Le Head!", "1"])
return jsonify({"message": "Image deleted"}), 200

134
onlylegs/app.py Normal file
View file

@ -0,0 +1,134 @@
"""
Onlylegs Gallery
This is the main app file, it loads all the other files and sets up the app
"""
import os
import logging
from flask_assets import Bundle
from flask_migrate import init as migrate_init
from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash
from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
from onlylegs.models import Users
from onlylegs.views import (
index as view_index,
image as view_image,
group as view_group,
settings as view_settings,
profile as view_profile,
)
from onlylegs import api
from onlylegs import auth as view_auth
from onlylegs import filters
app = Flask(__name__, instance_path=INSTANCE_DIR)
app.config.from_pyfile("config.py")
# DATABASE
db.init_app(app)
migrate.init_app(app, db, directory=MIGRATIONS_DIR)
# If database file doesn't exist, create it
if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
print("Creating database")
with app.app_context():
db.create_all()
register_user = Users(
username=app.config["ADMIN_CONF"]["username"],
email=app.config["ADMIN_CONF"]["email"],
password=generate_password_hash("changeme!", method="sha256"),
)
db.session.add(register_user)
db.session.commit()
print(
"""
####################################################
# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #
# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #
# PLEASE UPDATE IT IN THE SETTINGS! #
####################################################
"""
)
# Check if migrations directory exists, if not create it
with app.app_context():
if not os.path.exists(MIGRATIONS_DIR):
print("Creating migrations directory")
migrate_init(directory=MIGRATIONS_DIR)
# LOGIN MANAGER
# can also set session_protection to "strong"
# this would protect against session hijacking
login_manager.init_app(app)
login_manager.login_view = "onlylegs.index"
@login_manager.user_loader
def load_user(user_id):
return Users.query.filter_by(alt_id=user_id).first()
@login_manager.unauthorized_handler
def unauthorized():
error = 401
msg = "You are not authorized to view this page!!!!"
return render_template("error.html", error=error, msg=msg), error
# ERROR HANDLERS
@app.errorhandler(Exception)
def error_page(err):
"""
Error handlers, if the error is not a HTTP error, return 500
"""
if not isinstance(err, HTTPException):
abort(500)
return (
render_template("error.html", error=err.code, msg=err.description),
err.code,
)
# ASSETS
assets.init_app(app)
scripts = Bundle(
"js/*.js", output="gen/js.js", depends="js/*.js"
) # filter jsmin is broken :c
styles = Bundle(
"sass/style.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
)
assets.register("scripts", scripts)
assets.register("styles", styles)
# BLUEPRINTS
app.register_blueprint(view_auth.blueprint)
app.register_blueprint(view_index.blueprint)
app.register_blueprint(view_image.blueprint)
app.register_blueprint(view_group.blueprint)
app.register_blueprint(view_profile.blueprint)
app.register_blueprint(view_settings.blueprint)
app.register_blueprint(api.blueprint)
# FILTERS
app.register_blueprint(filters.blueprint)
# CACHE AND COMPRESS
cache.init_app(app)
compress.init_app(app)
# Yupee! We got there :3
print("Done!")
logging.info("Gallery started successfully!")

View file

@ -11,7 +11,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
from flask_login import login_user, logout_user, login_required
from onlylegs.extensions import db
from onlylegs.models import User
from onlylegs.models import Users
blueprint = Blueprint("auth", __name__, url_prefix="/auth")
@ -28,7 +28,7 @@ def login():
password = request.form["password"].strip()
remember = bool(request.form["remember-me"])
user = User.query.filter_by(username=username).first()
user = Users.query.filter_by(username=username).first()
if not user or not check_password_hash(user.password, password):
logging.error("Login attempt from %s", request.remote_addr)
@ -77,7 +77,7 @@ def register():
elif password_repeat != password:
error.append("Passwords do not match!")
user_exists = User.query.filter_by(username=username).first()
user_exists = Users.query.filter_by(username=username).first()
if user_exists:
error.append("User already exists!")
@ -86,7 +86,7 @@ def register():
print(error)
return jsonify(error), 400
register_user = User(
register_user = Users(
username=username,
email=email,
password=generate_password_hash(password, method="sha256"),

View file

@ -13,4 +13,4 @@ migrate = Migrate()
login_manager = LoginManager()
assets = Environment()
compress = Compress()
cache = Cache(config={'CACHE_TYPE': 'simple', "CACHE_DEFAULT_TIMEOUT": 300})
cache = Cache(config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 300})

View file

@ -3,7 +3,7 @@ OnlyLegs filters
Custom Jinja2 filters
"""
from flask import Blueprint
from onlylegs.utils.colour import contrast
from onlylegs.utils import colour as colour_utils
blueprint = Blueprint("filters", __name__)
@ -16,7 +16,5 @@ def colour_contrast(colour):
a css variable based on the contrast of text required to be readable
"color: var(--fg-white);" or "color: var(--fg-black);"
"""
bright = "var(--fg-white)"
dark = "var(--fg-black)"
return "color: RGB(" + contrast(colour, dark, bright) + ");"
colour_obj = colour_utils.Colour(colour)
return "rgb(var(--fg-black));" if colour_obj.is_light() else "rgb(var(--fg-white));"

View file

@ -1,4 +0,0 @@
"""
Gwa Gwa!
"""
print("Gwa Gwa!")

View file

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

View file

@ -6,18 +6,18 @@ from flask_login import UserMixin
from onlylegs.extensions import db
class GroupJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
class AlbumJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Junction table for posts and groups
Joins with posts and groups
Junction table for picturess and albums
Joins with picturess and albums
"""
__tablename__ = "group_junction"
__tablename__ = "album_junction"
id = db.Column(db.Integer, primary_key=True)
group_id = db.Column(db.Integer, db.ForeignKey("group.id"))
post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
album_id = db.Column(db.Integer, db.ForeignKey("albums.id"))
picture_id = db.Column(db.Integer, db.ForeignKey("pictures.id"))
date_added = db.Column(
db.DateTime,
@ -26,16 +26,15 @@ class GroupJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
)
class Post(db.Model): # pylint: disable=too-few-public-methods, C0103
class Pictures(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Post table
Pictures table
"""
__tablename__ = "post"
__tablename__ = "pictures"
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
filename = db.Column(db.String, unique=True, nullable=False)
mimetype = db.Column(db.String, nullable=False)
@ -51,37 +50,37 @@ class Post(db.Model): # pylint: disable=too-few-public-methods, C0103
server_default=db.func.now(), # pylint: disable=E1102
)
junction = db.relationship("GroupJunction", backref="posts")
album_fk = db.relationship("AlbumJunction", backref="pictures")
class Group(db.Model): # pylint: disable=too-few-public-methods, C0103
class Albums(db.Model): # pylint: disable=too-few-public-methods, C0103
"""
Group table
albums table
"""
__tablename__ = "group"
__tablename__ = "albums"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
description = db.Column(db.String, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(), # pylint: disable=E1102
)
junction = db.relationship("GroupJunction", backref="groups")
album_fk = db.relationship("AlbumJunction", backref="albums")
class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103
class Users(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103
"""
User table
Users table
"""
__tablename__ = "user"
__tablename__ = "users"
# Gallery used information
id = db.Column(db.Integer, primary_key=True)
@ -100,8 +99,8 @@ class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C010
server_default=db.func.now(), # pylint: disable=E1102
)
posts = db.relationship("Post", backref="author")
groups = db.relationship("Group", backref="author")
pictures_fk = db.relationship("Pictures", backref="author")
albums_fk = db.relationship("Albums", backref="author")
def get_id(self):
return str(self.alt_id)

Binary file not shown.

View file

@ -1,7 +0,0 @@
@font-face {
font-family: 'Rubik';
src: url('./Rubik.ttf') format('truetype');
font-style: normal;
font-display: block;
font-weight: 300 900;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -1,18 +0,0 @@
{
"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

@ -23,9 +23,8 @@
*
box-sizing: border-box
font-family: $font
scrollbar-color: RGB($primary) transparent
font-family: $font
::-webkit-scrollbar
width: 0.5rem

View file

@ -1,37 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ config.WEBSITE_CONF.name }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ config.WEBSITE_CONF.name }}</title>
<meta name="description" content="{{ config.WEBSITE_CONF.motto }}">
<meta name="author" content="{{ config.WEBSITE_CONF.author }}">
<meta name="description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta name="author" content="{{ config.WEBSITE_CONF.author }}"/>
<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:title" content="{{ config.WEBSITE_CONF.name }}"/>
<meta property="og:description" content="{{ config.WEBSITE_CONF.motto }}"/>
<meta property="og:type" content="website"/>
<meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}">
<meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}">
<meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}"/>
<meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}"/>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap">
<!-- phosphor icons!!! -->
<!-- phosphor icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<link
href="{{url_for('static', filename='logo-black.svg')}}"
rel="icon"
type="image/svg+xml"
media="(prefers-color-scheme: light)"/>
<link
href="{{url_for('static', filename='logo-white.svg')}}"
rel="icon"
type="image/svg+xml"
media="(prefers-color-scheme: dark)"/>
<!-- Favicon -->
<link rel="icon" href="{{url_for('static', filename='icon.png')}}" type="image/png">
<link rel="manifest" href="static/manifest.json"/>
<link rel="prefetch" href="{{url_for('static', filename='fonts/font.css')}}" type="stylesheet"/>
{% assets "scripts" %} <script type="text/javascript" src="{{ ASSET_URL }}"></script> {% endassets %}
{% assets "styles" %} <link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer> {% endassets %}
{% block head %}{% endblock %}
@ -70,14 +65,14 @@
{% if current_user.picture %}
<span class="nav-pfp">
<picture>
<source srcset="{{ url_for('media_api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=webp">
<source srcset="{{ url_for('media_api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=png">
<source srcset="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=webp">
<source srcset="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=png">
<img
src="{{ url_for('media_api.media', path='pfp/' + current_user.picture) }}?r=icon"
src="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=icon"
alt="Profile picture"
onload="imgFade(this)"
style="opacity:0;"
/>
>
</picture>
</span>
{% else %}
@ -109,12 +104,12 @@
<button class="fileDrop-block" type="button">
<i class="ph ph-upload"></i>
<span class="status">Choose or Drop file</span>
<input type="file" id="file" tab-index="-1"/>
<input type="file" id="file" tab-index="-1">
</button>
<input class="input-block" type="text" placeholder="alt" id="alt"/>
<input class="input-block" type="text" placeholder="description" id="description"/>
<input class="input-block" type="text" placeholder="tags" id="tags"/>
<input class="input-block" type="text" placeholder="alt" id="alt">
<input class="input-block" type="text" placeholder="description" id="description">
<input class="input-block" type="text" placeholder="tags" id="tags">
<button class="btn-block primary" type="submit">Upload</button>
</form>
<div class="upload-jobs"></div>

View file

@ -4,8 +4,8 @@
{% block head %}
{% if images %}
<meta property="og:image" content="{{ url_for('media_api.media', path='uploads/' + images.0.filename) }}"/>
<meta name="twitter:image" content="{{ url_for('media_api.media', path='uploads/' + images.0.filename) }}">
<meta property="og:image" content="{{ url_for('api.media', path='uploads/' + images.0.filename) }}"/>
<meta name="twitter:image" content="{{ url_for('api.media', path='uploads/' + images.0.filename) }}">
<meta name="theme-color" content="rgb{{ images.0.colours.0 }}"/>
<meta name="twitter:card" content="summary_large_image">
{% endif %}
@ -47,7 +47,7 @@
const formData = new FormData();
formData.append("group", formID);
fetch('{{ url_for('group_api.delete_group') }}', {
fetch('{{ url_for('api.delete_group') }}', {
method: 'POST',
body: formData
}).then(response => {
@ -143,7 +143,7 @@
formData.append("image", formImage);
formData.append("action", formAction);
fetch('{{ url_for('group_api.modify_group') }}', {
fetch('{{ url_for('api.modify_group') }}', {
method: 'POST',
body: formData
}).then(response => {
@ -185,9 +185,9 @@
.navigation-item.selected { color: {{ text_colour }} !important; }
.banner-header,
.banner-info,
.banner-subtitle {
.banner .banner-content .banner-header,
.banner .banner-content .banner-info,
.banner .banner-content .banner-subtitle {
color: {{ text_colour }} !important;
}
.banner-content .link {
@ -215,10 +215,10 @@
{% if images %}
<div class="banner">
<picture>
<source srcset="{{ url_for('media_api.media', path='uploads/' + images.0.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('media_api.media', path='uploads/' + images.0.filename) }}?r=prev&e=png">
<source srcset="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev&e=png">
<img
src="{{ url_for('media_api.media', path='uploads/' + images.0.filename) }}?r=prev"
src="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev"
alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"
onload="imgFade(this)" style="opacity:0;"
/>

View file

@ -3,8 +3,8 @@
{% if return_page %}?page={{ return_page }}{% endif %}{% endblock %}
{% block head %}
<meta property="og:image" content="{{ url_for('media_api.media', path='uploads/' + image.filename) }}"/>
<meta name="twitter:image" content="{{ url_for('media_api.media', path='uploads/' + image.filename) }}">
<meta property="og:image" content="{{ url_for('api.media', path='uploads/' + image.filename) }}"/>
<meta name="twitter:image" content="{{ url_for('api.media', path='uploads/' + image.filename) }}">
<meta name="theme-color" content="rgb{{ image.colours.0 }}"/>
<meta name="twitter:card" content="summary_large_image">
@ -47,7 +47,7 @@
function deleteConfirm() {
popupDissmiss();
fetch('{{ url_for('media_api.delete_image', image_id=image['id']) }}', {
fetch('{{ url_for('api.delete_image', image_id=image['id']) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -87,7 +87,7 @@
<div>
<button class="pill-item" onclick="fullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button>
<button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
<a class="pill-item" href="{{ url_for('media_api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a>
<a class="pill-item" href="{{ url_for('api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a>
</div>
{% if current_user.id == image.author.id %}
<div>
@ -104,18 +104,18 @@
{% block content %}
<div class="background">
<picture>
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
<img src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
<img src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
</picture>
</div>
<div class="image-container {% if close_tab %}collapsed{% endif %}">
<picture>
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
<img
src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev"
src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev"
alt="{{ image.alt }}"
onload="imgFade(this)"
style="opacity:0;"
@ -138,7 +138,9 @@
<tr>
<td>Author</td>
<td>
<img src="{{ url_for('media_api.media', path='pfp/' + image.author.picture) }}" alt="Profile Picture" class="pfp" onload="imgFade(this)" style="opacity: 0;"/>
{% if image.author.picture %}
<img src="{{ url_for('api.media', path='pfp/' + image.author.picture) }}" alt="Profile Picture" class="pfp" onload="imgFade(this)" style="opacity: 0;"/>
{% endif %}
<a href="{{ url_for('profile.profile', id=image.author.id) }}" class="link">{{ image.author.username }}</a>
</td>
</tr>
@ -155,7 +157,9 @@
</table>
<div class="img-colours">
{% for col in image.colours %}
<button style="background-color: rgb{{ col }}" onclick="copyToClipboard('rgb{{ col }}')"><i class="ph-fill ph-paint-bucket" style="{{ col|colour_contrast }}"></i></button>
<button style="background-color: rgb{{ col }}" onclick="copyToClipboard('rgb{{ col }}')">
<i class="ph-fill ph-paint-bucket" style="color:{{ col|colour_contrast }};"></i>
</button>
{% endfor %}
</div>
{% if image.groups %}

View file

@ -66,7 +66,7 @@
formData.append("name", formName);
formData.append("description", formDescription);
fetch('{{ url_for('group_api.create_group') }}', {
fetch('{{ url_for('api.create_group') }}', {
method: 'POST',
body: formData
}).then(response => {
@ -134,10 +134,10 @@
{% if group.images|length > 0 %}
{% for image in group.images %}
<picture>
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
<img
src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb"
src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb"
alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}"
class="data-{{ loop.index }}"
onload="imgFade(this)"

View file

@ -1,11 +1,19 @@
{% macro gallery_item(image) %}
<a id="image-{{ image.id }}" class="gallery-item square" href="{{ url_for('image.image', image_id=image.id) }}" style="background-color: rgb{{ image.colours.0 }}">
<div class="image-filter"><p class="image-title"><span class="time">{{ image.created_at }}</span></p></div>
<a
id="image-{{ image.id }}"
class="gallery-item square"
href="{{ url_for('image.image', image_id=image.id) }}"
style="background-color: rgb{{ image.colours.0 }}"
draggable="false">
<div class="image-filter">
<p class="image-subtitle">By {{ image.username }}</p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<picture>
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
<source srcset="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
<source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
<img
src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb"
src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb"
alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}"
onload="imgFade(this)"
style="opacity:0;"

View file

@ -4,8 +4,8 @@
{% block head %}
{% if user.picture %}
<meta property="og:image" content="{{ url_for('media_api.media', path='pfp/' + user.picture) }}"/>
<meta name="twitter:image" content="{{ url_for('media_api.media', path='pfp/' + user.picture) }}">
<meta property="og:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}"/>
<meta name="twitter:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}">
{% endif %}
{% if user.colour %}<meta name="theme-color" content="rgb{{ user.colour }}"/>{% endif %}
<meta name="twitter:card" content="summary">
@ -37,10 +37,10 @@
<div class="banner-content">
{% if user.picture %}
<picture class="banner-picture">
<source srcset="{{ url_for('media_api.media', path='pfp/' + user.picture) }}?r=pfp&e=webp">
<source srcset="{{ url_for('media_api.media', path='pfp/' + user.picture) }}?r=pfp&e=png">
<source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=webp">
<source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=png">
<img
src="{{ url_for('media_api.media', path='pfp/' + user.picture) }}?r=pfp"
src="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp"
alt="Profile picture"
onload="imgFade(this)"
style="opacity:0;"

View file

@ -23,12 +23,12 @@
<button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
</div>
<div class="info-table">
<form method="POST" action="{{ url_for('account_api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data">
<form method="POST" action="{{ url_for('api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data">
<h3>Profile Picture</h3>
<input type="file" name="file" tab-index="-1"/>
<input type="submit" value="Upload" class="btn-block">
</form>
<form method="POST" action="{{ url_for('account_api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data">
<form method="POST" action="{{ url_for('api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data">
<h3>Username</h3>
<input type="text" name="name" class="input-block" value="{{ current_user.username }}" />
<input type="submit" value="Upload" class="btn-block"/>

View file

@ -1 +0,0 @@
# :3

View file

@ -3,129 +3,67 @@ Colour tools used by OnlyLegs
Source 1: https://gist.github.com/mathebox/e0805f72e7db3269ec22
"""
import math
def contrast(background, light, dark, threshold=0.179):
"""
background: tuple of (r, g, b) values
light: color to use if the background is light
dark: color to use if the background is dark
threshold: the threshold to use for determining lightness, the default is w3 recommended
"""
red = background[0]
green = background[1]
blue = background[2]
class Colour:
def __init__(self, rgb):
self.rgb = rgb
# Calculate contrast
colors = [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 colors
]
lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
def is_light(self, threshold=0.179):
"""
returns True if background is light, False if dark
threshold: the threshold to use for determining lightness, the default is w3 recommended
"""
red, green, blue = self.rgb
return light if lightness > threshold else dark
# Calculate contrast
colors = [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 colors
]
lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
return True if lightness > threshold else False
def rgb_to_hsv(r, g, b):
r = float(r)
g = float(g)
b = float(b)
high = max(r, g, b)
low = min(r, g, b)
h, s, v = high, high, high
def to_hsv(self):
r, g, b = self.rgb
high = max(r, g, b)
low = min(r, g, b)
h, s, v = high, high, high
d = high - low
s = 0 if high == 0 else d/high
if high == low:
h = 0.0
else:
h = {
r: (g - b) / d + (6 if g < b else 0),
g: (b - r) / d + 2,
b: (r - g) / d + 4,
}[high]
h /= 6
return h, s, v
def hsv_to_rgb(h, s, v):
i = math.floor(h*6)
f = h*6 - i
p = v * (1-s)
q = v * (1-f*s)
t = v * (1-(1-f)*s)
r, g, b = [
(v, t, p),
(q, v, p),
(p, v, t),
(p, q, v),
(t, p, v),
(v, p, q),
][int(i % 6)]
return r, g, b
def rgb_to_hsl(r, g, b):
r = float(r)
g = float(g)
b = float(b)
high = max(r, g, b)
low = min(r, g, b)
h, s, v = ((high + low) / 2,)*3
if high == low:
h = 0.0
s = 0.0
else:
d = high - low
s = d / (2 - high - low) if l > 0.5 else d / (high + low)
h = {
r: (g - b) / d + (6 if g < b else 0),
g: (b - r) / d + 2,
b: (r - g) / d + 4,
}[high]
h /= 6
s = 0 if high == 0 else d / high
return h, s, v
if high == low:
h = 0.0
else:
h = {
r: (g - b) / d + (6 if g < b else 0),
g: (b - r) / d + 2,
b: (r - g) / d + 4,
}[high]
h /= 6
return h, s, v
def hsl_to_rgb(h, s, l):
def hue_to_rgb(p, q, t):
t += 1 if t < 0 else 0
t -= 1 if t > 1 else 0
if t < 1/6:
return p + (q - p) * 6 * t
if t < 1/2:
return q
if t < 2/3:
p + (q - p) * (2/3 - t) * 6
return p
def to_hsl(self):
r, g, b = self.rgb
high = max(r, g, b)
low = min(r, g, b)
h, s, v = ((high + low) / 2,) * 3
if s == 0:
r, g, b = l, l, l
else:
q = l * (1 + s) if l < 0.5 else l + s - l * s
p = 2 * l - q
r = hue_to_rgb(p, q, h + 1/3)
g = hue_to_rgb(p, q, h)
b = hue_to_rgb(p, q, h - 1/3)
if high == low:
h = 0.0
s = 0.0
else:
d = high - low
s = d / (2 - high - low) if l > 0.5 else d / (high + low)
h = {
r: (g - b) / d + (6 if g < b else 0),
g: (b - r) / d + 2,
b: (r - g) / d + 4,
}[high]
h /= 6
return r, g, b
def hsv_to_hsl(h, s, v):
l = 0.5 * v * (2 - s)
s = v * s / (1 - math.fabs(2*l-1))
return h, s, l
def hsl_to_hsv(h, s, l):
v = (2*l + s*(1-math.fabs(2*l-1)))/2
s = 2*(v-l)/v
return h, s, v
return h, s, v

View file

@ -18,10 +18,10 @@ def yoink(file_path):
"""
if not os.path.isfile(file_path):
return None
img_exif = {}
file = Image.open(file_path)
img_exif["FileName"] = os.path.basename(file_path)
img_exif["FileSize"] = os.path.getsize(file_path)
img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
@ -34,9 +34,9 @@ def yoink(file_path):
img_exif[value] = tags[tag]
except TypeError:
pass
file.close()
return _format_data(img_exif)

View file

@ -5,9 +5,9 @@ sounds more limiting that it actually is in this gallery
"""
from flask import Blueprint, render_template, url_for, request
from onlylegs.models import Post, User, GroupJunction, Group
from onlylegs.models import Pictures, Users, AlbumJunction, Albums
from onlylegs.extensions import db
from onlylegs.utils.colour import contrast
from onlylegs.utils import colour
blueprint = Blueprint("group", __name__, url_prefix="/group")
@ -18,21 +18,21 @@ def groups():
"""
Group overview, shows all image groups
"""
groups = Group.query.all()
groups = Albums.query.all()
# For each group, get the 3 most recent images
for group in groups:
group.author_username = (
User.query.with_entities(User.username)
.filter(User.id == group.author_id)
Users.query.with_entities(Users.username)
.filter(Users.id == group.author_id)
.first()[0]
)
# Get the 3 most recent images
images = (
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group.id)
.order_by(GroupJunction.date_added.desc())
AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(AlbumJunction.album_id == group.id)
.order_by(AlbumJunction.date_added.desc())
.limit(3)
)
@ -40,8 +40,10 @@ def groups():
group.images = []
for image in images:
group.images.append(
Post.query.with_entities(Post.filename, Post.alt, Post.colours, Post.id)
.filter(Post.id == image[0])
Pictures.query.with_entities(
Pictures.filename, Pictures.alt, Pictures.colours, Pictures.id
)
.filter(Pictures.id == image[0])
.first()
)
@ -54,26 +56,29 @@ def group(group_id):
Group view, shows all images in a group
"""
# Get the group, if it doesn't exist, 404
group = db.get_or_404(Group, group_id, description="Group not found! D:")
group = db.get_or_404(Albums, group_id, description="Group not found! D:")
# Get all images in the group from the junction table
junction = (
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group_id)
.order_by(GroupJunction.date_added.desc())
AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(AlbumJunction.album_id == group_id)
.order_by(AlbumJunction.date_added.desc())
.all()
)
# Get the image data for each image in the group
images = []
for image in junction:
images.append(Post.query.filter(Post.id == image[0]).first())
images.append(Pictures.query.filter(Pictures.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(
images[0].colours[0], "rgb(var(--fg-black))", "rgb(var(--fg-white))"
colour_obj = colour.Colour(images[0].colours[0])
text_colour = (
"rgb(var(--fg-black));"
if colour_obj.is_light()
else "rgb(var(--fg-white));"
)
return render_template(
@ -87,12 +92,12 @@ def group_post(group_id, image_id):
Image view, shows the image and its metadata from a specific group
"""
# Get the image, if it doesn't exist, 404
image = db.get_or_404(Post, image_id, description="Image not found :<")
image = db.get_or_404(Pictures, image_id, description="Image not found :<")
# Get all groups the image is in
groups = (
GroupJunction.query.with_entities(GroupJunction.group_id)
.filter(GroupJunction.post_id == image_id)
AlbumJunction.query.with_entities(AlbumJunction.album_id)
.filter(AlbumJunction.picture_id == image_id)
.all()
)
@ -100,24 +105,24 @@ def group_post(group_id, image_id):
image.groups = []
for group in groups:
image.groups.append(
Group.query.with_entities(Group.id, Group.name)
.filter(Group.id == group[0])
Albums.query.with_entities(Albums.id, Albums.name)
.filter(Albums.id == group[0])
.first()
)
# Get the next and previous images in the group
next_url = (
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group_id)
.filter(GroupJunction.post_id > image_id)
.order_by(GroupJunction.date_added.asc())
AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(AlbumJunction.album_id == group_id)
.filter(AlbumJunction.picture_id > image_id)
.order_by(AlbumJunction.date_added.asc())
.first()
)
prev_url = (
GroupJunction.query.with_entities(GroupJunction.post_id)
.filter(GroupJunction.group_id == group_id)
.filter(GroupJunction.post_id < image_id)
.order_by(GroupJunction.date_added.desc())
AlbumJunction.query.with_entities(AlbumJunction.picture_id)
.filter(AlbumJunction.album_id == group_id)
.filter(AlbumJunction.picture_id < image_id)
.order_by(AlbumJunction.date_added.desc())
.first()
)

View file

@ -3,7 +3,7 @@ Onlylegs - Image View
"""
from math import ceil
from flask import Blueprint, render_template, url_for, current_app, request
from onlylegs.models import Post, GroupJunction, Group
from onlylegs.models import Pictures, AlbumJunction, Albums
from onlylegs.extensions import db
@ -16,12 +16,12 @@ def image(image_id):
Image view, shows the image and its metadata
"""
# Get the image, if it doesn't exist, 404
image = db.get_or_404(Post, image_id, description="Image not found :<")
image = db.get_or_404(Pictures, image_id, description="Image not found :<")
# Get all groups the image is in
groups = (
GroupJunction.query.with_entities(GroupJunction.group_id)
.filter(GroupJunction.post_id == image_id)
AlbumJunction.query.with_entities(AlbumJunction.album_id)
.filter(AlbumJunction.picture_id == image_id)
.all()
)
@ -29,23 +29,23 @@ def image(image_id):
image.groups = []
for group in groups:
image.groups.append(
Group.query.with_entities(Group.id, Group.name)
.filter(Group.id == group[0])
Albums.query.with_entities(Albums.id, Albums.name)
.filter(Albums.id == group[0])
.first()
)
# Get the next and previous images
# Check if there is a group ID set
next_url = (
Post.query.with_entities(Post.id)
.filter(Post.id > image_id)
.order_by(Post.id.asc())
Pictures.query.with_entities(Pictures.id)
.filter(Pictures.id > image_id)
.order_by(Pictures.id.asc())
.first()
)
prev_url = (
Post.query.with_entities(Post.id)
.filter(Post.id < image_id)
.order_by(Post.id.desc())
Pictures.query.with_entities(Pictures.id)
.filter(Pictures.id < image_id)
.order_by(Pictures.id.desc())
.first()
)
@ -56,7 +56,9 @@ def image(image_id):
prev_url = url_for("image.image", image_id=prev_url[0])
# Yoink all the images in the database
total_images = Post.query.with_entities(Post.id).order_by(Post.id.desc()).all()
total_images = (
Pictures.query.with_entities(Pictures.id).order_by(Pictures.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

View file

@ -2,11 +2,9 @@
Onlylegs Gallery - Index view
"""
from math import ceil
from flask import Blueprint, render_template, request, current_app
from werkzeug.exceptions import abort
from onlylegs.models import Post
from onlylegs.models import Pictures, Users
blueprint = Blueprint("gallery", __name__)
@ -17,31 +15,32 @@ 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 = Post.query.with_entities(Post.id).count()
total_images = Pictures.query.with_entities(Pictures.id).count()
pages = ceil(max(total_images, limit) / limit)
if page > pages:
abort(
return abort(
404,
"You have reached the far and beyond, "
+ "but you will not find your answers here.",
"You have reached the far and beyond, but you will not find your answers here.",
)
# get the images for the current page
images = (
Post.query.with_entities(
Post.filename, Post.alt, Post.colours, Post.created_at, Post.id
Pictures.query.with_entities(
Pictures.filename,
Pictures.alt,
Pictures.colours,
Pictures.created_at,
Pictures.id,
Users.username,
)
.order_by(Post.id.desc())
.join(Users)
.order_by(Pictures.id.desc())
.offset((page - 1) * limit)
.limit(limit)
.all()

View file

@ -5,7 +5,7 @@ from flask import Blueprint, render_template, request
from werkzeug.exceptions import abort
from flask_login import current_user
from onlylegs.models import Post, User, Group
from onlylegs.models import Pictures, Users, Albums
from onlylegs.extensions import db
@ -27,9 +27,9 @@ def profile():
abort(404, "You must be logged in to view your own profile!")
# Get the user's data
user = db.get_or_404(User, user_id, description="User not found :<")
user = db.get_or_404(Users, user_id, description="User not found :<")
images = Post.query.filter(Post.author_id == user_id).all()
groups = Group.query.filter(Group.author_id == user_id).all()
images = Pictures.query.filter(Pictures.author_id == user_id).all()
groups = Albums.query.filter(Albums.author_id == user_id).all()
return render_template("profile.html", user=user, images=images, groups=groups)