Merge pull request #9 from Fluffy-Bean/unstable

It works, so yupeee
This commit is contained in:
Michał 2023-04-01 18:47:02 +01:00 committed by GitHub
commit 6099f25cdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1563 additions and 1062 deletions

View file

@ -11,7 +11,8 @@ import logging
from flask_compress import Compress from flask_compress import Compress
from flask_caching import Cache from flask_caching import Cache
from flask_assets import Environment, Bundle from flask_assets import Environment, Bundle
from flask import Flask, render_template from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
# Configuration # Configuration
import platformdirs import platformdirs
@ -59,7 +60,7 @@ def create_app(test_config=None):
app.config.from_mapping(test_config) app.config.from_mapping(test_config)
# Load theme # Load theme
theme_manager.CompileTheme('default', app.root_path) theme_manager.compile_theme('default', app.root_path)
# Bundle JS files # Bundle JS files
js_scripts = Bundle('js/*.js', output='gen/packed.js') js_scripts = Bundle('js/*.js', output='gen/packed.js')
@ -68,6 +69,10 @@ def create_app(test_config=None):
# Error handlers # Error handlers
@app.errorhandler(Exception) @app.errorhandler(Exception)
def error_page(err): def error_page(err):
# If the error is not an HTTPException, return a 500 error
if not isinstance(err, HTTPException):
abort(500)
error = err.code error = err.code
msg = err.description msg = err.description
return render_template('error.html', error=error, msg=msg), err.code return render_template('error.html', error=error, msg=msg), err.code
@ -77,7 +82,7 @@ def create_app(test_config=None):
app.register_blueprint(auth.blueprint) app.register_blueprint(auth.blueprint)
# Load the different routes # Load the different routes
from .routes import api, groups, routing, settings from gallery.routes import api, groups, routing, settings
app.register_blueprint(api.blueprint) app.register_blueprint(api.blueprint)
app.register_blueprint(groups.blueprint) app.register_blueprint(groups.blueprint)
app.register_blueprint(routing.blueprint) app.register_blueprint(routing.blueprint)

View file

@ -65,15 +65,16 @@ def register():
""" """
Register a new user Register a new user
""" """
username = request.form['username'] # Thanks Fennec for reminding me to strip out the whitespace lol
email = request.form['email'] username = request.form['username'].strip()
password = request.form['password'] email = request.form['email'].strip()
password_repeat = request.form['password-repeat'] password = request.form['password'].strip()
password_repeat = request.form['password-repeat'].strip()
error = [] error = []
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b') email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
username_regex = re.compile(r'\b[A-Za-z0-9._%+-]+\b') username_regex = re.compile(r'\b[A-Za-z0-9._-]+\b')
if not username or not username_regex.match(username): if not username or not username_regex.match(username):
error.append('Username is invalid!') error.append('Username is invalid!')
@ -116,8 +117,8 @@ def login():
""" """
Log in a registered user by adding the user id to the session Log in a registered user by adding the user id to the session
""" """
username = request.form['username'] username = request.form['username'].strip()
password = request.form['password'] password = request.form['password'].strip()
user = db_session.query(db.Users).filter_by(username=username).first() user = db_session.query(db.Users).filter_by(username=username).first()
error = [] error = []

View file

@ -13,6 +13,7 @@ USER_DIR = platformdirs.user_config_dir('onlylegs')
DB_PATH = os.path.join(USER_DIR, 'gallery.sqlite') DB_PATH = os.path.join(USER_DIR, 'gallery.sqlite')
# In the future, I want to add support for other databases
# engine = create_engine('postgresql://username:password@host:port/database_name', echo=False) # engine = create_engine('postgresql://username:password@host:port/database_name', echo=False)
# engine = create_engine('mysql://username:password@host:port/database_name', echo=False) # engine = create_engine('mysql://username:password@host:port/database_name', echo=False)
engine = create_engine(f'sqlite:///{DB_PATH}', echo=False) engine = create_engine(f'sqlite:///{DB_PATH}', echo=False)
@ -61,18 +62,6 @@ class Posts (base): # pylint: disable=too-few-public-methods, C0103
junction = relationship('GroupJunction', backref='posts') junction = relationship('GroupJunction', backref='posts')
class Thumbnails (base): # pylint: disable=too-few-public-methods, C0103
"""
Thumbnail table
"""
__tablename__ = 'thumbnails'
id = Column(Integer, primary_key=True)
file_name = Column(String, unique=True, nullable=False)
file_ext = Column(String, nullable=False)
data = Column(PickleType, nullable=False)
class Groups (base): # pylint: disable=too-few-public-methods, C0103 class Groups (base): # pylint: disable=too-few-public-methods, C0103
""" """
Group table Group table

View file

View file

@ -1,26 +1,24 @@
""" """
Onlylegs - API endpoints Onlylegs - API endpoints
Used internally by the frontend and possibly by other applications
""" """
from uuid import uuid4 from uuid import uuid4
import os import os
import pathlib import pathlib
import io
import logging import logging
from datetime import datetime as dt from datetime import datetime as dt
import platformdirs
from flask import (Blueprint, send_from_directory, send_file, from flask import Blueprint, send_from_directory, abort, flash, jsonify, request, g, current_app
abort, flash, jsonify, request, g, current_app)
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from colorthief import ColorThief from colorthief import ColorThief
from PIL import Image, ImageOps, ImageFilter
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from gallery.auth import login_required from gallery.auth import login_required
from gallery import db from gallery import db
from gallery.utils import metadata as mt from gallery.utils import metadata as mt
from gallery.utils.generate_image import generate_thumbnail
blueprint = Blueprint('api', __name__, url_prefix='/api') blueprint = Blueprint('api', __name__, url_prefix='/api')
@ -29,18 +27,13 @@ db_session = db_session()
@blueprint.route('/file/<file_name>', methods=['GET']) @blueprint.route('/file/<file_name>', methods=['GET'])
def get_file(file_name): def file(file_name):
""" """
Returns a file from the uploads folder Returns a file from the uploads folder
r for resolution, 400x400 or thumb for thumbnail r for resolution, 400x400 or thumb for thumbnail
f is whether to apply filters to the image, such as blurring NSFW images
b is whether to force blur the image, even if it's not NSFW
""" """
# Get args
res = request.args.get('r', default=None, type=str) # Type of file (thumb, etc) res = request.args.get('r', default=None, type=str) # Type of file (thumb, etc)
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters # pylint: disable=W0612 ext = request.args.get('e', default=None, type=str) # File extension
blur = request.args.get('b', default=False, type=bool) # Whether to force blur
file_name = secure_filename(file_name) # Sanitize file name file_name = secure_filename(file_name) # Sanitize file name
# if no args are passed, return the raw file # if no args are passed, return the raw file
@ -50,64 +43,12 @@ def get_file(file_name):
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name) return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name)
buff = io.BytesIO() thumb = generate_thumbnail(file_name, res, ext)
img = None # Image object to be set
try: # Open image and set extension if not thumb:
img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name))
except FileNotFoundError: # FileNotFound is raised if the file doesn't exist
logging.error('File not found: %s', file_name)
abort(404) abort(404)
except OSError as err: # OSError is raised if the file is broken or corrupted
logging.error('Possibly broken image %s, error: %s', file_name, err)
abort(500)
img_ext = pathlib.Path(file_name).suffix.replace('.', '').lower() # Get file extension return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
img_ext = current_app.config['ALLOWED_EXTENSIONS'][img_ext] # Convert to MIME type
img_icc = img.info.get("icc_profile") # Get ICC profile
img = ImageOps.exif_transpose(img) # Rotate image based on EXIF data
# Todo: If type is thumb(nail), return from database instead of file system pylint: disable=W0511
# as it's faster than generating a new thumbnail on every request
if res:
if res in ['thumb', 'thumbnail']:
width, height = 400, 400
elif res in ['prev', 'preview']:
width, height = 1920, 1080
else:
try:
width, height = res.split('x')
width = int(width)
height = int(height)
except ValueError:
abort(400)
img.thumbnail((width, height), Image.LANCZOS)
# Todo: If the image has a NSFW tag, blur image for example pylint: disable=W0511
# if filtered:
# pass
# If forced to blur, blur image
if blur:
img = img.filter(ImageFilter.GaussianBlur(20))
try:
img.save(buff, img_ext, icc_profile=img_icc)
except OSError:
# This usually happens when saving a JPEG with an ICC profile,
# so we convert to RGB and try again
img = img.convert('RGB')
img.save(buff, img_ext, icc_profile=img_icc)
except Exception as err:
logging.error('Could not resize image %s, error: %s', file_name, err)
abort(500)
img.close() # Close image to free memory, learned the hard way
buff.seek(0) # Reset buffer to start
return send_file(buff, mimetype='image/' + img_ext)
@blueprint.route('/upload', methods=['POST']) @blueprint.route('/upload', methods=['POST'])
@ -171,34 +112,37 @@ def delete_image(image_id):
""" """
img = db_session.query(db.Posts).filter_by(id=image_id).first() img = db_session.query(db.Posts).filter_by(id=image_id).first()
# Check if image exists and if user is allowed to delete it (author)
if img is None: if img is None:
abort(404) abort(404)
if img.author_id != g.user.id: if img.author_id != g.user.id:
abort(403) abort(403)
# Delete file
try: try:
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name)) os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
except FileNotFoundError: except FileNotFoundError:
# File was already deleted or doesn't exist
logging.warning('File not found: %s, already deleted or never existed', img.file_name) logging.warning('File not found: %s, already deleted or never existed', img.file_name)
except Exception as err:
logging.error('Could not remove file: %s', err)
abort(500)
try: # Delete cached files
db_session.query(db.Posts).filter_by(id=image_id).delete() cache_path = os.path.join(platformdirs.user_config_dir('onlylegs'), 'cache')
cache_name = img.file_name.rsplit('.')[0]
for cache_file in pathlib.Path(cache_path).glob(cache_name + '*'):
os.remove(cache_file)
groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all() # Delete from database
for group in groups: db_session.query(db.Posts).filter_by(id=image_id).delete()
db_session.delete(group)
db_session.commit() # Remove all entries in junction table
except Exception as err: groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all()
logging.error('Could not remove from database: %s', err) for group in groups:
abort(500) db_session.delete(group)
# Commit all changes
db_session.commit()
logging.info('Removed image (%s) %s', image_id, img.file_name) logging.info('Removed image (%s) %s', image_id, img.file_name)
flash(['Image was all in Le Head!', 1]) flash(['Image was all in Le Head!', '1'])
return 'Gwa Gwa' return 'Gwa Gwa'
@ -266,40 +210,3 @@ def metadata(img_id):
exif = mt.Metadata(img_path).yoink() exif = mt.Metadata(img_path).yoink()
return jsonify(exif) return jsonify(exif)
@blueprint.route('/logfile')
@login_required
def logfile():
"""
Gets the log file and returns it as a JSON object
"""
log_dict = {}
with open('only.log', encoding='utf-8', mode='r') as file:
for i, line in enumerate(file):
line = line.split(' : ')
event = line[0].strip().split(' ')
event_data = {
'date': event[0],
'time': event[1],
'severity': event[2],
'owner': event[3]
}
message = line[1].strip()
try:
message_data = {
'code': int(message[1:4]),
'message': message[5:].strip()
}
except ValueError:
message_data = {'code': 0, 'message': message}
except Exception as err:
logging.error('Could not parse log file: %s', err)
abort(500)
log_dict[i] = {'event': event_data, 'message': message_data}
return jsonify(log_dict)

View file

