mirror of
https://github.com/Derpy-Leggies/OnlyLegs.git
synced 2025-06-28 19:16:16 +00:00
commit
fd08fbd0c8
38 changed files with 1408 additions and 1226 deletions
|
@ -3,7 +3,6 @@ Onlylegs - API endpoints
|
|||
"""
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
|
@ -23,88 +22,13 @@ from flask_login import login_required, current_user
|
|||
from colorthief import ColorThief
|
||||
|
||||
from onlylegs.extensions import db
|
||||
from onlylegs.models import Users, Pictures, Exif
|
||||
from onlylegs.models import Pictures, Exif
|
||||
from onlylegs.utils.generate_image import generate_thumbnail
|
||||
|
||||
|
||||
blueprint = Blueprint("api", __name__, url_prefix="/api")
|
||||
|
||||
|
||||
@blueprint.route("/account/picture/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
def account_picture(user_id):
|
||||
"""
|
||||
Returns the profile of a user
|
||||
"""
|
||||
user = db.get_or_404(Users, user_id)
|
||||
file = request.files.get("file", None)
|
||||
|
||||
# If no image is uploaded, return 404 error
|
||||
if not file:
|
||||
return jsonify({"error": "No file uploaded"}), 400
|
||||
if user.id != current_user.id:
|
||||
return jsonify({"error": "You are not allowed to do this, go away"}), 403
|
||||
|
||||
# Get file extension, generate random name and set file path
|
||||
img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower()
|
||||
img_name = str(user.id)
|
||||
img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext)
|
||||
|
||||
# Check if file extension is allowed
|
||||
if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
|
||||
logging.info("File extension not allowed: %s", img_ext)
|
||||
return jsonify({"error": "File extension not allowed"}), 403
|
||||
|
||||
if user.picture:
|
||||
# Delete cached files and old image
|
||||
os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture))
|
||||
cache_name = user.picture.rsplit(".")[0]
|
||||
for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
|
||||
cache_name + "*"
|
||||
):
|
||||
os.remove(cache_file)
|
||||
|
||||
# Save file
|
||||
try:
|
||||
file.save(img_path)
|
||||
except OSError as err:
|
||||
logging.info("Error saving file %s because of %s", img_path, err)
|
||||
return jsonify({"error": "Error saving file"}), 500
|
||||
|
||||
img_colors = ColorThief(img_path).get_color()
|
||||
|
||||
# Save to database
|
||||
user.colour = img_colors
|
||||
user.picture = str(img_name + "." + img_ext)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"message": "File uploaded"}), 200
|
||||
|
||||
|
||||
@blueprint.route("/account/username/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
def account_username(user_id):
|
||||
"""
|
||||
Returns the profile of a user
|
||||
"""
|
||||
user = db.get_or_404(Users, user_id)
|
||||
new_name = request.form["name"]
|
||||
|
||||
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
|
||||
|
||||
# Validate the form
|
||||
if not new_name or not username_regex.match(new_name):
|
||||
return jsonify({"error": "Username is invalid"}), 400
|
||||
if user.id != current_user.id:
|
||||
return jsonify({"error": "You are not allowed to do this, go away"}), 403
|
||||
|
||||
# Save to database
|
||||
user.username = new_name
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"message": "Username changed"}), 200
|
||||
|
||||
|
||||
@blueprint.route("/media/<path:path>", methods=["GET"])
|
||||
def media(path):
|
||||
"""
|
||||
|
|
261
onlylegs/app.py
261
onlylegs/app.py
|
@ -1,136 +1,179 @@
|
|||
"""
|
||||
Onlylegs Gallery
|
||||
This is the main app file, it loads all the other files and sets up the app
|
||||
This is the main app file, checks on app stability and runs all da shit
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
from flask import Flask, render_template, abort, request
|
||||
from werkzeug.security import generate_password_hash
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from flask_assets import Bundle
|
||||
from flask_migrate import init as migrate_init
|
||||
|
||||
from flask import Flask, render_template, abort
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
|
||||
from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
|
||||
from onlylegs.models import Users
|
||||
from onlylegs.views import (
|
||||
index as view_index,
|
||||
image as view_image,
|
||||
group as view_group,
|
||||
settings as view_settings,
|
||||
profile as view_profile,
|
||||
from onlylegs.config import (
|
||||
INSTANCE_DIR,
|
||||
MIGRATIONS_DIR,
|
||||
APPLICATION_ROOT,
|
||||
DATABASE_NAME,
|
||||
)
|
||||
from onlylegs import api
|
||||
from onlylegs import auth as view_auth
|
||||
from onlylegs import filters
|
||||
from onlylegs.models import Users
|
||||
|
||||
from onlylegs.views.index import blueprint as view_index
|
||||
from onlylegs.views.image import blueprint as view_image
|
||||
from onlylegs.views.group import blueprint as view_group
|
||||
from onlylegs.views.settings import blueprint as view_settings
|
||||
from onlylegs.views.profile import blueprint as view_profile
|
||||
from onlylegs.api import blueprint as api
|
||||
from onlylegs.auth import blueprint as view_auth
|
||||
from onlylegs.filters import blueprint as filters
|
||||
|
||||
|
||||
def set_logger():
|
||||
file_name = os.path.join(APPLICATION_ROOT, "only.log")
|
||||
logging_level = logging.INFO
|
||||
date_format = "%Y-%m-%d %H:%M:%S"
|
||||
log_format = "%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s"
|
||||
|
||||
logging.getLogger("werkzeug").disabled = True
|
||||
logging.basicConfig(
|
||||
filename=file_name,
|
||||
level=logging_level,
|
||||
datefmt=date_format,
|
||||
format=log_format,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def create_db():
|
||||
path_to_database = os.path.join(INSTANCE_DIR, DATABASE_NAME)
|
||||
|
||||
if not os.path.exists(path_to_database):
|
||||
print("Database not found, creating...")
|
||||
|
||||
user = Users(
|
||||
username=app.config["ADMIN_CONF"]["username"],
|
||||
email=app.config["ADMIN_CONF"]["email"],
|
||||
password=generate_password_hash("changeme!", method="scrypt"),
|
||||
)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
migrate_init(directory=MIGRATIONS_DIR)
|
||||
|
||||
print(
|
||||
"####################################################",
|
||||
"# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #",
|
||||
'# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #',
|
||||
"# PLEASE RESET IT IN THE SETTINGS! #",
|
||||
"####################################################",
|
||||
sep="\n",
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
print("Database found, continuing...")
|
||||
|
||||
|
||||
def set_login_manager():
|
||||
"""
|
||||
LOGIN MANAGER
|
||||
can also set session_protection to "strong"
|
||||
this would protect against session hijacking
|
||||
"""
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "onlylegs.index"
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return Users.query.filter_by(alt_id=user_id).first()
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized():
|
||||
error = 401
|
||||
msg = "You are not authorized to view this page!!!!"
|
||||
return render_template("error.html", error=error, msg=msg), error
|
||||
|
||||
|
||||
def page_assets():
|
||||
"""
|
||||
ASSETS
|
||||
bundles all the sass and js and minifies them
|
||||
"""
|
||||
assets.init_app(app)
|
||||
|
||||
page_scripts = Bundle(
|
||||
"js/*.js", filters="jsmin", output="gen/main.js", depends="js/*.js"
|
||||
)
|
||||
page_styling = Bundle(
|
||||
"sass/style.sass",
|
||||
filters="libsass, cssmin",
|
||||
output="gen/styles.css",
|
||||
depends="sass/**/*.sass",
|
||||
)
|
||||
|
||||
assets.register("scripts", page_scripts)
|
||||
assets.register("styles", page_styling)
|
||||
|
||||
|
||||
def handle_errors():
|
||||
"""
|
||||
ERROR HANDLER
|
||||
handles all the errors and returns a nice error page
|
||||
Code errors are displayed as 500 errors so no
|
||||
sensitive information is leaked
|
||||
"""
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def error_page(err):
|
||||
if not isinstance(err, HTTPException):
|
||||
abort(500)
|
||||
|
||||
if request.method == "GET":
|
||||
return (
|
||||
render_template("error.html", error=err.code, msg=err.description),
|
||||
err.code,
|
||||
)
|
||||
else:
|
||||
return str(err.code) + ": " + err.description, err.code
|
||||
|
||||
|
||||
def register_blueprints():
|
||||
"""
|
||||
BLUEPRINTS
|
||||
registers all the blueprints
|
||||
"""
|
||||
app.register_blueprint(view_auth)
|
||||
app.register_blueprint(view_index)
|
||||
app.register_blueprint(view_image)
|
||||
app.register_blueprint(view_group)
|
||||
app.register_blueprint(view_profile)
|
||||
app.register_blueprint(view_settings)
|
||||
app.register_blueprint(api)
|
||||
app.register_blueprint(filters)
|
||||
|
||||
|
||||
app = Flask(__name__, instance_path=INSTANCE_DIR)
|
||||
app.config.from_pyfile("config.py")
|
||||
|
||||
# DATABASE
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db, directory=MIGRATIONS_DIR)
|
||||
|
||||
# If database file doesn't exist, create it
|
||||
if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
|
||||
print("Creating database")
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
create_db()
|
||||
|
||||
register_user = Users(
|
||||
username=app.config["ADMIN_CONF"]["username"],
|
||||
email=app.config["ADMIN_CONF"]["email"],
|
||||
password=generate_password_hash("changeme!", method="sha256"),
|
||||
)
|
||||
db.session.add(register_user)
|
||||
db.session.commit()
|
||||
set_logger()
|
||||
set_login_manager()
|
||||
page_assets()
|
||||
handle_errors()
|
||||
register_blueprints()
|
||||
|
||||
print(
|
||||
"""
|
||||
####################################################
|
||||
# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #
|
||||
# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED, #
|
||||
# PLEASE UPDATE IT IN THE SETTINGS! #
|
||||
####################################################
|
||||
"""
|
||||
)
|
||||
|
||||
# Check if migrations directory exists, if not create it
|
||||
with app.app_context():
|
||||
if not os.path.exists(MIGRATIONS_DIR):
|
||||
print("Creating migrations directory")
|
||||
migrate_init(directory=MIGRATIONS_DIR)
|
||||
|
||||
# LOGIN MANAGER
|
||||
# can also set session_protection to "strong"
|
||||
# this would protect against session hijacking
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "onlylegs.index"
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return Users.query.filter_by(alt_id=user_id).first()
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized():
|
||||
error = 401
|
||||
msg = "You are not authorized to view this page!!!!"
|
||||
return render_template("error.html", error=error, msg=msg), error
|
||||
|
||||
|
||||
# ERROR HANDLERS
|
||||
@app.errorhandler(Exception)
|
||||
def error_page(err):
|
||||
"""
|
||||
Error handlers, if the error is not a HTTP error, return 500
|
||||
"""
|
||||
if not isinstance(err, HTTPException):
|
||||
abort(500)
|
||||
return (
|
||||
render_template("error.html", error=err.code, msg=err.description),
|
||||
err.code,
|
||||
)
|
||||
|
||||
|
||||
# ASSETS
|
||||
assets.init_app(app)
|
||||
|
||||
scripts = Bundle(
|
||||
"js/*.js", output="gen/js.js", depends="js/*.js"
|
||||
) # filter jsmin is broken :c
|
||||
styles = Bundle(
|
||||
"sass/style.sass",
|
||||
filters="libsass, cssmin",
|
||||
output="gen/styles.css",
|
||||
depends="sass/**/*.sass",
|
||||
)
|
||||
|
||||
assets.register("scripts", scripts)
|
||||
assets.register("styles", styles)
|
||||
|
||||
# BLUEPRINTS
|
||||
app.register_blueprint(view_auth.blueprint)
|
||||
app.register_blueprint(view_index.blueprint)
|
||||
app.register_blueprint(view_image.blueprint)
|
||||
app.register_blueprint(view_group.blueprint)
|
||||
app.register_blueprint(view_profile.blueprint)
|
||||
app.register_blueprint(view_settings.blueprint)
|
||||
|
||||
app.register_blueprint(api.blueprint)
|
||||
|
||||
# FILTERS
|
||||
app.register_blueprint(filters.blueprint)
|
||||
|
||||
# CACHE AND COMPRESS
|
||||
cache.init_app(app)
|
||||
compress.init_app(app)
|
||||
|
||||
# Yupee! We got there :3
|
||||
print("Done!")
|
||||
logging.info("Gallery started successfully!")
|
||||
|
||||
|
||||
|
|
|
@ -105,5 +105,5 @@ def logout():
|
|||
Clear the current session, including the stored user id
|
||||
"""
|
||||
logout_user()
|
||||
flash(["Goodbye!!!", "4"])
|
||||
flash("Goodbye!!!", "4")
|
||||
return redirect(url_for("gallery.index"))
|
||||
|
|
|
@ -6,42 +6,43 @@ import platformdirs
|
|||
import importlib.metadata
|
||||
from dotenv import load_dotenv
|
||||
from yaml import safe_load
|
||||
from utils import startup
|
||||
|
||||
|
||||
# App Sanity Checks
|
||||
startup.check_dirs()
|
||||
startup.check_env()
|
||||
startup.check_conf()
|
||||
|
||||
|
||||
# Set dirs
|
||||
user_dir = platformdirs.user_config_dir("onlylegs")
|
||||
instance_dir = os.path.join(user_dir, "instance")
|
||||
APPLICATION_ROOT = platformdirs.user_config_dir("onlylegs")
|
||||
UPLOAD_FOLDER = os.path.join(APPLICATION_ROOT, "media", "uploads")
|
||||
MEDIA_FOLDER = os.path.join(APPLICATION_ROOT, "media")
|
||||
CACHE_FOLDER = os.path.join(APPLICATION_ROOT, "media", "cache")
|
||||
PFP_FOLDER = os.path.join(APPLICATION_ROOT, "media", "pfp")
|
||||
BANNER_FOLDER = os.path.join(APPLICATION_ROOT, "media", "banner")
|
||||
|
||||
# Load environment variables
|
||||
# print("Loading environment variables...")
|
||||
load_dotenv(os.path.join(user_dir, ".env"))
|
||||
# Load env and config files
|
||||
load_dotenv(os.path.join(APPLICATION_ROOT, ".env"))
|
||||
|
||||
# Load config from user dir
|
||||
# print("Loading config...")
|
||||
with open(os.path.join(user_dir, "conf.yml"), encoding="utf-8", mode="r") as file:
|
||||
config_file = os.path.join(APPLICATION_ROOT, "conf.yml")
|
||||
with open(config_file, encoding="utf-8", mode="r") as file:
|
||||
conf = safe_load(file)
|
||||
|
||||
|
||||
# Flask config
|
||||
SECRET_KEY = os.environ.get("FLASK_SECRET")
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///gallery.sqlite3"
|
||||
MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"]
|
||||
ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"]
|
||||
APP_VERSION = importlib.metadata.version("OnlyLegs")
|
||||
|
||||
# Database
|
||||
DATABASE_NAME = "gallery.sqlite3"
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///" + DATABASE_NAME
|
||||
INSTANCE_DIR = os.path.join(APPLICATION_ROOT, "instance")
|
||||
MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations")
|
||||
|
||||
# Pass YAML config to app
|
||||
ADMIN_CONF = conf["admin"]
|
||||
UPLOAD_CONF = conf["upload"]
|
||||
WEBSITE_CONF = conf["website"]
|
||||
|
||||
# Directories
|
||||
UPLOAD_FOLDER = os.path.join(user_dir, "media", "uploads")
|
||||
CACHE_FOLDER = os.path.join(user_dir, "media", "cache")
|
||||
PFP_FOLDER = os.path.join(user_dir, "media", "pfp")
|
||||
MEDIA_FOLDER = os.path.join(user_dir, "media")
|
||||
|
||||
# Database
|
||||
INSTANCE_DIR = instance_dir
|
||||
MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations")
|
||||
|
||||
# App
|
||||
APP_VERSION = importlib.metadata.version("OnlyLegs")
|
||||
|
|
|
@ -4,6 +4,7 @@ Custom Jinja2 filters
|
|||
"""
|
||||
from flask import Blueprint
|
||||
from onlylegs.utils import colour as colour_utils
|
||||
import colorsys
|
||||
|
||||
|
||||
blueprint = Blueprint("filters", __name__)
|
||||
|
@ -17,4 +18,32 @@ def colour_contrast(colour):
|
|||
"color: var(--fg-white);" or "color: var(--fg-black);"
|
||||
"""
|
||||
colour_obj = colour_utils.Colour(colour)
|
||||
return "rgb(var(--fg-black));" if colour_obj.is_light() else "rgb(var(--fg-white));"
|
||||
return (
|
||||
"var(--foreground-black);"
|
||||
if colour_obj.is_light()
|
||||
else "var(--foreground-white);"
|
||||
)
|
||||
|
||||
|
||||
@blueprint.app_template_filter()
|
||||
def hsl_hue(rgb):
|
||||
"""
|
||||
Pass in a rgb value and will return the hue value
|
||||
"""
|
||||
r, g, b = rgb
|
||||
r /= 255
|
||||
g /= 255
|
||||
b /= 255
|
||||
return colorsys.rgb_to_hls(r, g, b)[0] * 360
|
||||
|
||||
|
||||
@blueprint.app_template_filter()
|
||||
def hsl_saturation(rgb):
|
||||
"""
|
||||
Pass in a rgb value and will return the saturation value
|
||||
"""
|
||||
r, g, b = rgb
|
||||
r /= 255
|
||||
g /= 255
|
||||
b /= 255
|
||||
return colorsys.rgb_to_hls(r, g, b)[1] * 100
|
||||
|
|
|
@ -64,7 +64,7 @@ class Exif(db.Model):
|
|||
picture_id = db.Column(db.Integer, db.ForeignKey("pictures.id"))
|
||||
|
||||
key = db.Column(db.String, nullable=False)
|
||||
value = db.Column(db.String, nullable=False)
|
||||
value = db.Column(db.PickleType, nullable=False)
|
||||
|
||||
|
||||
class Albums(db.Model):
|
||||
|
|
|
@ -14,47 +14,6 @@ function imageFullscreen() {
|
|||
}
|
||||
|
||||
function imageShowOptionsPopup(obj) {
|
||||
// let title = 'Options';
|
||||
// let subtitle = null;
|
||||
//
|
||||
// let body = document.createElement('div');
|
||||
// body.style.cssText = 'display: flex; flex-direction: column; gap: 0.5rem;';
|
||||
//
|
||||
// let copyBtn = document.createElement('button');
|
||||
// copyBtn.classList.add('btn-block');
|
||||
// copyBtn.innerHTML = 'Copy URL';
|
||||
// copyBtn.onclick = () => {
|
||||
// copyToClipboard(window.location.href)
|
||||
// }
|
||||
//
|
||||
// let downloadBtn = document.createElement('a');
|
||||
// downloadBtn.classList.add('btn-block');
|
||||
// downloadBtn.innerHTML = 'Download';
|
||||
// downloadBtn.href = '/api/media/uploads/' + image_data["filename"];
|
||||
// downloadBtn.download = '';
|
||||
//
|
||||
// body.appendChild(copyBtn);
|
||||
// body.appendChild(downloadBtn);
|
||||
//
|
||||
// if (image_data["owner"]) {
|
||||
// let editBtn = document.createElement('button');
|
||||
// editBtn.classList.add('btn-block');
|
||||
// editBtn.classList.add('critical');
|
||||
// editBtn.innerHTML = 'Edit';
|
||||
// editBtn.onclick = imageEditPopup;
|
||||
//
|
||||
// let deleteBtn = document.createElement('button');
|
||||
// deleteBtn.classList.add('btn-block');
|
||||
// deleteBtn.classList.add('critical');
|
||||
// deleteBtn.innerHTML = 'Delete';
|
||||
// deleteBtn.onclick = imageDeletePopup;
|
||||
//
|
||||
// body.appendChild(editBtn);
|
||||
// body.appendChild(deleteBtn);
|
||||
// }
|
||||
//
|
||||
// popupShow(title, subtitle, body, [popupCancelButton]);
|
||||
|
||||
showContextMenu(obj, [
|
||||
{
|
||||
'value': 'Edit',
|
||||
|
@ -62,7 +21,8 @@ function imageShowOptionsPopup(obj) {
|
|||
dissmissContextMenu();
|
||||
imageEditPopup();
|
||||
},
|
||||
'type': 'critical'
|
||||
'type': 'critical',
|
||||
'icon': '<i class="ph-fill ph-pencil"></i>'
|
||||
},
|
||||
{
|
||||
'value': 'Delete',
|
||||
|
@ -70,7 +30,8 @@ function imageShowOptionsPopup(obj) {
|
|||
dissmissContextMenu();
|
||||
imageDeletePopup();
|
||||
},
|
||||
'type': 'critical'
|
||||
'type': 'critical',
|
||||
'icon': '<i class="ph-fill ph-trash"></i>'
|
||||
}
|
||||
], 'button')
|
||||
}
|
||||
|
|
|
@ -141,41 +141,14 @@ function clearUpload() {
|
|||
}
|
||||
|
||||
|
||||
// function createJob(file) {
|
||||
// jobContainer = document.createElement("div");
|
||||
// jobContainer.classList.add("job");
|
||||
|
||||
// jobStatus = document.createElement("span");
|
||||
// jobStatus.classList.add("job__status");
|
||||
// jobStatus.innerHTML = "Uploading...";
|
||||
|
||||
// jobProgress = document.createElement("span");
|
||||
// jobProgress.classList.add("progress");
|
||||
|
||||
// jobImg = document.createElement("img");
|
||||
// jobImg.src = URL.createObjectURL(file);
|
||||
|
||||
// jobImgFilter = document.createElement("span");
|
||||
// jobImgFilter.classList.add("img-filter");
|
||||
|
||||
// jobContainer.appendChild(jobStatus);
|
||||
// jobContainer.appendChild(jobProgress);
|
||||
// jobContainer.appendChild(jobImg);
|
||||
// jobContainer.appendChild(jobImgFilter);
|
||||
|
||||
// return jobContainer;
|
||||
// }
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Function to upload images
|
||||
const uploadTab = document.querySelector(".upload-panel");
|
||||
|
||||
if (!uploadTab) { return; } // If upload tab doesn't exist, don't run this code :3
|
||||
if (!uploadTab) { return }
|
||||
|
||||
const uploadTabDrag = uploadTab.querySelector("#dragIndicator");
|
||||
const uploadForm = uploadTab.querySelector('#uploadForm');
|
||||
// let jobList = document.querySelector(".upload-jobs");
|
||||
|
||||
const fileDrop = uploadForm.querySelector('.fileDrop-block');
|
||||
const fileDropTitle = fileDrop.querySelector('.status');
|
||||
|
@ -228,54 +201,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
formData.append("description", fileDescription.value);
|
||||
formData.append("tags", fileTags.value);
|
||||
|
||||
// jobItem = createJob(fileUpload.files[0]);
|
||||
// jobStatus = jobItem.querySelector(".job__status");
|
||||
|
||||
// Upload the information
|
||||
// $.ajax({
|
||||
// url: '/api/upload',
|
||||
// type: 'post',
|
||||
// data: formData,
|
||||
// contentType: false,
|
||||
// processData: false,
|
||||
// beforeSend: function () {
|
||||
// // Add job to list
|
||||
// jobList.appendChild(jobItem);
|
||||
// },
|
||||
// success: function (response) {
|
||||
// jobItem.classList.add("success");
|
||||
// jobStatus.innerHTML = "Uploaded successfully";
|
||||
// if (!document.querySelector(".upload-panel").classList.contains("open")) {
|
||||
// addNotification("Image uploaded successfully", 1);
|
||||
// }
|
||||
// },
|
||||
// error: function (response) {
|
||||
// jobItem.classList.add("critical");
|
||||
// switch (response.status) {
|
||||
// case 500:
|
||||
// jobStatus.innerHTML = "Server exploded, F's in chat";
|
||||
// break;
|
||||
// case 400:
|
||||
// case 404:
|
||||
// jobStatus.innerHTML = "Error uploading. Blame yourself";
|
||||
// break;
|
||||
// case 403:
|
||||
// jobStatus.innerHTML = "None but devils play past here...";
|
||||
// break;
|
||||
// case 413:
|
||||
// jobStatus.innerHTML = "File too large!!!!!!";
|
||||
// break;
|
||||
// default:
|
||||
// jobStatus.innerHTML = "Error uploading file, blame someone";
|
||||
// break;
|
||||
// }
|
||||
// if (!document.querySelector(".upload-panel").classList.contains("open")) {
|
||||
// addNotification("Error uploading file", 2);
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
|
||||
fetch('/api/media/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
.banner-small
|
||||
width: 100%
|
||||
position: relative
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
.link
|
||||
padding: 0.1rem 0.3rem
|
||||
|
@ -10,15 +10,15 @@
|
|||
text-decoration: none
|
||||
font-weight: 500
|
||||
|
||||
background-color: RGB($fg-white)
|
||||
color: RGB($fg-black)
|
||||
border-radius: $rad-inner
|
||||
background-color: var(--foreground-white)
|
||||
color: var(--foreground-black)
|
||||
border-radius: calc(var(--radius) / 2)
|
||||
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
background-color: RGB($fg-black)
|
||||
color: RGB($fg-white)
|
||||
background-color: var(--foreground-black)
|
||||
color: var(--foreground-white)
|
||||
|
||||
.banner
|
||||
height: 30rem
|
||||
|
@ -26,7 +26,8 @@
|
|||
|
||||
img
|
||||
position: absolute
|
||||
inset: 0
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
@ -43,7 +44,7 @@
|
|||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background: linear-gradient(to right, RGB($bg-100), transparent)
|
||||
background: linear-gradient(to right, var(--background-100), transparent 80%, var(--background-100) 100%)
|
||||
|
||||
z-index: +1
|
||||
|
||||
|
@ -79,7 +80,7 @@
|
|||
font-size: 6.9rem
|
||||
font-weight: 700
|
||||
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
.banner-info
|
||||
grid-area: info
|
||||
|
@ -104,8 +105,8 @@
|
|||
width: 6.9rem
|
||||
height: 6.9rem
|
||||
|
||||
background-color: RGB($primary)
|
||||
border-radius: $rad
|
||||
background-color: var(--primary)
|
||||
border-radius: var(--rad)
|
||||
overflow: hidden
|
||||
|
||||
.banner-small
|
||||
|
@ -144,7 +145,7 @@
|
|||
font-weight: 700
|
||||
font-size: 1.5rem
|
||||
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
.banner-info
|
||||
margin-right: 0.6rem
|
||||
|
@ -156,7 +157,7 @@
|
|||
margin-left: auto
|
||||
width: auto
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
.banner,
|
||||
.banner-small
|
||||
&::after
|
||||
|
@ -168,7 +169,7 @@
|
|||
max-height: 30vh
|
||||
|
||||
.banner-filter
|
||||
background: linear-gradient(to bottom, RGB($bg-100), transparent)
|
||||
background: linear-gradient(to top, var(--background-100), transparent)
|
||||
|
||||
.banner-content
|
||||
padding: 0.5rem
|
||||
|
@ -192,7 +193,7 @@
|
|||
display: none
|
||||
|
||||
.pill-row
|
||||
margin-top: 0rem
|
||||
margin-top: 0
|
||||
|
||||
.banner-picture
|
||||
margin: 0 auto
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
@mixin btn-block($color)
|
||||
background-color: RGBA($color, 0.1)
|
||||
color: RGB($color)
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: RGBA($color, 0.15)
|
||||
color: RGB($color)
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($color, 0.2)
|
||||
|
||||
|
||||
.btn-block
|
||||
padding: 0.4rem 0.7rem
|
||||
|
||||
|
@ -26,19 +15,17 @@
|
|||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
background-color: RGBA($white, 0.1)
|
||||
color: RGB($white)
|
||||
background-color: var(--white-transparent)
|
||||
color: var(--white)
|
||||
border: none
|
||||
border-radius: $rad-inner
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
outline: none
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, box-shadow 0.15s ease-in-out
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: RGBA($white, 0.2)
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
|
||||
background-color: var(--white-transparent)
|
||||
|
||||
&.transparent
|
||||
background-color: transparent
|
||||
|
@ -47,20 +34,50 @@
|
|||
text-decoration: underline
|
||||
|
||||
&.primary
|
||||
@include btn-block($primary)
|
||||
&.critical
|
||||
@include btn-block($critical)
|
||||
&.warning
|
||||
@include btn-block($warning)
|
||||
background-color: var(--primary-transparent)
|
||||
color: var(--primary)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: var(--primary)
|
||||
color: var(--black)
|
||||
&.success
|
||||
@include btn-block($success)
|
||||
background-color: var(--success-transparent)
|
||||
color: var(--success)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: var(--success)
|
||||
color: var(--black)
|
||||
&.warning
|
||||
background-color: var(--warning-transparent)
|
||||
color: var(--warning)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: var(--warning)
|
||||
color: var(--black)
|
||||
&.critical
|
||||
background-color: var(--danger-transparent)
|
||||
color: var(--danger)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: var(--danger)
|
||||
color: var(--black)
|
||||
&.info
|
||||
@include btn-block($info)
|
||||
background-color: var(--info-transparent)
|
||||
color: var(--info)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: var(--info)
|
||||
color: var(--black)
|
||||
&.black
|
||||
@include btn-block($black)
|
||||
background-color: var(--black-transparent)
|
||||
color: var(--white)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: var(--black)
|
||||
color: var(--white)
|
||||
|
||||
&.disabled, &:disabled
|
||||
color: RGB($fg-dim)
|
||||
color: var(--foreground-gray)
|
||||
cursor: unset
|
||||
|
||||
.input-checkbox
|
||||
|
@ -77,7 +94,7 @@
|
|||
font-weight: 400
|
||||
text-align: left
|
||||
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
.input-block
|
||||
padding: 0.4rem 0.7rem
|
||||
|
@ -95,28 +112,32 @@
|
|||
font-weight: 400
|
||||
text-align: left
|
||||
|
||||
background-color: RGBA($white, 0.1)
|
||||
color: RGB($white)
|
||||
background-color: var(--white-transparent)
|
||||
color: var(--white)
|
||||
border: none
|
||||
border-bottom: 3px solid RGBA($white, 0.1)
|
||||
border-radius: $rad-inner
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
border-bottom: 3px solid var(--white-transparent)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
outline: none
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
|
||||
|
||||
&:not(:focus):not([value=""]):not(:placeholder-shown)
|
||||
border-color: RGBA($white, 0.3)
|
||||
border-color: var(--white-transparent)
|
||||
|
||||
&:hover
|
||||
border-color: RGBA($white, 0.3)
|
||||
border-color: var(--white-transparent)
|
||||
|
||||
&:focus
|
||||
border-color: RGB($primary)
|
||||
border-color: var(--primary)
|
||||
|
||||
&.black
|
||||
@include btn-block($black)
|
||||
background-color: var(--black-transparent)
|
||||
color: var(--white)
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: var(--black)
|
||||
color: var(--white)
|
||||
|
||||
.fileDrop-block
|
||||
padding: 1rem 1.25rem
|
||||
|
@ -136,11 +157,10 @@
|
|||
font-weight: 400
|
||||
text-align: center
|
||||
|
||||
background-color: RGBA($white, 0.1)
|
||||
color: RGB($white)
|
||||
background-color: var(--white-transparent)
|
||||
color: var(--white)
|
||||
border: none
|
||||
border-radius: $rad-inner
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.2), 0 -1px 0 RGBA($white, 0.2)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
outline: none
|
||||
|
||||
cursor: pointer
|
||||
|
@ -164,24 +184,21 @@
|
|||
overflow: hidden
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: RGBA($white, 0.2)
|
||||
color: RGB($white)
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
|
||||
background-color: var(--white-transparent)
|
||||
color: var(--white)
|
||||
|
||||
&.active
|
||||
background-color: RGBA($primary, 0.2)
|
||||
color: RGB($primary)
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($primary, 0.3)
|
||||
background-color: var(--primary-transparent)
|
||||
color: var(--primary)
|
||||
|
||||
&.edging
|
||||
background-color: RGBA($white, 0.2)
|
||||
color: RGB($white)
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($white, 0.3)
|
||||
background-color: var(--white-transparent)
|
||||
color: var(--white)
|
||||
|
||||
input
|
||||
display: none // So it doesnt get in the way of the drop as that breaks things
|
||||
// So it doesnt get in the way of the drop as that breaks things
|
||||
display: none
|
||||
|
||||
&.error
|
||||
background-color: RGBA($critical, 0.2)
|
||||
color: RGB($critical)
|
||||
// box-shadow: 0 1px 0 RGBA($black, 0.3), 0 -1px 0 RGBA($critical, 0.3)
|
||||
background-color: var(--danger)
|
||||
color: var(--black)
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
.info-button
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
width: auto
|
||||
height: auto
|
||||
|
||||
position: fixed
|
||||
bottom: 0.75rem
|
||||
right: -3rem
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
color: RGB($fg-white)
|
||||
border-radius: $rad
|
||||
border: none
|
||||
opacity: 0
|
||||
|
||||
z-index: 20
|
||||
cursor: pointer
|
||||
transition: all 0.2s cubic-bezier(.86, 0, .07, 1)
|
||||
|
||||
i
|
||||
margin: 0.5rem
|
||||
font-size: 1.25rem
|
||||
|
||||
&:hover
|
||||
color: RGB($info)
|
||||
|
||||
&.show
|
||||
right: 0.75rem
|
||||
opacity: 1
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
.info-button
|
||||
bottom: 4.25rem
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
display: flex
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
border-radius: $rad
|
||||
// box-shadow: 0 1px 0 RGB($bg-100), 0 -1px 0 RGB($bg-300)
|
||||
background-color: var(--background-200)
|
||||
border-radius: var(--rad)
|
||||
|
||||
.pill-text
|
||||
margin: 0
|
||||
|
@ -37,9 +36,9 @@
|
|||
font-size: 1rem
|
||||
font-weight: 400
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
color: RGB($fg-white)
|
||||
border-radius: $rad
|
||||
background-color: var(--background-200)
|
||||
color: var(--foreground-white)
|
||||
border-radius: var(--rad)
|
||||
|
||||
.pill-item
|
||||
margin: 0
|
||||
|
@ -58,42 +57,41 @@
|
|||
|
||||
border: none
|
||||
background-color: transparent
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
i
|
||||
font-size: 1.25rem
|
||||
|
||||
&:hover
|
||||
cursor: pointer
|
||||
color: var(--primary)
|
||||
|
||||
color: RGB($primary)
|
||||
|
||||
&.disabled, &:disabled
|
||||
color: RGB($fg-dim)
|
||||
cursor: unset
|
||||
&:disabled, &[disabled], &.disabled
|
||||
color: var(--foreground-gray)
|
||||
cursor: default
|
||||
|
||||
.pill__critical
|
||||
color: RGB($critical)
|
||||
color: var(--danger)
|
||||
|
||||
span
|
||||
background: RGB($critical)
|
||||
color: RGB($fg-white)
|
||||
background: var(--danger)
|
||||
color: var(--foreground-white)
|
||||
|
||||
i
|
||||
color: RGB($critical)
|
||||
color: var(--danger)
|
||||
|
||||
&:hover
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
.pill__info
|
||||
color: RGB($info)
|
||||
color: var(--info)
|
||||
|
||||
span
|
||||
color: RGB($info)
|
||||
color: var(--info)
|
||||
|
||||
&:hover
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: var(--breakpoint))
|
||||
.tool-tip
|
||||
display: none
|
|
@ -13,9 +13,9 @@
|
|||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
color: RGB($fg-white)
|
||||
border-radius: $rad
|
||||
background-color: var(--background-100)
|
||||
color: var(--foreground-white)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
border: none
|
||||
opacity: 0
|
||||
|
||||
|
@ -28,12 +28,12 @@
|
|||
font-size: 1.25rem
|
||||
|
||||
&:hover
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
&.show
|
||||
right: 0.75rem
|
||||
opacity: 1
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
.top-of-page
|
||||
bottom: 4.25rem
|
||||
|
|
|
@ -25,13 +25,13 @@
|
|||
align-items: flex-start
|
||||
gap: 0.25rem
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
border: 1px solid RGB($bg-200)
|
||||
background-color: var(--background-300)
|
||||
border: 1px solid var(--background-200)
|
||||
border-radius: 6px
|
||||
|
||||
overflow: hidden
|
||||
|
||||
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out
|
||||
transition: transform 0.35s var(--animation-bounce), opacity 0.35s var(--animation-bounce)
|
||||
transform-origin: center center
|
||||
opacity: 0.5
|
||||
transform: scale(0, 0)
|
||||
|
@ -46,7 +46,7 @@
|
|||
text-align: center
|
||||
font-size: 1.2rem
|
||||
font-weight: 400
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
.contextMenuItem
|
||||
margin: 0
|
||||
|
@ -61,22 +61,22 @@
|
|||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
color: RGB($fg-white)
|
||||
background-color: var(--background-300)
|
||||
color: var(--foreground-white)
|
||||
border: none
|
||||
border-radius: 3px
|
||||
|
||||
cursor: pointer
|
||||
.contextMenuItem:hover
|
||||
background-color: RGB($bg-200)
|
||||
background-color: var(--background-200)
|
||||
.contextMenuItem__critical
|
||||
color: RGB($critical)
|
||||
color: var(--danger)
|
||||
.contextMenuItem__warning
|
||||
color: RGB($warning)
|
||||
color: var(--warning)
|
||||
.contextMenuItem__success
|
||||
color: RGB($success)
|
||||
color: var(--success)
|
||||
.contextMenuItem__info
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
.contextMenuText
|
||||
margin: 0
|
||||
|
@ -104,7 +104,7 @@
|
|||
height: 1px
|
||||
|
||||
border: none
|
||||
background-color: RGB($bg-200)
|
||||
background-color: var(--background-200)
|
||||
|
||||
.contextMenu__show
|
||||
opacity: 1
|
||||
|
|
|
@ -12,26 +12,21 @@
|
|||
font-weight: 700
|
||||
|
||||
.gallery-grid
|
||||
margin: 0
|
||||
padding: 0.35rem
|
||||
|
||||
width: 100%
|
||||
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
|
||||
gap: 0.5rem
|
||||
|
||||
.gallery-item
|
||||
margin: 0.35rem
|
||||
padding: 0
|
||||
|
||||
position: relative
|
||||
|
||||
border-radius: $rad-inner
|
||||
box-shadow: 0 0.15rem 0.4rem 0.1rem RGBA($bg-100, 0.4)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
box-shadow: 0 0.15rem 0.4rem 0.1rem var(--black-transparent)
|
||||
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
transition: box-shadow 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
transition: box-shadow 0.2s var(--animation-smooth)
|
||||
|
||||
.image-filter
|
||||
margin: 0
|
||||
|
@ -48,11 +43,11 @@
|
|||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
|
||||
background-image: linear-gradient(to top, rgba($bg-100, 0.69), transparent)
|
||||
background-image: linear-gradient(to top, var(--black-transparent), transparent)
|
||||
opacity: 0 // hide
|
||||
|
||||
z-index: +4
|
||||
transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
transition: opacity 0.2s var(--animation-smooth)
|
||||
|
||||
.image-title,
|
||||
.image-subtitle
|
||||
|
@ -63,8 +58,8 @@
|
|||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
color: RGB($fg-white)
|
||||
text-shadow: 0px 0px 2px RGB($fg-black)
|
||||
color: var(--foreground-white)
|
||||
text-shadow: 0 0 2px var(--foreground-black)
|
||||
|
||||
.image-title
|
||||
font-size: 0.9rem
|
||||
|
@ -84,21 +79,20 @@
|
|||
object-fit: cover
|
||||
object-position: center
|
||||
|
||||
background-color: RGB($bg-bright)
|
||||
background-color: var(--background-bright)
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 0.2rem 0.4rem 0.1rem RGBA($bg-100, 0.6)
|
||||
box-shadow: 0 0.2rem 0.4rem 0.1rem var(--black-transparent)
|
||||
|
||||
.image-filter
|
||||
opacity: 1
|
||||
|
||||
.group-item
|
||||
margin: 0.35rem
|
||||
padding: 0
|
||||
|
||||
position: relative
|
||||
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
|
@ -119,7 +113,7 @@
|
|||
flex-direction: column
|
||||
justify-content: flex-end
|
||||
|
||||
background-image: linear-gradient(to top, rgba($bg-100, 0.8), transparent)
|
||||
background-image: linear-gradient(to top, var(--black-transparent), transparent)
|
||||
|
||||
z-index: +4
|
||||
|
||||
|
@ -132,8 +126,8 @@
|
|||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
||||
color: RGB($fg-white)
|
||||
text-shadow: 0px 0px 2px RGB($fg-black)
|
||||
color: var(--foreground-white)
|
||||
text-shadow: 0 0 2px var(--foreground-black)
|
||||
|
||||
.image-title
|
||||
font-size: 0.9rem
|
||||
|
@ -165,11 +159,11 @@
|
|||
object-fit: cover
|
||||
object-position: center
|
||||
|
||||
background-color: RGB($bg-bright)
|
||||
border-radius: $rad-inner
|
||||
box-shadow: 0 0 0.4rem 0.25rem RGBA($bg-100, 0.1)
|
||||
background-color: var(--background-800)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
box-shadow: 0 0 0.4rem 0.25rem var(--black-transparent)
|
||||
|
||||
transition: transform 0.2s cubic-bezier(.79, .14, .15, .86)
|
||||
transition: transform 0.2s var(--animation-smooth)
|
||||
|
||||
&.size-1
|
||||
.data-1
|
||||
|
@ -224,5 +218,5 @@
|
|||
grid-template-columns: auto auto auto
|
||||
|
||||
.gallery-item
|
||||
margin: 0.35rem
|
||||
margin: 0.1rem
|
||||
position: relative
|
|
@ -1,11 +1,11 @@
|
|||
.info-container
|
||||
padding: 0.5rem 0 0 0.5rem
|
||||
padding: 0.5rem 0 0.5rem 0.5rem
|
||||
width: 27rem
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
bottom: 0
|
||||
background-image: linear-gradient(90deg, $bg-transparent, transparent)
|
||||
background-image: linear-gradient(90deg, var(--background-shade), transparent)
|
||||
overflow-y: auto
|
||||
transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
z-index: 2
|
||||
|
@ -18,7 +18,7 @@
|
|||
left: -27rem
|
||||
@media (max-width: 1100px)
|
||||
.info-container
|
||||
padding: 0 0.5rem 0 0.5rem
|
||||
padding: 0
|
||||
width: 100%
|
||||
position: relative
|
||||
background: none
|
||||
|
@ -32,9 +32,9 @@ details
|
|||
padding: 0.5rem
|
||||
display: flex
|
||||
flex-direction: column
|
||||
background-color: RGB($bg-300)
|
||||
color: RGB($fg-white)
|
||||
border-radius: $rad
|
||||
background-color: var(--background-300)
|
||||
color: var(--foreground-white)
|
||||
border-radius: var(--rad)
|
||||
overflow: hidden
|
||||
|
||||
summary
|
||||
|
@ -44,7 +44,7 @@ details
|
|||
justify-content: flex-start
|
||||
align-items: center
|
||||
position: relative
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
> i
|
||||
margin-right: 0
|
||||
|
@ -58,13 +58,6 @@ details
|
|||
font-size: 1.1rem
|
||||
font-weight: 500
|
||||
|
||||
&[open]
|
||||
summary
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
> i.collapse-indicator
|
||||
transform: rotate(90deg)
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
@ -79,7 +72,7 @@ details
|
|||
margin: 0
|
||||
padding: 0
|
||||
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
|
@ -91,7 +84,7 @@ details
|
|||
width: 1.1rem
|
||||
height: 1.1rem
|
||||
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
|
||||
object-fit: cover
|
||||
|
||||
|
@ -137,6 +130,16 @@ details
|
|||
tr:last-of-type td
|
||||
padding-bottom: 0
|
||||
|
||||
&[open]
|
||||
summary
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
> i.collapse-indicator
|
||||
transform: rotate(90deg)
|
||||
|
||||
&:last-of-type
|
||||
margin-bottom: 0
|
||||
|
||||
.img-colours
|
||||
width: 100%
|
||||
|
||||
|
@ -154,7 +157,7 @@ details
|
|||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
border: none
|
||||
|
||||
i
|
||||
|
@ -197,7 +200,9 @@ details
|
|||
max-height: 100%
|
||||
object-fit: contain
|
||||
object-position: center
|
||||
border-radius: $rad
|
||||
border-radius: var(--rad)
|
||||
|
||||
transition: border-radius 0.3s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
|
||||
&.collapsed
|
||||
padding: 0
|
||||
|
@ -207,6 +212,7 @@ details
|
|||
border-radius: 0
|
||||
@media (max-width: 1100px)
|
||||
.image-container
|
||||
padding: 0 0 0.5rem 0
|
||||
position: relative
|
||||
left: 0
|
||||
|
||||
|
@ -218,17 +224,17 @@ details
|
|||
max-height: 69vh
|
||||
|
||||
&.collapsed
|
||||
padding: 0.5rem
|
||||
padding: 0 0 0.5rem 0
|
||||
left: 0
|
||||
|
||||
picture img
|
||||
border-radius: $rad
|
||||
border-radius: var(--rad)
|
||||
|
||||
.background
|
||||
position: absolute
|
||||
inset: 0
|
||||
background-color: RGB($bg-300)
|
||||
background-image: linear-gradient(to right, RGB($bg-400) 15%, RGB($bg-200) 35%, RGB($bg-400) 50%)
|
||||
background-color: var(--background-300)
|
||||
background-image: linear-gradient(to right, var(--background-400) 15%, var(--background-200) 35%, var(--background-400) 50%)
|
||||
background-size: 1000px 640px
|
||||
animation: imgLoading 1.8s linear infinite forwards
|
||||
user-select: none
|
||||
|
@ -240,7 +246,7 @@ details
|
|||
inset: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
background-color: RGB($fg-white)
|
||||
background-color: var(--foreground-white)
|
||||
filter: blur(3rem) saturate(1.2) brightness(0.7)
|
||||
transform: scale(1.1)
|
||||
object-fit: cover
|
||||
|
|
|
@ -42,27 +42,27 @@ nav
|
|||
> i
|
||||
padding: 0.5rem
|
||||
font-size: 1.3rem
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
color: inherit
|
||||
|
||||
> .nav-pfp
|
||||
padding: 0.4rem
|
||||
width: 2.3rem
|
||||
height: 2.3rem
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
|
||||
&:hover
|
||||
> i, .nav-pfp
|
||||
background: RGBA($fg-white, 0.1)
|
||||
background: var(--white-transparent)
|
||||
|
||||
&.selected
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
&::before
|
||||
content: ''
|
||||
|
@ -76,9 +76,9 @@ nav
|
|||
height: calc(100% - 1rem)
|
||||
|
||||
background-color: currentColor
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
nav
|
||||
width: 100vw
|
||||
height: 3.5rem
|
||||
|
@ -91,7 +91,7 @@ nav
|
|||
bottom: 0
|
||||
left: 0
|
||||
|
||||
background-color: RGB($background)
|
||||
background-color: var(--background-100)
|
||||
|
||||
> span
|
||||
display: none
|
||||
|
|
|
@ -1,23 +1,9 @@
|
|||
@keyframes notificationTimeout
|
||||
0%
|
||||
left: -100%
|
||||
height: 3px
|
||||
90%
|
||||
left: 0%
|
||||
height: 3px
|
||||
95%
|
||||
left: 0%
|
||||
height: 0
|
||||
width: 0
|
||||
100%
|
||||
left: 0%
|
||||
height: 0
|
||||
|
||||
@mixin notification($color)
|
||||
color: RGB($color)
|
||||
width: 100%
|
||||
|
||||
&::after
|
||||
background-color: RGB($color)
|
||||
|
||||
.notifications
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
@ -46,9 +32,9 @@
|
|||
|
||||
position: relative
|
||||
|
||||
background-color: RGB($bg-300)
|
||||
border-radius: $rad-inner
|
||||
color: RGB($fg-white)
|
||||
background-color: var(--background-400)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
color: var(--foreground-white)
|
||||
opacity: 0
|
||||
transform: scale(0.8)
|
||||
|
||||
|
@ -60,26 +46,34 @@
|
|||
&::after
|
||||
content: ""
|
||||
|
||||
width: 100%
|
||||
width: 0
|
||||
height: 3px
|
||||
|
||||
position: absolute
|
||||
bottom: 0px
|
||||
left: 0px
|
||||
bottom: 0
|
||||
left: 0
|
||||
|
||||
background-color: RGB($fg-white)
|
||||
background-color: var(--foreground-white)
|
||||
|
||||
z-index: +2
|
||||
animation: notificationTimeout 5.1s linear
|
||||
animation: notificationTimeout 5.1s ease-out forwards
|
||||
|
||||
&.success
|
||||
@include notification($success)
|
||||
color: var(--success)
|
||||
&::after
|
||||
background-color: var(--success)
|
||||
&.warning
|
||||
@include notification($warning)
|
||||
color: var(--warning)
|
||||
&::after
|
||||
background-color: var(--warning)
|
||||
&.critical
|
||||
@include notification($critical)
|
||||
color: var(--danger)
|
||||
&::after
|
||||
background-color: var(--danger)
|
||||
&.info
|
||||
@include notification($info)
|
||||
color: var(--info)
|
||||
&::after
|
||||
background-color: var(--info)
|
||||
|
||||
&.show
|
||||
opacity: 1
|
||||
|
@ -89,7 +83,7 @@
|
|||
margin: 0
|
||||
max-height: 0
|
||||
opacity: 0
|
||||
transform: translateX(100%)
|
||||
transform: translateY(1rem)
|
||||
transition: all 0.4s ease-in-out, max-height 0.2s ease-in-out
|
||||
|
||||
.sniffle__notification-icon
|
||||
|
@ -103,7 +97,7 @@
|
|||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
background-color: var(--background-300)
|
||||
|
||||
i
|
||||
font-size: 1.25rem
|
||||
|
@ -125,7 +119,7 @@
|
|||
line-height: 1
|
||||
text-align: left
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
.notifications
|
||||
bottom: 3.8rem
|
||||
width: calc(100vw - 0.6rem)
|
||||
|
@ -133,10 +127,6 @@
|
|||
|
||||
.sniffle__notification
|
||||
width: 100%
|
||||
|
||||
&.hide
|
||||
opacity: 0
|
||||
transform: translateY(1rem)
|
||||
|
||||
.sniffle__notification-time
|
||||
width: 100%
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
display: none
|
||||
|
||||
background-color: $bg-transparent
|
||||
background-color: var(--background-shade)
|
||||
opacity: 0
|
||||
|
||||
z-index: 101
|
||||
|
@ -38,12 +38,12 @@
|
|||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
border-radius: $rad
|
||||
background-color: var(--background-400)
|
||||
border-radius: var(--rad)
|
||||
overflow: hidden
|
||||
|
||||
z-index: +2
|
||||
transition: transform 0.2s $animation-smooth
|
||||
transition: transform 0.2s var(--animation-bounce)
|
||||
|
||||
.pop-up-header
|
||||
margin: 0 0 0.5rem 0
|
||||
|
@ -73,7 +73,7 @@
|
|||
font-weight: 700
|
||||
text-align: left
|
||||
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
p
|
||||
margin: 0
|
||||
|
@ -84,7 +84,7 @@
|
|||
font-weight: 400
|
||||
text-align: left
|
||||
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
svg
|
||||
width: 1rem
|
||||
|
@ -94,7 +94,7 @@
|
|||
vertical-align: middle
|
||||
|
||||
a, .link
|
||||
color: RGB($primary)
|
||||
color: var(--primary)
|
||||
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
|
@ -127,15 +127,13 @@
|
|||
justify-content: flex-end
|
||||
gap: 0.5rem
|
||||
|
||||
// background-color: RGB($bg-100)
|
||||
|
||||
&.active
|
||||
opacity: 1
|
||||
|
||||
.pop-up-wrapper
|
||||
transform: translate(-50%, 50%) scale(1)
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
.pop-up
|
||||
.pop-up-wrapper
|
||||
max-width: calc(100% - 0.75rem)
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
justify-content: center
|
||||
gap: 1rem
|
||||
|
||||
background-color: RGB($bg-400)
|
||||
color: RGB($fg-white)
|
||||
border: 2px solid RGB($bg-200)
|
||||
border-radius: $rad-inner
|
||||
background-color: var(--background-400)
|
||||
color: var(--foreground-white)
|
||||
border: 2px solid var(--background-200)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
|
||||
h2
|
||||
margin: 0
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
font-weight: 500
|
||||
text-decoration: none
|
||||
|
||||
border-radius: $rad-inner
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
border: none
|
||||
background-color: RGBA($primary, 0.1)
|
||||
color: RGB($primary)
|
||||
background-color: var(--primary-transparent)
|
||||
color: var(--primary)
|
||||
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
|
||||
|
@ -23,4 +23,4 @@
|
|||
font-size: 1.15rem
|
||||
|
||||
&:hover
|
||||
background-color: RGBA($primary, 0.2)
|
||||
background-color: var(--primary-transparent)
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
height: 100vh
|
||||
|
||||
background-color: transparent
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
overflow: hidden
|
||||
z-index: 68
|
||||
transition: background-color 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
transition: background-color 0.25s var(--animation-smooth)
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
|
@ -66,11 +66,11 @@
|
|||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
background-color: var(--background-200)
|
||||
|
||||
z-index: +2
|
||||
|
||||
transition: left 0.25s cubic-bezier(0.76, 0, 0.17, 1), bottom 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
transition: left 0.25s var(--animation-smooth), bottom 0.25s var(--animation-smooth)
|
||||
|
||||
#dragIndicator
|
||||
display: none
|
||||
|
@ -94,21 +94,21 @@
|
|||
left: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
|
||||
background-color: RGB($bg-400)
|
||||
border-radius: $rad-inner
|
||||
background-color: var(--background-400)
|
||||
border-radius: calc(var(--rad) / 2)
|
||||
|
||||
transition: width 0.25s $animation-bounce
|
||||
transition: width 0.25s var(--animation-smooth)
|
||||
|
||||
&.dragging #dragIndicator::after
|
||||
width: 9rem
|
||||
background-color: RGB($primary)
|
||||
background-color: var(--primary)
|
||||
|
||||
.upload-jobs
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
border-radius: $rad
|
||||
border-radius: var(--rad)
|
||||
|
||||
overflow-y: auto
|
||||
|
||||
|
@ -123,8 +123,8 @@
|
|||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
background-color: RGB($bg-200)
|
||||
border-radius: $rad
|
||||
background-color: var(--background-200)
|
||||
border-radius: var(--rad)
|
||||
|
||||
overflow: hidden
|
||||
|
||||
|
@ -146,7 +146,7 @@
|
|||
width: 100%
|
||||
height: 100%
|
||||
|
||||
background-image: linear-gradient(to right, RGB($bg-100), transparent)
|
||||
background-image: linear-gradient(to right, var(--background-100), transparent)
|
||||
|
||||
.job__status
|
||||
margin: 0
|
||||
|
@ -159,51 +159,51 @@
|
|||
font-size: 1rem
|
||||
font-weight: 600
|
||||
|
||||
color: RGB($fg-white)
|
||||
color: var(--foreground-white)
|
||||
|
||||
z-index: +3
|
||||
|
||||
transition: color 0.25s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
transition: color 0.25s var(--animation-smooth)
|
||||
|
||||
.progress
|
||||
width: 100%
|
||||
height: $rad-inner
|
||||
height: calc(var(--rad) / 2)
|
||||
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: -100%
|
||||
|
||||
background-color: RGB($primary)
|
||||
background-color: var(--primary)
|
||||
|
||||
animation: uploadingLoop 1s cubic-bezier(0.76, 0, 0.17, 1) infinite
|
||||
animation: uploadingLoop 1s var(--animation-smooth) infinite
|
||||
|
||||
z-index: +5
|
||||
transition: left 1s cubic-bezier(0.76, 0, 0.17, 1)
|
||||
transition: left 1s var(--animation-smooth)
|
||||
|
||||
&.critical
|
||||
.job__status, .progress
|
||||
color: RGB($critical)
|
||||
color: var(--critical)
|
||||
&.success
|
||||
.job__status
|
||||
color: RGB($success)
|
||||
color: var(--success)
|
||||
.progress
|
||||
height: 0
|
||||
animation: none
|
||||
&.warning
|
||||
.job__status, .progress
|
||||
color: RGB($warning)
|
||||
color: var(--warning)
|
||||
|
||||
&.critical, &.success, &.warning
|
||||
.progress
|
||||
height: 0
|
||||
|
||||
&.open
|
||||
background-color: $bg-transparent
|
||||
background-color: var(--background-shade)
|
||||
|
||||
.container
|
||||
left: 0
|
||||
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
.upload-panel
|
||||
width: 100%
|
||||
height: calc(100vh - 3.5rem)
|
||||
|
@ -219,7 +219,7 @@
|
|||
left: 0
|
||||
bottom: -100vh
|
||||
|
||||
border-radius: $rad $rad 0 0
|
||||
border-radius: var(--rad) var(--rad) 0 0
|
||||
|
||||
#dragIndicator
|
||||
display: block
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Default theme for OnlyLegs by FluffyBean
|
||||
// Mockup link: https://www.figma.com/file/IMZT5kZr3sAngrSHSGu5di/OnlyLegs?node-id=0%3A1
|
||||
|
||||
|
||||
@import "variables"
|
||||
@import "animations"
|
||||
|
||||
|
@ -15,7 +16,6 @@
|
|||
@import "components/gallery"
|
||||
|
||||
@import "components/buttons/top-of-page"
|
||||
@import "components/buttons/info-button"
|
||||
@import "components/buttons/pill"
|
||||
@import "components/buttons/block"
|
||||
|
||||
|
@ -24,17 +24,17 @@
|
|||
|
||||
*
|
||||
box-sizing: border-box
|
||||
scrollbar-color: RGB($primary) transparent
|
||||
font-family: $font
|
||||
scrollbar-color: var(--primary) transparent
|
||||
font-family: var(--font-family)
|
||||
|
||||
::-webkit-scrollbar
|
||||
width: 0.5rem
|
||||
::-webkit-scrollbar-track
|
||||
background: RGB($bg-200)
|
||||
background: var(--background-200)
|
||||
::-webkit-scrollbar-thumb
|
||||
background: RGB($primary)
|
||||
background: var(--primary)
|
||||
::-webkit-scrollbar-thumb:hover
|
||||
background: RGB($fg-white)
|
||||
background: var(--foreground-white)
|
||||
|
||||
html
|
||||
margin: 0
|
||||
|
@ -51,25 +51,35 @@ body
|
|||
display: grid
|
||||
grid-template-rows: auto 1fr auto
|
||||
|
||||
background-color: RGB($background)
|
||||
color: RGB($foreground)
|
||||
font-family: var(--font-family)
|
||||
|
||||
background-color: var(--background-100)
|
||||
color: var(--foreground-white)
|
||||
|
||||
overflow-x: hidden
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
body
|
||||
padding: 0 0 3.5rem 0
|
||||
|
||||
main
|
||||
margin: 0 0.5rem 0.5rem 0
|
||||
padding: 0.5rem
|
||||
display: flex
|
||||
flex-direction: column
|
||||
position: relative
|
||||
background: RGBA($white, 1)
|
||||
color: RGB($fg-black)
|
||||
border-top-left-radius: $rad
|
||||
background: var(--background-800)
|
||||
color: var(--foreground-black)
|
||||
border-radius: var(--rad)
|
||||
overflow: hidden
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
main
|
||||
border-top-left-radius: 0
|
||||
margin: 0 0.5rem
|
||||
|
||||
// This is very broken, as it breaks when you open any context menu/popup
|
||||
// I need to fix this at some point as it looks really nice
|
||||
//header
|
||||
// position: sticky
|
||||
// top: 0
|
||||
|
||||
.error-page
|
||||
min-height: 100%
|
||||
|
@ -86,7 +96,7 @@ main
|
|||
font-weight: 900
|
||||
text-align: center
|
||||
|
||||
color: $primary
|
||||
color: var(--primary)
|
||||
|
||||
p
|
||||
margin: 0 2rem
|
||||
|
@ -95,7 +105,7 @@ main
|
|||
font-size: 1.25rem
|
||||
font-weight: 400
|
||||
text-align: center
|
||||
@media (max-width: $breakpoint)
|
||||
@media (max-width: 800px)
|
||||
.error-page
|
||||
h1
|
||||
font-size: 4.5rem
|
||||
|
|
|
@ -1,80 +1,56 @@
|
|||
$bg-transparent: rgba(var(--bg-dim), 0.8)
|
||||
$bg-dim: var(--bg-dim)
|
||||
$bg-bright: var(--bg-bright)
|
||||
$bg-100: var(--bg-100)
|
||||
$bg-200: var(--bg-200)
|
||||
$bg-300: var(--bg-300)
|
||||
$bg-400: var(--bg-400)
|
||||
$bg-500: var(--bg-500)
|
||||
$bg-600: var(--bg-600)
|
||||
|
||||
$fg-dim: var(--fg-dim)
|
||||
$fg-white: var(--fg-white)
|
||||
$fg-black: var(--fg-black)
|
||||
|
||||
$black: var(--black)
|
||||
$white: var(--white)
|
||||
$red: var(--red)
|
||||
$orange: var(--orange)
|
||||
$yellow: var(--yellow)
|
||||
$green: var(--green)
|
||||
$blue: var(--blue)
|
||||
$purple: var(--purple)
|
||||
|
||||
$primary: var(--primary)
|
||||
$warning: var(--warning)
|
||||
$critical: var(--critical)
|
||||
$success: var(--success)
|
||||
$info: var(--info)
|
||||
|
||||
$rad: var(--rad)
|
||||
$rad-inner: var(--rad-inner)
|
||||
|
||||
$animation-smooth: var(--animation-smooth)
|
||||
$animation-bounce: var(--animation-bounce)
|
||||
|
||||
$font: 'Rubik', sans-serif
|
||||
$breakpoint: 800px
|
||||
|
||||
|
||||
// New variables, Slowly moving over to them because I suck at planning ahead and coding reeee
|
||||
$background: var(--bg-100)
|
||||
$foreground: var(--fg-white)
|
||||
|
||||
|
||||
\:root
|
||||
--bg-dim: 16, 16, 16
|
||||
--bg-bright: 232, 227, 227
|
||||
--bg-100: 21, 21, 21
|
||||
--bg-200: #{red(adjust-color(rgb(21, 21, 21), $lightness: 2%)), green(adjust-color(rgb(21, 21, 21), $lightness: 2%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 2%))}
|
||||
--bg-300: #{red(adjust-color(rgb(21, 21, 21), $lightness: 4%)), green(adjust-color(rgb(21, 21, 21), $lightness: 4%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 4%))}
|
||||
--bg-400: #{red(adjust-color(rgb(21, 21, 21), $lightness: 6%)), green(adjust-color(rgb(21, 21, 21), $lightness: 6%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 6%))}
|
||||
--bg-500: #{red(adjust-color(rgb(21, 21, 21), $lightness: 8%)), green(adjust-color(rgb(21, 21, 21), $lightness: 8%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 8%))}
|
||||
--bg-600: #{red(adjust-color(rgb(21, 21, 21), $lightness: 10%)), green(adjust-color(rgb(21, 21, 21), $lightness: 10%)), blue(adjust-color(rgb(21, 21, 21), $lightness: 10%))}
|
||||
--background-hsl-hue: 69
|
||||
--background-hsl-saturation: 25%
|
||||
|
||||
--fg-dim: 102, 102, 102
|
||||
--fg-white: 232, 227, 227
|
||||
--fg-black: 16, 16, 16
|
||||
--background-100: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 6%)
|
||||
--background-200: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 8%)
|
||||
--background-300: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 10%)
|
||||
--background-400: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 12%)
|
||||
--background-500: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 14%)
|
||||
--background-600: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 16%)
|
||||
--background-700: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 18%)
|
||||
--background-800: rgb(232, 227, 227)
|
||||
|
||||
--black: 21, 21, 21
|
||||
--white: 232, 227, 227
|
||||
--red: 182, 100, 103
|
||||
--orange: 217, 140, 95
|
||||
--yellow: 217, 188, 140
|
||||
--green: 140, 151, 125
|
||||
--blue: 141, 163, 185
|
||||
--purple: 169, 136, 176
|
||||
--background-shade: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 6%, 0.5)
|
||||
|
||||
--foreground-white: rgb(232, 227, 227)
|
||||
--foreground-gray: rgb(102, 102, 102)
|
||||
--foreground-black: rgb(16, 16, 16)
|
||||
|
||||
--black: rgb(20, 20, 20)
|
||||
--black-transparent: rgba(20, 20, 20, 0.2)
|
||||
--white: rgb(232, 227, 227)
|
||||
--white-transparent: rgba(232, 227, 227, 0.2)
|
||||
|
||||
--red: rgb(182, 100, 103)
|
||||
--red-transparent: rgba(182, 100, 103, 0.1)
|
||||
--orange: rgb(217, 140, 95)
|
||||
--orange-transparent: rgba(217, 140, 95, 0.1)
|
||||
--yellow: rgb(198, 185, 166)
|
||||
--yellow-transparent: rgba(198, 185, 166, 0.1)
|
||||
--green: rgb(140, 151, 125)
|
||||
--green-transparent: rgba(140, 151, 125, 0.1)
|
||||
--blue: rgb(141, 163, 185)
|
||||
--blue-transparent: rgba(141, 163, 185, 0.1)
|
||||
--purple: rgb(169, 136, 176)
|
||||
--purple-transparent: rgba(169, 136, 176, 0.1)
|
||||
--primary: rgb(183, 169, 151)
|
||||
--primary-transparent: rgba(183, 169, 151, 0.1)
|
||||
|
||||
--primary: var(--green) // 183, 169, 151
|
||||
--warning: var(--orange)
|
||||
--critical: var(--red)
|
||||
--warning-transparent: var(--orange-transparent)
|
||||
--danger: var(--red)
|
||||
--danger-transparent: var(--red-transparent)
|
||||
--success: var(--green)
|
||||
--success-transparent: var(--green-transparent)
|
||||
--info: var(--blue)
|
||||
--info-transparent: var(--blue-transparent)
|
||||
|
||||
--rad: 0.5rem
|
||||
--rad-inner: calc(var(--rad) / 2)
|
||||
--rad: 0.4rem
|
||||
|
||||
--animation-smooth: cubic-bezier(0.76, 0, 0.17, 1)
|
||||
--animation-bounce: cubic-bezier(.68,-0.55,.27,1.55)
|
||||
|
||||
--breakpoint: 800px
|
||||
|
||||
--font-family: 'Switzer', sans-serif
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
<meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap">
|
||||
<link href="https://api.fontshare.com/v2/css?f[]=switzer@101,600,701,800,501,601,900,100,700,901,400,201,401,200,300,301,801,500&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- phosphor icons -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
|
@ -29,6 +27,23 @@
|
|||
|
||||
{% assets "scripts" %} <script type="text/javascript" src="{{ ASSET_URL }}"></script> {% endassets %}
|
||||
{% assets "styles" %} <link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer> {% endassets %}
|
||||
|
||||
<style>
|
||||
{% if config.WEBSITE_CONF.styling.force %}
|
||||
:root{
|
||||
--background-hsl-hue: {{ config.WEBSITE_CONF.styling.hue }} !important;
|
||||
--background-hsl-saturation: {{ config.WEBSITE_CONF.styling.saturation }}% !important;
|
||||
--rad: {{ config.WEBSITE_CONF.styling.rad }} !important;
|
||||
}
|
||||
{% else %}
|
||||
:root{
|
||||
--background-hsl-hue: {{ config.WEBSITE_CONF.styling.hue }};
|
||||
--background-hsl-saturation: {{ config.WEBSITE_CONF.styling.saturation }}%;
|
||||
--rad: {{ config.WEBSITE_CONF.styling.rad }};
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
@ -123,6 +138,10 @@
|
|||
<script type="text/javascript">
|
||||
keepSquare();
|
||||
|
||||
{% for message in get_flashed_messages() %}
|
||||
addNotification('{{ message[0] }}', {{ message[1] }});
|
||||
{% endfor %}
|
||||
|
||||
const times = document.querySelectorAll('.time');
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
// Remove milliseconds
|
||||
|
@ -184,10 +203,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{% for message in get_flashed_messages() %}
|
||||
addNotification('{{ message[0] }}', {{ message[1] }});
|
||||
{% endfor %}
|
||||
</script>
|
||||
|
||||
{% block script %}{% endblock %}
|
||||
|
|
|
@ -20,40 +20,9 @@
|
|||
|
||||
<style>
|
||||
{% if images %}
|
||||
:root { --bg-100: {{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }} }
|
||||
|
||||
body {
|
||||
background: rgb{{ images.0.colours.0 }} !important;
|
||||
color: {{ text_colour }} !important;
|
||||
}
|
||||
main {
|
||||
background: rgba(var(--white), 0.6) !important;
|
||||
}
|
||||
|
||||
|
||||
.navigation-item.selected { color: {{ text_colour }} !important; }
|
||||
|
||||
.banner .banner-content .banner-header,
|
||||
.banner .banner-content .banner-info,
|
||||
.banner .banner-content .banner-subtitle {
|
||||
color: {{ text_colour }} !important;
|
||||
}
|
||||
.banner-content .link {
|
||||
background-color: {{ text_colour }} !important;
|
||||
color: rgb{{ images.0.colours.0 }} !important;
|
||||
}
|
||||
.banner-content .link:hover {
|
||||
background-color: rgb{{ images.0.colours.0 }} !important;
|
||||
color: {{ text_colour }} !important;
|
||||
}
|
||||
|
||||
.banner-filter {
|
||||
background: linear-gradient(90deg, rgb{{ images.0.colours.0 }}, rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.banner-filter {
|
||||
background: linear-gradient(180deg, rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.4), rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.3)) !important;
|
||||
}
|
||||
:root {
|
||||
--background-hsl-hue: {{ images.0.colours.0 | hsl_hue }};
|
||||
--background-hsl-saturation: {{ images.0.colours.0 | hsl_saturation }}%;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--background-hsl-hue: {{ image.colours.2 | hsl_hue }};
|
||||
--background-hsl-saturation: {{ image.colours.2 | hsl_saturation }}%;
|
||||
}
|
||||
.background::after {
|
||||
background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 0.8),
|
||||
rgba({{ image.colours.1.0 }}, {{ image.colours.1.1 }}, {{ image.colours.1.2 }}, 0.2));
|
||||
|
@ -30,15 +34,23 @@
|
|||
<div class="banner-content">
|
||||
<h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
|
||||
<div class="pill-row">
|
||||
{% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %}
|
||||
<div>
|
||||
<button class="pill-item" onclick="imageFullscreen()"><i class="ph ph-info"></i></button>
|
||||
<a {% if next_url %}class="pill-item" href="{{ next_url }}"{% else %}class="pill-item disabled"{% endif %}>
|
||||
<i class="ph ph-arrow-left"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button class="pill-item" onclick="imageFullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button>
|
||||
<button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
|
||||
{% if image.author.id == current_user.id %}
|
||||
<button class="pill-item pill__critical" onclick="imageShowOptionsPopup(this)"><i class="ph-fill ph-dots-three-outline-vertical"></i></button>
|
||||
<button class="pill-item" onclick="imageShowOptionsPopup(this)"><i class="ph ph-list"></i></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if prev_url %}<div><a class="pill-item" href="{{ prev_url }}"><i class="ph ph-arrow-right"></i></a></div>{% endif %}
|
||||
<div>
|
||||
<a {% if prev_url %}class="pill-item" href="{{ prev_url }}"{% else %}class="pill-item disabled"{% endif %}>
|
||||
<i class="ph ph-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
{% block header %}
|
||||
<div class="banner">
|
||||
{% if user.banner %}
|
||||
<img src="{{ url_for('static', filename='icon.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/>
|
||||
<img src="{{ url_for('api.media', path='banner/' + user.banner) }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='banner.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/>
|
||||
{% endif %}
|
||||
|
|
|
@ -16,50 +16,86 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem">
|
||||
<div class="info-header">
|
||||
<i class="ph ph-info"></i>
|
||||
<h2>Profile Settings</h2>
|
||||
<button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<form method="POST" action="{{ url_for('api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data">
|
||||
<h3>Profile Picture</h3>
|
||||
<input type="file" name="file" tab-index="-1"/>
|
||||
<input type="submit" value="Upload" class="btn-block">
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data">
|
||||
<h3>Username</h3>
|
||||
<input type="text" name="name" class="input-block" value="{{ current_user.username }}" />
|
||||
<input type="submit" value="Upload" class="btn-block"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<details open>
|
||||
<summary>
|
||||
<i class="ph ph-info"></i><h2>Profile</h2><span style="width: 100%"></span>
|
||||
<i class="ph ph-caret-down collapse-indicator"></i>
|
||||
</summary>
|
||||
|
||||
<div class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem">
|
||||
<div class="info-header">
|
||||
<i class="ph ph-info"></i>
|
||||
<h2>Account Settings</h2>
|
||||
<button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<form method="POST" action="" enctype="multipart/form-data">
|
||||
<h3>Email</h3>
|
||||
<input type="text" name="email" class="input-block" value="{{ current_user.email }}" />
|
||||
<input type="submit" value="Upload" class="btn-block"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('settings.account_picture') }}" enctype="multipart/form-data">
|
||||
<h3>Profile Picture</h3>
|
||||
<input type="file" name="file" tab-index="-1"/>
|
||||
<button type="submit" class="btn-block">Change Profile Picture</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('settings.account_banner') }}" enctype="multipart/form-data">
|
||||
<h3>Profile Banner</h3>
|
||||
<input type="file" name="file" tab-index="-1"/>
|
||||
<button type="submit" class="btn-block">Change Profile Banner</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('settings.account_username') }}" enctype="multipart/form-data">
|
||||
<h3>Username</h3>
|
||||
<input type="text" name="name" class="input-block" value="{{ current_user.username }}" />
|
||||
<button type="submit" class="btn-block">Change Username</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>
|
||||
<i class="ph ph-info"></i><h2>Account</h2><span style="width: 100%"></span>
|
||||
<i class="ph ph-caret-down collapse-indicator"></i>
|
||||
</summary>
|
||||
|
||||
<form method="POST" action="{{ url_for('settings.account_email') }}" enctype="multipart/form-data">
|
||||
<h3>Email</h3>
|
||||
<input type="text" name="email" class="input-block" value="{{ current_user.email }}" />
|
||||
<input type="password" name="current" class="input-block" placeholder="Current Password" />
|
||||
<button type="submit" class="btn-block">Change Email</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('settings.account_password') }}" enctype="multipart/form-data">
|
||||
<h3>Password</h3>
|
||||
<input type="password" name="current" class="input-block" placeholder="Current Password" />
|
||||
<input type="password" name="password" class="input-block" placeholder="New Password" />
|
||||
<input type="password" name="confirm" class="input-block" placeholder="Confirm Password" />
|
||||
<button type="submit" class="btn-block">Change Password</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>
|
||||
<i class="ph ph-info"></i><h2>Server</h2><span style="width: 100%"></span>
|
||||
<i class="ph ph-caret-down collapse-indicator"></i>
|
||||
</summary>
|
||||
|
||||
<form method="POST" action="{{ url_for('settings.website_style') }}" enctype="multipart/form-data">
|
||||
<h3>Style</h3>
|
||||
<input type="range" name="hue" class="input-block" placeholder="Website Hue" value="{{ config.WEBSITE_CONF.styling.hue }}" min="0" max="360" />
|
||||
<input type="range" name="saturation" class="input-block" placeholder="Strength of Color" value="{{ config.WEBSITE_CONF.styling.saturation }}" min="0" max="100" />
|
||||
<input type="text" name="rad" class="input-block" placeholder="Roundy roundy" value="{{ config.WEBSITE_CONF.styling.rad }}" />
|
||||
<input type="checkbox" name="force" class="input-block" placeholder="Force styling?" {% if config.WEBSITE_CONF.styling.force %}checked{% endif %} />
|
||||
<button type="submit" class="btn-block">Update style</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<footer>
|
||||
<p>Built on coffee and love, by Fluffy</p>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
let infoTab = document.querySelectorAll('.info-tab');
|
||||
<script>
|
||||
let hue = document.querySelector('input[name=hue]');
|
||||
let saturation = document.querySelector('input[name=saturation]');
|
||||
let rad = document.querySelector('input[name=rad]');
|
||||
|
||||
for (let i = 0; i < infoTab.length; i++) {
|
||||
infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() {
|
||||
infoTab[i].classList.toggle('collapsed');
|
||||
});
|
||||
}
|
||||
hue.addEventListener('input', () => {
|
||||
document.documentElement.style.setProperty('--background-hsl-hue', hue.value, 'important');
|
||||
});
|
||||
saturation.addEventListener('input', () => {
|
||||
document.documentElement.style.setProperty('--background-hsl-saturation', saturation.value + '%', 'important');
|
||||
});
|
||||
rad.addEventListener('input', () => {
|
||||
document.documentElement.style.setProperty('--rad', rad.value, 'important');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ class Colour:
|
|||
s = 0.0
|
||||
else:
|
||||
d = high - low
|
||||
s = d / (2 - high - low) if l > 0.5 else d / (high + low)
|
||||
s = d / (2 - high - low) if low > 0.5 else d / (high + low)
|
||||
h = {
|
||||
r: (g - b) / d + (6 if g < b else 0),
|
||||
g: (b - r) / d + 2,
|
||||
|
|
133
onlylegs/utils/startup.py
Normal file
133
onlylegs/utils/startup.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
"""
|
||||
OnlyLegs - Setup
|
||||
Runs when the app detects that there is no user directory
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import platformdirs
|
||||
import yaml
|
||||
|
||||
|
||||
APPLICATION_ROOT = platformdirs.user_config_dir("onlylegs")
|
||||
REQUIRED_DIRS = {
|
||||
"root": APPLICATION_ROOT,
|
||||
"instance": os.path.join(APPLICATION_ROOT, "instance"),
|
||||
"media": os.path.join(APPLICATION_ROOT, "media"),
|
||||
"uploads": os.path.join(APPLICATION_ROOT, "media", "uploads"),
|
||||
"cache": os.path.join(APPLICATION_ROOT, "media", "cache"),
|
||||
"pfp": os.path.join(APPLICATION_ROOT, "media", "pfp"),
|
||||
"banner": os.path.join(APPLICATION_ROOT, "media", "banner"),
|
||||
}
|
||||
|
||||
EMAIL_REGEX = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
|
||||
USERNAME_REGEX = re.compile(r"\b[A-Za-z0-9._%+-]+\b")
|
||||
|
||||
config = {
|
||||
# Version of the config file
|
||||
"version": "0.1.7",
|
||||
# Not really used much, but good to have for future use
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
},
|
||||
"upload": {
|
||||
"allowed-extensions": {
|
||||
"jpg": "jpeg",
|
||||
"jpeg": "jpeg",
|
||||
"png": "png",
|
||||
"webp": "webp",
|
||||
},
|
||||
# Max size in MB
|
||||
"max-size": 69,
|
||||
# Max images to load per page
|
||||
"max-load": 50,
|
||||
},
|
||||
"website": {
|
||||
# Website name and motto
|
||||
# Also CSS styling, hue is the color offset for hsl
|
||||
"name": "OnlyLegs",
|
||||
"motto": "A gallery built for fast and simple image management!",
|
||||
"styling": {
|
||||
"force": False,
|
||||
"hue": "69",
|
||||
"saturation": "25%",
|
||||
"rad": "0.4rem",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def check_dirs():
|
||||
for directory in REQUIRED_DIRS.values():
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
print("Created directory at:", directory)
|
||||
print("User directory already exists at:", directory)
|
||||
|
||||
|
||||
def check_env():
|
||||
if os.path.exists(os.path.join(APPLICATION_ROOT, ".env")):
|
||||
print("Environment file already exists at:", APPLICATION_ROOT)
|
||||
return
|
||||
|
||||
env_conf = {
|
||||
"FLASK_SECRET": os.urandom(32).hex(),
|
||||
}
|
||||
|
||||
with open(
|
||||
os.path.join(APPLICATION_ROOT, ".env"), encoding="utf-8", mode="w+"
|
||||
) as file:
|
||||
for key, value in env_conf.items():
|
||||
file.write(key + "=" + value + "\n")
|
||||
|
||||
print(
|
||||
"####################################################",
|
||||
"# A NEW KEY WAS GENERATED FOR YOU! PLEASE NOTE #",
|
||||
"# DOWN THE FLASK_SECRET KEY LOCATED IN YOUR #",
|
||||
"# ~/.config/onlylegs/.env FOLDER! LOOSING THIS KEY #",
|
||||
"# WILL RESULT IN YOU BEING UNABLE TO LOG IN! #",
|
||||
"####################################################",
|
||||
sep="\n",
|
||||
)
|
||||
|
||||
|
||||
def check_conf():
|
||||
config_file = os.path.join(APPLICATION_ROOT, "conf.yml")
|
||||
if os.path.exists(config_file):
|
||||
print("Config file already exists at:", APPLICATION_ROOT)
|
||||
return
|
||||
|
||||
cant_continue = True
|
||||
username = "admin"
|
||||
email = "admin@example.com"
|
||||
|
||||
print("No config file found, please enter the following information:")
|
||||
while cant_continue:
|
||||
username = input("Admin username: ").strip()
|
||||
email = input("Admin email: ").strip()
|
||||
|
||||
if not username or not USERNAME_REGEX.match(username):
|
||||
print("Username is invalid!")
|
||||
continue
|
||||
if not email or not EMAIL_REGEX.match(email):
|
||||
print("Email is invalid!")
|
||||
continue
|
||||
|
||||
# Check if user is happy with the values
|
||||
is_correct = input("Is this correct? (Y/n): ").lower().strip()
|
||||
if is_correct == "y" or not is_correct:
|
||||
cant_continue = False
|
||||
|
||||
config["admin"]["username"] = username
|
||||
config["admin"]["email"] = email
|
||||
|
||||
with open(config_file, encoding="utf-8", mode="w+") as file:
|
||||
yaml.dump(config, file, default_flow_style=False)
|
||||
|
||||
print(
|
||||
"####################################################",
|
||||
"# A NEW CONFIG HAS BEEN GENERATED AT: #",
|
||||
"# ~/.config/onlylegs/conf.yml #",
|
||||
"####################################################",
|
||||
sep="\n",
|
||||
)
|
|
@ -1,17 +1,191 @@
|
|||
"""
|
||||
OnlyLegs - Settings page
|
||||
"""
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import logging
|
||||
import yaml
|
||||
from colorthief import ColorThief
|
||||
from flask import (
|
||||
Blueprint,
|
||||
request,
|
||||
current_app,
|
||||
render_template,
|
||||
flash,
|
||||
redirect,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
from onlylegs.extensions import db
|
||||
from onlylegs.models import Users
|
||||
from onlylegs.config import APPLICATION_ROOT
|
||||
|
||||
|
||||
blueprint = Blueprint("settings", __name__, url_prefix="/settings")
|
||||
|
||||
|
||||
@blueprint.route("/")
|
||||
@blueprint.route("/", methods=["GET"])
|
||||
@login_required
|
||||
def general():
|
||||
"""
|
||||
General settings page
|
||||
"""
|
||||
return render_template("settings.html")
|
||||
|
||||
|
||||
@blueprint.route("/account/pfp", methods=["POST"])
|
||||
@login_required
|
||||
def account_picture():
|
||||
user_record = Users.query.filter_by(id=current_user.id).first()
|
||||
uploaded_file = request.files.get("file", None)
|
||||
if not uploaded_file:
|
||||
return "No file uploaded!", 400
|
||||
|
||||
image_mime = pathlib.Path(uploaded_file.filename).suffix.replace(".", "").lower()
|
||||
image_name = str(user_record.id) + "_pfp." + image_mime
|
||||
image_path = os.path.join(current_app.config["PFP_FOLDER"], image_name)
|
||||
|
||||
if image_mime not in current_app.config["ALLOWED_EXTENSIONS"].keys():
|
||||
logging.info("File extension not allowed: %s", image_mime)
|
||||
return "File extension not allowed", 403
|
||||
|
||||
if user_record.picture:
|
||||
os.remove(os.path.join(current_app.config["PFP_FOLDER"], user_record.picture))
|
||||
cache_name = user_record.picture.rsplit(".")[0]
|
||||
for file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
|
||||
cache_name + "*"
|
||||
):
|
||||
os.remove(file)
|
||||
|
||||
uploaded_file.save(image_path)
|
||||
image_colours = ColorThief(image_path).get_color()
|
||||
|
||||
user_record.colour = image_colours
|
||||
user_record.picture = image_name
|
||||
db.session.commit()
|
||||
|
||||
return "File uploaded", 200
|
||||
|
||||
|
||||
@blueprint.route("/account/banner", methods=["POST"])
|
||||
@login_required
|
||||
def account_banner():
|
||||
user_record = Users.query.filter_by(id=current_user.id).first()
|
||||
uploaded_file = request.files.get("file", None)
|
||||
if not uploaded_file:
|
||||
return "No file uploaded!", 400
|
||||
|
||||
image_mime = pathlib.Path(uploaded_file.filename).suffix.replace(".", "").lower()
|
||||
image_name = str(user_record.id) + "_banner." + image_mime
|
||||
image_path = os.path.join(current_app.config["BANNER_FOLDER"], image_name)
|
||||
|
||||
if image_mime not in current_app.config["ALLOWED_EXTENSIONS"].keys():
|
||||
logging.info("File extension not allowed: %s", image_mime)
|
||||
return "File extension not allowed", 403
|
||||
|
||||
if user_record.banner:
|
||||
os.remove(os.path.join(current_app.config["BANNER_FOLDER"], user_record.banner))
|
||||
cache_name = user_record.banner.rsplit(".")[0]
|
||||
for file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
|
||||
cache_name + "*"
|
||||
):
|
||||
os.remove(file)
|
||||
|
||||
uploaded_file.save(image_path)
|
||||
user_record.banner = image_name
|
||||
db.session.commit()
|
||||
|
||||
return "File uploaded", 200
|
||||
|
||||
|
||||
@blueprint.route("/account/username", methods=["POST"])
|
||||
@login_required
|
||||
def account_username():
|
||||
user_record = Users.query.filter_by(id=current_user.id).first()
|
||||
new_username = request.form.get("username", "").strip()
|
||||
|
||||
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
|
||||
|
||||
if not new_username or not username_regex.match(new_username):
|
||||
return "Username is invalid", 400
|
||||
|
||||
user_record.username = new_username
|
||||
db.session.commit()
|
||||
|
||||
return "Username changed", 200
|
||||
|
||||
|
||||
@blueprint.route("/account/email", methods=["POST"])
|
||||
@login_required
|
||||
def account_email():
|
||||
user_record = Users.query.filter_by(id=current_user.id).first()
|
||||
current_password = request.form.get("current", "").strip()
|
||||
new_email = request.form.get("email", "").strip()
|
||||
|
||||
email_regex = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
|
||||
|
||||
if not current_password or not new_email:
|
||||
return "Fill in all the fields!", 400
|
||||
if not email_regex.match(new_email):
|
||||
return "Email is invalid!", 400
|
||||
if not check_password_hash(user_record.password, current_password):
|
||||
return "Incorrect password!", 400
|
||||
|
||||
user_record.email = new_email
|
||||
db.session.commit()
|
||||
|
||||
return "Email changed", 200
|
||||
|
||||
|
||||
@blueprint.route("/account/password", methods=["POST"])
|
||||
@login_required
|
||||
def account_password():
|
||||
user_record = Users.query.filter_by(id=current_user.id).first()
|
||||
current_password = request.form.get("current", "").strip()
|
||||
new_password = request.form.get("password", "").strip()
|
||||
new_confirm = request.form.get("confirm", "").strip()
|
||||
|
||||
if not current_password or not new_password or not new_confirm:
|
||||
return "Fill in all the fields!", 400
|
||||
if new_password != new_confirm:
|
||||
return "Passwords do not match!", 400
|
||||
if not check_password_hash(user_record.password, current_password):
|
||||
return "Incorrect password!", 400
|
||||
|
||||
user_record.password = generate_password_hash(new_password, method="scrypt")
|
||||
db.session.commit()
|
||||
|
||||
flash(["Password changed! You must login now", 0])
|
||||
return redirect(url_for("auth.logout"))
|
||||
|
||||
|
||||
@blueprint.route("/website/style", methods=["POST"])
|
||||
@login_required
|
||||
def website_style():
|
||||
config_file = os.path.join(APPLICATION_ROOT, "conf.yml")
|
||||
|
||||
website_hue = request.form.get("hue", 69, type=int)
|
||||
website_saturation = request.form.get("saturation", 25, type=int)
|
||||
website_rad = request.form.get("rad", "0.4rem").strip()
|
||||
website_force_styling = request.form.get("force", False)
|
||||
|
||||
config = None
|
||||
with open(config_file, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
current_app.config["WEBSITE_CONF"]["styling"]["hue"] = website_hue
|
||||
current_app.config["WEBSITE_CONF"]["styling"]["saturation"] = website_saturation
|
||||
current_app.config["WEBSITE_CONF"]["styling"]["rad"] = website_rad
|
||||
current_app.config["WEBSITE_CONF"]["styling"]["force"] = website_force_styling
|
||||
|
||||
config["website"]["styling"]["hue"] = website_hue
|
||||
config["website"]["styling"]["saturation"] = website_saturation
|
||||
config["website"]["styling"]["rad"] = website_rad
|
||||
config["website"]["styling"]["force"] = website_force_styling
|
||||
|
||||
with open(config_file, "w") as file:
|
||||
yaml.dump(config, file)
|
||||
|
||||
return "Website style changed", 200
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue