Merge pull request #24 from Fluffy-Bean/unstable

Unstable
This commit is contained in:
Michał 2023-04-21 18:51:09 +01:00 committed by GitHub
commit 63b725a5b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 740 additions and 653 deletions

View file

@ -4,42 +4,42 @@ This is the main app file, it loads all the other files and sets up the app
""" """
import os import os
import logging import logging
import platformdirs
from flask_assets import Bundle from flask_assets import Bundle
from flask_migrate import init as migrate_init from flask_migrate import init as migrate_init
from flask_migrate import upgrade as migrate_upgrade
from flask_migrate import migrate as migrate_migrate
from flask import Flask, render_template, abort from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
from onlylegs.views import index, image, group, settings, profile from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
from onlylegs import api
from onlylegs import auth
from onlylegs.models import User from onlylegs.models import User
from onlylegs.views import (
index as view_index,
INSTACE_DIR = os.path.join(platformdirs.user_config_dir("onlylegs"), "instance") image as view_image,
MIGRATIONS_DIR = os.path.join(INSTACE_DIR, "migrations") 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 gwagwa
def create_app(): # pylint: disable=R0914 def create_app(): # pylint: disable=R0914
""" """
Create and configure the main app Create and configure the main app
""" """
app = Flask(__name__, instance_path=INSTACE_DIR) app = Flask(__name__, instance_path=INSTANCE_DIR)
app.config.from_pyfile("config.py") app.config.from_pyfile("config.py")
# DATABASE # DATABASE
db.init_app(app) db.init_app(app)
migrate.init_app(app, db) migrate.init_app(app, db, directory=MIGRATIONS_DIR)
# If database file doesn't exist, create it # If database file doesn't exist, create it
if not os.path.exists(os.path.join(INSTACE_DIR, "gallery.sqlite3")): if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
print("Creating database") print("Creating database")
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
@ -68,12 +68,6 @@ def create_app(): # pylint: disable=R0914
print("Creating migrations directory") print("Creating migrations directory")
migrate_init(directory=MIGRATIONS_DIR) migrate_init(directory=MIGRATIONS_DIR)
# Check if migrations are up to date
with app.app_context():
print("Checking for schema changes...")
migrate_migrate(directory=MIGRATIONS_DIR)
migrate_upgrade(directory=MIGRATIONS_DIR)
# LOGIN MANAGER # LOGIN MANAGER
# can also set session_protection to "strong" # can also set session_protection to "strong"
# this would protect against session hijacking # this would protect against session hijacking
@ -106,9 +100,11 @@ def create_app(): # pylint: disable=R0914
# ASSETS # ASSETS
assets.init_app(app) assets.init_app(app)
scripts = Bundle("js/*.js", output="gen/js.js", depends="js/*.js") # filter jsmin is broken :c scripts = Bundle(
"js/*.js", output="gen/js.js", depends="js/*.js"
) # filter jsmin is broken :c
styles = Bundle( styles = Bundle(
"sass/*.sass", "sass/style.sass",
filters="libsass, cssmin", filters="libsass, cssmin",
output="gen/styles.css", output="gen/styles.css",
depends="sass/**/*.sass", depends="sass/**/*.sass",
@ -118,13 +114,17 @@ def create_app(): # pylint: disable=R0914
assets.register("styles", styles) assets.register("styles", styles)
# BLUEPRINTS # BLUEPRINTS
app.register_blueprint(auth.blueprint) app.register_blueprint(view_auth.blueprint)
app.register_blueprint(api.blueprint) app.register_blueprint(view_index.blueprint)
app.register_blueprint(index.blueprint) app.register_blueprint(view_image.blueprint)
app.register_blueprint(image.blueprint) app.register_blueprint(view_group.blueprint)
app.register_blueprint(group.blueprint) app.register_blueprint(view_profile.blueprint)
app.register_blueprint(profile.blueprint) app.register_blueprint(view_settings.blueprint)
app.register_blueprint(settings.blueprint)
# APIS
app.register_blueprint(api_media.blueprint)
app.register_blueprint(api_group.blueprint)
app.register_blueprint(api_account.blueprint)
# CACHE AND COMPRESS # CACHE AND COMPRESS
cache.init_app(app) cache.init_app(app)

View file

@ -1,205 +0,0 @@
"""
Onlylegs - API endpoints
"""
from uuid import uuid4
import os
import pathlib
import logging
import platformdirs
from flask import Blueprint, send_from_directory, abort, flash, request, current_app
from werkzeug.utils import secure_filename
from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import Post, Group, GroupJunction
from onlylegs.utils import metadata as mt
from onlylegs.utils.generate_image import generate_thumbnail
blueprint = Blueprint("api", __name__, url_prefix="/api")
@blueprint.route("/file/<file_name>", methods=["GET"])
def file(file_name):
"""
Returns a file from the uploads folder
r for resolution, 400x400 or thumb for thumbnail
"""
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
file_name = secure_filename(file_name) # Sanitize file name
# 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["UPLOAD_FOLDER"], file_name)
):
abort(404)
return send_from_directory(current_app.config["UPLOAD_FOLDER"], file_name)
thumb = generate_thumbnail(file_name, res, ext)
if not thumb:
abort(404)
return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
@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 no image is uploaded, return 404 error
if not form_file:
return abort(404)
# 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)
abort(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)
abort(500)
img_exif = mt.Metadata(img_path).yoink() # 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 "Gwa Gwa" # Return something so the browser doesn't show an error
@blueprint.route("/delete/<int:image_id>", methods=["POST"])
@login_required
def delete_image(image_id):
"""
Deletes an image from the server and database
"""
post = Post.query.filter_by(id=image_id).first()
# Check if image exists and if user is allowed to delete it (author)
if post is None:
abort(404)
if post.author_id != current_user.id:
abort(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_path = os.path.join(platformdirs.user_config_dir("onlylegs"), "cache")
cache_name = post.filename.rsplit(".")[0]
for cache_file in pathlib.Path(cache_path).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 "Gwa Gwa"
@blueprint.route("/group/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 ":3"
@blueprint.route("/group/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:
abort(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 ":3"
@blueprint.route("/group/delete", methods=["POST"])
def delete_group():
"""
Deletes a group
"""
group_id = request.form["group"]
group = Group.query.filter_by(id=group_id).first()
if group is None:
abort(404)
elif group.author_id != current_user.id:
abort(403)
GroupJunction.query.filter_by(group_id=group_id).delete()
db.session.delete(group)
db.session.commit()
flash(["Group yeeted!", "1"])
return ":3"

0
onlylegs/api/__init__.py Normal file
View file

93
onlylegs/api/account.py Normal file
View file

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

78
onlylegs/api/group.py Normal file
View file

@ -0,0 +1,78 @@
"""
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"})

144
onlylegs/api/media.py Normal file
View file

@ -0,0 +1,144 @@
"""
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 import metadata as mt
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 a file from the uploads 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)
# path = secure_filename(path)
# 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)
thumb = generate_thumbnail(path, res, ext)
if not thumb:
abort(500)
return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
@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 = mt.Metadata(img_path).yoink() # 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

View file

@ -12,11 +12,11 @@ user_dir = platformdirs.user_config_dir("onlylegs")
instance_dir = os.path.join(user_dir, "instance") instance_dir = os.path.join(user_dir, "instance")
# Load environment variables # Load environment variables
print("Loading environment variables...") # print("Loading environment variables...")
load_dotenv(os.path.join(user_dir, ".env")) load_dotenv(os.path.join(user_dir, ".env"))
# Load config from user dir # Load config from user dir
print("Loading config...") # print("Loading config...")
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)
@ -24,13 +24,20 @@ with open(os.path.join(user_dir, "conf.yml"), encoding="utf-8", mode="r") as fil
# Flask config # Flask config
SECRET_KEY = os.environ.get("FLASK_SECRET") SECRET_KEY = os.environ.get("FLASK_SECRET")
SQLALCHEMY_DATABASE_URI = "sqlite:///gallery.sqlite3" SQLALCHEMY_DATABASE_URI = "sqlite:///gallery.sqlite3"
# Upload config
MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"] MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"]
UPLOAD_FOLDER = os.path.join(user_dir, "uploads")
ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"] ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"]
# Pass YAML config to app # Pass YAML config to app
ADMIN_CONF = conf["admin"] ADMIN_CONF = conf["admin"]
UPLOAD_CONF = conf["upload"] UPLOAD_CONF = conf["upload"]
WEBSITE_CONF = conf["website"] WEBSITE_CONF = conf["website"]
# Directories
UPLOAD_FOLDER = os.path.join(user_dir, "media", "uploads")
CACHE_FOLDER = os.path.join(user_dir, "media", "cache")
PFP_FOLDER = os.path.join(user_dir, "media", "pfp")
MEDIA_FOLDER = os.path.join(user_dir, "media")
# Database
INSTANCE_DIR = instance_dir
MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations")

4
onlylegs/gwagwa.py Normal file
View file

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

View file

@ -87,9 +87,11 @@ class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C010
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
alt_id = db.Column(db.String, unique=True, nullable=False, default=str(uuid4())) alt_id = db.Column(db.String, unique=True, nullable=False, default=str(uuid4()))
profile_picture = db.Column(db.String, nullable=True, default=None) picture = db.Column(db.String, default=None)
username = db.Column(db.String, unique=True, nullable=False) colour = db.Column(db.PickleType, default=None)
banner = db.Column(db.String, default=None)
username = db.Column(db.String, unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False) email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False)
joined_at = db.Column( joined_at = db.Column(

BIN
onlylegs/static/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

View file

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

View file

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

View file

@ -276,12 +276,14 @@ document.addEventListener('DOMContentLoaded', () => {
// }); // });
fetch('/api/upload', { fetch('/api/media/upload', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
// .then(response => response.json()) // .then(response => response.json())
.then(data => { addNotification("Image uploaded successfully", 1); }) .then(data => {
addNotification("Image uploaded successfully", 1);
})
.catch(error => { .catch(error => {
switch (response.status) { switch (response.status) {
case 500: case 500:
@ -303,7 +305,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
clearUpload(); clearUpload();
// Reset drop // Reset drop

View file

@ -36,6 +36,8 @@
.banner .banner
height: 30rem height: 30rem
max-height: 69vh
background-color: RGB($bg-300) background-color: RGB($bg-300)
img img
@ -72,9 +74,8 @@
bottom: 0 bottom: 0
display: grid display: grid
grid-template-columns: 1fr auto grid-template-columns: auto 1fr auto
grid-template-rows: 1fr auto auto grid-template-areas: 'info info info' 'image header header' 'subtitle subtitle options'
grid-template-areas: 'info info' 'header header' 'subtitle options'
z-index: +2 z-index: +2
@ -109,6 +110,19 @@
margin-top: auto margin-top: auto
grid-area: options grid-area: options
.banner-picture
grid-area: image
margin: auto 1rem auto 0
position: relative
width: 6.9rem
height: 6.9rem
background-color: RGB($primary)
border-radius: $rad
.banner-small .banner-small
height: 3.5rem height: 3.5rem
background-color: RGB($bg-100) background-color: RGB($bg-100)
@ -165,8 +179,12 @@
display: none display: none
.banner .banner
min-height: 17rem min-height: 15rem
height: auto height: auto
max-height: 30vh
.banner-filter
background: linear-gradient(to bottom, RGB($bg-100), transparent)
.banner-content .banner-content
padding: 0.5rem padding: 0.5rem
@ -178,7 +196,7 @@
align-items: center align-items: center
.banner-header .banner-header
margin: 1rem 0 margin: 0.7rem 0
text-align: center text-align: center
font-size: 2.5rem font-size: 2.5rem
@ -192,6 +210,12 @@
.pill-row .pill-row
margin-top: 0rem margin-top: 0rem
.banner-picture
margin: 0 auto
width: 4rem
height: 4rem
display: flex
.banner-small .banner-small
.banner-content .banner-content
.banner-info .banner-info

View file

@ -149,6 +149,9 @@
opacity: 0 opacity: 0
cursor: pointer cursor: pointer
i
font-size: 1.2rem
.status .status
width: 100% width: 100%
white-space: nowrap white-space: nowrap

View file

@ -23,15 +23,13 @@
cursor: pointer cursor: pointer
transition: all 0.2s cubic-bezier(.86, 0, .07, 1) transition: all 0.2s cubic-bezier(.86, 0, .07, 1)
i
margin: 0.5rem
font-size: 1.25rem
&:hover &:hover
color: RGB($info) color: RGB($info)
svg
margin: 0.5rem
width: 1.25rem
height: 1.25rem
&.show &.show
right: 0.75rem right: 0.75rem
opacity: 1 opacity: 1

View file

@ -54,13 +54,14 @@
position: relative position: relative
text-decoration: none
border: none border: none
background-color: transparent background-color: transparent
color: RGB($fg-white) color: RGB($fg-white)
svg i
width: 1.25rem font-size: 1.25rem
height: 1.25rem
&:hover &:hover
cursor: pointer cursor: pointer
@ -74,7 +75,7 @@
background: RGB($critical) background: RGB($critical)
color: RGB($fg-white) color: RGB($fg-white)
svg i
color: RGB($critical) color: RGB($critical)
&:hover &:hover

View file

@ -23,15 +23,13 @@
cursor: pointer cursor: pointer
transition: all 0.2s cubic-bezier(.86, 0, .07, 1) transition: all 0.2s cubic-bezier(.86, 0, .07, 1)
i
margin: 0.5rem
font-size: 1.25rem
&:hover &:hover
color: RGB($primary) color: RGB($primary)
svg
margin: 0.5rem
width: 1.25rem
height: 1.25rem
&.show &.show
right: 0.75rem right: 0.75rem
opacity: 1 opacity: 1

View file

@ -1,3 +1,16 @@
.gallery-header
margin: 0.5rem
padding: 0
width: 100%
display: flex
flex-direction: row
justify-content: flex-start
font-size: 2rem
font-weight: 700
.gallery-grid .gallery-grid
margin: 0 margin: 0
padding: 0.35rem padding: 0.35rem

View file

@ -1,6 +1,5 @@
.image-container .image-container
margin: auto margin: auto
padding: 0.5rem
width: 100% width: 100%
height: 100% height: 100%
@ -18,4 +17,12 @@
max-height: 100% max-height: 100%
object-fit: contain object-fit: contain
object-position: center object-position: center
@media (max-width: 1100px)
.image-container
margin: 0 auto
max-height: 69vh
img
max-height: 69vh

View file

@ -1,4 +1,6 @@
.info-container .info-container
padding: 0.5rem 0 0.5rem 0.5rem
width: 27rem width: 27rem
height: 100vh height: 100vh
@ -8,13 +10,17 @@
display: flex display: flex
flex-direction: column flex-direction: column
gap: 0 gap: 0.5rem
background-color: RGB($bg-200) background-image: linear-gradient(90deg, $bg-transparent, transparent)
overflow-y: auto overflow-y: auto
z-index: +4 z-index: +4
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1) transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
-ms-overflow-style: none
scrollbar-width: none
&::-webkit-scrollbar
display: none
&.collapsed &.collapsed
left: -27rem left: -27rem
@ -27,7 +33,7 @@
position: relative position: relative
background-color: RGB($bg-200) background-color: RGB($bg-300)
border-radius: $rad border-radius: $rad
transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86) transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86)
@ -38,17 +44,16 @@
.collapse-indicator .collapse-indicator
transform: rotate(90deg) transform: rotate(90deg)
.info-header
border-radius: $rad
.info-table .info-table
height: 0 display: none
padding: 0
.collapse-indicator .collapse-indicator
margin: 0 margin: 0
padding: 0 padding: 0
width: 1.25rem
height: 1.25rem
position: absolute position: absolute
top: 0.6rem top: 0.6rem
right: 0.6rem right: 0.6rem
@ -62,8 +67,11 @@
transition: transform 0.15s cubic-bezier(.79, .14, .15, .86) transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
cursor: pointer cursor: pointer
> i
font-size: 1.1rem
color: RGB($primary)
.info-header .info-header
margin: 0
padding: 0.5rem padding: 0.5rem
width: 100% width: 100%
@ -74,24 +82,15 @@
align-items: center align-items: center
gap: 0.5rem gap: 0.5rem
position: sticky
top: 0
z-index: +1
background-color: RGB($bg-200) background-color: RGB($bg-200)
border-radius: $rad $rad 0 0
svg > i
margin: 0 font-size: 1.25rem
padding: 0 color: RGB($primary)
width: 1.25rem
height: 1.25rem
fill: RGB($primary)
h2 h2
margin: 0 margin: 0
padding: 0
font-size: 1.1rem font-size: 1.1rem
font-weight: 500 font-weight: 500
@ -202,14 +201,15 @@
@media (max-width: 1100px) @media (max-width: 1100px)
.info-container .info-container
padding: 0.5rem
width: 100% width: 100%
height: 100% height: 100%
position: relative position: relative
display: flex
flex-direction: column
gap: 0.5rem
&.collapsed &.collapsed
left: unset left: unset
.info-container
background: transparent

View file

@ -13,12 +13,11 @@
display: flex display: flex
flex-direction: column flex-direction: column
gap: 0.5rem
z-index: 3 z-index: 3
.image-block .image-block
margin: 0 0 0 27rem margin: 0 0 0 27rem
padding: 0 padding: 0.5rem
width: calc(100% - 27rem) width: calc(100% - 27rem)
height: 100vh height: 100vh
@ -33,49 +32,28 @@
transition: margin 0.3s cubic-bezier(0.76, 0, 0.17, 1), width 0.3s cubic-bezier(0.76, 0, 0.17, 1) 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 .pill-row
margin-bottom: 0.5rem margin-top: 0.5rem
&.collapsed &.collapsed
.image-block .image-block
margin: 0 margin: 0
width: 100% width: 100%
@media (max-width: 1100px) @media (max-width: 1100px)
.image-grid .image-grid
padding: 0.5rem
height: auto height: auto
.image-block .image-block
margin: 0 margin: 0
padding: 0.5rem 0.5rem 0 0.5rem
width: 100% width: 100%
height: auto height: auto
gap: 0.5rem
transition: margin 0s, width 0s transition: margin 0s, width 0s
.image-container
margin: 0 auto
padding: 0
max-height: 69vh
img
max-height: 69vh
.pill-row .pill-row
margin-bottom: 0
#fullscreenImage #fullscreenImage
display: none display: none
.info-container
background: transparent
.info-header
border-radius: $rad $rad 0 0
.info-tab.collapsed .info-header
border-radius: $rad

View file

@ -54,20 +54,25 @@
text-decoration: none text-decoration: none
> svg > i
margin: 0
padding: 0.5rem padding: 0.5rem
font-size: 1.3rem
width: 2.5rem
height: 2.5rem
border-radius: $rad-inner border-radius: $rad-inner
color: RGB($fg-white) color: RGB($fg-white)
transition: color 0.2s ease-out, transform 0.2s ease-out > .nav-pfp
padding: 0.4rem
width: 2.3rem
height: 2.3rem
border-radius: $rad-inner
img
width: 100%
height: 100%
object-fit: cover
border-radius: $rad-inner
.tool-tip .tool-tip
margin: 0
padding: 0.4rem 0.7rem padding: 0.4rem 0.7rem
display: block display: block
@ -89,24 +94,20 @@
pointer-events: none pointer-events: none
> svg > i
margin: 0
font-size: 1rem
width: 0.75rem
height: 0.75rem
display: block display: block
position: absolute position: absolute
top: 50% top: 50%
left: -0.45rem left: -0.45rem
transform: translateY(-50%) transform: translateY(-50%)
font-size: 0.75rem
color: RGB($bg-100) color: RGB($bg-100)
&:hover &:hover
> svg > i, .nav-pfp
background: RGBA($fg-white, 0.1) background: RGBA($fg-white, 0.1)
span span
@ -114,7 +115,7 @@
left: 3.9rem left: 3.9rem
&.selected &.selected
> svg > i
color: RGB($primary) color: RGB($primary)
&::before &::before

View file

@ -108,9 +108,8 @@
background-color: RGB($bg-200) background-color: RGB($bg-200)
svg i
width: 1.25rem font-size: 1.25rem
height: 1.25rem
.sniffle__notification-text .sniffle__notification-text
margin: 0 margin: 0

View file

@ -138,7 +138,7 @@
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.pop-up .pop-up
.pop-up-wrapper .pop-up-wrapper
width: calc(100% - 0.75rem) max-width: calc(100% - 0.75rem)
max-height: 95vh max-height: 95vh
.pop-up-content .pop-up-content

View file

@ -0,0 +1,29 @@
.settings-content
margin: 0.5rem
padding: 1rem
position: relative
display: flex
flex-direction: column
justify-content: center
gap: 1rem
background-color: RGB($bg-400)
color: RGB($fg-white)
border: 2px solid RGB($bg-200)
border-radius: $rad-inner
h2
margin: 0
padding: 0
font-size: 1.5rem
font-weight: 700
h3
margin: 0
padding: 0
font-size: 1.25rem
font-weight: 700

View file

@ -86,7 +86,7 @@
&::after &::after
content: '' content: ''
width: 8rem width: 6rem
height: 3px height: 3px
position: absolute position: absolute
@ -97,6 +97,12 @@
background-color: RGB($bg-400) background-color: RGB($bg-400)
border-radius: $rad-inner border-radius: $rad-inner
transition: width 0.25s $animation-bounce
&.dragging #dragIndicator::after
width: 9rem
background-color: RGB($primary)
.upload-jobs .upload-jobs
display: flex display: flex
flex-direction: column flex-direction: column

View file

@ -19,6 +19,7 @@
@import "components/buttons/block" @import "components/buttons/block"
@import "components/image-view/view" @import "components/image-view/view"
@import "components/settings"
// Reset // Reset
* *

View file

@ -51,7 +51,7 @@
const formData = new FormData(); const formData = new FormData();
formData.append("group", formID); formData.append("group", formID);
fetch('{{ url_for('api.delete_group') }}', { fetch('{{ url_for('group_api.delete_group') }}', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(response => { }).then(response => {
@ -147,7 +147,7 @@
formData.append("image", formImage); formData.append("image", formImage);
formData.append("action", formAction); formData.append("action", formAction);
fetch('{{ url_for('api.modify_group') }}', { fetch('{{ url_for('group_api.modify_group') }}', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(response => { }).then(response => {
@ -209,8 +209,7 @@
.navigation { .navigation {
background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important; background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
} }
.navigation-item > svg { .navigation-item > i {
fill: {{ text_colour }} !important;
color: {{ text_colour }} !important; color: {{ text_colour }} !important;
} }
.navigation-item.selected::before { .navigation-item.selected::before {
@ -222,7 +221,7 @@
{% 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;" alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"/> <img src="{{ url_for('media_api.media', path='uploads/' + images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;" alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"/>
<span class="banner-filter"></span> <span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
<p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author.id) }}" class="link">By {{ group.author.username }}</a></p> <p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author.id) }}" class="link">By {{ group.author.username }}</a></p>
@ -230,18 +229,12 @@
<p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p> <p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p>
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="groupShare()"> <button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button>
</div> </div>
{% if current_user.id == group.author.id %} {% if current_user.id == group.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="groupDelete()"> <button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button>
<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 class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button>
</button>
<button class="pill-item pill__critical" onclick="groupEdit()">
<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> </div>
{% endif %} {% endif %}
</div> </div>
@ -254,18 +247,12 @@
<p class="banner-info">By {{ group.author.username }}</p> <p class="banner-info">By {{ group.author.username }}</p>
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="groupShare()"> <button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button>
</div> </div>
{% if current_user.id == group.author.id %} {% if current_user.id == group.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="groupDelete()"> <button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button>
<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 class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button>
</button>
<button class="pill-item pill__critical" onclick="groupEdit()">
<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> </div>
{% endif %} {% endif %}
</div> </div>
@ -278,10 +265,9 @@
{% for image in images %} {% for image in images %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('group.group_post', group_id=group.id, 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('group.group_post', group_id=group.id, 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-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="{% 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"/> <img alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -2,7 +2,7 @@
{% block page_index %} {% block page_index %}
{% if return_page %}?page={{ return_page }}{% endif %}{% endblock %} {% 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('media_api.media', path='uploads/' + 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">
@ -50,7 +50,7 @@
function deleteConfirm() { function deleteConfirm() {
popupDissmiss(); popupDissmiss();
fetch('{{ url_for('api.delete_image', image_id=image['id']) }}', { fetch('{{ url_for('media_api.delete_image', image_id=image['id']) }}', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -82,7 +82,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="background"> <div class="background">
<img src="{{ url_for('api.file', file_name=image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/> <img src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span> <span></span>
</div> </div>
@ -90,7 +90,7 @@
<div class="image-block"> <div class="image-block">
<div class="image-container"> <div class="image-container">
<img <img
src="{{ url_for('api.file', file_name=image.filename) }}?r=prev" src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev"
alt="{{ image.alt }}" alt="{{ image.alt }}"
onload="imgFade(this)" onload="imgFade(this)"
style="opacity: 0;" style="opacity: 0;"
@ -103,54 +103,28 @@
</div> </div>
<div class="pill-row"> <div class="pill-row">
{% if next_url %} {% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %}
<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" onclick="fullscreen()" id="fullscreenImage"> <button class="pill-item" onclick="fullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button>
<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> <button class="pill-item" onclick="imageShare()"><i class="ph ph-export"></i></button>
</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>
<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>
</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>
</a>
</div> </div>
{% if current_user.id == image.author.id %} {% if current_user.id == image.author.id %}
<div> <div>
<button class="pill-item pill__critical" onclick="imageDelete()"> <button class="pill-item pill__critical" onclick="imageDelete()"><i class="ph ph-trash"></i></button>
<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 class="pill-item pill__critical" onclick="imageEdit()"><i class="ph ph-pencil-simple"></i></button>
</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> </div>
{% endif %} {% endif %}
{% if prev_url %}<div><a class="pill-item" href="{{ prev_url }}"><i class="ph ph-arrow-right"></i></a></div>{% endif %}
</div> </div>
</div> </div>
<div class="info-container"> <div class="info-container">
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> <i class="ph ph-info"></i>
<h2>Info</h2> <h2>Info</h2>
<button class="collapse-indicator"> <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
</svg>
</button>
</div> </div>
<div class="info-table"> <div class="info-table">
<table> <table>
@ -178,7 +152,7 @@
<div class="img-groups"> <div class="img-groups">
{% for group in image.groups %} {% for group in image.groups %}
<a href="{{ url_for('group.group', group_id=group.id) }}" class="tag-icon"> <a href="{{ url_for('group.group', group_id=group.id) }}" class="tag-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,72H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.62A15.4,15.4,0,0,0,39.38,216H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72ZM40,56H92.69l16,16H40ZM216,200H40V88H216Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M223.68,66.15,135.68,18a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM128,32l80.34,44-29.77,16.3-80.35-44ZM128,120,47.66,76l33.9-18.56,80.34,44ZM40,90l80,43.78v85.79L40,175.82Zm176,85.78h0l-80,43.79V133.82l32-17.51V152a8,8,0,0,0,16,0V107.55L216,90v85.77Z"></path></svg>
{{ group['name'] }} {{ group['name'] }}
</a> </a>
{% endfor %} {% endfor %}
@ -190,26 +164,17 @@
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
{% if tag == 'Photographer' %} {% if tag == 'Photographer' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M160,40a32,32,0,1,0-32,32A32,32,0,0,0,160,40ZM128,56a16,16,0,1,1,16-16A16,16,0,0,1,128,56Zm90.34,78.05L173.17,82.83a32,32,0,0,0-24-10.83H106.83a32,32,0,0,0-24,10.83L37.66,134.05a20,20,0,0,0,28.13,28.43l16.3-13.08L65.55,212.28A20,20,0,0,0,102,228.8l26-44.87,26,44.87a20,20,0,0,0,36.41-16.52L173.91,149.4l16.3,13.08a20,20,0,0,0,28.13-28.43Zm-11.51,16.77a4,4,0,0,1-5.66,0c-.21-.2-.42-.4-.65-.58L165,121.76A8,8,0,0,0,152.26,130L175.14,217a7.72,7.72,0,0,0,.48,1.35,4,4,0,1,1-7.25,3.38,6.25,6.25,0,0,0-.33-.63L134.92,164a8,8,0,0,0-13.84,0L88,221.05a6.25,6.25,0,0,0-.33.63,4,4,0,0,1-2.26,2.07,4,4,0,0,1-5-5.45,7.72,7.72,0,0,0,.48-1.35L103.74,130A8,8,0,0,0,91,121.76L55.48,150.24c-.23.18-.44.38-.65.58a4,4,0,1,1-5.66-5.65c.12-.12.23-.24.34-.37L94.83,93.41a16,16,0,0,1,12-5.41h42.34a16,16,0,0,1,12,5.41l45.32,51.39c.11.13.22.25.34.37A4,4,0,0,1,206.83,150.82Z"></path></svg> <i class="ph ph-person"></i><h2>Photographer</h2>
<h2>Photographer</h2>
{% elif tag == 'Camera' %} {% elif tag == 'Camera' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,56H180.28L166.65,35.56A8,8,0,0,0,160,32H96a8,8,0,0,0-6.65,3.56L75.71,56H48A24,24,0,0,0,24,80V192a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V80A24,24,0,0,0,208,56Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H80a8,8,0,0,0,6.66-3.56L100.28,48h55.43l13.63,20.44A8,8,0,0,0,176,72h32a8,8,0,0,1,8,8ZM128,88a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,88Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,128,160Z"></path></svg> <i class="ph ph-camera"></i><h2>Camera</h2>
<h2>Camera</h2>
{% elif tag == 'Software' %} {% elif tag == 'Software' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M24,96v72a8,8,0,0,0,8,8h80a8,8,0,0,1,0,16H96v16h16a8,8,0,0,1,0,16H64a8,8,0,0,1,0-16H80V192H32A24,24,0,0,1,8,168V96A24,24,0,0,1,32,72h80a8,8,0,0,1,0,16H32A8,8,0,0,0,24,96ZM208,64H176a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16Zm0,32H176a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16Zm40-48V208a16,16,0,0,1-16,16H152a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h80A16,16,0,0,1,248,48ZM232,208V48H152V208h80Zm-40-40a12,12,0,1,0,12,12A12,12,0,0,0,192,168Z"></path></svg> <i class="ph ph-desktop-tower"></i><h2>Software</h2>
<h2>Software</h2>
{% elif tag == 'File' %} {% elif tag == 'File' %}
<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> <i class="ph ph-file-image"></i><h2>File</h2>
<h2>File</h2>
{% else %} {% else %}
<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> <i class="ph ph-file-image"></i><h2>{{ tag }}</h2>
<h2>{{ tag }}</h2>
{% endif %} {% endif %}
<button class="collapse-indicator"> <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
</svg>
</button>
</div> </div>
<div class="info-table"> <div class="info-table">
<table> <table>

View file

@ -15,29 +15,13 @@
{% if pages > 1 %} {% if pages > 1 %}
<div class="pill-row"> <div class="pill-row">
<div> <div>
{% if pages > 4 %} {% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index') }}"><i class="ph ph-arrow-line-left"></i></a>{% endif %}
<a class="pill-item" href="{{ url_for('gallery.index') }}"> <a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page-1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}"><i class="ph ph-arrow-left"></i></a>
<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> </div>
<span class="pill-text">{{ page }} / {{ pages }}</span>
<span class="pill-text">
{{ page }} / {{ pages }}
</span>
<div> <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 %}"> <a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page+1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}"><i class="ph ph-arrow-right"></i></a>
<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> {% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}"><i class="ph ph-arrow-line-right"></i></a>{% endif %}
</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>
</div> </div>
{% endif %} {% endif %}
@ -49,10 +33,9 @@
{% for image in images %} {% for image in images %}
<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 }})"> <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-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="{% 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"/> <img fetchpriority="low" alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -18,6 +18,9 @@
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<link rel="manifest" href="static/manifest.json"/> <link rel="manifest" href="static/manifest.json"/>
<!-- phosphor icons!!! -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<link <link
href="{{url_for('static', filename='logo-black.svg')}}" href="{{url_for('static', filename='logo-black.svg')}}"
@ -48,14 +51,8 @@
<body> <body>
<div class="notifications"></div> <div class="notifications"></div>
<button class="top-of-page" aria-label="Jump to top of page"> <button class="top-of-page" aria-label="Jump to top of page"><i class="ph ph-arrow-up"></i></button>
<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> {% if request.path == "/" %}<button class="info-button" aria-label="Show info on gallery"><i class="ph ph-question"></i></button>{% endif %}
</button>
{% if request.path == "/" %}
<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="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>
{% endif %}
<div class="pop-up"> <div class="pop-up">
<span class="pop-up__click-off" onclick="popupDissmiss()"></span> <span class="pop-up__click-off" onclick="popupDissmiss()"></span>
@ -71,28 +68,19 @@
<!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">--> <!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">-->
<a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}"> <a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96ZM104,88a16,16,0,1,1,16,16A16,16,0,0,1,104,88Z"></path></svg> <i class="ph-fill ph-images-square"></i>
<span class="tool-tip"> <span class="tool-tip">Home<i class="ph-fill ph-caret-left"></i></span>
Home
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a> </a>
<a href="{{ url_for('group.groups') }}" class="navigation-item {% block nav_groups %}{% endblock %}"> <a href="{{ url_for('group.groups') }}" class="navigation-item {% block nav_groups %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64l27.73,20.8a16.12,16.12,0,0,0,9.6,3.2H200v16H146.43a16,16,0,0,0-8.88,2.69l-20,13.31H69.42a15.94,15.94,0,0,0-14.86,10.06L40,166.46V64Z"></path></svg> <i class="ph-fill ph-package"></i>
<span class="tool-tip"> <span class="tool-tip">Groups<i class="ph-fill ph-caret-left"></i></span>
Groups
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a> </a>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()"> <button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,77.66a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,0l48,48a8,8,0,0,1-11.32,11.32L136,43.31V128a8,8,0,0,1-16,0V43.31L85.66,77.66A8,8,0,0,1,74.34,77.66ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16h68a4,4,0,0,1,4,4v3.46c0,13.45,11,24.79,24.46,24.54A24,24,0,0,0,152,128v-4a4,4,0,0,1,4-4h68A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg> <i class="ph-fill ph-upload"></i>
<span class="tool-tip"> <span class="tool-tip">Upload<i class="ph-fill ph-caret-left"></i></span>
Upload
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</button> </button>
{% endif %} {% endif %}
@ -100,27 +88,29 @@
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}"> <a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg> {% if current_user.picture %}
<span class="tool-tip"> <span class="nav-pfp">
Profile <img
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg> src="{{ url_for('media_api.media', path='pfp/' + current_user.picture) }}?r=icon"
</span> alt="Profile picture"
onload="imgFade(this)"
style="opacity:0;"
/>
</span>
{% else %}
<i class="ph-fill ph-folder-simple-user"></i>
{% endif %}
<span class="tool-tip">Profile<i class="ph-fill ph-caret-left"></i></span>
</a> </a>
<a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}"> <a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"></path></svg> <i class="ph-fill ph-gear-fine"></i>
<span class="tool-tip"> <span class="tool-tip">Settings<i class="ph-fill ph-caret-left"></i></span>
Settings
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a> </a>
{% else %} {% else %}
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()"> <button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M141.66,133.66l-40,40A8,8,0,0,1,88,168V136H24a8,8,0,0,1,0-16H88V88a8,8,0,0,1,13.66-5.66l40,40A8,8,0,0,1,141.66,133.66ZM192,32H136a8,8,0,0,0,0,16h56V208H136a8,8,0,0,0,0,16h56a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Z"></path></svg> <i class="ph-fill ph-sign-in"></i>
<span class="tool-tip"> <span class="tool-tip">Login<i class="ph-fill ph-caret-left"></i></span>
Login
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -134,7 +124,7 @@
<p>May the world see your stuff 👀</p> <p>May the world see your stuff 👀</p>
<form id="uploadForm"> <form id="uploadForm">
<button class="fileDrop-block" type="button"> <button class="fileDrop-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H80a8,8,0,0,1,0,16H32v64H224V136H176a8,8,0,0,1,0-16h48A16,16,0,0,1,240,136ZM85.66,77.66,120,43.31V128a8,8,0,0,0,16,0V43.31l34.34,34.35a8,8,0,0,0,11.32-11.32l-48-48a8,8,0,0,0-11.32,0l-48,48A8,8,0,0,0,85.66,77.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg> <i class="ph ph-upload"></i>
<span class="status">Choose or Drop file</span> <span class="status">Choose or Drop file</span>
<input type="file" id="file" tab-index="-1"/> <input type="file" id="file" tab-index="-1"/>
</button> </button>

View file

@ -67,7 +67,7 @@
formData.append("name", formName); formData.append("name", formName);
formData.append("description", formDescription); formData.append("description", formDescription);
fetch('{{ url_for('api.create_group') }}', { fetch('{{ url_for('group_api.create_group') }}', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(response => { }).then(response => {
@ -108,9 +108,7 @@
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<div class="pill-row"> <div class="pill-row">
<div> <div>
<button class="pill-item" onclick="showCreate()"> <button class="pill-item" onclick="showCreate()"><i class="ph ph-plus"></i></button>
<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,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path></svg>
</button>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -128,7 +126,7 @@
<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 }}" {% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}/> <img data-src="{{ url_for('media_api.media', path='uploads/' + 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" alt="Error thumbnail"/> <img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/>

View file

@ -1,29 +1,86 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block head %}
{% if user.picture %}
<meta property="og:image" content="{{ url_for('media_api.media', path='pfp/' + user.picture) }}"/>
{% endif %}
{% if user.colour %}
<meta name="theme-color" content="rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }})"/>
{% endif %}
{% block nav_profile %}selected{% endblock %} <script type="text/javascript">
function moreInfo() {
popUpShow('{{ user.username }}',
'<p>Joined: {{ user.joined_at }}</p><br>' +
'<p>Images: {{ images|length }}</p><br>' +
'<p>Groups: {{ groups|length }}</p>');
}
</script>
<style>
.banner-picture {
background-color: rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }}) !important;
}
</style>
{% endblock %}
{% block nav_profile %}{% if user.id == current_user.id %}selected{% endif %}{% endblock %}
{% block content %} {% block content %}
<div class="banner-small"> <div class="banner">
{% if user.banner %}
<img src="{{ url_for('static', filename='icon.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/>
{% else %}
<img src="{{ url_for('static', filename='banner.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/>
{% endif %}
<span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
{% if user.picture %}
<img
class="banner-picture"
src="{{ url_for('media_api.media', path='pfp/' + user.picture) }}?r=pfp"
alt="Profile picture"
onload="imgFade(this)"
style="opacity:0;"
/>
{% else %}
<img
class="banner-picture"
src="{{ url_for('static', filename='icon.png') }}"
alt="Profile picture"
onload="imgFade(this)"
style="opacity:0;"
/>
{% endif %}
<h1 class="banner-header">{{ user.username }}</h1> <h1 class="banner-header">{{ user.username }}</h1>
<p class="banner-info">Member since <span class="time">{{ user.joined_at }}</span></p> <p class="banner-subtitle">{{ images|length }} Images · {{ groups|length }} Groups</p>
<div class="pill-row">
<div>
<button class="pill-item" onclick="profileShare()"><i class="ph ph-export"></i></button>
<button class="pill-item" onclick="moreInfo()"><i class="ph ph-info"></i></button>
</div>
{% if user.id == current_user.id %}
<div>
<a href="{{ url_for('settings.general') }}#profileSettings" class="pill-item pill__critical"><i class="ph ph-user-circle-gear"></i></a>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
{% if images %} {% if images %}
<h1 class="gallery-header">Images</h1>
<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('image.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-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="{{ image.alt }}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">
<h1>*crickets chirping*</h1> <h1>*crickets chirping*</h1>
<p>There are no images here yet, upload some!</p> <p>There are no images here yet, oopsie!</p>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,40 @@
{% extends 'layout.html' %}
{% block nav_settings %}selected{% endblock %}
{% block content %}
<div class="banner-small">
<div class="banner-content">
<h1 class="banner-header">Settings</h1>
<p class="banner-info">{% block banner_subtitle%}{% endblock %}</p>
<div class="pill-row">
<div>
<a class="pill-item pill__critical" href="{{ url_for( 'auth.logout' ) }}"><i class="ph ph-sign-out"></i></a>
</div>
</div>
</div>
</div>
<div class="settings-content" id="profileSettings">
<h2>Profile Settings</h2>
<form method="POST" action="{{ url_for('account_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">
<h3>Username</h3>
<input type="text" name="name" class="input-block" value="{{ current_user.username }}" />
<input type="submit" value="Upload" class="btn-block"/>
</form>
</div>
<div class="settings-content" id="profileSettings">
<h2>Account Settings</h2>
<form method="POST" action="" enctype="multipart/form-data">
<h3>Email</h3>
<input type="text" name="email" class="input-block" value="{{ current_user.email }}" />
<input type="submit" value="Upload" class="btn-block"/>
</form>
</div>
{% endblock %}

View file

@ -1,12 +0,0 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_account %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>Account</h2>
<p>Is session fresh?</p>
{% if fresh %}
<p>Yes</p>
{% else %}
<p>No</p>
{% endif %}
{% endblock %}

View file

@ -1,6 +0,0 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_general %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>General</h2>
{% endblock %}

View file

@ -1,30 +0,0 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_logs %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>Logs</h2>
<div class="settings-list" id="logs">
<div class="log" style="display:flex;flex-direction:row;gap:0.5rem;"></div>
</div>
{% endblock %}
{% block script %}
<script>
const output = document.getElementById('logs');
setInterval(function() {
$.ajax({
url: '{{ url_for('api.logfile') }}',
type: 'GET',
dataType: "json",
success: function(response) {
// for each item in response, log to console
response.forEach(function(item) {
console.log(item);
});
}
});
}, 1000); // 10 seconds
</script>
{% endblock %}

View file

@ -1,6 +0,0 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_server %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>Server</h2>
{% endblock %}

View file

@ -1,29 +0,0 @@
{% extends 'layout.html' %}
{% block nav_settings %}selected{% endblock %}
{% block content %}
<div class="banner-small">
<div class="banner-content">
<h1 class="banner-header">Settings</h1>
<p class="banner-info">{% block banner_subtitle%}{% endblock %}</p>
<div class="pill-row">
<div>
<a class="pill-item pill__critical" href="{{ url_for( 'auth.logout' ) }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M112,216a8,8,0,0,1-8,8H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32h56a8,8,0,0,1,0,16H48V208h56A8,8,0,0,1,112,216Zm109.66-93.66-40-40a8,8,0,0,0-11.32,11.32L196.69,120H104a8,8,0,0,0,0,16h92.69l-26.35,26.34a8,8,0,0,0,11.32,11.32l40-40A8,8,0,0,0,221.66,122.34Z"></path></svg>
</a>
</div>
</div>
</div>
</div>
<div class="settings-nav">
<a href="{{ url_for('settings.general') }}" class="settings-nav__item {% block settings_general %}{% endblock %}">General</a>
<a href="{{ url_for('settings.server') }}" class="settings-nav__item {% block settings_server %}{% endblock %}">Server</a>
<a href="{{ url_for('settings.account') }}" class="settings-nav__item {% block settings_account %}{% endblock %}">Account</a>
<a href="{{ url_for('settings.logs') }}" class="settings-nav__item {% block settings_logs %}{% endblock %}">Logs</a>
</div>
<div class="settings-content">
{% block settings_content %}{% endblock %}
</div>
{% endblock %}

View file

@ -1,18 +1,13 @@
""" """
Tools for generating images and thumbnails Tools for generating images and thumbnails
""" """
import os import os
import platformdirs
from PIL import Image, ImageOps from PIL import Image, ImageOps
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from onlylegs.config import MEDIA_FOLDER, CACHE_FOLDER
CACHE_PATH = os.path.join(platformdirs.user_config_dir("onlylegs"), "cache") def generate_thumbnail(file_path, resolution, ext=None):
UPLOAD_PATH = os.path.join(platformdirs.user_config_dir("onlylegs"), "uploads")
def generate_thumbnail(file_name, resolution, ext=None):
""" """
Image thumbnail generator Image thumbnail generator
Uses PIL to generate a thumbnail of the image and saves it to the cache directory Uses PIL to generate a thumbnail of the image and saves it to the cache directory
@ -21,10 +16,11 @@ def generate_thumbnail(file_name, resolution, ext=None):
ext is the file extension of the image ext is the file extension of the image
""" """
# Make image cache directory if it doesn't exist # Make image cache directory if it doesn't exist
if not os.path.exists(CACHE_PATH): if not os.path.exists(CACHE_FOLDER):
os.makedirs(CACHE_PATH) os.makedirs(CACHE_FOLDER)
# no sussy business # no sussy business
file_name = os.path.basename(file_path)
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(".")
@ -38,21 +34,23 @@ def generate_thumbnail(file_name, resolution, ext=None):
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 = (400, 400) res_x, res_y = (400, 400)
elif resolution in ["pfp", "profile"]:
res_x, res_y = (200, 200)
elif resolution in ["icon", "favicon"]: elif resolution in ["icon", "favicon"]:
res_x, res_y = (10, 10) res_x, res_y = (25, 25)
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_FOLDER, 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_FOLDER, 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(MEDIA_FOLDER, file_path)):
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(MEDIA_FOLDER, file_path))
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
@ -63,7 +61,7 @@ def generate_thumbnail(file_name, resolution, ext=None):
# Save image to cache directory # Save image to cache directory
try: try:
image.save( image.save(
os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}"), os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}"),
icc_profile=image_icc, icc_profile=image_icc,
) )
except OSError: except OSError:
@ -71,11 +69,11 @@ def generate_thumbnail(file_name, resolution, ext=None):
# 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( image.save(
os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}"), os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}"),
icc_profile=image_icc, 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_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}")

View file

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

View file

@ -4,6 +4,7 @@ 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")
@ -13,31 +14,4 @@ def general():
""" """
General settings page General settings page
""" """
return render_template("settings/general.html") return render_template("settings.html")
@blueprint.route("/server")
@login_required
def server():
"""
Server settings page
"""
return render_template("settings/server.html")
@blueprint.route("/account")
@login_required
def account():
"""
Account settings page
"""
return render_template("settings/account.html")
@blueprint.route("/logs")
@login_required
def logs():
"""
Logs settings page
"""
return render_template("settings/logs.html")

6
poetry.lock generated
View file

@ -959,14 +959,14 @@ files = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "67.6.1" version = "67.7.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, {file = "setuptools-67.7.0-py3-none-any.whl", hash = "sha256:888be97fde8cc3afd60f7784e678fa29ee13c4e5362daa7104a93bba33646c50"},
{file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, {file = "setuptools-67.7.0.tar.gz", hash = "sha256:b7e53a01c6c654d26d2999ee033d8c6125e5fa55f03b7b193f937ae7ac999f22"},
] ]
[package.extras] [package.extras]

View file

@ -1,10 +1,11 @@
[tool.poetry] [tool.poetry]
name = "onlylegs" name = "OnlyLegs"
version = "0.1.0" version = "0.1.2"
description = "Gallery built for fast and simple image management" repository = "https://github.com/Fluffy-Bean/onlylegs"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
description = "Gallery built for fast and simple image management"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"

6
run.py
View file

@ -1,12 +1,13 @@
""" """
Run script for OnlyLegs Run script for OnlyLegs
""" """
import importlib.metadata
from setup.args import PORT, ADDRESS, WORKERS, DEBUG from setup.args import PORT, ADDRESS, WORKERS, DEBUG
from setup.configuration import Configuration from setup.configuration import Configuration
print( print(
""" f"""
:::::::: :::: ::: ::: ::: ::: ::: ::::::::: ::::::::: :::::::: :::::::: :::: ::: ::: ::: ::: ::: ::::::::: ::::::::: ::::::::
:+: :+: :+:+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
+:+ +:+ :+:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ :+:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
@ -15,12 +16,11 @@ print(
#+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+#
######## ### #### ########## ### ########## ######### ######### ######## ######## ### #### ########## ### ########## ######### ######### ########
Created by Fluffy Bean - Version 0.1.0 Created by Fluffy Bean - {importlib.metadata.version("OnlyLegs")}
""" """
) )
# Run pre-startup checks and load configuration
Configuration() Configuration()

View file

@ -45,7 +45,10 @@ class Configuration:
""" """
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, "media"))
os.makedirs(os.path.join(USER_DIR, "media", "uploads"))
os.makedirs(os.path.join(USER_DIR, "media", "cache"))
os.makedirs(os.path.join(USER_DIR, "media", "pfp"))
print("Created user directory at:", USER_DIR) print("Created user directory at:", USER_DIR)