@ -7,6 +7,7 @@ from flask import Blueprint, abort, render_template, url_for
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from gallery import db from gallery import db
from gallery.utils import contrast
blueprint = Blueprint('group', __name__, url_prefix='/group') blueprint = Blueprint('group', __name__, url_prefix='/group')
@ -19,21 +20,29 @@ def groups():
""" """
Group overview, shows all image groups Group overview, shows all image groups
""" """
group_list = db_session.query(db.Groups).all() groups = db_session.query(db.Groups).all()
for group_item in group_list: # For each group, get the 3 most recent images
thumbnail = db_session.query(db.GroupJunction.post_id)\ for group in groups:
.filter(db.GroupJunction.group_id == group_item.id)\ group.author_username = db_session.query(db.Users.username)\
.filter(db.Users.id == group.author_id)\
.first()[0]
# Get the 3 most recent images
images = db_session.query(db.GroupJunction.post_id)\
.filter(db.GroupJunction.group_id == group.id)\
.order_by(db.GroupJunction.date_added.desc())\ .order_by(db.GroupJunction.date_added.desc())\
.first() .limit(3)
if thumbnail: # For each image, get the image data and add it to the group item
group_item.thumbnail = db_session.query(db.Posts.file_name, db.Posts.post_alt, group.images = []
db.Posts.image_colours, db.Posts.id)\ for image in images:
.filter(db.Posts.id == thumbnail[0])\ group.images.append(db_session.query(db.Posts.file_name, db.Posts.post_alt,
.first() db.Posts.image_colours, db.Posts.id)\
.filter(db.Posts.id == image[0])\
.first())
return render_template('groups/list.html', groups=group_list) return render_template('groups/list.html', groups=groups)
@blueprint.route('/<int:group_id>') @blueprint.route('/<int:group_id>')
@ -41,26 +50,39 @@ def group(group_id):
""" """
Group view, shows all images in a group Group view, shows all images in a group
""" """
group_item = db_session.query(db.Groups).filter(db.Groups.id == group_id).first() # Get the group, if it doesn't exist, 404
group = db_session.query(db.Groups).filter(db.Groups.id == group_id).first()
if group_item is None: if group is None:
abort(404, 'Group not found! D:') abort(404, 'Group not found! D:')
group_item.author_username = db_session.query(db.Users.username)\ # Get the group's author username
.filter(db.Users.id == group_item.author_id)\ group.author_username = db_session.query(db.Users.username)\
.filter(db.Users.id == group.author_id)\
.first()[0] .first()[0]
group_images = db_session.query(db.GroupJunction.post_id)\ # Get all images in the group from the junction table
junction = db_session.query(db.GroupJunction.post_id)\
.filter(db.GroupJunction.group_id == group_id)\ .filter(db.GroupJunction.group_id == group_id)\
.order_by(db.GroupJunction.date_added.desc())\ .order_by(db.GroupJunction.date_added.desc())\
.all() .all()
# Get the image data for each image in the group
images = [] images = []
for image in group_images: for image in junction:
image = db_session.query(db.Posts).filter(db.Posts.id == image[0]).first() image = db_session.query(db.Posts).filter(db.Posts.id == image[0]).first()
images.append(image) images.append(image)
return render_template('groups/group.html', group=group_item, images=images) # Check contrast for the first image in the group for the banner
text_colour = 'rgb(var(--fg-black))'
if images[0]:
text_colour = contrast.contrast(images[0].image_colours[0],
'rgb(var(--fg-black))',
'rgb(var(--fg-white))')
return render_template('groups/group.html',
group=group,
images=images,
text_colour=text_colour)
@blueprint.route('/<int:group_id>/<int:image_id>') @blueprint.route('/<int:group_id>/<int:image_id>')
@ -68,39 +90,45 @@ def group_post(group_id, image_id):
""" """
Image view, shows the image and its metadata from a specific group Image view, shows the image and its metadata from a specific group
""" """
img = db_session.query(db.Posts).filter(db.Posts.id == image_id).first() # Get the image, if it doesn't exist, 404
image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first()
if img is None: if image is None:
abort(404, 'Image not found') abort(404, 'Image not found')
img.author_username = db_session.query(db.Users.username)\ # Get the image's author username
.filter(db.Users.id == img.author_id)\ image.author_username = db_session.query(db.Users.username)\
.filter(db.Users.id == image.author_id)\
.first()[0] .first()[0]
group_list = db_session.query(db.GroupJunction.group_id)\ # Get all groups the image is in
groups = db_session.query(db.GroupJunction.group_id)\
.filter(db.GroupJunction.post_id == image_id)\ .filter(db.GroupJunction.post_id == image_id)\
.all() .all()
img.group_list = [] # Get the group data for each group the image is in
for group_item in group_list: image.groups = []
group_item = db_session.query(db.Groups).filter(db.Groups.id == group_item[0]).first() for group in groups:
img.group_list.append(group_item) group = db_session.query(db.Groups.id, db.Groups.name)\
.filter(db.Groups.id == group[0])\
.first()
image.groups.append(group)
# Get the next and previous images in the group
next_url = db_session.query(db.GroupJunction.post_id)\ next_url = db_session.query(db.GroupJunction.post_id)\
.filter(db.GroupJunction.group_id == group_id)\ .filter(db.GroupJunction.group_id == group_id)\
.filter(db.GroupJunction.post_id > image_id)\ .filter(db.GroupJunction.post_id > image_id)\
.order_by(db.GroupJunction.date_added.asc())\ .order_by(db.GroupJunction.date_added.asc())\
.first() .first()
prev_url = db_session.query(db.GroupJunction.post_id)\ prev_url = db_session.query(db.GroupJunction.post_id)\
.filter(db.GroupJunction.group_id == group_id)\ .filter(db.GroupJunction.group_id == group_id)\
.filter(db.GroupJunction.post_id < image_id)\ .filter(db.GroupJunction.post_id < image_id)\
.order_by(db.GroupJunction.date_added.desc())\ .order_by(db.GroupJunction.date_added.desc())\
.first() .first()
# If there is a next or previous image, get the URL for it
if next_url is not None: if next_url is not None:
next_url = url_for('group.group_post', group_id=group_id, image_id=next_url[0]) next_url = url_for('group.group_post', group_id=group_id, image_id=next_url[0])
if prev_url is not None: if prev_url is not None:
prev_url = url_for('group.group_post', group_id=group_id, image_id=prev_url[0]) prev_url = url_for('group.group_post', group_id=group_id, image_id=prev_url[0])
return render_template('image.html', image=img, next_url=next_url, prev_url=prev_url) return render_template('image.html', image=image, next_url=next_url, prev_url=prev_url)

View file

@ -35,22 +35,28 @@ def image(image_id):
""" """
Image view, shows the image and its metadata Image view, shows the image and its metadata
""" """
img = db_session.query(db.Posts).filter(db.Posts.id == image_id).first() # Get the image, if it doesn't exist, 404
image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first()
if not img: if not image:
abort(404, 'Image not found :<') abort(404, 'Image not found :<')
img.author_username = db_session.query(db.Users.username)\ # Get the image's author username
.filter(db.Users.id == img.author_id).first()[0] image.author_username = db_session.query(db.Users.username)\
.filter(db.Users.id == image.author_id).first()[0]
# Get the image's groups
groups = db_session.query(db.GroupJunction.group_id)\ groups = db_session.query(db.GroupJunction.group_id)\
.filter(db.GroupJunction.post_id == image_id).all() .filter(db.GroupJunction.post_id == image_id).all()
img.groups = [] # For each group, get the group data and add it to the image item
image.groups = []
for group in groups: for group in groups:
group = db_session.query(db.Groups).filter(db.Groups.id == group[0]).first() group = db_session.query(db.Groups.id, db.Groups.name)\
img.groups.append(group) .filter(db.Groups.id == group[0])\
.first()
image.groups.append(group)
# Get the next and previous images
next_url = db_session.query(db.Posts.id)\ next_url = db_session.query(db.Posts.id)\
.filter(db.Posts.id > image_id)\ .filter(db.Posts.id > image_id)\
.order_by(db.Posts.id.asc())\ .order_by(db.Posts.id.asc())\
@ -60,12 +66,13 @@ def image(image_id):
.order_by(db.Posts.id.desc())\ .order_by(db.Posts.id.desc())\
.first() .first()
# If there is a next or previous image, get the url
if next_url: if next_url:
next_url = url_for('gallery.image', image_id=next_url[0]) next_url = url_for('gallery.image', image_id=next_url[0])
if prev_url: if prev_url:
prev_url = url_for('gallery.image', image_id=prev_url[0]) prev_url = url_for('gallery.image', image_id=prev_url[0])
return render_template('image.html', image=img, next_url=next_url, prev_url=prev_url) return render_template('image.html', image=image, next_url=next_url, prev_url=prev_url)
@blueprint.route('/profile') @blueprint.route('/profile')

View file

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View file

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 225 KiB

View file

@ -0,0 +1,25 @@
document.addEventListener('DOMContentLoaded', function() {
let labels = document.querySelectorAll('[data-label]');
for (let i = 0; i < labels.length; i++) {
labels[i].addEventListener('mouseover', function() {
let label = document.createElement('div');
label.classList.add('label');
label.innerHTML = this.dataset.label;
document.body.appendChild(label);
label.style.left = (this.offsetLeft + this.offsetWidth + 8) + 'px';
label.style.top = (this.offsetTop + (label.offsetHeight / 2) - 2) + 'px';
setTimeout(function() {
label.style.opacity = 1;
}.bind(this), 250);
});
labels[i].addEventListener('mouseout', function() {
let label = document.querySelector('.label');
label.parentNode.removeChild(label);
});
}
});

View file

@ -1,14 +1,43 @@
// Function to show login // Function to show login
function showLogin() { 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';
loginForm.appendChild(usernameInput);
loginForm.appendChild(passwordInput);
popUpShow( popUpShow(
'Login!', 'Login!',
'Need an account? <span class="link" onclick="showRegister()">Register!</span>', 'Need an account? <span class="link" onclick="showRegister()">Register!</span>',
'<button class="btn-block" onclick="popupDissmiss()">Cancelee</button>' + loginForm,
'<button class="btn-block primary" form="loginForm" type="submit">Login</button>', [cancelBtn, loginBtn]
'<form id="loginForm" onsubmit="return login(event)">' +
'<input class="input-block" type="text" placeholder="Namey" id="username"/>' +
'<input class="input-block" type="password" placeholder="Passywassy" id="password"/>' +
'</form>'
); );
} }
// Function to login // Function to login
@ -29,16 +58,13 @@ function login(event) {
formData.append("username", formUsername); formData.append("username", formUsername);
formData.append("password", formPassword); formData.append("password", formPassword);
$.ajax({ fetch('/auth/login', {
url: '/auth/login', method: 'POST',
type: 'post', body: formData
data: formData, }).then(response => {
contentType: false, if (response.status === 200) {
processData: false,
success: function (response) {
location.reload(); location.reload();
}, } else {
error: function (response) {
switch (response.status) { switch (response.status) {
case 500: case 500:
addNotification('Server exploded, F\'s in chat', 2); addNotification('Server exploded, F\'s in chat', 2);
@ -51,25 +77,68 @@ function login(event) {
break; break;
} }
} }
}).catch(error => {
addNotification('Error logging in, blame someone', 2);
}); });
} }
// Function to show register // Function to show register
function showRegister() { 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( popUpShow(
'Who are you?', 'Who are you?',
'Already have an account? <span class="link" onclick="showLogin()">Login!</span>', 'Already have an account? <span class="link" onclick="showLogin()">Login!</span>',
'<button class="btn-block" onclick="popupDissmiss()">Canceleee</button>\ registerForm,
<button class="btn-block primary" form="registerForm" type="submit">Register</button>', [cancelBtn, registerBtn]
'<form id="registerForm" onsubmit="return register(event)">\
<input class="input-block" type="text" placeholder="Namey" id="username"/>\
<input class="input-block" type="text" placeholder="E mail!" id="email"/>\
<input class="input-block" type="password" placeholder="Passywassy" id="password"/>\
<input class="input-block" type="password" placeholder="Passywassy again!" id="password-repeat"/>\
</form>'
); );
} }
// Function to register // Function to register
function register(obj) { function register(event) {
// AJAX takes control of subby form // AJAX takes control of subby form
event.preventDefault(); event.preventDefault();
@ -90,13 +159,12 @@ function register(obj) {
formData.append("password", formPassword); formData.append("password", formPassword);
formData.append("password-repeat", formPasswordRepeat); formData.append("password-repeat", formPasswordRepeat);
$.ajax({ // Send form to server
url: '/auth/register', fetch('/auth/login', {
type: 'post', method: 'POST',
data: formData, body: formData
contentType: false, }).then(response => {
processData: false, if (response.status === 200) {
success: function (response) {
if (response === "gwa gwa") { if (response === "gwa gwa") {
addNotification('Registered successfully! Now please login to continue', 1); addNotification('Registered successfully! Now please login to continue', 1);
showLogin(); showLogin();
@ -105,19 +173,20 @@ function register(obj) {
addNotification(response[i], 2); addNotification(response[i], 2);
} }
} }
}, } else {
error: function (response) {
switch (response.status) { switch (response.status) {
case 500: case 500:
addNotification('Server exploded, F\'s in chat', 2); addNotification('Server exploded, F\'s in chat', 2);
break; break;
case 403: case 403:
addNotification('None but devils play past here...', 2); addNotification('None but devils play past here... Wrong information', 2);
break; break;
default: default:
addNotification('Error logging in, blame someone', 2); addNotification('Error logging in, blame someone', 2);
break; break;
} }
} }
}).catch(error => {
addNotification('Error logging in, blame someone', 2);
}); });
} }

View file

