Added settings page

Added logging to a .log file
Fixed Images loosing colour and rotation on thumbnail generation
Added more info to README
This commit is contained in:
Michał 2023-03-01 23:29:34 +00:00
parent a9b13f1e39
commit 828167f762
36 changed files with 819 additions and 131 deletions

23
.github/README.md vendored
View file

@ -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)

BIN
.github/homepage.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
.github/imageview.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
# Remove all development files
gallery/user/logs/*
gallery/user/uploads/*
gallery/user/conf.yml
gallery/user/conf.json

View file

@ -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

View file

@ -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/<file>/<int:quality>', 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)
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)

View file

@ -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)

View file

@ -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)

111
gallery/logger.py Normal file
View file

@ -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

View file

@ -6,7 +6,6 @@ import os
class metadata:
def yoink(filename):
exif = metadata.getFile(filename)
file_size = os.path.getsize(filename)

View file

@ -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/<int:id>')
@ -63,10 +66,4 @@ def profile():
@blueprint.route('/profile/<int:id>')
def profile_id(id):
return render_template('profile.html', user_id=id)
@blueprint.route('/settings')
@login_required
def settings():
return render_template('settings.html')
return render_template('profile.html', user_id=id)

View file

@ -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)

View file

@ -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)

32
gallery/settings.py Normal file
View file

@ -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')

View file

@ -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);
}

View file

@ -1,12 +1,6 @@
{% extends 'layout.html' %}
{% block header %}
<div class="background-decoration">
<img src="{{ url_for('static', filename='images/background.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
</div>
{% endblock %}
{% block wrapper_class %}error-wrapper{% endblock %}
{% block content %}
<h1>{{error}}</h1>
<p>{{msg}}</p>

View file

@ -166,12 +166,12 @@
</svg>
<h2>Software</h2>
</div>
{% elif tag == 'Photo' %}
{% elif tag == 'File' %}
<div class="image-info__header">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -2 24 24" fill="currentColor">
<path d="M14 8.322V2H2v12h3.576l3.97-5.292A3 3 0 0 1 14 8.322zm0 3.753l-1.188-2.066a1 1 0 0 0-1.667-.101L8.076 14H14v-1.925zM14 16H2v2h12v-2zM2 0h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm4 9a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
</svg>
<h2>Photo</h2>
<h2>File</h2>
</div>
{% else %}
<div class="image-info__header">
@ -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() {

View file

@ -1,5 +1,18 @@
{% extends 'layout.html' %}
{% block header %}
<div class="banner">
<img src="{{ url_for('static', filename='images/leaves.jpg') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
<div class="banner__content">
{% block banner_subtitle%}{% endblock %}
<p>{{ motto }}</p>
<h1>{{ name }}</h1>
<p>Serving {{ image_count }} images</p>
</div>
</div>
{% endblock %}
{% block nav_home %}navigation-item__selected{% endblock %}
{% block wrapper_class %}index-wrapper{% endblock %}

View file

@ -51,7 +51,7 @@
<span>Profile</span>
</a>
<a href="{{url_for('gallery.settings')}}" 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" viewBox="-1 -2 24 24" width="24" fill="currentColor">
<path d="M9.815 3.094a3.467 3.467 0 0 1-2.78-1.09l-.084-.001a3.467 3.467 0 0 1-2.781 1.09 3.477 3.477 0 0 1-1.727 2.51 3.471 3.471 0 0 1 0 2.794 3.477 3.477 0 0 1 1.727 2.51 3.467 3.467 0 0 1 2.78 1.09h.084a3.467 3.467 0 0 1 2.78-1.09 3.477 3.477 0 0 1 1.727-2.51 3.471 3.471 0 0 1 0-2.794 3.477 3.477 0 0 1-1.726-2.51zM14 5.714a1.474 1.474 0 0 0 0 2.572l-.502 1.684a1.473 1.473 0 0 0-1.553 2.14l-1.443 1.122A1.473 1.473 0 0 0 8.143 14l-2.304-.006a1.473 1.473 0 0 0-2.352-.765l-1.442-1.131A1.473 1.473 0 0 0 .5 9.968L0 8.278a1.474 1.474 0 0 0 0-2.555l.5-1.69a1.473 1.473 0 0 0 1.545-2.13L3.487.77A1.473 1.473 0 0 0 5.84.005L8.143 0a1.473 1.473 0 0 0 2.358.768l1.444 1.122a1.473 1.473 0 0 0 1.553 2.14L14 5.714zm-5.812 9.198a7.943 7.943 0 0 0 2.342-.73 3.468 3.468 0 0 1-.087.215 3.477 3.477 0 0 1 1.727 2.51 3.467 3.467 0 0 1 2.78 1.09h.084a3.467 3.467 0 0 1 2.78-1.09 3.477 3.477 0 0 1 1.727-2.51 3.471 3.471 0 0 1 0-2.794 3.477 3.477 0 0 1-1.726-2.51 3.467 3.467 0 0 1-2.78-1.09h-.084l-.015.016a8.077 8.077 0 0 0 .002-2.016L16.144 6a1.473 1.473 0 0 0 2.358.768l1.444 1.122a1.473 1.473 0 0 0 1.553 2.14L22 11.714a1.474 1.474 0 0 0 0 2.572l-.502 1.684a1.473 1.473 0 0 0-1.553 2.14l-1.443 1.122a1.473 1.473 0 0 0-2.359.768l-2.304-.006a1.473 1.473 0 0 0-2.352-.765l-1.442-1.131a1.473 1.473 0 0 0-1.545-2.13l-.312-1.056zM7 10a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
</svg>
@ -73,13 +73,14 @@
</div>
<div class="pop-up">
<span class="pop-up__click-off" onclick="popupDissmiss()"></span>
<div class="pop-up-wrapper">
<div class="pop-up-content">
<h3>Title</h3>
<p>Very very very drawn out example description</p>
</div>
<div class="pop-up-controlls">
<button class="pop-up__btn" onclick="popupClose()">Cancel</button>
<button class="pop-up__btn" onclick="popupDissmiss()">Cancel</button>
</div>
</div>
</div>

View file

@ -1,14 +0,0 @@
{% extends 'layout.html' %}
{% block header %}
<div class="background-decoration">
<img src="{{ url_for('static', filename='images/background.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
</div>
{% endblock %}
{% block nav_settings %}navigation-item__selected{% endblock %}
{% block content %}
<h1>Settings</h1>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_account %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>Account</h2>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_general %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>General</h2>
{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_logs %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>Logs</h2>
<div class="settings-list" id="logs">
<div class="log" style="display:flex;flex-direction:row;gap:0.5rem;"></div>
</div>
{% endblock %}
{% block script %}
<script>
const output = document.getElementById('logs');
setInterval(function() {
$.ajax({
url: '{{ url_for('api.logfile') }}',
type: 'GET',
dataType: "json",
success: function(response) {
// for each item in response, log to console
response.forEach(function(item) {
console.log(item);
});
}
});
}, 1000); // 10 seconds
</script>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends 'settings/settings_layout.html' %}
{% block settings_server %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>Server</h2>
{% endblock %}

View file

@ -0,0 +1,29 @@
{% extends 'layout.html' %}
{% block header %}
<div class="banner">
<img src="{{ url_for('static', filename='images/leaves.jpg') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
<div class="banner__content">
{% block banner_subtitle%}{% endblock %}
<h1>Settings</h1>
<p>All the red buttons in one place, what could go wrong?</p>
</div>
</div>
{% endblock %}
{% block nav_settings %}navigation-item__selected{% endblock %}
{% block wrapper_class %}settings-wrapper{% endblock %}
{% block content %}
<div class="settings-nav">
<a href="{{ url_for('settings.general') }}" class="settings-nav__item {% block settings_general %}{% endblock %}">General</a>
<a href="{{ url_for('settings.server') }}" class="settings-nav__item {% block settings_server %}{% endblock %}">Server</a>
<a href="{{ url_for('settings.account') }}" class="settings-nav__item {% block settings_account %}{% endblock %}">Account</a>
<a href="{{ url_for('settings.logs') }}" class="settings-nav__item {% block settings_logs %}{% endblock %}">Logs</a>
</div>
<div class="settings-content">
{% block settings_content %}{% endblock %}
</div>
{% endblock %}

View file

@ -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

View file

@ -26,6 +26,8 @@
width: 100%
height: 100%
background-color: $white
filter: blur(1rem)
transform: scale(1.1)

View file

@ -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

View file

@ -1,5 +1,7 @@
@import "wrappers/index"
@import "wrappers/image"
@import "wrappers/settings"
@import "wrappers/error"
.content
width: calc(100% - 3.5rem)

View file

@ -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)

View file

@ -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)
transform: translateY(0)
.pop-up__hide
opacity: 0
transition: opacity 0.2s ease
.pop-up-wrapper
transform: translateY(5rem)
transition: transform 0.2s ease

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -15,7 +15,7 @@ $critical: $red
$succes: $green
$info: $blue
$rad: 8px
$rad: 6px
$rad-inner: 3px
//$font: "Work Sans", sans-serif

View file

@ -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=[