mirror of
https://github.com/Derpy-Leggies/OnlyLegs.git
synced 2025-06-29 03:26:16 +00:00
Rename the gallery to onlylegs in the files
Im sorry
This commit is contained in:
parent
8029fff73e
commit
2174b10879
76 changed files with 66 additions and 20 deletions
136
onlylegs/__init__.py
Normal file
136
onlylegs/__init__.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
"""
|
||||
Onlylegs Gallery
|
||||
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.models import User
|
||||
from onlylegs import api
|
||||
from onlylegs import auth
|
||||
|
||||
|
||||
INSTACE_DIR = os.path.join(platformdirs.user_config_dir("onlylegs"), "instance")
|
||||
MIGRATIONS_DIR = os.path.join(INSTACE_DIR, "migrations")
|
||||
|
||||
|
||||
def create_app(): # pylint: disable=R0914
|
||||
"""
|
||||
Create and configure the main app
|
||||
"""
|
||||
app = Flask(__name__, instance_path=INSTACE_DIR)
|
||||
app.config.from_pyfile("config.py")
|
||||
|
||||
# DATABASE
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
|
||||
# If database file doesn't exist, create it
|
||||
if not os.path.exists(os.path.join(INSTACE_DIR, "gallery.sqlite3")):
|
||||
print("Creating database")
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
register_user = User(
|
||||
username=app.config["ADMIN_CONF"]["username"],
|
||||
email=app.config["ADMIN_CONF"]["email"],
|
||||
password=generate_password_hash("changeme!", method="sha256"),
|
||||
)
|
||||
db.session.add(register_user)
|
||||
db.session.commit()
|
||||
|
||||
print(
|
||||
"""
|
||||
####################################################
|
||||
# DEFAULY ADMIN USER GENERATED WITH GIVEN USERNAME #
|
||||
# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #
|
||||
# PLEASE UPDATE IT IN THE SETTINGS! #
|
||||
####################################################
|
||||
"""
|
||||
)
|
||||
|
||||
# Check if migrations directory exists, if not create it
|
||||
with app.app_context():
|
||||
if not os.path.exists(MIGRATIONS_DIR):
|
||||
print("Creating migrations directory")
|
||||
migrate_init(directory=MIGRATIONS_DIR)
|
||||
|
||||
# Check if migrations are up to date
|
||||
with app.app_context():
|
||||
print("Checking for schema changes...")
|
||||
migrate_migrate(directory=MIGRATIONS_DIR)
|
||||
migrate_upgrade(directory=MIGRATIONS_DIR)
|
||||
|
||||
# LOGIN MANAGER
|
||||
# can also set session_protection to "strong"
|
||||
# this would protect against session hijacking
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "gallery.index"
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.filter_by(alt_id=user_id).first()
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized():
|
||||
error = 401
|
||||
msg = "You are not authorized to view this page!!!!"
|
||||
return render_template("error.html", error=error, msg=msg), error
|
||||
|
||||
# ERROR HANDLERS
|
||||
@app.errorhandler(Exception)
|
||||
def error_page(err): # noqa
|
||||
"""
|
||||
Error handlers, if the error is not a HTTP error, return 500
|
||||
"""
|
||||
if not isinstance(err, HTTPException):
|
||||
abort(500)
|
||||
return (
|
||||
render_template("error.html", error=err.code, msg=err.description),
|
||||
err.code,
|
||||
)
|
||||
|
||||
# ASSETS
|
||||
assets.init_app(app)
|
||||
|
||||
scripts = Bundle("js/*.js", filters="jsmin", output="gen/js.js", depends="js/*.js")
|
||||
styles = Bundle(
|
||||
"sass/*.sass",
|
||||
filters="libsass, cssmin",
|
||||
output="gen/styles.css",
|
||||
depends="sass/**/*.sass",
|
||||
)
|
||||
|
||||
assets.register("scripts", scripts)
|
||||
assets.register("styles", styles)
|
||||
|
||||
# BLUEPRINTS
|
||||
app.register_blueprint(auth.blueprint)
|
||||
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)
|
||||
|
||||
# CACHE AND COMPRESS
|
||||
cache.init_app(app)
|
||||
compress.init_app(app)
|
||||
|
||||
# Yupee! We got there :3
|
||||
print("Done!")
|
||||
logging.info("Gallery started successfully!")
|
||||
return app
|
205
onlylegs/api.py
Normal file
205
onlylegs/api.py
Normal file
|
@ -0,0 +1,205 @@
|
|||
"""
|
||||
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"
|
109
onlylegs/auth.py
Normal file
109
onlylegs/auth.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
"""
|
||||
OnlyLegs - Authentication
|
||||
User registration, login and logout and locking access to pages behind a login
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, flash, redirect, request, url_for, abort, jsonify
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from flask_login import login_user, logout_user, login_required
|
||||
|
||||
from onlylegs.extensions import db
|
||||
from onlylegs.models import User
|
||||
|
||||
|
||||
blueprint = Blueprint("auth", __name__, url_prefix="/auth")
|
||||
|
||||
|
||||
@blueprint.route("/login", methods=["POST"])
|
||||
def login():
|
||||
"""
|
||||
Log in a registered user by adding the user id to the session
|
||||
"""
|
||||
error = []
|
||||
|
||||
username = request.form["username"].strip()
|
||||
password = request.form["password"].strip()
|
||||
remember = bool(request.form["remember-me"])
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if not user or not check_password_hash(user.password, password):
|
||||
logging.error("Login attempt from %s", request.remote_addr)
|
||||
error.append("Username or Password is incorrect!")
|
||||
|
||||
if error:
|
||||
abort(403)
|
||||
|
||||
login_user(user, remember=remember)
|
||||
|
||||
logging.info("User %s logged in from %s", username, request.remote_addr)
|
||||
flash(["Logged in successfully!", "4"])
|
||||
return "ok", 200
|
||||
|
||||
|
||||
@blueprint.route("/register", methods=["POST"])
|
||||
def register():
|
||||
"""
|
||||
Register a new user
|
||||
"""
|
||||
error = []
|
||||
|
||||
# Thanks Fennec for reminding me to strip out the whitespace lol
|
||||
username = request.form["username"].strip()
|
||||
email = request.form["email"].strip()
|
||||
password = request.form["password"].strip()
|
||||
password_repeat = request.form["password-repeat"].strip()
|
||||
|
||||
email_regex = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
|
||||
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
|
||||
|
||||
# Validate the form
|
||||
if not username or not username_regex.match(username):
|
||||
error.append("Username is invalid!")
|
||||
|
||||
if not email or not email_regex.match(email):
|
||||
error.append("Email is invalid!")
|
||||
|
||||
if not password:
|
||||
error.append("Password is empty!")
|
||||
elif len(password) < 8:
|
||||
error.append("Password is too short! Longer than 8 characters pls")
|
||||
|
||||
if not password_repeat:
|
||||
error.append("Enter password again!")
|
||||
elif password_repeat != password:
|
||||
error.append("Passwords do not match!")
|
||||
|
||||
user_exists = User.query.filter_by(username=username).first()
|
||||
if user_exists:
|
||||
error.append("User already exists!")
|
||||
|
||||
# If there are errors, return them
|
||||
if error:
|
||||
print(error)
|
||||
return jsonify(error), 400
|
||||
|
||||
register_user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
password=generate_password_hash(password, method="sha256"),
|
||||
)
|
||||
db.session.add(register_user)
|
||||
db.session.commit()
|
||||
|
||||
logging.info("User %s registered", username)
|
||||
return "ok", 200
|
||||
|
||||
|
||||
@blueprint.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
"""
|
||||
Clear the current session, including the stored user id
|
||||
"""
|
||||
logout_user()
|
||||
flash(["Goodbye!!!", "4"])
|
||||
return redirect(url_for("gallery.index"))
|
36
onlylegs/config.py
Normal file
36
onlylegs/config.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""
|
||||
Gallery configuration file
|
||||
"""
|
||||
import os
|
||||
import platformdirs
|
||||
from dotenv import load_dotenv
|
||||
from yaml import safe_load
|
||||
|
||||
|
||||
# Set dirs
|
||||
user_dir = platformdirs.user_config_dir("onlylegs")
|
||||
instance_dir = os.path.join(user_dir, "instance")
|
||||
|
||||
# Load environment variables
|
||||
print("Loading environment variables...")
|
||||
load_dotenv(os.path.join(user_dir, ".env"))
|
||||
|
||||
# Load config from user dir
|
||||
print("Loading config...")
|
||||
with open(os.path.join(user_dir, "conf.yml"), encoding="utf-8", mode="r") as file:
|
||||
conf = safe_load(file)
|
||||
|
||||
|
||||
# Flask config
|
||||
SECRET_KEY = os.environ.get("FLASK_SECRET")
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///gallery.sqlite3"
|
||||
|
||||
# Upload config
|
||||
MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"]
|
||||
UPLOAD_FOLDER = os.path.join(user_dir, "uploads")
|
||||
ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"]
|
||||
|
||||
# Pass YAML config to app
|
||||
ADMIN_CONF = conf["admin"]
|
||||
UPLOAD_CONF = conf["upload"]
|
||||
WEBSITE_CONF = conf["website"]
|
16
onlylegs/extensions.py
Normal file
16
onlylegs/extensions.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
Extensions used by the application
|
||||
"""
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
from flask_assets import Environment
|
||||
from flask_compress import Compress
|
||||
from flask_caching import Cache
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
login_manager = LoginManager()
|
||||
assets = Environment()
|
||||
compress = Compress()
|
||||
cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300})
|
4
onlylegs/langs/gb.json
Normal file
4
onlylegs/langs/gb.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"IMAGES_UPLOADED": "%s images uploaded!",
|
||||
"DONT USE THIS": "variable:format(data), jinja2 doesnt use the same method as Django does, odd"
|
||||
}
|
105
onlylegs/models.py
Normal file
105
onlylegs/models.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
"""
|
||||
OnlyLegs - Database models and ions for SQLAlchemy
|
||||
"""
|
||||
from uuid import uuid4
|
||||
from flask_login import UserMixin
|
||||
from onlylegs.extensions import db
|
||||
|
||||
|
||||
class GroupJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Junction table for posts and groups
|
||||
Joins with posts and groups
|
||||
"""
|
||||
|
||||
__tablename__ = "group_junction"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
group_id = db.Column(db.Integer, db.ForeignKey("group.id"))
|
||||
post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
|
||||
|
||||
date_added = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
server_default=db.func.now(), # pylint: disable=E1102
|
||||
)
|
||||
|
||||
|
||||
class Post(db.Model): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Post table
|
||||
"""
|
||||
|
||||
__tablename__ = "post"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
|
||||
filename = db.Column(db.String, unique=True, nullable=False)
|
||||
mimetype = db.Column(db.String, nullable=False)
|
||||
exif = db.Column(db.PickleType, nullable=False)
|
||||
colours = db.Column(db.PickleType, nullable=False)
|
||||
|
||||
description = db.Column(db.String, nullable=False)
|
||||
alt = db.Column(db.String, nullable=False)
|
||||
|
||||
created_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
server_default=db.func.now(), # pylint: disable=E1102
|
||||
)
|
||||
|
||||
junction = db.relationship("GroupJunction", backref="posts")
|
||||
|
||||
|
||||
class Group(db.Model): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
Group table
|
||||
"""
|
||||
|
||||
__tablename__ = "group"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
name = db.Column(db.String, nullable=False)
|
||||
description = db.Column(db.String, nullable=False)
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
created_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
server_default=db.func.now(), # pylint: disable=E1102
|
||||
)
|
||||
|
||||
junction = db.relationship("GroupJunction", backref="groups")
|
||||
|
||||
|
||||
class User(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103
|
||||
"""
|
||||
User table
|
||||
"""
|
||||
|
||||
__tablename__ = "user"
|
||||
|
||||
# Gallery used information
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
alt_id = db.Column(db.String, unique=True, nullable=False, default=str(uuid4()))
|
||||
|
||||
profile_picture = db.Column(db.String, nullable=True, default=None)
|
||||
username = db.Column(db.String, unique=True, nullable=False)
|
||||
|
||||
email = db.Column(db.String, unique=True, nullable=False)
|
||||
password = db.Column(db.String, nullable=False)
|
||||
joined_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
server_default=db.func.now(), # pylint: disable=E1102
|
||||
)
|
||||
|
||||
posts = db.relationship("Post", backref="author")
|
||||
groups = db.relationship("Group", backref="author")
|
||||
|
||||
def get_id(self):
|
||||
return str(self.alt_id)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
onlylegs/static/error.png
Normal file
BIN
onlylegs/static/error.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
onlylegs/static/fonts/Rubik.ttf
Normal file
BIN
onlylegs/static/fonts/Rubik.ttf
Normal file
Binary file not shown.
7
onlylegs/static/fonts/font.css
Normal file
7
onlylegs/static/fonts/font.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'Rubik';
|
||||
src: url('./Rubik.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-weight: 300 900;
|
||||
}
|
45
onlylegs/static/gen/js.js
Normal file
45
onlylegs/static/gen/js.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
function imgFade(obj,time=250){obj.style.transition=`opacity ${time}ms`;obj.style.opacity=1;}
|
||||
function loadOnView(){const lazyLoad=document.querySelectorAll('#lazy-load');const webpSupport=checkWebpSupport();for(let i=0;i<lazyLoad.length;i++){let image=lazyLoad[i];if(image.getBoundingClientRect().top<window.innerHeight&&image.getBoundingClientRect().bottom>0){if(!image.src&&webpSupport){image.src=image.getAttribute('data-src')+'&e=webp'}else if(!image.src){image.src=image.getAttribute('data-src')}}}}
|
||||
window.onload=function(){loadOnView();let times=document.querySelectorAll('.time');for(let i=0;i<times.length;i++){const raw=times[i].innerHTML.split('.')[0];const time=raw.split(' ')[1]
|
||||
const date=raw.split(' ')[0].split('-');let formatted=date[0]+'/'+date[1]+'/'+date[2]+' '+time+' UTC';let dateTime=new Date(formatted);times[i].innerHTML=dateTime.toLocaleDateString()+' '+dateTime.toLocaleTimeString();}
|
||||
let topOfPage=document.querySelector('.top-of-page');if(document.body.scrollTop>300||document.documentElement.scrollTop>20){topOfPage.classList.add('show');}else{topOfPage.classList.remove('show');}
|
||||
topOfPage.onclick=function(){document.body.scrollTop=0;document.documentElement.scrollTop=0;}
|
||||
let infoButton=document.querySelector('.info-button');if(infoButton){if(document.body.scrollTop>300||document.documentElement.scrollTop>20){infoButton.classList.remove('show');}else{infoButton.classList.add('show');}
|
||||
infoButton.onclick=function(){popUpShow('OnlyLegs','<a href="https://github.com/Fluffy-Bean/onlylegs">V23.04.10</a> '+
|
||||
'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.'+
|
||||
'<br>Made by Fluffy and others with ❤️');}}};window.onscroll=function(){loadOnView();let topOfPage=document.querySelector('.top-of-page');if(document.body.scrollTop>300||document.documentElement.scrollTop>20){topOfPage.classList.add('show');}else{topOfPage.classList.remove('show');}
|
||||
let infoButton=document.querySelector('.info-button');if(infoButton){if(document.body.scrollTop>300||document.documentElement.scrollTop>20){infoButton.classList.remove('show');}else{infoButton.classList.add('show');}}};window.onresize=function(){loadOnView();};function showLogin(){cancelBtn=document.createElement('button');cancelBtn.classList.add('btn-block');cancelBtn.innerHTML='nuuuuuuuu';cancelBtn.onclick=popupDissmiss;loginBtn=document.createElement('button');loginBtn.classList.add('btn-block');loginBtn.classList.add('primary');loginBtn.innerHTML='Login';loginBtn.type='submit';loginBtn.setAttribute('form','loginForm');loginForm=document.createElement('form');loginForm.id='loginForm';loginForm.setAttribute('onsubmit','return login(event);');usernameInput=document.createElement('input');usernameInput.classList.add('input-block');usernameInput.type='text';usernameInput.placeholder='Namey';usernameInput.id='username';passwordInput=document.createElement('input');passwordInput.classList.add('input-block');passwordInput.type='password';passwordInput.placeholder='Passywassy';passwordInput.id='password';rememberMeSpan=document.createElement('span');rememberMeSpan.classList.add('input-checkbox');rememberMeInput=document.createElement('input');rememberMeInput.type='checkbox';rememberMeInput.id='remember-me';rememberMeLabel=document.createElement('label');rememberMeLabel.innerHTML='No forgetty me pls';rememberMeLabel.setAttribute('for','remember-me');rememberMeSpan.appendChild(rememberMeInput);rememberMeSpan.appendChild(rememberMeLabel);loginForm.appendChild(usernameInput);loginForm.appendChild(passwordInput);loginForm.appendChild(rememberMeSpan);popUpShow('Login!','Need an account? <span class="link" onclick="showRegister()">Register!</span>',loginForm,[cancelBtn,loginBtn]);}
|
||||
function login(event){event.preventDefault();let formUsername=document.querySelector("#username").value;let formPassword=document.querySelector("#password").value;let formRememberMe=document.querySelector("#remember-me").checked;if(formUsername===""||formPassword===""){addNotification("Please fill in all fields!!!!",3);return;}
|
||||
const formData=new FormData();formData.append("username",formUsername);formData.append("password",formPassword);formData.append("remember-me",formRememberMe);fetch('/auth/login',{method:'POST',body:formData}).then(response=>{if(response.ok){location.reload();}else{if(response.status===403){addNotification('None but devils play past here... Wrong information',2);}else if(response.status===500){addNotification('Server exploded, F\'s in chat',2);}else{addNotification('Error logging in, blame someone',2);}}}).catch(error=>{addNotification('Error logging in, blame someone',2);});}
|
||||
function showRegister(){cancelBtn=document.createElement('button');cancelBtn.classList.add('btn-block');cancelBtn.innerHTML='nuuuuuuuu';cancelBtn.onclick=popupDissmiss;registerBtn=document.createElement('button');registerBtn.classList.add('btn-block');registerBtn.classList.add('primary');registerBtn.innerHTML='Register';registerBtn.type='submit';registerBtn.setAttribute('form','registerForm');registerForm=document.createElement('form');registerForm.id='registerForm';registerForm.setAttribute('onsubmit','return register(event);');usernameInput=document.createElement('input');usernameInput.classList.add('input-block');usernameInput.type='text';usernameInput.placeholder='Namey';usernameInput.id='username';emailInput=document.createElement('input');emailInput.classList.add('input-block');emailInput.type='text';emailInput.placeholder='E mail!';emailInput.id='email';passwordInput=document.createElement('input');passwordInput.classList.add('input-block');passwordInput.type='password';passwordInput.placeholder='Passywassy';passwordInput.id='password';passwordInputRepeat=document.createElement('input');passwordInputRepeat.classList.add('input-block');passwordInputRepeat.type='password';passwordInputRepeat.placeholder='Passywassy again!';passwordInputRepeat.id='password-repeat';registerForm.appendChild(usernameInput);registerForm.appendChild(emailInput);registerForm.appendChild(passwordInput);registerForm.appendChild(passwordInputRepeat);popUpShow('Who are you?','Already have an account? <span class="link" onclick="showLogin()">Login!</span>',registerForm,[cancelBtn,registerBtn]);}
|
||||
function register(event){event.preventDefault();let formUsername=document.querySelector("#username").value;let formEmail=document.querySelector("#email").value;let formPassword=document.querySelector("#password").value;let formPasswordRepeat=document.querySelector("#password-repeat").value;if(formUsername===""||formEmail===""||formPassword===""||formPasswordRepeat===""){addNotification("Please fill in all fields!!!!",3);return;}
|
||||
const formData=new FormData();formData.append("username",formUsername);formData.append("email",formEmail);formData.append("password",formPassword);formData.append("password-repeat",formPasswordRepeat);fetch('/auth/register',{method:'POST',body:formData}).then(response=>{if(response.ok){addNotification('Registered successfully! Now please login to continue',1);showLogin();}else{if(response.status===400){response.json().then(data=>{for(let i=0;i<data.length;i++){addNotification(data[i],2);}});}else if(response.status===403){addNotification('None but devils play past here... Wrong information',2);}else if(response.status===500){addNotification('Server exploded, F\'s in chat',2);}else{addNotification('Error logging in, blame someone',2);}}}).catch(error=>{addNotification('Error logging in, blame someone',2);});}
|
||||
function addNotification(notificationText,notificationLevel){const notificationContainer=document.querySelector('.notifications');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>';const notification=document.createElement('div');notification.classList.add('sniffle__notification');notification.onclick=function(){if(notification){notification.classList.add('hide');setTimeout(function(){notificationContainer.removeChild(notification);},500);}};const iconElement=document.createElement('span');iconElement.classList.add('sniffle__notification-icon');notification.appendChild(iconElement);if(notificationLevel===1){notification.classList.add('success');iconElement.innerHTML=successIcon;}else if(notificationLevel===2){notification.classList.add('critical');iconElement.innerHTML=criticalIcon;}else if(notificationLevel===3){notification.classList.add('warning');iconElement.innerHTML=warningIcon;}else{notification.classList.add('info');iconElement.innerHTML=infoIcon;}
|
||||
const description=document.createElement('span');description.classList.add('sniffle__notification-text');description.innerHTML=notificationText;notification.appendChild(description);notificationContainer.appendChild(notification);setTimeout(function(){notification.classList.add('show');},5);setTimeout(function(){if(notification){notification.classList.add('hide');setTimeout(function(){notificationContainer.removeChild(notification);},500);}},5000);}
|
||||
function popUpShow(titleText,subtitleText,bodyContent=null,userActions=null){const popupSelector=document.querySelector('.pop-up');const headerSelector=document.querySelector('.pop-up-header');const actionsSelector=document.querySelector('.pop-up-controlls');headerSelector.innerHTML='';actionsSelector.innerHTML='';const titleElement=document.createElement('h2');titleElement.innerHTML=titleText;headerSelector.appendChild(titleElement);const subtitleElement=document.createElement('p');subtitleElement.innerHTML=subtitleText;headerSelector.appendChild(subtitleElement);if(bodyContent){headerSelector.appendChild(bodyContent);}
|
||||
if(userActions){for(let i=0;i<userActions.length;i++){actionsSelector.appendChild(userActions[i]);}}else{actionsSelector.innerHTML='<button class="btn-block" onclick="popupDissmiss()">Close</button>';}
|
||||
document.querySelector("html").style.overflow="hidden";popupSelector.style.display='block';setTimeout(function(){popupSelector.classList.add('active')},5);}
|
||||
function popupDissmiss(){const popupSelector=document.querySelector('.pop-up');document.querySelector("html").style.overflow="auto";popupSelector.classList.remove('active');setTimeout(function(){popupSelector.style.display='none';},200);}
|
||||
window.addEventListener("dragover",(event)=>{event.preventDefault();},false);window.addEventListener("drop",(event)=>{event.preventDefault();},false);function openUploadTab(){let uploadTab=document.querySelector(".upload-panel");document.querySelector("html").style.overflow="hidden";uploadTab.style.display="block";setTimeout(function(){uploadTab.classList.add("open");},5);}
|
||||
function closeUploadTab(){let uploadTab=document.querySelector(".upload-panel");let uploadTabContainer=document.querySelector(".upload-panel .container");document.querySelector("html").style.overflow="auto";uploadTab.classList.remove("open");setTimeout(function(){uploadTab.style.display="none";uploadTabContainer.style.transform="";uploadTab.dataset.lastY=0;},250);}
|
||||
function toggleUploadTab(){let uploadTab=document.querySelector(".upload-panel");if(uploadTab.classList.contains("open")){closeUploadTab();}else{openUploadTab();}}
|
||||
function tabDragStart(event){event.preventDefault();let uploadTab=document.querySelector(".upload-panel .container");let offset=uploadTab.getBoundingClientRect().y;uploadTab.classList.add("dragging");document.addEventListener('touchmove',event=>{if(uploadTab.classList.contains("dragging")){if(event.touches[0].clientY-offset>=0){uploadTab.dataset.lastY=event.touches[0].clientY;}else{uploadTab.dataset.lastY=offset;}
|
||||
uploadTab.style.transform=`translateY(${uploadTab.dataset.lastY-offset}px)`;}});}
|
||||
function tabDragStopped(event){event.preventDefault();let uploadTab=document.querySelector(".upload-panel .container");uploadTab.classList.remove("dragging");if(uploadTab.dataset.lastY>(screen.height*0.3)){closeUploadTab();}else{uploadTab.style.transition="transform 0.25s cubic-bezier(0.76, 0, 0.17, 1)";uploadTab.style.transform="translateY(0px)";setTimeout(function(){uploadTab.style.transition="";},250);}}
|
||||
function fileActivate(event){event.preventDefault()
|
||||
let fileDrop=document.querySelector('.fileDrop-block');let fileDropTitle=fileDrop.querySelector('.status');fileDrop.classList.remove('error');fileDrop.classList.add('edging');fileDropTitle.innerHTML='Drop to upload!';}
|
||||
function fileDefault(){let fileDrop=document.querySelector('.fileDrop-block');let fileDropTitle=fileDrop.querySelector('.status');fileDrop.classList.remove('error');fileDrop.classList.remove('edging');fileDropTitle.innerHTML='Choose or Drop file';}
|
||||
function fileDropHandle(event){event.preventDefault()
|
||||
let fileDrop=document.querySelector('.fileDrop-block');let fileUpload=fileDrop.querySelector('#file');fileUpload.files=event.dataTransfer.files;fileDefault();fileChanged();}
|
||||
function fileChanged(){let dropBlock=document.querySelector('.fileDrop-block');let dropBlockStatus=dropBlock.querySelector('.status');let dropBlockInput=dropBlock.querySelector('#file');if(dropBlockInput.value!==""){dropBlock.classList.add('active');dropBlockStatus.innerHTML=dropBlockInput.files[0].name;}else{fileDefault();}}
|
||||
function clearUpload(){let fileDrop=document.querySelector('#uploadForm');let fileUpload=fileDrop.querySelector('#file');let fileAlt=fileDrop.querySelector('#alt');let fileDescription=fileDrop.querySelector('#description');let fileTags=fileDrop.querySelector('#tags');fileUpload.value="";fileAlt.value="";fileDescription.value="";fileTags.value="";}
|
||||
document.addEventListener('DOMContentLoaded',()=>{let uploadTab=document.querySelector(".upload-panel");if(!uploadTab){return;}
|
||||
let uploadTabDrag=uploadTab.querySelector("#dragIndicator");let uploadForm=uploadTab.querySelector('#uploadForm');let fileDrop=uploadForm.querySelector('.fileDrop-block');let fileDropTitle=fileDrop.querySelector('.status');let fileUpload=fileDrop.querySelector('#file');let fileAlt=uploadForm.querySelector('#alt');let fileDescription=uploadForm.querySelector('#description');let fileTags=uploadForm.querySelector('#tags');clearUpload();fileDefault();uploadTabDrag.addEventListener('touchstart',tabDragStart,false);uploadTabDrag.addEventListener('touchend',tabDragStopped,false);fileDrop.addEventListener('dragover',fileActivate,false);fileDrop.addEventListener('dragenter',fileActivate,false);fileDrop.addEventListener('dragleave',fileDefault,false);fileDrop.addEventListener('drop',fileDropHandle,false);fileUpload.addEventListener('change',fileChanged,false);fileUpload.addEventListener('click',fileDefault,false);uploadForm.addEventListener('submit',(event)=>{event.preventDefault()
|
||||
if(fileUpload.value===""){fileDrop.classList.add('error');fileDropTitle.innerHTML='No file selected!';return;}
|
||||
let formData=new FormData();formData.append("file",fileUpload.files[0]);formData.append("alt",fileAlt.value);formData.append("description",fileDescription.value);formData.append("tags",fileTags.value);fetch('/api/upload',{method:'POST',body:formData})
|
||||
.then(data=>{addNotification("Image uploaded successfully",1);}).catch(error=>{switch(response.status){case 500:addNotification("Server exploded, F's in chat",2)
|
||||
break;case 400:case 404:addNotification("Error uploading. Blame yourself",2)
|
||||
break;case 403:addNotification("None but devils play past here...",2)
|
||||
break;case 413:addNotification("File too large!!!!!!",2);break;default:addNotification("Error uploading file, blame someone",2)
|
||||
break;}});clearUpload();fileDrop.classList.remove('active');fileDropTitle.innerHTML='Choose or Drop file';});});function checkWebpSupport(){let webpSupport=false;try{webpSupport=document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp')===0;}catch(e){webpSupport=false;}
|
||||
return webpSupport;}
|
1
onlylegs/static/gen/styles.css
Normal file
1
onlylegs/static/gen/styles.css
Normal file
File diff suppressed because one or more lines are too long
BIN
onlylegs/static/icon.png
Normal file
BIN
onlylegs/static/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
98
onlylegs/static/js/index.js
Normal file
98
onlylegs/static/js/index.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
// fade in images
|
||||
function imgFade(obj, time = 250) {
|
||||
obj.style.transition = `opacity ${time}ms`;
|
||||
obj.style.opacity = 1;
|
||||
}
|
||||
// Lazy load images when they are in view
|
||||
function loadOnView() {
|
||||
const lazyLoad = document.querySelectorAll('#lazy-load');
|
||||
const webpSupport = checkWebpSupport();
|
||||
|
||||
for (let i = 0; i < lazyLoad.length; i++) {
|
||||
let image = lazyLoad[i];
|
||||
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
|
||||
if (!image.src && webpSupport) {
|
||||
image.src = image.getAttribute('data-src') + '&e=webp'
|
||||
} else if (!image.src) {
|
||||
image.src = image.getAttribute('data-src')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
loadOnView();
|
||||
|
||||
let times = document.querySelectorAll('.time');
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
// Remove milliseconds
|
||||
const raw = times[i].innerHTML.split('.')[0];
|
||||
|
||||
// Parse YYYY-MM-DD HH:MM:SS to Date object
|
||||
const time = raw.split(' ')[1]
|
||||
const date = raw.split(' ')[0].split('-');
|
||||
|
||||
// Format to YYYY/MM/DD HH:MM:SS
|
||||
let formatted = date[0] + '/' + date[1] + '/' + date[2] + ' ' + time + ' UTC';
|
||||
|
||||
// Convert to UTC Date object
|
||||
let dateTime = new Date(formatted);
|
||||
|
||||
// Convert to local time
|
||||
times[i].innerHTML = dateTime.toLocaleDateString() + ' ' + dateTime.toLocaleTimeString();
|
||||
}
|
||||
|
||||
// Top Of Page button
|
||||
let topOfPage = document.querySelector('.top-of-page');
|
||||
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
|
||||
topOfPage.classList.add('show');
|
||||
} else {
|
||||
topOfPage.classList.remove('show');
|
||||
}
|
||||
topOfPage.onclick = function () {
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
}
|
||||
|
||||
// Info button
|
||||
let infoButton = document.querySelector('.info-button');
|
||||
|
||||
if (infoButton) {
|
||||
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
|
||||
infoButton.classList.remove('show');
|
||||
} else {
|
||||
infoButton.classList.add('show');
|
||||
}
|
||||
infoButton.onclick = function () {
|
||||
popUpShow('OnlyLegs',
|
||||
'<a href="https://github.com/Fluffy-Bean/onlylegs">V23.04.10</a> ' +
|
||||
'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' +
|
||||
'<br>Made by Fluffy and others with ❤️');
|
||||
}
|
||||
}
|
||||
};
|
||||
window.onscroll = function () {
|
||||
loadOnView();
|
||||
|
||||
// Top Of Page button
|
||||
let topOfPage = document.querySelector('.top-of-page');
|
||||
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
|
||||
topOfPage.classList.add('show');
|
||||
} else {
|
||||
topOfPage.classList.remove('show');
|
||||
}
|
||||
|
||||
// Info button
|
||||
let infoButton = document.querySelector('.info-button');
|
||||
|
||||
if (infoButton) {
|
||||
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
|
||||
infoButton.classList.remove('show');
|
||||
} else {
|
||||
infoButton.classList.add('show');
|
||||
}
|
||||
}
|
||||
};
|
||||
window.onresize = function () {
|
||||
loadOnView();
|
||||
};
|
202
onlylegs/static/js/login.js
Normal file
202
onlylegs/static/js/login.js
Normal file
|
@ -0,0 +1,202 @@
|
|||
// Function to show login
|
||||
function showLogin() {
|
||||
// Create elements
|
||||
cancelBtn = document.createElement('button');
|
||||
cancelBtn.classList.add('btn-block');
|
||||
cancelBtn.innerHTML = 'nuuuuuuuu';
|
||||
cancelBtn.onclick = popupDissmiss;
|
||||
|
||||
loginBtn = document.createElement('button');
|
||||
loginBtn.classList.add('btn-block');
|
||||
loginBtn.classList.add('primary');
|
||||
loginBtn.innerHTML = 'Login';
|
||||
loginBtn.type = 'submit';
|
||||
loginBtn.setAttribute('form', 'loginForm');
|
||||
|
||||
// Create form
|
||||
loginForm = document.createElement('form');
|
||||
loginForm.id = 'loginForm';
|
||||
loginForm.setAttribute('onsubmit', 'return login(event);');
|
||||
|
||||
usernameInput = document.createElement('input');
|
||||
usernameInput.classList.add('input-block');
|
||||
usernameInput.type = 'text';
|
||||
usernameInput.placeholder = 'Namey';
|
||||
usernameInput.id = 'username';
|
||||
|
||||
passwordInput = document.createElement('input');
|
||||
passwordInput.classList.add('input-block');
|
||||
passwordInput.type = 'password';
|
||||
passwordInput.placeholder = 'Passywassy';
|
||||
passwordInput.id = 'password';
|
||||
|
||||
// Container for remember me checkbox
|
||||
rememberMeSpan = document.createElement('span');
|
||||
rememberMeSpan.classList.add('input-checkbox');
|
||||
|
||||
rememberMeInput = document.createElement('input');
|
||||
rememberMeInput.type = 'checkbox';
|
||||
rememberMeInput.id = 'remember-me';
|
||||
|
||||
rememberMeLabel = document.createElement('label');
|
||||
rememberMeLabel.innerHTML = 'No forgetty me pls';
|
||||
rememberMeLabel.setAttribute('for', 'remember-me');
|
||||
|
||||
rememberMeSpan.appendChild(rememberMeInput);
|
||||
rememberMeSpan.appendChild(rememberMeLabel);
|
||||
|
||||
loginForm.appendChild(usernameInput);
|
||||
loginForm.appendChild(passwordInput);
|
||||
loginForm.appendChild(rememberMeSpan);
|
||||
|
||||
popUpShow(
|
||||
'Login!',
|
||||
'Need an account? <span class="link" onclick="showRegister()">Register!</span>',
|
||||
loginForm,
|
||||
[cancelBtn, loginBtn]
|
||||
);
|
||||
}
|
||||
// Function to login
|
||||
function login(event) {
|
||||
// AJAX takes control of subby form :3
|
||||
event.preventDefault();
|
||||
|
||||
let formUsername = document.querySelector("#username").value;
|
||||
let formPassword = document.querySelector("#password").value;
|
||||
let formRememberMe = document.querySelector("#remember-me").checked;
|
||||
|
||||
if (formUsername === "" || formPassword === "") {
|
||||
addNotification("Please fill in all fields!!!!", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
const formData = new FormData();
|
||||
formData.append("username", formUsername);
|
||||
formData.append("password", formPassword);
|
||||
formData.append("remember-me", formRememberMe);
|
||||
|
||||
fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
if (response.status === 403) {
|
||||
addNotification('None but devils play past here... Wrong information', 2);
|
||||
} else if (response.status === 500) {
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
} else {
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
});
|
||||
}
|
||||
// Function to show register
|
||||
function showRegister() {
|
||||
// Create buttons
|
||||
cancelBtn = document.createElement('button');
|
||||
cancelBtn.classList.add('btn-block');
|
||||
cancelBtn.innerHTML = 'nuuuuuuuu';
|
||||
cancelBtn.onclick = popupDissmiss;
|
||||
|
||||
registerBtn = document.createElement('button');
|
||||
registerBtn.classList.add('btn-block');
|
||||
registerBtn.classList.add('primary');
|
||||
registerBtn.innerHTML = 'Register';
|
||||
registerBtn.type = 'submit';
|
||||
registerBtn.setAttribute('form', 'registerForm');
|
||||
|
||||
// Create form
|
||||
registerForm = document.createElement('form');
|
||||
registerForm.id = 'registerForm';
|
||||
registerForm.setAttribute('onsubmit', 'return register(event);');
|
||||
|
||||
usernameInput = document.createElement('input');
|
||||
usernameInput.classList.add('input-block');
|
||||
usernameInput.type = 'text';
|
||||
usernameInput.placeholder = 'Namey';
|
||||
usernameInput.id = 'username';
|
||||
|
||||
emailInput = document.createElement('input');
|
||||
emailInput.classList.add('input-block');
|
||||
emailInput.type = 'text';
|
||||
emailInput.placeholder = 'E mail!';
|
||||
emailInput.id = 'email';
|
||||
|
||||
passwordInput = document.createElement('input');
|
||||
passwordInput.classList.add('input-block');
|
||||
passwordInput.type = 'password';
|
||||
passwordInput.placeholder = 'Passywassy';
|
||||
passwordInput.id = 'password';
|
||||
|
||||
passwordInputRepeat = document.createElement('input');
|
||||
passwordInputRepeat.classList.add('input-block');
|
||||
passwordInputRepeat.type = 'password';
|
||||
passwordInputRepeat.placeholder = 'Passywassy again!';
|
||||
passwordInputRepeat.id = 'password-repeat';
|
||||
|
||||
registerForm.appendChild(usernameInput);
|
||||
registerForm.appendChild(emailInput);
|
||||
registerForm.appendChild(passwordInput);
|
||||
registerForm.appendChild(passwordInputRepeat);
|
||||
|
||||
popUpShow(
|
||||
'Who are you?',
|
||||
'Already have an account? <span class="link" onclick="showLogin()">Login!</span>',
|
||||
registerForm,
|
||||
[cancelBtn, registerBtn]
|
||||
);
|
||||
}
|
||||
// Function to register
|
||||
function register(event) {
|
||||
// AJAX takes control of subby form
|
||||
event.preventDefault();
|
||||
|
||||
let formUsername = document.querySelector("#username").value;
|
||||
let formEmail = document.querySelector("#email").value;
|
||||
let formPassword = document.querySelector("#password").value;
|
||||
let formPasswordRepeat = document.querySelector("#password-repeat").value;
|
||||
|
||||
if (formUsername === "" || formEmail === "" || formPassword === "" || formPasswordRepeat === "") {
|
||||
addNotification("Please fill in all fields!!!!", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
const formData = new FormData();
|
||||
formData.append("username", formUsername);
|
||||
formData.append("email", formEmail);
|
||||
formData.append("password", formPassword);
|
||||
formData.append("password-repeat", formPasswordRepeat);
|
||||
|
||||
// Send form to server
|
||||
fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
addNotification('Registered successfully! Now please login to continue', 1);
|
||||
showLogin();
|
||||
} else {
|
||||
if (response.status === 400) {
|
||||
response.json().then(data => {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
addNotification(data[i], 2);
|
||||
}
|
||||
});
|
||||
} else if (response.status === 403) {
|
||||
addNotification('None but devils play past here... Wrong information', 2);
|
||||
} else if (response.status === 500) {
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
} else {
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
});
|
||||
}
|
63
onlylegs/static/js/notifications.js
Normal file
63
onlylegs/static/js/notifications.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
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');
|
||||
notification.onclick = function() {
|
||||
if (notification) {
|
||||
notification.classList.add('hide');
|
||||
|
||||
setTimeout(function() {
|
||||
notificationContainer.removeChild(notification);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Create icon element and append to notification
|
||||
const iconElement = document.createElement('span');
|
||||
iconElement.classList.add('sniffle__notification-icon');
|
||||
notification.appendChild(iconElement);
|
||||
|
||||
// Set the icon based on the notification level, not pretty but it works :3
|
||||
if (notificationLevel === 1) {
|
||||
notification.classList.add('success');
|
||||
iconElement.innerHTML = successIcon;
|
||||
} else if (notificationLevel === 2) {
|
||||
notification.classList.add('critical');
|
||||
iconElement.innerHTML = criticalIcon;
|
||||
} else if (notificationLevel === 3) {
|
||||
notification.classList.add('warning');
|
||||
iconElement.innerHTML = warningIcon;
|
||||
} else {
|
||||
notification.classList.add('info');
|
||||
iconElement.innerHTML = infoIcon;
|
||||
}
|
||||
|
||||
// Create text element and append to notification
|
||||
const description = document.createElement('span');
|
||||
description.classList.add('sniffle__notification-text');
|
||||
description.innerHTML = notificationText;
|
||||
notification.appendChild(description);
|
||||
|
||||
// Append notification to container
|
||||
notificationContainer.appendChild(notification);
|
||||
setTimeout(function() { notification.classList.add('show'); }, 5);
|
||||
|
||||
// Remove notification after 5 seconds
|
||||
setTimeout(function() {
|
||||
if (notification) {
|
||||
notification.classList.add('hide');
|
||||
|
||||
setTimeout(function() {
|
||||
notificationContainer.removeChild(notification);
|
||||
}, 500);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
46
onlylegs/static/js/popup.js
Normal file
46
onlylegs/static/js/popup.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) {
|
||||
// Get popup elements
|
||||
const popupSelector = document.querySelector('.pop-up');
|
||||
const headerSelector = document.querySelector('.pop-up-header');
|
||||
const actionsSelector = document.querySelector('.pop-up-controlls');
|
||||
|
||||
// Clear popup elements
|
||||
headerSelector.innerHTML = '';
|
||||
actionsSelector.innerHTML = '';
|
||||
|
||||
// Set popup header and subtitle
|
||||
const titleElement = document.createElement('h2');
|
||||
titleElement.innerHTML = titleText;
|
||||
headerSelector.appendChild(titleElement);
|
||||
|
||||
const subtitleElement = document.createElement('p');
|
||||
subtitleElement.innerHTML = subtitleText;
|
||||
headerSelector.appendChild(subtitleElement);
|
||||
|
||||
if (bodyContent) {
|
||||
headerSelector.appendChild(bodyContent);
|
||||
}
|
||||
|
||||
// Set buttons that will be displayed
|
||||
if (userActions) {
|
||||
// for each user action, add the element
|
||||
for (let i = 0; i < userActions.length; i++) {
|
||||
actionsSelector.appendChild(userActions[i]);
|
||||
}
|
||||
} else {
|
||||
actionsSelector.innerHTML = '<button class="btn-block" onclick="popupDissmiss()">Close</button>';
|
||||
}
|
||||
|
||||
// Stop scrolling and show popup
|
||||
document.querySelector("html").style.overflow = "hidden";
|
||||
popupSelector.style.display = 'block';
|
||||
setTimeout(function() { popupSelector.classList.add('active') }, 5); // 2ms delay to allow for css transition >:C
|
||||
}
|
||||
|
||||
function popupDissmiss() {
|
||||
const popupSelector = document.querySelector('.pop-up');
|
||||
|
||||
document.querySelector("html").style.overflow = "auto";
|
||||
popupSelector.classList.remove('active');
|
||||
setTimeout(function() { popupSelector.style.display = 'none'; }, 200);
|
||||
}
|
313
onlylegs/static/js/uploadTab.js
Normal file
313
onlylegs/static/js/uploadTab.js
Normal file
|
@ -0,0 +1,313 @@
|
|||
// Remove default events on file drop, otherwise the browser will open the file
|
||||
window.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
window.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
|
||||
|
||||
// open upload tab
|
||||
function openUploadTab() {
|
||||
let uploadTab = document.querySelector(".upload-panel");
|
||||
|
||||
// Stop scrolling and open upload tab
|
||||
document.querySelector("html").style.overflow = "hidden";
|
||||
uploadTab.style.display = "block";
|
||||
setTimeout(function () { uploadTab.classList.add("open"); }, 5);
|
||||
}
|
||||
|
||||
// close upload tab
|
||||
function closeUploadTab() {
|
||||
let uploadTab = document.querySelector(".upload-panel");
|
||||
let uploadTabContainer = document.querySelector(".upload-panel .container");
|
||||
|
||||
// un-Stop scrolling and close upload tab
|
||||
document.querySelector("html").style.overflow = "auto";
|
||||
uploadTab.classList.remove("open");
|
||||
setTimeout(function () {
|
||||
uploadTab.style.display = "none";
|
||||
|
||||
uploadTabContainer.style.transform = "";
|
||||
uploadTab.dataset.lastY = 0;
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// toggle upload tab
|
||||
function toggleUploadTab() {
|
||||
let uploadTab = document.querySelector(".upload-panel");
|
||||
|
||||
if (uploadTab.classList.contains("open")) {
|
||||
closeUploadTab();
|
||||
} else {
|
||||
openUploadTab();
|
||||
}
|
||||
}
|
||||
|
||||
function tabDragStart(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let uploadTab = document.querySelector(".upload-panel .container");
|
||||
let offset = uploadTab.getBoundingClientRect().y;
|
||||
|
||||
uploadTab.classList.add("dragging");
|
||||
|
||||
document.addEventListener('touchmove', event => {
|
||||
if (uploadTab.classList.contains("dragging")) {
|
||||
if (event.touches[0].clientY - offset >= 0) {
|
||||
uploadTab.dataset.lastY = event.touches[0].clientY;
|
||||
} else {
|
||||
uploadTab.dataset.lastY = offset;
|
||||
}
|
||||
|
||||
uploadTab.style.transform = `translateY(${uploadTab.dataset.lastY - offset}px)`;
|
||||
}
|
||||
});
|
||||
}
|
||||
function tabDragStopped(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let uploadTab = document.querySelector(".upload-panel .container");
|
||||
|
||||
uploadTab.classList.remove("dragging");
|
||||
|
||||
if (uploadTab.dataset.lastY > (screen.height * 0.3)) {
|
||||
closeUploadTab();
|
||||
} else {
|
||||
uploadTab.style.transition = "transform 0.25s cubic-bezier(0.76, 0, 0.17, 1)";
|
||||
uploadTab.style.transform = "translateY(0px)";
|
||||
setTimeout(function () { uploadTab.style.transition = ""; }, 250);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Edging the file plunge :3
|
||||
function fileActivate(event) {
|
||||
event.preventDefault()
|
||||
|
||||
let fileDrop = document.querySelector('.fileDrop-block');
|
||||
let fileDropTitle = fileDrop.querySelector('.status');
|
||||
|
||||
fileDrop.classList.remove('error');
|
||||
fileDrop.classList.add('edging');
|
||||
fileDropTitle.innerHTML = 'Drop to upload!';
|
||||
}
|
||||
function fileDefault() {
|
||||
let fileDrop = document.querySelector('.fileDrop-block');
|
||||
let fileDropTitle = fileDrop.querySelector('.status');
|
||||
|
||||
fileDrop.classList.remove('error');
|
||||
fileDrop.classList.remove('edging');
|
||||
fileDropTitle.innerHTML = 'Choose or Drop file';
|
||||
}
|
||||
|
||||
function fileDropHandle(event) {
|
||||
event.preventDefault()
|
||||
|
||||
let fileDrop = document.querySelector('.fileDrop-block');
|
||||
let fileUpload = fileDrop.querySelector('#file');
|
||||
|
||||
fileUpload.files = event.dataTransfer.files;
|
||||
|
||||
fileDefault();
|
||||
fileChanged();
|
||||
}
|
||||
|
||||
function fileChanged() {
|
||||
let dropBlock = document.querySelector('.fileDrop-block');
|
||||
let dropBlockStatus = dropBlock.querySelector('.status');
|
||||
let dropBlockInput = dropBlock.querySelector('#file');
|
||||
|
||||
if (dropBlockInput.value !== "") {
|
||||
dropBlock.classList.add('active');
|
||||
dropBlockStatus.innerHTML = dropBlockInput.files[0].name;
|
||||
} else {
|
||||
fileDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function clearUpload() {
|
||||
let fileDrop = document.querySelector('#uploadForm');
|
||||
|
||||
let fileUpload = fileDrop.querySelector('#file');
|
||||
let fileAlt = fileDrop.querySelector('#alt');
|
||||
let fileDescription = fileDrop.querySelector('#description');
|
||||
let fileTags = fileDrop.querySelector('#tags');
|
||||
|
||||
fileUpload.value = "";
|
||||
fileAlt.value = "";
|
||||
fileDescription.value = "";
|
||||
fileTags.value = "";
|
||||
}
|
||||
|
||||
|
||||
// function createJob(file) {
|
||||
// jobContainer = document.createElement("div");
|
||||
// jobContainer.classList.add("job");
|
||||
|
||||
// jobStatus = document.createElement("span");
|
||||
// jobStatus.classList.add("job__status");
|
||||
// jobStatus.innerHTML = "Uploading...";
|
||||
|
||||
// jobProgress = document.createElement("span");
|
||||
// jobProgress.classList.add("progress");
|
||||
|
||||
// jobImg = document.createElement("img");
|
||||
// jobImg.src = URL.createObjectURL(file);
|
||||
|
||||
// jobImgFilter = document.createElement("span");
|
||||
// jobImgFilter.classList.add("img-filter");
|
||||
|
||||
// jobContainer.appendChild(jobStatus);
|
||||
// jobContainer.appendChild(jobProgress);
|
||||
// jobContainer.appendChild(jobImg);
|
||||
// jobContainer.appendChild(jobImgFilter);
|
||||
|
||||
// return jobContainer;
|
||||
// }
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Function to upload images
|
||||
let uploadTab = document.querySelector(".upload-panel");
|
||||
|
||||
if (!uploadTab) { return; } // If upload tab doesn't exist, don't run this code :3
|
||||
|
||||
let uploadTabDrag = uploadTab.querySelector("#dragIndicator");
|
||||
let uploadForm = uploadTab.querySelector('#uploadForm');
|
||||
// let jobList = document.querySelector(".upload-jobs");
|
||||
|
||||
let fileDrop = uploadForm.querySelector('.fileDrop-block');
|
||||
let fileDropTitle = fileDrop.querySelector('.status');
|
||||
let fileUpload = fileDrop.querySelector('#file');
|
||||
|
||||
let fileAlt = uploadForm.querySelector('#alt');
|
||||
let fileDescription = uploadForm.querySelector('#description');
|
||||
let fileTags = uploadForm.querySelector('#tags');
|
||||
|
||||
|
||||
clearUpload();
|
||||
fileDefault();
|
||||
|
||||
|
||||
// Tab is dragged
|
||||
uploadTabDrag.addEventListener('touchstart', tabDragStart, false);
|
||||
uploadTabDrag.addEventListener('touchend', tabDragStopped, false);
|
||||
|
||||
// Drag over/enter event
|
||||
fileDrop.addEventListener('dragover', fileActivate, false);
|
||||
fileDrop.addEventListener('dragenter', fileActivate, false);
|
||||
// Drag out
|
||||
fileDrop.addEventListener('dragleave', fileDefault, false);
|
||||
// Drop file into box
|
||||
fileDrop.addEventListener('drop', fileDropHandle, false);
|
||||
|
||||
// File upload change
|
||||
fileUpload.addEventListener('change', fileChanged, false);
|
||||
// File upload clicked
|
||||
fileUpload.addEventListener('click', fileDefault, false);
|
||||
|
||||
|
||||
// Submit form
|
||||
uploadForm.addEventListener('submit', (event) => {
|
||||
// AJAX takes control of subby form
|
||||
event.preventDefault()
|
||||
|
||||
if (fileUpload.value === "") {
|
||||
fileDrop.classList.add('error');
|
||||
fileDropTitle.innerHTML = 'No file selected!';
|
||||
// Stop the function
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append("file", fileUpload.files[0]);
|
||||
formData.append("alt", fileAlt.value);
|
||||
formData.append("description", fileDescription.value);
|
||||
formData.append("tags", fileTags.value);
|
||||
|
||||
// jobItem = createJob(fileUpload.files[0]);
|
||||
// jobStatus = jobItem.querySelector(".job__status");
|
||||
|
||||
// Upload the information
|
||||
// $.ajax({
|
||||
// url: '/api/upload',
|
||||
// type: 'post',
|
||||
// data: formData,
|
||||
// contentType: false,
|
||||
// processData: false,
|
||||
// beforeSend: function () {
|
||||
// // Add job to list
|
||||
// jobList.appendChild(jobItem);
|
||||
// },
|
||||
// success: function (response) {
|
||||
// jobItem.classList.add("success");
|
||||
// jobStatus.innerHTML = "Uploaded successfully";
|
||||
// if (!document.querySelector(".upload-panel").classList.contains("open")) {
|
||||
// addNotification("Image uploaded successfully", 1);
|
||||
// }
|
||||
// },
|
||||
// error: function (response) {
|
||||
// jobItem.classList.add("critical");
|
||||
// switch (response.status) {
|
||||
// case 500:
|
||||
// jobStatus.innerHTML = "Server exploded, F's in chat";
|
||||
// break;
|
||||
// case 400:
|
||||
// case 404:
|
||||
// jobStatus.innerHTML = "Error uploading. Blame yourself";
|
||||
// break;
|
||||
// case 403:
|
||||
// jobStatus.innerHTML = "None but devils play past here...";
|
||||
// break;
|
||||
// case 413:
|
||||
// jobStatus.innerHTML = "File too large!!!!!!";
|
||||
// break;
|
||||
// default:
|
||||
// jobStatus.innerHTML = "Error uploading file, blame someone";
|
||||
// break;
|
||||
// }
|
||||
// if (!document.querySelector(".upload-panel").classList.contains("open")) {
|
||||
// addNotification("Error uploading file", 2);
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
|
||||
fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
// .then(response => response.json())
|
||||
.then(data => { addNotification("Image uploaded successfully", 1); })
|
||||
.catch(error => {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification("Server exploded, F's in chat", 2)
|
||||
break;
|
||||
case 400:
|
||||
case 404:
|
||||
addNotification("Error uploading. Blame yourself", 2)
|
||||
break;
|
||||
case 403:
|
||||
addNotification("None but devils play past here...", 2)
|
||||
break;
|
||||
case 413:
|
||||
addNotification("File too large!!!!!!", 2);
|
||||
break;
|
||||
default:
|
||||
addNotification("Error uploading file, blame someone", 2)
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
clearUpload();
|
||||
|
||||
// Reset drop
|
||||
fileDrop.classList.remove('active');
|
||||
fileDropTitle.innerHTML = 'Choose or Drop file';
|
||||
});
|
||||
});
|
10
onlylegs/static/js/webp.js
Normal file
10
onlylegs/static/js/webp.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
function checkWebpSupport() {
|
||||
let webpSupport = false;
|
||||
try {
|
||||
webpSupport = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0;
|
||||
} catch (e) {
|
||||
webpSupport = false;
|
||||
}
|
||||
|
||||
return webpSupport;
|
||||
}
|
60
onlylegs/static/logo-black.svg
Normal file
60
onlylegs/static/logo-black.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.9 KiB |
62
onlylegs/static/logo-white.svg
Normal file
62
onlylegs/static/logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.1 KiB |
18
onlylegs/static/manifest.json
Normal file
18
onlylegs/static/manifest.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "OnlyLegs",
|
||||
"short_name": "OnlyLegs",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#151515",
|
||||
"theme_color": "#151515",
|
||||
"description": "A gallery built for fast and simple image management!",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon.png",
|
||||
"sizes": "621x621",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"splash_pages": null
|
||||
}
|
||||
|
11
onlylegs/static/sass/animations.sass
Normal file
11
onlylegs/static/sass/animations.sass
Normal file
|
@ -0,0 +1,11 @@
|
|||
@keyframes imgLoading
|
||||
0%
|
||||
background-position: -468px 0
|
||||
100%
|
||||
background-position: 468px 0
|
||||
|
||||
@keyframes uploadingLoop
|
||||
0%
|
||||
left: -100%
|
||||
100%
|
||||
left: 100%
|
198
onlylegs/static/sass/components/banner.sass
Normal file
198
onlylegs/static/sass/components/banner.sass
Normal file
|
@ -0,0 +1,198 @@
|
|||
.banner,
|
||||
.banner-small
|
||||
width: 100%
|
||||
position: relative
|
||||
color: RGB($fg-white)
|
||||
|
||||
.link
|
||||
padding: 0.1rem 0.3rem
|
||||
|
||||
text-decoration: none
|
||||
font-weight: 500
|
||||
|
||||
background-color: RGB($fg-white)
|
||||
color: RGB($fg-black)
|
||||
border-radius: $rad-inner
|
||||
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
background-color: RGB($fg-black)
|
||||
color: RGB($fg-white)
|
||||
|
||||
&::after
|
||||
content: ''
|
||||
|
||||
width: $rad
|
||||
height: calc(#{$rad} * 2)
|
||||
|
||||
position: absolute
|
||||
bottom: calc(#{$rad} * -2)
|
||||
left: 0
|
||||
|
||||
background-color: RGB($bg-bright)
|
||||
border-radius: $rad 0 0 0
|
||||
box-shadow: 0 calc(#{$rad} * -1) 0 0 RGB($bg-100)
|
||||
|
||||
.banner
|
||||
height: 30rem
|
||||
background-color: RGB($bg-300)
|
||||
|
||||
img
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-color: inherit
|
||||
|
||||
object-fit: cover
|
||||
object-position: center center
|
||||
|
||||
.banner-filter
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background: linear-gradient(to right, RGB($primary), transparent)
|
||||
|
||||
z-index: +1
|
||||
|
||||
.banner-content
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
position: absolute
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
display: grid
|
||||
grid-template-columns: 1fr auto
|
||||
grid-template-rows: 1fr auto auto
|
||||
grid-template-areas: 'info info' 'header header' 'subtitle options'
|
||||
|
||||
z-index: +2
|
||||
|
||||
.banner-header,
|
||||
.banner-info,
|
||||
.banner-subtitle
|
||||
margin: 0
|
||||
padding: 0
|
||||
width: 100%
|
||||
|
||||
.banner-header
|
||||
grid-area: header
|
||||
|
||||
margin: 0.5rem 0
|
||||
|
||||
text-align: left
|
||||
font-size: 6.9rem
|
||||
font-weight: 700
|
||||
|
||||
color: RGB($primary)
|
||||
|
||||
.banner-info
|
||||
grid-area: info
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
.banner-subtitle
|
||||
grid-area: subtitle
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
|
||||
.pill-row
|
||||
margin-top: auto
|
||||
grid-area: options
|
||||
|
||||
.banner-small
|
||||
height: 3.5rem
|
||||
background-color: RGB($bg-100)
|
||||
|
||||
.banner-content
|
||||
padding: 0 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: flex-start
|
||||
|
||||
z-index: +2
|
||||
|
||||
.banner-header,
|
||||
.banner-info
|
||||
margin: auto 0
|
||||
padding: 0
|
||||
width: auto
|
||||
height: auto
|
||||
justify-self: flex-start
|
||||
|
||||
.banner-header
|
||||
margin-right: 0.6rem
|
||||
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
text-align: left
|
||||
font-weight: 700
|
||||
font-size: 1.5rem
|
||||
|
||||
color: RGB($primary)
|
||||
|
||||
.banner-info
|
||||
margin-right: 0.6rem
|
||||
|
||||
font-size: 0.9rem
|
||||
font-weight: 400
|
||||
|
||||
.pill-row
|
||||
margin-left: auto
|
||||
width: auto
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.banner,
|
||||
.banner-small
|
||||
&::after
|
||||
display: none
|
||||
|
||||
.banner
|
||||
min-height: 17rem
|
||||
height: auto
|
||||
|
||||
.banner-content
|
||||
padding: 0.5rem
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
.banner-header
|
||||
margin: 1rem 0
|
||||
text-align: center
|
||||
font-size: 2.5rem
|
||||
|
||||
.banner-info
|
||||
font-size: 1.1rem
|
||||
text-align: center
|
||||
|
||||
.banner-subtitle
|
||||
display: none
|
||||
|
||||
.pill-row
|
||||
margin-top: 0rem
|
||||
|
||||
.banner-small
|
||||
.banner-content
|
||||
.banner-info
|
||||
display: none
|
178
onlylegs/static/sass/components/buttons/block.sass
Normal file
178
onlylegs/static/sass/components/buttons/block.sass
Normal file
|
@ -0,0 +1,178 @@
|
|||
@mixin btn-block($color)
|
||||
color: RGB($color)
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: RGBA($color, 0.1)
|
||||
color: RGB($color)
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($color, 0.2)
|
||||
|
||||
|
||||
.btn-block
|
||||
padding: 0.4rem 0.7rem
|
||||
|
||||
width: auto
|
||||
min-height: 2.3rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
position: relative
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
background-color: RGBA($white, 0.1)
|
||||
color: RGB($white)
|
||||
border: none
|
||||
border-radius: $rad-inner
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
outline: none
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, box-shadow 0.15s ease-in-out
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: RGBA($white, 0.2)
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
|
||||
|
||||
&.primary
|
||||
@include btn-block($primary)
|
||||
|
||||
&.critical
|
||||
@include btn-block($critical)
|
||||
|
||||
&.warning
|
||||
@include btn-block($warning)
|
||||
|
||||
&.success
|
||||
@include btn-block($success)
|
||||
|
||||
&.info
|
||||
@include btn-block($info)
|
||||
|
||||
&.black
|
||||
@include btn-block($black)
|
||||
|
||||
.input-checkbox
|
||||
padding: 0
|
||||
display: flex
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
position: relative
|
||||
|
||||
label
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
text-align: left
|
||||
|
||||
color: RGB($fg-white)
|
||||
|
||||
.input-block
|
||||
padding: 0.4rem 0.7rem
|
||||
|
||||
width: auto
|
||||
min-height: 2.3rem
|
||||
|
||||
display: flex
|
||||
justify-content: flex-start
|
||||
align-items: center
|
||||
|
||||
position: relative
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
text-align: left
|
||||
|
||||
background-color: RGBA($white, 0.1)
|
||||
color: RGB($white)
|
||||
border: none
|
||||
border-bottom: 3px solid RGBA($white, 0.1)
|
||||
border-radius: $rad-inner
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
outline: none
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
|
||||
|
||||
&:not(:focus):not([value=""]):not(:placeholder-shown)
|
||||
border-color: RGBA($white, 0.3)
|
||||
|
||||
&:hover
|
||||
border-color: RGBA($white, 0.3)
|
||||
|
||||
&:focus
|
||||
border-color: RGB($primary)
|
||||
|
||||
&.black
|
||||
@include btn-block($black)
|
||||
|
||||
.fileDrop-block
|
||||
padding: 1rem 1.25rem
|
||||
|
||||
width: 100%
|
||||
min-height: 2.3rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
position: relative
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
background-color: RGBA($white, 0.1)
|
||||
color: RGB($white)
|
||||
border: none
|
||||
border-radius: $rad-inner
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
outline: none
|
||||
|
||||
cursor: pointer
|
||||
overflow: hidden
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, box-shadow 0.15s ease-in-out
|
||||
|
||||
input
|
||||
position: absolute
|
||||
inset: 0
|
||||
opacity: 0
|
||||
cursor: pointer
|
||||
|
||||
.status
|
||||
width: 100%
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
text-align: center
|
||||
overflow: hidden
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: RGBA($white, 0.2)
|
||||
color: RGB($white)
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
|
||||
|
||||
&.active
|
||||
background-color: RGBA($primary, 0.2)
|
||||
color: RGB($primary)
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($primary, 0.3)
|
||||
|
||||
&.edging
|
||||
background-color: RGBA($white, 0.2)
|
||||
color: RGB($white)
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
|
||||
|
||||
input
|
||||
display: none // So it doesnt get in the way of the drop as that breaks things
|
||||
|
||||
&.error
|
||||
background-color: RGBA($critical, 0.2)
|
||||
color: RGB($critical)
|
||||
box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($critical, 0.3)
|
41
onlylegs/static/sass/components/buttons/info-button.sass
Normal file
41
onlylegs/static/sass/components/buttons/info-button.sass
Normal file
|
@ -0,0 +1,41 @@
|
|||
.info-button
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
|
||||
position: fixed
|
||||
bottom: 0.75rem
|
||||
right: -3rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
color: RGB($fg-white)
|
||||
border-radius: $rad
|
||||
border: none
|
||||
opacity: 0
|
||||
|
||||
z-index: 20
|
||||
cursor: pointer
|
||||
transition: all 0.2s cubic-bezier(.86, 0, .07, 1)
|
||||
|
||||
&:hover
|
||||
color: RGB($info)
|
||||
|
||||
svg
|
||||
margin: 0.5rem
|
||||
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
&.show
|
||||
right: 0.75rem
|
||||
opacity: 1
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.info-button
|
||||
bottom: 4.25rem
|
94
onlylegs/static/sass/components/buttons/pill.sass
Normal file
94
onlylegs/static/sass/components/buttons/pill.sass
Normal file
|
@ -0,0 +1,94 @@
|
|||
.pill-row
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
> div
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
display: flex
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
border-radius: $rad
|
||||
box-shadow: 0 1px 0 RGB($bg-100), 0 -1px 0 RGB($bg-300)
|
||||
|
||||
.pill-text
|
||||
margin: 0
|
||||
padding: 0.5rem 1rem
|
||||
|
||||
width: auto
|
||||
height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
position: relative
|
||||
|
||||
text-align: center
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
color: RGB($fg-white)
|
||||
border-radius: $rad
|
||||
|
||||
.pill-item
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 2.5rem
|
||||
height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
position: relative
|
||||
|
||||
border: none
|
||||
background-color: transparent
|
||||
color: RGB($fg-white)
|
||||
|
||||
svg
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
&:hover
|
||||
cursor: pointer
|
||||
|
||||
color: RGB($primary)
|
||||
|
||||
.pill__critical
|
||||
color: RGB($critical)
|
||||
|
||||
span
|
||||
background: RGB($critical)
|
||||
color: RGB($fg-white)
|
||||
|
||||
svg
|
||||
color: RGB($critical)
|
||||
|
||||
&:hover
|
||||
color: RGB($fg-white)
|
||||
|
||||
.pill__info
|
||||
color: RGB($info)
|
||||
|
||||
span
|
||||
color: RGB($info)
|
||||
|
||||
&:hover
|
||||
color: RGB($fg-white)
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.tool-tip
|
||||
display: none
|
41
onlylegs/static/sass/components/buttons/top-of-page.sass
Normal file
41
onlylegs/static/sass/components/buttons/top-of-page.sass
Normal file
|
@ -0,0 +1,41 @@
|
|||
.top-of-page
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
|
||||
position: fixed
|
||||
bottom: 0.75rem
|
||||
right: -3rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
color: RGB($fg-white)
|
||||
border-radius: $rad
|
||||
border: none
|
||||
opacity: 0
|
||||
|
||||
z-index: 20
|
||||
cursor: pointer
|
||||
transition: all 0.2s cubic-bezier(.86, 0, .07, 1)
|
||||
|
||||
&:hover
|
||||
color: RGB($primary)
|
||||
|
||||
svg
|
||||
margin: 0.5rem
|
||||
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
&.show
|
||||
right: 0.75rem
|
||||
opacity: 1
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.top-of-page
|
||||
bottom: 4.25rem
|
241
onlylegs/static/sass/components/gallery.sass
Normal file
241
onlylegs/static/sass/components/gallery.sass
Normal file
|
@ -0,0 +1,241 @@
|
|||
.gallery-grid
|
||||
margin: 0
|
||||
padding: 0.35rem
|
||||
|
||||
width: 100%
|
||||
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
|
||||
|
||||
.gallery-item
|
||||
margin: 0.35rem
|
||||
padding: 0
|
||||
|
||||
height: auto
|
||||
|
||||
position: relative
|
||||
|
||||
border-radius: $rad-inner
|
||||
box-shadow: 0 0.15rem 0.4rem 0.1rem RGBA($bg-100, 0.4)
|
||||
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
transition: box-shadow 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
.image-filter
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
min-height: 30%
|
||||
height: auto
|
||||
|
||||
position: absolute
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
|
||||
background-image: linear-gradient(to top, rgba($bg-100, 0.69), transparent)
|
||||
opacity: 0 // hide
|
||||
|
||||
z-index: +4
|
||||
transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
.image-title,
|
||||
.image-subtitle
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
color: RGB($fg-white)
|
||||
text-shadow: 0px 0px 2px RGB($fg-black)
|
||||
|
||||
.image-title
|
||||
font-size: 0.9rem
|
||||
font-weight: 700
|
||||
|
||||
.image-subtitle
|
||||
font-size: 0.8rem
|
||||
font-weight: 400
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
object-fit: cover
|
||||
object-position: center
|
||||
|
||||
background-color: RGB($bg-bright)
|
||||
filter: blur(0.5rem)
|
||||
opacity: 0
|
||||
|
||||
transition: all 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
&.loaded
|
||||
filter: blur(0)
|
||||
opacity: 1
|
||||
|
||||
&:after
|
||||
content: ""
|
||||
display: block
|
||||
padding-bottom: 100%
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 0.2rem 0.4rem 0.1rem RGBA($bg-100, 0.6)
|
||||
|
||||
.image-filter
|
||||
opacity: 1
|
||||
|
||||
.group-item
|
||||
margin: 0.35rem
|
||||
padding: 0
|
||||
|
||||
height: auto
|
||||
|
||||
position: relative
|
||||
|
||||
border-radius: $rad-inner
|
||||
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
|
||||
.image-filter
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
min-height: 30%
|
||||
height: auto
|
||||
|
||||
position: absolute
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
|
||||
background-image: linear-gradient(to top, rgba($bg-100, 0.8), transparent)
|
||||
|
||||
z-index: +4
|
||||
|
||||
.image-title,
|
||||
.image-subtitle
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
color: RGB($fg-white)
|
||||
text-shadow: 0px 0px 2px RGB($fg-black)
|
||||
|
||||
.image-title
|
||||
font-size: 0.9rem
|
||||
font-weight: 700
|
||||
|
||||
.image-subtitle
|
||||
font-size: 0.8rem
|
||||
font-weight: 400
|
||||
|
||||
.images
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
display: block
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
object-fit: cover
|
||||
object-position: center
|
||||
|
||||
background-color: RGB($bg-bright)
|
||||
border-radius: $rad-inner
|
||||
box-shadow: 0 0 0.4rem 0.25rem RGBA($bg-100, 0.1)
|
||||
filter: blur(0.5rem)
|
||||
opacity: 0
|
||||
|
||||
transition: all 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
&.loaded
|
||||
filter: blur(0)
|
||||
opacity: 1
|
||||
|
||||
&.size-1
|
||||
.data-1
|
||||
transform: scale(0.8) rotate(3deg)
|
||||
|
||||
&.size-2
|
||||
.data-1
|
||||
transform: scale(0.7) rotate(-3deg) translate(10%, 10%)
|
||||
z-index: +2
|
||||
.data-2
|
||||
transform: scale(0.7) rotate(3deg) translate(-10%, -10%)
|
||||
z-index: +1
|
||||
|
||||
&.size-3
|
||||
.data-1
|
||||
transform: scale(0.6) rotate(3deg) translate(-25%, 25%)
|
||||
z-index: +3
|
||||
.data-2
|
||||
transform: scale(0.6) rotate(-5deg) translate(25%, 10%)
|
||||
z-index: +2
|
||||
.data-3
|
||||
transform: scale(0.6) rotate(-1deg) translate(-15%, -23%)
|
||||
z-index: +1
|
||||
|
||||
&:after
|
||||
content: ""
|
||||
display: block
|
||||
padding-bottom: 100%
|
||||
|
||||
&:hover
|
||||
.images
|
||||
&.size-1
|
||||
.data-1
|
||||
transform: scale(0.9) rotate(0deg)
|
||||
|
||||
&.size-2
|
||||
.data-1
|
||||
transform: scale(0.75) rotate(-1deg) translate(12%, 14%)
|
||||
z-index: +2
|
||||
.data-2
|
||||
transform: scale(0.75) rotate(1deg) translate(-12%, -10%)
|
||||
z-index: +1
|
||||
|
||||
&.size-3
|
||||
.data-1
|
||||
transform: scale(0.65) rotate(1deg) translate(-24%, 24%)
|
||||
z-index: +3
|
||||
.data-2
|
||||
transform: scale(0.65) rotate(-2deg) translate(24%, 10%)
|
||||
z-index: +2
|
||||
.data-3
|
||||
transform: scale(0.65) rotate(0deg) translate(-15%, -25%)
|
||||
z-index: +1
|
||||
|
||||
@media (max-width: 800px)
|
||||
.gallery-grid
|
||||
grid-template-columns: auto auto auto
|
42
onlylegs/static/sass/components/image-view/background.sass
Normal file
42
onlylegs/static/sass/components/image-view/background.sass
Normal file
|
@ -0,0 +1,42 @@
|
|||
.background
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
background-image: linear-gradient(to right, RGB($bg-400) 15%, RGB($bg-200) 35%, RGB($bg-400) 50%)
|
||||
background-size: 1000px 640px
|
||||
animation: imgLoading 1.8s linear infinite forwards
|
||||
|
||||
user-select: none
|
||||
overflow: hidden
|
||||
z-index: 1
|
||||
|
||||
img
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-color: RGB($fg-white)
|
||||
|
||||
filter: blur(1rem) saturate(1.2)
|
||||
transform: scale(1.1)
|
||||
|
||||
object-fit: cover
|
||||
object-position: center center
|
||||
|
||||
span
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
z-index: +1
|
21
onlylegs/static/sass/components/image-view/image.sass
Normal file
21
onlylegs/static/sass/components/image-view/image.sass
Normal file
|
@ -0,0 +1,21 @@
|
|||
.image-container
|
||||
margin: auto
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
overflow: hidden
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 100%
|
||||
|
||||
object-fit: contain
|
||||
object-position: center
|
215
onlylegs/static/sass/components/image-view/info-tab.sass
Normal file
215
onlylegs/static/sass/components/image-view/info-tab.sass
Normal file
|
@ -0,0 +1,215 @@
|
|||
.info-container
|
||||
width: 27rem
|
||||
height: 100vh
|
||||
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
|
||||
overflow-y: auto
|
||||
z-index: +4
|
||||
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
&.collapsed
|
||||
left: -27rem
|
||||
|
||||
.info-tab
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
position: relative
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
border-radius: $rad
|
||||
|
||||
transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86)
|
||||
|
||||
&.collapsed
|
||||
height: 2.5rem
|
||||
|
||||
.collapse-indicator
|
||||
transform: rotate(90deg)
|
||||
|
||||
.info-table
|
||||
height: 0
|
||||
padding: 0
|
||||
|
||||
.collapse-indicator
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
position: absolute
|
||||
top: 0.6rem
|
||||
right: 0.6rem
|
||||
|
||||
background-color: transparent
|
||||
color: RGB($primary)
|
||||
border: none
|
||||
|
||||
z-index: +2
|
||||
|
||||
transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
|
||||
cursor: pointer
|
||||
|
||||
.info-header
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 100%
|
||||
height: 2.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: start
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
position: sticky
|
||||
top: 0
|
||||
z-index: +1
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
|
||||
svg
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
fill: RGB($primary)
|
||||
|
||||
h2
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1.1rem
|
||||
font-weight: 500
|
||||
|
||||
color: RGB($primary)
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
.info-table
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
color: RGB($fg-white)
|
||||
|
||||
overflow-x: hidden
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
.link
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
color: RGB($primary)
|
||||
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
table
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
overflow-x: hidden
|
||||
border-collapse: collapse
|
||||
|
||||
tr
|
||||
white-space: nowrap
|
||||
|
||||
td
|
||||
padding-bottom: 0.5rem
|
||||
|
||||
max-width: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
|
||||
vertical-align: top
|
||||
|
||||
td:first-child
|
||||
padding-right: 0.5rem
|
||||
|
||||
width: 50%
|
||||
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
td:last-child
|
||||
width: 50%
|
||||
|
||||
white-space: normal
|
||||
word-break: break-word
|
||||
|
||||
tr:last-of-type td
|
||||
padding-bottom: 0
|
||||
|
||||
.img-colours
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
gap: 0.5rem
|
||||
|
||||
span
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 1.5rem
|
||||
height: 1.5rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
border-radius: $rad-inner
|
||||
// border: 1px solid RGB($white)
|
||||
|
||||
.img-groups
|
||||
width: 100%
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
gap: 0.5rem
|
||||
|
||||
@media (max-width: 1100px)
|
||||
.info-container
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
&.collapsed
|
||||
left: unset
|
81
onlylegs/static/sass/components/image-view/view.sass
Normal file
81
onlylegs/static/sass/components/image-view/view.sass
Normal file
|
@ -0,0 +1,81 @@
|
|||
@import 'background'
|
||||
@import 'info-tab'
|
||||
@import 'image'
|
||||
|
||||
|
||||
.image-grid
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
z-index: 3
|
||||
|
||||
.image-block
|
||||
margin: 0 0 0 27rem
|
||||
padding: 0
|
||||
|
||||
width: calc(100% - 27rem)
|
||||
height: 100vh
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
|
||||
z-index: 3
|
||||
transition: margin 0.3s cubic-bezier(0.76, 0, 0.17, 1), width 0.3s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
.pill-row
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
&.collapsed
|
||||
.image-block
|
||||
margin: 0
|
||||
width: 100%
|
||||
|
||||
|
||||
@media (max-width: 1100px)
|
||||
.image-grid
|
||||
padding: 0.5rem
|
||||
height: auto
|
||||
|
||||
.image-block
|
||||
margin: 0
|
||||
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
|
||||
|
||||
|
170
onlylegs/static/sass/components/navigation.sass
Normal file
170
onlylegs/static/sass/components/navigation.sass
Normal file
|
@ -0,0 +1,170 @@
|
|||
.navigation
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 3.5rem
|
||||
height: 100%
|
||||
height: 100dvh
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: space-between
|
||||
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
background-color: RGB($bg-100)
|
||||
color: RGB($fg-white)
|
||||
|
||||
z-index: 69
|
||||
|
||||
.logo
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 3.5rem
|
||||
height: 3.5rem
|
||||
min-height: 3.5rem
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
|
||||
.navigation-spacer
|
||||
height: 100%
|
||||
|
||||
.navigation-item
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 3.5rem
|
||||
height: 3.5rem
|
||||
min-height: 3.5rem
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: transparent
|
||||
border: none
|
||||
|
||||
text-decoration: none
|
||||
|
||||
> svg
|
||||
margin: 0
|
||||
padding: 0.5rem
|
||||
|
||||
width: 2.5rem
|
||||
height: 2.5rem
|
||||
|
||||
border-radius: $rad-inner
|
||||
color: RGB($fg-white)
|
||||
|
||||
transition: color 0.2s ease-out, transform 0.2s ease-out
|
||||
|
||||
.tool-tip
|
||||
margin: 0
|
||||
padding: 0.4rem 0.7rem
|
||||
|
||||
display: block
|
||||
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 3rem
|
||||
transform: translateY(-50%)
|
||||
|
||||
font-size: 0.9rem
|
||||
font-weight: 500
|
||||
|
||||
background-color: RGB($bg-100)
|
||||
color: RGB($fg-white)
|
||||
opacity: 0
|
||||
border-radius: $rad-inner
|
||||
|
||||
transition: opacity 0.2s cubic-bezier(.76,0,.17,1), left 0.2s cubic-bezier(.76,0,.17,1)
|
||||
|
||||
pointer-events: none
|
||||
|
||||
> svg
|
||||
margin: 0
|
||||
font-size: 1rem
|
||||
|
||||
width: 0.75rem
|
||||
height: 0.75rem
|
||||
|
||||
display: block
|
||||
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: -0.45rem
|
||||
transform: translateY(-50%)
|
||||
|
||||
color: RGB($bg-100)
|
||||
|
||||
&:hover
|
||||
> svg
|
||||
background: RGBA($fg-white, 0.1)
|
||||
|
||||
span
|
||||
opacity: 1
|
||||
left: 3.9rem
|
||||
|
||||
&.selected
|
||||
> svg
|
||||
color: RGB($primary)
|
||||
|
||||
&::before
|
||||
content: ''
|
||||
display: block
|
||||
|
||||
position: absolute
|
||||
top: 3px
|
||||
left: 0
|
||||
|
||||
width: 3px
|
||||
height: calc(100% - 6px)
|
||||
|
||||
background-color: RGB($primary)
|
||||
border-radius: $rad-inner
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.navigation
|
||||
width: 100vw
|
||||
height: 3.5rem
|
||||
|
||||
flex-direction: row
|
||||
justify-content: space-around
|
||||
|
||||
position: fixed
|
||||
top: unset
|
||||
bottom: 0
|
||||
left: 0
|
||||
|
||||
> span
|
||||
display: none
|
||||
|
||||
.logo
|
||||
display: none
|
||||
|
||||
.navigation-item
|
||||
margin: 0.25rem
|
||||
padding: 0
|
||||
|
||||
width: 3rem
|
||||
height: 3rem
|
||||
min-height: 3rem
|
||||
|
||||
.tool-tip
|
||||
display: none
|
||||
|
||||
&.selected::before
|
||||
top: unset
|
||||
bottom: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 3px
|
145
onlylegs/static/sass/components/notification.sass
Normal file
145
onlylegs/static/sass/components/notification.sass
Normal file
|
@ -0,0 +1,145 @@
|
|||
@keyframes notificationTimeout
|
||||
0%
|
||||
left: -100%
|
||||
height: 3px
|
||||
90%
|
||||
left: 0%
|
||||
height: 3px
|
||||
95%
|
||||
left: 0%
|
||||
height: 0
|
||||
100%
|
||||
left: 0%
|
||||
height: 0
|
||||
|
||||
@mixin notification($color)
|
||||
color: RGB($color)
|
||||
|
||||
&::after
|
||||
background-color: RGB($color)
|
||||
|
||||
.notifications
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 450px
|
||||
height: auto
|
||||
|
||||
position: fixed
|
||||
top: 0.3rem
|
||||
right: 0.3rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
z-index: 621
|
||||
|
||||
.sniffle__notification
|
||||
margin: 0 0 0.3rem 0
|
||||
padding: 0
|
||||
|
||||
width: 450px
|
||||
height: auto
|
||||
max-height: 100px
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
|
||||
position: relative
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
border-radius: $rad-inner
|
||||
color: RGB($fg-white)
|
||||
opacity: 0
|
||||
transform: scale(0.8)
|
||||
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
|
||||
transition: all 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
|
||||
|
||||
&::after
|
||||
content: ""
|
||||
|
||||
width: 100%
|
||||
height: 3px
|
||||
|
||||
position: absolute
|
||||
bottom: 0px
|
||||
left: 0px
|
||||
|
||||
background-color: RGB($fg-white)
|
||||
|
||||
z-index: +2
|
||||
animation: notificationTimeout 5.1s linear
|
||||
|
||||
&.success
|
||||
@include notification($success)
|
||||
&.warning
|
||||
@include notification($warning)
|
||||
&.critical
|
||||
@include notification($critical)
|
||||
&.info
|
||||
@include notification($info)
|
||||
|
||||
&.show
|
||||
opacity: 1
|
||||
transform: scale(1)
|
||||
|
||||
&.hide
|
||||
margin: 0
|
||||
max-height: 0
|
||||
|
||||
opacity: 0
|
||||
transform: translateX(100%)
|
||||
|
||||
transition: all 0.4s ease-in-out, max-height 0.2s ease-in-out
|
||||
|
||||
.sniffle__notification-icon
|
||||
margin: 0
|
||||
padding: 1rem
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
|
||||
svg
|
||||
width: 1.25rem
|
||||
height: 1.25rem
|
||||
|
||||
.sniffle__notification-text
|
||||
margin: 0
|
||||
padding: 1rem
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-self: center
|
||||
align-self: center
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
line-height: 1
|
||||
text-align: left
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.notifications
|
||||
width: calc(100vw - 0.6rem)
|
||||
height: auto
|
||||
|
||||
.sniffle__notification
|
||||
width: 100%
|
||||
|
||||
&.hide
|
||||
opacity: 0
|
||||
transform: translateY(-5rem)
|
||||
|
||||
.sniffle__notification-time
|
||||
width: 100%
|
165
onlylegs/static/sass/components/pop-up.sass
Normal file
165
onlylegs/static/sass/components/pop-up.sass
Normal file
|
@ -0,0 +1,165 @@
|
|||
.pop-up
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
position: fixed
|
||||
inset: 0
|
||||
|
||||
display: none
|
||||
|
||||
background-color: $bg-transparent
|
||||
opacity: 0
|
||||
|
||||
z-index: 101
|
||||
transition: opacity 0.2s ease
|
||||
|
||||
.pop-up__click-off
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
z-index: +1
|
||||
|
||||
.pop-up-wrapper
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 621px
|
||||
height: auto
|
||||
|
||||
position: absolute
|
||||
bottom: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, 50%) scale(0.8)
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
border-radius: $rad
|
||||
overflow: hidden
|
||||
|
||||
z-index: +2
|
||||
transition: transform 0.2s $animation-smooth
|
||||
|
||||
.pop-up-header
|
||||
margin: 0
|
||||
padding: 1rem
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
text-size-adjust: auto
|
||||
text-overflow: ellipsis
|
||||
|
||||
h2, h3
|
||||
margin: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
position: sticky
|
||||
top: 0
|
||||
|
||||
font-size: 1.5rem
|
||||
font-weight: 700
|
||||
text-align: left
|
||||
|
||||
color: RGB($fg-white)
|
||||
|
||||
p
|
||||
margin: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
text-align: left
|
||||
|
||||
color: RGB($fg-white)
|
||||
|
||||
svg
|
||||
width: 1rem
|
||||
height: 1rem
|
||||
|
||||
display: inline-block
|
||||
vertical-align: middle
|
||||
|
||||
a, .link
|
||||
color: RGB($primary)
|
||||
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
img
|
||||
margin: auto
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
max-width: 100%
|
||||
max-height: 40vh
|
||||
|
||||
border-radius: $rad-inner
|
||||
|
||||
form
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
justify-content: center
|
||||
|
||||
.pop-up-controlls
|
||||
margin: 0
|
||||
padding: 0.25rem
|
||||
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: flex-end
|
||||
gap: 0.25rem
|
||||
|
||||
background-color: RGB($bg-100)
|
||||
|
||||
&.active
|
||||
opacity: 1
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translate(-50%, 50%) scale(1)
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.pop-up
|
||||
.pop-up-wrapper
|
||||
width: calc(100% - 0.75rem)
|
||||
max-height: 95vh
|
||||
|
||||
.pop-up-content
|
||||
max-height: 100%
|
||||
|
||||
img
|
||||
max-height: 50vh
|
||||
|
||||
.pop-up-controlls button
|
||||
width: 100%
|
||||
|
||||
&.active
|
||||
opacity: 1
|
27
onlylegs/static/sass/components/tags.sass
Normal file
27
onlylegs/static/sass/components/tags.sass
Normal file
|
@ -0,0 +1,27 @@
|
|||
.tag-icon
|
||||
margin: 0
|
||||
padding: 0.25rem 0.5rem
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
gap: 0.25rem
|
||||
|
||||
font-size: 0.9rem
|
||||
font-weight: 500
|
||||
text-decoration: none
|
||||
|
||||
border-radius: $rad-inner
|
||||
border: none
|
||||
background-color: RGBA($primary, 0.1)
|
||||
color: RGB($primary)
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
|
||||
|
||||
svg
|
||||
width: 1.15rem
|
||||
height: 1.15rem
|
||||
|
||||
&:hover
|
||||
background-color: RGBA($primary, 0.3)
|
224
onlylegs/static/sass/components/upload-panel.sass
Normal file
224
onlylegs/static/sass/components/upload-panel.sass
Normal file
|
@ -0,0 +1,224 @@
|
|||
.upload-panel
|
||||
position: fixed
|
||||
left: 3.5rem
|
||||
bottom: 0
|
||||
|
||||
display: none
|
||||
|
||||
width: calc(100% - 3.5rem)
|
||||
height: 100vh
|
||||
|
||||
background-color: transparent
|
||||
color: RGB($fg-white)
|
||||
|
||||
overflow: hidden
|
||||
z-index: 68
|
||||
transition: background-color 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1.5rem
|
||||
font-weight: 700
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
|
||||
form
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
input, button
|
||||
width: 100%
|
||||
|
||||
.click-off
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
z-index: +1
|
||||
|
||||
.container
|
||||
padding: 1rem
|
||||
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: -27rem
|
||||
|
||||
width: 27rem
|
||||
height: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
|
||||
z-index: +2
|
||||
|
||||
transition: left 0.25s cubic-bezier(0.76, 0, 0.17, 1), bottom 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
#dragIndicator
|
||||
display: none
|
||||
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 5rem
|
||||
|
||||
z-index: +1
|
||||
|
||||
&::after
|
||||
content: ''
|
||||
width: 8rem
|
||||
height: 3px
|
||||
|
||||
position: absolute
|
||||
top: 0.5rem
|
||||
left: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
|
||||
background-color: RGB($bg-400)
|
||||
border-radius: $rad-inner
|
||||
|
||||
.upload-jobs
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
border-radius: $rad
|
||||
|
||||
overflow-y: auto
|
||||
|
||||
.job
|
||||
width: 100%
|
||||
height: 5rem
|
||||
min-height: 5rem
|
||||
|
||||
position: relative
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
border-radius: $rad
|
||||
|
||||
overflow: hidden
|
||||
|
||||
img
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 5rem
|
||||
|
||||
object-fit: cover
|
||||
|
||||
.img-filter
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-image: linear-gradient(to right, RGB($bg-100), transparent)
|
||||
|
||||
.job__status
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
position: absolute
|
||||
top: 0.5rem
|
||||
left: 0.5rem
|
||||
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
color: RGB($fg-white)
|
||||
|
||||
z-index: +3
|
||||
|
||||
transition: color 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
.progress
|
||||
width: 100%
|
||||
height: $rad-inner
|
||||
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: -100%
|
||||
|
||||
background-color: RGB($primary)
|
||||
|
||||
animation: uploadingLoop 1s cubic-bezier(0.76, 0, 0.17, 1) infinite
|
||||
|
||||
z-index: +5
|
||||
transition: left 1s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
&.critical
|
||||
.job__status, .progress
|
||||
color: RGB($critical)
|
||||
&.success
|
||||
.job__status
|
||||
color: RGB($success)
|
||||
.progress
|
||||
height: 0
|
||||
animation: none
|
||||
&.warning
|
||||
.job__status, .progress
|
||||
color: RGB($warning)
|
||||
|
||||
&.critical, &.success, &.warning
|
||||
.progress
|
||||
height: 0
|
||||
|
||||
&.open
|
||||
background-color: $bg-transparent
|
||||
|
||||
.container
|
||||
left: 0
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.upload-panel
|
||||
width: 100%
|
||||
height: calc(100vh - 3.5rem)
|
||||
height: calc(100dvh - 3.5rem)
|
||||
|
||||
left: 0
|
||||
bottom: 3.5rem
|
||||
|
||||
.container
|
||||
width: 100%
|
||||
height: 95%
|
||||
|
||||
left: 0
|
||||
bottom: -100vh
|
||||
|
||||
border-radius: $rad $rad 0 0
|
||||
|
||||
#dragIndicator
|
||||
display: block
|
||||
|
||||
&.open
|
||||
.container
|
||||
left: 0
|
||||
bottom: 0
|
137
onlylegs/static/sass/style.sass
Normal file
137
onlylegs/static/sass/style.sass
Normal file
|
@ -0,0 +1,137 @@
|
|||
// Default theme for OnlyLegs by FluffyBean
|
||||
// Mockup link: https://www.figma.com/file/IMZT5kZr3sAngrSHSGu5di/OnlyLegs?node-id=0%3A1
|
||||
|
||||
@import "variables"
|
||||
@import "animations"
|
||||
|
||||
@import "components/notification"
|
||||
@import "components/pop-up"
|
||||
@import "components/upload-panel"
|
||||
@import "components/tags"
|
||||
|
||||
@import "components/navigation"
|
||||
@import "components/banner"
|
||||
@import "components/gallery"
|
||||
|
||||
@import "components/buttons/top-of-page"
|
||||
@import "components/buttons/info-button"
|
||||
@import "components/buttons/pill"
|
||||
@import "components/buttons/block"
|
||||
|
||||
@import "components/image-view/view"
|
||||
|
||||
// Reset
|
||||
*
|
||||
box-sizing: border-box
|
||||
font-family: $font
|
||||
|
||||
scrollbar-color: RGB($primary) transparent
|
||||
|
||||
::-webkit-scrollbar
|
||||
width: 0.5rem
|
||||
::-webkit-scrollbar-track
|
||||
background: RGB($bg-200)
|
||||
::-webkit-scrollbar-thumb
|
||||
background: RGB($primary)
|
||||
::-webkit-scrollbar-thumb:hover
|
||||
background: RGB($fg-white)
|
||||
|
||||
html, body
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
min-height: 100vh
|
||||
max-width: 100vw
|
||||
|
||||
background-color: RGB($fg-white)
|
||||
|
||||
scroll-behavior: smooth
|
||||
overflow-x: hidden
|
||||
|
||||
.wrapper
|
||||
margin: 0
|
||||
padding: 0 0 0 3.5rem
|
||||
|
||||
min-height: 100vh
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
background-color: RGB($bg-bright)
|
||||
color: RGB($bg-100)
|
||||
|
||||
.big-text
|
||||
height: 20rem
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
color: RGB($bg-100)
|
||||
|
||||
h1
|
||||
margin: 0 2rem
|
||||
|
||||
font-size: 4rem
|
||||
font-weight: 900
|
||||
text-align: center
|
||||
|
||||
p
|
||||
margin: 0 2rem
|
||||
|
||||
max-width: 40rem
|
||||
font-size: 1rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
.error-page
|
||||
width: 100%
|
||||
height: 100vh
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: RGB($bg-bright)
|
||||
|
||||
h1
|
||||
margin: 0 2rem
|
||||
|
||||
font-size: 6.9rem
|
||||
font-weight: 900
|
||||
text-align: center
|
||||
|
||||
color: $primary
|
||||
|
||||
p
|
||||
margin: 0 2rem
|
||||
|
||||
max-width: 40rem
|
||||
font-size: 1.25rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
color: $fg-black
|
||||
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.wrapper
|
||||
padding: 0 0 3.5rem 0
|
||||
|
||||
.big-text
|
||||
height: calc(75vh - 3.5rem)
|
||||
|
||||
h1
|
||||
font-size: 3.5rem
|
||||
|
||||
.error-page
|
||||
height: calc(100vh - 3.5rem)
|
||||
|
||||
h1
|
||||
font-size: 4.5rem
|
||||
|
||||
p
|
||||
max-width: 100%
|
||||
font-size: 1rem
|
76
onlylegs/static/sass/variables.sass
Normal file
76
onlylegs/static/sass/variables.sass
Normal file
|
@ -0,0 +1,76 @@
|
|||
$bg-transparent: rgba(var(--bg-dim), 0.8)
|
||||
$bg-dim: var(--bg-dim)
|
||||
$bg-bright: var(--bg-bright)
|
||||
$bg-100: var(--bg-100)
|
||||
$bg-200: var(--bg-200)
|
||||
$bg-300: var(--bg-300)
|
||||
$bg-400: var(--bg-400)
|
||||
$bg-500: var(--bg-500)
|
||||
$bg-600: var(--bg-600)
|
||||
|
||||
$fg-dim: var(--fg-dim)
|
||||
$fg-white: var(--fg-white)
|
||||
$fg-black: var(--fg-black)
|
||||
|
||||
$black: var(--black)
|
||||
$white: var(--white)
|
||||
$red: var(--red)
|
||||
$orange: var(--orange)
|
||||
$yellow: var(--yellow)
|
||||
$green: var(--green)
|
||||
$blue: var(--blue)
|
||||
$purple: var(--purple)
|
||||
|
||||
$primary: var(--primary)
|
||||
$warning: var(--warning)
|
||||
$critical: var(--critical)
|
||||
$success: var(--success)
|
||||
$info: var(--info)
|
||||
|
||||
$rad: var(--rad)
|
||||
$rad-inner: var(--rad-inner)
|
||||
|
||||
$animation-smooth: var(--animation-smooth)
|
||||
$animation-bounce: var(--animation-bounce)
|
||||
|
||||
$font: 'Rubik', sans-serif
|
||||
$breakpoint: 800px
|
||||
|
||||
|
||||
\:root
|
||||
--bg-dim: 16, 16, 16
|
||||
--bg-bright: 232, 227, 227
|
||||
--bg-100: 21, 21, 21
|
||||
--bg-200: #{red(adjust-color(rgb(21, 21, 21), $lightness: 2%)), green(adjust-color(rgb(21, 21, 21), $lightness: 2%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 2%))}
|
||||
--bg-300: #{red(adjust-color(rgb(21, 21, 21), $lightness: 4%)), green(adjust-color(rgb(21, 21, 21), $lightness: 4%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 4%))}
|
||||
--bg-400: #{red(adjust-color(rgb(21, 21, 21), $lightness: 6%)), green(adjust-color(rgb(21, 21, 21), $lightness: 6%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 6%))}
|
||||
--bg-500: #{red(adjust-color(rgb(21, 21, 21), $lightness: 8%)), green(adjust-color(rgb(21, 21, 21), $lightness: 8%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 8%))}
|
||||
--bg-600: #{red(adjust-color(rgb(21, 21, 21), $lightness: 10%)), green(adjust-color(rgb(21, 21, 21), $lightness: 10%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 10%))}
|
||||
|
||||
--fg-dim: 102, 102, 102
|
||||
--fg-white: 232, 227, 227
|
||||
--fg-black: 16, 16, 16
|
||||
|
||||
--black: 21, 21, 21
|
||||
--white: 232, 227, 227
|
||||
--red: 182, 100, 103
|
||||
--orange: 217, 140, 95
|
||||
--yellow: 217, 188, 140
|
||||
--green: 140, 151, 125
|
||||
--blue: 141, 163, 185
|
||||
--purple: 169, 136, 176
|
||||
|
||||
--primary: var(--green) // 183, 169, 151
|
||||
--warning: var(--orange)
|
||||
--critical: var(--red)
|
||||
--success: var(--green)
|
||||
--info: var(--blue)
|
||||
|
||||
--rad: 6px
|
||||
--rad-inner: calc(var(--rad) / 2)
|
||||
|
||||
--animation-smooth: cubic-bezier(0.76, 0, 0.17, 1)
|
||||
--animation-bounce: cubic-bezier(.68,-0.55,.27,1.55)
|
||||
|
||||
--breakpoint: 800px
|
||||
|
7
onlylegs/templates/error.html
Normal file
7
onlylegs/templates/error.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block content %}
|
||||
<span class="error-page">
|
||||
<h1>{{error}}</h1>
|
||||
<p>{{msg}}</p>
|
||||
</span>
|
||||
{% endblock %}
|
296
onlylegs/templates/group.html
Normal file
296
onlylegs/templates/group.html
Normal file
|
@ -0,0 +1,296 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block nav_groups %}selected{% endblock %}
|
||||
{% block head %}
|
||||
{% if images %}
|
||||
<meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/>
|
||||
{% endif %}
|
||||
|
||||
<script type="text/javascript">
|
||||
function groupShare() {
|
||||
try {
|
||||
navigator.clipboard.writeText(window.location.href)
|
||||
addNotification("Copied link!", 4);
|
||||
} catch (err) {
|
||||
addNotification("Failed to copy link! Are you on HTTP?", 2);
|
||||
}
|
||||
}
|
||||
|
||||
{% if current_user.id == group.author.id %}
|
||||
function groupDelete() {
|
||||
cancelBtn = document.createElement('button');
|
||||
cancelBtn.classList.add('btn-block');
|
||||
cancelBtn.innerHTML = 'AAAAAAAAAA';
|
||||
cancelBtn.onclick = popupDissmiss;
|
||||
|
||||
deleteBtn = document.createElement('button');
|
||||
deleteBtn.classList.add('btn-block');
|
||||
deleteBtn.classList.add('critical');
|
||||
deleteBtn.innerHTML = 'No ragrats!';
|
||||
deleteBtn.onclick = deleteConfirm;
|
||||
|
||||
popUpShow('Yeet!',
|
||||
'Are you surrrre? This action is irreversible and very final.' +
|
||||
' This wont delete the images, but it will remove them from this group.',
|
||||
null,
|
||||
[cancelBtn, deleteBtn]);
|
||||
}
|
||||
|
||||
function deleteConfirm(event) {
|
||||
// AJAX takes control of subby form :3
|
||||
event.preventDefault();
|
||||
|
||||
let formID = {{ group.id }};
|
||||
|
||||
if (!formID) {
|
||||
addNotification("Dont tamper with the JavaScript pls!", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
const formData = new FormData();
|
||||
formData.append("group", formID);
|
||||
|
||||
fetch('{{ url_for('api.delete_group') }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
// Redirect to groups page
|
||||
window.location.href = '{{ url_for('group.groups') }}';
|
||||
} else {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here... Bad information', 2);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
addNotification('Error yeeting group!', 2);
|
||||
});
|
||||
}
|
||||
|
||||
function groupEdit() {
|
||||
// Create elements
|
||||
cancelBtn = document.createElement('button');
|
||||
cancelBtn.classList.add('btn-block');
|
||||
cancelBtn.innerHTML = 'go baaaaack';
|
||||
cancelBtn.onclick = popupDissmiss;
|
||||
|
||||
submitBtn = document.createElement('button');
|
||||
submitBtn.classList.add('btn-block');
|
||||
submitBtn.classList.add('primary');
|
||||
submitBtn.innerHTML = 'Saveeee';
|
||||
submitBtn.type = 'submit';
|
||||
submitBtn.setAttribute('form', 'editForm');
|
||||
|
||||
// Create form
|
||||
editForm = document.createElement('form');
|
||||
editForm.id = 'editForm';
|
||||
editForm.setAttribute('onsubmit', 'return edit(event);');
|
||||
|
||||
groupInput = document.createElement('input');
|
||||
groupInput.classList.add('input-block');
|
||||
groupInput.type = 'text';
|
||||
groupInput.placeholder = 'Group ID';
|
||||
groupInput.value = {{ group.id }};
|
||||
groupInput.id = 'group';
|
||||
|
||||
imageInput = document.createElement('input');
|
||||
imageInput.classList.add('input-block');
|
||||
imageInput.type = 'text';
|
||||
imageInput.placeholder = 'Image ID';
|
||||
imageInput.id = 'image';
|
||||
|
||||
actionInput = document.createElement('input');
|
||||
actionInput.classList.add('input-block');
|
||||
actionInput.type = 'text';
|
||||
actionInput.placeholder = 'add/remove';
|
||||
actionInput.value = 'add';
|
||||
actionInput.id = 'action';
|
||||
|
||||
editForm.appendChild(groupInput);
|
||||
editForm.appendChild(imageInput);
|
||||
editForm.appendChild(actionInput);
|
||||
|
||||
popUpShow(
|
||||
'Nothing stays the same',
|
||||
'Add, remove, or change, the power is in your hands...',
|
||||
editForm,
|
||||
[cancelBtn, submitBtn]
|
||||
);
|
||||
}
|
||||
|
||||
function edit(event) {
|
||||
// AJAX takes control of subby form :3
|
||||
event.preventDefault();
|
||||
|
||||
let formGroup = document.querySelector("#group").value;
|
||||
let formImage = document.querySelector("#image").value;
|
||||
let formAction = document.querySelector("#action").value;
|
||||
|
||||
if (!formGroup || !formImage || !formAction) {
|
||||
addNotification("All values must be set!", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
const formData = new FormData();
|
||||
formData.append("group", formGroup);
|
||||
formData.append("image", formImage);
|
||||
formData.append("action", formAction);
|
||||
|
||||
fetch('{{ url_for('api.modify_group') }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
addNotification('Group edited!!!', 1);
|
||||
popupDissmiss();
|
||||
} else {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here... Bad information', 2);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
addNotification('Error!!!!! Panic!!!!', 2);
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
{% if images %}
|
||||
.banner::after {
|
||||
box-shadow: 0 calc(var(--rad) * -1) 0 0 rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }});
|
||||
}
|
||||
.banner-content p {
|
||||
color: {{ text_colour }} !important;
|
||||
}
|
||||
.banner-content h1 {
|
||||
color: {{ text_colour }} !important;
|
||||
}
|
||||
|
||||
.banner-content .link {
|
||||
background-color: {{ text_colour }} !important;
|
||||
color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
|
||||
}
|
||||
.banner-content .link:hover {
|
||||
background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
|
||||
color: {{ text_colour }} !important;
|
||||
}
|
||||
|
||||
.banner-filter {
|
||||
background: linear-gradient(90deg, rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}),
|
||||
rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.banner-filter {
|
||||
background: linear-gradient(180deg, rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 1),
|
||||
rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.5)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
color: {{ text_colour }} !important;
|
||||
}
|
||||
.navigation-item.selected::before {
|
||||
background-color: {{ text_colour }} !important;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% 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 %}"/>
|
||||
<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>
|
||||
<h1 class="banner-header">{{ group.name }}</h1>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="banner-small">
|
||||
<div class="banner-content">
|
||||
<h1 class="banner-header">{{ group.name }}</h1>
|
||||
<p class="banner-info">By {{ group.author.username }}</p>
|
||||
<div class="pill-row">
|
||||
<div>
|
||||
<button class="pill-item" onclick="groupShare()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% if current_user.id == group.author.id %}
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if images %}
|
||||
<div class="gallery-grid">
|
||||
{% 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"/>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-text">
|
||||
<h1>*crickets chirping*</h1>
|
||||
{% if current_user.is_authenticated %}
|
||||
<p>Add some images to the group!</p>
|
||||
{% else %}
|
||||
<p>Login to start managing this image group!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
249
onlylegs/templates/image.html
Normal file
249
onlylegs/templates/image.html
Normal file
|
@ -0,0 +1,249 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% 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 name="theme-color" content="rgb({{ image.colours.0.0 }}{{ image.colours.0.1 }}{{ image.colours.0.2 }})"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
function imageShare() {
|
||||
try {
|
||||
navigator.clipboard.writeText(window.location.href)
|
||||
addNotification("Copied link!", 4);
|
||||
} catch (err) {
|
||||
addNotification("Failed to copy link! Are you on HTTP?", 2);
|
||||
}
|
||||
}
|
||||
function fullscreen() {
|
||||
let info = document.querySelector('.info-container');
|
||||
let wrapper = document.querySelector('.image-grid');
|
||||
|
||||
if (info.classList.contains('collapsed')) {
|
||||
info.classList.remove('collapsed');
|
||||
wrapper.classList.remove('collapsed');
|
||||
} else {
|
||||
info.classList.add('collapsed');
|
||||
wrapper.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
{% if current_user.id == image.author.id %}
|
||||
function imageDelete() {
|
||||
cancelBtn = document.createElement('button');
|
||||
cancelBtn.classList.add('btn-block');
|
||||
cancelBtn.innerHTML = 'nuuuuuuuu';
|
||||
cancelBtn.onclick = popupDissmiss;
|
||||
|
||||
deleteBtn = document.createElement('button');
|
||||
deleteBtn.classList.add('btn-block');
|
||||
deleteBtn.classList.add('critical');
|
||||
deleteBtn.innerHTML = 'Dewww eeeet!';
|
||||
deleteBtn.onclick = deleteConfirm;
|
||||
|
||||
popUpShow('DESTRUCTION!!!!!!',
|
||||
'Do you want to delete this image along with all of its data??? ' +
|
||||
'This action is irreversible!',
|
||||
null,
|
||||
[cancelBtn, deleteBtn]);
|
||||
}
|
||||
function deleteConfirm() {
|
||||
popupDissmiss();
|
||||
|
||||
fetch('{{ url_for('api.delete_image', image_id=image['id']) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete'
|
||||
})
|
||||
}).then(function(response) {
|
||||
if (response.ok) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
addNotification(`Image *clings*`, 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function imageEdit() {
|
||||
addNotification("Not an option, oops!", 3);
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.background span {
|
||||
background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 0.8),
|
||||
rgba({{ image.colours.1.0 }}, {{ image.colours.1.1 }}, {{ image.colours.1.2 }}, 0.2));
|
||||
}
|
||||
</style>
|
||||
{% 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;"/>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div class="image-grid">
|
||||
<div class="image-block">
|
||||
<div class="image-container">
|
||||
<img
|
||||
src="{{ url_for('api.file', file_name=image.filename) }}?r=prev"
|
||||
alt="{{ image.alt }}"
|
||||
onload="imgFade(this)"
|
||||
style="opacity: 0;"
|
||||
onerror="this.src='{{ url_for('static', filename='error.png')}}'"
|
||||
{% if "File" in image.exif %}
|
||||
width="{{ image.exif.File.Width.raw }}"
|
||||
height="{{ image.exif.File.Height.raw }}"
|
||||
{% endif %}
|
||||
/>
|
||||
</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 %}
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Author</td>
|
||||
<td><a href="{{ url_for('profile.profile', id=image.author.id) }}" class="link">{{ image.author.username }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upload date</td>
|
||||
<td><span class="time">{{ image.created_at }}</span></td>
|
||||
</tr>
|
||||
{% if image.description %}
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ image.description }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<div class="img-colours">
|
||||
{% for col in image.colours %}
|
||||
<span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if image.groups %}
|
||||
<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>
|
||||
{{ group['name'] }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% for tag in image.exif %}
|
||||
<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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<table>
|
||||
{% for subtag in image.exif[tag] %}
|
||||
<tr>
|
||||
<td>{{ subtag }}</td>
|
||||
{% if image.exif[tag][subtag]['formatted'] %}
|
||||
{% if image.exif[tag][subtag]['type'] == 'date' %}
|
||||
<td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
|
||||
{% else %}
|
||||
<td>{{ image.exif[tag][subtag]['formatted'] }}</td>
|
||||
{% endif %}
|
||||
{% elif image.exif[tag][subtag]['raw'] %}
|
||||
<td>{{ image.exif[tag][subtag]['raw'] }}</td>
|
||||
{% else %}
|
||||
<td class="empty-table">Oops, an error</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
let infoTab = document.querySelectorAll('.info-tab');
|
||||
|
||||
for (let i = 0; i < infoTab.length; i++) {
|
||||
infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() {
|
||||
infoTab[i].classList.toggle('collapsed');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
65
onlylegs/templates/index.html
Normal file
65
onlylegs/templates/index.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block nav_home %}selected{% endblock %}
|
||||
{% block content %}
|
||||
<div class="banner-small">
|
||||
<div class="banner-content">
|
||||
<h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
|
||||
{% if total_images == 0 %}
|
||||
<p class="banner-info">0 images D:</p>
|
||||
{% elif total_images == 69 %}
|
||||
<p class="banner-info">{{ total_images }} images, nice</p>
|
||||
{% else %}
|
||||
<p class="banner-info">{{ total_images }} images</p>
|
||||
{% endif %}
|
||||
|
||||
{% if pages > 1 %}
|
||||
<div class="pill-row">
|
||||
<div>
|
||||
{% if pages > 4 %}
|
||||
<a class="pill-item" href="{{ url_for('gallery.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M232,128a8,8,0,0,1-8,8H91.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L91.31,120H224A8,8,0,0,1,232,128ZM40,32a8,8,0,0,0-8,8V216a8,8,0,0,0,16,0V40A8,8,0,0,0,40,32Z"></path></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page-1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<span class="pill-text">
|
||||
{{ page }} / {{ pages }}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page+1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>
|
||||
</a>
|
||||
{% if pages > 4 %}
|
||||
<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M189.66,122.34a8,8,0,0,1,0,11.32l-72,72a8,8,0,0,1-11.32-11.32L164.69,136H32a8,8,0,0,1,0-16H164.69L106.34,61.66a8,8,0,0,1,11.32-11.32ZM216,32a8,8,0,0,0-8,8V216a8,8,0,0,0,16,0V40A8,8,0,0,0,216,32Z"></path></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if images %}
|
||||
<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="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-text">
|
||||
<h1>*crickets chirping*</h1>
|
||||
<p>There are no images here yet, upload some!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
167
onlylegs/templates/layout.html
Normal file
167
onlylegs/templates/layout.html
Normal file
|
@ -0,0 +1,167 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ config.WEBSITE_CONF.name }}</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="{{ config.WEBSITE_CONF.motto }}"/>
|
||||
<meta name="author" content="{{ config.WEBSITE_CONF.author }}"/>
|
||||
|
||||
<meta property="og:title" content="{{ config.WEBSITE_CONF.name }}"/>
|
||||
<meta property="og:description" content="{{ config.WEBSITE_CONF.motto }}"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
|
||||
<meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}"/>
|
||||
<meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}"/>
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<link rel="manifest" href="static/manifest.json"/>
|
||||
|
||||
<link
|
||||
href="{{url_for('static', filename='logo-black.svg')}}"
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
media="(prefers-color-scheme: light)"/>
|
||||
<link
|
||||
href="{{url_for('static', filename='logo-white.svg')}}"
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
media="(prefers-color-scheme: dark)"/>
|
||||
|
||||
<link
|
||||
rel="prefetch"
|
||||
href="{{url_for('static', filename='fonts/font.css')}}"
|
||||
type="stylesheet"/>
|
||||
|
||||
{% assets "scripts" %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
|
||||
{% assets "styles" %}
|
||||
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer>
|
||||
{% endassets %}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<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 %}
|
||||
|
||||
<div class="pop-up">
|
||||
<span class="pop-up__click-off" onclick="popupDissmiss()"></span>
|
||||
<div class="pop-up-wrapper">
|
||||
<div class="pop-up-header"></div>
|
||||
<div class="pop-up-controlls"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
<div class="navigation">
|
||||
<!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">-->
|
||||
|
||||
<a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}">
|
||||
<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>
|
||||
</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>
|
||||
</a>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,77.66a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,0l48,48a8,8,0,0,1-11.32,11.32L136,43.31V128a8,8,0,0,1-16,0V43.31L85.66,77.66A8,8,0,0,1,74.34,77.66ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16h68a4,4,0,0,1,4,4v3.46c0,13.45,11,24.79,24.46,24.54A24,24,0,0,0,152,128v-4a4,4,0,0,1,4-4h68A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Upload
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<span class="navigation-spacer"></span>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Profile
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Settings
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M141.66,133.66l-40,40A8,8,0,0,1,88,168V136H24a8,8,0,0,1,0-16H88V88a8,8,0,0,1,13.66-5.66l40,40A8,8,0,0,1,141.66,133.66ZM192,32H136a8,8,0,0,0,0,16h56V208H136a8,8,0,0,0,0,16h56a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Z"></path></svg>
|
||||
<span class="tool-tip">
|
||||
Login
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="upload-panel">
|
||||
<span class="click-off" onclick="closeUploadTab()"></span>
|
||||
<div class="container">
|
||||
<span id="dragIndicator"></span>
|
||||
<h3>Upload stuffs</h3>
|
||||
<p>May the world see your stuff 👀</p>
|
||||
<form id="uploadForm">
|
||||
<button class="fileDrop-block" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H80a8,8,0,0,1,0,16H32v64H224V136H176a8,8,0,0,1,0-16h48A16,16,0,0,1,240,136ZM85.66,77.66,120,43.31V128a8,8,0,0,0,16,0V43.31l34.34,34.35a8,8,0,0,0,11.32-11.32l-48-48a8,8,0,0,0-11.32,0l-48,48A8,8,0,0,0,85.66,77.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
|
||||
<span class="status">Choose or Drop file</span>
|
||||
<input type="file" id="file" tab-index="-1"/>
|
||||
</button>
|
||||
|
||||
<input class="input-block" type="text" placeholder="alt" id="alt"/>
|
||||
<input class="input-block" type="text" placeholder="description" id="description"/>
|
||||
<input class="input-block" type="text" placeholder="tags" id="tags"/>
|
||||
<button class="btn-block primary" type="submit">Upload</button>
|
||||
</form>
|
||||
<div class="upload-jobs"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
// Show notifications on page load
|
||||
{% for message in get_flashed_messages() %}
|
||||
addNotification('{{ message[0] }}', {{ message[1] }});
|
||||
{% endfor %}
|
||||
</script>
|
||||
|
||||
{% block script %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
149
onlylegs/templates/list.html
Normal file
149
onlylegs/templates/list.html
Normal file
|
@ -0,0 +1,149 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block nav_groups %}selected{% endblock %}
|
||||
{% block head %}
|
||||
{% if images %}
|
||||
<meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<script type="text/javascript">
|
||||
function showCreate() {
|
||||
// Create elements
|
||||
cancelBtn = document.createElement('button');
|
||||
cancelBtn.classList.add('btn-block');
|
||||
cancelBtn.innerHTML = 'nuuuuuuuu';
|
||||
cancelBtn.onclick = popupDissmiss;
|
||||
|
||||
submitBtn = document.createElement('button');
|
||||
submitBtn.classList.add('btn-block');
|
||||
submitBtn.classList.add('primary');
|
||||
submitBtn.innerHTML = 'Submit!!';
|
||||
submitBtn.type = 'submit';
|
||||
submitBtn.setAttribute('form', 'createForm');
|
||||
|
||||
// Create form
|
||||
createForm = document.createElement('form');
|
||||
createForm.id = 'createForm';
|
||||
createForm.setAttribute('onsubmit', 'return create(event);');
|
||||
|
||||
titleInput = document.createElement('input');
|
||||
titleInput.classList.add('input-block');
|
||||
titleInput.type = 'text';
|
||||
titleInput.placeholder = 'Group namey';
|
||||
titleInput.id = 'name';
|
||||
|
||||
descriptionInput = document.createElement('input');
|
||||
descriptionInput.classList.add('input-block');
|
||||
descriptionInput.type = 'text';
|
||||
descriptionInput.placeholder = 'What it about????';
|
||||
descriptionInput.id = 'description';
|
||||
|
||||
createForm.appendChild(titleInput);
|
||||
createForm.appendChild(descriptionInput);
|
||||
|
||||
popUpShow(
|
||||
'New stuff!',
|
||||
'Image groups are a simple way to "group" images together, are you ready?',
|
||||
createForm,
|
||||
[cancelBtn, submitBtn]
|
||||
);
|
||||
}
|
||||
|
||||
function create(event) {
|
||||
// AJAX takes control of subby form :3
|
||||
event.preventDefault();
|
||||
|
||||
let formName = document.querySelector("#name").value;
|
||||
let formDescription = document.querySelector("#description").value;
|
||||
|
||||
if (!formName) {
|
||||
addNotification("Group name must be set!", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make form
|
||||
const formData = new FormData();
|
||||
formData.append("name", formName);
|
||||
formData.append("description", formDescription);
|
||||
|
||||
fetch('{{ url_for('api.create_group') }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
addNotification('Group created!', 1);
|
||||
popupDissmiss();
|
||||
} else {
|
||||
switch (response.status) {
|
||||
case 500:
|
||||
addNotification('Server exploded, F\'s in chat', 2);
|
||||
break;
|
||||
case 403:
|
||||
addNotification('None but devils play past here... Bad information', 2);
|
||||
break;
|
||||
default:
|
||||
addNotification('Error logging in, blame someone', 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
addNotification('Error making group! :c', 2);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="banner-small">
|
||||
<div class="banner-content">
|
||||
<h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
|
||||
{% if groups|length == 0 %}
|
||||
<p class="banner-info">No groups!!!!</p>
|
||||
{% elif groups|length == 69 %}
|
||||
<p class="banner-info">{{ groups|length }} groups, uwu</p>
|
||||
{% else %}
|
||||
<p class="banner-info">{{ groups|length }} groups</p>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if groups %}
|
||||
<div class="gallery-grid">
|
||||
{% for group in groups %}
|
||||
<a id="group-{{ group.id }}" class="group-item" href="{{ url_for('group.group', group_id=group.id) }}" {% if group.images|length > 0 %} style="background-color: rgba({{ group.images.0.colours.0.0 }}, {{ group.images.0.colours.0.1 }}, {{ group.images.0.colours.0.2 }}, 0.4);" {% endif %}>
|
||||
<div class="image-filter">
|
||||
<p class="image-subtitle">By {{ group.author.username }}</p>
|
||||
<p class="image-title">{{ group.name }}</p>
|
||||
</div>
|
||||
<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 %}/>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-text">
|
||||
<h1>*crickets chirping*</h1>
|
||||
{% if current_user.is_authenticated %}
|
||||
<p>You can get started by creating a new image group!</p>
|
||||
{% else %}
|
||||
<p>Login to start seeing anything here!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
30
onlylegs/templates/profile.html
Normal file
30
onlylegs/templates/profile.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block nav_profile %}selected{% endblock %}
|
||||
{% block content %}
|
||||
<div class="banner-small">
|
||||
<div class="banner-content">
|
||||
<h1 class="banner-header">{{ user.username }}</h1>
|
||||
<p class="banner-info">Member since <span class="time">{{ user.joined_at }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if images %}
|
||||
<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"/>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="big-text">
|
||||
<h1>*crickets chirping*</h1>
|
||||
<p>There are no images here yet, upload some!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
12
onlylegs/templates/settings/account.html
Normal file
12
onlylegs/templates/settings/account.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% 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 %}
|
6
onlylegs/templates/settings/general.html
Normal file
6
onlylegs/templates/settings/general.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends 'settings/settings_layout.html' %}
|
||||
|
||||
{% block settings_general %}settings-nav__item-selected{% endblock %}
|
||||
{% block settings_content %}
|
||||
<h2>General</h2>
|
||||
{% endblock %}
|
30
onlylegs/templates/settings/logs.html
Normal file
30
onlylegs/templates/settings/logs.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% 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 %}
|
6
onlylegs/templates/settings/server.html
Normal file
6
onlylegs/templates/settings/server.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends 'settings/settings_layout.html' %}
|
||||
|
||||
{% block settings_server %}settings-nav__item-selected{% endblock %}
|
||||
{% block settings_content %}
|
||||
<h2>Server</h2>
|
||||
{% endblock %}
|
29
onlylegs/templates/settings/settings_layout.html
Normal file
29
onlylegs/templates/settings/settings_layout.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% 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 %}
|
0
onlylegs/utils/__init__.py
Normal file
0
onlylegs/utils/__init__.py
Normal file
25
onlylegs/utils/contrast.py
Normal file
25
onlylegs/utils/contrast.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
Calculate the contrast between two colors
|
||||
"""
|
||||
|
||||
|
||||
def contrast(background, light, dark, threshold=0.179):
|
||||
"""
|
||||
background: tuple of (r, g, b) values
|
||||
light: color to use if the background is light
|
||||
dark: color to use if the background is dark
|
||||
threshold: the threshold to use for determining lightness, the default is w3 recommended
|
||||
"""
|
||||
red = background[0]
|
||||
green = background[1]
|
||||
blue = background[2]
|
||||
|
||||
# Calculate contrast
|
||||
uicolors = [red / 255, green / 255, blue / 255]
|
||||
cont = [
|
||||
col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4
|
||||
for col in uicolors
|
||||
]
|
||||
lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
|
||||
|
||||
return light if lightness > threshold else dark
|
81
onlylegs/utils/generate_image.py
Normal file
81
onlylegs/utils/generate_image.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
"""
|
||||
Tools for generating images and thumbnails
|
||||
"""
|
||||
|
||||
import os
|
||||
import platformdirs
|
||||
from PIL import Image, ImageOps
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Image thumbnail generator
|
||||
Uses PIL to generate a thumbnail of the image and saves it to the cache directory
|
||||
Name is the filename
|
||||
resolution: 400x400 or thumb, or any other resolution
|
||||
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)
|
||||
|
||||
# no sussy business
|
||||
file_name, file_ext = secure_filename(file_name).rsplit(".")
|
||||
if not ext:
|
||||
ext = file_ext.strip(".")
|
||||
|
||||
# PIL doesnt like jpg so we convert it to jpeg
|
||||
if ext.lower() == "jpg":
|
||||
ext = "jpeg"
|
||||
|
||||
# Set resolution based on preset resolutions
|
||||
if resolution in ["prev", "preview"]:
|
||||
res_x, res_y = (1920, 1080)
|
||||
elif resolution in ["thumb", "thumbnail"]:
|
||||
res_x, res_y = (400, 400)
|
||||
elif resolution in ["icon", "favicon"]:
|
||||
res_x, res_y = (10, 10)
|
||||
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}")
|
||||
|
||||
# Check if image exists in the uploads directory
|
||||
if not os.path.exists(os.path.join(UPLOAD_PATH, f"{file_name}.{file_ext}")):
|
||||
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_icc = image.info.get("icc_profile")
|
||||
img_x, img_y = image.size
|
||||
|
||||
# Resize image to fit the resolution
|
||||
image = ImageOps.exif_transpose(image)
|
||||
image.thumbnail((min(img_x, int(res_x)), min(img_y, int(res_y))), Image.ANTIALIAS)
|
||||
|
||||
# Save image to cache directory
|
||||
try:
|
||||
image.save(
|
||||
os.path.join(CACHE_PATH, f"{file_name}_{res_x}x{res_y}.{ext}"),
|
||||
icc_profile=image_icc,
|
||||
)
|
||||
except OSError:
|
||||
# This usually happens when saving a JPEG with an ICC profile,
|
||||
# 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}"),
|
||||
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}")
|
101
onlylegs/utils/metadata/__init__.py
Normal file
101
onlylegs/utils/metadata/__init__.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
"""
|
||||
OnlyLegs - Metadata Parser
|
||||
Parse metadata from images if available
|
||||
otherwise get some basic information from the file
|
||||
"""
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import TAGS
|
||||
|
||||
from .helpers import *
|
||||
from .mapping import *
|
||||
|
||||
|
||||
class Metadata:
|
||||
"""
|
||||
Metadata parser
|
||||
"""
|
||||
|
||||
def __init__(self, file_path):
|
||||
"""
|
||||
Initialize the metadata parser
|
||||
"""
|
||||
self.file_path = file_path
|
||||
img_exif = {}
|
||||
|
||||
try:
|
||||
file = Image.open(file_path)
|
||||
tags = file._getexif()
|
||||
img_exif = {}
|
||||
|
||||
for tag, value in TAGS.items():
|
||||
if tag in tags:
|
||||
img_exif[value] = tags[tag]
|
||||
|
||||
img_exif["FileName"] = os.path.basename(file_path)
|
||||
img_exif["FileSize"] = os.path.getsize(file_path)
|
||||
img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
|
||||
img_exif["FileWidth"], img_exif["FileHeight"] = file.size
|
||||
|
||||
file.close()
|
||||
except TypeError:
|
||||
img_exif["FileName"] = os.path.basename(file_path)
|
||||
img_exif["FileSize"] = os.path.getsize(file_path)
|
||||
img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
|
||||
img_exif["FileWidth"], img_exif["FileHeight"] = file.size
|
||||
|
||||
self.encoded = img_exif
|
||||
|
||||
def yoink(self):
|
||||
"""
|
||||
Yoinks the metadata from the image
|
||||
"""
|
||||
if not os.path.isfile(self.file_path):
|
||||
return None
|
||||
return self.format_data(self.encoded)
|
||||
|
||||
@staticmethod
|
||||
def format_data(encoded_exif):
|
||||
"""
|
||||
Formats the data into a dictionary
|
||||
"""
|
||||
exif = {
|
||||
"Photographer": {},
|
||||
"Camera": {},
|
||||
"Software": {},
|
||||
"File": {},
|
||||
}
|
||||
|
||||
# Thanks chatGPT xP
|
||||
# the helper function works, so not sure why it triggers pylint
|
||||
for key, value in encoded_exif.items():
|
||||
for mapping_name, mapping_val in EXIF_MAPPING:
|
||||
if key in mapping_val:
|
||||
if len(mapping_val[key]) == 2:
|
||||
exif[mapping_name][mapping_val[key][0]] = {
|
||||
"raw": value,
|
||||
"formatted": (
|
||||
getattr(
|
||||
helpers, # pylint: disable=E0602
|
||||
mapping_val[key][1],
|
||||
)(value)
|
||||
),
|
||||
}
|
||||
else:
|
||||
exif[mapping_name][mapping_val[key][0]] = {
|
||||
"raw": value,
|
||||
}
|
||||
continue
|
||||
|
||||
# Remove empty keys
|
||||
if not exif["Photographer"]:
|
||||
del exif["Photographer"]
|
||||
if not exif["Camera"]:
|
||||
del exif["Camera"]
|
||||
if not exif["Software"]:
|
||||
del exif["Software"]
|
||||
if not exif["File"]:
|
||||
del exif["File"]
|
||||
|
||||
return exif
|
421
onlylegs/utils/metadata/helpers.py
Normal file
421
onlylegs/utils/metadata/helpers.py
Normal file
|
@ -0,0 +1,421 @@
|
|||
"""
|
||||
OnlyLegs - Metadata Parser
|
||||
Metadata formatting helpers
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def human_size(value):
|
||||
"""
|
||||
Formats the size of a file in a human readable format
|
||||
"""
|
||||
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||
if abs(value) < 1024.0:
|
||||
return f"{value:3.1f}{unit}B"
|
||||
value /= 1024.0
|
||||
|
||||
return f"{value:.1f}YiB"
|
||||
|
||||
|
||||
def date_format(value):
|
||||
"""
|
||||
Formats the date into a standard format
|
||||
"""
|
||||
return str(datetime.strptime(value, "%Y:%m:%d %H:%M:%S"))
|
||||
|
||||
|
||||
def fnumber(value):
|
||||
"""
|
||||
Formats the f-number into a standard format
|
||||
"""
|
||||
return "ƒ/" + str(value)
|
||||
|
||||
|
||||
def iso(value):
|
||||
"""
|
||||
Formats the ISO into a standard format
|
||||
"""
|
||||
return "ISO " + str(value)
|
||||
|
||||
|
||||
def shutter(value):
|
||||
"""
|
||||
Formats the shutter speed into a standard format
|
||||
"""
|
||||
return str(value) + " s"
|
||||
|
||||
|
||||
def focal_length(value):
|
||||
"""
|
||||
Formats the focal length into a standard format
|
||||
"""
|
||||
try:
|
||||
calculated = value[0] / value[1]
|
||||
except TypeError:
|
||||
calculated = value
|
||||
|
||||
return str(calculated) + " mm"
|
||||
|
||||
|
||||
def exposure(value):
|
||||
"""
|
||||
Formats the exposure value into a standard format
|
||||
"""
|
||||
return str(value) + " EV"
|
||||
|
||||
|
||||
def color_space(value):
|
||||
"""
|
||||
Maps the value of the color space to a human readable format
|
||||
"""
|
||||
value_map = {0: "Reserved", 1: "sRGB", 65535: "Uncalibrated"}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def flash(value):
|
||||
"""
|
||||
Maps the value of the flash to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Did not fire",
|
||||
1: "Fired",
|
||||
5: "Strobe return light not detected",
|
||||
7: "Strobe return light detected",
|
||||
9: "Fired, compulsory",
|
||||
13: "Fired, compulsory, return light not detected",
|
||||
15: "Fired, compulsory, return light detected",
|
||||
16: "Did not fire, compulsory",
|
||||
24: "Did not fire, auto mode",
|
||||
25: "Fired, auto mode",
|
||||
29: "Fired, auto mode, return light not detected",
|
||||
31: "Fired, auto mode, return light detected",
|
||||
32: "No function",
|
||||
65: "Fired, red-eye reduction mode",
|
||||
69: "Fired, red-eye reduction mode, return light not detected",
|
||||
71: "Fired, red-eye reduction mode, return light detected",
|
||||
73: "Fired, compulsory, red-eye reduction mode",
|
||||
77: "Fired, compulsory, red-eye reduction mode, return light not detected",
|
||||
79: "Fired, compulsory, red-eye reduction mode, return light detected",
|
||||
89: "Fired, auto mode, red-eye reduction mode",
|
||||
93: "Fired, auto mode, return light not detected, red-eye reduction mode",
|
||||
95: "Fired, auto mode, return light detected, red-eye reduction mode",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def exposure_program(value):
|
||||
"""
|
||||
Maps the value of the exposure program to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Not defined",
|
||||
1: "Manual",
|
||||
2: "Normal program",
|
||||
3: "Aperture priority",
|
||||
4: "Shutter priority",
|
||||
5: "Creative program",
|
||||
6: "Action program",
|
||||
7: "Portrait mode",
|
||||
8: "Landscape mode",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def metering_mode(value):
|
||||
"""
|
||||
Maps the value of the metering mode to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Unknown",
|
||||
1: "Average",
|
||||
2: "Center-Weighted Average",
|
||||
3: "Spot",
|
||||
4: "Multi-Spot",
|
||||
5: "Pattern",
|
||||
6: "Partial",
|
||||
255: "Other",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def resolution_unit(value):
|
||||
"""
|
||||
Maps the value of the resolution unit to a human readable format
|
||||
"""
|
||||
value_map = {1: "No absolute unit of measurement", 2: "Inch", 3: "Centimeter"}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def light_source(value):
|
||||
"""
|
||||
Maps the value of the light source to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Unknown",
|
||||
1: "Daylight",
|
||||
2: "Fluorescent",
|
||||
3: "Tungsten (incandescent light)",
|
||||
4: "Flash",
|
||||
9: "Fine weather",
|
||||
10: "Cloudy weather",
|
||||
11: "Shade",
|
||||
12: "Daylight fluorescent (D 5700 - 7100K)",
|
||||
13: "Day white fluorescent (N 4600 - 5400K)",
|
||||
14: "Cool white fluorescent (W 3900 - 4500K)",
|
||||
15: "White fluorescent (WW 3200 - 3700K)",
|
||||
17: "Standard light A",
|
||||
18: "Standard light B",
|
||||
19: "Standard light C",
|
||||
20: "D55",
|
||||
21: "D65",
|
||||
22: "D75",
|
||||
23: "D50",
|
||||
24: "ISO studio tungsten",
|
||||
255: "Other light source",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def scene_capture_type(value):
|
||||
"""
|
||||
Maps the value of the scene capture type to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Standard",
|
||||
1: "Landscape",
|
||||
2: "Portrait",
|
||||
3: "Night scene",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def white_balance(value):
|
||||
"""
|
||||
Maps the value of the white balance to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Auto",
|
||||
1: "Manual",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def exposure_mode(value):
|
||||
"""
|
||||
Maps the value of the exposure mode to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Auto",
|
||||
1: "Manual",
|
||||
2: "Auto bracket",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def sensitivity_type(value):
|
||||
"""
|
||||
Maps the value of the sensitivity type to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Unknown",
|
||||
1: "Standard Output Sensitivity",
|
||||
2: "Recommended Exposure Index",
|
||||
3: "ISO Speed",
|
||||
4: "Standard Output Sensitivity and Recommended Exposure Index",
|
||||
5: "Standard Output Sensitivity and ISO Speed",
|
||||
6: "Recommended Exposure Index and ISO Speed",
|
||||
7: "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def lens_specification(value):
|
||||
"""
|
||||
Maps the value of the lens specification to a human readable format
|
||||
"""
|
||||
try:
|
||||
return str(value[0] / value[1]) + "mm - " + str(value[2] / value[3]) + "mm"
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
|
||||
def compression_type(value):
|
||||
"""
|
||||
Maps the value of the compression type to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
1: "Uncompressed",
|
||||
2: "CCITT 1D",
|
||||
3: "T4/Group 3 Fax",
|
||||
4: "T6/Group 4 Fax",
|
||||
5: "LZW",
|
||||
6: "JPEG (old-style)",
|
||||
7: "JPEG",
|
||||
8: "Adobe Deflate",
|
||||
9: "JBIG B&W",
|
||||
10: "JBIG Color",
|
||||
99: "JPEG",
|
||||
262: "Kodak 262",
|
||||
32766: "Next",
|
||||
32767: "Sony ARW Compressed",
|
||||
32769: "Packed RAW",
|
||||
32770: "Samsung SRW Compressed",
|
||||
32771: "CCIRLEW",
|
||||
32772: "Samsung SRW Compressed 2",
|
||||
32773: "PackBits",
|
||||
32809: "Thunderscan",
|
||||
32867: "Kodak KDC Compressed",
|
||||
32895: "IT8CTPAD",
|
||||
32896: "IT8LW",
|
||||
32897: "IT8MP",
|
||||
32898: "IT8BL",
|
||||
32908: "PixarFilm",
|
||||
32909: "PixarLog",
|
||||
32946: "Deflate",
|
||||
32947: "DCS",
|
||||
33003: "Aperio JPEG 2000 YCbCr",
|
||||
33005: "Aperio JPEG 2000 RGB",
|
||||
34661: "JBIG",
|
||||
34676: "SGILog",
|
||||
34677: "SGILog24",
|
||||
34712: "JPEG 2000",
|
||||
34713: "Nikon NEF Compressed",
|
||||
34715: "JBIG2 TIFF FX",
|
||||
34718: "(MDI) Binary Level Codec",
|
||||
34719: "(MDI) Progressive Transform Codec",
|
||||
34720: "(MDI) Vector",
|
||||
34887: "ESRI Lerc",
|
||||
34892: "Lossy JPEG",
|
||||
34925: "LZMA2",
|
||||
34926: "Zstd",
|
||||
34927: "WebP",
|
||||
34933: "PNG",
|
||||
34934: "JPEG XR",
|
||||
65000: "Kodak DCR Compressed",
|
||||
65535: "Pentax PEF Compressed",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def orientation(value):
|
||||
"""
|
||||
Maps the value of the orientation to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Undefined",
|
||||
1: "Horizontal (normal)",
|
||||
2: "Mirror horizontal",
|
||||
3: "Rotate 180",
|
||||
4: "Mirror vertical",
|
||||
5: "Mirror horizontal and rotate 270 CW",
|
||||
6: "Rotate 90 CW",
|
||||
7: "Mirror horizontal and rotate 90 CW",
|
||||
8: "Rotate 270 CW",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def components_configuration(value):
|
||||
"""
|
||||
Maps the value of the components configuration to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "",
|
||||
1: "Y",
|
||||
2: "Cb",
|
||||
3: "Cr",
|
||||
4: "R",
|
||||
5: "G",
|
||||
6: "B",
|
||||
}
|
||||
try:
|
||||
return "".join([value_map[int(x)] for x in value])
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def rating(value):
|
||||
"""
|
||||
Maps the value of the rating to a human readable format
|
||||
"""
|
||||
return str(value) + " stars"
|
||||
|
||||
|
||||
def rating_percent(value):
|
||||
"""
|
||||
Maps the value of the rating to a human readable format
|
||||
"""
|
||||
return str(value) + "%"
|
||||
|
||||
|
||||
def pixel_dimension(value):
|
||||
"""
|
||||
Maps the value of the pixel dimension to a human readable format
|
||||
"""
|
||||
return str(value) + " px"
|
||||
|
||||
|
||||
def title(value):
|
||||
"""
|
||||
Maps the value of the title to a human readable format
|
||||
"""
|
||||
return str(value.title())
|
||||
|
||||
|
||||
def subject_distance(value):
|
||||
"""
|
||||
Maps the value of the subject distance to a human readable format
|
||||
"""
|
||||
return str(value) + " m"
|
||||
|
||||
|
||||
def subject_distance_range(value):
|
||||
"""
|
||||
Maps the value of the subject distance range to a human readable format
|
||||
"""
|
||||
value_map = {
|
||||
0: "Unknown",
|
||||
1: "Macro",
|
||||
2: "Close view",
|
||||
3: "Distant view",
|
||||
}
|
||||
try:
|
||||
return value_map[int(value)]
|
||||
except KeyError:
|
||||
return None
|
71
onlylegs/utils/metadata/mapping.py
Normal file
71
onlylegs/utils/metadata/mapping.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
"""
|
||||
OnlyLegs - Metatada Parser
|
||||
Mapping for metadata
|
||||
"""
|
||||
|
||||
PHOTOGRAHER_MAPPING = {
|
||||
"Artist": ["Artist"],
|
||||
"UserComment": ["Comment"],
|
||||
"ImageDescription": ["Description"],
|
||||
"Copyright": ["Copyright"],
|
||||
}
|
||||
CAMERA_MAPPING = {
|
||||
"Model": ["Model", "title"],
|
||||
"Make": ["Manifacturer", "title"],
|
||||
"BodySerialNumber": ["Camera Type"],
|
||||
"LensMake": ["Lens Make", "title"],
|
||||
"LensModel": ["Lens Model", "title"],
|
||||
"LensSpecification": ["Lens Specification", "lens_specification"],
|
||||
"ComponentsConfiguration": ["Components Configuration", "components_configuration"],
|
||||
"DateTime": ["Date and Time", "date_format"],
|
||||
"DateTimeOriginal": ["Date and Time (Original)", "date_format"],
|
||||
"DateTimeDigitized": ["Date and Time (Digitized)", "date_format"],
|
||||
"OffsetTime": ["Time Offset"],
|
||||
"OffsetTimeOriginal": ["Time Offset (Original)"],
|
||||
"OffsetTimeDigitized": ["Time Offset (Digitized)"],
|
||||
"FNumber": ["FNumber", "fnumber"],
|
||||
"FocalLength": ["Focal Length", "focal_length"],
|
||||
"FocalLengthIn35mmFilm": ["Focal Length in 35mm format", "focal_length"],
|
||||
"MaxApertureValue": ["Max Aperture", "fnumber"],
|
||||
"ApertureValue": ["Aperture", "fnumber"],
|
||||
"ShutterSpeedValue": ["Shutter Speed", "shutter"],
|
||||
"ISOSpeedRatings": ["ISO Speed Ratings", "iso"],
|
||||
"ISOSpeed": ["ISO Speed", "iso"],
|
||||
"SensitivityType": ["Sensitivity Type", "sensitivity_type"],
|
||||
"ExposureBiasValue": ["Exposure Bias", "exposure"],
|
||||
"ExposureTime": ["Exposure Time", "shutter"],
|
||||
"ExposureMode": ["Exposure Mode", "exposure_mode"],
|
||||
"ExposureProgram": ["Exposure Program", "exposure_program"],
|
||||
"WhiteBalance": ["White Balance", "white_balance"],
|
||||
"Flash": ["Flash", "flash"],
|
||||
"MeteringMode": ["Metering Mode", "metering_mode"],
|
||||
"LightSource": ["Light Source", "light_source"],
|
||||
"SceneCaptureType": ["Scene Capture Type", "scene_capture_type"],
|
||||
"SubjectDistance": ["Subject Distance", "subject_distance"],
|
||||
"SubjectDistanceRange": ["Subject Distance Range", "subject_distance_range"],
|
||||
}
|
||||
SOFTWARE_MAPPING = {
|
||||
"Software": ["Software"],
|
||||
"ColorSpace": ["Colour Space", "color_space"],
|
||||
"Compression": ["Compression", "compression_type"],
|
||||
}
|
||||
FILE_MAPPING = {
|
||||
"FileName": ["Name"],
|
||||
"FileSize": ["Size", "human_size"],
|
||||
"FileFormat": ["Format"],
|
||||
"FileWidth": ["Width", "pixel_dimension"],
|
||||
"FileHeight": ["Height", "pixel_dimension"],
|
||||
"Orientation": ["Orientation", "orientation"],
|
||||
"XResolution": ["X-resolution"],
|
||||
"YResolution": ["Y-resolution"],
|
||||
"ResolutionUnit": ["Resolution Units", "resolution_unit"],
|
||||
"Rating": ["Rating", "rating"],
|
||||
"RatingPercent": ["Rating Percent", "rating_percent"],
|
||||
}
|
||||
|
||||
EXIF_MAPPING = [
|
||||
("Photographer", PHOTOGRAHER_MAPPING),
|
||||
("Camera", CAMERA_MAPPING),
|
||||
("Software", SOFTWARE_MAPPING),
|
||||
("File", FILE_MAPPING),
|
||||
]
|
1
onlylegs/views/__init__.py
Normal file
1
onlylegs/views/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# :3
|
132
onlylegs/views/group.py
Normal file
132
onlylegs/views/group.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
Onlylegs - Image Groups
|
||||
Why groups? Because I don't like calling these albums
|
||||
sounds more limiting that it actually is in this gallery
|
||||
"""
|
||||
from flask import Blueprint, render_template, url_for
|
||||
|
||||
from onlylegs.models import Post, User, GroupJunction, Group
|
||||
from onlylegs.extensions import db
|
||||
from onlylegs.utils import contrast
|
||||
|
||||
|
||||
blueprint = Blueprint("group", __name__, url_prefix="/group")
|
||||
|
||||
|
||||
@blueprint.route("/", methods=["GET"])
|
||||
def groups():
|
||||
"""
|
||||
Group overview, shows all image groups
|
||||
"""
|
||||
groups = Group.query.all()
|
||||
|
||||
# For each group, get the 3 most recent images
|
||||
for group in groups:
|
||||
group.author_username = (
|
||||
User.query.with_entities(User.username)
|
||||
.filter(User.id == group.author_id)
|
||||
.first()[0]
|
||||
)
|
||||
|
||||
# Get the 3 most recent images
|
||||
images = (
|
||||
GroupJunction.query.with_entities(GroupJunction.post_id)
|
||||
.filter(GroupJunction.group_id == group.id)
|
||||
.order_by(GroupJunction.date_added.desc())
|
||||
.limit(3)
|
||||
)
|
||||
|
||||
# For each image, get the image data and add it to the group item
|
||||
group.images = []
|
||||
for image in images:
|
||||
group.images.append(
|
||||
Post.query.with_entities(Post.filename, Post.alt, Post.colours, Post.id)
|
||||
.filter(Post.id == image[0])
|
||||
.first()
|
||||
)
|
||||
|
||||
return render_template("list.html", groups=groups)
|
||||
|
||||
|
||||
@blueprint.route("/<int:group_id>")
|
||||
def group(group_id):
|
||||
"""
|
||||
Group view, shows all images in a group
|
||||
"""
|
||||
# Get the group, if it doesn't exist, 404
|
||||
group = db.get_or_404(Group, group_id, description="Group not found! D:")
|
||||
|
||||
# Get all images in the group from the junction table
|
||||
junction = (
|
||||
GroupJunction.query.with_entities(GroupJunction.post_id)
|
||||
.filter(GroupJunction.group_id == group_id)
|
||||
.order_by(GroupJunction.date_added.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get the image data for each image in the group
|
||||
images = []
|
||||
for image in junction:
|
||||
images.append(Post.query.filter(Post.id == image[0]).first())
|
||||
|
||||
# Check contrast for the first image in the group for the banner
|
||||
text_colour = "rgb(var(--fg-black))"
|
||||
if images:
|
||||
text_colour = contrast.contrast(
|
||||
images[0].colours[0], "rgb(var(--fg-black))", "rgb(var(--fg-white))"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"group.html", group=group, images=images, text_colour=text_colour
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route("/<int:group_id>/<int:image_id>")
|
||||
def group_post(group_id, image_id):
|
||||
"""
|
||||
Image view, shows the image and its metadata from a specific group
|
||||
"""
|
||||
# Get the image, if it doesn't exist, 404
|
||||
image = db.get_or_404(Post, image_id, description="Image not found :<")
|
||||
|
||||
# Get all groups the image is in
|
||||
groups = (
|
||||
GroupJunction.query.with_entities(GroupJunction.group_id)
|
||||
.filter(GroupJunction.post_id == image_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get the group data for each group the image is in
|
||||
image.groups = []
|
||||
for group in groups:
|
||||
image.groups.append(
|
||||
Group.query.with_entities(Group.id, Group.name)
|
||||
.filter(Group.id == group[0])
|
||||
.first()
|
||||
)
|
||||
|
||||
# Get the next and previous images in the group
|
||||
next_url = (
|
||||
GroupJunction.query.with_entities(GroupJunction.post_id)
|
||||
.filter(GroupJunction.group_id == group_id)
|
||||
.filter(GroupJunction.post_id > image_id)
|
||||
.order_by(GroupJunction.date_added.asc())
|
||||
.first()
|
||||
)
|
||||
prev_url = (
|
||||
GroupJunction.query.with_entities(GroupJunction.post_id)
|
||||
.filter(GroupJunction.group_id == group_id)
|
||||
.filter(GroupJunction.post_id < image_id)
|
||||
.order_by(GroupJunction.date_added.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
# If there is a next or previous image, get the URL for it
|
||||
if next_url:
|
||||
next_url = url_for("group.group_post", group_id=group_id, image_id=next_url[0])
|
||||
if prev_url:
|
||||
prev_url = url_for("group.group_post", group_id=group_id, image_id=prev_url[0])
|
||||
|
||||
return render_template(
|
||||
"image.html", image=image, next_url=next_url, prev_url=prev_url
|
||||
)
|
81
onlylegs/views/image.py
Normal file
81
onlylegs/views/image.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
"""
|
||||
Onlylegs - Image View
|
||||
"""
|
||||
from math import ceil
|
||||
from flask import Blueprint, render_template, url_for, current_app
|
||||
from onlylegs.models import Post, GroupJunction, Group
|
||||
from onlylegs.extensions import db
|
||||
|
||||
|
||||
blueprint = Blueprint("image", __name__, url_prefix="/image")
|
||||
|
||||
|
||||
@blueprint.route("/<int:image_id>")
|
||||
def image(image_id):
|
||||
"""
|
||||
Image view, shows the image and its metadata
|
||||
"""
|
||||
# Get the image, if it doesn't exist, 404
|
||||
image = db.get_or_404(Post, image_id, description="Image not found :<")
|
||||
|
||||
# Get all groups the image is in
|
||||
groups = (
|
||||
GroupJunction.query.with_entities(GroupJunction.group_id)
|
||||
.filter(GroupJunction.post_id == image_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get the group data for each group the image is in
|
||||
image.groups = []
|
||||
for group in groups:
|
||||
image.groups.append(
|
||||
Group.query.with_entities(Group.id, Group.name)
|
||||
.filter(Group.id == group[0])
|
||||
.first()
|
||||
)
|
||||
|
||||
# Get the next and previous images
|
||||
# Check if there is a group ID set
|
||||
next_url = (
|
||||
Post.query.with_entities(Post.id)
|
||||
.filter(Post.id > image_id)
|
||||
.order_by(Post.id.asc())
|
||||
.first()
|
||||
)
|
||||
prev_url = (
|
||||
Post.query.with_entities(Post.id)
|
||||
.filter(Post.id < image_id)
|
||||
.order_by(Post.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
# If there is a next or previous image, get the url
|
||||
if next_url:
|
||||
next_url = url_for("image.image", image_id=next_url[0])
|
||||
if prev_url:
|
||||
prev_url = url_for("image.image", image_id=prev_url[0])
|
||||
|
||||
# Yoink all the images in the database
|
||||
total_images = Post.query.with_entities(Post.id).order_by(Post.id.desc()).all()
|
||||
limit = current_app.config["UPLOAD_CONF"]["max-load"]
|
||||
|
||||
# If the number of items is less than the limit, no point of calculating the page
|
||||
if len(total_images) <= limit:
|
||||
return_page = None
|
||||
else:
|
||||
# How many pages should there be
|
||||
for i in range(ceil(len(total_images) / limit)):
|
||||
# Slice the list of IDs into chunks of the limit
|
||||
for j in total_images[i * limit : (i + 1) * limit]:
|
||||
# Is our image in this chunk?
|
||||
if not image_id > j[-1]:
|
||||
return_page = i + 1
|
||||
break
|
||||
|
||||
return render_template(
|
||||
"image.html",
|
||||
image=image,
|
||||
next_url=next_url,
|
||||
prev_url=prev_url,
|
||||
return_page=return_page,
|
||||
)
|
52
onlylegs/views/index.py
Normal file
52
onlylegs/views/index.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
"""
|
||||
Onlylegs Gallery - Index view
|
||||
"""
|
||||
from math import ceil
|
||||
|
||||
from flask import Blueprint, render_template, request, current_app
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from onlylegs.models import Post
|
||||
|
||||
|
||||
blueprint = Blueprint("gallery", __name__)
|
||||
|
||||
|
||||
@blueprint.route("/")
|
||||
def index():
|
||||
"""
|
||||
Home page of the website, shows the feed of the latest images
|
||||
"""
|
||||
# meme
|
||||
if request.args.get("coffee") == "please":
|
||||
abort(418)
|
||||
|
||||
# pagination, defaults to page 1 if no page is specified
|
||||
page = request.args.get("page", default=1, type=int)
|
||||
limit = current_app.config["UPLOAD_CONF"]["max-load"]
|
||||
|
||||
# get the total number of images in the database
|
||||
# calculate the total number of pages, and make sure the page number is valid
|
||||
total_images = Post.query.with_entities(Post.id).count()
|
||||
pages = ceil(max(total_images, limit) / limit)
|
||||
if page > pages:
|
||||
abort(
|
||||
404,
|
||||
"You have reached the far and beyond, "
|
||||
+ "but you will not find your answers here.",
|
||||
)
|
||||
|
||||
# get the images for the current page
|
||||
images = (
|
||||
Post.query.with_entities(
|
||||
Post.filename, Post.alt, Post.colours, Post.created_at, Post.id
|
||||
)
|
||||
.order_by(Post.id.desc())
|
||||
.offset((page - 1) * limit)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"index.html", images=images, total_images=total_images, pages=pages, page=page
|
||||
)
|
36
onlylegs/views/profile.py
Normal file
36
onlylegs/views/profile.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""
|
||||
Onlylegs Gallery - Profile view
|
||||
"""
|
||||
from flask import Blueprint, render_template, request
|
||||
from werkzeug.exceptions import abort
|
||||
from flask_login import current_user
|
||||
|
||||
from onlylegs.models import Post, User
|
||||
|
||||
|
||||
blueprint = Blueprint("profile", __name__, url_prefix="/profile")
|
||||
|
||||
|
||||
@blueprint.route("/")
|
||||
def profile():
|
||||
"""
|
||||
Profile overview, shows all profiles on the onlylegs gallery
|
||||
"""
|
||||
user_id = request.args.get("id", default=None, type=int)
|
||||
|
||||
# If there is no userID set, check if the user is logged in and display their profile
|
||||
if not user_id:
|
||||
if current_user.is_authenticated:
|
||||
user_id = current_user.id
|
||||
else:
|
||||
abort(404, "You must be logged in to view your own profile!")
|
||||
|
||||
# Get the user's data
|
||||
user = User.query.filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
abort(404, "User not found :c")
|
||||
|
||||
images = Post.query.filter(Post.author_id == user_id).all()
|
||||
|
||||
return render_template("profile.html", user=user, images=images)
|
43
onlylegs/views/settings.py
Normal file
43
onlylegs/views/settings.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
OnlyLegs - Settings page
|
||||
"""
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
blueprint = Blueprint("settings", __name__, url_prefix="/settings")
|
||||
|
||||
|
||||
@blueprint.route("/")
|
||||
@login_required
|
||||
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")
|
Loading…
Add table
Add a link
Reference in a new issue