@ -2,34 +2,6 @@
function imgFade(obj, time = 250) { function imgFade(obj, time = 250) {
$(obj).animate({ opacity: 1 }, time); $(obj).animate({ opacity: 1 }, time);
} }
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
function colourContrast(bgColor, lightColor, darkColor, threshold = 0.179) {
// if color is in hex format then convert to rgb else parese rgb
let r = 0
let g = 0
let b = 0
if (bgColor.charAt(0) === '#') {
const color = (bgColor.charAt(0) === '#') ? bgColor.substring(1, 7) : bgColor;
r = parseInt(color.substring(0, 2), 16); // hexToR
g = parseInt(color.substring(2, 4), 16); // hexToG
b = parseInt(color.substring(4, 6), 16); // hexToB
} else {
const color = bgColor.replace('rgb(', '').replace(')', '').split(',');
r = color[0];
g = color[1];
b = color[2];
}
const uicolors = [r / 255, g / 255, b / 255];
const c = uicolors.map((col) => {
if (col <= 0.03928) {
return col / 12.92;
}
return Math.pow((col + 0.055) / 1.055, 2.4);
});
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
return (L > threshold) ? darkColor : lightColor;
}
// Lazy load images when they are in view // Lazy load images when they are in view
function loadOnView() { function loadOnView() {
let lazyLoad = document.querySelectorAll('#lazy-load'); let lazyLoad = document.querySelectorAll('#lazy-load');
@ -38,7 +10,7 @@ function loadOnView() {
let image = lazyLoad[i]; let image = lazyLoad[i];
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) { if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
if (!image.src) { if (!image.src) {
image.src = `/api/file/${image.getAttribute('data-src')}?r=thumb` image.src = `/api/file/${image.getAttribute('data-src')}?r=thumb` // e=webp
} }
} }
} }
@ -47,14 +19,6 @@ function loadOnView() {
window.onload = function () { window.onload = function () {
loadOnView(); loadOnView();
const darkColor = '#151515';
const lightColor = '#E8E3E3';
let contrastCheck = document.querySelectorAll('#contrast-check');
for (let i = 0; i < contrastCheck.length; i++) {
let bgColor = contrastCheck[i].getAttribute('data-color');
contrastCheck[i].style.color = colourContrast(bgColor, lightColor, darkColor);
}
let times = document.querySelectorAll('.time'); let times = document.querySelectorAll('.time');
for (let i = 0; i < times.length; i++) { for (let i = 0; i < times.length; i++) {
// Remove milliseconds // Remove milliseconds
@ -88,16 +52,20 @@ window.onload = function () {
// Info button // Info button
let infoButton = document.querySelector('.info-button'); let infoButton = document.querySelector('.info-button');
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.remove('show'); if (infoButton) {
} else { if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.add('show'); infoButton.classList.remove('show');
} } else {
infoButton.onclick = function () { infoButton.classList.add('show');
popUpShow('OnlyLegs Gallery', }
'Using <a href="https://phosphoricons.com/">Phosphoricons</a> and <a href="https://www.gent.media/manrope">Manrope</a> <br>' + infoButton.onclick = function () {
'Made by Fluffy and others with ❤️ <br>' + popUpShow('OnlyLegs on Flask',
'<a href="https://github.com/Fluffy-Bean/onlylegs">V23.03.23</a>'); 'Using <a href="https://phosphoricons.com/">Phosphoricons</a> and ' +
'<a href="https://www.gent.media/manrope">Manrope</a> <br>' +
'Made by Fluffy and others with ❤️ <br>' +
'<a href="https://github.com/Fluffy-Bean/onlylegs">V23.03.30</a>');
}
} }
}; };
window.onscroll = function () { window.onscroll = function () {
@ -113,10 +81,13 @@ window.onscroll = function () {
// Info button // Info button
let infoButton = document.querySelector('.info-button'); let infoButton = document.querySelector('.info-button');
if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.remove('show'); if (infoButton) {
} else { if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
infoButton.classList.add('show'); infoButton.classList.remove('show');
} else {
infoButton.classList.add('show');
}
} }
}; };
window.onresize = function () { window.onresize = function () {

View file

@ -1,67 +1,69 @@
function addNotification(text='Sample notification', type=4) { function addNotification(notificationText, notificationLevel) {
let container = document.querySelector('.notifications'); let notificationContainer = document.querySelector('.notifications');
// Set the different icons for the different notification levels
let 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>';
let 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>';
let 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>';
let infoIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>';
// Create notification element // Create notification element
let div = document.createElement('div'); let notification = document.createElement('div');
div.classList.add('sniffle__notification'); notification.classList.add('sniffle__notification');
div.onclick = function() { notification.onclick = function() {
if (div.parentNode) { if (notification) {
div.classList.add('sniffle__notification--hide'); notification.classList.add('hide');
setTimeout(function() { setTimeout(function() {
container.removeChild(div); notificationContainer.removeChild(notification);
}, 500); }, 500);
} }
}; };
// Create icon element and append to notification // Create icon element and append to notification
let icon = document.createElement('span'); let iconElement = document.createElement('span');
icon.classList.add('sniffle__notification-icon'); iconElement.classList.add('sniffle__notification-icon');
switch (type) { notification.appendChild(iconElement);
case 1: // Set the icon based on the notification level, not pretty but it works :3
div.classList.add('sniffle__notification--success'); if (notificationLevel == 1) {
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M237.66,85.26l-128.4,128.4a8,8,0,0,1-11.32,0l-71.6-72a8,8,0,0,1,0-11.31l24-24a8,8,0,0,1,11.32,0l36.68,35.32a8,8,0,0,0,11.32,0l92.68-91.32a8,8,0,0,1,11.32,0l24,23.6A8,8,0,0,1,237.66,85.26Z" opacity="0.2"></path><path d="M243.28,68.24l-24-23.56a16,16,0,0,0-22.58,0L104,136h0l-.11-.11L67.25,100.62a16,16,0,0,0-22.57.06l-24,24a16,16,0,0,0,0,22.61l71.62,72a16,16,0,0,0,22.63,0L243.33,90.91A16,16,0,0,0,243.28,68.24ZM103.62,208,32,136l24-24,.11.11,36.64,35.27a16,16,0,0,0,22.52,0L208.06,56,232,79.6Z"></path></svg>'; notification.classList.add('success');
break; iconElement.innerHTML = successIcon;
case 2: } else if (notificationLevel == 2) {
div.classList.add('sniffle__notification--error'); notification.classList.add('critical');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M215.46,216H40.54C27.92,216,20,202.79,26.13,192.09L113.59,40.22c6.3-11,22.52-11,28.82,0l87.46,151.87C236,202.79,228.08,216,215.46,216Z" opacity="0.2"></path><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>'; iconElement.innerHTML = criticalIcon;
break; } else if (notificationLevel == 3) {
case 3: notification.classList.add('warning');
div.classList.add('sniffle__notification--warning'); iconElement.innerHTML = warningIcon;
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M215.46,216H40.54C27.92,216,20,202.79,26.13,192.09L113.59,40.22c6.3-11,22.52-11,28.82,0l87.46,151.87C236,202.79,228.08,216,215.46,216Z" opacity="0.2"></path><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>'; } else {
break; notification.classList.add('info');
default: iconElement.innerHTML = infoIcon;
div.classList.add('sniffle__notification--info');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2"></path><path d="M144,176a8,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,176Zm88-48A104,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,128ZM124,96a12,12,0,1,0-12-12A12,12,0,0,0,124,96Z"></path></svg>';
break;
} }
div.appendChild(icon);
// Create text element and append to notification // Create text element and append to notification
let description = document.createElement('span'); let description = document.createElement('span');
description.classList.add('sniffle__notification-text'); description.classList.add('sniffle__notification-text');
description.innerHTML = text; description.innerHTML = notificationText;
div.appendChild(description); notification.appendChild(description);
// Create span to show time remaining // Create span to show time remaining
let timer = document.createElement('span'); let timer = document.createElement('span');
timer.classList.add('sniffle__notification-time'); timer.classList.add('sniffle__notification-time');
div.appendChild(timer); notification.appendChild(timer);
// Append notification to container // Append notification to container
container.appendChild(div); notificationContainer.appendChild(notification);
setTimeout(function() { setTimeout(function() { notification.classList.add('show'); }, 5);
div.classList.add('sniffle__notification-show');
}, 100);
// Remove notification after 5 seconds // Remove notification after 5 seconds
setTimeout(function() { setTimeout(function() {
if (div.parentNode) { if (notification) {
div.classList.add('sniffle__notification--hide'); notification.classList.add('hide');
setTimeout(function() { setTimeout(function() {
container.removeChild(div); notificationContainer.removeChild(notification);
}, 500); }, 500);
} }
}, 5000); }, 5000);
} }
// uwu

View file

@ -1,42 +1,47 @@
function popUpShow(title, body, actions='<button class="btn-block" onclick="popupDissmiss()">Close</button>', content='') { function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) {
// Stop scrolling
document.querySelector("html").style.overflow = "hidden";
// Get popup elements // Get popup elements
let popup = document.querySelector('.pop-up'); let popupSelector = document.querySelector('.pop-up');
let popupContent = document.querySelector('.pop-up-content'); let headerSelector = document.querySelector('.pop-up-header');
let popupActions = document.querySelector('.pop-up-controlls'); let actionsSelector = document.querySelector('.pop-up-controlls');
// Set popup content // Clear popup elements
popupContent.innerHTML = `<h3>${title}</h3><p>${body}</p>${content}`; headerSelector.innerHTML = '';
actionsSelector.innerHTML = '';
// Set popup header and subtitle
let titleElement = document.createElement('h2');
titleElement.innerHTML = titleText;
headerSelector.appendChild(titleElement);
let subtitleElement = document.createElement('p');
subtitleElement.innerHTML = subtitleText;
headerSelector.appendChild(subtitleElement);
if (bodyContent) {
headerSelector.appendChild(bodyContent);
}
// Set buttons that will be displayed // Set buttons that will be displayed
popupActions.innerHTML = actions; if (userActions) {
// for each user action, add the element
for (let i = 0; i < userActions.length; i++) {
let action = userActions[i];
actionsSelector.appendChild(action);
}
} else {
actionsSelector.innerHTML = '<button class="btn-block" onclick="popupDissmiss()">Close</button>';
}
// Show popup // Stop scrolling and show popup
popup.style.display = 'block'; document.querySelector("html").style.overflow = "hidden";
setTimeout(function() { popupSelector.style.display = 'block';
popup.classList.add('active') setTimeout(function() { popupSelector.classList.add('active') }, 5); // 2ms delay to allow for css transition >:C
}, 10);
} }
function popupDissmiss() { function popupDissmiss() {
// un-Stop scrolling let popupSelector = document.querySelector('.pop-up');
document.querySelector("html").style.overflow = "auto"; document.querySelector("html").style.overflow = "auto";
popupSelector.classList.remove('active');
let popup = document.querySelector('.pop-up'); setTimeout(function() { popupSelector.style.display = 'none'; }, 200);
popup.classList.remove('active');
setTimeout(function() {
popup.style.display = 'none';
}, 200);
} }
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
if (document.querySelector('.pop-up').classList.contains('active')) {
popupDissmiss();
}
}
});

View file

@ -1,196 +1,281 @@
window.addEventListener("dragover",(event) => { // Remove default events on file drop, otherwise the browser will open the file
event.preventDefault(); window.addEventListener("dragover", (event) => {
},false); event.preventDefault();
window.addEventListener("drop",(event) => { }, false);
event.preventDefault(); window.addEventListener("drop", (event) => {
},false); event.preventDefault();
}, false);
function fileChanged(obj) {
document.querySelector('.fileDrop-block').classList.remove('error');
if ($(obj).val() === '') {
document.querySelector('.fileDrop-block').classList.remove('active');
document.querySelector('.fileDrop-block .status').innerHTML = 'Choose or Drop file';
} else {
document.querySelector('.fileDrop-block').classList.add('active');
document.querySelector('.fileDrop-block .status').innerHTML = obj.files[0].name;
}
}
document.addEventListener('DOMContentLoaded', function() {
// Function to upload images
const uploadForm = document.querySelector('#uploadForm');
const fileDrop = document.querySelector('.fileDrop-block');
const fileDropTitle = fileDrop.querySelector('.status');
const fileUpload = uploadForm.querySelector('#file');
$(fileUpload).val('');
// Choose or drop file button
['dragover', 'dragenter'].forEach(eventName => {
fileDrop.addEventListener(eventName, fileActivate, false);
});
['dragleave', 'drop'].forEach(eventName => {
fileDrop.addEventListener(eventName, fileDefault, false);
})
// Drop file into box
fileDrop.addEventListener('drop', fileDropHandle, false);
// Edging the file plunge :3
function fileActivate(event) {
fileDrop.classList.remove('error');
fileDrop.classList.add('edging');
fileDropTitle.innerHTML = 'Drop to upload!';
}
function fileDefault(event) {
fileDrop.classList.remove('error');
fileDrop.classList.remove('edging');
fileDropTitle.innerHTML = 'Choose or Drop file';
}
function fileDropHandle(event) {
event.preventDefault()
fileUpload.files = event.dataTransfer.files;
fileDropTitle.innerHTML = fileUpload.files[0].name;
fileDrop.classList.add('active');
}
uploadForm.addEventListener('submit', (event) => {
// AJAX takes control of subby form
event.preventDefault()
const jobList = document.querySelector(".upload-jobs");
// Check for empty upload
if ($(fileUpload).val() === '') {
fileDrop.classList.add('error');
fileDropTitle.innerHTML = 'No file selected!';
} else {
// Make form
let formData = new FormData();
formData.append("file", $("#file").prop("files")[0]);
formData.append("alt", $("#alt").val());
formData.append("description", $("#description").val());
formData.append("tags", $("#tags").val());
formData.append("submit", $("#submit").val());
// Upload the information
$.ajax({
url: '/api/upload',
type: 'post',
data: formData,
contentType: false,
processData: false,
beforeSend: function () {
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").prop("files")[0]);
jobImgFilter = document.createElement("span");
jobImgFilter.classList.add("img-filter");
jobContainer.appendChild(jobStatus);
jobContainer.appendChild(jobProgress);
jobContainer.appendChild(jobImg);
jobContainer.appendChild(jobImgFilter);
jobList.appendChild(jobContainer);
},
success: function (response) {
jobContainer.classList.add("success");
jobStatus.innerHTML = "Uploaded!";
if (!document.querySelector(".upload-panel").classList.contains("open")) {
addNotification("Image uploaded successfully", 1);
}
},
error: function (response) {
jobContainer.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);
}
},
});
// Empty values
$(fileUpload).val('');
$("#alt").val('');
$("#description").val('');
$("#tags").val('');
// Reset drop
fileDrop.classList.remove('active');
fileDropTitle.innerHTML = 'Choose or Drop file';
}
});
});
// open upload tab // open upload tab
function openUploadTab() { function openUploadTab() {
// Stop scrolling let uploadTab = document.querySelector(".upload-panel");
// Stop scrolling and open upload tab
document.querySelector("html").style.overflow = "hidden"; document.querySelector("html").style.overflow = "hidden";
document.querySelector(".content").tabIndex = "-1";
// Open upload tab
const uploadTab = document.querySelector(".upload-panel");
uploadTab.style.display = "block"; uploadTab.style.display = "block";
setTimeout(function () { uploadTab.classList.add("open"); }, 5);
setTimeout(function () {
uploadTab.classList.add("open");
}, 10);
} }
// close upload tab // close upload tab
function closeUploadTab() { function closeUploadTab() {
// un-Stop scrolling 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"; document.querySelector("html").style.overflow = "auto";
document.querySelector(".content").tabIndex = "";
// Close upload tab
const uploadTab = document.querySelector(".upload-panel");
uploadTab.classList.remove("open"); uploadTab.classList.remove("open");
setTimeout(function () { setTimeout(function () {
uploadTab.style.display = "none"; uploadTab.style.display = "none";
uploadTabContainer.style.transform = "";
uploadTab.dataset.lastY = 0;
}, 250); }, 250);
} }
// toggle upload tab // toggle upload tab
function toggleUploadTab() { function toggleUploadTab() {
if (document.querySelector(".upload-panel").classList.contains("open")) { let uploadTab = document.querySelector(".upload-panel");
if (uploadTab.classList.contains("open")) {
closeUploadTab(); closeUploadTab();
} else { } else {
openUploadTab(); 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() {
// Function to upload images
let uploadTab = document.querySelector(".upload-panel");
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);
}
},
});
clearUpload();
// Reset drop
fileDrop.classList.remove('active');
fileDropTitle.innerHTML = 'Choose or Drop file';
});
});

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -1,8 +1,7 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<span class="error-page"> <span class="error-page">
<h1>{{error}}</h1> <h1>{{error}}</h1>
<p>{{msg}}</p> <p>{{msg}}</p>
</span> </span>
{% endblock %} {% endblock %}

View file

