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ł Gdula 2023-03-01 23:29:34 +00:00
parent a9b13f1e39
commit 828167f762
36 changed files with 819 additions and 131 deletions

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