mirror of
https://github.com/Derpy-Leggies/OnlyLegs.git
synced 2025-01-27 08:41:41 +00:00
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:
parent
a9b13f1e39
commit
828167f762
23
.github/README.md
vendored
23
.github/README.md
vendored
|
@ -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
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
BIN
.github/imageview.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
# Remove all development files
|
||||
gallery/user/logs/*
|
||||
gallery/user/uploads/*
|
||||
gallery/user/conf.yml
|
||||
gallery/user/conf.json
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
111
gallery/logger.py
Normal 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
|
|
@ -6,7 +6,6 @@ import os
|
|||
|
||||
|
||||
class metadata:
|
||||
|
||||
def yoink(filename):
|
||||
exif = metadata.getFile(filename)
|
||||
file_size = os.path.getsize(filename)
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
32
gallery/settings.py
Normal 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')
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
6
gallery/templates/settings/account.html
Normal file
6
gallery/templates/settings/account.html
Normal 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 %}
|
6
gallery/templates/settings/general.html
Normal file
6
gallery/templates/settings/general.html
Normal 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 %}
|
30
gallery/templates/settings/logs.html
Normal file
30
gallery/templates/settings/logs.html
Normal 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 %}
|
6
gallery/templates/settings/server.html
Normal file
6
gallery/templates/settings/server.html
Normal 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 %}
|
29
gallery/templates/settings/settings_layout.html
Normal file
29
gallery/templates/settings/settings_layout.html
Normal 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 %}
|
|
@ -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
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-color: $white
|
||||
|
||||
filter: blur(1rem)
|
||||
transform: scale(1.1)
|
||||
|
||||
|
|
108
gallery/user/themes/default/ui/banner.sass
Normal file
108
gallery/user/themes/default/ui/banner.sass
Normal 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
|
|
@ -1,5 +1,7 @@
|
|||
@import "wrappers/index"
|
||||
@import "wrappers/image"
|
||||
@import "wrappers/settings"
|
||||
@import "wrappers/error"
|
||||
|
||||
.content
|
||||
width: calc(100% - 3.5rem)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
35
gallery/user/themes/default/ui/wrappers/error.sass
Normal file
35
gallery/user/themes/default/ui/wrappers/error.sass
Normal 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
|
|
@ -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
|
||||
|
|
115
gallery/user/themes/default/ui/wrappers/settings.sass
Normal file
115
gallery/user/themes/default/ui/wrappers/settings.sass
Normal 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
|
|
@ -15,7 +15,7 @@ $critical: $red
|
|||
$succes: $green
|
||||
$info: $blue
|
||||
|
||||
$rad: 8px
|
||||
$rad: 6px
|
||||
$rad-inner: 3px
|
||||
|
||||
//$font: "Work Sans", sans-serif
|
||||
|
|
Loading…
Reference in a new issue