diff --git a/.github/README.md b/.github/README.md index 055c942..e40e19f 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,2 +1,23 @@ # onlylegs -The successor to the PHP based only legs gallery +The successor to the PHP based only legs gallery. This project is still under heavy development, not reccommended for use just yet! + +## Features + +### Currently implemented +- Easy uploading and managing of a gallery of images +- Multi user support, helping you manage a whole group of photographers +- Custom CSS support + +### Coming soon tm +- Image groups, helping you sort your favorite memories +- Password locked images/image groups, helping you share photos only to those who you want to +- Logging and automatic login attempt warnings and timeouts +- Searching through tags, file names, users (and metadata maybe, no promises) + +## screenshots + +Homescreen +![screenshot](homepage.png) + +Image view +![screenshot](imageview.png) \ No newline at end of file diff --git a/.github/homepage.png b/.github/homepage.png new file mode 100644 index 0000000..ad8e9e6 Binary files /dev/null and b/.github/homepage.png differ diff --git a/.github/imageview.png b/.github/imageview.png new file mode 100644 index 0000000..df368e8 Binary files /dev/null and b/.github/imageview.png differ diff --git a/.gitignore b/.gitignore index 82f01cf..0801037 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Remove all development files +gallery/user/logs/* gallery/user/uploads/* gallery/user/conf.yml gallery/user/conf.json diff --git a/gallery/__init__.py b/gallery/__init__.py index 1951a03..89a75ac 100644 --- a/gallery/__init__.py +++ b/gallery/__init__.py @@ -5,15 +5,18 @@ print(""" | |_| | | | | | |_| | |__| __/ (_| \\__ \\ \\___/|_| |_|_|\\__, |_____\\___|\\__, |___/ |___/ |___/ -Created by Fluffy Bean - Version 310123 +Created by Fluffy Bean - Version 23.03.01 """) from flask import Flask, render_template from flask_compress import Compress +from flask.helpers import get_root_path from dotenv import load_dotenv import yaml import os +print(f"Running at {get_root_path(__name__)}\n") + def create_app(test_config=None): # create and configure the app app = Flask(__name__) @@ -21,12 +24,12 @@ def create_app(test_config=None): # Get environment variables load_dotenv(os.path.join(app.root_path, 'user', '.env')) - print("Loaded env") - + print("Loaded environment variables") + # Get config file with open(os.path.join(app.root_path, 'user', 'conf.yml'), 'r') as f: conf = yaml.load(f, Loader=yaml.FullLoader) - print("Loaded config") + print("Loaded gallery config") # App configuration app.config.from_mapping( @@ -35,6 +38,7 @@ def create_app(test_config=None): UPLOAD_FOLDER=os.path.join(app.root_path, 'user', 'uploads'), MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'], ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'], + WEBSITE=conf['website'], ) if test_config is None: @@ -57,35 +61,39 @@ def create_app(test_config=None): # Load theme from . import sassy sassy.compile('default', app.root_path) + + # Load logger + from .logger import logger + logger.innit_logger(app) @app.errorhandler(405) def method_not_allowed(e): error = '405' - msg = 'Method sussy wussy' - return render_template('error.html', error=error, msg=msg), 404 + msg = e.description + return render_template('error.html', error=error, msg=e), 404 @app.errorhandler(404) def page_not_found(e): error = '404' - msg = 'Could not find what you need!' + msg = e.description return render_template('error.html', error=error, msg=msg), 404 @app.errorhandler(403) def forbidden(e): error = '403' - msg = 'Go away! This is no place for you!' + msg = e.description return render_template('error.html', error=error, msg=msg), 403 @app.errorhandler(410) def gone(e): error = '410' - msg = 'The page is no longer available! *sad face*' + msg = e.description return render_template('error.html', error=error, msg=msg), 410 @app.errorhandler(500) def internal_server_error(e): error = '500' - msg = 'Server died inside :c' + msg = e.description return render_template('error.html', error=error, msg=msg), 500 # Load login, registration and logout manager @@ -96,6 +104,10 @@ def create_app(test_config=None): from . import routing app.register_blueprint(routing.blueprint) app.add_url_rule('/', endpoint='index') + + # Load routes for settings + from . import settings + app.register_blueprint(settings.blueprint) # Load APIs from . import api diff --git a/gallery/api.py b/gallery/api.py index c791ed0..9b5a462 100644 --- a/gallery/api.py +++ b/gallery/api.py @@ -1,14 +1,20 @@ from flask import Blueprint, current_app, send_from_directory, send_file, request, g, abort, flash, jsonify from werkzeug.utils import secure_filename + from gallery.auth import login_required from gallery.db import get_db + from PIL import Image, ImageOps from . import metadata as mt + +from .logger import logger + +from uuid import uuid4 import io import os -from uuid import uuid4 +import time -blueprint = Blueprint('viewsbp', __name__, url_prefix='/api') +blueprint = Blueprint('api', __name__, url_prefix='/api') @blueprint.route('/uploads//', methods=['GET']) @@ -20,13 +26,18 @@ def uploads(file, quality): as_attachment=True) # Set variables - set_ext = {'jpg': 'jpeg', 'jpeg': 'jpeg', 'png': 'png', 'webp': 'webp'} + set_ext = current_app.config['ALLOWED_EXTENSIONS'] buff = io.BytesIO() # Open image and set extension - img = Image.open( - os.path.join(current_app.config['UPLOAD_FOLDER'], - secure_filename(file))) + try: + img = Image.open( + os.path.join(current_app.config['UPLOAD_FOLDER'], + secure_filename(file))) + except Exception as e: + logger.server(600, f"Error opening image: {e}") + abort(500) + img_ext = os.path.splitext(secure_filename(file))[-1].lower().replace( '.', '') img_ext = set_ext[img_ext] @@ -36,7 +47,17 @@ def uploads(file, quality): # Resize image and orientate correctly img.thumbnail((quality, quality), Image.LANCZOS) img = ImageOps.exif_transpose(img) - img.save(buff, img_ext, icc_profile=img_icc) + try: + img.save(buff, img_ext, icc_profile=img_icc) + except OSError: + # This usually happens when saving a JPEG with an ICC profile + # Convert to RGB and try again + img = img.convert('RGB') + img.save(buff, img_ext, icc_profile=img_icc) + except: + logger.server(600, f"Error resizing image: {file}") + abort(500) + img.close() # Seek to beginning of buffer and return @@ -53,11 +74,15 @@ def upload(): if not form_file: return abort(404) - img_ext = os.path.splitext(secure_filename(form_file.filename))[-1].lower() - img_name = f"GWAGWA_{uuid4().__str__()}{img_ext}" + img_ext = os.path.splitext(secure_filename(form_file.filename))[-1].replace('.', '').lower() + img_name = f"GWAGWA_{uuid4().__str__()}.{img_ext}" - if not img_ext in current_app.config['ALLOWED_EXTENSIONS']: + if not img_ext in current_app.config['ALLOWED_EXTENSIONS'].keys(): + logger.add(303, f"File extension not allowed: {img_ext}") abort(403) + + if os.path.isdir(current_app.config['UPLOAD_FOLDER']) == False: + os.mkdir(current_app.config['UPLOAD_FOLDER']) # Save to database try: @@ -66,15 +91,17 @@ def upload(): 'INSERT INTO posts (file_name, author_id, description, alt)' ' VALUES (?, ?, ?, ?)', (img_name, g.user['id'], form['description'], form['alt'])) - db.commit() except Exception as e: + logger.server(600, f"Error saving to database: {e}") abort(500) - + # Save file try: form_file.save( os.path.join(current_app.config['UPLOAD_FOLDER'], img_name)) - except: + db.commit() + except Exception as e: + logger.server(600, f"Error saving file: {e}") abort(500) return 'Gwa Gwa' @@ -97,6 +124,7 @@ def remove(id): os.path.join(current_app.config['UPLOAD_FOLDER'], img['file_name'])) except Exception as e: + logger.server(600, f"Error removing file: {e}") abort(500) try: @@ -104,8 +132,10 @@ def remove(id): db.execute('DELETE FROM posts WHERE id = ?', (id, )) db.commit() except: + logger.server(600, f"Error removing from database: {e}") abort(500) + logger.server(301, f"Removed image {id}") flash(['Image was all in Le Head!', 1]) return 'Gwa Gwa' @@ -122,4 +152,44 @@ def metadata(id): exif = mt.metadata.yoink( os.path.join(current_app.config['UPLOAD_FOLDER'], img['file_name'])) - return jsonify(exif) \ No newline at end of file + return jsonify(exif) + +@blueprint.route('/logfile') +@login_required +def logfile(): + filename = logger.filename() + log_dict = {} + i = 0 + + with open(filename) as f: + for line in f: + 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: + message_data = { + 'code': 0, + 'message': message + } + + log_dict[i] = { + 'event': event_data, + 'message': message_data + } + + i += 1 # Line number, starts at 0 + + return jsonify(log_dict) \ No newline at end of file diff --git a/gallery/auth.py b/gallery/auth.py index 5a23713..6ab9968 100644 --- a/gallery/auth.py +++ b/gallery/auth.py @@ -1,22 +1,56 @@ import functools -from flask import Blueprint, flash, g, redirect, render_template, request, session, url_for, abort, jsonify +from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify, current_app from werkzeug.security import check_password_hash, generate_password_hash + from gallery.db import get_db + +from .logger import logger + import re +import uuid blueprint = Blueprint('auth', __name__, url_prefix='/auth') +# def add_log(code, note=None): +# code = int(code) +# note = str(note) + +# user_id = session.get('user_id') +# user_ip = request.remote_addr +# db = get_db() + +# db.execute( +# 'INSERT INTO logs (ip, user_id, code, note)' +# ' VALUES (?, ?, ?, ?)', +# (user_ip, user_id, code, note) +# ) +# db.commit() + + @blueprint.before_app_request def load_logged_in_user(): user_id = session.get('user_id') + user_uuid = session.get('uuid') - if user_id is None: + if user_id is None or user_uuid is None: + # This is not needed as the user is not logged in anyway, also spams the logs + #add_log(103, 'Auth error before app request') g.user = None + session.clear() else: - g.user = get_db().execute( - 'SELECT * FROM users WHERE id = ?', (user_id,) - ).fetchone() + db = get_db() + is_alive = db.execute('SELECT * FROM devices WHERE session_uuid = ?', + (session.get('uuid'), )).fetchone() + + if is_alive is None: + logger.add(103, 'Session expired') + flash(['Session expired!', '3']) + session.clear() + else: + g.user = db.execute('SELECT * FROM users WHERE id = ?', + (user_id, )).fetchone() + @blueprint.route('/register', methods=['POST']) def register(): @@ -29,17 +63,18 @@ def register(): if not username: error.append('Username is empty!') - + if not email: error.append('Email is empty!') - elif not re.match(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email): + elif not re.match( + r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email): error.append('Email is invalid!') - + if not password: error.append('Password is empty!') elif len(password) < 8: error.append('Password is too short! Longer than 8 characters pls') - + if not password_repeat: error.append('Password repeat is empty!') elif password_repeat != password: @@ -48,15 +83,16 @@ def register(): if not error: try: db.execute( - "INSERT INTO users (username, email, password) VALUES (?, ?, ?)", + 'INSERT INTO users (username, email, password) VALUES (?, ?, ?)', (username, email, generate_password_hash(password)), ) db.commit() except db.IntegrityError: error.append(f"User {username} is already registered!") else: + logger.add(103, f"User {username} registered") return 'gwa gwa' - + return jsonify(error) @@ -66,24 +102,40 @@ def login(): password = request.form['password'] db = get_db() error = None - user = db.execute( - 'SELECT * FROM users WHERE username = ?', (username,) - ).fetchone() + user = db.execute('SELECT * FROM users WHERE username = ?', + (username, )).fetchone() if user is None: + logger.add(101, f"User {username} does not exist from {request.remote_addr}") abort(403) elif not check_password_hash(user['password'], password): + logger.add(102, f"User {username} password error from {request.remote_addr}") abort(403) - if error is None: + try: session.clear() session['user_id'] = user['id'] + session['uuid'] = str(uuid.uuid4()) + + db.execute( + 'INSERT INTO devices (user_id, session_uuid, ip) VALUES (?, ?, ?)', + (user['id'], session.get('uuid'), request.remote_addr)) + db.commit() + except error as err: + logger.add(105, f"User {username} auth error: {err}") + abort(500) + + if error is None: + logger.add(100, f"User {username} logged in from {request.remote_addr}") flash(['Logged in successfully!', '4']) return 'gwa gwa' + abort(500) + @blueprint.route('/logout') def logout(): + logger.add(103, f"User {g.user['username']} - id: {g.user['id']} logged out") session.clear() return redirect(url_for('index')) @@ -91,8 +143,10 @@ def logout(): def login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): - if g.user is None: - return redirect(url_for('auth.login')) + if g.user is None or session.get('uuid') is None: + logger.add(103, "Auth error") + session.clear() + return redirect(url_for('gallery.index')) return view(**kwargs) diff --git a/gallery/db.py b/gallery/db.py index d09bc14..b4242d1 100644 --- a/gallery/db.py +++ b/gallery/db.py @@ -1,9 +1,15 @@ import sqlite3 - import click from flask import current_app, g +@click.command('init-db') +def init_db_command(): + """Create tables if not already created""" + init_db() + click.echo('Initialized the database!') + + def get_db(): if 'db' not in g: g.db = sqlite3.connect(current_app.config['DATABASE'], @@ -27,13 +33,6 @@ def init_db(): db.executescript(f.read().decode('utf8')) -@click.command('init-db') -def init_db_command(): - """Create tables if not already created""" - init_db() - click.echo('Initialized the database!') - - def init_app(app): app.teardown_appcontext(close_db) app.cli.add_command(init_db_command) \ No newline at end of file diff --git a/gallery/logger.py b/gallery/logger.py new file mode 100644 index 0000000..2d09ea4 --- /dev/null +++ b/gallery/logger.py @@ -0,0 +1,111 @@ +import logging +import os +from datetime import datetime + +# Prevent werkzeug from logging +logging.getLogger('werkzeug').disabled = True + + +class logger: + def innit_logger(app): + filepath = os.path.join(app.root_path, 'user', 'logs') + #filename = f'onlylogs_{datetime.now().strftime("%Y%m%d")}.log' + filename = 'only.log' + + if not os.path.isdir(filepath): + os.mkdir(filepath) + + logging.basicConfig( + filename=os.path.join(filepath, filename), + level=logging.INFO, + datefmt='%Y-%m-%d %H:%M:%S', + format= + '%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s', + encoding='utf-8') + + """ + Login and Auth error codes + -------------------------- + 100: Login + 101: Login attempt + 102: Login attempt (password error) + 103: Logout + 104: Registration + 105: Auth error + + Account error codes - User actions + ---------------------------------- + 200: Account password reset + 201: Account email change + 202: Account delete + 203: Account error + + Image error codes + ----------------- + 300: Image upload + 301: Image delete + 302: Image edit + 303: Image error + + Group error codes + ----------------- + 400: Group create + 401: Group delete + 402: Group edit + 403: Group error + + User error codes - Admin actions + -------------------------------- + 500: User delete + 501: User edit + 502: User ban + 503: User unban + 504: User permission change + 505: User error + + Server and Website errors - Internal + ------------------------------------ + 600: Server error + 601: Server crash + 602: Website error + 603: Website crash + 604: Maintenance + 605: Startup + 606: Other + 621: :3 + """ + + def add(error, message): + # Allowed error codes, as listed above + log_levels = [ + 100, 101, 102, 103, 104, 105, 200, 201, 202, 203, 300, 301, 302, + 303, 400, 401, 402, 403, 500, 501, 502, 503, 504, 505 + ] + + if error in log_levels: + logging.log(logging.INFO, f'[{error}] {message}') + else: + logging.log(logging.WARN, f'[606] Improper use of error code {error}') + + def server(error, message): + log_levels = { + 600: logging.ERROR, + 601: logging.CRITICAL, + 602: logging.ERROR, + 603: logging.CRITICAL, + 604: logging.DEBUG, + 605: logging.DEBUG, + 606: logging.INFO, + 621: logging.INFO, + } + + if error in log_levels: + logging.log(log_levels[error], f'[{error}] {message}') + else: + logging.log(logging.WARN, f'[606] Invalid error code {error}') + + def filename(): + handler = logging.getLogger().handlers[0] + filename = handler.baseFilename + + return filename \ No newline at end of file diff --git a/gallery/metadata.py b/gallery/metadata.py index 64b3997..22af64e 100644 --- a/gallery/metadata.py +++ b/gallery/metadata.py @@ -6,7 +6,6 @@ import os class metadata: - def yoink(filename): exif = metadata.getFile(filename) file_size = os.path.getsize(filename) diff --git a/gallery/routing.py b/gallery/routing.py index f971c3d..adeea9d 100644 --- a/gallery/routing.py +++ b/gallery/routing.py @@ -1,4 +1,4 @@ -from flask import Blueprint, flash, g, redirect, render_template, request, url_for, jsonify, current_app +from flask import Blueprint, render_template, current_app from werkzeug.exceptions import abort from werkzeug.utils import secure_filename @@ -12,7 +12,6 @@ import os from datetime import datetime dt = datetime.now() - blueprint = Blueprint('gallery', __name__) @@ -22,7 +21,11 @@ def index(): images = db.execute('SELECT * FROM posts' ' ORDER BY created_at DESC').fetchall() - return render_template('index.html', images=images) + return render_template('index.html', + images=images, + image_count=len(images), + name=current_app.config['WEBSITE']['name'], + motto=current_app.config['WEBSITE']['motto']) @blueprint.route('/image/') @@ -63,10 +66,4 @@ def profile(): @blueprint.route('/profile/') def profile_id(id): - return render_template('profile.html', user_id=id) - - -@blueprint.route('/settings') -@login_required -def settings(): - return render_template('settings.html') \ No newline at end of file + return render_template('profile.html', user_id=id) \ No newline at end of file diff --git a/gallery/sassy.py b/gallery/sassy.py index bc5edb2..d1463c3 100644 --- a/gallery/sassy.py +++ b/gallery/sassy.py @@ -1,6 +1,6 @@ import datetime - now = datetime.datetime.now() + import sys import shutil import os @@ -8,7 +8,6 @@ import sass class compile(): - def __init__(self, theme, dir): print(f"Loading '{theme}' theme...") @@ -16,7 +15,7 @@ class compile(): font_path = os.path.join(dir, 'user', 'themes', theme, 'fonts') dest = os.path.join(dir, 'static', 'theme') - print(f"Theme path: {theme_path}") + # print(f"Theme path: {theme_path}") if os.path.exists(theme_path): if os.path.exists(os.path.join(theme_path, 'style.scss')): @@ -52,7 +51,7 @@ class compile(): dest = os.path.join(dest, 'fonts') if os.path.exists(dest): - print("Removing old fonts...") + print("Updating fonts...") try: shutil.rmtree(dest) except Exception as e: @@ -61,7 +60,8 @@ class compile(): try: shutil.copytree(source, dest) - print("Copied fonts to:", dest) + # print("Copied fonts to:", dest) + print("Copied new fonts!") except Exception as e: print("Failed to copy fonts!\n", e) sys.exit(1) \ No newline at end of file diff --git a/gallery/schema.sql b/gallery/schema.sql index b6824e8..73d44a8 100644 --- a/gallery/schema.sql +++ b/gallery/schema.sql @@ -44,8 +44,7 @@ CREATE TABLE IF NOT EXISTS permissions ( CREATE TABLE IF NOT EXISTS devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, - device_id TEXT NOT NULL, - cookie TEXT NOT NULL, + session_uuid TEXT NOT NULL, ip TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) diff --git a/gallery/settings.py b/gallery/settings.py new file mode 100644 index 0000000..8596923 --- /dev/null +++ b/gallery/settings.py @@ -0,0 +1,32 @@ +from flask import Blueprint, render_template, url_for +from werkzeug.exceptions import abort + +from gallery.auth import login_required +from gallery.db import get_db + +from datetime import datetime + + +now = datetime.now() +blueprint = Blueprint('settings', __name__, url_prefix='/settings') + + +@blueprint.route('/') +@login_required +def general(): + return render_template('settings/general.html') + +@blueprint.route('/server') +@login_required +def server(): + return render_template('settings/server.html') + +@blueprint.route('/account') +@login_required +def account(): + return render_template('settings/account.html') + +@blueprint.route('/logs') +@login_required +def logs(): + return render_template('settings/logs.html') \ No newline at end of file diff --git a/gallery/static/js/main.js b/gallery/static/js/main.js index 7e9cc9a..f8a0f99 100644 --- a/gallery/static/js/main.js +++ b/gallery/static/js/main.js @@ -8,6 +8,17 @@ document.onscroll = function() { console.log('No background decoration found'); } + try { + if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) { + document.querySelector('.banner').classList = 'banner banner-scrolled'; + } else { + document.querySelector('.banner').classList = 'banner'; + } + } + catch (e) { + console.log('No banner found'); + } + if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) { document.querySelector('.jumpUp').classList = 'jumpUp jumpUp--show'; } else { @@ -21,7 +32,7 @@ document.querySelector('.jumpUp').onclick = function() { } function imgFade(obj) { - $(obj).animate({opacity: 1}, 500); + $(obj).animate({opacity: 1}, 250); } var times = document.getElementsByClassName('time'); @@ -140,5 +151,10 @@ function popUpShow(title, body, actions, content) { function popupDissmiss() { var popup = document.querySelector('.pop-up'); - popup.classList.remove('pop-up__active'); + + popup.classList.add('pop-up__hide'); + + setTimeout(function() { + popup.classList = 'pop-up'; + }, 200); } \ No newline at end of file diff --git a/gallery/templates/error.html b/gallery/templates/error.html index 464532c..0d63473 100644 --- a/gallery/templates/error.html +++ b/gallery/templates/error.html @@ -1,12 +1,6 @@ {% extends 'layout.html' %} -{% block header %} -
- - -
-{% endblock %} - +{% block wrapper_class %}error-wrapper{% endblock %} {% block content %}

