diff --git a/gallery/__init__.py b/gallery/__init__.py index 8657f37..f3bd732 100644 --- a/gallery/__init__.py +++ b/gallery/__init__.py @@ -11,7 +11,8 @@ import logging from flask_compress import Compress from flask_caching import Cache 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 import platformdirs @@ -59,7 +60,7 @@ def create_app(test_config=None): app.config.from_mapping(test_config) # Load theme - theme_manager.CompileTheme('default', app.root_path) + theme_manager.compile_theme('default', app.root_path) # Bundle JS files js_scripts = Bundle('js/*.js', output='gen/packed.js') @@ -68,6 +69,10 @@ def create_app(test_config=None): # Error handlers @app.errorhandler(Exception) 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 msg = err.description 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) # 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(groups.blueprint) app.register_blueprint(routing.blueprint) diff --git a/gallery/auth.py b/gallery/auth.py index 0c7cfa4..0518a60 100644 --- a/gallery/auth.py +++ b/gallery/auth.py @@ -65,15 +65,16 @@ def register(): """ Register a new user """ - username = request.form['username'] - email = request.form['email'] - password = request.form['password'] - password_repeat = request.form['password-repeat'] + # Thanks Fennec for reminding me to strip out the whitespace lol + username = request.form['username'].strip() + email = request.form['email'].strip() + password = request.form['password'].strip() + password_repeat = request.form['password-repeat'].strip() error = [] 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): error.append('Username is invalid!') @@ -116,8 +117,8 @@ def login(): """ Log in a registered user by adding the user id to the session """ - username = request.form['username'] - password = request.form['password'] + username = request.form['username'].strip() + password = request.form['password'].strip() user = db_session.query(db.Users).filter_by(username=username).first() error = [] diff --git a/gallery/db.py b/gallery/db.py index fc68d75..64b243e 100644 --- a/gallery/db.py +++ b/gallery/db.py @@ -13,6 +13,7 @@ USER_DIR = platformdirs.user_config_dir('onlylegs') 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('mysql://username:password@host:port/database_name', 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') -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 """ Group table diff --git a/gallery/routes/__init__.py b/gallery/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gallery/routes/api.py b/gallery/routes/api.py index 000042c..517838f 100644 --- a/gallery/routes/api.py +++ b/gallery/routes/api.py @@ -1,26 +1,24 @@ """ Onlylegs - API endpoints -Used internally by the frontend and possibly by other applications """ from uuid import uuid4 import os import pathlib -import io import logging from datetime import datetime as dt +import platformdirs -from flask import (Blueprint, send_from_directory, send_file, - abort, flash, jsonify, request, g, current_app) +from flask import Blueprint, send_from_directory, abort, flash, jsonify, request, g, current_app from werkzeug.utils import secure_filename from colorthief import ColorThief -from PIL import Image, ImageOps, ImageFilter from sqlalchemy.orm import sessionmaker from gallery.auth import login_required from gallery import db from gallery.utils import metadata as mt +from gallery.utils.generate_image import generate_thumbnail blueprint = Blueprint('api', __name__, url_prefix='/api') @@ -29,18 +27,13 @@ db_session = db_session() @blueprint.route('/file/', methods=['GET']) -def get_file(file_name): +def file(file_name): """ Returns a file from the uploads folder 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) - filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters # pylint: disable=W0612 - blur = request.args.get('b', default=False, type=bool) # Whether to force blur - + ext = request.args.get('e', default=None, type=str) # File extension file_name = secure_filename(file_name) # Sanitize file name # if no args are passed, return the raw file @@ -50,64 +43,12 @@ def get_file(file_name): return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name) - buff = io.BytesIO() - img = None # Image object to be set + thumb = generate_thumbnail(file_name, res, ext) - try: # Open image and set extension - 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) + if not thumb: 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 - 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) + return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb)) @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() + # Check if image exists and if user is allowed to delete it (author) if img is None: abort(404) if img.author_id != g.user.id: abort(403) + # Delete file try: os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name)) except FileNotFoundError: - # File was already deleted or doesn't exist 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: - db_session.query(db.Posts).filter_by(id=image_id).delete() + # Delete cached files + 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() - for group in groups: - db_session.delete(group) + # Delete from database + db_session.query(db.Posts).filter_by(id=image_id).delete() - db_session.commit() - except Exception as err: - logging.error('Could not remove from database: %s', err) - abort(500) + # Remove all entries in junction table + groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all() + for group in groups: + db_session.delete(group) + + # Commit all changes + db_session.commit() 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' @@ -266,40 +210,3 @@ def metadata(img_id): exif = mt.Metadata(img_path).yoink() 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) diff --git a/gallery/routes/groups.py b/gallery/routes/groups.py index 0c5f170..be09582 100644 --- a/gallery/routes/groups.py +++ b/gallery/routes/groups.py @@ -7,6 +7,7 @@ from flask import Blueprint, abort, render_template, url_for from sqlalchemy.orm import sessionmaker from gallery import db +from gallery.utils import contrast blueprint = Blueprint('group', __name__, url_prefix='/group') @@ -19,21 +20,29 @@ def 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: - thumbnail = db_session.query(db.GroupJunction.post_id)\ - .filter(db.GroupJunction.group_id == group_item.id)\ + # For each group, get the 3 most recent images + for group in groups: + 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())\ - .first() + .limit(3) - if thumbnail: - group_item.thumbnail = db_session.query(db.Posts.file_name, db.Posts.post_alt, - db.Posts.image_colours, db.Posts.id)\ - .filter(db.Posts.id == thumbnail[0])\ - .first() + # For each image, get the image data and add it to the group item + group.images = [] + for image in images: + group.images.append(db_session.query(db.Posts.file_name, db.Posts.post_alt, + 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('/') @@ -41,26 +50,39 @@ def group(group_id): """ Group view, shows all images in a group """ - group_item = db_session.query(db.Groups).filter(db.Groups.id == group_id).first() - - if group_item is None: + # Get the group, if it doesn't exist, 404 + group = db_session.query(db.Groups).filter(db.Groups.id == group_id).first() + if group is None: abort(404, 'Group not found! D:') - group_item.author_username = db_session.query(db.Users.username)\ - .filter(db.Users.id == group_item.author_id)\ + # Get the group's author username + group.author_username = db_session.query(db.Users.username)\ + .filter(db.Users.id == group.author_id)\ .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)\ .order_by(db.GroupJunction.date_added.desc())\ .all() + # Get the image data for each image in the group images = [] - for image in group_images: + for image in junction: image = db_session.query(db.Posts).filter(db.Posts.id == image[0]).first() 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('//') @@ -68,39 +90,45 @@ def group_post(group_id, image_id): """ 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() - - if img is None: + # Get the image, if it doesn't exist, 404 + image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first() + if image is None: abort(404, 'Image not found') - img.author_username = db_session.query(db.Users.username)\ - .filter(db.Users.id == img.author_id)\ + # Get the image's author username + image.author_username = db_session.query(db.Users.username)\ + .filter(db.Users.id == image.author_id)\ .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)\ .all() - img.group_list = [] - for group_item in group_list: - group_item = db_session.query(db.Groups).filter(db.Groups.id == group_item[0]).first() - img.group_list.append(group_item) + # Get the group data for each group the image is in + image.groups = [] + for group in groups: + 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)\ .filter(db.GroupJunction.group_id == group_id)\ .filter(db.GroupJunction.post_id > image_id)\ .order_by(db.GroupJunction.date_added.asc())\ .first() - prev_url = db_session.query(db.GroupJunction.post_id)\ .filter(db.GroupJunction.group_id == group_id)\ .filter(db.GroupJunction.post_id < image_id)\ .order_by(db.GroupJunction.date_added.desc())\ .first() + # If there is a next or previous image, get the URL for it if next_url is not None: next_url = url_for('group.group_post', group_id=group_id, image_id=next_url[0]) if prev_url is not None: 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) diff --git a/gallery/routes/routing.py b/gallery/routes/routing.py index cfcc915..0ff8df4 100644 --- a/gallery/routes/routing.py +++ b/gallery/routes/routing.py @@ -35,22 +35,28 @@ def image(image_id): """ Image view, shows the image and its metadata """ - img = db_session.query(db.Posts).filter(db.Posts.id == image_id).first() - - if not img: + # Get the image, if it doesn't exist, 404 + image = db_session.query(db.Posts).filter(db.Posts.id == image_id).first() + if not image: abort(404, 'Image not found :<') - img.author_username = db_session.query(db.Users.username)\ - .filter(db.Users.id == img.author_id).first()[0] + # Get the image's author username + 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)\ .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: - group = db_session.query(db.Groups).filter(db.Groups.id == group[0]).first() - img.groups.append(group) + 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 next_url = db_session.query(db.Posts.id)\ .filter(db.Posts.id > image_id)\ .order_by(db.Posts.id.asc())\ @@ -60,12 +66,13 @@ def image(image_id): .order_by(db.Posts.id.desc())\ .first() + # If there is a next or previous image, get the url if next_url: next_url = url_for('gallery.image', image_id=next_url[0]) if prev_url: 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') diff --git a/gallery/static/images/error.png b/gallery/static/error.png similarity index 100% rename from gallery/static/images/error.png rename to gallery/static/error.png diff --git a/gallery/static/images/icon.png b/gallery/static/icon.png similarity index 100% rename from gallery/static/images/icon.png rename to gallery/static/icon.png diff --git a/gallery/static/images/bg.svg b/gallery/static/images/bg.svg deleted file mode 100644 index b133190..0000000 --- a/gallery/static/images/bg.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/gallery/static/js/lable.js b/gallery/static/js/lable.js new file mode 100644 index 0000000..4188422 --- /dev/null +++ b/gallery/static/js/lable.js @@ -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); + }); + } +}); diff --git a/gallery/static/js/login.js b/gallery/static/js/login.js index 08964da..363d088 100644 --- a/gallery/static/js/login.js +++ b/gallery/static/js/login.js @@ -1,14 +1,43 @@ // Function to show login function showLogin() { + // Create elements + cancelBtn = document.createElement('button'); + cancelBtn.classList.add('btn-block'); + cancelBtn.innerHTML = 'nuuuuuuuu'; + cancelBtn.onclick = popupDissmiss; + + loginBtn = document.createElement('button'); + loginBtn.classList.add('btn-block'); + loginBtn.classList.add('primary'); + loginBtn.innerHTML = 'Login'; + loginBtn.type = 'submit'; + loginBtn.setAttribute('form', 'loginForm'); + + // Create form + loginForm = document.createElement('form'); + loginForm.id = 'loginForm'; + loginForm.setAttribute('onsubmit', 'return login(event);'); + + usernameInput = document.createElement('input'); + usernameInput.classList.add('input-block'); + usernameInput.type = 'text'; + usernameInput.placeholder = 'Namey'; + usernameInput.id = 'username'; + + passwordInput = document.createElement('input'); + passwordInput.classList.add('input-block'); + passwordInput.type = 'password'; + passwordInput.placeholder = 'Passywassy'; + passwordInput.id = 'password'; + + loginForm.appendChild(usernameInput); + loginForm.appendChild(passwordInput); + popUpShow( 'Login!', 'Need an account? Register!', - '' + - '', - '
' + - '' + - '' + - '
' + loginForm, + [cancelBtn, loginBtn] ); } // Function to login @@ -29,16 +58,13 @@ function login(event) { formData.append("username", formUsername); formData.append("password", formPassword); - $.ajax({ - url: '/auth/login', - type: 'post', - data: formData, - contentType: false, - processData: false, - success: function (response) { + fetch('/auth/login', { + method: 'POST', + body: formData + }).then(response => { + if (response.status === 200) { location.reload(); - }, - error: function (response) { + } else { switch (response.status) { case 500: addNotification('Server exploded, F\'s in chat', 2); @@ -51,25 +77,68 @@ function login(event) { break; } } + }).catch(error => { + addNotification('Error logging in, blame someone', 2); }); } // Function to show register function showRegister() { + // Create buttons + cancelBtn = document.createElement('button'); + cancelBtn.classList.add('btn-block'); + cancelBtn.innerHTML = 'nuuuuuuuu'; + cancelBtn.onclick = popupDissmiss; + + registerBtn = document.createElement('button'); + registerBtn.classList.add('btn-block'); + registerBtn.classList.add('primary'); + registerBtn.innerHTML = 'Register'; + registerBtn.type = 'submit'; + registerBtn.setAttribute('form', 'registerForm'); + + // Create form + registerForm = document.createElement('form'); + registerForm.id = 'registerForm'; + registerForm.setAttribute('onsubmit', 'return register(event);'); + + usernameInput = document.createElement('input'); + usernameInput.classList.add('input-block'); + usernameInput.type = 'text'; + usernameInput.placeholder = 'Namey'; + usernameInput.id = 'username'; + + emailInput = document.createElement('input'); + emailInput.classList.add('input-block'); + emailInput.type = 'text'; + emailInput.placeholder = 'E mail!'; + emailInput.id = 'email'; + + passwordInput = document.createElement('input'); + passwordInput.classList.add('input-block'); + passwordInput.type = 'password'; + passwordInput.placeholder = 'Passywassy'; + passwordInput.id = 'password'; + + passwordInputRepeat = document.createElement('input'); + passwordInputRepeat.classList.add('input-block'); + passwordInputRepeat.type = 'password'; + passwordInputRepeat.placeholder = 'Passywassy again!'; + passwordInputRepeat.id = 'password-repeat'; + + registerForm.appendChild(usernameInput); + registerForm.appendChild(emailInput); + registerForm.appendChild(passwordInput); + registerForm.appendChild(passwordInputRepeat); + popUpShow( 'Who are you?', 'Already have an account? Login!', - '\ - ', - '
\ - \ - \ - \ - \ -
' + registerForm, + [cancelBtn, registerBtn] ); } // Function to register -function register(obj) { +function register(event) { // AJAX takes control of subby form event.preventDefault(); @@ -90,13 +159,12 @@ function register(obj) { formData.append("password", formPassword); formData.append("password-repeat", formPasswordRepeat); - $.ajax({ - url: '/auth/register', - type: 'post', - data: formData, - contentType: false, - processData: false, - success: function (response) { + // Send form to server + fetch('/auth/login', { + method: 'POST', + body: formData + }).then(response => { + if (response.status === 200) { if (response === "gwa gwa") { addNotification('Registered successfully! Now please login to continue', 1); showLogin(); @@ -105,19 +173,20 @@ function register(obj) { addNotification(response[i], 2); } } - }, - error: function (response) { + } else { switch (response.status) { case 500: addNotification('Server exploded, F\'s in chat', 2); break; case 403: - addNotification('None but devils play past here...', 2); + addNotification('None but devils play past here... Wrong information', 2); break; default: addNotification('Error logging in, blame someone', 2); break; } } + }).catch(error => { + addNotification('Error logging in, blame someone', 2); }); } diff --git a/gallery/static/js/main.js b/gallery/static/js/main.js index 6853dfb..d974947 100644 --- a/gallery/static/js/main.js +++ b/gallery/static/js/main.js @@ -2,34 +2,6 @@ function imgFade(obj, time = 250) { $(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 function loadOnView() { let lazyLoad = document.querySelectorAll('#lazy-load'); @@ -38,7 +10,7 @@ function loadOnView() { let image = lazyLoad[i]; if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) { 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 () { 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'); for (let i = 0; i < times.length; i++) { // Remove milliseconds @@ -88,16 +52,20 @@ window.onload = function () { // Info button let infoButton = document.querySelector('.info-button'); - if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) { - infoButton.classList.remove('show'); - } else { - infoButton.classList.add('show'); - } - infoButton.onclick = function () { - popUpShow('OnlyLegs Gallery', - 'Using Phosphoricons and Manrope
' + - 'Made by Fluffy and others with ❤️
' + - 'V23.03.23'); + + if (infoButton) { + if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) { + infoButton.classList.remove('show'); + } else { + infoButton.classList.add('show'); + } + infoButton.onclick = function () { + popUpShow('OnlyLegs on Flask', + 'Using Phosphoricons and ' + + 'Manrope
' + + 'Made by Fluffy and others with ❤️
' + + 'V23.03.30'); + } } }; window.onscroll = function () { @@ -113,10 +81,13 @@ window.onscroll = function () { // Info button let infoButton = document.querySelector('.info-button'); - if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) { - infoButton.classList.remove('show'); - } else { - infoButton.classList.add('show'); + + if (infoButton) { + if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) { + infoButton.classList.remove('show'); + } else { + infoButton.classList.add('show'); + } } }; window.onresize = function () { diff --git a/gallery/static/js/notifications.js b/gallery/static/js/notifications.js index de7e3c6..80d75eb 100644 --- a/gallery/static/js/notifications.js +++ b/gallery/static/js/notifications.js @@ -1,67 +1,69 @@ -function addNotification(text='Sample notification', type=4) { - let container = document.querySelector('.notifications'); +function addNotification(notificationText, notificationLevel) { + let notificationContainer = document.querySelector('.notifications'); + + // Set the different icons for the different notification levels + let successIcon = ''; + let criticalIcon = ''; + let warningIcon = ''; + let infoIcon = ''; // Create notification element - let div = document.createElement('div'); - div.classList.add('sniffle__notification'); - div.onclick = function() { - if (div.parentNode) { - div.classList.add('sniffle__notification--hide'); + let notification = document.createElement('div'); + notification.classList.add('sniffle__notification'); + notification.onclick = function() { + if (notification) { + notification.classList.add('hide'); setTimeout(function() { - container.removeChild(div); + notificationContainer.removeChild(notification); }, 500); } }; // Create icon element and append to notification - let icon = document.createElement('span'); - icon.classList.add('sniffle__notification-icon'); - switch (type) { - case 1: - div.classList.add('sniffle__notification--success'); - icon.innerHTML = ''; - break; - case 2: - div.classList.add('sniffle__notification--error'); - icon.innerHTML = ''; - break; - case 3: - div.classList.add('sniffle__notification--warning'); - icon.innerHTML = ''; - break; - default: - div.classList.add('sniffle__notification--info'); - icon.innerHTML = ''; - break; + let iconElement = document.createElement('span'); + iconElement.classList.add('sniffle__notification-icon'); + notification.appendChild(iconElement); + // Set the icon based on the notification level, not pretty but it works :3 + if (notificationLevel == 1) { + notification.classList.add('success'); + iconElement.innerHTML = successIcon; + } else if (notificationLevel == 2) { + notification.classList.add('critical'); + iconElement.innerHTML = criticalIcon; + } else if (notificationLevel == 3) { + notification.classList.add('warning'); + iconElement.innerHTML = warningIcon; + } else { + notification.classList.add('info'); + iconElement.innerHTML = infoIcon; } - div.appendChild(icon); // Create text element and append to notification let description = document.createElement('span'); description.classList.add('sniffle__notification-text'); - description.innerHTML = text; - div.appendChild(description); + description.innerHTML = notificationText; + notification.appendChild(description); // Create span to show time remaining let timer = document.createElement('span'); timer.classList.add('sniffle__notification-time'); - div.appendChild(timer); + notification.appendChild(timer); // Append notification to container - container.appendChild(div); - setTimeout(function() { - div.classList.add('sniffle__notification-show'); - }, 100); + notificationContainer.appendChild(notification); + setTimeout(function() { notification.classList.add('show'); }, 5); // Remove notification after 5 seconds setTimeout(function() { - if (div.parentNode) { - div.classList.add('sniffle__notification--hide'); + if (notification) { + notification.classList.add('hide'); setTimeout(function() { - container.removeChild(div); + notificationContainer.removeChild(notification); }, 500); } }, 5000); -} \ No newline at end of file +} + +// uwu diff --git a/gallery/static/js/popup.js b/gallery/static/js/popup.js index e58e047..301c5b2 100644 --- a/gallery/static/js/popup.js +++ b/gallery/static/js/popup.js @@ -1,42 +1,47 @@ -function popUpShow(title, body, actions='', content='') { - // Stop scrolling - document.querySelector("html").style.overflow = "hidden"; - +function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) { // Get popup elements - let popup = document.querySelector('.pop-up'); - let popupContent = document.querySelector('.pop-up-content'); - let popupActions = document.querySelector('.pop-up-controlls'); - - // Set popup content - popupContent.innerHTML = `

${title}

${body}

${content}`; + let popupSelector = document.querySelector('.pop-up'); + let headerSelector = document.querySelector('.pop-up-header'); + let actionsSelector = document.querySelector('.pop-up-controlls'); + + // Clear popup elements + 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 - 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 = ''; + } - // Show popup - popup.style.display = 'block'; - setTimeout(function() { - popup.classList.add('active') - }, 10); + // Stop scrolling and show popup + document.querySelector("html").style.overflow = "hidden"; + popupSelector.style.display = 'block'; + setTimeout(function() { popupSelector.classList.add('active') }, 5); // 2ms delay to allow for css transition >:C } function popupDissmiss() { - // un-Stop scrolling + let popupSelector = document.querySelector('.pop-up'); + document.querySelector("html").style.overflow = "auto"; - - let popup = document.querySelector('.pop-up'); - - popup.classList.remove('active'); - - setTimeout(function() { - popup.style.display = 'none'; - }, 200); + popupSelector.classList.remove('active'); + setTimeout(function() { popupSelector.style.display = 'none'; }, 200); } - -document.addEventListener('keydown', function(event) { - if (event.key === 'Escape') { - if (document.querySelector('.pop-up').classList.contains('active')) { - popupDissmiss(); - } - } -}); \ No newline at end of file diff --git a/gallery/static/js/uploadTab.js b/gallery/static/js/uploadTab.js index 27bb9b7..3b02eed 100644 --- a/gallery/static/js/uploadTab.js +++ b/gallery/static/js/uploadTab.js @@ -1,196 +1,281 @@ -window.addEventListener("dragover",(event) => { - event.preventDefault(); -},false); -window.addEventListener("drop",(event) => { - event.preventDefault(); -},false); +// Remove default events on file drop, otherwise the browser will open the file +window.addEventListener("dragover", (event) => { + event.preventDefault(); +}, false); +window.addEventListener("drop", (event) => { + event.preventDefault(); +}, false); -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 function openUploadTab() { - // Stop scrolling + let uploadTab = document.querySelector(".upload-panel"); + + // Stop scrolling and open upload tab document.querySelector("html").style.overflow = "hidden"; - document.querySelector(".content").tabIndex = "-1"; - - // Open upload tab - const uploadTab = document.querySelector(".upload-panel"); uploadTab.style.display = "block"; - - setTimeout(function () { - uploadTab.classList.add("open"); - }, 10); + setTimeout(function () { uploadTab.classList.add("open"); }, 5); } // close upload tab 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(".content").tabIndex = ""; - - // Close upload tab - const uploadTab = document.querySelector(".upload-panel"); uploadTab.classList.remove("open"); - setTimeout(function () { uploadTab.style.display = "none"; + + uploadTabContainer.style.transform = ""; + uploadTab.dataset.lastY = 0; }, 250); } // toggle upload tab function toggleUploadTab() { - if (document.querySelector(".upload-panel").classList.contains("open")) { + let uploadTab = document.querySelector(".upload-panel"); + + if (uploadTab.classList.contains("open")) { closeUploadTab(); } else { openUploadTab(); } } + +function tabDragStart(event) { + event.preventDefault(); + + let uploadTab = document.querySelector(".upload-panel .container"); + let offset = uploadTab.getBoundingClientRect().y; + + uploadTab.classList.add("dragging"); + + document.addEventListener('touchmove', event => { + if (uploadTab.classList.contains("dragging")) { + if (event.touches[0].clientY - offset >= 0) { + uploadTab.dataset.lastY = event.touches[0].clientY; + } else { + uploadTab.dataset.lastY = offset; + } + + uploadTab.style.transform = `translateY(${uploadTab.dataset.lastY - offset}px)`; + } + }); +} +function tabDragStopped(event) { + event.preventDefault(); + + let uploadTab = document.querySelector(".upload-panel .container"); + + uploadTab.classList.remove("dragging"); + + if (uploadTab.dataset.lastY > (screen.height * 0.3)) { + closeUploadTab(); + } else { + uploadTab.style.transition = "transform 0.25s cubic-bezier(0.76, 0, 0.17, 1)"; + uploadTab.style.transform = "translateY(0px)"; + setTimeout(function () { uploadTab.style.transition = ""; }, 250); + } +} + + +// Edging the file plunge :3 +function fileActivate(event) { + event.preventDefault() + + let fileDrop = document.querySelector('.fileDrop-block'); + let fileDropTitle = fileDrop.querySelector('.status'); + + fileDrop.classList.remove('error'); + fileDrop.classList.add('edging'); + fileDropTitle.innerHTML = 'Drop to upload!'; +} +function fileDefault() { + let fileDrop = document.querySelector('.fileDrop-block'); + let fileDropTitle = fileDrop.querySelector('.status'); + + fileDrop.classList.remove('error'); + fileDrop.classList.remove('edging'); + fileDropTitle.innerHTML = 'Choose or Drop file'; +} + +function fileDropHandle(event) { + event.preventDefault() + + let fileDrop = document.querySelector('.fileDrop-block'); + let fileUpload = fileDrop.querySelector('#file'); + + fileUpload.files = event.dataTransfer.files; + + fileDefault(); + fileChanged(); +} + +function fileChanged() { + let dropBlock = document.querySelector('.fileDrop-block'); + let dropBlockStatus = dropBlock.querySelector('.status'); + let dropBlockInput = dropBlock.querySelector('#file'); + + if (dropBlockInput.value !== "") { + dropBlock.classList.add('active'); + dropBlockStatus.innerHTML = dropBlockInput.files[0].name; + } else { + fileDefault(); + } +} + +function clearUpload() { + let fileDrop = document.querySelector('#uploadForm'); + + let fileUpload = fileDrop.querySelector('#file'); + let fileAlt = fileDrop.querySelector('#alt'); + let fileDescription = fileDrop.querySelector('#description'); + let fileTags = fileDrop.querySelector('#tags'); + + fileUpload.value = ""; + fileAlt.value = ""; + fileDescription.value = ""; + fileTags.value = ""; +} + + +function createJob(file) { + jobContainer = document.createElement("div"); + jobContainer.classList.add("job"); + + jobStatus = document.createElement("span"); + jobStatus.classList.add("job__status"); + jobStatus.innerHTML = "Uploading..."; + + jobProgress = document.createElement("span"); + jobProgress.classList.add("progress"); + + jobImg = document.createElement("img"); + jobImg.src = URL.createObjectURL(file); + + jobImgFilter = document.createElement("span"); + jobImgFilter.classList.add("img-filter"); + + jobContainer.appendChild(jobStatus); + jobContainer.appendChild(jobProgress); + jobContainer.appendChild(jobImg); + jobContainer.appendChild(jobImgFilter); + + return jobContainer; +} + + +document.addEventListener('DOMContentLoaded', function() { + // 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'; + }); +}); diff --git a/gallery/static/images/logo-black.svg b/gallery/static/logo-black.svg similarity index 100% rename from gallery/static/images/logo-black.svg rename to gallery/static/logo-black.svg diff --git a/gallery/static/images/logo-white.svg b/gallery/static/logo-white.svg similarity index 100% rename from gallery/static/images/logo-white.svg rename to gallery/static/logo-white.svg diff --git a/gallery/templates/error.html b/gallery/templates/error.html index 72b570e..9d76c79 100644 --- a/gallery/templates/error.html +++ b/gallery/templates/error.html @@ -1,8 +1,7 @@ {% extends 'layout.html' %} - {% block content %}

{{error}}

{{msg}}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gallery/templates/groups/group.html b/gallery/templates/groups/group.html index 5cc2542..9556124 100644 --- a/gallery/templates/groups/group.html +++ b/gallery/templates/groups/group.html @@ -1,29 +1,40 @@ {% extends 'layout.html' %} -{% block nav_groups %}navigation-item__selected{% endblock %} -{% block content %} - - {{ image.post_alt }} + {{ image.post_alt }} {% endfor %} {% else %}
-

No image!

+

*crickets chirping*

{% if g.user %} -

You can get started by uploading an image!

+

Add some images to the group!

{% else %} -

Login to start uploading images!

+

Login to start managing this image group!

{% endif %}
{% endif %} diff --git a/gallery/templates/groups/list.html b/gallery/templates/groups/list.html index 6b5dd7f..62cb3e6 100644 --- a/gallery/templates/groups/list.html +++ b/gallery/templates/groups/list.html @@ -1,16 +1,15 @@ {% extends 'layout.html' %} -{% block nav_groups %}navigation-item__selected{% endblock %} +{% block nav_groups %}selected{% endblock %} {% block content %} -