@ -1,29 +1,40 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block nav_groups %}navigation-item__selected{% endblock %} {% block nav_groups %}selected{% endblock %}
{% block content %} {% block head %}
<div class="banner"> <style>
{% if images %} {% if images %}
<img .banner-content p {
src="/api/file/{{ images.0.file_name }}?w=1920&h=1080" color: {{ text_colour }} !important;
onload="imgFade(this)" }
style="opacity:0; background-color:rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})" .banner-content h1 {
/> color: {{ text_colour }} !important;
<span }
class="banner-filter"
style="background: linear-gradient(to right, rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}), transparent)" .banner-filter {
></span> background: linear-gradient(90deg, rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}), rgba({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}, 0.3)) !important;
}
@media (max-width: 800px) {
.banner-filter {
background: linear-gradient(180deg, rgba({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}, 0.8), rgba({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}, 0.5)) !important;
}
}
{% endif %}
</style>
{% endblock %}
{% block content %}
<div class="banner {% if not images %}small{% endif %}">
{% if not images %}
<div class="banner-content"> <div class="banner-content">
<p><span id="contrast-check" data-color="rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})">{{ group.description }}</span></p> <h1>{{ group.name }}</h1>
<h1><span id="contrast-check" data-color="rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})">{{ group.name }}</span></h1> <p>By {{ group.author_username }}</p>
<p><span id="contrast-check" data-color="rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})">By {{ group.author_username }} - {{ images|length }} Images</span></p>
</div> </div>
{% else %} {% else %}
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/> <img src="{{ url_for('api.file', file_name=images.0.file_name ) }}?r=prev" onload="imgFade(this)" style="opacity:0;"/>
<span></span> <span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
<p>{{ group.description }}</p>
<h1>{{ group.name }}</h1>
<p>By {{ group.author_username }} - {{ images|length }} Images</p> <p>By {{ group.author_username }} - {{ images|length }} Images</p>
<h1>{{ group.name }}</h1>
<p>{{ group.description }}</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -43,17 +54,17 @@
<p class="image-subtitle"></p> <p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p> <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div> </div>
<img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="imgFade(this)" style="opacity:0;" id="lazy-load"/> <img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="this.classList.add('loaded');" id="lazy-load"/>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">
<h1>No image!</h1> <h1>*crickets chirping*</h1>
{% if g.user %} {% if g.user %}
<p>You can get started by uploading an image!</p> <p>Add some images to the group!</p>
{% else %} {% else %}
<p>Login to start uploading images!</p> <p>Login to start managing this image group!</p>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View file

@ -1,16 +1,15 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block nav_groups %}navigation-item__selected{% endblock %} {% block nav_groups %}selected{% endblock %}
{% block content %} {% block content %}
<div class="banner"> <div class="banner small">
<div class="banner-content"> <div class="banner-content">
<p>{{ config.WEBSITE.motto }}</p> <h1>{{ config.WEBSITE.name }}</h1>
<h1>Groups</h1>
{% if groups|length == 0 %} {% if groups|length == 0 %}
<p>0 photo groups :<</p> <p>0 groups :<</p>
{% elif groups|length == 69 %} {% elif groups|length == 69 %}
<p>{{ groups|length }} photo groups, uwu</p> <p>{{ groups|length }} groups, uwu</p>
{% else %} {% else %}
<p>{{ groups|length }} photo groups</p> <p>{{ groups|length }} groups</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -24,37 +23,31 @@
{% if groups %} {% if groups %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for group in groups %} {% for group in groups %}
{% if group.thumbnail %} <a id="group-{{ group.id }}" class="group-item" href="{{ url_for('group.group', group_id=group.id) }}">
<a id="group-{{ group.id }}" class="gallery-item" href="{{ url_for('group.group', group_id=group.id) }}" style="background-color: rgb({{ group.thumbnail.image_colours.0.0 }}, {{ group.thumbnail.image_colours.0.1 }}, {{ group.thumbnail.image_colours.0.2 }})"> <div class="image-filter">
<div class="image-filter"> <p class="image-subtitle">By {{ group.author_username }}</p>
<p class="image-subtitle"></p> <p class="image-title">{{ group.name }}</p>
<p class="image-title">{{ group.name }}</p> </div>
</div> <div class="images size-{{ group.images|length }}">
<img data-src="{{ group.thumbnail.file_name }}" onload="imgFade(this)" style="opacity:0;" id="lazy-load"/> {% if group.images|length > 0 %}
</a> {% for image in group.images %}
{% else %} <img data-src="{{ image.file_name }}" onload="this.classList.add('loaded');" id="lazy-load" class="data-{{ loop.index }}"/>
<a id="group-{{ group.id }}" class="gallery-item" href="{{ url_for('group.group', group_id=group.id) }}"> {% endfor %}
<div class="image-filter"> {% else %}
<p class="image-subtitle"></p> <img src="{{ url_for('static', filename='error.png') }}" class="loaded"/>
<p class="image-title">{{ group.name }}</p> {% endif %}
</div> </div>
<img src="{{ url_for('static', filename='images/error.png') }}" onload="imgFade(this)" style="opacity:0;"/> </a>
</a>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">
<h1>No image groups!</h1> <h1>*crickets chirping*</h1>
{% if g.user %} {% if g.user %}
<p>You can get started by creating a new image group!</p> <p>You can get started by creating a new image group!</p>
{% else %} {% else %}
<p>Login to get started!</p> <p>Login to start seeing anything here!</p>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block script %}
<script></script>
{% endblock %}

View file

