Functional settings menu plus sass stuff apparently?

This commit is contained in:
Michał 2023-09-27 11:32:39 +01:00
parent 1a59e413a9
commit 317c875cf0
22 changed files with 256 additions and 153 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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