Compare commits
17 Commits
db677abaa5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
dc0aa0dba7
|
|||
|
dbf0150a5e
|
|||
|
1539486456
|
|||
|
c18dad4a77
|
|||
|
2b45cab4e8
|
|||
|
37c1ffc2a1
|
|||
|
09a19b5352
|
|||
|
6c96563a0e
|
|||
|
77677eef6d
|
|||
|
f99ae75503
|
|||
|
552fb67c6c
|
|||
|
e9c03b9046
|
|||
|
f0b0fb8909
|
|||
|
9ae4e376b8
|
|||
|
d1bc1c644b
|
|||
|
7840399d01
|
|||
|
508b313871
|
2
.gitignore
vendored
@@ -4,7 +4,9 @@
|
||||
data/db/*
|
||||
data/static/avatars/*
|
||||
!data/static/avatars/default.webp
|
||||
data/static/badges/user
|
||||
|
||||
config/secrets.prod.env
|
||||
config/pyrom_config.toml
|
||||
|
||||
.local/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from flask import Flask, session, request, render_template
|
||||
from dotenv import load_dotenv
|
||||
from .models import Avatars, Users, PostHistory, Posts, MOTD
|
||||
from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads
|
||||
from .auth import digest
|
||||
from .routes.users import is_logged_in, get_active_user, get_prefers_theme
|
||||
from .routes.threads import get_post_url
|
||||
@@ -16,6 +16,7 @@ import os
|
||||
import time
|
||||
import secrets
|
||||
import tomllib
|
||||
import json
|
||||
|
||||
def create_default_avatar():
|
||||
if Avatars.count() == 0:
|
||||
@@ -99,9 +100,43 @@ def reparse_babycode():
|
||||
|
||||
print('Re-parsing done.')
|
||||
|
||||
def bind_default_badges(path):
|
||||
from .db import db
|
||||
with db.transaction():
|
||||
potential_stales = BadgeUploads.get_default()
|
||||
d = os.listdir(path)
|
||||
for bu in potential_stales:
|
||||
if os.path.basename(bu.file_path) not in d:
|
||||
print(f'Deleted stale default badge{os.path.basename(bu.file_path)}')
|
||||
bu.delete()
|
||||
|
||||
for f in d:
|
||||
real_path = os.path.join(path, f)
|
||||
if not os.path.isfile(real_path):
|
||||
continue
|
||||
if not f.endswith('.webp'):
|
||||
continue
|
||||
proxied_path = f'/static/badges/{f}'
|
||||
bu = BadgeUploads.find({'file_path': proxied_path})
|
||||
if not bu:
|
||||
BadgeUploads.create({
|
||||
'file_path': proxied_path,
|
||||
'uploaded_at': int(os.path.getmtime(real_path)),
|
||||
})
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
|
||||
app.config['SITE_NAME'] = 'Pyrom'
|
||||
app.config['DISABLE_SIGNUP'] = False
|
||||
app.config['MODS_CAN_INVITE'] = True
|
||||
app.config['USERS_CAN_INVITE'] = False
|
||||
app.config['ADMIN_CONTACT_INFO'] = ''
|
||||
app.config['GUIDE_DESCRIPTION'] = ''
|
||||
try:
|
||||
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
|
||||
except FileNotFoundError:
|
||||
print('No configuration file found, leaving defaults.')
|
||||
|
||||
if os.getenv("PYROM_PROD") is None:
|
||||
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
|
||||
@@ -114,9 +149,12 @@ def create_app():
|
||||
app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY")
|
||||
|
||||
app.config['AVATAR_UPLOAD_PATH'] = 'data/static/avatars/'
|
||||
app.config['MAX_CONTENT_LENGTH'] = 1000 * 1000
|
||||
app.config['BADGES_PATH'] = 'data/static/badges/'
|
||||
app.config['BADGES_UPLOAD_PATH'] = 'data/static/badges/user/'
|
||||
app.config['MAX_CONTENT_LENGTH'] = 3 * 1000 * 1000 # 3M total, subject to further limits per route
|
||||
|
||||
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
|
||||
os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True)
|
||||
|
||||
css_dir = 'data/static/css/'
|
||||
allowed_themes = []
|
||||
@@ -141,6 +179,8 @@ def create_app():
|
||||
|
||||
reparse_babycode()
|
||||
|
||||
bind_default_badges(app.config['BADGES_PATH'])
|
||||
|
||||
from app.routes.app import bp as app_bp
|
||||
from app.routes.topics import bp as topics_bp
|
||||
from app.routes.threads import bp as threads_bp
|
||||
@@ -228,6 +268,10 @@ def create_app():
|
||||
for id_, text in matches
|
||||
]
|
||||
|
||||
@app.template_filter('basename_noext')
|
||||
def basename_noext(subj):
|
||||
return os.path.splitext(os.path.basename(subj))[0]
|
||||
|
||||
@app.errorhandler(404)
|
||||
def _handle_404(e):
|
||||
if request.path.startswith('/hyperapi/'):
|
||||
@@ -237,6 +281,15 @@ def create_app():
|
||||
else:
|
||||
return render_template('common/404.html'), e.code
|
||||
|
||||
@app.errorhandler(413)
|
||||
def _handle_413(e):
|
||||
if request.path.startswith('/hyperapi/'):
|
||||
return '<h1>request body too large</h1>', e.code
|
||||
elif request.path.startswith('/api/'):
|
||||
return {'error': 'body too large'}, e.code
|
||||
else:
|
||||
return render_template('common/413.html'), e.code
|
||||
|
||||
# this only happens at build time but
|
||||
# build time is when updates are done anyway
|
||||
# sooo... /shrug
|
||||
@@ -249,6 +302,10 @@ def create_app():
|
||||
if subject == 'style':
|
||||
return 'Default'
|
||||
|
||||
return f'{subject.removeprefix('theme-').capitalize()} (beta)'
|
||||
return f'{subject.removeprefix('theme-').replace('-', ' ').capitalize()} (beta)'
|
||||
|
||||
@app.template_filter('fromjson')
|
||||
def fromjson(subject: str):
|
||||
return json.loads(subject)
|
||||
|
||||
return app
|
||||
|
||||
@@ -31,6 +31,7 @@ class DB:
|
||||
except Exception as e:
|
||||
if in_transaction and self._connection:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
if in_transaction:
|
||||
self._transaction_depth -= 1
|
||||
@@ -126,7 +127,7 @@ class DB:
|
||||
def where(self, condition, operator = "="):
|
||||
if isinstance(condition, dict):
|
||||
for key, value in condition.items():
|
||||
self._where.append((key, "=", value))
|
||||
self._where.append((key, operator, value))
|
||||
elif isinstance(condition, list):
|
||||
for c in condition:
|
||||
self._where.append(c)
|
||||
|
||||
@@ -116,6 +116,9 @@ class Users(Model):
|
||||
def has_display_name(self):
|
||||
return self.display_name != ''
|
||||
|
||||
def get_badges(self):
|
||||
return Badges.findall({'user_id': int(self.id)})
|
||||
|
||||
|
||||
class Topics(Model):
|
||||
table = "topics"
|
||||
@@ -243,6 +246,23 @@ class Threads(Model):
|
||||
|
||||
class Posts(Model):
|
||||
FULL_POSTS_QUERY = """
|
||||
WITH user_badges AS (
|
||||
SELECT
|
||||
b.user_id,
|
||||
json_group_array(
|
||||
json_object(
|
||||
'label', b.label,
|
||||
'link', b.link,
|
||||
'sort_order', b.sort_order,
|
||||
'file_path', bu.file_path
|
||||
)
|
||||
) AS badges_json
|
||||
FROM badges b
|
||||
LEFT JOIN badge_uploads bu ON b.upload = bu.id
|
||||
GROUP BY b.user_id
|
||||
ORDER BY b.sort_order
|
||||
)
|
||||
|
||||
SELECT
|
||||
posts.id, posts.created_at,
|
||||
post_history.content, post_history.edited_at,
|
||||
@@ -250,7 +270,8 @@ class Posts(Model):
|
||||
avatars.file_path AS avatar_path, posts.thread_id,
|
||||
users.id AS user_id, post_history.original_markup,
|
||||
users.signature_rendered, threads.slug AS thread_slug,
|
||||
threads.is_locked AS thread_is_locked, threads.title AS thread_title
|
||||
threads.is_locked AS thread_is_locked, threads.title AS thread_title,
|
||||
COALESCE(user_badges.badges_json, '[]') AS badges_json
|
||||
FROM
|
||||
posts
|
||||
JOIN
|
||||
@@ -260,7 +281,9 @@ class Posts(Model):
|
||||
JOIN
|
||||
threads ON posts.thread_id = threads.id
|
||||
LEFT JOIN
|
||||
avatars ON users.avatar_id = avatars.id"""
|
||||
avatars ON users.avatar_id = avatars.id
|
||||
LEFT JOIN
|
||||
user_badges ON users.id = user_badges.user_id"""
|
||||
|
||||
table = "posts"
|
||||
|
||||
@@ -434,3 +457,31 @@ class MOTD(Model):
|
||||
|
||||
class Mentions(Model):
|
||||
table = 'mentions'
|
||||
|
||||
|
||||
class BadgeUploads(Model):
|
||||
table = 'badge_uploads'
|
||||
|
||||
@classmethod
|
||||
def get_default(cls):
|
||||
return BadgeUploads.findall({'user_id': None}, 'IS')
|
||||
|
||||
@classmethod
|
||||
def get_for_user(cls, user_id):
|
||||
q = "SELECT * FROM badge_uploads WHERE user_id = ? OR user_id IS NULL ORDER BY uploaded_at"
|
||||
res = db.query(q, int(user_id))
|
||||
return [cls.from_data(row) for row in res]
|
||||
|
||||
@classmethod
|
||||
def get_unused_for_user(cls, user_id):
|
||||
q = 'SELECT bu.* FROM badge_uploads bu LEFT JOIN badges b ON bu.id = b.upload WHERE bu.user_id = ? AND b.upload IS NULL'
|
||||
res = db.query(q, int(user_id))
|
||||
return [cls.from_data(row) for row in res]
|
||||
|
||||
|
||||
class Badges(Model):
|
||||
table = 'badges'
|
||||
|
||||
def get_image_url(self):
|
||||
bu = BadgeUploads.find({'id': int(self.upload)})
|
||||
return bu.file_path
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from flask import Blueprint, request, url_for
|
||||
from flask import Blueprint, request, url_for, make_response
|
||||
from ..lib.babycode import babycode_to_html
|
||||
from ..constants import REACTION_EMOJI
|
||||
from .users import is_logged_in, get_active_user
|
||||
from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections, BookmarkedThreads, BookmarkedPosts
|
||||
from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections, BookmarkedThreads, BookmarkedPosts, BadgeUploads
|
||||
from ..db import db
|
||||
|
||||
bp = Blueprint("api", __name__, url_prefix="/api/")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from flask import Blueprint, render_template, abort, request
|
||||
from .users import get_active_user, is_logged_in
|
||||
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads
|
||||
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, BadgeUploads, Badges
|
||||
from functools import wraps
|
||||
|
||||
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
|
||||
@@ -26,7 +26,7 @@ def handle_403(e):
|
||||
return "<h1>forbidden</h1>", 403
|
||||
|
||||
|
||||
@bp.get('bookmarks-dropdown/<bookmark_type>')
|
||||
@bp.get('/bookmarks-dropdown/<bookmark_type>')
|
||||
@login_required
|
||||
@account_required
|
||||
def bookmarks_dropdown(bookmark_type):
|
||||
@@ -51,3 +51,20 @@ def bookmarks_dropdown(bookmark_type):
|
||||
|
||||
|
||||
return render_template('components/bookmarks_dropdown.html', collections=collections, id=concept_id, selected=selected, type=bookmark_type, memo=memo, require_reload=require_reload)
|
||||
|
||||
|
||||
@bp.get('/badge-editor')
|
||||
@login_required
|
||||
@account_required
|
||||
def get_badges():
|
||||
uploads = BadgeUploads.get_for_user(get_active_user().id)
|
||||
badges = sorted(Badges.findall({'user_id': int(get_active_user().id)}), key=lambda x: x['sort_order'])
|
||||
return render_template('components/badge_editor_badges.html', uploads=uploads, badges=badges)
|
||||
|
||||
|
||||
@bp.get('/badge-editor/template')
|
||||
@login_required
|
||||
@account_required
|
||||
def get_badge_template():
|
||||
uploads = BadgeUploads.get_for_user(get_active_user().id)
|
||||
return render_template('components/badge_editor_template.html', uploads=uploads)
|
||||
|
||||
@@ -8,7 +8,7 @@ from ..models import (
|
||||
Users, Sessions, Subscriptions,
|
||||
Avatars, PasswordResetLinks, InviteKeys,
|
||||
BookmarkCollections, BookmarkedThreads,
|
||||
Mentions, PostHistory,
|
||||
Mentions, PostHistory, Badges, BadgeUploads,
|
||||
)
|
||||
from ..constants import InfoboxKind, PermissionLevel, SIG_BANNED_TAGS
|
||||
from ..auth import digest, verify
|
||||
@@ -20,12 +20,17 @@ import time
|
||||
import re
|
||||
import os
|
||||
|
||||
AVATAR_MAX_SIZE = 1000 * 1000
|
||||
BADGE_MAX_SIZE = 1000 * 500
|
||||
|
||||
bp = Blueprint("users", __name__, url_prefix = "/users/")
|
||||
|
||||
|
||||
def validate_and_create_avatar(input_image, filename):
|
||||
try:
|
||||
with Image(blob=input_image) as img:
|
||||
if hasattr(img, 'sequence') and len(img.sequence) > 1:
|
||||
img = Image(image=img.sequence[0])
|
||||
img.strip()
|
||||
img.gravity = 'center'
|
||||
|
||||
@@ -52,6 +57,21 @@ def validate_and_create_avatar(input_image, filename):
|
||||
except WandException:
|
||||
return False
|
||||
|
||||
def validate_and_create_badge(input_image, filename):
|
||||
try:
|
||||
with Image(blob=input_image) as img:
|
||||
if img.width != 88 or img.height != 31:
|
||||
return False
|
||||
if hasattr(img, 'sequence') and len(img.sequence) > 1:
|
||||
img = Image(image=img.sequence[0])
|
||||
img.strip()
|
||||
|
||||
img.format = 'webp'
|
||||
img.compression_quality = 90
|
||||
img.save(filename=filename)
|
||||
return True
|
||||
except WandException:
|
||||
return False
|
||||
|
||||
def is_logged_in():
|
||||
return "pyrom_session_key" in session
|
||||
@@ -400,7 +420,7 @@ def settings_form(username):
|
||||
else:
|
||||
rendered_sig = ''
|
||||
session['subscribe_by_default'] = request.form.get('subscribe_by_default', default='off') == 'on'
|
||||
display_name = request.form.get('display_name', default='')
|
||||
display_name = request.form.get('display_name', default='').replace('@', '_')
|
||||
if not validate_display_name(display_name):
|
||||
flash('Invalid display name.', InfoboxKind.ERROR)
|
||||
return redirect('.settings', username=user.username)
|
||||
@@ -451,6 +471,14 @@ def set_avatar(username):
|
||||
flash('Avatar missing.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_size = file.tell()
|
||||
file.seek(0, os.SEEK_SET)
|
||||
|
||||
if file_size > AVATAR_MAX_SIZE:
|
||||
flash('Avatar image is over 1MB.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
file_bytes = file.read()
|
||||
|
||||
now = int(time.time())
|
||||
@@ -843,3 +871,96 @@ def delete_page_confirm(username):
|
||||
session.clear()
|
||||
target_user.delete()
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
|
||||
@bp.post('/<username>/save-badges')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def save_badges(username):
|
||||
user = get_active_user()
|
||||
badge_choices = request.form.getlist('badge_choice[]')
|
||||
badge_files = request.files.getlist('badge_file[]')
|
||||
badge_labels = request.form.getlist('badge_label[]')
|
||||
badge_links = request.form.getlist('badge_link[]')
|
||||
|
||||
if not (len(badge_choices) == len(badge_files) == len(badge_labels) == len(badge_links)):
|
||||
return 'nope'
|
||||
pending_badges = []
|
||||
rejected_filenames = []
|
||||
|
||||
# lack of file can be checked with a simple `if not file`
|
||||
|
||||
for i in range(len(badge_choices)):
|
||||
is_custom = badge_choices[i] == 'custom'
|
||||
file = badge_files[i]
|
||||
pending_badge = {
|
||||
'upload': badge_choices[i],
|
||||
'is_custom': is_custom,
|
||||
'label': badge_labels[i],
|
||||
'link': badge_links[i],
|
||||
'sort_order': i,
|
||||
}
|
||||
if is_custom:
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_size = file.tell()
|
||||
file.seek(0, os.SEEK_SET)
|
||||
|
||||
if file_size >= BADGE_MAX_SIZE:
|
||||
rejected_filenames.append(file.filename)
|
||||
continue
|
||||
|
||||
file_bytes = file.read()
|
||||
|
||||
pending_badge['original_filename'] = file.filename
|
||||
|
||||
now = int(time.time())
|
||||
filename = f'u{user.id}d{now}s{i}.webp'
|
||||
output_path = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], filename)
|
||||
proxied_filename = f'/static/badges/user/{filename}'
|
||||
res = validate_and_create_badge(file_bytes, output_path)
|
||||
if not res:
|
||||
rejected_filenames.append(file.filename)
|
||||
continue
|
||||
|
||||
pending_badge['proxied_filename'] = proxied_filename
|
||||
pending_badge['uploaded_at'] = now
|
||||
|
||||
pending_badges.append(pending_badge)
|
||||
|
||||
if rejected_filenames:
|
||||
flash(f'Invalid badges.;Some of your uploaded badges are incorrect: {", ".join(rejected_filenames)}. Your badges have not been modified.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
with db.transaction():
|
||||
existing_badges = Badges.findall({'user_id': int(user.id)})
|
||||
for badge in existing_badges:
|
||||
badge.delete()
|
||||
|
||||
with db.transaction():
|
||||
for pending_badge in pending_badges:
|
||||
if pending_badge['is_custom']:
|
||||
bu = BadgeUploads.create({
|
||||
'file_path': pending_badge['proxied_filename'],
|
||||
'uploaded_at': pending_badge['uploaded_at'],
|
||||
'original_filename': pending_badge['original_filename'],
|
||||
'user_id': int(user.id),
|
||||
})
|
||||
else:
|
||||
bu = BadgeUploads.find({
|
||||
'id': int(pending_badge['upload'])
|
||||
})
|
||||
badge = Badges.create({
|
||||
'user_id': int(user.id),
|
||||
'upload': int(bu.id),
|
||||
'label': pending_badge['label'],
|
||||
'link': pending_badge['link'],
|
||||
'sort_order': pending_badge['sort_order']
|
||||
})
|
||||
|
||||
for stale_upload in BadgeUploads.get_unused_for_user(user.id):
|
||||
filename = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], os.path.basename(stale_upload.file_path))
|
||||
os.remove(filename)
|
||||
stale_upload.delete()
|
||||
|
||||
flash('Badges saved.', InfoboxKind.INFO)
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
@@ -141,6 +141,23 @@ SCHEMA = [
|
||||
"original_mention_text" TEXT NOT NULL
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "badge_uploads" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"file_path" TEXT NOT NULL UNIQUE,
|
||||
"uploaded_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
|
||||
"original_filename" TEXT,
|
||||
"user_id" REFERENCES users(id) ON DELETE CASCADE
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "badges" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"user_id" NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
"upload" NOT NULL REFERENCES badge_uploads(id) ON DELETE CASCADE,
|
||||
"label" TEXT NOT NULL,
|
||||
"link" TEXT DEFAULT '',
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0
|
||||
)""",
|
||||
|
||||
# INDEXES
|
||||
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",
|
||||
@@ -167,6 +184,9 @@ SCHEMA = [
|
||||
|
||||
"CREATE INDEX IF NOT EXISTS idx_mentioned_user ON mentions(mentioned_user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_mention_revision_id ON mentions(revision_id)",
|
||||
|
||||
"CREATE INDEX IF NOT EXISTS idx_badge_upload_user ON badge_uploads(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_badge_user ON badges(user_id)",
|
||||
]
|
||||
|
||||
def create():
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}not found{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<h1 class="thread-title">404 Not Found</h1>
|
||||
<div class="darkbg">
|
||||
<h1>404 Not Found</h1>
|
||||
<p>The requested URL does not exist.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
8
app/templates/common/413.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}request entity too large{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg">
|
||||
<h1>413 Request Entity Too Large</h1>
|
||||
<p>The file(s) you tried to upload are too large.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -99,7 +99,7 @@
|
||||
<button data-send="insertBabycodeTag" data-tag="spoiler=" data-break-line="1" data-prefill="hidden content" class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler" {{"disabled" if "spoiler" in banned_tags else ""}}>{{ icn_spoiler() }}</button>
|
||||
</span>
|
||||
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }} autocomplete="off" data-receive="insertBabycodeTag addQuote">{{ prefill }}</textarea>
|
||||
<a href="{{ url_for("guides.guide_page", category='user', slug='babycode') }}" target="_blank">babycode guide</a>
|
||||
<a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}" target="_blank">babycode guide</a>
|
||||
{% if banned_tags %}
|
||||
<div>Forbidden tags:</div>
|
||||
<div>
|
||||
@@ -138,6 +138,17 @@
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro badge_button(badge) %}
|
||||
{% set img_url = badge.file_path if badge.file_path else badge.get_image_url() %}
|
||||
{% if badge.link %}
|
||||
<a href="{{badge.link}}" rel="noopener noreferrer me" target="_blank">
|
||||
{% endif %}
|
||||
<img class="badge-button" src="{{img_url}}" alt="{{badge.label}}" title="{{badge.label}}"></img>
|
||||
{% if badge.link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro full_post(
|
||||
post, render_sig = True, is_latest = False,
|
||||
editing = False, active_user = None, no_reply = false,
|
||||
@@ -161,6 +172,11 @@
|
||||
{% if post['status'] %}
|
||||
<em class="user-status">{{ post['status'] }}</em>
|
||||
{% endif %}
|
||||
<div class="badges-container">
|
||||
{% for badge_data in (post.badges_json | fromjson) %}
|
||||
{{ badge_button(badge_data) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -306,3 +322,40 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro badge_editor_single(options={}, selected=none, fp_hidden=true, badge=none) %}
|
||||
{% set defaults = options | selectattr('user_id', 'none') | list | sort(attribute='file_path') %}
|
||||
{% set uploads = options | selectattr('user_id') | list %}
|
||||
{% if selected is not none %}
|
||||
{% set selected_href = (options | selectattr('id', 'equalto', selected) | list)[0].file_path %}
|
||||
{% else %}
|
||||
{% set selected_href = defaults[0].file_path %}
|
||||
{% endif %}
|
||||
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorBadge" data-listeners="click input submit change">
|
||||
<div class="settings-badge-container" data-receive="deleteBadge">
|
||||
<div class="settings-badge-select">
|
||||
<select data-send="badgeUpdatePreview badgeToggleFilePicker" name="badge_choice[]" required>
|
||||
<optgroup label="Default">
|
||||
{% for option in defaults %}
|
||||
<option data-file-path="{{ option.file_path }}" value="{{ option.id }}" {{ "selected" if selected==option.id else "" }}>{{option.file_path | basename_noext}}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
<optgroup label="Your uploads">
|
||||
{% for option in uploads %}
|
||||
<option data-file-path="{{ option.file_path }}" value="{{ option.id }}" {{ "selected" if selected==option.id else "" }}>{{option.original_filename | basename_noext}}</option>
|
||||
{% endfor %}
|
||||
<option value="custom">Upload new...</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<img class="badge-button" data-receive="badgeUpdatePreview badgeUpdatePreviewCustom" src="{{ selected_href }}"></img>
|
||||
</div>
|
||||
<div class="settings-badge-file-picker{{ " hidden" if fp_hidden else ""}}" data-receive="badgeToggleFilePicker">
|
||||
<button data-send="openBadgeFilePicker" type=button>Browse…</button>
|
||||
<input data-receive="openBadgeFilePicker badgeErrorSize badgeErrorDim badgeHideErrors" data-send="badgeUpdatePreviewCustom" type="file" accept="image/*" class="hidden" name="badge_file[]">
|
||||
</div>
|
||||
<input type="text" required placeholder="Label" name="badge_label[]" value="{{badge.label}}">
|
||||
<input type="text" placeholder="(Optional) Link" name="badge_link[]" value="{{badge.link}}">
|
||||
<button data-send="deleteBadge" type="button" class="critical" title="Delete">X</button>
|
||||
</div>
|
||||
</bitty-7-0>
|
||||
{% endmacro %}
|
||||
|
||||
4
app/templates/components/badge_editor_badges.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{% from 'common/macros.html' import badge_editor_single with context %}
|
||||
{% for badge in badges %}
|
||||
{{ badge_editor_single(options=uploads, selected=badge.upload, badge=badge) }}
|
||||
{% endfor %}
|
||||
2
app/templates/components/badge_editor_template.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% from 'common/macros.html' import badge_editor_single with context %}
|
||||
{{ badge_editor_single(options=uploads) }}
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}guide - {{ guide.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg" id="top">
|
||||
<h1 class="thread-title">Guide: {{ guide.title }}</h1>
|
||||
<h1>Guide: {{ guide.title }}</h1>
|
||||
<ul class="horizontal">
|
||||
<li><a href="{{ url_for('guides.category_index', category=category) }}">↑ Back to category</a></li>
|
||||
{% if prev_guide %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}guides - {{ category | title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<h1 class="thread-title">All guides in category "{{ category | replace('-', ' ') | title }}"</h1>
|
||||
<div class="darkbg">
|
||||
<h1>All guides in category "{{ category | replace('-', ' ') | title }}"</h1>
|
||||
<ul>
|
||||
{% for page in pages %}
|
||||
<li><a href="{{ page.url }}">{{ page.title }}</a></li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}contact us{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>Contact</h1>
|
||||
{% if config.ADMIN_CONTACT_INFO %}
|
||||
<p>The administrators of {{ config.SITE_NAME }} provide the following contact information:</p>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}guides{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<h1 class="thread-title">Guides index</h1>
|
||||
<div class="darkbg">
|
||||
<h1>Guides index</h1>
|
||||
<ul>
|
||||
{% for category in categories %}
|
||||
<li>
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
<p>A thread may be locked by the original poster (OP) or a moderator. When a thread is locked, nobody except moderators can reply to it.</p>
|
||||
<p>A thread may also be stickied by a moderator. Stickied threads show above all other threads in the topic it's under. If a thread is stickied, it is likely important.</p>
|
||||
<p>You can subscribe to any thread. When subscribed, you will receive an overview of posts you missed for that thread in your inbox.</p>
|
||||
<p>You can</p>
|
||||
</section>
|
||||
<section class="guide-section">
|
||||
<h2 id="posts">Posts</h2>
|
||||
@@ -54,7 +53,7 @@
|
||||
<p>A post is split up into five sections:</p>
|
||||
<ol>
|
||||
<li>Usercard
|
||||
<ul><li>The post author's information shows up to the left of the post. This includes their avatar, display name, mention, and status.</li></ul>
|
||||
<ul><li>The post author's information shows up to the left of the post. This includes their avatar, display name, mention, status, and badges.</li></ul>
|
||||
</li>
|
||||
<li>Post actions
|
||||
<ul><li>This shows the time and date when the post has been made (or edited), and buttons for actions you can perform on the post, such as quoting or editing.</li></ul>
|
||||
@@ -71,6 +70,6 @@
|
||||
</ol>
|
||||
<p>You may edit or delete your posts after creating them. Edited posts will show when they were edited instead of when they were posted.</p>
|
||||
<p>You can quote another user's post by pressing the <button>Quote</button> button on that post. This will copy the contents of it into the reply box and mention the user.</p>
|
||||
<p>Posts are written in a markup language called Babycode. <a href="{{ url_for("guides.guide_page", category='user', slug='babycode') }}">More information on it can be found in a separate guide.</a></p>
|
||||
<p>Posts are written in a markup language called Babycode. <a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}">More information on it can be found in a separate guide.</a></p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<li>Their username and display name;</li>
|
||||
<li>Their status;</li>
|
||||
<li>Their signature;</li>
|
||||
<li>Their badges;</li>
|
||||
<li>Their stats:
|
||||
<ul>
|
||||
<li>Their permission level (regular user, moderator, etc);</li>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</li>
|
||||
<li>Status
|
||||
<ul>
|
||||
<li>If set, your status will show up below your @mention in a post's usercard. 100 characters limit, no <a href="{{ url_for("guides.guide_page", category='user', slug='babycode') }}">Babycodes.</a></li>
|
||||
<li>If set, your status will show up below your @mention in a post's usercard. 100 characters limit, no <a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}">Babycodes.</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Subscribe by default
|
||||
@@ -44,14 +44,42 @@
|
||||
</li>
|
||||
<li>Signature
|
||||
<ul>
|
||||
<li>If set, this signature will appear under all of your posts. <a href="{{ url_for("guides.guide_page", category='user', slug='babycode') }}">Babycode</a> is allowed (except @mentions).</li>
|
||||
<li>If set, this signature will appear under all of your posts. <a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}">Babycode</a> is allowed (except @mentions).</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="guide-section">
|
||||
<h2 id="password">Changing your password</h2>
|
||||
<p>You can change your password by typing it in the "New password" field and again in the "Confirm new password" field, then pressing the <button class="warn">Change password</button> button. The passwords in the two fields must match.</p>
|
||||
<p>You can change your password by typing your desired new password in the "New password" field and again in the "Confirm new password" field, then pressing the <button class="warn">Change password</button> button. The passwords in the two fields must match.</p>
|
||||
</section>
|
||||
<section class="guide-section">
|
||||
<h2 id="badges">Badges</h2>
|
||||
<p>Badges, also known as buttons, are 88x31 images that you can use to add more flair to your profile or link to other websites.</p>
|
||||
<p>Badges you set will be shown on the usercard in threads and on your profile page. You can have up to 10 badges.</p>
|
||||
<p>To add a badge, press the "Add badge" button. A badge editor will be added. A badge consists of three parts:</p>
|
||||
<ol>
|
||||
<li>Image
|
||||
<ul>
|
||||
<li>{{ config.SITE_NAME }} provides a selection of default badges that you can use. You may select one by using the dropdown, under the "Default" category.</li>
|
||||
<li>Alternatively, you may upload your own image. To do so, select the "Upload…" option in the dropdown, under the "Your uploads" category. A <button>Browse…</button> button will appear, letting you select a file. The image must be exactly 88x31 pixels and may not be over 500KB in size.<br>
|
||||
Custom badge images that you have uploaded before can be reused and will appear under "Your uploads". If a badge image you've uploaded before is not used by any of your badges, it will be deleted and you will have to upload it again if you wish to reuse it.<br>
|
||||
Other users can not use images you've uploaded as part of their badges unless they download it manually.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Label
|
||||
<ul>
|
||||
<li>The label will be shown when the badge is hovered over. It will also be the badge image's alt text.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Link
|
||||
<ul>
|
||||
<li>Optionally, you may turn the badge into a clickable button by providing a link. The link will open in a new tab when pressed, and will use <code class="inline-code">rel="me"</code> so you can use your profile as verification on platforms like Mastodon.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>If a badge is not valid, its editor and any invalid fields will have a dashed border.</p>
|
||||
<p>You can delete a badge by pressing the <button class="critical">X</button> button. Your changes are not saved until you press the <button>Save badges</button> button.</p>
|
||||
</section>
|
||||
<section class="guide-section">
|
||||
<h2 id="deleting">Deleting your account</h2>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}editing MOTD{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>Edit Message of the Day</h1>
|
||||
<p>The Message of the Day will show up on the main page and in every topic.</p>
|
||||
<form method="POST">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}moderation{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>Moderation actions</h1>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('mod.user_list') }}">User list</a></li>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>Change topics order</h1>
|
||||
<p>Drag topic titles to reoder them. Press submit when done. The topics will appear to users in the order set here.</p>
|
||||
<form method="post" id=topics-container>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}drafting a thread{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>New thread</h1>
|
||||
<form method="post">
|
||||
<label for="topic_id">Topic</label>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}creating a topic{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>Create topic</h1>
|
||||
<form method="post">
|
||||
<label for=name>Name</label>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}creating a topic{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>Editing topic {{ topic['name'] }}</h1>
|
||||
<form method="post">
|
||||
<label for=name>Name</label>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}managing bookmark collections{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<div class="darkbg">
|
||||
<h1>Manage bookmark collections</h1>
|
||||
<p>Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.</p>
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}delete confirmation{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg login-container">
|
||||
<div class="darkbg">
|
||||
<h1>Confirm account deletion</h1>
|
||||
<p>Are you sure you want to delete your account on {{ config.SITE_NAME }}? <strong>This action is irreversible.</strong> Your posts and threads will remain accessible to preserve history but will be de-personalized, showing up as authored by a system user. Posts that @mention you will also mention the system user instead.</p>
|
||||
<p>If you wish for any and all content relating to you to be removed, you will have to <a href="{{url_for("guides.contact")}}" target="_blank">contact {{ config.SITE_NAME }}'s administrators separately.</a></p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Log in{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg login-container">
|
||||
<div class="darkbg">
|
||||
<h1>Log in</h1>
|
||||
<form method="post">
|
||||
<label for="username">Username</label><br>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Reset password{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg login-container">
|
||||
<div class="darkbg">
|
||||
<h1>Reset password for {{username}}</h1>
|
||||
<p>Send this link to {{username}} to allow them to reset their password.</p>
|
||||
<form method="post">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}settings{% endblock %}
|
||||
{% block content %}
|
||||
{% set disable_avatar = not is_logged_in() %}
|
||||
<div class='darkbg settings-container'>
|
||||
<div class='darkbg'>
|
||||
<h1>User settings</h1>
|
||||
<div class="settings-grid">
|
||||
<fieldset class="hfc">
|
||||
@@ -15,7 +15,7 @@
|
||||
<input type='submit' value='Save avatar' {{ 'disabled' if disable_avatar else '' }}>
|
||||
<input type='submit' value='Clear avatar' formaction='{{ url_for('users.clear_avatar', username=active_user.username) }}' formnovalidate {{ 'disabled' if active_user.is_default_avatar() else '' }}>
|
||||
</div>
|
||||
<span>1MB maximum size. Avatar will be scaled down to fit a square.</span>
|
||||
<span>1MB maximum size. Avatar will be cropped to square.</span>
|
||||
</form>
|
||||
</fieldset>
|
||||
<fieldset class="hfc">
|
||||
@@ -53,6 +53,16 @@
|
||||
<input class="warn" type="submit" value="Change password">
|
||||
</form>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Badges</legend>
|
||||
<a href="{{ url_for('guides.guide_page', category='user-guides', slug='settings', _anchor='badges')}}">Badges help</a>
|
||||
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorForm" data-listeners="click input submit change">
|
||||
<form data-use="badgeEditorPrepareSubmit" data-init='loadBadgeEditor' data-receive='addBadge' method='post' enctype='multipart/form-data' action='{{ url_for('users.save_badges', username=active_user.username) }}'>
|
||||
<div>Loading badges…</div>
|
||||
<div>If badges fail to load, JS may be disabled.</div>
|
||||
</form>
|
||||
</bitty-7-0>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div>
|
||||
<a class="linkbutton critical" href="{{ url_for('users.delete_page', username=active_user.username) }}">Delete account</a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Sign up{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg login-container">
|
||||
<div class="darkbg">
|
||||
<h1>Sign up</h1>
|
||||
{% if inviter %}
|
||||
<p>You have been invited by <a href="{{ url_for('users.page', username=inviter.username) }}">{{ inviter.get_readable_name() }}</a> to join {{ config.SITE_NAME }}. Create an identity below.</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% from 'common/macros.html' import timestamp %}
|
||||
{% from 'common/macros.html' import timestamp, badge_button %}
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ target_user.get_readable_name() }}'s profile{% endblock %}
|
||||
{% block content %}
|
||||
@@ -54,6 +54,11 @@
|
||||
Signature:
|
||||
<div>{{ target_user.signature_rendered | safe }}</div>
|
||||
{% endif %}
|
||||
<div class="badges-container">
|
||||
{% for badge in target_user.get_badges() %}
|
||||
{{ badge_button(badge) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-page-stats">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
SITE_NAME = "Porom"
|
||||
SITE_NAME = "Pyrom"
|
||||
DISABLE_SIGNUP = false # if true, no one can sign up.
|
||||
|
||||
# if neither of the following two options is true,
|
||||
BIN
data/static/badges/link-bsky.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
data/static/badges/link-itch-io.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
data/static/badges/link-mastodon.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
data/static/badges/link-www.webp
Normal file
|
After Width: | Height: | Size: 1000 B |
BIN
data/static/badges/pride-asexual.webp
Normal file
|
After Width: | Height: | Size: 256 B |
BIN
data/static/badges/pride-intersex.webp
Normal file
|
After Width: | Height: | Size: 682 B |
BIN
data/static/badges/pride-lesbian.webp
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
data/static/badges/pride-nonbinary.webp
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
data/static/badges/pride-progress.webp
Normal file
|
After Width: | Height: | Size: 756 B |
BIN
data/static/badges/pride-six.webp
Normal file
|
After Width: | Height: | Size: 478 B |
BIN
data/static/badges/pride-trans.webp
Normal file
|
After Width: | Height: | Size: 402 B |
BIN
data/static/badges/pronoun-any-all.webp
Normal file
|
After Width: | Height: | Size: 676 B |
BIN
data/static/badges/pronoun-fae-faer.webp
Normal file
|
After Width: | Height: | Size: 772 B |
BIN
data/static/badges/pronoun-he-him.webp
Normal file
|
After Width: | Height: | Size: 616 B |
BIN
data/static/badges/pronoun-it-its.webp
Normal file
|
After Width: | Height: | Size: 582 B |
BIN
data/static/badges/pronoun-no-pronouns.webp
Normal file
|
After Width: | Height: | Size: 850 B |
BIN
data/static/badges/pronoun-she-her.webp
Normal file
|
After Width: | Height: | Size: 690 B |
BIN
data/static/badges/pronoun-they-them.webp
Normal file
|
After Width: | Height: | Size: 842 B |
BIN
data/static/badges/pronoun-xe-xem.webp
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
data/static/badges/pronoun-xe-xir.webp
Normal file
|
After Width: | Height: | Size: 620 B |
@@ -60,10 +60,11 @@ body {
|
||||
color: black;
|
||||
}
|
||||
|
||||
a:link {
|
||||
:where(a:link) {
|
||||
color: #c11c1c;
|
||||
}
|
||||
a:visited {
|
||||
|
||||
:where(a:visited) {
|
||||
color: #730c0c;
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ a:visited {
|
||||
font-size: 3rem;
|
||||
margin: 0 20px;
|
||||
text-decoration: none;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
@@ -133,7 +134,7 @@ a:visited {
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-columns: 230px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
@@ -710,7 +711,7 @@ blockquote {
|
||||
button, input[type=submit], .linkbutton {
|
||||
display: inline-block;
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
@@ -731,7 +732,7 @@ button.icon, input[type=submit].icon, .linkbutton.icon {
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: red;
|
||||
color: white !important;
|
||||
color: white;
|
||||
}
|
||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||
background-color: #ff3333;
|
||||
@@ -752,7 +753,7 @@ button.critical.icon, input[type=submit].critical.icon, .linkbutton.critical.ico
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||
background-color: rgb(251.8, 251.8, 163.8);
|
||||
@@ -774,7 +775,7 @@ button.warn.icon, input[type=submit].warn.icon, .linkbutton.warn.icon {
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
@@ -803,7 +804,7 @@ p {
|
||||
|
||||
.pagebutton {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
.pagebutton:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
@@ -843,16 +844,6 @@ p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.settings-container > * {
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.avatar-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -973,10 +964,6 @@ textarea {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1141,7 +1128,7 @@ textarea {
|
||||
|
||||
.tab-button {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
.tab-button:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
@@ -1302,7 +1289,7 @@ footer {
|
||||
|
||||
.reaction-button.active {
|
||||
background-color: #beb1ce;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
.reaction-button.active:hover {
|
||||
background-color: rgb(203, 192.6, 215.8);
|
||||
@@ -1473,8 +1460,66 @@ a.mention:hover, a.mention:visited:hover {
|
||||
.settings-grid fieldset {
|
||||
border: 1px solid white;
|
||||
border-radius: 4px;
|
||||
background-color: rgb(171.527945374, 172.0409719488, 170.8965280512);
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-badge-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.settings-badge-container:has(input:invalid) {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
.settings-badge-container input:invalid {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
|
||||
.settings-badge-file-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.settings-badge-file-picker input.hidden[type=file] {
|
||||
width: 100%;
|
||||
}
|
||||
.settings-badge-file-picker input.hidden[type=file]::file-selector-button {
|
||||
display: none;
|
||||
}
|
||||
.settings-badge-file-picker.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-badge-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
img.badge-button {
|
||||
min-width: 88px;
|
||||
min-height: 31px;
|
||||
max-width: 88px;
|
||||
max-height: 31px;
|
||||
}
|
||||
|
||||
.badges-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
@@ -60,10 +60,11 @@ body {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
a:link {
|
||||
:where(a:link) {
|
||||
color: #e87fe1;
|
||||
}
|
||||
a:visited {
|
||||
|
||||
:where(a:visited) {
|
||||
color: #ed4fb1;
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ a:visited {
|
||||
font-size: 3rem;
|
||||
margin: 0 20px;
|
||||
text-decoration: none;
|
||||
color: white !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
@@ -133,7 +134,7 @@ a:visited {
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-columns: 230px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
@@ -710,7 +711,7 @@ blockquote {
|
||||
button, input[type=submit], .linkbutton {
|
||||
display: inline-block;
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
@@ -731,7 +732,7 @@ button.icon, input[type=submit].icon, .linkbutton.icon {
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: #d53232;
|
||||
color: #e6e6e6 !important;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||
background-color: rgb(221.4, 91, 91);
|
||||
@@ -752,7 +753,7 @@ button.critical.icon, input[type=submit].critical.icon, .linkbutton.critical.ico
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #eaea6a;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||
background-color: rgb(238.2, 238.2, 135.8);
|
||||
@@ -774,7 +775,7 @@ button.warn.icon, input[type=submit].warn.icon, .linkbutton.warn.icon {
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
@@ -803,7 +804,7 @@ p {
|
||||
|
||||
.pagebutton {
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.pagebutton:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
@@ -843,16 +844,6 @@ p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.settings-container > * {
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.avatar-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -973,10 +964,6 @@ textarea {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1141,7 +1128,7 @@ textarea {
|
||||
|
||||
.tab-button {
|
||||
background-color: #3c283c;
|
||||
color: #e6e6e6 !important;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.tab-button:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
@@ -1302,7 +1289,7 @@ footer {
|
||||
|
||||
.reaction-button.active {
|
||||
background-color: #8a5584;
|
||||
color: #e6e6e6 !important;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.reaction-button.active:hover {
|
||||
background-color: rgb(167.4843049327, 112.9156950673, 161.3067264574);
|
||||
@@ -1473,12 +1460,70 @@ a.mention:hover, a.mention:visited:hover {
|
||||
.settings-grid fieldset {
|
||||
border: 1px solid black;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(141.6, 79.65, 141.6);
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-badge-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
border: 1px solid black;
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.settings-badge-container:has(input:invalid) {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
.settings-badge-container input:invalid {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
|
||||
.settings-badge-file-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.settings-badge-file-picker input.hidden[type=file] {
|
||||
width: 100%;
|
||||
}
|
||||
.settings-badge-file-picker input.hidden[type=file]::file-selector-button {
|
||||
display: none;
|
||||
}
|
||||
.settings-badge-file-picker.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-badge-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
img.badge-button {
|
||||
min-width: 88px;
|
||||
min-height: 31px;
|
||||
max-width: 88px;
|
||||
max-height: 31px;
|
||||
}
|
||||
|
||||
.badges-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
margin-bottom: 10px;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
|
||||
@@ -60,10 +60,11 @@ body {
|
||||
color: black;
|
||||
}
|
||||
|
||||
a:link {
|
||||
:where(a:link) {
|
||||
color: black;
|
||||
}
|
||||
a:visited {
|
||||
|
||||
:where(a:visited) {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ a:visited {
|
||||
font-size: 3rem;
|
||||
margin: 0 12px;
|
||||
text-decoration: none;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
@@ -133,7 +134,7 @@ a:visited {
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-columns: 230px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
@@ -710,7 +711,7 @@ blockquote {
|
||||
button, input[type=submit], .linkbutton {
|
||||
display: inline-block;
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
@@ -731,7 +732,7 @@ button.icon, input[type=submit].icon, .linkbutton.icon {
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: #f73030;
|
||||
color: white !important;
|
||||
color: white;
|
||||
}
|
||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||
background-color: rgb(248.6, 89.4, 89.4);
|
||||
@@ -752,7 +753,7 @@ button.critical.icon, input[type=submit].critical.icon, .linkbutton.critical.ico
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||
background-color: rgb(251.8, 251.8, 163.8);
|
||||
@@ -774,7 +775,7 @@ button.warn.icon, input[type=submit].warn.icon, .linkbutton.warn.icon {
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
@@ -803,7 +804,7 @@ p {
|
||||
|
||||
.pagebutton {
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
.pagebutton:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
@@ -843,16 +844,6 @@ p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.settings-container > * {
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.avatar-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -973,10 +964,6 @@ textarea {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1141,7 +1128,7 @@ textarea {
|
||||
|
||||
.tab-button {
|
||||
background-color: #f27a5a;
|
||||
color: black !important;
|
||||
color: black;
|
||||
}
|
||||
.tab-button:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
@@ -1302,7 +1289,7 @@ footer {
|
||||
|
||||
.reaction-button.active {
|
||||
background-color: #b54444;
|
||||
color: white !important;
|
||||
color: white;
|
||||
}
|
||||
.reaction-button.active:hover {
|
||||
background-color: rgb(197.978313253, 103.221686747, 103.221686747);
|
||||
@@ -1473,20 +1460,76 @@ a.mention:hover, a.mention:visited:hover {
|
||||
.settings-grid fieldset {
|
||||
border: 1px solid white;
|
||||
border-radius: 16px;
|
||||
background-color: rgb(176.5961538462, 106.9038461538, 147.1947115385);
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-badge-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 3px;
|
||||
border: 1px solid black;
|
||||
border-radius: 16px;
|
||||
padding: 3px 6px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.settings-badge-container:has(input:invalid) {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
.settings-badge-container input:invalid {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
|
||||
.settings-badge-file-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.settings-badge-file-picker input.hidden[type=file] {
|
||||
width: 100%;
|
||||
}
|
||||
.settings-badge-file-picker input.hidden[type=file]::file-selector-button {
|
||||
display: none;
|
||||
}
|
||||
.settings-badge-file-picker.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-badge-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
img.badge-button {
|
||||
min-width: 88px;
|
||||
min-height: 31px;
|
||||
max-width: 88px;
|
||||
max-height: 31px;
|
||||
}
|
||||
|
||||
.badges-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -1494,9 +1537,10 @@ textarea {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
#footer {
|
||||
border-radius: 16px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
1525
data/static/css/theme-snow-white.css
Normal file
@@ -1,4 +1,5 @@
|
||||
const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown';
|
||||
const badgeEditorEndpoint = '/hyperapi/badge-editor';
|
||||
const previewEndpoint = '/api/babycode-preview';
|
||||
const userEndpoint = '/api/current-user';
|
||||
|
||||
@@ -277,3 +278,200 @@ export default class {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BadgeEditorForm {
|
||||
#badgeTemplate = undefined;
|
||||
async loadBadgeEditor(ev, el) {
|
||||
const badges = await this.api.getHTML(badgeEditorEndpoint);
|
||||
if (!badges.value) {
|
||||
return;
|
||||
}
|
||||
if (this.#badgeTemplate === undefined){
|
||||
const badge = await this.api.getHTML(`${badgeEditorEndpoint}/template`)
|
||||
if (!badge.value){
|
||||
return;
|
||||
}
|
||||
this.#badgeTemplate= badge.value;
|
||||
}
|
||||
el.replaceChildren();
|
||||
const addButton = `<button data-disable-if-max="1" data-receive="updateBadgeCount" DISABLE_IF_MAX type="button" data-send="addBadge">Add badge</button>`;
|
||||
const submitButton = `<input data-receive="updateBadgeCount" type="submit" value="Save badges">`;
|
||||
const controls = `<span>${addButton} ${submitButton} <span data-count="1" data-receive="updateBadgeCount">BADGECOUNT/10</span></span>`
|
||||
const badgeCount = badges.value.querySelectorAll('.settings-badge-container').length;
|
||||
const subs = [
|
||||
['BADGECOUNT', badgeCount],
|
||||
['DISABLE_IF_MAX', badgeCount === 10 ? 'disabled' : ''],
|
||||
];
|
||||
el.appendChild(this.api.makeHTML(controls, subs));
|
||||
el.appendChild(badges.value);
|
||||
}
|
||||
|
||||
addBadge(ev, el) {
|
||||
if (this.#badgeTemplate === undefined) {
|
||||
return;
|
||||
}
|
||||
const badge = this.#badgeTemplate.cloneNode(true);
|
||||
el.appendChild(badge);
|
||||
this.api.localTrigger('updateBadgeCount');
|
||||
}
|
||||
|
||||
deleteBadge(ev, el) {
|
||||
if (!el.contains(el.sender)) {
|
||||
return;
|
||||
}
|
||||
el.remove();
|
||||
this.api.localTrigger('updateBadgeCount');
|
||||
}
|
||||
|
||||
updateBadgeCount(_ev, el) {
|
||||
const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length;
|
||||
if (el.dsInt('disableIfMax') === 1) {
|
||||
el.disabled = badgeCount === 10;
|
||||
} else if (el.dsInt('count') === 1) {
|
||||
el.textContent = `${badgeCount}/10`;
|
||||
}
|
||||
}
|
||||
|
||||
badgeEditorPrepareSubmit(ev, el) {
|
||||
if (ev.type !== 'submit') {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
const badges = el.querySelectorAll('.settings-badge-container').length;
|
||||
|
||||
const noUploads = el.querySelectorAll('.settings-badge-file-picker.hidden input[type=file]');
|
||||
noUploads.forEach(e => {
|
||||
e.value = null;
|
||||
})
|
||||
// console.log(noUploads);
|
||||
el.submit();
|
||||
// console.log('would submit now');
|
||||
}
|
||||
}
|
||||
|
||||
const validateBase64Img = dataURL => new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
resolve(img.width === 88 && img.height === 31);
|
||||
};
|
||||
img.src = dataURL;
|
||||
});
|
||||
|
||||
export class BadgeEditorBadge {
|
||||
#badgeCustomImageData = null;
|
||||
badgeUpdatePreview(ev, el) {
|
||||
if (ev.type !== 'change') {
|
||||
return;
|
||||
}
|
||||
// TODO: el.sender doesn't have a bittyParentBittyId
|
||||
const selectBittyParent = el.sender.closest('bitty-7-0');
|
||||
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.val === 'custom') {
|
||||
if (this.#badgeCustomImageData) {
|
||||
el.src = this.#badgeCustomImageData;
|
||||
} else {
|
||||
el.removeAttribute('src');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const option = el.sender.selectedOptions[0];
|
||||
el.src = option.dataset.filePath;
|
||||
}
|
||||
|
||||
async badgeUpdatePreviewCustom(ev, el) {
|
||||
if (ev.type !== 'change') {
|
||||
return;
|
||||
}
|
||||
if (el.bittyParentBittyId !== el.sender.bittyParentBittyId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = ev.target.files[0];
|
||||
if (file.size >= 1000 * 500) {
|
||||
this.api.trigger('badgeErrorSize');
|
||||
this.#badgeCustomImageData = null;
|
||||
el.removeAttribute('src');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async e => {
|
||||
const dimsValid = await validateBase64Img(e.target.result);
|
||||
if (!dimsValid) {
|
||||
this.api.trigger('badgeErrorDim');
|
||||
this.#badgeCustomImageData = null;
|
||||
el.removeAttribute('src');
|
||||
return;
|
||||
}
|
||||
this.#badgeCustomImageData = e.target.result;
|
||||
el.src = this.#badgeCustomImageData;
|
||||
this.api.trigger('badgeHideErrors');
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
badgeToggleFilePicker(ev, el) {
|
||||
if (ev.type !== 'change') {
|
||||
return;
|
||||
}
|
||||
// TODO: el.sender doesn't have a bittyParentBittyId
|
||||
const selectBittyParent = el.sender.closest('bitty-7-0');
|
||||
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
|
||||
return;
|
||||
}
|
||||
const filePicker = el.querySelector('input[type=file]');
|
||||
if (ev.val === 'custom') {
|
||||
el.classList.remove('hidden');
|
||||
if (filePicker.dataset.validity) {
|
||||
filePicker.setCustomValidity(filePicker.dataset.validity);
|
||||
}
|
||||
filePicker.required = true;
|
||||
} else {
|
||||
el.classList.add('hidden');
|
||||
filePicker.setCustomValidity('');
|
||||
filePicker.required = false;
|
||||
}
|
||||
}
|
||||
|
||||
openBadgeFilePicker(ev, el) {
|
||||
// TODO: el.sender doesn't have a bittyParentBittyId
|
||||
if (el.sender.parentNode !== el.parentNode) {
|
||||
return;
|
||||
}
|
||||
el.click();
|
||||
}
|
||||
|
||||
badgeErrorSize(_ev, el) {
|
||||
if (el.sender !== el.bittyParent) {
|
||||
return;
|
||||
}
|
||||
const validity = "Image can't be over 500KB."
|
||||
el.dataset.validity = validity;
|
||||
el.setCustomValidity(validity);
|
||||
el.reportValidity();
|
||||
}
|
||||
|
||||
badgeErrorDim(_ev, el) {
|
||||
if (el.sender !== el.bittyParent) {
|
||||
return;
|
||||
}
|
||||
const validity = "Image must be exactly 88x31 pixels."
|
||||
el.dataset.validity = validity;
|
||||
el.setCustomValidity(validity);
|
||||
el.reportValidity();
|
||||
}
|
||||
|
||||
badgeHideErrors(_ev, el) {
|
||||
if (el.sender !== el.bittyParent) {
|
||||
return;
|
||||
}
|
||||
delete el.dataset.validity;
|
||||
el.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
$ACCENT_COLOR: #c1ceb1 !default;
|
||||
|
||||
$DARK_1: color.scale($ACCENT_COLOR, $lightness: -25%, $saturation: -97%) !default;
|
||||
$DARK_1_LIGHTER: color.scale($DARK_1, $lightness: 25%);
|
||||
$DARK_2: color.scale($ACCENT_COLOR, $lightness: -30%, $saturation: -60%) !default;
|
||||
$DARK_3: color.scale($ACCENT_COLOR, $lightness: -80%, $saturation: -70%) !default;
|
||||
|
||||
@@ -51,8 +52,6 @@ $BIGGER_PADDING: 30px !default;
|
||||
|
||||
$PAGE_SIDE_MARGIN: 100px !default;
|
||||
|
||||
$SETTINGS_WIDTH: 85% !default;
|
||||
|
||||
// **************
|
||||
// BORDERS
|
||||
// **************
|
||||
@@ -134,7 +133,7 @@ $icon_button_padding_left: $BIG_PADDING - 4px !default;
|
||||
@mixin button($color, $font_color) {
|
||||
@extend %button-base;
|
||||
background-color: $color;
|
||||
color: $font_color !important; //!important because linkbutton is an <a>
|
||||
color: $font_color;
|
||||
|
||||
&:hover {
|
||||
background-color: color.scale($color, $lightness: 20%);
|
||||
@@ -181,13 +180,11 @@ body {
|
||||
|
||||
$link_color: #c11c1c !default;
|
||||
$link_color_visited: #730c0c !default;
|
||||
a{
|
||||
&:link {
|
||||
color: $link_color;
|
||||
}
|
||||
&:visited {
|
||||
color: $link_color_visited;
|
||||
}
|
||||
:where(a:link){
|
||||
color: $link_color;
|
||||
}
|
||||
:where(a:visited) {
|
||||
color: $link_color_visited;
|
||||
}
|
||||
|
||||
.big {
|
||||
@@ -234,7 +231,7 @@ $site_title_color: $DEFAULT_FONT_COLOR !default;
|
||||
font-size: $site_title_size;
|
||||
margin: $site_title_margin;
|
||||
text-decoration: none;
|
||||
color: $site_title_color !important;
|
||||
color: $site_title_color;
|
||||
}
|
||||
|
||||
$thread_title_margin: $ZERO_PADDING !default;
|
||||
@@ -252,7 +249,7 @@ $thread_actions_gap: $SMALL_PADDING !default;
|
||||
gap: $thread_actions_gap;
|
||||
}
|
||||
|
||||
$post_usercard_width: 200px !default;
|
||||
$post_usercard_width: 230px !default;
|
||||
$post_border: 2px outset $DARK_2 !default;
|
||||
.post {
|
||||
display: grid;
|
||||
@@ -681,18 +678,6 @@ $pagebutton_min_width: $BIG_PADDING !default;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
$login_container_width: $SETTINGS_WIDTH !default;
|
||||
.login-container > * {
|
||||
width: $login_container_width;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
$settings_container_width: $SETTINGS_WIDTH !default;
|
||||
.settings-container > * {
|
||||
width: $settings_container_width;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
$avatar_form_padding: $BIG_PADDING $ZERO_PADDING !default;
|
||||
.avatar-form {
|
||||
display: flex;
|
||||
@@ -849,10 +834,6 @@ $thread_info_header_gap: $SMALL_PADDING !default;
|
||||
gap: $thread_info_header_gap;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important; // :(
|
||||
}
|
||||
|
||||
$thread_info_post_preview_margin_right: $post_inner_padding_right !default;
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
@@ -1447,9 +1428,86 @@ $settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||
& fieldset {
|
||||
border: $settings_grid_fieldset_border;
|
||||
border-radius: $settings_grid_fieldset_border_radius;
|
||||
background-color: $DARK_1_LIGHTER;
|
||||
}
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
$compact_h1_margin: $ZERO_PADDING !default;
|
||||
h1 {
|
||||
margin: $compact_h1_margin;
|
||||
}
|
||||
|
||||
$settings_badge_container_gap: $SMALL_PADDING !default;
|
||||
$settings_badge_container_border: $DEFAULT_BORDER !default;
|
||||
$settings_badge_container_border_invalid: 2px dashed red !default;
|
||||
$settings_badge_container_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||
$settings_badge_container_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
|
||||
$settings_badge_container_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
|
||||
.settings-badge-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: $settings_badge_container_gap;
|
||||
border: $settings_badge_container_border;
|
||||
border-radius: $settings_badge_container_border_radius;
|
||||
padding: $settings_badge_container_padding;
|
||||
margin: $settings_badge_container_margin;
|
||||
|
||||
// the file picker's validity is managed by js
|
||||
// so we got lucky here. when the file picker
|
||||
// is hidden, its set to be valid. it's only invalid
|
||||
// when, well, invalid.
|
||||
&:has(input:invalid) {
|
||||
border: $settings_badge_container_border_invalid;
|
||||
}
|
||||
|
||||
input:invalid {
|
||||
border: $settings_badge_container_border_invalid;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-badge-file-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
& input.hidden[type=file] {
|
||||
width: 100%;
|
||||
|
||||
&::file-selector-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
$settings_badge_select_gap: $SMALL_PADDING !default;
|
||||
$settings_badge_select_min_width: 200px !default;
|
||||
.settings-badge-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $settings_badge_select_gap;
|
||||
align-items: center;
|
||||
min-width: $settings_badge_select_min_width;
|
||||
}
|
||||
|
||||
img.badge-button {
|
||||
min-width: 88px;
|
||||
min-height: 31px;
|
||||
max-width: 88px;
|
||||
max-height: 31px;
|
||||
}
|
||||
|
||||
$badges_container_gap: $SMALL_PADDING !default;
|
||||
.badges-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: $badges_container_gap;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ $br: 8px;
|
||||
$bookmarks_dropdown_background_color: $lightish_accent,
|
||||
|
||||
$mention_font_color: $fc,
|
||||
|
||||
// $settings_badge_container_border_invalid: 2px dashed $crit,
|
||||
);
|
||||
|
||||
#topnav {
|
||||
|
||||
@@ -73,9 +73,6 @@ $br: 16px;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
border-bottom-left-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -83,9 +80,10 @@ textarea {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
#footer {
|
||||
border-radius: $br;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
7
sass/snow-white.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
// a simple light theme
|
||||
|
||||
@use 'default' with (
|
||||
$ACCENT_COLOR: #ced9ee,
|
||||
$link_color: #711579,
|
||||
$link_color_visited: #4a144f,
|
||||
)
|
||||