From 9821db72c6afc348dc0554444d9426b23e7bedc0 Mon Sep 17 00:00:00 2001
From: Fluffy-Bean <michal-gdula@protonmail.com>
Date: Tue, 26 Sep 2023 13:43:00 +0100
Subject: [PATCH 1/5] Yeet old runner script as it was useless Clean up code
 and styling

---
 onlylegs/app.py                               | 115 ++++++-------
 onlylegs/config.py                            |  19 ++-
 onlylegs/static/js/imagePage.js               |  41 -----
 onlylegs/static/js/uploadTab.js               |  77 +--------
 .../static/sass/components/notification.sass  |  28 +---
 onlylegs/static/sass/style.sass               |   6 +-
 onlylegs/utils/startup.py                     | 156 ++++++++++++++++++
 poetry.lock                                   | 118 ++++++++++++-
 pyproject.toml                                |   1 +
 setup/args.py                                 |  33 ----
 setup/configuration.py                        | 156 ------------------
 setup/runner.py                               |  35 ----
 12 files changed, 348 insertions(+), 437 deletions(-)
 create mode 100644 onlylegs/utils/startup.py
 delete mode 100644 setup/args.py
 delete mode 100644 setup/configuration.py
 delete mode 100644 setup/runner.py

diff --git a/onlylegs/app.py b/onlylegs/app.py
index 0dd4349..75533d7 100644
--- a/onlylegs/app.py
+++ b/onlylegs/app.py
@@ -1,67 +1,55 @@
 """
 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_assets import Bundle
 from flask_migrate import init as migrate_init
-
-from flask import Flask, render_template, abort
+from flask import Flask, render_template, abort, request
 from werkzeug.exceptions import HTTPException
-from werkzeug.security import generate_password_hash
 
+from onlylegs.utils import startup
 from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
-from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
+from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR, APPLICATION_ROOT
 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.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
+
+
+logging.getLogger("werkzeug").disabled = True
+logging.basicConfig(
+    filename=os.path.join(APPLICATION_ROOT, "only.log"),
+    level=logging.INFO,
+    datefmt="%Y-%m-%d %H:%M:%S",
+    format="%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s",
+    encoding="utf-8",
 )
-from onlylegs import api
-from onlylegs import auth as view_auth
-from onlylegs import 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
+# App Sanity Checks
+startup.check_dirs()
+startup.check_env()
+startup.check_conf()
+
 if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
-    print("Creating database")
-    with app.app_context():
-        db.create_all()
+    startup.make_admin_user(app)
+    migrate_init(directory=MIGRATIONS_DIR)
 
-        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()
-
-        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"
@@ -90,40 +78,41 @@ def error_page(err):
     """
     if not isinstance(err, HTTPException):
         abort(500)
-    return (
-        render_template("error.html", error=err.code, msg=err.description),
-        err.code,
-    )
+
+    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
 
 
 # ASSETS
 assets.init_app(app)
 
-scripts = Bundle(
-    "js/*.js", output="gen/js.js", depends="js/*.js"
-)  # filter jsmin is broken :c
-styles = Bundle(
+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", scripts)
-assets.register("styles", styles)
+assets.register("scripts", page_scripts)
+assets.register("styles", page_styling)
 
 # 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)
+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)
 
 # CACHE AND COMPRESS
 cache.init_app(app)
diff --git a/onlylegs/config.py b/onlylegs/config.py
index 06cd271..9bf47a1 100644
--- a/onlylegs/config.py
+++ b/onlylegs/config.py
@@ -9,16 +9,17 @@ from yaml import safe_load
 
 
 # Set dirs
-user_dir = platformdirs.user_config_dir("onlylegs")
-instance_dir = os.path.join(user_dir, "instance")
+APPLICATION_ROOT = platformdirs.user_config_dir("onlylegs")
 
 # Load environment variables
 # print("Loading environment variables...")
-load_dotenv(os.path.join(user_dir, ".env"))
+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:
+with open(
+    os.path.join(APPLICATION_ROOT, "conf.yml"), encoding="utf-8", mode="r"
+) as file:
     conf = safe_load(file)
 
 
@@ -34,13 +35,13 @@ 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")
+UPLOAD_FOLDER = os.path.join(APPLICATION_ROOT, "media", "uploads")
+CACHE_FOLDER = os.path.join(APPLICATION_ROOT, "media", "cache")
+PFP_FOLDER = os.path.join(APPLICATION_ROOT, "media", "pfp")
+MEDIA_FOLDER = os.path.join(APPLICATION_ROOT, "media")
 
 # Database
-INSTANCE_DIR = instance_dir
+INSTANCE_DIR = os.path.join(APPLICATION_ROOT, "instance")
 MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations")
 
 # App
diff --git a/onlylegs/static/js/imagePage.js b/onlylegs/static/js/imagePage.js
index a1eb24c..40209a7 100644
--- a/onlylegs/static/js/imagePage.js
+++ b/onlylegs/static/js/imagePage.js
@@ -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',
diff --git a/onlylegs/static/js/uploadTab.js b/onlylegs/static/js/uploadTab.js
index a27e57f..2526a8a 100644
--- a/onlylegs/static/js/uploadTab.js
+++ b/onlylegs/static/js/uploadTab.js
@@ -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
diff --git a/onlylegs/static/sass/components/notification.sass b/onlylegs/static/sass/components/notification.sass
index feea589..2a182dc 100644
--- a/onlylegs/static/sass/components/notification.sass
+++ b/onlylegs/static/sass/components/notification.sass
@@ -1,23 +1,15 @@
 @keyframes notificationTimeout
     0%
-        left: -100%
-        height: 3px
-    90%
-        left: 0%
-        height: 3px
-    95%
-        left: 0%
-        height: 0
+        width: 0
     100%
-        left: 0%
-        height: 0
+        width: 100%
     
 @mixin notification($color)
     color: RGB($color)
 
     &::after
         background-color: RGB($color)
-    
+
 .notifications
     margin: 0
     padding: 0
@@ -60,17 +52,17 @@
     &::after
         content: ""
 
-        width: 100%
+        width: 0
         height: 3px
 
         position: absolute
-        bottom: 0px
-        left: 0px
+        bottom: 0
+        left: 0
 
         background-color: RGB($fg-white)
 
         z-index: +2
-        animation: notificationTimeout 5.1s linear
+        animation: notificationTimeout 5.1s ease-out forwards
 
     &.success
         @include notification($success)
@@ -89,7 +81,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
@@ -133,10 +125,6 @@
 
     .sniffle__notification
         width: 100%
-
-        &.hide
-            opacity: 0
-            transform: translateY(1rem)
     
     .sniffle__notification-time
         width: 100%
diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass
index c363234..cbc602d 100644
--- a/onlylegs/static/sass/style.sass
+++ b/onlylegs/static/sass/style.sass
@@ -60,16 +60,18 @@ body
         padding: 0 0 3.5rem 0
 
 main
+    margin: 0 0.5rem 0.5rem 0
     display: flex
     flex-direction: column
     position: relative
     background: RGBA($white, 1)
     color: RGB($fg-black)
-    border-top-left-radius: $rad
+    border-radius: $rad
     overflow: hidden
 @media (max-width: $breakpoint)
     main
-        border-top-left-radius: 0
+        margin: 0
+        border-radius: 0
 
 .error-page
     min-height: 100%
diff --git a/onlylegs/utils/startup.py b/onlylegs/utils/startup.py
new file mode 100644
index 0000000..a42730f
--- /dev/null
+++ b/onlylegs/utils/startup.py
@@ -0,0 +1,156 @@
+"""
+OnlyLegs - Setup
+Runs when the app detects that there is no user directory
+"""
+import os
+import re
+import platformdirs
+import yaml
+from werkzeug.security import generate_password_hash
+from onlylegs.extensions import db
+from onlylegs.models import Users
+
+
+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"),
+}
+
+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")
+
+
+def check_dirs():
+    """
+    Create the user directory
+    """
+
+    for directory in REQUIRED_DIRS.values():
+        if os.path.exists(directory):
+            print("User directory already exists at:", directory)
+            return
+        os.makedirs(directory)
+        print("Created directory at:", directory)
+
+
+def check_env():
+    """
+    Create the .env file with default values
+    """
+    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():
+    """
+    Create the YAML config file with default values
+    """
+    if os.path.exists(os.path.join(APPLICATION_ROOT, "conf.yml")):
+        print("Config file already exists at:", APPLICATION_ROOT)
+        return
+
+    can_continue = False
+    username = "admin"
+    name = "Admin"
+    email = "admin@example.com"
+
+    print("No config file found, please enter the following information:")
+    while can_continue:
+        username = input("Admin username: ")
+        name = input("Admin name: ")
+        email = input("Admin email: ")
+
+        if not username or not USERNAME_REGEX.match(username):
+            print("Username is invalid!")
+        if not name:
+            print("Name is invalid!")
+        if not email or not EMAIL_REGEX.match(email):
+            print("Email is invalid!")
+
+        # Check if user is happy with the values
+        is_correct = input("Is this correct? (Y/n): ").lower()
+        if is_correct == "y" or is_correct == "":
+            can_continue = True
+
+    yaml_conf = {
+        "admin": {
+            "name": name,
+            "username": username,
+            "email": email,
+        },
+        "upload": {
+            "allowed-extensions": {
+                "jpg": "jpeg",
+                "jpeg": "jpeg",
+                "png": "png",
+                "webp": "webp",
+            },
+            "max-size": 69,
+            "max-load": 50,
+            "rename": "GWA_{{username}}_{{time}}",
+        },
+        "website": {
+            "name": "OnlyLegs",
+            "motto": "A gallery built for fast and simple image management!",
+            "language": "en",
+        },
+    }
+
+    with open(
+        os.path.join(APPLICATION_ROOT, "conf.yml"), encoding="utf-8", mode="w+"
+    ) as file:
+        yaml.dump(yaml_conf, file, default_flow_style=False)
+
+    print(
+        "####################################################"
+        "# A NEW CONFIG HAS BEEN GENERATED AT:              #"
+        "# ~/.config/onlylegs/conf.yml                      #"
+        "####################################################",
+        sep="\n",
+    )
+
+
+def make_admin_user(app):
+    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(Users(username=username, email=email, password=password))
+        db.session.commit()
+
+        print(
+            "####################################################"
+            "# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #"
+            '# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED,  #'
+            "# PLEASE RESET IT IN THE SETTINGS!                 #"
+            "####################################################",
+            sep="\n",
+        )
diff --git a/poetry.lock b/poetry.lock
index 8c1c926..3246c75 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
 
 [[package]]
 name = "alembic"
@@ -727,6 +727,22 @@ files = [
     {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
 ]
 
+[[package]]
+name = "material-color-utilities-python"
+version = "0.1.5"
+description = "Python port of material-color-utilities used for Material You colors"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "material-color-utilities-python-0.1.5.tar.gz", hash = "sha256:3c6f02e7ce70595885447bdef37cf76fd3628c7c95fa2198d8174c269c951fae"},
+    {file = "material_color_utilities_python-0.1.5-py2.py3-none-any.whl", hash = "sha256:48abd8695a1355ab3ad43fe314ca8664c66282a86fbf94a717571273bf422bdf"},
+]
+
+[package.dependencies]
+Pillow = ">=9.2.0,<10.0.0"
+regex = "*"
+
 [[package]]
 name = "mccabe"
 version = "0.7.0"
@@ -966,6 +982,104 @@ files = [
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
 ]
 
