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 logging
import platformdirs
from flask_assets import Bundle
from flask_migrate import init as migrate_init
from flask_migrate import upgrade as migrate_upgrade
from flask_migrate import migrate as migrate_migrate
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.views import index, image, group, settings, profile
from onlylegs import api
from onlylegs import auth
from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
from onlylegs.models import User
INSTACE_DIR = os.path.join(platformdirs.user_config_dir("onlylegs"), "instance")
MIGRATIONS_DIR = os.path.join(INSTACE_DIR, "migrations")
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 gwagwa
def create_app(): # pylint: disable=R0914
"""
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")
# DATABASE
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 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")
with app.app_context():
db.create_all()
@ -68,12 +68,6 @@ def create_app(): # pylint: disable=R0914
print("Creating migrations directory")
migrate_init(directory=MIGRATIONS_DIR)
# Check if migrations are up to date
with app.app_context():
print("Checking for schema changes...")
migrate_migrate(directory=MIGRATIONS_DIR)
migrate_upgrade(directory=MIGRATIONS_DIR)
# LOGIN MANAGER
# can also set session_protection to "strong"
# this would protect against session hijacking
@ -106,9 +100,11 @@ def create_app(): # pylint: disable=R0914
# ASSETS
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(
"sass/*.sass",
"sass/style.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/**/*.sass",
@ -118,13 +114,17 @@ def create_app(): # pylint: disable=R0914
assets.register("styles", styles)
# BLUEPRINTS
app.register_blueprint(auth.blueprint)
app.register_blueprint(api.blueprint)
app.register_blueprint(index.blueprint)
app.register_blueprint(image.blueprint)
app.register_blueprint(group.blueprint)
app.register_blueprint(profile.blueprint)
app.register_blueprint(settings.blueprint)
app.register_blueprint(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)
# CACHE AND COMPRESS
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")
# Load environment variables
print("Loading environment variables...")
# print("Loading environment variables...")
load_dotenv(os.path.join(user_dir, ".env"))
# 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:
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
SECRET_KEY = os.environ.get("FLASK_SECRET")
SQLALCHEMY_DATABASE_URI = "sqlite:///gallery.sqlite3"
# Upload config
MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"]
UPLOAD_FOLDER = os.path.join(user_dir, "uploads")
ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"]
# Pass YAML config to app
ADMIN_CONF = conf["admin"]
UPLOAD_CONF = conf["upload"]
WEBSITE_CONF = conf["website"]
# 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)
alt_id = db.Column(db.String, unique=True, nullable=False, default=str(uuid4()))
profile_picture = db.Column(db.String, nullable=True, default=None)
username = db.Column(db.String, unique=True, nullable=False)
picture = db.Column(db.String, default=None)
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)
password = db.Column(db.String, nullable=False)
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 () {
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.' +
'<br>Made by Fluffy and others with ❤️');
}

View file

@ -1,12 +1,6 @@
function addNotification(notificationText, notificationLevel) {
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
const notification = document.createElement('div');
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
if (notificationLevel === 1) {
notification.classList.add('success');
iconElement.innerHTML = successIcon;
iconElement.innerHTML = '<i class="ph ph-check-circle"></i>';
} else if (notificationLevel === 2) {
notification.classList.add('critical');
iconElement.innerHTML = criticalIcon;
iconElement.innerHTML = '<i class="ph ph-warning"></i>';
} else if (notificationLevel === 3) {
notification.classList.add('warning');
iconElement.innerHTML = warningIcon;
iconElement.innerHTML = '<i class="ph ph-siren"></i>';
} else {
notification.classList.add('info');
iconElement.innerHTML = infoIcon;
iconElement.innerHTML = '<i class="ph ph-info"></i>';
}
// 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',
body: formData
})
// .then(response => response.json())
.then(data => { addNotification("Image uploaded successfully", 1); })
.then(data => {
addNotification("Image uploaded successfully", 1);
})
.catch(error => {
switch (response.status) {
case 500:
@ -303,7 +305,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
clearUpload();
// Reset drop

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,12 +13,11 @@
display: flex
flex-direction: column
gap: 0.5rem
z-index: 3
.image-block
margin: 0 0 0 27rem
padding: 0
padding: 0.5rem
width: calc(100% - 27rem)
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)
.pill-row
margin-bottom: 0.5rem
margin-top: 0.5rem
&.collapsed
.image-block
margin: 0
width: 100%
@media (max-width: 1100px)
.image-grid
padding: 0.5rem
height: auto
.image-block
margin: 0
padding: 0.5rem 0.5rem 0 0.5rem
width: 100%
height: auto
gap: 0.5rem
transition: margin 0s, width 0s
.image-container
margin: 0 auto
padding: 0
max-height: 69vh
img
max-height: 69vh
.pill-row
margin-bottom: 0
#fullscreenImage
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
> svg
margin: 0
> i
padding: 0.5rem
width: 2.5rem
height: 2.5rem
font-size: 1.3rem
border-radius: $rad-inner
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
margin: 0
padding: 0.4rem 0.7rem
display: block
@ -89,13 +94,7 @@
pointer-events: none
> svg
margin: 0
font-size: 1rem
width: 0.75rem
height: 0.75rem
> i
display: block
position: absolute
@ -103,10 +102,12 @@
left: -0.45rem
transform: translateY(-50%)
font-size: 0.75rem
color: RGB($bg-100)
&:hover
> svg
> i, .nav-pfp
background: RGBA($fg-white, 0.1)
span
@ -114,7 +115,7 @@
left: 3.9rem
&.selected
> svg
> i
color: RGB($primary)
&::before

View file

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

View file

@ -138,7 +138,7 @@
@media (max-width: $breakpoint)
.pop-up
.pop-up-wrapper
width: calc(100% - 0.75rem)
max-width: calc(100% - 0.75rem)
max-height: 95vh
.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
content: ''
width: 8rem
width: 6rem
height: 3px
position: absolute
@ -97,6 +97,12 @@
background-color: RGB($bg-400)
border-radius: $rad-inner
transition: width 0.25s $animation-bounce
&.dragging #dragIndicator::after
width: 9rem
background-color: RGB($primary)
.upload-jobs
display: flex
flex-direction: column

View file

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

View file

@ -51,7 +51,7 @@
const formData = new FormData();
formData.append("group", formID);
fetch('{{ url_for('api.delete_group') }}', {
fetch('{{ url_for('group_api.delete_group') }}', {
method: 'POST',
body: formData
}).then(response => {
@ -147,7 +147,7 @@
formData.append("image", formImage);
formData.append("action", formAction);
fetch('{{ url_for('api.modify_group') }}', {
fetch('{{ url_for('group_api.modify_group') }}', {
method: 'POST',
body: formData
}).then(response => {
@ -209,8 +209,7 @@
.navigation {
background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
}
.navigation-item > svg {
fill: {{ text_colour }} !important;
.navigation-item > i {
color: {{ text_colour }} !important;
}
.navigation-item.selected::before {
@ -222,7 +221,7 @@
{% block content %}
{% if images %}
<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>
<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>
@ -230,18 +229,12 @@
<p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p>
<div class="pill-row">
<div>
<button class="pill-item" onclick="groupShare()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button>
<button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button>
</div>
{% if current_user.id == group.author.id %}
<div>
<button class="pill-item pill__critical" onclick="groupDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
</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>
<button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button>
<button class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button>
</div>
{% endif %}
</div>
@ -254,18 +247,12 @@
<p class="banner-info">By {{ group.author.username }}</p>
<div class="pill-row">
<div>
<button class="pill-item" onclick="groupShare()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button>
<button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button>
</div>
{% if current_user.id == group.author.id %}
<div>
<button class="pill-item pill__critical" onclick="groupDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
</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>
<button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button>
<button class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button>
</div>
{% endif %}
</div>
@ -278,10 +265,9 @@
{% 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 }})">
<div class="image-filter">
<p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</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>
{% endfor %}
</div>

View file

@ -2,7 +2,7 @@
{% block page_index %}
{% if return_page %}?page={{ return_page }}{% endif %}{% endblock %}
{% 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 }})"/>
<script type="text/javascript">
@ -50,7 +50,7 @@
function deleteConfirm() {
popupDissmiss();
fetch('{{ url_for('api.delete_image', image_id=image['id']) }}', {
fetch('{{ url_for('media_api.delete_image', image_id=image['id']) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -82,7 +82,7 @@
{% endblock %}
{% block content %}
<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>
</div>
@ -90,7 +90,7 @@
<div class="image-block">
<div class="image-container">
<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 }}"
onload="imgFade(this)"
style="opacity: 0;"
@ -103,54 +103,28 @@
</div>
<div class="pill-row">
{% if next_url %}
<div>
<a class="pill-item" href="{{ next_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
</a>
</div>
{% endif %}
{% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %}
<div>
<button class="pill-item" onclick="fullscreen()" id="fullscreenImage">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="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>
<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>
<button class="pill-item" onclick="fullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button>
<button class="pill-item" onclick="imageShare()"><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>
</div>
{% if current_user.id == image.author.id %}
<div>
<button class="pill-item pill__critical" onclick="imageDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
</button>
<button class="pill-item pill__critical" onclick="imageEdit()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg>
</button>
</div>
{% endif %}
{% if prev_url %}
<div>
<a class="pill-item" href="{{ prev_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>
</a>
<button class="pill-item pill__critical" onclick="imageDelete()"><i class="ph ph-trash"></i></button>
<button class="pill-item pill__critical" onclick="imageEdit()"><i class="ph ph-pencil-simple"></i></button>
</div>
{% 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 class="info-container">
<div class="info-tab">
<div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>
<i class="ph ph-info"></i>
<h2>Info</h2>
<button class="collapse-indicator">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
</svg>
</button>
<button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
</div>
<div class="info-table">
<table>
@ -178,7 +152,7 @@
<div class="img-groups">
{% for group in image.groups %}
<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'] }}
</a>
{% endfor %}
@ -190,26 +164,17 @@
<div class="info-tab">
<div class="info-header">
{% 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>
<h2>Photographer</h2>
<i class="ph ph-person"></i><h2>Photographer</h2>
{% 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>
<h2>Camera</h2>
<i class="ph ph-camera"></i><h2>Camera</h2>
{% 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>
<h2>Software</h2>
<i class="ph ph-desktop-tower"></i><h2>Software</h2>
{% 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>
<h2>File</h2>
<i class="ph ph-file-image"></i><h2>File</h2>
{% 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>
<h2>{{ tag }}</h2>
<i class="ph ph-file-image"></i><h2>{{ tag }}</h2>
{% endif %}
<button class="collapse-indicator">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
<path d="M7.071 5.314l4.95-4.95a1 1 0 1 1 1.414 1.414L7.778 7.435a1 1 0 0 1-1.414 0L.707 1.778A1 1 0 1 1 2.121.364l4.95 4.95z"></path>
</svg>
</button>
<button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
</div>
<div class="info-table">
<table>

View file

@ -15,29 +15,13 @@
{% if pages > 1 %}
<div class="pill-row">
<div>
{% if pages > 4 %}
<a class="pill-item" href="{{ url_for('gallery.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M232,128a8,8,0,0,1-8,8H91.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L91.31,120H224A8,8,0,0,1,232,128ZM40,32a8,8,0,0,0-8,8V216a8,8,0,0,0,16,0V40A8,8,0,0,0,40,32Z"></path></svg>
</a>
{% endif %}
<a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page-1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
</a>
{% 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="{% 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>
</div>
<span class="pill-text">
{{ page }} / {{ pages }}
</span>
<span class="pill-text">{{ page }} / {{ pages }}</span>
<div>
<a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page+1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>
</a>
{% if pages > 4 %}
<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M189.66,122.34a8,8,0,0,1,0,11.32l-72,72a8,8,0,0,1-11.32-11.32L164.69,136H32a8,8,0,0,1,0-16H164.69L106.34,61.66a8,8,0,0,1,11.32-11.32ZM216,32a8,8,0,0,0-8,8V216a8,8,0,0,0,16,0V40A8,8,0,0,0,216,32Z"></path></svg>
</a>
{% endif %}
<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>
{% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}"><i class="ph ph-arrow-line-right"></i></a>{% endif %}
</div>
</div>
{% endif %}
@ -49,10 +33,9 @@
{% 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 }})">
<div class="image-filter">
<p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</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>
{% endfor %}
</div>

View file

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

View file

@ -67,7 +67,7 @@
formData.append("name", formName);
formData.append("description", formDescription);
fetch('{{ url_for('api.create_group') }}', {
fetch('{{ url_for('group_api.create_group') }}', {
method: 'POST',
body: formData
}).then(response => {
@ -108,9 +108,7 @@
{% if current_user.is_authenticated %}
<div class="pill-row">
<div>
<button class="pill-item" onclick="showCreate()">
<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>
<button class="pill-item" onclick="showCreate()"><i class="ph ph-plus"></i></button>
</div>
</div>
{% endif %}
@ -128,7 +126,7 @@
<div class="images size-{{ group.images|length }}">
{% if group.images|length > 0 %}
{% 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 %}
{% else %}
<img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/>

View file

@ -1,29 +1,86 @@
{% 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 %}
<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">
{% 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>
<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>
{% if images %}
<h1 class="gallery-header">Images</h1>
<div class="gallery-grid">
{% 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 }})">
<div class="image-filter">
<p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</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>
{% endfor %}
</div>
{% else %}
<div class="big-text">
<h1>*crickets chirping*</h1>
<p>There are no images here yet, upload some!</p>
<p>There are no images here yet, oopsie!</p>
</div>
{% endif %}
{% 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
"""
import os
import platformdirs
from PIL import Image, ImageOps
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")
UPLOAD_PATH = os.path.join(platformdirs.user_config_dir("onlylegs"), "uploads")
def generate_thumbnail(file_name, resolution, ext=None):
def generate_thumbnail(file_path, resolution, ext=None):
"""
Image thumbnail generator
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
"""
# Make image cache directory if it doesn't exist
if not os.path.exists(CACHE_PATH):
os.makedirs(CACHE_PATH)
if not os.path.exists(CACHE_FOLDER):
os.makedirs(CACHE_FOLDER)
# no sussy business
file_name = os.path.basename(file_path)
file_name, file_ext = secure_filename(file_name).rsplit(".")
if not ext:
ext = file_ext.strip(".")
@ -38,21 +34,23 @@ def generate_thumbnail(file_name, resolution, ext=None):
res_x, res_y = (1920, 1080)
elif resolution in ["thumb", "thumbnail"]:
res_x, res_y = (400, 400)
elif resolution in ["pfp", "profile"]:
res_x, res_y = (200, 200)
elif resolution in ["icon", "favicon"]:
res_x, res_y = (10, 10)
res_x, res_y = (25, 25)
else:
return None
# 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}")):
return 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_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}")
# 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
# 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")
img_x, img_y = image.size
@ -63,7 +61,7 @@ def generate_thumbnail(file_name, resolution, ext=None):
# Save image to cache directory
try:
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,
)
except OSError:
@ -71,11 +69,11 @@ def generate_thumbnail(file_name, resolution, ext=None):
# so we convert to RGB and try again
image = image.convert("RGB")
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,
)
# No need to keep the image in memory, learned the hard way
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 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")
@ -26,11 +27,9 @@ def profile():
abort(404, "You must be logged in to view your own profile!")
# Get the user's data
user = User.query.filter(User.id == user_id).first()
if not user:
abort(404, "User not found :c")
user = db.get_or_404(User, 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()
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_login import login_required
blueprint = Blueprint("settings", __name__, url_prefix="/settings")
@ -13,31 +14,4 @@ def general():
"""
General settings page
"""
return render_template("settings/general.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")
return render_template("settings.html")

6
poetry.lock generated
View file

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

View file

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

6
run.py
View file

@ -1,12 +1,13 @@
"""
Run script for OnlyLegs
"""
import importlib.metadata
from setup.args import PORT, ADDRESS, WORKERS, DEBUG
from setup.configuration import Configuration
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()

View file

@ -45,7 +45,10 @@ class Configuration:
"""
os.makedirs(USER_DIR)
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)