{{error}}

{{msg}}

diff --git a/gallery/templates/image.html b/gallery/templates/image.html index fd25b19..7e0af8d 100644 --- a/gallery/templates/image.html +++ b/gallery/templates/image.html @@ -166,12 +166,12 @@

Software

- {% elif tag == 'Photo' %} + {% elif tag == 'File' %}
-

Photo

+

File

{% else %}
@@ -222,7 +222,10 @@ } $('.image-fullscreen').click(function() { - $('.image-fullscreen').removeClass('image-fullscreen__active'); + $('.image-fullscreen').addClass('image-fullscreen__hide'); + setTimeout(function() { + $('.image-fullscreen').removeClass('image-fullscreen__active image-fullscreen__hide'); + }, 200); }); $('#img-fullscreen').click(function() { diff --git a/gallery/templates/index.html b/gallery/templates/index.html index 84bc33c..1900624 100644 --- a/gallery/templates/index.html +++ b/gallery/templates/index.html @@ -1,5 +1,18 @@ {% extends 'layout.html' %} +{% block header %} + +{% endblock %} {% block nav_home %}navigation-item__selected{% endblock %} {% block wrapper_class %}index-wrapper{% endblock %} diff --git a/gallery/templates/layout.html b/gallery/templates/layout.html index 2a1b0f5..5e9b575 100644 --- a/gallery/templates/layout.html +++ b/gallery/templates/layout.html @@ -51,7 +51,7 @@ Profile - + @@ -73,13 +73,14 @@
+

Title

Very very very drawn out example description

- +
diff --git a/gallery/templates/settings.html b/gallery/templates/settings.html deleted file mode 100644 index 5dddd44..0000000 --- a/gallery/templates/settings.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'layout.html' %} - -{% block header %} -
- - -
-{% endblock %} - -{% block nav_settings %}navigation-item__selected{% endblock %} - -{% block content %} -

Settings

-{% endblock %} \ No newline at end of file diff --git a/gallery/templates/settings/account.html b/gallery/templates/settings/account.html new file mode 100644 index 0000000..64d6633 --- /dev/null +++ b/gallery/templates/settings/account.html @@ -0,0 +1,6 @@ +{% extends 'settings/settings_layout.html' %} + +{% block settings_account %}settings-nav__item-selected{% endblock %} +{% block settings_content %} +

Account

+{% endblock %} \ No newline at end of file diff --git a/gallery/templates/settings/general.html b/gallery/templates/settings/general.html new file mode 100644 index 0000000..9198463 --- /dev/null +++ b/gallery/templates/settings/general.html @@ -0,0 +1,6 @@ +{% extends 'settings/settings_layout.html' %} + +{% block settings_general %}settings-nav__item-selected{% endblock %} +{% block settings_content %} +

General

+{% endblock %} \ No newline at end of file diff --git a/gallery/templates/settings/logs.html b/gallery/templates/settings/logs.html new file mode 100644 index 0000000..698a16c --- /dev/null +++ b/gallery/templates/settings/logs.html @@ -0,0 +1,30 @@ +{% extends 'settings/settings_layout.html' %} + +{% block settings_logs %}settings-nav__item-selected{% endblock %} +{% block settings_content %} +

Logs

+
+
+
+{% endblock %} + +{% block script %} + +{% endblock %} \ No newline at end of file diff --git a/gallery/templates/settings/server.html b/gallery/templates/settings/server.html new file mode 100644 index 0000000..191dbcd --- /dev/null +++ b/gallery/templates/settings/server.html @@ -0,0 +1,6 @@ +{% extends 'settings/settings_layout.html' %} + +{% block settings_server %}settings-nav__item-selected{% endblock %} +{% block settings_content %} +

Server

+{% endblock %} \ No newline at end of file diff --git a/gallery/templates/settings/settings_layout.html b/gallery/templates/settings/settings_layout.html new file mode 100644 index 0000000..c139695 --- /dev/null +++ b/gallery/templates/settings/settings_layout.html @@ -0,0 +1,29 @@ +{% extends 'layout.html' %} + +{% block header %} + +{% endblock %} +{% block nav_settings %}navigation-item__selected{% endblock %} +{% block wrapper_class %}settings-wrapper{% endblock %} + +{% block content %} +
+ +
+ {% block settings_content %}{% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/gallery/user/themes/default/style.sass b/gallery/user/themes/default/style.sass index 847fb3f..7ac1da2 100644 --- a/gallery/user/themes/default/style.sass +++ b/gallery/user/themes/default/style.sass @@ -9,6 +9,7 @@ @import "ui/navigation" @import "ui/content" @import "ui/background" +@import "ui/banner" @import "ui/gallery" @import "buttons/jumpUp" @@ -24,10 +25,12 @@ html, body padding: 0 min-height: 100vh + max-width: 100vw background-color: $white scroll-behavior: smooth + overflow-x: hidden .wrapper margin: 0 diff --git a/gallery/user/themes/default/ui/background.sass b/gallery/user/themes/default/ui/background.sass index 99ace47..e22baa7 100644 --- a/gallery/user/themes/default/ui/background.sass +++ b/gallery/user/themes/default/ui/background.sass @@ -26,6 +26,8 @@ width: 100% height: 100% + background-color: $white + filter: blur(1rem) transform: scale(1.1) diff --git a/gallery/user/themes/default/ui/banner.sass b/gallery/user/themes/default/ui/banner.sass new file mode 100644 index 0000000..c72d028 --- /dev/null +++ b/gallery/user/themes/default/ui/banner.sass @@ -0,0 +1,108 @@ +.banner + margin: 0 + padding: 0 + + width: calc(100vw - 3.5rem) + height: 40vh + + position: relative + top: 0 + left: 3.5rem + + background-color: $white + color: $black + + background-image: linear-gradient(to right, darken($white, 1%) 15%, darken($white, 10%) 35%, darken($white, 1%) 50%) + background-size: 1000px 640px + animation: imgLoading 1.8s linear infinite forwards + + overflow: hidden + transition: opacity 0.3s ease-in-out + + img + position: absolute + top: 0 + left: 0 + + width: 100% + height: 100% + + background-color: $white + + object-fit: cover + object-position: center center + + span + position: absolute + top: 0 + left: 0 + + width: 100% + height: 100% + + background: linear-gradient(to right, rgba($primary, 1), rgba($primary, 0)) + + z-index: +1 + +.banner__content + margin: 0 + padding: 1rem + + width: 100% + height: 100% + + position: relative + + display: flex + flex-direction: column + justify-content: flex-end + gap: 0.5rem + + z-index: +2 + + h1 + margin: 0 + padding: 0 + + font-size: 6.9rem + font-weight: 700 + line-height: 1 + text-align: left + + color: $black + + p + margin: 0 + padding: 0 + + font-size: 1rem + font-weight: 600 + line-height: 1 + text-align: left + + color: $black + +@media (max-width: $breakpoint) + .banner + width: 100vw + height: 25vh + + left: 0 + + span + background-image: linear-gradient(to bottom, rgba($primary, 1), rgba($primary, 0)) + + .banner__content + padding: 0.5rem + + display: flex + justify-content: center + align-items: center + + h1 + font-size: 3.5rem + text-align: center + + p + font-size: 1.1rem + text-align: center \ No newline at end of file diff --git a/gallery/user/themes/default/ui/content.sass b/gallery/user/themes/default/ui/content.sass index faf82bf..3f579a1 100644 --- a/gallery/user/themes/default/ui/content.sass +++ b/gallery/user/themes/default/ui/content.sass @@ -1,5 +1,7 @@ @import "wrappers/index" @import "wrappers/image" +@import "wrappers/settings" +@import "wrappers/error" .content width: calc(100% - 3.5rem) diff --git a/gallery/user/themes/default/ui/gallery.sass b/gallery/user/themes/default/ui/gallery.sass index cb80536..aa0ef02 100644 --- a/gallery/user/themes/default/ui/gallery.sass +++ b/gallery/user/themes/default/ui/gallery.sass @@ -37,6 +37,17 @@ display: block padding-bottom: 100% + &:hover + .gallery__item-info + opacity: 1 + transform: scale(1) + + h2, p + opacity: 1 + + .gallery__item-image + transform: scale(1.1) + .gallery__item-info margin: 0 padding: 0.5rem @@ -58,7 +69,7 @@ opacity: 0 // hide transform: scale(1.05) // scale up - transition: all 0.5s cubic-bezier(.79, .14, .15, .86) + transition: all 0.3s cubic-bezier(.79, .14, .15, .86) h2 margin: 0 @@ -90,28 +101,19 @@ opacity: 0 // hide transition: all 0.2s ease-in-out - &:hover - opacity: 1 - transform: scale(1) - - h2, p - opacity: 1 - .gallery__item-image - margin: 0 - padding: 0 + width: 100% + height: 100% - width: 100% - height: 100% + position: absolute + top: 0 + left: 0 + right: 0 + bottom: 0 - position: absolute - top: 0 - left: 0 - right: 0 - bottom: 0 + object-fit: cover + object-position: center - object-fit: cover - object-position: center + background-color: $white - //background-color: $black - border-radius: $rad + transition: all 0.3s cubic-bezier(.79, .14, .15, .86) diff --git a/gallery/user/themes/default/ui/pop-up.sass b/gallery/user/themes/default/ui/pop-up.sass index 3822451..40b3430 100644 --- a/gallery/user/themes/default/ui/pop-up.sass +++ b/gallery/user/themes/default/ui/pop-up.sass @@ -32,6 +32,17 @@ transition: opacity 0.2s ease +.pop-up__click-off + width: 100vw + height: 100vh + height: 100dvh + + position: absolute + top: 0 + left: 0 + + z-index: +1 + .pop-up-wrapper margin: 0 padding: 0.5rem @@ -54,6 +65,7 @@ overflow: hidden transition: transform 0.2s cubic-bezier(.68,-0.55,.27,1.55) + z-index: +2 .pop-up-content margin: 0 @@ -207,6 +219,14 @@ .pop-up-wrapper transform: translate(-50%, 50%) scale(1) +.pop-up__hide + opacity: 0 + transition: opacity 0.2s ease + + .pop-up-wrapper + transform: translate(-50%, 50%) scaleY(0) + transition: transform 0.2s ease + @media (max-width: $breakpoint) .pop-up width: 100% @@ -241,4 +261,12 @@ top: unset .pop-up-wrapper - transform: translateY(0) \ No newline at end of file + transform: translateY(0) + + .pop-up__hide + opacity: 0 + transition: opacity 0.2s ease + + .pop-up-wrapper + transform: translateY(5rem) + transition: transform 0.2s ease \ No newline at end of file diff --git a/gallery/user/themes/default/ui/wrappers/error.sass b/gallery/user/themes/default/ui/wrappers/error.sass new file mode 100644 index 0000000..4aca0df --- /dev/null +++ b/gallery/user/themes/default/ui/wrappers/error.sass @@ -0,0 +1,35 @@ +.error-wrapper + display: flex + flex-direction: column + justify-content: center + align-items: center + + background-color: $black + + h1 + margin: 0 2rem + + font-size: 6.9rem + font-weight: 900 + text-align: center + + color: $primary + + p + margin: 0 2rem + + max-width: 40rem + font-size: 1.25rem + font-weight: 400 + text-align: center + + color: $white + +@media (max-width: $breakpoint) + .error-wrapper + h1 + font-size: 4.5rem + + p + max-width: 100% + font-size: 1rem \ No newline at end of file diff --git a/gallery/user/themes/default/ui/wrappers/image.sass b/gallery/user/themes/default/ui/wrappers/image.sass index 8c28e0e..ef99bca 100644 --- a/gallery/user/themes/default/ui/wrappers/image.sass +++ b/gallery/user/themes/default/ui/wrappers/image.sass @@ -43,16 +43,24 @@ transform: scale(0.8) - &__active - top: 0 +.image-fullscreen__active + top: 0 - opacity: 1 // show + opacity: 1 // show - transition: opacity 0.3s cubic-bezier(.79, .14, .15, .86) + transition: opacity 0.3s cubic-bezier(.79, .14, .15, .86) - img - transform: scale(1) - transition: transform 0.2s cubic-bezier(.68,-0.55,.27,1.55) + img + transform: scale(1) + transition: transform 0.2s cubic-bezier(.68,-0.55,.27,1.55) +.image-fullscreen__hide + opacity: 0 // hide + + transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86) + + img + transform: scaleY(0) // scale(0.8) + transition: transform 0.2s ease .image-container margin: auto @@ -114,16 +122,16 @@ margin: 0 padding: 0 - width: 1.5rem - height: 1.5rem + width: 1.25rem + height: 1.25rem display: flex justify-content: center align-items: center position: absolute - top: 0.5rem - right: 0.5rem + top: 0.6rem + right: 0.6rem cursor: pointer z-index: +2 diff --git a/gallery/user/themes/default/ui/wrappers/settings.sass b/gallery/user/themes/default/ui/wrappers/settings.sass new file mode 100644 index 0000000..eea9523 --- /dev/null +++ b/gallery/user/themes/default/ui/wrappers/settings.sass @@ -0,0 +1,115 @@ +@mixin settings-btn($color, $fill: false) + @if $fill + color: $white + background-color: $color + border: 2px solid $color + + &:hover + background-color: $white + color: $color + @else + color: $color + background-color: $white + border: 2px solid $color + + &:hover + background-color: $color + color: $white + +@mixin settings-log($color) + font-size: 1rem + font-weight: 600 + + color: $white + background-color: $black + background-image: linear-gradient(120deg, rgba($color, 0.3), rgba($color, 0)); + //border-left: 3px solid $color + + +.settings-wrapper + margin: 0 + padding: 0.5rem + + display: flex + flex-direction: column + gap: 0 + +.settings-nav + width: 100% + height: auto + + position: sticky + top: 0 + left: 0 + + display: flex + flex-direction: row + justify-content: center + gap: 0.5rem + + background-color: $white + +.settings-nav__item + margin: 0 + padding: 0.5rem + + width: 100% + height: 2.5rem + + display: flex + justify-content: center + align-items: center + + font-size: 1rem + font-weight: 600 + text-align: center + line-height: 1 + text-decoration: none + + border-radius: $rad + + cursor: pointer + transition: background-color 0.2s ease, color 0.2s ease + + @include settings-btn($black) + + &:focus + outline: none + +.settings-nav__item-selected + @include settings-btn($black, true) + +.settings-list + margin: 0 + padding: 0 + + width: 100% + height: auto + + display: flex + flex-direction: column + gap: 0.5rem + +.log + margin: 0 + padding: 1rem + + height: auto + + display: flex + flex-direction: column + gap: 0.5rem + + border-radius: $rad + + @include settings-log($critical) + +@media (max-width: 450px) + .settings-nav + position: relative + + flex-direction: column + gap: 0.5rem + + .settings-wrapper + padding-bottom: 4rem \ No newline at end of file diff --git a/gallery/user/themes/default/variables.sass b/gallery/user/themes/default/variables.sass index 5bb6dbb..af43341 100644 --- a/gallery/user/themes/default/variables.sass +++ b/gallery/user/themes/default/variables.sass @@ -15,7 +15,7 @@ $critical: $red $succes: $green $info: $blue -$rad: 8px +$rad: 6px $rad-inner: 3px //$font: "Work Sans", sans-serif diff --git a/setup.py b/setup.py index 0926f9b..8796b97 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup setup( name='onlylegs', - version='310123', + version='23.03.01', packages=find_packages(), include_package_data=True, install_requires=[