@ -1,29 +1,105 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block head %}
<meta property="og:image" content="/api/file/{{ image.file_name }}"/>
<meta name="theme-color" content="#{{ image.image_colours.0.0 }}{{ image.image_colours.0.1 }}{{ image.image_colours.0.2 }}"/>
{% endblock %}
{% block wrapper_class %}image-wrapper{% endblock %} {% block wrapper_class %}image-wrapper{% endblock %}
{% block head %}
<meta property="og:image" content="{{ url_for('api.file', file_name=image.file_name) }}"/>
<meta name="theme-color" content="#{{ image.image_colours.0.0 }}{{ image.image_colours.0.1 }}{{ image.image_colours.0.2 }}"/>
<script type="text/javascript">
function imageFullscreenOff() {
document.querySelector("html").style.overflow = "auto";
let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.classList.remove('active');
setTimeout(function() {
fullscreen.style.display = 'none';
}, 200);
}
function imageFullscreenOn() {
let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.querySelector('img').src = '{{ url_for('api.file', file_name=image.file_name) }}';
document.querySelector("html").style.overflow = "hidden";
fullscreen.style.display = 'flex';
setTimeout(function() { fullscreen.classList.add('active'); }, 5);
}
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);
}
}
{% if g.user['id'] == image['author_id'] %}
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;
function imageDelete() {
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();
$.ajax({
url: '{{ url_for('api.delete_image', image_id=image['id']) }}',
type: 'post',
data: {
action: 'delete'
},
success: function (response) {
window.location.href = '/';
},
error: function (response) {
addNotification(`Image *clings*: ${response}`, 2);
}
});
}
function imageEdit() {
addNotification("Not an option, oops!", 3);
}
{% endif %}
</script>
<style>
.background span {
background-image: linear-gradient(to top, rgba({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }}, 1), transparent);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="background"> <div class="background">
<img src="/api/file/{{ image.file_name }}?r=prev" alt="{{ image.post_alt }}" onload="imgFade(this)" style="opacity:0;"/> <img src="{{ url_for('api.file', file_name=image.file_name) }}?r=prev" alt="{{ image.post_alt }}" onload="imgFade(this)" style="opacity:0;"/>
<span style="background-image: linear-gradient(to top, rgba({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }}, 1), transparent);"></span> <span></span>
</div> </div>
<div class="image-fullscreen"> <div class="image-fullscreen" onclick="imageFullscreenOff()">
<img src="" alt="{{ image.post_alt }}" onload="imgFade(this);" style="opacity:0;" /> <img src="" alt="{{ image.post_alt }}" onload="imgFade(this);" style="opacity:0;" />
</div> </div>
<div class="image-grid"> <div class="image-grid">
<div class="image-container" id="image-container"> <div class="image-container" id="image-container">
<img <img
src="/api/file/{{ image.file_name }}?r=prev" src="{{ url_for('api.file', file_name=image.file_name) }}?r=prev"
alt="{{ image.post_alt }}" alt="{{ image.post_alt }}"
onload="imgFade(this)" style="opacity:0;" onload="imgFade(this)" style="opacity:0;"
onerror="this.src='/static/images/error.png'" onerror="this.src='{{ url_for('static', filename='error.png')}}'"
{% if "File" in image.image_exif %} {% if "File" in image.image_exif %}
width="{{ image.image_exif.File.Width.raw }}" width="{{ image.image_exif.File.Width.raw }}"
height="{{ image.image_exif.File.Height.raw }}" height="{{ image.image_exif.File.Height.raw }}"
@ -35,7 +111,7 @@
{% if next_url %} {% if next_url %}
<div> <div>
<a class="pill-item" href="{{ next_url }}"> <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="M112,56V200L40,128Z" opacity="0.2"></path><path d="M216,120H120V56a8,8,0,0,0-13.66-5.66l-72,72a8,8,0,0,0,0,11.32l72,72A8,8,0,0,0,120,200V136h96a8,8,0,0,0,0-16ZM104,180.69,51.31,128,104,75.31Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm40,112H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168a8,8,0,0,1,0,16Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Previous Previous
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
@ -44,22 +120,22 @@
</div> </div>
{% endif %} {% endif %}
<div> <div>
<button class="pill-item" id="img-fullscreen"> <button class="pill-item" onclick="imageFullscreenOn()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,56V200a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H216A8,8,0,0,1,224,56Z" opacity="0.2"></path><path d="M200,80v32a8,8,0,0,1-16,0V88H160a8,8,0,0,1,0-16h32A8,8,0,0,1,200,80ZM96,168H72V144a8,8,0,0,0-16,0v32a8,8,0,0,0,8,8H96a8,8,0,0,0,0-16ZM232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M117.66,138.34a8,8,0,0,1,0,11.32L83.31,184l18.35,18.34A8,8,0,0,1,96,216H48a8,8,0,0,1-8-8V160a8,8,0,0,1,13.66-5.66L72,172.69l34.34-34.35A8,8,0,0,1,117.66,138.34ZM208,40H160a8,8,0,0,0-5.66,13.66L172.69,72l-34.35,34.34a8,8,0,0,0,11.32,11.32L184,83.31l18.34,18.35A8,8,0,0,0,216,96V48A8,8,0,0,0,208,40Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Fullscreen Fullscreen
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span> </span>
</button> </button>
<button class="pill-item" id="img-share"> <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="M208,104V216H48V104Z" opacity="0.2"></path><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> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M212,200a36,36,0,1,1-69.85-12.25l-53-34.05a36,36,0,1,1,0-51.4l53-34a36.09,36.09,0,1,1,8.67,13.45l-53,34.05a36,36,0,0,1,0,24.5l53,34.05A36,36,0,0,1,212,200Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Share Share
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span> </span>
</button> </button>
<a class="pill-item" href="/api/file/{{ image.file_name }}" download onclick="addNotification('Download started!', 4)"> <a class="pill-item" href="/api/file/{{ image.file_name }}" 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="M232,136v64a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V136a8,8,0,0,1,8-8H224A8,8,0,0,1,232,136Z" opacity="0.2"></path><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,85.66A8,8,0,0,1,85.66,74.34L120,108.69V24a8,8,0,0,1,16,0v84.69l34.34-34.35a8,8,0,0,1,11.32,11.32l-48,48a8,8,0,0,1-11.32,0ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H84.4a4,4,0,0,1,2.83,1.17L111,145A24,24,0,0,0,145,145l23.8-23.8A4,4,0,0,1,171.6,120H224A16,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"> <span class="tool-tip">
Download Download
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
@ -68,15 +144,15 @@
</div> </div>
{% if image.author_id == g.user.id %} {% if image.author_id == g.user.id %}
<div> <div>
<button class="pill-item pill__critical" id="img-delete"> <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="M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56Z" opacity="0.2"></path><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="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-16ZM112,168a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm0-120H96V40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Delete Delete
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span> </span>
</button> </button>
<button class="pill-item pill__critical" id="img-edit"> <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="M221.66,90.34,192,120,136,64l29.66-29.66a8,8,0,0,1,11.31,0L221.66,79A8,8,0,0,1,221.66,90.34Z" opacity="0.2"></path><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="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.63ZM51.31,160l90.35-90.35,16.68,16.69L68,176.68ZM48,179.31,76.69,208H48Zm48,25.38L79.31,188l90.35-90.35h0l16.68,16.69Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Edit Edit
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
@ -87,7 +163,7 @@
{% if prev_url %} {% if prev_url %}
<div> <div>
<a class="pill-item" href="{{ prev_url }}"> <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="M216,128l-72,72V56Z" opacity="0.2"></path><path d="M221.66,122.34l-72-72A8,8,0,0,0,136,56v64H40a8,8,0,0,0,0,16h96v64a8,8,0,0,0,13.66,5.66l72-72A8,8,0,0,0,221.66,122.34ZM152,180.69V75.31L204.69,128Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,109.66-32,32a8,8,0,0,1-11.32-11.32L148.69,136H88a8,8,0,0,1,0-16h60.69l-18.35-18.34a8,8,0,0,1,11.32-11.32l32,32A8,8,0,0,1,173.66,133.66Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Next Next
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
@ -101,7 +177,7 @@
{% if image.post_description %} {% if image.post_description %}
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,56V200a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H216A8,8,0,0,1,224,56Z" opacity="0.2"></path><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,160H40V56H216V200ZM184,96a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,96Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,128Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,160Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,160H40V56H216V200ZM184,96a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,96Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,128Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,160Z"></path></svg>
<h2>Description</h2> <h2>Description</h2>
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor"> <svg class="collapse-indicator" 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> <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>
@ -114,52 +190,36 @@
{% endif %} {% endif %}
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2"></path><path d="M144,176a8,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,176Zm88-48A104,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,128ZM124,96a12,12,0,1,0-12-12A12,12,0,0,0,124,96Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg>
<h2>Info</h2> <h2>Info</h2>
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor"> <svg class="collapse-indicator" 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> <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> </svg>
</div> </div>
<div class="info-table"> <div class="info-table">
<div class="img-colours">
{% for col in image.image_colours %}
<span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
{% endfor %}
</div>
<table> <table>
<tr>
<td>Image ID</td>
<td>{{ image['id'] }}</td>
</tr>
<tr> <tr>
<td>Author</td> <td>Author</td>
<td>{{ image.author_username }}</td> <td><a href="{{ url_for('gallery.profile_id', user_id=image.author_id) }}" class="link">{{ image.author_username }}</a></td>
</tr> </tr>
<tr> <tr>
<td>Upload date</td> <td>Upload date</td>
<td><span class="time">{{ image.created_at }}</span></td> <td><span class="time">{{ image.created_at }}</span></td>
</tr> </tr>
</table> </table>
{% if group and image.author_id == g.user.id %} <div class="img-colours">
{% for col in image.image_colours %}
<span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
{% endfor %}
</div>
{% if image.groups %}
<div class="img-groups"> <div class="img-groups">
{% for group in image.groups %} {% for group in image.groups %}
<a href="/group/{{ group.id }}" class="tag-icon"> <a href="{{ url_for('group.group', group_id=group.id) }}" class="tag-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,88V200.89a7.11,7.11,0,0,1-7.11,7.11H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6l27.74,20.8a8,8,0,0,0,4.8,1.6H216A8,8,0,0,1,224,88Z" opacity="0.2"></path><path d="M216,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,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216Z"></path></svg> <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'] }} {{ group['name'] }}
</a> </a>
{% endfor %} {% endfor %}
<button class="tag-icon" id="#img-group">
<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>
Add
</button>
</div>
{% elif image.author_id == g.user.id %}
<div class="img-groups">
<button class="tag-icon" id="#img-group">
<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>
Add
</button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -168,19 +228,19 @@
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
{% if tag == 'Photographer' %} {% if tag == 'Photographer' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M104,40a24,24,0,1,1,24,24A24,24,0,0,1,104,40Zm108.49,99.51L167.17,88.13a24,24,0,0,0-18-8.13H106.83a24,24,0,0,0-18,8.13L43.51,139.51a12,12,0,0,0,17,17L96,128,73.13,214.93a12,12,0,0,0,21.75,10.14L128,168l33.12,57.07a12,12,0,0,0,21.75-10.14L160,128l35.51,28.49a12,12,0,0,0,17-17Z" opacity="0.2"></path><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> <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> <h2>Photographer</h2>
{% elif tag == 'Camera' %} {% elif tag == 'Camera' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,64H176L160,40H96L80,64H48A16,16,0,0,0,32,80V192a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V80A16,16,0,0,0,208,64ZM128,168a36,36,0,1,1,36-36A36,36,0,0,1,128,168Z" opacity="0.2"></path><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> <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> <h2>Camera</h2>
{% elif tag == 'Software' %} {% elif tag == 'Software' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,48V208a8,8,0,0,1-8,8H152a8,8,0,0,1-8-8V48a8,8,0,0,1,8-8h80A8,8,0,0,1,240,48Z" opacity="0.2"></path><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> <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> <h2>Software</h2>
{% elif tag == 'File' %} {% elif tag == 'File' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M104,152l48,72H24l36-56,16.36,25.45ZM152,32V88h56Z" opacity="0.2"></path><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,6.67,3.67A7.91,7.91,0,0,0,83,197.89l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M110.66,147.56a8,8,0,0,0-13.32,0L76.49,178.85l-9.76-15.18a8,8,0,0,0-13.46,0l-36,56A8,8,0,0,0,24,232H152a8,8,0,0,0,6.66-12.44ZM38.65,216,60,182.79l9.63,15a8,8,0,0,0,13.39.11l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg>
<h2>File</h2> <h2>File</h2>
{% else %} {% else %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M104,152l48,72H24l36-56,16.36,25.45ZM152,32V88h56Z" opacity="0.2"></path><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,6.67,3.67A7.91,7.91,0,0,0,83,197.89l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M110.66,147.56a8,8,0,0,0-13.32,0L76.49,178.85l-9.76-15.18a8,8,0,0,0-13.46,0l-36,56A8,8,0,0,0,24,232H152a8,8,0,0,0,6.66-12.44ZM38.65,216,60,182.79l9.63,15a8,8,0,0,0,13.39.11l21-31.47L137.05,216Zm175-133.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v96a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z"></path></svg>
<h2>{{ tag }}</h2> <h2>{{ tag }}</h2>
{% endif %} {% endif %}
<svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor"> <svg class="collapse-indicator" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -8 24 24" fill="currentColor">
@ -214,77 +274,12 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script> <script type="text/javascript">
var infoTab = document.querySelectorAll('.info-tab'); let infoTab = document.querySelectorAll('.info-tab');
for (var i = 0; i < infoTab.length; i++) { for (let i = 0; i < infoTab.length; i++) {
infoTab[i].querySelector('.info-header').addEventListener('click', function() { infoTab[i].querySelector('.info-header').addEventListener('click', function() {
this.parentNode.classList.toggle('collapsed'); this.parentNode.classList.toggle('collapsed');
}); });
} }
$('.image-fullscreen').click(function() {
// un-Stop scrolling
document.querySelector("html").style.overflow = "auto";
let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.classList.remove('active');
setTimeout(function() {
fullscreen.style.display = 'none';
}, 200);
});
$('#img-fullscreen').click(function() {
// Stop scrolling
document.querySelector("html").style.overflow = "hidden";
let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.querySelector('img').src = '/api/file/{{ image.file_name }}';
fullscreen.style.display = 'flex';
setTimeout(function() {
fullscreen.classList.add('active');
}, 10);
});
$('#img-share').click(function() {
try {
navigator.clipboard.writeText(window.location.href);
addNotification("Copied link!", 4);
} catch (err) {
addNotification("Failed to copy link! Are you on HTTP?", 2);
}
});
{% if g.user['id'] == image['author_id'] %}
$('#img-delete').click(function() {
popUpShow(
'DESTRUCTION!!!!!!',
'Do you want to delete this image along with all of its data??? This action is irreversible!',
'<button class="btn-block" onclick="popupDissmiss()">Nuuu</button>' +
'<button class="btn-block critical" onclick="deleteImage()">Dewww eeeet!</button>'
);
});
function deleteImage() {
popupDissmiss();
$.ajax({
url: '{{ url_for('api.delete_image', image_id=image['id']) }}',
type: 'post',
data: {
action: 'delete'
},
success: function (response) {
window.location.href = '/';
},
error: function (response) {
addNotification(`Image *clings*: ${response}`, 2);
}
});
}
$('#img-edit').click(function() {
window.location.href = '/image/{{ image.id }}/edit';
});
{% endif %}
</script> </script>
{% endblock %} {% endblock %}

View file

@ -1,51 +1,47 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block nav_home %}selected{% endblock %}
{% block nav_home %}navigation-item__selected{% endblock %}
{% block content %} {% block content %}
<div class="banner"> <div class="banner small">
<div class="banner-content"> <div class="banner-content">
<p>{{ config.WEBSITE.motto }}</p>
<h1>{{ config.WEBSITE.name }}</h1> <h1>{{ config.WEBSITE.name }}</h1>
{% if images|length == 0 %} {% if images|length == 0 %}
<p>Serving 0 images :<</p> <p>0 images :<</p>
{% elif images|length == 69 %} {% elif images|length == 69 %}
<p>Serving {{ images|length }} images, nice</p> <p>{{ images|length }} images, nice</p>
{% else %} {% else %}
<p>Serving {{ images|length }} images</p> <p>{{ images|length }} images</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if images %} {% if images %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% for image in images %}
<a id="image-{{ image.id }}" class="gallery-item" href="/image/{{ image.id }}" style="background-color: rgb({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }})"> <a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('gallery.image', image_id=image.id) }}" style="background-color: rgb({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }})">
<div class="image-filter"> <div class="image-filter">
<p class="image-subtitle"></p> <p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p> <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div> </div>
<img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="imgFade(this)" style="opacity:0;" id="lazy-load"/> <img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="this.classList.add('loaded');" id="lazy-load"/>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">
<h1>*crickets chirping*</h1> <h1>*crickets chirping*</h1>
<p>There are no images here yet, upload some!</p>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script> <script type="text/javascript">
if (document.referrer.includes('image')) { if (document.referrer.includes('image')) {
try { try {
let referrerId = document.referrer.split('/').pop(); let referrerId = document.referrer.split('/').pop();
let imgOffset = document.getElementById('image-' + referrerId).offsetTop; let imgOffset = document.getElementById('image-' + referrerId).offsetTop;
let imgHeight = document.getElementById('image-' + referrerId).offsetHeight; let imgHeight = document.getElementById('image-' + referrerId).offsetHeight;
let windowHeight = window.innerHeight; let windowHeight = window.innerHeight;
document.querySelector('html').style.scrollBehavior = 'auto'; document.querySelector('html').style.scrollBehavior = 'auto';
window.scrollTo(0, imgOffset + (imgHeight / 2) - (windowHeight / 2)); window.scrollTo(0, imgOffset + (imgHeight / 2) - (windowHeight / 2));
document.querySelector('html').style.scrollBehavior = 'smooth'; document.querySelector('html').style.scrollBehavior = 'smooth';

View file

@ -17,12 +17,12 @@
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<link <link
href="{{url_for('static', filename='images/logo-black.svg')}}" href="{{url_for('static', filename='logo-black.svg')}}"
rel="icon" rel="icon"
type="image/svg+xml" type="image/svg+xml"
media="(prefers-color-scheme: light)"/> media="(prefers-color-scheme: light)"/>
<link <link
href="{{url_for('static', filename='images/logo-white.svg')}}" href="{{url_for('static', filename='logo-white.svg')}}"
rel="icon" rel="icon"
type="image/svg+xml" type="image/svg+xml"
media="(prefers-color-scheme: dark)"/> media="(prefers-color-scheme: dark)"/>
@ -32,39 +32,36 @@
{% assets "js_all" %} {% assets "js_all" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endassets %}
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<div class="notifications"></div> <div class="notifications"></div>
<button class="top-of-page"> <button class="top-of-page">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M200,144H56l72-72Z" opacity="0.2"></path><path d="M133.66,66.34a8,8,0,0,0-11.32,0l-72,72A8,8,0,0,0,56,152h64v72a8,8,0,0,0,16,0V152h64a8,8,0,0,0,5.66-13.66ZM75.31,136,128,83.31,180.69,136ZM224,40a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,40Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M184,216a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,216Zm45.66-101.66-96-96a8,8,0,0,0-11.32,0l-96,96A8,8,0,0,0,32,128H72v24a8,8,0,0,0,8,8h96a8,8,0,0,0,8-8V128h40a8,8,0,0,0,5.66-13.66ZM176,176H80a8,8,0,0,0,0,16h96a8,8,0,0,0,0-16Z"></path></svg>
</button>
<button class="info-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2"></path><path d="M144,176a8,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,176Zm88-48A104,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,128ZM124,96a12,12,0,1,0-12-12A12,12,0,0,0,124,96Z"></path></svg>
</button> </button>
{% if request.path == "/" %}
<button class="info-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm-4,48a12,12,0,1,1-12,12A12,12,0,0,1,124,72Zm12,112a16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40a8,8,0,0,1,0,16Z"></path></svg>
</button>
{% endif %}
<div class="pop-up"> <div class="pop-up">
<span class="pop-up__click-off" onclick="popupDissmiss()"></span> <span class="pop-up__click-off" onclick="popupDissmiss()"></span>
<div class="pop-up-wrapper"> <div class="pop-up-wrapper">
<div class="pop-up-content"> <div class="pop-up-header"></div>
<h3>Title</h3> <div class="pop-up-controlls"></div>
<p>Very very very drawn out example description</p>
</div>
<div class="pop-up-controlls">
<button class="pop-up__btn" onclick="popupDissmiss()">Cancel</button>
</div>
</div> </div>
</div> </div>
<div class="wrapper"> <div class="wrapper">
<div class="navigation"> <div class="navigation">
<img src="{{url_for('static', filename='images/icon.png')}}" alt="Logo" class="logo" onload="imgFade(this)" style="opacity:0;"> <img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">
<a href="{{url_for('gallery.index')}}" class="navigation-item {% block nav_home %}{% endblock %}"> <a href="{{url_for('gallery.index')}}" 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,40H80a8,8,0,0,0-8,8V176a8,8,0,0,0,8,8H96.69l77.65-77.66a8,8,0,0,1,11.32,0L216,136.69V48A8,8,0,0,0,208,40Zm-88,64a16,16,0,1,1,16-16A16,16,0,0,1,120,104Z" opacity="0.2"></path><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,16h96Zm32-32H116l64-64,28,28v36Zm-88-64A24,24,0,1,0,96,88,24,24,0,0,0,120,112Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,120,80Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96ZM104,88a16,16,0,1,1,16,16A16,16,0,0,1,104,88Z"></path></svg>
<span> <span>
Home 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> <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>
@ -72,7 +69,7 @@
</a> </a>
<a href="{{url_for('group.groups')}}" class="navigation-item {% block nav_groups %}{% endblock %}"> <a href="{{url_for('group.groups')}}" class="navigation-item {% block nav_groups %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,88v24H146.42a8.07,8.07,0,0,0-4.44,1.34l-20,13.32a8.07,8.07,0,0,1-4.44,1.34H69.42A8,8,0,0,0,62,133L32,208V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6l27.74,20.8a8,8,0,0,0,4.8,1.6H200A8,8,0,0,1,208,88Z" opacity="0.2"></path><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.46V64Zm112,136H43.82l25.6-64h48.16a16,16,0,0,0,8.88-2.69l20-13.31H232Z"></path></svg> <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> <span>
Groups 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> <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>
@ -81,7 +78,7 @@
{% if g.user %} {% if g.user %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()"> <button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,80H88l40-40Z" opacity="0.2"></path><path d="M224,152v56a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V152a8,8,0,0,1,16,0v56H208V152a8,8,0,0,1,16,0ZM80.61,83.06a8,8,0,0,1,1.73-8.72l40-40a8,8,0,0,1,11.32,0l40,40A8,8,0,0,1,168,88H136v64a8,8,0,0,1-16,0V88H88A8,8,0,0,1,80.61,83.06ZM107.31,72h41.38L128,51.31Z"></path></svg> <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> <span>
Upload 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> <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>
@ -89,11 +86,11 @@
</button> </button>
{% endif %} {% endif %}
<span></span> <span class="navigation-spacer"></span>
{% if g.user %} {% if g.user %}
<a href="{{url_for('gallery.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}"> <a href="{{url_for('gallery.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="M216,176a24,24,0,1,1-24-24A24,24,0,0,1,216,176Z" opacity="0.2"></path><path d="M214.61,198.62a32,32,0,1,0-45.23,0,40,40,0,0,0-17.11,23.32,8,8,0,0,0,5.67,9.79A8.15,8.15,0,0,0,160,232a8,8,0,0,0,7.73-5.95C170.56,215.42,180.54,208,192,208s21.44,7.42,24.27,18.05a8,8,0,1,0,15.46-4.11A40,40,0,0,0,214.61,198.62ZM192,160a16,16,0,1,1-16,16A16,16,0,0,1,192,160Zm24-88H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.61A15.4,15.4,0,0,0,39.38,216h81.18a8,8,0,0,0,0-16H40V88H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72ZM92.69,56l16,16H40V56Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg>
<span> <span>
Profile 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> <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>
@ -101,7 +98,7 @@
</a> </a>
<a href="{{url_for('settings.general')}}" class="navigation-item {% block nav_settings %}{% endblock %}"> <a href="{{url_for('settings.general')}}" class="navigation-item {% block nav_settings %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M207.86,123.18l16.78-21a99.14,99.14,0,0,0-10.07-24.29l-26.7-3a81,81,0,0,0-6.81-6.81l-3-26.71a99.43,99.43,0,0,0-24.3-10l-21,16.77a81.59,81.59,0,0,0-9.64,0l-21-16.78A99.14,99.14,0,0,0,77.91,41.43l-3,26.7a81,81,0,0,0-6.81,6.81l-26.71,3a99.43,99.43,0,0,0-10,24.3l16.77,21a81.59,81.59,0,0,0,0,9.64l-16.78,21a99.14,99.14,0,0,0,10.07,24.29l26.7,3a81,81,0,0,0,6.81,6.81l3,26.71a99.43,99.43,0,0,0,24.3,10l21-16.77a81.59,81.59,0,0,0,9.64,0l21,16.78a99.14,99.14,0,0,0,24.29-10.07l3-26.7a81,81,0,0,0,6.81-6.81l26.71-3a99.43,99.43,0,0,0,10-24.3l-16.77-21A81.59,81.59,0,0,0,207.86,123.18ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z" opacity="0.2"></path><path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.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.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8.06,8.06,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8.06,8.06,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z"></path></svg> <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> <span>
Settings 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> <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>
@ -109,7 +106,7 @@
</a> </a>
{% else %} {% else %}
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()"> <button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M136,128,96,168V88Z" opacity="0.2"></path><path d="M141.66,122.34l-40-40A8,8,0,0,0,88,88v32H24a8,8,0,0,0,0,16H88v32a8,8,0,0,0,13.66,5.66l40-40A8,8,0,0,0,141.66,122.34ZM104,148.69V107.31L124.69,128ZM208,48V208a16,16,0,0,1-16,16H136a8,8,0,0,1,0-16h56V48H136a8,8,0,0,1,0-16h56A16,16,0,0,1,208,48Z"></path></svg> <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> <span>
Login 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> <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>
@ -122,14 +119,15 @@
<div class="upload-panel"> <div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span> <span class="click-off" onclick="closeUploadTab()"></span>
<div class="container"> <div class="container">
<span id="dragIndicator"></span>
<h3>Upload stuffs</h3> <h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p> <p>May the world see your stuff 👀</p>
<form id="uploadForm"> <form id="uploadForm">
<div class="fileDrop-block"> <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="M232,136v64a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V136a8,8,0,0,1,8-8H224A8,8,0,0,1,232,136Z" opacity="0.2"></path><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> <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> <span class="status">Choose or Drop file</span>
<input type="file" id="file" onchange="fileChanged(this)" onclick="this.value=null; fileChanged(this)"/> <input type="file" id="file" tab-index="-1"/>
</div> </button>
<input class="input-block" type="text" placeholder="alt" id="alt"/> <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="description" id="description"/>
@ -147,12 +145,13 @@
</div> </div>
</div> </div>
<script> <script type="text/javascript">
// Show notifications on page load
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
// Show notifications on page load addNotification('{{ message[0] }}', {{ message[1] }});
addNotification('{{ message[0] }}', '{{ message[1] }}');
{% endfor %} {% endfor %}
</script> </script>
{% block script %}{% endblock %} {% block script %}{% endblock %}
</body> </body>
</html> </html>

View file

@ -1,9 +1,9 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block nav_profile %}navigation-item__selected{% endblock %} {% block nav_profile %}selected{% endblock %}
{% block content %} {% block content %}
<div class="banner"> <div class="banner">
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/> <img src="{{ url_for('static', filename='') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span> <span></span>
<div class="banner-content"> <div class="banner-content">

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block nav_settings %}navigation-item__selected{% endblock %} {% block nav_settings %}selected{% endblock %}
{% block content %} {% block content %}
<div class="banner"> <div class="banner">
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/> <img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>

View file

@ -4,8 +4,8 @@
position: relative position: relative
background-color: $black background-color: RGB($bg-300)
color: $white color: RGB($fg-white)
overflow: hidden overflow: hidden
transition: opacity 0.3s ease-in-out transition: opacity 0.3s ease-in-out
@ -18,7 +18,7 @@
width: 100% width: 100%
height: 100% height: 100%
background-color: $black2 background-color: RGB($bg-300)
object-fit: cover object-fit: cover
object-position: center center object-position: center center
@ -31,7 +31,7 @@
width: 100% width: 100%
height: 100% height: 100%
background: linear-gradient(to right, rgba($primary, 1), rgba($primary, 0)) background: linear-gradient(to right, RGB($primary), transparent)
z-index: +1 z-index: +1
@ -57,17 +57,38 @@
font-weight: 800 font-weight: 800
text-align: left text-align: left
color: $primary color: RGB($primary)
p p
margin: 0 margin: 0
padding: 0 padding: 0
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 600
line-height: 1 line-height: 1
text-align: left text-align: left
&.small
height: 3.5rem
background-color: RGB($bg-100)
.banner-content
padding: 0.5rem
flex-direction: row
justify-content: flex-start
align-items: center
gap: 1rem
h1
padding-bottom: 0.25rem
font-size: 1.5rem
text-align: left
p
font-size: 0.9rem
text-align: left
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.banner .banner
width: 100vw width: 100vw
@ -81,9 +102,18 @@
align-items: center align-items: center
h1 h1
font-size: 3.5rem font-size: 3rem
text-align: center text-align: center
p p
font-size: 1.1rem font-size: 1.1rem
text-align: center text-align: center
&.small .banner-content
justify-content: center
h1
text-align: center
p
display: none

View file

@ -1,13 +1,12 @@
@mixin btn-block($color) @mixin btn-block($color)
background-color: rgba($color, 0) color: RGB($color)
color: $color
&:hover &:hover
background-color: rgba($color, 0.2) background-color: RGBA($color, 0.1)
color: $color color: RGB($color)
&:focus-visible &:focus-visible
outline: 2px solid rgba($color, 0.5) outline: 2px solid RGBA($color, 0.3)
.btn-block .btn-block
padding: 0.5rem 1rem padding: 0.5rem 1rem
@ -26,20 +25,19 @@
font-weight: 600 font-weight: 600
text-align: center text-align: center
background-color: rgba($white, 0) background-color: transparent
color: $white color: RGB($white)
border: none border: none
border-radius: $rad-inner border-radius: $rad-inner
cursor: pointer cursor: pointer
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
&:hover &:hover
background-color: rgba($white, 0.2) background-color: RGBA($white, 0.1)
&:focus-visible &:focus-visible
outline: 2px solid rgba($white, 0.5) outline: 2px solid RGBA($white, 0.3)
&.primary &.primary
@include btn-block($primary) @include btn-block($primary)
@ -76,24 +74,24 @@
font-weight: 600 font-weight: 600
text-align: left text-align: left
background-color: rgba($white, 0.1) background-color: RGBA($white, 0.1)
color: $white color: RGB($white)
border: none border: none
border-bottom: 3px solid rgba($white, 0.1) border-bottom: 3px solid RGBA($white, 0.1)
border-radius: $rad-inner border-radius: $rad-inner
cursor: pointer cursor: pointer
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
&:not(:focus):not([value=""]):not(:placeholder-shown) &:not(:focus):not([value=""]):not(:placeholder-shown)
border-color: rgba($white, 0.3) border-color: RGBA($white, 0.3)
&:hover &:hover
border-color: rgba($white, 0.3) border-color: RGBA($white, 0.3)
&:focus &:focus
border-color: $primary border-color: RGB($primary)
outline: none outline: none
&.black &.black
@ -117,8 +115,8 @@
font-weight: 600 font-weight: 600
text-align: center text-align: center
background-color: rgba($white, 0.1) background-color: RGBA($white, 0.1)
color: $white color: RGB($white)
border: none border: none
border-radius: $rad-inner border-radius: $rad-inner
@ -133,20 +131,23 @@
cursor: pointer cursor: pointer
&:hover &:hover
background-color: rgba($white, 0.2) background-color: RGBA($white, 0.2)
color: $white color: RGB($white)
&:focus-visible
outline: 2px solid RGBA($white, 0.3)
&.active &.active
background-color: rgba($primary, 0.2) background-color: RGBA($primary, 0.2)
color: $primary color: RGB($primary)
&.edging &.edging
background-color: rgba($white, 0.2) background-color: RGBA($white, 0.2)
color: $white color: RGB($white)
input input
display: none // So it doesnt get in the way of the drop as that breaks things display: none // So it doesnt get in the way of the drop as that breaks things
&.error &.error
background-color: rgba($critical, 0.2) background-color: RGBA($critical, 0.2)
color: $critical color: RGB($critical)

View file

@ -13,8 +13,8 @@
justify-content: center justify-content: center
align-items: center align-items: center
background-color: $black2 background-color: RGB($bg-300)
color: $white color: RGB($fg-white)
border-radius: $rad border-radius: $rad
border: none border: none
opacity: 0 opacity: 0
@ -24,7 +24,7 @@
transition: all 0.2s cubic-bezier(.86, 0, .07, 1) transition: all 0.2s cubic-bezier(.86, 0, .07, 1)
&:hover &:hover
color: $info color: RGB($info)
svg svg
margin: 0.5rem margin: 0.5rem

View file

@ -16,7 +16,7 @@
display: flex display: flex
background-color: $black background-color: RGB($bg-100)
border-radius: $rad border-radius: $rad
.pill-item .pill-item
@ -34,7 +34,7 @@
border: none border: none
background-color: transparent background-color: transparent
color: $white color: RGB($fg-white)
svg svg
width: 1.25rem width: 1.25rem
@ -43,34 +43,38 @@
&:hover &:hover
cursor: pointer cursor: pointer
color: $primary color: RGB($primary)
.tool-tip .tool-tip
opacity: 1 opacity: 1
top: -2.7rem top: -2.3rem
transform: translateX(calc(-50% + 1.25rem )) transform: translateX(calc(-50% + 1.25rem ))
.pill__critical .pill__critical
color: $critical color: RGB($critical)
span span
color: $critical background: RGB($critical)
color: RGB($fg-white)
svg
color: RGB($critical)
&:hover &:hover
color: $white color: RGB($fg-white)
.pill__info .pill__info
color: $info color: RGB($info)
span span
color: $info color: RGB($info)
&:hover &:hover
color: $white color: RGB($fg-white)
.tool-tip .tool-tip
margin: 0 margin: 0
padding: 0.5rem 0.75rem padding: 0.35rem 0.7rem
width: auto width: auto
@ -81,11 +85,11 @@
left: 0 left: 0
transform: translateX(calc(-50% + 1.25rem )) transform: translateX(calc(-50% + 1.25rem ))
font-size: 1rem font-size: 0.9rem
font-weight: 600 font-weight: 700
background-color: #000000 background-color: #000000
color: $white color: RGB($fg-white)
opacity: 0 opacity: 0
border-radius: $rad-inner border-radius: $rad-inner
@ -107,4 +111,8 @@
bottom: -0.46rem bottom: -0.46rem
transform: translateX(-50%) transform: translateX(-50%)
color: #000000 color: #000000
@media (max-width: $breakpoint)
.tool-tip
display: none

View file

@ -13,8 +13,8 @@
justify-content: center justify-content: center
align-items: center align-items: center
background-color: $black2 background-color: RGB($bg-300)
color: $white color: RGB($fg-white)
border-radius: $rad border-radius: $rad
border: none border: none
opacity: 0 opacity: 0
@ -24,7 +24,7 @@
transition: all 0.2s cubic-bezier(.86, 0, .07, 1) transition: all 0.2s cubic-bezier(.86, 0, .07, 1)
&:hover &:hover
color: $primary color: RGB($primary)
svg svg
margin: 0.5rem margin: 0.5rem

View file

@ -0,0 +1,33 @@
.label
padding: 0.4rem 0.7rem
display: block
position: absolute
font-size: 0.9rem
font-weight: 600
background-color: RGB($bg-dim)
color: RGB($fg-white)
border-radius: $rad-inner
opacity: 0
transition: opacity 0.2s cubic-bezier(.76,0,.17,1), left 0.2s cubic-bezier(.76,0,.17,1)
pointer-events: none
z-index: 999
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-dim)

View file

@ -1,8 +1,8 @@
@mixin notification($color) @mixin notification($color)
color: $color color: RGB($color)
.sniffle__notification-time .sniffle__notification-time
background-color: $color background-color: RGB($color)
.notifications .notifications
margin: 0 margin: 0
@ -33,9 +33,9 @@
position: relative position: relative
background-color: $black background-color: RGB($bg-300)
border-radius: $rad-inner border-radius: $rad-inner
color: $white color: RGB($fg-white)
opacity: 0 opacity: 0
transform: scale(0.8) transform: scale(0.8)
@ -44,6 +44,28 @@
transition: all 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55) transition: all 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
&.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 .sniffle__notification-icon
margin: 0 margin: 0
padding: 1rem padding: 1rem
@ -55,7 +77,7 @@
justify-content: center justify-content: center
align-items: center align-items: center
background-color: $black2 background-color: RGB($bg-200)
svg svg
width: 1.25rem width: 1.25rem
@ -89,27 +111,10 @@
bottom: 0px bottom: 0px
left: 0px left: 0px
background-color: $white background-color: RGB($fg-white)
animation: notificationTimeout 5.1s linear animation: notificationTimeout 5.1s linear
@each $name, $colour in (success: $success, error: $critical, warning: $warning, info: $info)
.sniffle__notification--#{$name}
@include notification($colour)
.sniffle__notification-show
opacity: 1
transform: scale(1)
.sniffle__notification--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
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.notifications .notifications
width: calc(100vw - 0.6rem) width: calc(100vw - 0.6rem)
@ -117,10 +122,10 @@
.sniffle__notification .sniffle__notification
width: 100% width: 100%
&.hide
opacity: 0
transform: translateY(-5rem)
.sniffle__notification-time .sniffle__notification-time
width: 100% width: 100%
.sniffle__notification--hide
opacity: 0
transform: translateY(-5rem)

View file

@ -7,7 +7,7 @@
display: none display: none
background-color: rgba($black, 0.8) background-color: $bg-transparent
opacity: 0 opacity: 0
z-index: 101 z-index: 101
@ -38,14 +38,14 @@
display: flex display: flex
flex-direction: column flex-direction: column
background-color: $black background-color: RGB($bg-200)
border-radius: $rad border-radius: $rad
overflow: hidden overflow: hidden
z-index: +2 z-index: +2
transition: transform 0.2s $animation-smooth transition: transform 0.2s $animation-smooth
.pop-up-content .pop-up-header
margin: 0 margin: 0
padding: 1rem padding: 1rem
@ -61,7 +61,7 @@
text-size-adjust: auto text-size-adjust: auto
text-overflow: ellipsis text-overflow: ellipsis
h3 h2, h3
margin: 0 margin: 0
width: 100% width: 100%
@ -73,7 +73,7 @@
font-weight: 800 font-weight: 800
text-align: center text-align: center
color: $white color: RGB($fg-white)
p p
margin: 0 margin: 0
@ -84,7 +84,7 @@
font-weight: 500 font-weight: 500
text-align: center text-align: center
color: $white color: RGB($fg-white)
svg svg
width: 1rem width: 1rem
@ -99,7 +99,7 @@
text-align: center text-align: center
line-height: 1 line-height: 1
color: $primary color: RGB($primary)
cursor: pointer cursor: pointer
text-decoration: none text-decoration: none
@ -133,7 +133,7 @@
.pop-up-controlls .pop-up-controlls
margin: 0 margin: 0
padding: 0.5rem padding: 0.25rem
width: 100% width: 100%
height: auto height: auto
@ -141,9 +141,9 @@
display: flex display: flex
flex-direction: row flex-direction: row
justify-content: flex-end justify-content: flex-end
gap: 0.5rem gap: 0.25rem
background-color: $black2 background-color: RGB($bg-100)
&.active &.active
opacity: 1 opacity: 1

View file

@ -2,23 +2,26 @@
margin: 0 margin: 0
padding: 0.25rem 0.5rem padding: 0.25rem 0.5rem
display: flex display: flex
align-items: center align-items: center
justify-content: center justify-content: center
gap: 0.25rem gap: 0.25rem
font-size: 1rem font-size: 0.9rem
font-weight: 500 font-weight: 500
text-decoration: none text-decoration: none
border-radius: $rad-inner border-radius: $rad-inner
background-color: $primary
color: $black
border: none border: none
background-color: RGBA($primary, 0.1)
color: RGB($primary)
cursor: pointer cursor: pointer
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
svg svg
width: 1.15rem width: 1.15rem
height: 1.15rem height: 1.15rem
&:hover
background-color: RGBA($primary, 0.3)

View file

@ -8,7 +8,7 @@
width: calc(100% - 3.5rem) width: calc(100% - 3.5rem)
height: 100vh height: 100vh
background-color: rgba($black, 0) background-color: transparent
overflow: hidden overflow: hidden
z-index: 68 z-index: 68
@ -18,10 +18,10 @@
margin: 0 margin: 0
padding: 0 padding: 0
font-size: 1.75rem font-size: 1.5rem
font-weight: 700 font-weight: 700
color: $primary color: RGB($primary)
p p
margin: 0 margin: 0
@ -30,7 +30,7 @@
font-size: 1rem font-size: 1rem
font-weight: 500 font-weight: 500
color: $white color: RGB($fg-white)
form form
margin: 0 margin: 0
@ -56,7 +56,6 @@
z-index: +1 z-index: +1
.container .container
padding: 1rem padding: 1rem
@ -71,13 +70,38 @@
flex-direction: column flex-direction: column
gap: 1rem gap: 1rem
background-color: $black background-color: RGB($bg-200)
opacity: 0 opacity: 0
z-index: +2 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), opacity 0.25s cubic-bezier(0.76, 0, 0.17, 1) transition: left 0.25s cubic-bezier(0.76, 0, 0.17, 1), bottom 0.25s cubic-bezier(0.76, 0, 0.17, 1), opacity 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 .upload-jobs
display: flex display: flex
flex-direction: column flex-direction: column
@ -98,7 +122,7 @@
align-items: center align-items: center
gap: 0.5rem gap: 0.5rem
background-color: $black2 background-color: RGB($bg-200)
border-radius: $rad border-radius: $rad
overflow: hidden overflow: hidden
@ -121,7 +145,7 @@
width: 100% width: 100%
height: 100% height: 100%
background-image: linear-gradient(to right, rgba($black, 0.8), rgba($black, 0)) background-image: linear-gradient(to right, RGB($bg-100), transparent)
.job__status .job__status
margin: 0 margin: 0
@ -134,7 +158,7 @@
font-size: 1rem font-size: 1rem
font-weight: 600 font-weight: 600
color: $white color: RGB($fg-white)
z-index: +3 z-index: +3
@ -148,7 +172,7 @@
bottom: 0 bottom: 0
left: -100% left: -100%
background-color: $primary background-color: RGB($primary)
animation: uploadingLoop 1s cubic-bezier(0.76, 0, 0.17, 1) infinite animation: uploadingLoop 1s cubic-bezier(0.76, 0, 0.17, 1) infinite
@ -157,23 +181,23 @@
&.critical &.critical
.job__status, .progress .job__status, .progress
color: $critical color: RGB($critical)
&.success &.success
.job__status .job__status
color: $success color: RGB($success)
.progress .progress
height: 0 height: 0
animation: none animation: none
&.warning &.warning
.job__status, .progress .job__status, .progress
color: $warning color: RGB($warning)
&.critical, &.success, &.warning &.critical, &.success, &.warning
.progress .progress
height: 0 height: 0
&.open &.open
background-color: rgba($black, 0.5) background-color: $bg-transparent
.container .container
left: 0 left: 0
@ -183,20 +207,24 @@
.upload-panel .upload-panel
width: 100% width: 100%
height: calc(100vh - 3.5rem) height: calc(100vh - 3.5rem)
height: calc(100dvh - 3.5rem)
left: 0 left: 0
bottom: 3.5rem bottom: 3.5rem
.container .container
width: 100% width: 100%
height: calc(100% - 10rem) height: 95%
left: 0 left: 0
bottom: calc(-100vh + 3.5rem) bottom: calc(-100vh + 3.5rem)
border-radius: $rad $rad 0 0 border-radius: $rad $rad 0 0
#dragIndicator
display: block
&.open &.open
.container .container
left: 0 left: 0
bottom: 0 bottom: 0