+[[package]]
+name = "regex"
+version = "2023.8.8"
+description = "Alternative regular expression module, to replace re."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "regex-2023.8.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88900f521c645f784260a8d346e12a1590f79e96403971241e64c3a265c8ecdb"},
+    {file = "regex-2023.8.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3611576aff55918af2697410ff0293d6071b7e00f4b09e005d614686ac4cd57c"},
+    {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a0ccc8f2698f120e9e5742f4b38dc944c38744d4bdfc427616f3a163dd9de5"},
+    {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c662a4cbdd6280ee56f841f14620787215a171c4e2d1744c9528bed8f5816c96"},
+    {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf0633e4a1b667bfe0bb10b5e53fe0d5f34a6243ea2530eb342491f1adf4f739"},
+    {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551ad543fa19e94943c5b2cebc54c73353ffff08228ee5f3376bd27b3d5b9800"},
+    {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54de2619f5ea58474f2ac211ceea6b615af2d7e4306220d4f3fe690c91988a61"},
+    {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ec4b3f0aebbbe2fc0134ee30a791af522a92ad9f164858805a77442d7d18570"},
+    {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ae646c35cb9f820491760ac62c25b6d6b496757fda2d51be429e0e7b67ae0ab"},
+    {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca339088839582d01654e6f83a637a4b8194d0960477b9769d2ff2cfa0fa36d2"},
+    {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d9b6627408021452dcd0d2cdf8da0534e19d93d070bfa8b6b4176f99711e7f90"},
+    {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:bd3366aceedf274f765a3a4bc95d6cd97b130d1dda524d8f25225d14123c01db"},
+    {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7aed90a72fc3654fba9bc4b7f851571dcc368120432ad68b226bd593f3f6c0b7"},
+    {file = "regex-2023.8.8-cp310-cp310-win32.whl", hash = "sha256:80b80b889cb767cc47f31d2b2f3dec2db8126fbcd0cff31b3925b4dc6609dcdb"},
+    {file = "regex-2023.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:b82edc98d107cbc7357da7a5a695901b47d6eb0420e587256ba3ad24b80b7d0b"},
+    {file = "regex-2023.8.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e7d84d64c84ad97bf06f3c8cb5e48941f135ace28f450d86af6b6512f1c9a71"},
+    {file = "regex-2023.8.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce0f9fbe7d295f9922c0424a3637b88c6c472b75eafeaff6f910494a1fa719ef"},
+    {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06c57e14ac723b04458df5956cfb7e2d9caa6e9d353c0b4c7d5d54fcb1325c46"},
+    {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7a9aaa5a1267125eef22cef3b63484c3241aaec6f48949b366d26c7250e0357"},
+    {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7408511fca48a82a119d78a77c2f5eb1b22fe88b0d2450ed0756d194fe7a9a"},
+    {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14dc6f2d88192a67d708341f3085df6a4f5a0c7b03dec08d763ca2cd86e9f559"},
+    {file = "regex-2023.8.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c640b99213643d141550326f34f0502fedb1798adb3c9eb79650b1ecb2f177"},
+    {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0085da0f6c6393428bf0d9c08d8b1874d805bb55e17cb1dfa5ddb7cfb11140bf"},
+    {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:964b16dcc10c79a4a2be9f1273fcc2684a9eedb3906439720598029a797b46e6"},
+    {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7ce606c14bb195b0e5108544b540e2c5faed6843367e4ab3deb5c6aa5e681208"},
+    {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:40f029d73b10fac448c73d6eb33d57b34607f40116e9f6e9f0d32e9229b147d7"},
+    {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3b8e6ea6be6d64104d8e9afc34c151926f8182f84e7ac290a93925c0db004bfd"},
+    {file = "regex-2023.8.8-cp311-cp311-win32.whl", hash = "sha256:942f8b1f3b223638b02df7df79140646c03938d488fbfb771824f3d05fc083a8"},
+    {file = "regex-2023.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:51d8ea2a3a1a8fe4f67de21b8b93757005213e8ac3917567872f2865185fa7fb"},
+    {file = "regex-2023.8.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e951d1a8e9963ea51efd7f150450803e3b95db5939f994ad3d5edac2b6f6e2b4"},
+    {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704f63b774218207b8ccc6c47fcef5340741e5d839d11d606f70af93ee78e4d4"},
+    {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22283c769a7b01c8ac355d5be0715bf6929b6267619505e289f792b01304d898"},
+    {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91129ff1bb0619bc1f4ad19485718cc623a2dc433dff95baadbf89405c7f6b57"},
+    {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de35342190deb7b866ad6ba5cbcccb2d22c0487ee0cbb251efef0843d705f0d4"},
+    {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b993b6f524d1e274a5062488a43e3f9f8764ee9745ccd8e8193df743dbe5ee61"},
+    {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3026cbcf11d79095a32d9a13bbc572a458727bd5b1ca332df4a79faecd45281c"},
+    {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:293352710172239bf579c90a9864d0df57340b6fd21272345222fb6371bf82b3"},
+    {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d909b5a3fff619dc7e48b6b1bedc2f30ec43033ba7af32f936c10839e81b9217"},
+    {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3d370ff652323c5307d9c8e4c62efd1956fb08051b0e9210212bc51168b4ff56"},
+    {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:b076da1ed19dc37788f6a934c60adf97bd02c7eea461b73730513921a85d4235"},
+    {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e9941a4ada58f6218694f382e43fdd256e97615db9da135e77359da257a7168b"},
+    {file = "regex-2023.8.8-cp36-cp36m-win32.whl", hash = "sha256:a8c65c17aed7e15a0c824cdc63a6b104dfc530f6fa8cb6ac51c437af52b481c7"},
+    {file = "regex-2023.8.8-cp36-cp36m-win_amd64.whl", hash = "sha256:aadf28046e77a72f30dcc1ab185639e8de7f4104b8cb5c6dfa5d8ed860e57236"},
+    {file = "regex-2023.8.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:423adfa872b4908843ac3e7a30f957f5d5282944b81ca0a3b8a7ccbbfaa06103"},
+    {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ae594c66f4a7e1ea67232a0846649a7c94c188d6c071ac0210c3e86a5f92109"},
+    {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e51c80c168074faa793685656c38eb7a06cbad7774c8cbc3ea05552d615393d8"},
+    {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09b7f4c66aa9d1522b06e31a54f15581c37286237208df1345108fcf4e050c18"},
+    {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e73e5243af12d9cd6a9d6a45a43570dbe2e5b1cdfc862f5ae2b031e44dd95a8"},
+    {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941460db8fe3bd613db52f05259c9336f5a47ccae7d7def44cc277184030a116"},
+    {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f0ccf3e01afeb412a1a9993049cb160d0352dba635bbca7762b2dc722aa5742a"},
+    {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2e9216e0d2cdce7dbc9be48cb3eacb962740a09b011a116fd7af8c832ab116ca"},
+    {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cd9cd7170459b9223c5e592ac036e0704bee765706445c353d96f2890e816c8"},
+    {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4873ef92e03a4309b3ccd8281454801b291b689f6ad45ef8c3658b6fa761d7ac"},
+    {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:239c3c2a339d3b3ddd51c2daef10874410917cd2b998f043c13e2084cb191684"},
+    {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1005c60ed7037be0d9dea1f9c53cc42f836188227366370867222bda4c3c6bd7"},
+    {file = "regex-2023.8.8-cp37-cp37m-win32.whl", hash = "sha256:e6bd1e9b95bc5614a7a9c9c44fde9539cba1c823b43a9f7bc11266446dd568e3"},
+    {file = "regex-2023.8.8-cp37-cp37m-win_amd64.whl", hash = "sha256:9a96edd79661e93327cfeac4edec72a4046e14550a1d22aa0dd2e3ca52aec921"},
+    {file = "regex-2023.8.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2181c20ef18747d5f4a7ea513e09ea03bdd50884a11ce46066bb90fe4213675"},
+    {file = "regex-2023.8.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a2ad5add903eb7cdde2b7c64aaca405f3957ab34f16594d2b78d53b8b1a6a7d6"},
+    {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9233ac249b354c54146e392e8a451e465dd2d967fc773690811d3a8c240ac601"},
+    {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920974009fb37b20d32afcdf0227a2e707eb83fe418713f7a8b7de038b870d0b"},
+    {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2b6c5dfe0929b6c23dde9624483380b170b6e34ed79054ad131b20203a1a63"},
+    {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96979d753b1dc3b2169003e1854dc67bfc86edf93c01e84757927f810b8c3c93"},
+    {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ae54a338191e1356253e7883d9d19f8679b6143703086245fb14d1f20196be9"},
+    {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2162ae2eb8b079622176a81b65d486ba50b888271302190870b8cc488587d280"},
+    {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c884d1a59e69e03b93cf0dfee8794c63d7de0ee8f7ffb76e5f75be8131b6400a"},
+    {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf9273e96f3ee2ac89ffcb17627a78f78e7516b08f94dc435844ae72576a276e"},
+    {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:83215147121e15d5f3a45d99abeed9cf1fe16869d5c233b08c56cdf75f43a504"},
+    {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f7454aa427b8ab9101f3787eb178057c5250478e39b99540cfc2b889c7d0586"},
+    {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0640913d2c1044d97e30d7c41728195fc37e54d190c5385eacb52115127b882"},
+    {file = "regex-2023.8.8-cp38-cp38-win32.whl", hash = "sha256:0c59122ceccb905a941fb23b087b8eafc5290bf983ebcb14d2301febcbe199c7"},
+    {file = "regex-2023.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:c12f6f67495ea05c3d542d119d270007090bad5b843f642d418eb601ec0fa7be"},
+    {file = "regex-2023.8.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82cd0a69cd28f6cc3789cc6adeb1027f79526b1ab50b1f6062bbc3a0ccb2dbc3"},
+    {file = "regex-2023.8.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bb34d1605f96a245fc39790a117ac1bac8de84ab7691637b26ab2c5efb8f228c"},
+    {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:987b9ac04d0b38ef4f89fbc035e84a7efad9cdd5f1e29024f9289182c8d99e09"},
+    {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dd6082f4e2aec9b6a0927202c85bc1b09dcab113f97265127c1dc20e2e32495"},
+    {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7eb95fe8222932c10d4436e7a6f7c99991e3fdd9f36c949eff16a69246dee2dc"},
+    {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7098c524ba9f20717a56a8d551d2ed491ea89cbf37e540759ed3b776a4f8d6eb"},
+    {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b694430b3f00eb02c594ff5a16db30e054c1b9589a043fe9174584c6efa8033"},
+    {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2aeab3895d778155054abea5238d0eb9a72e9242bd4b43f42fd911ef9a13470"},
+    {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:988631b9d78b546e284478c2ec15c8a85960e262e247b35ca5eaf7ee22f6050a"},
+    {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:67ecd894e56a0c6108ec5ab1d8fa8418ec0cff45844a855966b875d1039a2e34"},
+    {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:14898830f0a0eb67cae2bbbc787c1a7d6e34ecc06fbd39d3af5fe29a4468e2c9"},
+    {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f2200e00b62568cfd920127782c61bc1c546062a879cdc741cfcc6976668dfcf"},
+    {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9691a549c19c22d26a4f3b948071e93517bdf86e41b81d8c6ac8a964bb71e5a6"},
+    {file = "regex-2023.8.8-cp39-cp39-win32.whl", hash = "sha256:6ab2ed84bf0137927846b37e882745a827458689eb969028af8032b1b3dac78e"},
+    {file = "regex-2023.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5543c055d8ec7801901e1193a51570643d6a6ab8751b1f7dd9af71af467538bb"},
+    {file = "regex-2023.8.8.tar.gz", hash = "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e"},
+]
+
 [[package]]
 name = "setuptools"
 version = "68.0.0"
@@ -1232,4 +1346,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "96ec0d1f7b512afb05455262fa2de8c4f862bf68fdae513f8552dc30c6e5ab49"
+content-hash = "11735dbbdd45dcc5085a04dbbd4e786dd8aeb4ba743f0c20294861e7b4c35dd0"
diff --git a/pyproject.toml b/pyproject.toml
index 3752e8a..ae79283 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ cssmin = "^0.2.0"
 pylint = "^2.16.3"
 black = "^23.3.0"
 cachetools = "^5.3.0"
+material-color-utilities-python = "^0.1.5"
 
 [build-system]
 requires = ["poetry-core"]
diff --git a/setup/args.py b/setup/args.py
deleted file mode 100644
index a443338..0000000
--- a/setup/args.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""
-Startup arguments for the OnlyLegs gallery
-
-    -p, --port: Port to run on (default: 5000)
-    -a, --address: Address to run on (default: 127.0.0.0)
-    -w, --workers: Number of workers to run (default: 4)
-
-    -d, --debug: Run as Flask app in debug mode (default: False)
-    -S, --scream: Show verbose output (default: False)
-    -h, --help: Show a help message
-"""
-
-import argparse
-
-
-parser = argparse.ArgumentParser(description="Run the OnlyLegs gallery")
-parser.add_argument("-p", "--port", type=int, default=5000, help="Port to run on")
-parser.add_argument(
-    "-a", "--address", type=str, default="127.0.0.0", help="Address to run on"
-)
-parser.add_argument(
-    "-w", "--workers", type=int, default=4, help="Number of workers to run"
-)
-parser.add_argument(
-    "-d", "--debug", action="store_true", help="Run as Flask app in debug mode"
-)
-args = parser.parse_args()
-
-
-PORT = args.port
-ADDRESS = args.address
-WORKERS = args.workers
-DEBUG = args.debug
diff --git a/setup/configuration.py b/setup/configuration.py
deleted file mode 100644
index 2727ce0..0000000
--- a/setup/configuration.py
+++ /dev/null
@@ -1,156 +0,0 @@
-"""
-OnlyLegs - Setup
-Runs when the app detects that there is no user directory
-"""
-import os
-import logging
-import re
-import platformdirs
-import yaml
-
-
-USER_DIR = platformdirs.user_config_dir("onlylegs")
-
-
-class Configuration:
-    """
-    Setup the application on first run
-    """
-
-    def __init__(self):
-        """
-        Main setup function
-        """
-        print("Running startup checks...")
-
-        # Check if the user directory exists
-        if not os.path.exists(USER_DIR):
-            self.make_dir()
-
-        # Check if the .env file exists
-        if not os.path.exists(os.path.join(USER_DIR, ".env")):
-            self.make_env()
-
-        # Check if the conf.yml file exists
-        if not os.path.exists(os.path.join(USER_DIR, "conf.yml")):
-            self.make_yaml()
-
-        # Load the config files
-        self.logging_config()
-
-    @staticmethod
-    def make_dir():
-        """
-        Create the user directory
-        """
-        os.makedirs(USER_DIR)
-        os.makedirs(os.path.join(USER_DIR, "instance"))
-        os.makedirs(os.path.join(USER_DIR, "media"))
-        os.makedirs(os.path.join(USER_DIR, "media", "uploads"))
-        os.makedirs(os.path.join(USER_DIR, "media", "cache"))
-        os.makedirs(os.path.join(USER_DIR, "media", "pfp"))
-
-        print("Created user directory at:", USER_DIR)
-
-    @staticmethod
-    def make_env():
-        """
-        Create the .env file with default values
-        """
-        env_conf = {
-            "FLASK_SECRET": os.urandom(32).hex(),
-        }
-
-        with open(os.path.join(USER_DIR, ".env"), encoding="utf-8", mode="w+") as file:
-            for key, value in env_conf.items():
-                file.write(f"{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!       #
-####################################################
-              """
-        )
-
-    @staticmethod
-    def make_yaml():
-        """
-        Create the YAML config file with default values
-        """
-        is_correct = False
-        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")
-
-        print("\nNo config file found, please enter the following information:")
-        while not is_correct:
-            username = input("Admin username: ")
-            name = input("Admin name: ")
-            email = input("Admin email: ")
-
-            # Check if the values are valid
-            if not username or not username_regex.match(username):
-                print("Username is invalid!")
-                continue
-
-            if not name:
-                print("Name 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
-            if input("Is this correct? (y/n): ").lower() == "y":
-                is_correct = True
-
-        yaml_conf = {
-            "admin": {
-                "name": name,
-                "username": username,
-                "email": email,
-            },
-            "upload": {
-                "allowed-extensions": {
-                    "jpg": "jpeg",
-                    "jpeg": "jpeg",
-                    "png": "png",
-                    "webp": "webp",
-                },
-                "max-size": 69,
-                "max-load": 50,
-                "rename": "GWA_{{username}}_{{time}}",
-            },
-            "website": {
-                "name": "OnlyLegs",
-                "motto": "A gallery built for fast and simple image management!",
-                "language": "en",
-            },
-        }
-
-        with open(
-            os.path.join(USER_DIR, "conf.yml"), encoding="utf-8", mode="w+"
-        ) as file:
-            yaml.dump(yaml_conf, file, default_flow_style=False)
-
-        print(
-            "Generated config file, you can change these values in the settings of the app"
-        )
-
-    @staticmethod
-    def logging_config():
-        """
-        Set the logging config
-        """
-        logging.getLogger("werkzeug").disabled = True
-        logging.basicConfig(
-            filename=os.path.join(USER_DIR, "only.log"),
-            level=logging.INFO,
-            datefmt="%Y-%m-%d %H:%M:%S",
-            format="%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s",
-            encoding="utf-8",
-        )
diff --git a/setup/runner.py b/setup/runner.py
deleted file mode 100644
index 94cd827..0000000
--- a/setup/runner.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""
-Gunicorn configuration file
-"""
-from gunicorn.app.base import Application
-from gunicorn import util
-
-
-class OnlyLegs(Application):
-    """
-    Gunicorn application
-    """
-
-    # TODO: Make this not shit, thanks
-    def __init__(self, options={}):  # skipcq: PYL-W0231 # pylint: disable=W0231
-        self.usage = None
-        self.callable = None
-        self.options = options
-        self.do_load_config()
-
-    def init(self, *args):
-        """
-        Initialize the application
-        """
-        cfg = {}
-        for setting, value in self.options.items():
-            if setting.lower() in self.cfg.settings and value is not None:
-                cfg[setting.lower()] = value
-        return cfg
-
-    @staticmethod
-    def prog():  # skipcq: PYL-E0202 # pylint: disable=E0202, C0116
-        return "OnlyLegs"
-
-    def load(self):
-        return util.import_app("onlylegs.app:app")

From 1a59e413a99087ec5475739df4da0b87bcdf0708 Mon Sep 17 00:00:00 2001
From: Fluffy-Bean <michal-gdula@protonmail.com>
Date: Tue, 26 Sep 2023 19:36:49 +0100
Subject: [PATCH 2/5] Move colours to HSL, probably a mistake

---
 onlylegs/app.py                               | 226 ++++---
 onlylegs/config.py                            |  10 +-
 onlylegs/filters.py                           |  25 +
 onlylegs/models.py                            |   2 +-
 onlylegs/static/js/imagePage.js               |   6 +-
 onlylegs/static/sass/components/banner.sass   |  31 +-
 .../static/sass/components/buttons/block.sass | 123 ++--
 .../sass/components/buttons/info-button.sass  |  39 --
 .../static/sass/components/buttons/pill.sass  |  35 +-
 .../sass/components/buttons/top-of-page.sass  |  10 +-
 .../static/sass/components/context-menu.sass  |  22 +-
 onlylegs/static/sass/components/gallery.sass  |  36 +-
 .../static/sass/components/image-view.sass    |  33 +-
 .../static/sass/components/navigation.sass    |  16 +-
 .../static/sass/components/notification.sass  |  34 +-
 onlylegs/static/sass/components/pop-up.sass   |  18 +-
 onlylegs/static/sass/components/settings.sass |   8 +-
 onlylegs/static/sass/components/tags.sass     |   8 +-
 .../static/sass/components/upload-panel.sass  |  38 +-
 onlylegs/static/sass/style.sass               |  37 +-
 onlylegs/static/sass/variables.sass           | 104 ++--
 onlylegs/templates/base.html                  |   7 +
 onlylegs/templates/group.html                 |  18 +-
 onlylegs/templates/image.html                 |   4 +-
 onlylegs/templates/settings.html              |  66 +--
 onlylegs/utils/colour.py                      |   2 +-
 onlylegs/utils/startup.py                     |  60 +-
 poetry.lock                                   | 558 +++++++++++-------
 pyproject.toml                                |   6 +-
 29 files changed, 852 insertions(+), 730 deletions(-)
 delete mode 100644 onlylegs/static/sass/components/buttons/info-button.sass

diff --git a/onlylegs/app.py b/onlylegs/app.py
index 75533d7..5e1e632 100644
--- a/onlylegs/app.py
+++ b/onlylegs/app.py
@@ -5,14 +5,20 @@ 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, request
-from werkzeug.exceptions import HTTPException
 
-from onlylegs.utils import startup
+
 from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
-from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR, APPLICATION_ROOT
+from onlylegs.config import (
+    INSTANCE_DIR,
+    MIGRATIONS_DIR,
+    APPLICATION_ROOT,
+    DATABASE_NAME,
+)
 from onlylegs.models import Users
 
 from onlylegs.views.index import blueprint as view_index
@@ -25,14 +31,130 @@ from onlylegs.auth import blueprint as view_auth
 from onlylegs.filters import blueprint as filters
 
 
-logging.getLogger("werkzeug").disabled = True
-logging.basicConfig(
-    filename=os.path.join(APPLICATION_ROOT, "only.log"),
-    level=logging.INFO,
-    datefmt="%Y-%m-%d %H:%M:%S",
-    format="%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s",
-    encoding="utf-8",
-)
+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)
@@ -41,85 +163,17 @@ app.config.from_pyfile("config.py")
 db.init_app(app)
 migrate.init_app(app, db, directory=MIGRATIONS_DIR)
 
-# App Sanity Checks
-startup.check_dirs()
-startup.check_env()
-startup.check_conf()
+create_db()
 
-if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
-    startup.make_admin_user(app)
-    migrate_init(directory=MIGRATIONS_DIR)
+set_logger()
+set_login_manager()
+page_assets()
+handle_errors()
+register_blueprints()
 
-
-# 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)
-
-    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
-
-
-# ASSETS
-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)
-
-# 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)
-
-# CACHE AND COMPRESS
 cache.init_app(app)
 compress.init_app(app)
 
-# Yupee! We got there :3
-print("Done!")
 logging.info("Gallery started successfully!")
 
 
diff --git a/onlylegs/config.py b/onlylegs/config.py
index 9bf47a1..2b792d4 100644
--- a/onlylegs/config.py
+++ b/onlylegs/config.py
@@ -6,6 +6,13 @@ 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
@@ -25,7 +32,8 @@ with open(
 
 # Flask config
 SECRET_KEY = os.environ.get("FLASK_SECRET")
-SQLALCHEMY_DATABASE_URI = "sqlite:///gallery.sqlite3"
+DATABASE_NAME = "gallery.sqlite3"
+SQLALCHEMY_DATABASE_URI = "sqlite:///" + DATABASE_NAME
 MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"]
 ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"]
 
diff --git a/onlylegs/filters.py b/onlylegs/filters.py
index 765003b..bdecca5 100644
--- a/onlylegs/filters.py
+++ b/onlylegs/filters.py
@@ -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__)
@@ -18,3 +19,27 @@ def colour_contrast(colour):
     """
     colour_obj = colour_utils.Colour(colour)
     return "rgb(var(--fg-black));" if colour_obj.is_light() else "rgb(var(--fg-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
diff --git a/onlylegs/models.py b/onlylegs/models.py
index 565b130..2aca5c0 100644
--- a/onlylegs/models.py
+++ b/onlylegs/models.py
@@ -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):
diff --git a/onlylegs/static/js/imagePage.js b/onlylegs/static/js/imagePage.js
index 40209a7..43dd207 100644
--- a/onlylegs/static/js/imagePage.js
+++ b/onlylegs/static/js/imagePage.js
@@ -21,7 +21,8 @@ function imageShowOptionsPopup(obj) {
                 dissmissContextMenu();
                 imageEditPopup();
             },
-            'type': 'critical'
+            'type': 'critical',
+            'icon': '<i class="ph-fill ph-pencil"></i>'
         },
         {
             'value': 'Delete',
@@ -29,7 +30,8 @@ function imageShowOptionsPopup(obj) {
                 dissmissContextMenu();
                 imageDeletePopup();
             },
-            'type': 'critical'
+            'type': 'critical',
+            'icon': '<i class="ph-fill ph-trash"></i>'
         }
     ], 'button')
 }
diff --git a/onlylegs/static/sass/components/banner.sass b/onlylegs/static/sass/components/banner.sass
index 037e38d..a88b2c3 100644
--- a/onlylegs/static/sass/components/banner.sass
+++ b/onlylegs/static/sass/components/banner.sass
@@ -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)
 
         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: var(--breakpoint))
     .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 bottom, 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
diff --git a/onlylegs/static/sass/components/buttons/block.sass b/onlylegs/static/sass/components/buttons/block.sass
index f16f96e..121aba2 100644
--- a/onlylegs/static/sass/components/buttons/block.sass
+++ b/onlylegs/static/sass/components/buttons/block.sass
@@ -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(--white)
     &.success
-        @include btn-block($success)
+        background-color: var(--success-transparent)
+        color: var(--success)
+
+        &:hover, &:focus-visible
+            background-color: var(--success)
+            color: var(--white)
+    &.warning
+        background-color: var(--warning-transparent)
+        color: var(--warning)
+
+        &:hover, &:focus-visible
+            background-color: var(--warning)
+            color: var(--white)
+    &.critical
+        background-color: var(--danger-transparent)
+        color: var(--danger)
+
+        &:hover, &:focus-visible
+            background-color: var(--danger)
+            color: var(--white)
     &.info
-        @include btn-block($info)
+        background-color: var(--info-transparent)
+        color: var(--info)
+
+        &:hover, &:focus-visible
+            background-color: var(--info)
+            color: var(--white)
     &.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(--danger)
diff --git a/onlylegs/static/sass/components/buttons/info-button.sass b/onlylegs/static/sass/components/buttons/info-button.sass
deleted file mode 100644
index 4805c59..0000000
--- a/onlylegs/static/sass/components/buttons/info-button.sass
+++ /dev/null
@@ -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
diff --git a/onlylegs/static/sass/components/buttons/pill.sass b/onlylegs/static/sass/components/buttons/pill.sass
index 3767c01..f63b02e 100644
--- a/onlylegs/static/sass/components/buttons/pill.sass
+++ b/onlylegs/static/sass/components/buttons/pill.sass
@@ -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,7 +57,7 @@
 
     border: none
     background-color: transparent
-    color: RGB($fg-white)
+    color: var(--foreground-white)
 
     i
         font-size: 1.25rem
@@ -66,34 +65,34 @@
     &:hover
         cursor: pointer
 
-        color: RGB($primary)
+        color: var(--primary)
 
     &.disabled, &:disabled
-        color: RGB($fg-dim)
+        color: var(--foreground-dim)
         cursor: unset
 
 .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
\ No newline at end of file
diff --git a/onlylegs/static/sass/components/buttons/top-of-page.sass b/onlylegs/static/sass/components/buttons/top-of-page.sass
index 918356d..1ce9cdb 100644
--- a/onlylegs/static/sass/components/buttons/top-of-page.sass
+++ b/onlylegs/static/sass/components/buttons/top-of-page.sass
@@ -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-300)
+    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: var(--breakpoint))
     .top-of-page
         bottom: 4.25rem
diff --git a/onlylegs/static/sass/components/context-menu.sass b/onlylegs/static/sass/components/context-menu.sass
index 15ab78b..0a58aed 100644
--- a/onlylegs/static/sass/components/context-menu.sass
+++ b/onlylegs/static/sass/components/context-menu.sass
@@ -25,8 +25,8 @@
     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
@@ -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
diff --git a/onlylegs/static/sass/components/gallery.sass b/onlylegs/static/sass/components/gallery.sass
index 87d63ca..7194f63 100644
--- a/onlylegs/static/sass/components/gallery.sass
+++ b/onlylegs/static/sass/components/gallery.sass
@@ -12,22 +12,17 @@
     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
@@ -48,7 +43,7 @@
         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
@@ -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,9 +159,9 @@
             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)
 
diff --git a/onlylegs/static/sass/components/image-view.sass b/onlylegs/static/sass/components/image-view.sass
index 1439670..1edcd8a 100644
--- a/onlylegs/static/sass/components/image-view.sass
+++ b/onlylegs/static/sass/components/image-view.sass
@@ -5,7 +5,7 @@
     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
@@ -79,7 +79,7 @@ details
         margin: 0
         padding: 0
 
-        color: RGB($primary)
+        color: var(--primary)
 
         cursor: pointer
         text-decoration: none
@@ -91,7 +91,7 @@ details
         width: 1.1rem
         height: 1.1rem
 
-        border-radius: $rad-inner
+        border-radius: calc(var(--rad) / 2)
 
         object-fit: cover
 
@@ -154,7 +154,7 @@ details
         justify-content: center
         align-items: center
 
-        border-radius: $rad-inner
+        border-radius: calc(var(--rad) / 2)
         border: none
 
         i
@@ -197,7 +197,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 +209,7 @@ details
             border-radius: 0
 @media (max-width: 1100px)
     .image-container
+        padding: 0 0 0.5rem 0
         position: relative
         left: 0
 
@@ -218,17 +221,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 +243,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
diff --git a/onlylegs/static/sass/components/navigation.sass b/onlylegs/static/sass/components/navigation.sass
index 760a7d3..a3d8cd9 100644
--- a/onlylegs/static/sass/components/navigation.sass
+++ b/onlylegs/static/sass/components/navigation.sass
@@ -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: var(--breakpoint))
     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
diff --git a/onlylegs/static/sass/components/notification.sass b/onlylegs/static/sass/components/notification.sass
index 2a182dc..d2c74b0 100644
--- a/onlylegs/static/sass/components/notification.sass
+++ b/onlylegs/static/sass/components/notification.sass
@@ -3,12 +3,6 @@
         width: 0
     100%
         width: 100%
-    
-@mixin notification($color)
-    color: RGB($color)
-
-    &::after
-        background-color: RGB($color)
 
 .notifications
     margin: 0
@@ -38,9 +32,9 @@
 
     position: relative
 
-    background-color: RGB($bg-300)
-    border-radius: $rad-inner
-    color: RGB($fg-white)
+    background-color: var(--background-300)
+    border-radius: calc(var(--rad) / 2)
+    color: var(--foreground-white)
     opacity: 0
     transform: scale(0.8)
 
@@ -59,19 +53,27 @@
         bottom: 0
         left: 0
 
-        background-color: RGB($fg-white)
+        background-color: var(--foreground-white)
 
         z-index: +2
         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
@@ -95,7 +97,7 @@
     justify-content: center
     align-items: center
 
-    background-color: RGB($bg-200)
+    background-color: var(--background-200)
 
     i
         font-size: 1.25rem
@@ -117,7 +119,7 @@
     line-height: 1
     text-align: left
 
-@media (max-width: $breakpoint)
+@media (max-width: var(--breakpoint))
     .notifications
         bottom: 3.8rem
         width: calc(100vw - 0.6rem)
diff --git a/onlylegs/static/sass/components/pop-up.sass b/onlylegs/static/sass/components/pop-up.sass
index 0e6974b..565f63b 100644
--- a/onlylegs/static/sass/components/pop-up.sass
+++ b/onlylegs/static/sass/components/pop-up.sass
@@ -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-200)
+        border-radius: var(--rad)
         overflow: hidden
 
         z-index: +2
-        transition: transform 0.2s $animation-smooth
+        transition: transform 0.2s var(--animation-smooth)
 
     .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: var(--breakpoint))
     .pop-up
         .pop-up-wrapper
             max-width: calc(100% - 0.75rem)
diff --git a/onlylegs/static/sass/components/settings.sass b/onlylegs/static/sass/components/settings.sass
index 8587a76..1bf56e0 100644
--- a/onlylegs/static/sass/components/settings.sass
+++ b/onlylegs/static/sass/components/settings.sass
@@ -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
diff --git a/onlylegs/static/sass/components/tags.sass b/onlylegs/static/sass/components/tags.sass
index 6da6e67..346918a 100644
--- a/onlylegs/static/sass/components/tags.sass
+++ b/onlylegs/static/sass/components/tags.sass
@@ -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)
diff --git a/onlylegs/static/sass/components/upload-panel.sass b/onlylegs/static/sass/components/upload-panel.sass
index ed090f0..04bb941 100644
--- a/onlylegs/static/sass/components/upload-panel.sass
+++ b/onlylegs/static/sass/components/upload-panel.sass
@@ -9,7 +9,7 @@
     height: 100vh
 
     background-color: transparent
-    color: RGB($fg-white)
+    color: var(--foreground-white)
 
     overflow: hidden
     z-index: 68
@@ -66,7 +66,7 @@
         flex-direction: column
         gap: 1rem
 
-        background-color: RGB($bg-200)
+        background-color: var(--background-200)
 
         z-index: +2
 
@@ -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-bounce)
 
         &.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,7 +159,7 @@
                 font-size: 1rem
                 font-weight: 600
 
-                color: RGB($fg-white)
+                color: var(--foreground-white)
 
                 z-index: +3
 
@@ -167,13 +167,13 @@
 
             .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
 
@@ -182,28 +182,28 @@
 
             &.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: var(--breakpoint))
     .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
diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass
index cbc602d..e607c97 100644
--- a/onlylegs/static/sass/style.sass
+++ b/onlylegs/static/sass/style.sass
@@ -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,27 +51,30 @@ 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: var(--breakpoint))
     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-radius: $rad
+    background: var(--background-800)
+    color: var(--foreground-black)
+    border-radius: var(--rad)
     overflow: hidden
-@media (max-width: $breakpoint)
+@media (max-width: var(--breakpoint))
     main
-        margin: 0
-        border-radius: 0
+        margin: 0 0.5rem
+        // border-radius: 0
 
 .error-page
     min-height: 100%
@@ -88,7 +91,7 @@ main
         font-weight: 900
         text-align: center
 
-        color: $primary
+        color: var(--primary)
 
     p
         margin: 0 2rem
@@ -97,7 +100,7 @@ main
         font-size: 1.25rem
         font-weight: 400
         text-align: center
-@media (max-width: $breakpoint)
+@media (max-width: var(--breakpoint))
     .error-page
         h1
             font-size: 4.5rem
diff --git a/onlylegs/static/sass/variables.sass b/onlylegs/static/sass/variables.sass
index e3fcd83..754caa2 100644
--- a/onlylegs/static/sass/variables.sass
+++ b/onlylegs/static/sass/variables.sass
@@ -1,80 +1,50 @@
-$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: 0
+    --background-hsl-saturation: 2%
 
-    --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)
 
-    --primary: var(--green)  // 183, 169, 151
+    --foreground-gray: rgb(102, 102, 102)
+    --foreground-white: rgb(232, 227, 227)
+    --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(217, 188, 140)
+    --yellow-transparent: rgba(217, 188, 140, 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)
     --warning: var(--orange)
-    --critical: var(--red)
+    --danger: var(--red)
     --success: var(--green)
     --info: var(--blue)
 
-    --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: 'Rubik', sans-serif
diff --git a/onlylegs/templates/base.html b/onlylegs/templates/base.html
index b580c7f..58b7bb2 100644
--- a/onlylegs/templates/base.html
+++ b/onlylegs/templates/base.html
@@ -29,6 +29,13 @@
 
     {% 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 type="text/css">
+        :root{
+            --primary: {{ config.WEBSITE_CONF.primary_color | default('rgb(198, 185, 166)') }};
+        }
+    </style>
+
     {% block head %}{% endblock %}
 </head>
 <body>
diff --git a/onlylegs/templates/group.html b/onlylegs/templates/group.html
index 67cfb1f..077ea93 100644
--- a/onlylegs/templates/group.html
+++ b/onlylegs/templates/group.html
@@ -20,30 +20,29 @@
 
     <style>
         {% if images %}
-            :root { --bg-100: {{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }} }
+            :root {
+                --background-hsl-hue: {{ images.0.colours.0 | hsl_hue }};
+                --background-hsl-saturation: {{ images.0.colours.0 | hsl_saturation }}%;
+            }
 
+            /*
             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;
             }
 
-
-            .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;
             }
 
@@ -55,6 +54,7 @@
                     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;
                 }
             }
+            */
         {% endif %}
     </style>
 {% endblock %}
diff --git a/onlylegs/templates/image.html b/onlylegs/templates/image.html
index edb8a12..2070353 100644
--- a/onlylegs/templates/image.html
+++ b/onlylegs/templates/image.html
@@ -32,10 +32,10 @@
             <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>
+                    <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 %}
diff --git a/onlylegs/templates/settings.html b/onlylegs/templates/settings.html
index 10840cb..88216b4 100644
--- a/onlylegs/templates/settings.html
+++ b/onlylegs/templates/settings.html
@@ -16,50 +16,34 @@
 {% 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">
+        <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>
+    </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="" 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>
-{% endblock %}
-
-{% block script %}
-    <script type="text/javascript">
-        let infoTab = document.querySelectorAll('.info-tab');
-
-        for (let i = 0; i < infoTab.length; i++) {
-            infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() {
-                infoTab[i].classList.toggle('collapsed');
-            });
-        }
-    </script>
+    </details>
 {% endblock %}
diff --git a/onlylegs/utils/colour.py b/onlylegs/utils/colour.py
index 4c9c8b0..f257637 100644
--- a/onlylegs/utils/colour.py
+++ b/onlylegs/utils/colour.py
@@ -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,
diff --git a/onlylegs/utils/startup.py b/onlylegs/utils/startup.py
index a42730f..722b37f 100644
--- a/onlylegs/utils/startup.py
+++ b/onlylegs/utils/startup.py
@@ -6,12 +6,9 @@ import os
 import re
 import platformdirs
 import yaml
-from werkzeug.security import generate_password_hash
-from onlylegs.extensions import db
-from onlylegs.models import Users
 
 
-APPLICATION_ROOT = platformdirs.user_config_dir("onlyLegs")
+APPLICATION_ROOT = platformdirs.user_config_dir("onlylegs")
 REQUIRED_DIRS = {
     "root": APPLICATION_ROOT,
     "instance": os.path.join(APPLICATION_ROOT, "instance"),
@@ -57,11 +54,11 @@ def check_env():
             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!       #"
+        "####################################################",
+        "# 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",
     )
@@ -75,28 +72,31 @@ def check_conf():
         print("Config file already exists at:", APPLICATION_ROOT)
         return
 
-    can_continue = False
+    cant_continue = True
     username = "admin"
     name = "Admin"
     email = "admin@example.com"
 
     print("No config file found, please enter the following information:")
-    while can_continue:
-        username = input("Admin username: ")
-        name = input("Admin name: ")
-        email = input("Admin email: ")
+    while cant_continue:
+        username = input("Admin username: ").strip()
+        name = input("Admin name: ").strip()
+        email = input("Admin email: ").strip()
 
         if not username or not USERNAME_REGEX.match(username):
             print("Username is invalid!")
+            continue
         if not name:
             print("Name 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()
-        if is_correct == "y" or is_correct == "":
-            can_continue = True
+        is_correct = input("Is this correct? (Y/n): ").lower().strip()
+        if is_correct == "y" or not is_correct:
+            cant_continue = False
 
     yaml_conf = {
         "admin": {
@@ -128,29 +128,9 @@ def check_conf():
         yaml.dump(yaml_conf, file, default_flow_style=False)
 
     print(
-        "####################################################"
-        "# A NEW CONFIG HAS BEEN GENERATED AT:              #"
-        "# ~/.config/onlylegs/conf.yml                      #"
+        "####################################################",
+        "# A NEW CONFIG HAS BEEN GENERATED AT:              #",
+        "# ~/.config/onlylegs/conf.yml                      #",
         "####################################################",
         sep="\n",
     )
-
-
-def make_admin_user(app):
-    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(Users(username=username, email=email, password=password))
-        db.session.commit()
-
-        print(
-            "####################################################"
-            "# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #"
-            '# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED,  #'
-            "# PLEASE RESET IT IN THE SETTINGS!                 #"
-            "####################################################",
-            sep="\n",
-        )
diff --git a/poetry.lock b/poetry.lock
index 3246c75..2bf4c0d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,14 +2,14 @@
 
 [[package]]
 name = "alembic"
-version = "1.11.2"
+version = "1.12.0"
 description = "A database migration tool for SQLAlchemy."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"},
-    {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"},
+    {file = "alembic-1.12.0-py3-none-any.whl", hash = "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f"},
+    {file = "alembic-1.12.0.tar.gz", hash = "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b"},
 ]
 
 [package.dependencies]
@@ -24,14 +24,14 @@ tz = ["python-dateutil"]
 
 [[package]]
 name = "astroid"
-version = "2.15.6"
+version = "2.15.8"
 description = "An abstract syntax tree for Python with inference support."
 category = "main"
 optional = false
 python-versions = ">=3.7.2"
 files = [
-    {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"},
-    {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"},
+    {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"},
+    {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"},
 ]
 
 [package.dependencies]
@@ -44,34 +44,34 @@ wrapt = [
 
 [[package]]
 name = "black"
-version = "23.7.0"
+version = "23.9.1"
 description = "The uncompromising code formatter."
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"},
-    {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"},
-    {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"},
-    {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"},
-    {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"},
-    {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"},
-    {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"},
-    {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"},
-    {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"},
-    {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"},
-    {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"},
-    {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"},
-    {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"},
-    {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"},
-    {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"},
-    {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"},
-    {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"},
-    {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"},
-    {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"},
-    {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"},
-    {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"},
-    {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"},
+    {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"},
+    {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"},
+    {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"},
+    {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"},
+    {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"},
+    {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"},
+    {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"},
+    {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"},
+    {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"},
+    {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"},
+    {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"},
+    {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"},
+    {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"},
+    {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"},
+    {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"},
+    {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"},
+    {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"},
+    {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"},
+    {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"},
+    {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"},
+    {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"},
+    {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"},
 ]
 
 [package.dependencies]
@@ -81,7 +81,7 @@ packaging = ">=22.0"
 pathspec = ">=0.9.0"
 platformdirs = ">=2"
 tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
 
 [package.extras]
 colorama = ["colorama (>=0.4.3)"]
@@ -103,96 +103,137 @@ files = [
 
 [[package]]
 name = "brotli"
-version = "1.0.9"
+version = "1.1.0"
 description = "Python bindings for the Brotli compression library"
 category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
-    {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
-    {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"},
-    {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"},
-    {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"},
-    {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"},
-    {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"},
-    {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"},
-    {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"},
-    {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"},
-    {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"},
-    {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"},
-    {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"},
-    {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"},
-    {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"},
-    {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"},
-    {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc0283a406774f465fb45ec7efb66857c09ffefbe49ec20b7882eff6d3c86d3a"},
-    {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11d3283d89af7033236fa4e73ec2cbe743d4f6a81d41bd234f24bf63dde979df"},
-    {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1306004d49b84bd0c4f90457c6f57ad109f5cc6067a9664e12b7b79a9948ad"},
-    {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1375b5d17d6145c798661b67e4ae9d5496920d9265e2f00f1c2c0b5ae91fbde"},
-    {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cab1b5964b39607a66adbba01f1c12df2e55ac36c81ec6ed44f2fca44178bf1a"},
-    {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ed6a5b3d23ecc00ea02e1ed8e0ff9a08f4fc87a1f58a2530e71c0f48adf882f"},
-    {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cb02ed34557afde2d2da68194d12f5719ee96cfb2eacc886352cb73e3808fc5d"},
-    {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b3523f51818e8f16599613edddb1ff924eeb4b53ab7e7197f85cbc321cdca32f"},
-    {file = "Brotli-1.0.9-cp311-cp311-win32.whl", hash = "sha256:ba72d37e2a924717990f4d7482e8ac88e2ef43fb95491eb6e0d124d77d2a150d"},
-    {file = "Brotli-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:3ffaadcaeafe9d30a7e4e1e97ad727e4f5610b9fa2f7551998471e3736738679"},
-    {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
-    {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
-    {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
-    {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"},
-    {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"},
-    {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"},
-    {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
-    {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
-    {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"},
-    {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"},
-    {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"},
-    {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"},
-    {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
-    {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
-    {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
-    {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
-    {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
-    {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"},
-    {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"},
-    {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"},
-    {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"},
-    {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
-    {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
-    {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"},
-    {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"},
-    {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
-    {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
-    {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"},
-    {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"},
-    {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"},
-    {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"},
-    {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
-    {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
-    {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"},
-    {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"},
-    {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
-    {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
-    {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"},
-    {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"},
-    {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"},
-    {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"},
-    {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
-    {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
-    {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"},
-    {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73fd30d4ce0ea48010564ccee1a26bfe39323fde05cb34b5863455629db61dc7"},
-    {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02177603aaca36e1fd21b091cb742bb3b305a569e2402f1ca38af471777fb019"},
-    {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"},
-    {file = "Brotli-1.0.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b43775532a5904bc938f9c15b77c613cb6ad6fb30990f3b0afaea82797a402d8"},
-    {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bf37a08493232fbb0f8229f1824b366c2fc1d02d64e7e918af40acd15f3e337"},
-    {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:330e3f10cd01da535c70d09c4283ba2df5fb78e915bea0a28becad6e2ac010be"},
-    {file = "Brotli-1.0.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e1abbeef02962596548382e393f56e4c94acd286bd0c5afba756cffc33670e8a"},
-    {file = "Brotli-1.0.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3148362937217b7072cf80a2dcc007f09bb5ecb96dae4617316638194113d5be"},
-    {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336b40348269f9b91268378de5ff44dc6fbaa2268194f85177b53463d313842a"},
-    {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b09a16a1950b9ef495a0f8b9d0a87599a9d1f179e2d4ac014b2ec831f87e7"},
-    {file = "Brotli-1.0.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c8e521a0ce7cf690ca84b8cc2272ddaf9d8a50294fd086da67e517439614c755"},
-    {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
+    {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"},
+    {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"},
+    {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"},
+    {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"},
+    {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"},
+    {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
+    {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
+    {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
+    {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
+    {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
+    {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"},
+    {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"},
+    {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"},
+    {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"},
+    {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
+    {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
+    {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
+    {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
+    {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
+    {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
+    {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
+    {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"},
+    {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"},
+    {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
+    {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
+    {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
+    {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
+    {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
+    {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
+    {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
+    {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"},
+    {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
+    {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
+    {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
+    {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
+    {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
+    {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"},
+    {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"},
+    {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"},
+    {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
+    {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
+    {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
+    {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
+    {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
+    {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"},
+    {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"},
+    {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"},
+    {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"},
+    {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
+    {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
+    {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
+    {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
+    {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
+    {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"},
+    {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"},
+    {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"},
+    {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"},
+    {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
+    {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
+    {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
+    {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
+    {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
 ]
 
+[[package]]
+name = "brotlicffi"
+version = "1.1.0.0"
+description = "Python CFFI bindings to the Brotli library"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"},
+    {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"},
+    {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"},
+    {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"},
+    {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"},
+    {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"},
+    {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"},
+    {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"},
+    {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"},
+    {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"},
+    {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"},
+    {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"},
+    {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"},
+    {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"},
+    {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"},
+    {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"},
+    {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"},
+    {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"},
+    {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"},
+    {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"},
+    {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"},
+    {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"},
+    {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"},
+    {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"},
+    {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"},
+    {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"},
+    {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"},
+]
+
+[package.dependencies]
+cffi = ">=1.0.0"
+
 [[package]]
 name = "cachetools"
 version = "5.3.1"
@@ -205,16 +246,93 @@ files = [
     {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"},
 ]
 
+[[package]]
+name = "cffi"
+version = "1.15.1"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
+    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
+    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
+    {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
+    {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
+    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
+    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
+    {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
+    {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
+    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
+    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
+    {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
+    {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
+    {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
+    {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
+    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
+    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
+    {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
+    {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
+    {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
+    {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
+    {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
+    {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
+    {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
+    {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
+    {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
+    {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
+    {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
+    {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
+    {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
+    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
+    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
+    {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
+    {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
+    {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
 [[package]]
 name = "click"
-version = "8.1.6"
+version = "8.1.7"
 description = "Composable command line interface toolkit"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
-    {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
 ]
 
 [package.dependencies]
@@ -275,14 +393,14 @@ graph = ["objgraph (>=1.7.2)"]
 
 [[package]]
 name = "flask"
-version = "2.3.2"
+version = "2.3.3"
 description = "A simple framework for building complex web applications."
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "Flask-2.3.2-py3-none-any.whl", hash = "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0"},
-    {file = "Flask-2.3.2.tar.gz", hash = "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"},
+    {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"},
+    {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"},
 ]
 
 [package.dependencies]
@@ -291,7 +409,7 @@ click = ">=8.1.3"
 importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
 itsdangerous = ">=2.1.2"
 Jinja2 = ">=3.1.2"
-Werkzeug = ">=2.3.3"
+Werkzeug = ">=2.3.7"
 
 [package.extras]
 async = ["asgiref (>=3.2)"]
@@ -330,18 +448,19 @@ Flask = "*"
 
 [[package]]
 name = "flask-compress"
-version = "1.13"
+version = "1.14"
 description = "Compress responses in your Flask app with gzip, deflate or brotli."
 category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "Flask-Compress-1.13.tar.gz", hash = "sha256:ee96f18bf9b00f2deb4e3406ca4a05093aa80e2ef0578525a3b4d32ecdff129d"},
-    {file = "Flask_Compress-1.13-py3-none-any.whl", hash = "sha256:1128f71fbd788393ce26830c51f8b5a1a7a4d085e79a21a5cddf4c057dcd559b"},
+    {file = "Flask-Compress-1.14.tar.gz", hash = "sha256:e46528f37b91857012be38e24e65db1a248662c3dc32ee7808b5986bf1d123ee"},
+    {file = "Flask_Compress-1.14-py3-none-any.whl", hash = "sha256:b86c9808f0f38ea2246c9730972cf978f2cdf6a9a1a69102ba81e07891e6b26c"},
 ]
 
 [package.dependencies]
-brotli = "*"
+brotli = {version = "*", markers = "platform_python_implementation != \"PyPy\""}
+brotlicffi = {version = "*", markers = "platform_python_implementation == \"PyPy\""}
 flask = "*"
 
 [[package]]
@@ -362,14 +481,14 @@ Werkzeug = ">=1.0.1"
 
 [[package]]
 name = "flask-migrate"
-version = "4.0.4"
+version = "4.0.5"
 description = "SQLAlchemy database migrations for Flask applications using Alembic."
 category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"},
-    {file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"},
+    {file = "Flask-Migrate-4.0.5.tar.gz", hash = "sha256:d3f437a8b5f3849d1bb1b60e1b818efc564c66e3fefe90b62e5db08db295e1b1"},
+    {file = "Flask_Migrate-4.0.5-py3-none-any.whl", hash = "sha256:613a2df703998e78716cace68cd83972960834424457f5b67f56e74fff950aef"},
 ]
 
 [package.dependencies]
@@ -379,19 +498,19 @@ Flask-SQLAlchemy = ">=1.0"
 
 [[package]]
 name = "flask-sqlalchemy"
-version = "3.0.5"
+version = "3.1.1"
 description = "Add SQLAlchemy support to your Flask application."
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"},
-    {file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"},
+    {file = "flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0"},
+    {file = "flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"},
 ]
 
 [package.dependencies]
 flask = ">=2.2.5"
-sqlalchemy = ">=1.4.18"
+sqlalchemy = ">=2.0.16"
 
 [[package]]
 name = "greenlet"
@@ -469,18 +588,18 @@ test = ["objgraph", "psutil"]
 
 [[package]]
 name = "gunicorn"
-version = "20.1.0"
+version = "21.2.0"
 description = "WSGI HTTP Server for UNIX"
 category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
-    {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
-    {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
+    {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
+    {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
 ]
 
 [package.dependencies]
-setuptools = ">=3.0"
+packaging = "*"
 
 [package.extras]
 eventlet = ["eventlet (>=0.24.1)"]
@@ -510,22 +629,22 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
 
 [[package]]
 name = "importlib-resources"
-version = "6.0.0"
+version = "6.1.0"
 description = "Read resources from Python packages"
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"},
-    {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"},
+    {file = "importlib_resources-6.1.0-py3-none-any.whl", hash = "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"},
+    {file = "importlib_resources-6.1.0.tar.gz", hash = "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9"},
 ]
 
 [package.dependencies]
 zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"]
 
 [[package]]
 name = "isort"
@@ -887,20 +1006,32 @@ files = [
 docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
 test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
 
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+
 [[package]]
 name = "pylint"
-version = "2.17.5"
+version = "2.17.6"
 description = "python code static checker"
 category = "main"
 optional = false
 python-versions = ">=3.7.2"
 files = [
-    {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"},
-    {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"},
+    {file = "pylint-2.17.6-py3-none-any.whl", hash = "sha256:18a1412e873caf8ffb56b760ce1b5643675af23e6173a247b502406b24c716af"},
+    {file = "pylint-2.17.6.tar.gz", hash = "sha256:be928cce5c76bf9acdc65ad01447a1e0b1a7bccffc609fb7fc40f2513045bd05"},
 ]
 
 [package.dependencies]
-astroid = ">=2.15.6,<=2.17.0-dev0"
+astroid = ">=2.15.7,<=2.17.0-dev0"
 colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
 dill = [
     {version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -919,14 +1050,14 @@ testutils = ["gitpython (>3)"]
 
 [[package]]
 name = "python-dotenv"
-version = "0.21.1"
+version = "1.0.0"
 description = "Read key-value pairs from a .env file and set them as environment variables"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49"},
-    {file = "python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"},
+    {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
+    {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
 ]
 
 [package.extras]
@@ -1080,72 +1211,55 @@ files = [
     {file = "regex-2023.8.8.tar.gz", hash = "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e"},
 ]
 
-[[package]]
-name = "setuptools"
-version = "68.0.0"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
-    {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
-
 [[package]]
 name = "sqlalchemy"
-version = "2.0.19"
+version = "2.0.21"
 description = "Database Abstraction Library"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3"},
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5"},
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a"},
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1"},
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae"},
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15"},
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-win32.whl", hash = "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2"},
-    {file = "SQLAlchemy-2.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-win32.whl", hash = "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145"},
-    {file = "SQLAlchemy-2.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c"},
-    {file = "SQLAlchemy-2.0.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c"},
-    {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12"},
-    {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2"},
-    {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84"},
-    {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148"},
-    {file = "SQLAlchemy-2.0.19-cp37-cp37m-win32.whl", hash = "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b"},
-    {file = "SQLAlchemy-2.0.19-cp37-cp37m-win_amd64.whl", hash = "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-win32.whl", hash = "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531"},
-    {file = "SQLAlchemy-2.0.19-cp38-cp38-win_amd64.whl", hash = "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-win32.whl", hash = "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb"},
-    {file = "SQLAlchemy-2.0.19-cp39-cp39-win_amd64.whl", hash = "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0"},
-    {file = "SQLAlchemy-2.0.19-py3-none-any.whl", hash = "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972"},
-    {file = "SQLAlchemy-2.0.19.tar.gz", hash = "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-win32.whl", hash = "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4"},
+    {file = "SQLAlchemy-2.0.21-cp310-cp310-win_amd64.whl", hash = "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"},
+    {file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"},
+    {file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"},
+    {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"},
+    {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"},
+    {file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178"},
+    {file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01"},
+    {file = "SQLAlchemy-2.0.21-cp37-cp37m-win32.whl", hash = "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9"},
+    {file = "SQLAlchemy-2.0.21-cp37-cp37m-win_amd64.whl", hash = "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-win32.whl", hash = "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6"},
+    {file = "SQLAlchemy-2.0.21-cp38-cp38-win_amd64.whl", hash = "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-win32.whl", hash = "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9"},
+    {file = "SQLAlchemy-2.0.21-cp39-cp39-win_amd64.whl", hash = "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce"},
+    {file = "SQLAlchemy-2.0.21-py3-none-any.whl", hash = "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce"},
+    {file = "SQLAlchemy-2.0.21.tar.gz", hash = "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258"},
 ]
 
 [package.dependencies]
@@ -1153,7 +1267,7 @@ greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or
 typing-extensions = ">=4.2.0"
 
 [package.extras]
-aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
+aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
 aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
 asyncio = ["greenlet (!=0.4.17)"]
 asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
@@ -1202,14 +1316,14 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.7.1"
-description = "Backported and Experimental Type Hints for Python 3.7+"
+version = "4.8.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
-    {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+    {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
+    {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
 ]
 
 [[package]]
@@ -1226,14 +1340,14 @@ files = [
 
 [[package]]
 name = "werkzeug"
-version = "2.3.6"
+version = "2.3.7"
 description = "The comprehensive WSGI web application library."
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"},
-    {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"},
+    {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"},
+    {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"},
 ]
 
 [package.dependencies]
@@ -1329,21 +1443,21 @@ files = [
 
 [[package]]
 name = "zipp"
-version = "3.16.2"
+version = "3.17.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"},
-    {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"},
+    {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
+    {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
 testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "11735dbbdd45dcc5085a04dbbd4e786dd8aeb4ba743f0c20294861e7b4c35dd0"
+content-hash = "f3ff236ae81d76796722dcd183466f483b6d5aba6dbfcffc145747aa0bb36087"
diff --git a/pyproject.toml b/pyproject.toml
index ae79283..ed1a9d3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "OnlyLegs"
-version = "0.1.5"
+version = "0.1.7"
 repository = "https://github.com/Fluffy-Bean/onlylegs"
 license = "MIT"
 readme = "README.md"
@@ -16,8 +16,8 @@ Flask-Compress = "^1.13"
 Flask-Caching = "1.10.1" 
 Flask-Assets = "^2.0"
 Flask-Login = "^0.6.2"
-python-dotenv = "^0.21.0"
-gunicorn = "^20.1.0"
+python-dotenv = "1.0.0"
+gunicorn = "^21.2.0"
 pyyaml = "^6.0"
 colorthief = "^0.2.1"
 Pillow = "^9.4.0"

From 317c875cf08a97db61ffdafb29a3b6ab030808f7 Mon Sep 17 00:00:00 2001
From: Fluffy-Bean <michal-gdula@protonmail.com>
Date: Wed, 27 Sep 2023 11:32:39 +0100
Subject: [PATCH 3/5] Functional settings menu plus sass stuff apparently?

---
 onlylegs/api.py                               |  78 +--------
 onlylegs/auth.py                              |   2 +-
 onlylegs/config.py                            |   3 +-
 onlylegs/filters.py                           |   6 +-
 onlylegs/static/sass/components/banner.sass   |   6 +-
 .../static/sass/components/buttons/block.sass |  12 +-
 .../sass/components/buttons/top-of-page.sass  |   4 +-
 .../static/sass/components/context-menu.sass  |   2 +-
 onlylegs/static/sass/components/gallery.sass  |   8 +-
 .../static/sass/components/image-view.sass    |  17 +-
 .../static/sass/components/navigation.sass    |   2 +-
 .../static/sass/components/notification.sass  |   6 +-
 onlylegs/static/sass/components/pop-up.sass   |   6 +-
 onlylegs/static/sass/components/tags.sass     |   4 +-
 .../static/sass/components/upload-panel.sass  |  14 +-
 onlylegs/static/sass/style.sass               |  11 +-
 onlylegs/static/sass/variables.sass           |  16 +-
 onlylegs/templates/base.html                  |  12 +-
 onlylegs/templates/profile.html               |   2 +-
 onlylegs/templates/settings.html              |  40 +++--
 onlylegs/utils/startup.py                     |  10 +-
 onlylegs/views/settings.py                    | 148 +++++++++++++++++-
 22 files changed, 256 insertions(+), 153 deletions(-)

diff --git a/onlylegs/api.py b/onlylegs/api.py
index 44c187c..0c68f82 100644
--- a/onlylegs/api.py
+++ b/onlylegs/api.py
@@ -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):
     """
diff --git a/onlylegs/auth.py b/onlylegs/auth.py
index c3d0698..4394b31 100644
--- a/onlylegs/auth.py
+++ b/onlylegs/auth.py
@@ -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"))
diff --git a/onlylegs/config.py b/onlylegs/config.py
index 2b792d4..3300411 100644
--- a/onlylegs/config.py
+++ b/onlylegs/config.py
@@ -44,9 +44,10 @@ WEBSITE_CONF = conf["website"]
 
 # Directories
 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")
-MEDIA_FOLDER = os.path.join(APPLICATION_ROOT, "media")
+BANNER_FOLDER = os.path.join(APPLICATION_ROOT, "media", "banner")
 
 # Database
 INSTANCE_DIR = os.path.join(APPLICATION_ROOT, "instance")
diff --git a/onlylegs/filters.py b/onlylegs/filters.py
index bdecca5..1f4515b 100644
--- a/onlylegs/filters.py
+++ b/onlylegs/filters.py
@@ -18,7 +18,11 @@ 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()
diff --git a/onlylegs/static/sass/components/banner.sass b/onlylegs/static/sass/components/banner.sass
index a88b2c3..724a5ee 100644
--- a/onlylegs/static/sass/components/banner.sass
+++ b/onlylegs/static/sass/components/banner.sass
@@ -44,7 +44,7 @@
         width: 100%
         height: 100%
 
-        background: linear-gradient(to right, var(--background-100), transparent)
+        background: linear-gradient(to right, var(--background-100), transparent 80%, var(--background-100) 100%)
 
         z-index: +1
 
@@ -157,7 +157,7 @@
             margin-left: auto
             width: auto
 
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     .banner,
     .banner-small
         &::after
@@ -169,7 +169,7 @@
         max-height: 30vh
 
         .banner-filter
-            background: linear-gradient(to bottom, var(--background-100), transparent)
+            background: linear-gradient(to top, var(--background-100), transparent)
 
         .banner-content
             padding: 0.5rem
diff --git a/onlylegs/static/sass/components/buttons/block.sass b/onlylegs/static/sass/components/buttons/block.sass
index 121aba2..7994647 100644
--- a/onlylegs/static/sass/components/buttons/block.sass
+++ b/onlylegs/static/sass/components/buttons/block.sass
@@ -39,35 +39,35 @@
 
         &:hover, &:focus-visible
             background-color: var(--primary)
-            color: var(--white)
+            color: var(--black)
     &.success
         background-color: var(--success-transparent)
         color: var(--success)
 
         &:hover, &:focus-visible
             background-color: var(--success)
-            color: var(--white)
+            color: var(--black)
     &.warning
         background-color: var(--warning-transparent)
         color: var(--warning)
 
         &:hover, &:focus-visible
             background-color: var(--warning)
-            color: var(--white)
+            color: var(--black)
     &.critical
         background-color: var(--danger-transparent)
         color: var(--danger)
 
         &:hover, &:focus-visible
             background-color: var(--danger)
-            color: var(--white)
+            color: var(--black)
     &.info
         background-color: var(--info-transparent)
         color: var(--info)
 
         &:hover, &:focus-visible
             background-color: var(--info)
-            color: var(--white)
+            color: var(--black)
     &.black
         background-color: var(--black-transparent)
         color: var(--white)
@@ -201,4 +201,4 @@
 
     &.error
         background-color: var(--danger)
-        color: var(--danger)
+        color: var(--black)
diff --git a/onlylegs/static/sass/components/buttons/top-of-page.sass b/onlylegs/static/sass/components/buttons/top-of-page.sass
index 1ce9cdb..dd356ba 100644
--- a/onlylegs/static/sass/components/buttons/top-of-page.sass
+++ b/onlylegs/static/sass/components/buttons/top-of-page.sass
@@ -13,7 +13,7 @@
     justify-content: center
     align-items: center
 
-    background-color: var(--background-300)
+    background-color: var(--background-100)
     color: var(--foreground-white)
     border-radius: calc(var(--rad) / 2)
     border: none
@@ -34,6 +34,6 @@
         right: 0.75rem
         opacity: 1
 
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     .top-of-page
         bottom: 4.25rem
diff --git a/onlylegs/static/sass/components/context-menu.sass b/onlylegs/static/sass/components/context-menu.sass
index 0a58aed..8d4b4aa 100644
--- a/onlylegs/static/sass/components/context-menu.sass
+++ b/onlylegs/static/sass/components/context-menu.sass
@@ -31,7 +31,7 @@
 
     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)
diff --git a/onlylegs/static/sass/components/gallery.sass b/onlylegs/static/sass/components/gallery.sass
index 7194f63..081af88 100644
--- a/onlylegs/static/sass/components/gallery.sass
+++ b/onlylegs/static/sass/components/gallery.sass
@@ -26,7 +26,7 @@
 
     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
@@ -47,7 +47,7 @@
         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
@@ -163,7 +163,7 @@
             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
@@ -218,5 +218,5 @@
         grid-template-columns: auto auto auto
 
     .gallery-item
-        margin: 0.35rem
+        margin: 0.1rem
         position: relative
\ No newline at end of file
diff --git a/onlylegs/static/sass/components/image-view.sass b/onlylegs/static/sass/components/image-view.sass
index 1edcd8a..473bdc6 100644
--- a/onlylegs/static/sass/components/image-view.sass
+++ b/onlylegs/static/sass/components/image-view.sass
@@ -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
@@ -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%
 
diff --git a/onlylegs/static/sass/components/navigation.sass b/onlylegs/static/sass/components/navigation.sass
index a3d8cd9..9ea4332 100644
--- a/onlylegs/static/sass/components/navigation.sass
+++ b/onlylegs/static/sass/components/navigation.sass
@@ -78,7 +78,7 @@ nav
             background-color: currentColor
             border-radius: calc(var(--rad) / 2)
 
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     nav
         width: 100vw
         height: 3.5rem
diff --git a/onlylegs/static/sass/components/notification.sass b/onlylegs/static/sass/components/notification.sass
index d2c74b0..d7a3de5 100644
--- a/onlylegs/static/sass/components/notification.sass
+++ b/onlylegs/static/sass/components/notification.sass
@@ -32,7 +32,7 @@
 
     position: relative
 
-    background-color: var(--background-300)
+    background-color: var(--background-400)
     border-radius: calc(var(--rad) / 2)
     color: var(--foreground-white)
     opacity: 0
@@ -97,7 +97,7 @@
     justify-content: center
     align-items: center
 
-    background-color: var(--background-200)
+    background-color: var(--background-300)
 
     i
         font-size: 1.25rem
@@ -119,7 +119,7 @@
     line-height: 1
     text-align: left
 
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     .notifications
         bottom: 3.8rem
         width: calc(100vw - 0.6rem)
diff --git a/onlylegs/static/sass/components/pop-up.sass b/onlylegs/static/sass/components/pop-up.sass
index 565f63b..9542f13 100644
--- a/onlylegs/static/sass/components/pop-up.sass
+++ b/onlylegs/static/sass/components/pop-up.sass
@@ -38,12 +38,12 @@
         display: flex
         flex-direction: column
 
-        background-color: var(--background-200)
+        background-color: var(--background-400)
         border-radius: var(--rad)
         overflow: hidden
 
         z-index: +2
-        transition: transform 0.2s var(--animation-smooth)
+        transition: transform 0.2s var(--animation-bounce)
 
     .pop-up-header
         margin: 0 0 0.5rem 0
@@ -133,7 +133,7 @@
         .pop-up-wrapper
             transform: translate(-50%, 50%) scale(1)
 
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     .pop-up
         .pop-up-wrapper
             max-width: calc(100% - 0.75rem)
diff --git a/onlylegs/static/sass/components/tags.sass b/onlylegs/static/sass/components/tags.sass
index 346918a..b45b630 100644
--- a/onlylegs/static/sass/components/tags.sass
+++ b/onlylegs/static/sass/components/tags.sass
@@ -13,7 +13,7 @@
 
     border-radius: calc(var(--rad) / 2)
     border: none
-    background-color: var(--primary--transparent)
+    background-color: var(--primary-transparent)
     color: var(--primary)
 
     cursor: pointer
@@ -23,4 +23,4 @@
         font-size: 1.15rem
 
     &:hover
-        background-color: var(--primary--transparent)
+        background-color: var(--primary-transparent)
diff --git a/onlylegs/static/sass/components/upload-panel.sass b/onlylegs/static/sass/components/upload-panel.sass
index 04bb941..e4fb816 100644
--- a/onlylegs/static/sass/components/upload-panel.sass
+++ b/onlylegs/static/sass/components/upload-panel.sass
@@ -13,7 +13,7 @@
 
     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
@@ -70,7 +70,7 @@
 
         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
@@ -97,7 +97,7 @@
                 background-color: var(--background-400)
                 border-radius: calc(var(--rad) / 2)
 
-                transition: width 0.25s var(--animation-bounce)
+                transition: width 0.25s var(--animation-smooth)
 
         &.dragging #dragIndicator::after
             width: 9rem
@@ -163,7 +163,7 @@
 
                 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%
@@ -175,10 +175,10 @@
 
                 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
@@ -203,7 +203,7 @@
         .container
             left: 0
 
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     .upload-panel
         width: 100%
         height: calc(100vh - 3.5rem)
diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass
index e607c97..19a6ec1 100644
--- a/onlylegs/static/sass/style.sass
+++ b/onlylegs/static/sass/style.sass
@@ -57,7 +57,7 @@ body
     color: var(--foreground-white)
 
     overflow-x: hidden
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     body
         padding: 0 0 3.5rem 0
 
@@ -71,10 +71,13 @@ main
     color: var(--foreground-black)
     border-radius: var(--rad)
     overflow: hidden
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     main
         margin: 0 0.5rem
-        // border-radius: 0
+
+header
+    position: sticky
+    top: 0
 
 .error-page
     min-height: 100%
@@ -100,7 +103,7 @@ main
         font-size: 1.25rem
         font-weight: 400
         text-align: center
-@media (max-width: var(--breakpoint))
+@media (max-width: 800px)
     .error-page
         h1
             font-size: 4.5rem
diff --git a/onlylegs/static/sass/variables.sass b/onlylegs/static/sass/variables.sass
index 754caa2..8f5818f 100644
--- a/onlylegs/static/sass/variables.sass
+++ b/onlylegs/static/sass/variables.sass
@@ -1,6 +1,6 @@
 \:root
-    --background-hsl-hue: 0
-    --background-hsl-saturation: 2%
+    --background-hsl-hue: 69
+    --background-hsl-saturation: 25%
 
     --background-100: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 6%)
     --background-200: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 8%)
@@ -13,14 +13,15 @@
 
     --background-shade: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 6%, 0.5)
 
-    --foreground-gray: rgb(102, 102, 102)
     --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)
@@ -33,12 +34,17 @@
     --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)
+
     --warning: var(--orange)
+    --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.4rem
 
@@ -47,4 +53,4 @@
 
     --breakpoint: 800px
 
-    --font-family: 'Rubik', sans-serif
+    --font-family: 'Switzer', sans-serif
diff --git a/onlylegs/templates/base.html b/onlylegs/templates/base.html
index 58b7bb2..403288f 100644
--- a/onlylegs/templates/base.html
+++ b/onlylegs/templates/base.html
@@ -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>
@@ -130,6 +128,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
@@ -191,10 +193,6 @@
                 }
             }
         }
-
-        {% for message in get_flashed_messages() %}
-            addNotification('{{ message[0] }}', {{ message[1] }});
-        {% endfor %}
     </script>
 
     {% block script %}{% endblock %}
diff --git a/onlylegs/templates/profile.html b/onlylegs/templates/profile.html
index 7880339..c0e42af 100644
--- a/onlylegs/templates/profile.html
+++ b/onlylegs/templates/profile.html
@@ -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 %}
diff --git a/onlylegs/templates/settings.html b/onlylegs/templates/settings.html
index 88216b4..7cad7ab 100644
--- a/onlylegs/templates/settings.html
+++ b/onlylegs/templates/settings.html
@@ -22,15 +22,20 @@
             <i class="ph ph-caret-down collapse-indicator"></i>
         </summary>
 
-        <form method="POST" action="{{ url_for('api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data">
+        <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"/>
-            <input type="submit" value="Upload" class="btn-block">
+            <button type="submit" class="btn-block">Change Profile Picture</button>
         </form>
-        <form method="POST" action="{{ url_for('api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data">
+        <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 }}" />
-            <input type="submit" value="Upload" class="btn-block"/>
+            <button type="submit" class="btn-block">Change Username</button>
         </form>
     </details>
 
@@ -40,10 +45,27 @@
             <i class="ph ph-caret-down collapse-indicator"></i>
         </summary>
 
-        <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>
+        <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>
+
+        <p>Nothing here :3</p>
     </details>
 {% endblock %}
diff --git a/onlylegs/utils/startup.py b/onlylegs/utils/startup.py
index 722b37f..c08fc3d 100644
--- a/onlylegs/utils/startup.py
+++ b/onlylegs/utils/startup.py
@@ -16,6 +16,7 @@ REQUIRED_DIRS = {
     "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")
@@ -28,11 +29,10 @@ def check_dirs():
     """
 
     for directory in REQUIRED_DIRS.values():
-        if os.path.exists(directory):
-            print("User directory already exists at:", directory)
-            return
-        os.makedirs(directory)
-        print("Created directory at:", directory)
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+            print("Created directory at:", directory)
+        print("User directory already exists at:", directory)
 
 
 def check_env():
diff --git a/onlylegs/views/settings.py b/onlylegs/views/settings.py
index 389433b..7bf360e 100644
--- a/onlylegs/views/settings.py
+++ b/onlylegs/views/settings.py
@@ -1,17 +1,159 @@
 """
 OnlyLegs - Settings page
 """
-from flask import Blueprint, render_template
-from flask_login import login_required
+import os
+import pathlib
+import re
+import logging
+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
 
 
 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"))

From 3496a3bbe9d031b2964d3ef515f19285619de860 Mon Sep 17 00:00:00 2001
From: Fluffy-Bean <michal-gdula@protonmail.com>
Date: Wed, 27 Sep 2023 11:37:34 +0100
Subject: [PATCH 4/5] Update readme with notice

---
 README.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.md b/README.md
index ac0254c..35218ee 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+> **IMPORTANT**  
+> This project is maintained irregularly and is still in rapid development.
+> Things maybe here now, but not again in a few weeks
+
 <div align="center">
     <img src=".github/images/OnlyLegs.png" width="200" height="200"/>
     <div id="user-content-toc">

From b4bc8c61ecde06843c6fa1a6dc9093b85cfb0d3c Mon Sep 17 00:00:00 2001
From: Fluffy-Bean <michal-gdula@protonmail.com>
Date: Wed, 27 Sep 2023 13:59:31 +0100
Subject: [PATCH 5/5] Allow for styling

---
 onlylegs/config.py                            | 47 +++++------
 .../static/sass/components/buttons/pill.sass  |  7 +-
 .../static/sass/components/image-view.sass    |  2 +-
 onlylegs/static/sass/style.sass               |  8 +-
 onlylegs/static/sass/variables.sass           |  4 +-
 onlylegs/templates/base.html                  | 18 +++-
 onlylegs/templates/group.html                 | 31 -------
 onlylegs/templates/image.html                 | 16 +++-
 onlylegs/templates/settings.html              | 32 ++++++-
 onlylegs/utils/startup.py                     | 83 +++++++++----------
 onlylegs/views/settings.py                    | 32 +++++++
 11 files changed, 161 insertions(+), 119 deletions(-)

diff --git a/onlylegs/config.py b/onlylegs/config.py
index 3300411..a24c24d 100644
--- a/onlylegs/config.py
+++ b/onlylegs/config.py
@@ -17,41 +17,32 @@ startup.check_conf()
 
 # Set dirs
 APPLICATION_ROOT = platformdirs.user_config_dir("onlylegs")
-
-# Load environment variables
-# print("Loading environment variables...")
-load_dotenv(os.path.join(APPLICATION_ROOT, ".env"))
-
-# Load config from user dir
-# print("Loading config...")
-with open(
-    os.path.join(APPLICATION_ROOT, "conf.yml"), encoding="utf-8", mode="r"
-) as file:
-    conf = safe_load(file)
-
-
-# Flask config
-SECRET_KEY = os.environ.get("FLASK_SECRET")
-DATABASE_NAME = "gallery.sqlite3"
-SQLALCHEMY_DATABASE_URI = "sqlite:///" + DATABASE_NAME
-MAX_CONTENT_LENGTH = 1024 * 1024 * conf["upload"]["max-size"]
-ALLOWED_EXTENSIONS = conf["upload"]["allowed-extensions"]
-
-# Pass YAML config to app
-ADMIN_CONF = conf["admin"]
-UPLOAD_CONF = conf["upload"]
-WEBSITE_CONF = conf["website"]
-
-# Directories
 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 env and config files
+load_dotenv(os.path.join(APPLICATION_ROOT, ".env"))
+
+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")
+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")
 
-# App
-APP_VERSION = importlib.metadata.version("OnlyLegs")
+# Pass YAML config to app
+ADMIN_CONF = conf["admin"]
+UPLOAD_CONF = conf["upload"]
+WEBSITE_CONF = conf["website"]
diff --git a/onlylegs/static/sass/components/buttons/pill.sass b/onlylegs/static/sass/components/buttons/pill.sass
index f63b02e..25fc812 100644
--- a/onlylegs/static/sass/components/buttons/pill.sass
+++ b/onlylegs/static/sass/components/buttons/pill.sass
@@ -64,12 +64,11 @@
 
     &:hover
         cursor: pointer
-
         color: var(--primary)
 
-    &.disabled, &:disabled
-        color: var(--foreground-dim)
-        cursor: unset
+    &:disabled, &[disabled], &.disabled
+        color: var(--foreground-gray)
+        cursor: default
 
 .pill__critical
     color: var(--danger)
diff --git a/onlylegs/static/sass/components/image-view.sass b/onlylegs/static/sass/components/image-view.sass
index 473bdc6..249f04c 100644
--- a/onlylegs/static/sass/components/image-view.sass
+++ b/onlylegs/static/sass/components/image-view.sass
@@ -1,5 +1,5 @@
 .info-container
-    padding: 0.5rem 0 0 0.5rem
+    padding: 0.5rem 0 0.5rem 0.5rem
     width: 27rem
     position: absolute
     top: 0
diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass
index 19a6ec1..159aa1e 100644
--- a/onlylegs/static/sass/style.sass
+++ b/onlylegs/static/sass/style.sass
@@ -75,9 +75,11 @@ main
     main
         margin: 0 0.5rem
 
-header
-    position: sticky
-    top: 0
+// 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%
diff --git a/onlylegs/static/sass/variables.sass b/onlylegs/static/sass/variables.sass
index 8f5818f..a31a7bd 100644
--- a/onlylegs/static/sass/variables.sass
+++ b/onlylegs/static/sass/variables.sass
@@ -26,8 +26,8 @@
     --red-transparent: rgba(182, 100, 103, 0.1)
     --orange: rgb(217, 140, 95)
     --orange-transparent: rgba(217, 140, 95, 0.1)
-    --yellow: rgb(217, 188, 140)
-    --yellow-transparent: rgba(217, 188, 140, 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)
diff --git a/onlylegs/templates/base.html b/onlylegs/templates/base.html
index 403288f..3fb4855 100644
--- a/onlylegs/templates/base.html
+++ b/onlylegs/templates/base.html
@@ -28,10 +28,20 @@
     {% 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 type="text/css">
-        :root{
-            --primary: {{ config.WEBSITE_CONF.primary_color | default('rgb(198, 185, 166)') }};
-        }
+    <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 %}
diff --git a/onlylegs/templates/group.html b/onlylegs/templates/group.html
index 077ea93..afaec5c 100644
--- a/onlylegs/templates/group.html
+++ b/onlylegs/templates/group.html
@@ -24,37 +24,6 @@
                 --background-hsl-hue: {{ images.0.colours.0 | hsl_hue }};
                 --background-hsl-saturation: {{ images.0.colours.0 | hsl_saturation }}%;
             }
-
-            /*
-            body {
-                color: {{ text_colour }} !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 {
-                color: rgb{{ images.0.colours.0 }} !important;
-            }
-            .banner-content .link:hover {
-                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;
-                }
-            }
-            */
         {% endif %}
     </style>
 {% endblock %}
diff --git a/onlylegs/templates/image.html b/onlylegs/templates/image.html
index 2070353..725a363 100644
--- a/onlylegs/templates/image.html
+++ b/onlylegs/templates/image.html
@@ -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,7 +34,11 @@
         <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>
+                    <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>
@@ -38,7 +46,11 @@
                         <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>
diff --git a/onlylegs/templates/settings.html b/onlylegs/templates/settings.html
index 7cad7ab..521296a 100644
--- a/onlylegs/templates/settings.html
+++ b/onlylegs/templates/settings.html
@@ -66,6 +66,36 @@
             <i class="ph ph-caret-down collapse-indicator"></i>
         </summary>
 
-        <p>Nothing here :3</p>
+        <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>
+        let hue = document.querySelector('input[name=hue]');
+        let saturation = document.querySelector('input[name=saturation]');
+        let rad = document.querySelector('input[name=rad]');
+
+        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 %}
+
diff --git a/onlylegs/utils/startup.py b/onlylegs/utils/startup.py
index c08fc3d..b8e30eb 100644
--- a/onlylegs/utils/startup.py
+++ b/onlylegs/utils/startup.py
@@ -22,12 +22,42 @@ REQUIRED_DIRS = {
 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():
-    """
-    Create the user directory
-    """
-
     for directory in REQUIRED_DIRS.values():
         if not os.path.exists(directory):
             os.makedirs(directory)
@@ -36,9 +66,6 @@ def check_dirs():
 
 
 def check_env():
-    """
-    Create the .env file with default values
-    """
     if os.path.exists(os.path.join(APPLICATION_ROOT, ".env")):
         print("Environment file already exists at:", APPLICATION_ROOT)
         return
@@ -65,30 +92,23 @@ def check_env():
 
 
 def check_conf():
-    """
-    Create the YAML config file with default values
-    """
-    if os.path.exists(os.path.join(APPLICATION_ROOT, "conf.yml")):
+    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"
-    name = "Admin"
     email = "admin@example.com"
 
     print("No config file found, please enter the following information:")
     while cant_continue:
         username = input("Admin username: ").strip()
-        name = input("Admin name: ").strip()
         email = input("Admin email: ").strip()
 
         if not username or not USERNAME_REGEX.match(username):
             print("Username is invalid!")
             continue
-        if not name:
-            print("Name is invalid!")
-            continue
         if not email or not EMAIL_REGEX.match(email):
             print("Email is invalid!")
             continue
@@ -98,34 +118,11 @@ def check_conf():
         if is_correct == "y" or not is_correct:
             cant_continue = False
 
-    yaml_conf = {
-        "admin": {
-            "name": name,
-            "username": username,
-            "email": email,
-        },
-        "upload": {
-            "allowed-extensions": {
-                "jpg": "jpeg",
-                "jpeg": "jpeg",
-                "png": "png",
-                "webp": "webp",
-            },
-            "max-size": 69,
-            "max-load": 50,
-            "rename": "GWA_{{username}}_{{time}}",
-        },
-        "website": {
-            "name": "OnlyLegs",
-            "motto": "A gallery built for fast and simple image management!",
-            "language": "en",
-        },
-    }
+    config["admin"]["username"] = username
+    config["admin"]["email"] = email
 
-    with open(
-        os.path.join(APPLICATION_ROOT, "conf.yml"), encoding="utf-8", mode="w+"
-    ) as file:
-        yaml.dump(yaml_conf, file, default_flow_style=False)
+    with open(config_file, encoding="utf-8", mode="w+") as file:
+        yaml.dump(config, file, default_flow_style=False)
 
     print(
         "####################################################",
diff --git a/onlylegs/views/settings.py b/onlylegs/views/settings.py
index 7bf360e..092ed45 100644
--- a/onlylegs/views/settings.py
+++ b/onlylegs/views/settings.py
@@ -5,6 +5,7 @@ import os
 import pathlib
 import re
 import logging
+import yaml
 from colorthief import ColorThief
 from flask import (
     Blueprint,
@@ -19,6 +20,7 @@ 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")
@@ -157,3 +159,33 @@ def account_password():
 
     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