View file

@ -26,51 +26,40 @@
padding: 0.5rem padding: 0.5rem
width: 100% width: 100%
height: 100% height: 30%
position: absolute position: absolute
left: 0 left: 0
bottom: -1rem bottom: 0
display: flex display: flex
flex-direction: column flex-direction: column
justify-content: flex-end justify-content: flex-end
background-image: linear-gradient(to bottom, rgba($black, 0), rgba($black, 0.8)) background-image: linear-gradient(to top, rgba($bg-100, 0.5), transparent)
z-index: +1
opacity: 0 // hide opacity: 0 // hide
transform: scale(1.05) // scale up
transition: all 0.3s cubic-bezier(.79, .14, .15, .86)
.image-title z-index: +4
margin: 0 transition: background-image 0.3s cubic-bezier(.79, .14, .15, .86), opacity 0.3s cubic-bezier(.79, .14, .15, .86)
font-size: 1rem .image-title,
font-weight: 600 .image-subtitle
margin: 0
padding: 0
color: $white white-space: nowrap
text-overflow: ellipsis
overflow: hidden
text-overflow: ellipsis color: RGB($fg-white)
overflow: hidden
opacity: 0 // hide .image-title
transition: all 0.2s ease-in-out font-size: 0.9rem
font-weight: 800
.image-subtitle .image-subtitle
margin: 0 font-size: 0.8rem
font-weight: 600
font-size: 0.8rem
font-weight: 500
color: $white
text-overflow: ellipsis
overflow: hidden
opacity: 0 // hide
transition: all 0.2s ease-in-out
img img
width: 100% width: 100%
@ -81,11 +70,17 @@
object-fit: cover object-fit: cover
object-position: center object-position: center
background-color: $white
transform: scale(1.05) transform: scale(1.05)
transition: all 0.3s cubic-bezier(.79, .14, .15, .86) background-color: RGB($fg-white)
filter: blur(0.5rem)
opacity: 0
transition: all 0.2s cubic-bezier(.79, .14, .15, .86)
&.loaded
filter: blur(0)
opacity: 1
&:after &:after
content: "" content: ""
@ -94,16 +89,155 @@
&:hover &:hover
.image-filter .image-filter
bottom: 0 background-image: linear-gradient(to top, rgba($bg-100, 0.69), transparent)
opacity: 1
transform: scale(1)
.image-title,
.image-subtitle
opacity: 1 opacity: 1
img img
transform: scale(1) transform: scale(1)
.group-item
margin: 0
padding: 0
height: auto
position: relative
border-radius: $rad
box-sizing: border-box
overflow: hidden
.image-filter
margin: 0
padding: 0.5rem
width: 100%
height: 30%
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)
.image-title
font-size: 0.9rem
font-weight: 800
.image-subtitle
font-size: 0.8rem
font-weight: 600
.images
margin: 0
padding: 0
width: 100%
height: 100%
position: absolute
inset: 0
display: block
background-color: RGB($fg-white)
img
width: 100%
height: 100%
position: absolute
top: 0
left: 0
object-fit: cover
object-position: center
background-color: RGB($fg-white)
border-radius: $rad-inner
box-shadow: 0 0 0.4rem 0.25rem RGBA($bg-100, 0.25)
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%, -25%)
z-index: +1
&:after
content: ""
display: block
padding-bottom: 100%
&:hover
.images
img
box-shadow: 0 0 0.4rem 0.25rem RGBA($bg-100, 0.1)
&.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) @media (max-width: 800px)
.gallery-grid .gallery-grid

View file

@ -6,9 +6,8 @@
top: 0 top: 0
left: 0 left: 0
background-color: $white background-color: RGB($bg-300)
background-image: linear-gradient(to right, RGB($bg-400) 15%, RGB($bg-200) 35%, RGB($bg-400) 50%)
background-image: linear-gradient(to right, darken($white, 1%) 15%, darken($white, 10%) 35%, darken($white, 1%) 50%)
background-size: 1000px 640px background-size: 1000px 640px
animation: imgLoading 1.8s linear infinite forwards animation: imgLoading 1.8s linear infinite forwards
@ -24,7 +23,7 @@
width: 100% width: 100%
height: 100% height: 100%
background-color: $white background-color: RGB($fg-white)
filter: blur(1rem) filter: blur(1rem)
transform: scale(1.1) transform: scale(1.1)

View file

@ -13,7 +13,7 @@
display: none display: none
opacity: 0 // hide opacity: 0 // hide
background-color: rgba($black, 0.8) background-color: $bg-transparent
z-index: 21 z-index: 21
box-sizing: border-box box-sizing: border-box

View file

@ -4,22 +4,21 @@
display: flex display: flex
flex-direction: column flex-direction: column
gap: 0.5rem gap: 0
background-color: $black background-color: RGB($bg-200)
overflow-y: auto overflow-y: auto
.info-tab .info-tab
width: 100% width: 100%
max-height: 100%
display: flex display: flex
flex-direction: column flex-direction: column
position: relative position: relative
background-color: $black background-color: RGB($bg-200)
border-radius: $rad border-radius: $rad
transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86) transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86)
@ -42,7 +41,7 @@
top: 0.6rem top: 0.6rem
right: 0.6rem right: 0.6rem
color: $primary color: RGB($primary)
z-index: +2 z-index: +2
@ -65,7 +64,7 @@
top: 0 top: 0
z-index: +1 z-index: +1
background: $black2 background-color: RGB($bg-200)
svg svg
margin: 0 margin: 0
@ -74,16 +73,16 @@
width: 1.25rem width: 1.25rem
height: 1.25rem height: 1.25rem
fill: $primary fill: RGB($primary)
h2 h2
margin: 0 margin: 0
padding: 0 padding: 0
font-size: 1.25rem font-size: 1.1rem
font-weight: 600 font-weight: 600
color: $primary color: RGB($primary)
text-overflow: ellipsis text-overflow: ellipsis
overflow: hidden overflow: hidden
@ -96,12 +95,10 @@
flex-direction: column flex-direction: column
gap: 1rem gap: 1rem
color: $white color: RGB($fg-white)
overflow-x: hidden overflow-x: hidden
transition: opacity 0.3s cubic-bezier(.79, .14, .15, .86)
p p
margin: 0 margin: 0
padding: 0 padding: 0
@ -112,11 +109,28 @@
text-overflow: ellipsis text-overflow: ellipsis
overflow: hidden overflow: hidden
.link
margin: 0
padding: 0
font-size: 1rem
font-weight: 500
text-align: center
line-height: 1
color: RGB($primary)
cursor: pointer
text-decoration: none
&:hover
text-decoration: underline
table table
margin: 0 margin: 0
padding: 0 padding: 0
max-width: 100% width: 100%
overflow-x: hidden overflow-x: hidden
border-collapse: collapse border-collapse: collapse
@ -180,11 +194,15 @@
justify-content: center justify-content: center
align-items: center align-items: center
border-radius: 50% border-radius: $rad-inner
// border: 1px solid $white // border: 1px solid RGB($white)
.img-groups .img-groups
width: 100% width: 100%
display: flex display: flex
gap: 0.5rem flex-wrap: wrap
gap: 0.5rem
@media (max-width: 1100px)
.info-container
gap: 0.5rem

View file

@ -14,15 +14,11 @@
top: 0 top: 0
left: 0 left: 0
background-color: $black2 background-color: RGB($bg-100)
color: $white color: RGB($fg-white)
z-index: 69 z-index: 69
// Spacer
> span
height: 100%
.logo .logo
margin: 0 margin: 0
padding: 0 padding: 0
@ -35,33 +31,44 @@
flex-direction: row flex-direction: row
align-items: center align-items: center
.navigation-spacer
height: 100%
.navigation-item .navigation-item
margin: 0 margin: 0
padding: 1rem padding: 0
width: 3.5rem width: 3.5rem
height: 3.5rem height: 3.5rem
min-height: 3.5rem
display: flex
flex-direction: row
align-items: center
position: relative position: relative
background-color: $black2 display: flex
flex-direction: row
justify-content: center
align-items: center
background-color: transparent
border: none border: none
text-decoration: none text-decoration: none
> svg > svg
margin: 0 margin: 0
font-size: 1.5rem padding: 0.5rem
color: $white
transition: color 0.2s ease-out width: 2.5rem
height: 2.5rem
color: RGB($fg-white)
border-radius: $rad-inner
transition: color 0.2s ease-out, transform 0.2s ease-out
span span
margin: 0 margin: 0
padding: 0.5rem 0.75rem padding: 0.35rem 0.7rem
display: block display: block
@ -70,11 +77,11 @@
left: 3rem left: 3rem
transform: translateY(-50%) transform: translateY(-50%)
font-size: 1rem font-size: 0.9rem
font-weight: 600 font-weight: 700
background-color: #000000 background-color: #000000
color: $white color: RGB($fg-white)
opacity: 0 opacity: 0
border-radius: $rad-inner border-radius: $rad-inner
@ -99,27 +106,30 @@
color: #000000 color: #000000
&:hover &:hover
background-color: $black2
> svg > svg
color: $primary background: RGB($bg-300)
span span
opacity: 1 opacity: 1
left: 3.9rem left: 3.9rem
.navigation-item__selected &.selected
background: $primary
> svg
color: $black
&:hover
background: $primary
> svg > svg
color: $white 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: 0 $rad-inner $rad-inner 0
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
.navigation .navigation
@ -146,14 +156,17 @@
width: 3rem width: 3rem
height: 3rem height: 3rem
min-height: 3rem
border-radius: $rad-inner
svg
margin: auto
width: 1.5rem
height: 1.5rem
span span
display: none display: none
&.selected::before
top: unset
bottom: 0
left: 0
width: 100%
height: 3px
border-radius: $rad-inner

View file

@ -8,6 +8,7 @@
@import "components/elements/pop-up" @import "components/elements/pop-up"
@import "components/elements/upload-panel" @import "components/elements/upload-panel"
@import "components/elements/tags" @import "components/elements/tags"
@import "components/elements/labels"
@import "components/navigation" @import "components/navigation"
@import "components/banner" @import "components/banner"
@ -25,6 +26,15 @@
box-sizing: border-box box-sizing: border-box
font-family: $font font-family: $font
::-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 html, body
margin: 0 margin: 0
padding: 0 padding: 0
@ -32,7 +42,7 @@ html, body
min-height: 100vh min-height: 100vh
max-width: 100vw max-width: 100vw
background-color: $white background-color: RGB($fg-white)
scroll-behavior: smooth scroll-behavior: smooth
overflow-x: hidden overflow-x: hidden
@ -46,8 +56,8 @@ html, body
display: flex display: flex
flex-direction: column flex-direction: column
background-color: $white background-color: RGB($bg-bright)
color: $black color: RGB($bg-100)
.big-text .big-text
height: 20rem height: 20rem
@ -57,7 +67,7 @@ html, body
justify-content: center justify-content: center
align-items: center align-items: center
color: $black color: RGB($bg-100)
h1 h1
margin: 0 2rem margin: 0 2rem
@ -83,7 +93,7 @@ html, body
justify-content: center justify-content: center
align-items: center align-items: center
background-color: $black background-color: RGB($bg-bright)
h1 h1
margin: 0 2rem margin: 0 2rem
@ -102,10 +112,7 @@ html, body
font-weight: 400 font-weight: 400
text-align: center text-align: center
color: $white color: $fg-black
#contrast-check
transition: color 0.15s ease-in-out
@media (max-width: $breakpoint) @media (max-width: $breakpoint)
@ -126,4 +133,4 @@ html, body
p p
max-width: 100% max-width: 100%
font-size: 1rem font-size: 1rem

View file

@ -1,30 +1,80 @@
$black: #151515 $bg-transparent: rgba(var(--bg-dim), 0.8)
$black2: #101010 $bg-dim: var(--bg-dim)
$gray: #666 $bg-bright: var(--bg-bright)
$white: #E8E3E3 $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)
$red: #B66467 $fg-dim: var(--fg-dim)
$orange: #D98C5F $fg-white: var(--fg-white)
$yellow: #D9BC8C $fg-black: var(--fg-black)
$green: #8C977D
$blue: #8DA3B9
$purple: #A988B0
$primary: $green $black: var(--black)
$warning: $orange $white: var(--white)
$critical: $red $red: var(--red)
$success: $green $orange: var(--orange)
$info: $blue $yellow: var(--yellow)
$green: var(--green)
$blue: var(--blue)
$purple: var(--purple)
$rad: 6px $primary: var(--primary)
$rad-inner: 3px $warning: var(--warning)
$critical: var(--critical)
$success: var(--success)
$info: var(--info)
$animation-smooth: cubic-bezier(0.76, 0, 0.17, 1) $rad: var(--rad)
$animation-bounce: cubic-bezier(.68,-0.55,.27,1.55) $rad-inner: var(--rad-inner)
$font: "Manrope", sans-serif $animation-smooth: var(--animation-smooth)
$animation-bounce: var(--animation-bounce)
$font: 'Manrope', sans-serif
$breakpoint: 800px $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
@font-face @font-face
font-family: 'Work Sans' font-family: 'Work Sans'
src: url('fonts/worksans-regular.woff2') src: url('fonts/worksans-regular.woff2')
@ -43,5 +93,5 @@ $breakpoint: 800px
@font-face @font-face
font-family: 'Manrope' font-family: 'Manrope'
src: url('fonts/Manrope[wght].woff2') format('woff2') src: url('fonts/Manrope[wght].woff2') format('woff2')
font-style: normal; font-style: normal
font-display: swap; font-display: swap

22
gallery/utils/contrast.py Normal file
View file

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

View file

@ -0,0 +1,79 @@
"""
Tools for generating images and thumbnails
"""
import os
import platformdirs
from PIL import Image, ImageOps #, ImageFilter
from werkzeug.utils import secure_filename
CACHE_PATH = platformdirs.user_config_dir('onlylegs') + '/cache'
UPLOAD_PATH = 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)
elif len(resolution.split('x')) == 2:
res_x, res_y = resolution.split('x')
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}')

View file

@ -8,75 +8,56 @@ from datetime import datetime
import sass import sass
class CompileTheme: def compile_theme(theme_name, app_path):
""" """
Compiles the theme into the static folder Compiles the theme into the static folder
""" """
def __init__(self, theme_name, app_path): print(f"Loading '{theme_name}' theme...")
"""
Initialize the theme manager
Compiles the theme into the static folder and loads the fonts
"""
print(f"Loading '{theme_name}' theme...") # Set Paths
theme_source = os.path.join(app_path, 'themes', theme_name)
theme_destination = os.path.join(app_path, 'static', 'theme')
theme_path = os.path.join(app_path, 'themes', theme_name) # If the theme doesn't exist, exit
theme_dest = os.path.join(app_path, 'static', 'theme') if not os.path.exists(theme_source):
print("Theme does not exist!")
sys.exit(1)
if not os.path.exists(theme_path): # If the destination folder doesn't exist, create it
print("Theme does not exist!") if not os.path.exists(theme_destination):
sys.exit(1) os.makedirs(theme_destination)
if not os.path.exists(theme_dest): # Theme source file doesn't exist, exit
os.makedirs(theme_dest) if not os.path.join(theme_source, 'style.sass'):
print("No sass file found!")
self.load_sass(theme_path, theme_dest) sys.exit(1)
self.load_fonts(theme_path, theme_dest)
print(f"{datetime.now().hour}:{datetime.now().minute}:{datetime.now().second} - Done!\n")
@staticmethod
def load_sass(source_path, css_dest):
"""
Compile the sass (or scss) file into css and save it to the static folder
"""
if os.path.join(source_path, 'style.sass'):
sass_path = os.path.join(source_path, 'style.sass')
elif os.path.join(source_path, 'style.scss'):
sass_path = os.path.join(source_path, 'style.scss')
else:
print("No sass file found!")
sys.exit(1)
with open(os.path.join(css_dest, 'style.css'), encoding='utf-8', mode='w+') as file:
try:
file.write(sass.compile(filename=sass_path,output_style='compressed'))
except sass.CompileError as err:
print("Failed to compile!\n", err)
sys.exit(1)
print("Compiled successfully!")
@staticmethod
def load_fonts(source_path, font_dest):
"""
Copy the fonts folder to the static folder
"""
# Append fonts to the destination path
source_path = os.path.join(source_path, 'fonts')
font_dest = os.path.join(font_dest, 'fonts')
if os.path.exists(font_dest):
try:
shutil.rmtree(font_dest)
except Exception as err:
print("Failed to remove old fonts!\n", err)
sys.exit(1)
# Compile the theme
with open(os.path.join(theme_destination, 'style.css'),
encoding='utf-8', mode='w+') as file:
try: try:
shutil.copytree(source_path, font_dest) file.write(sass.compile(filename=os.path.join(theme_source, 'style.sass'),
output_style='compressed'))
except sass.CompileError as err:
print("Failed to compile!\n", err)
sys.exit(1)
print("Compiled successfully!")
# If the destination folder exists, remove it
if os.path.exists(os.path.join(theme_destination, 'fonts')):
try:
shutil.rmtree(os.path.join(theme_destination, 'fonts'))
except Exception as err: except Exception as err:
print("Failed to copy fonts!\n", err) print("Failed to remove old fonts!\n", err)
sys.exit(1) sys.exit(1)
# Copy the fonts
try:
shutil.copytree(os.path.join(theme_source, 'fonts'),
os.path.join(theme_destination, 'fonts'))
print("Fonts copied successfully!") print("Fonts copied successfully!")
except Exception as err:
print("Failed to copy fonts!\n", err)
sys.exit(1)
print(f"{datetime.now().hour}:{datetime.now().minute}:{datetime.now().second} - Done!\n")

28
poetry.lock generated
View file

@ -2,14 +2,14 @@
[[package]] [[package]]
name = "astroid" name = "astroid"
version = "2.15.0" version = "2.15.1"
description = "An abstract syntax tree for Python with inference support." description = "An abstract syntax tree for Python with inference support."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.2" python-versions = ">=3.7.2"
files = [ files = [
{file = "astroid-2.15.0-py3-none-any.whl", hash = "sha256:e3e4d0ffc2d15d954065579689c36aac57a339a4679a679579af6401db4d3fdb"}, {file = "astroid-2.15.1-py3-none-any.whl", hash = "sha256:89860bda98fe2bbd1f5d262229be7629d778ce280de68d95d4a73d1f592ad268"},
{file = "astroid-2.15.0.tar.gz", hash = "sha256:525f126d5dc1b8b0b6ee398b33159105615d92dc4a17f2cd064125d57f6186fa"}, {file = "astroid-2.15.1.tar.gz", hash = "sha256:af4e0aff46e2868218502789898269ed95b663fba49e65d91c1e09c966266c34"},
] ]
[package.dependencies] [package.dependencies]
@ -641,19 +641,19 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "3.1.1" version = "3.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"}, {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
{file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"}, {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]] [[package]]
name = "pylint" name = "pylint"
@ -752,14 +752,14 @@ files = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "67.6.0" version = "67.6.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"},
{file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"},
] ]
[package.extras] [package.extras]
@ -859,14 +859,14 @@ files = [
[[package]] [[package]]
name = "tomlkit" name = "tomlkit"
version = "0.11.6" version = "0.11.7"
description = "Style preserving TOML library" description = "Style preserving TOML library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.7"
files = [ files = [
{file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"},
{file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"},
] ]
[[package]] [[package]]

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "onlylegs" name = "onlylegs"
version = "23.03.23" version = "23.03.30"
description = "Gallery built for fast and simple image management" description = "Gallery built for fast and simple image management"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"] authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT" license = "MIT"
@ -29,4 +29,6 @@ build-backend = "poetry.core.masonry.api"
[tool.pylint.messages_control] [tool.pylint.messages_control]
# C0415: Flask uses it to register blueprints # C0415: Flask uses it to register blueprints
# W0718: Exception are logged so we don't need to raise them # W0718: Exception are logged so we don't need to raise them
disable = "C0415, W0718" # W0621: Flask deals with this fine, so I dont care about it lol
# R0801: Duplicate code will be dealt with later
disable = "C0415, W0718, W0621, R0801"

16
run.py
View file

@ -6,13 +6,15 @@ from setup.configuration import Configuration
print(""" print("""
___ _ _ :::::::: :::: ::: ::: ::: ::: ::: ::::::::: ::::::::: ::::::::
/ _ \\ _ __ | |_ _| | ___ __ _ ___ :+: :+: :+:+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
| | | | '_ \\| | | | | | / _ \\/ _` / __| +:+ +:+ :+:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
| |_| | | | | | |_| | |__| __/ (_| \\__ \\ +#+ +:+ +#+ +:+ +#+ +#+ +#++: +#+ +#++:++# :#: +#++:++#++
\\___/|_| |_|_|\\__, |_____\\___|\\__, |___/ +#+ +#+ +#+ +#+#+# +#+ +#+ +#+ +#+ +#+ +#+# +#+
|___/ |___/ #+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+#
Created by Fluffy Bean - Version 23.03.23 ######## ### #### ########## ### ########## ######### ######### ########
Created by Fluffy Bean - Version 23.03.30
""") """)