From dbf0150a5e91d108cab13391fda9516a42d0c558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Tue, 9 Dec 2025 03:33:27 +0300 Subject: [PATCH] add badges --- .gitignore | 1 + app/__init__.py | 41 +++- app/models.py | 55 ++++- app/routes/api.py | 4 +- app/routes/hyperapi.py | 21 +- app/routes/users.py | 111 +++++++++- app/schema.py | 20 ++ app/templates/common/macros.html | 53 +++++ .../components/badge_editor_badges.html | 4 + .../components/badge_editor_template.html | 2 + .../guides/user-guides/01-introduction.html | 2 +- .../guides/user-guides/02-profiles.html | 1 + .../guides/user-guides/03-settings.html | 30 ++- app/templates/users/settings.html | 10 + app/templates/users/user.html | 7 +- data/static/badges/link-bsky.webp | Bin 0 -> 1290 bytes data/static/badges/link-itch-io.webp | Bin 0 -> 1358 bytes data/static/badges/link-mastodon.webp | Bin 0 -> 1248 bytes data/static/badges/link-www.webp | Bin 0 -> 1000 bytes data/static/badges/pride-asexual.webp | Bin 0 -> 256 bytes data/static/badges/pride-intersex.webp | Bin 0 -> 682 bytes data/static/badges/pride-lesbian.webp | Bin 0 -> 394 bytes data/static/badges/pride-nonbinary.webp | Bin 0 -> 274 bytes data/static/badges/pride-progress.webp | Bin 0 -> 756 bytes data/static/badges/pride-six.webp | Bin 0 -> 478 bytes data/static/badges/pride-trans.webp | Bin 0 -> 402 bytes data/static/badges/pronoun-any-all.webp | Bin 0 -> 676 bytes data/static/badges/pronoun-fae-faer.webp | Bin 0 -> 772 bytes data/static/badges/pronoun-he-him.webp | Bin 0 -> 616 bytes data/static/badges/pronoun-it-its.webp | Bin 0 -> 582 bytes data/static/badges/pronoun-no-pronouns.webp | Bin 0 -> 850 bytes data/static/badges/pronoun-she-her.webp | Bin 0 -> 690 bytes data/static/badges/pronoun-they-them.webp | Bin 0 -> 842 bytes data/static/badges/pronoun-xe-xem.webp | Bin 0 -> 658 bytes data/static/badges/pronoun-xe-xir.webp | Bin 0 -> 620 bytes data/static/css/style.css | 80 +++++-- data/static/css/theme-otomotone.css | 80 +++++-- data/static/css/theme-peachy.css | 87 ++++++-- data/static/css/theme-snow-white.css | 114 +++++++--- data/static/js/bitties/pyrom-bitty.js | 198 ++++++++++++++++++ sass/_default.scss | 93 ++++++-- sass/otomotone.scss | 2 + sass/peachy.scss | 8 +- 43 files changed, 913 insertions(+), 111 deletions(-) create mode 100644 app/templates/components/badge_editor_badges.html create mode 100644 app/templates/components/badge_editor_template.html create mode 100644 data/static/badges/link-bsky.webp create mode 100644 data/static/badges/link-itch-io.webp create mode 100644 data/static/badges/link-mastodon.webp create mode 100644 data/static/badges/link-www.webp create mode 100644 data/static/badges/pride-asexual.webp create mode 100644 data/static/badges/pride-intersex.webp create mode 100644 data/static/badges/pride-lesbian.webp create mode 100644 data/static/badges/pride-nonbinary.webp create mode 100644 data/static/badges/pride-progress.webp create mode 100644 data/static/badges/pride-six.webp create mode 100644 data/static/badges/pride-trans.webp create mode 100644 data/static/badges/pronoun-any-all.webp create mode 100644 data/static/badges/pronoun-fae-faer.webp create mode 100644 data/static/badges/pronoun-he-him.webp create mode 100644 data/static/badges/pronoun-it-its.webp create mode 100644 data/static/badges/pronoun-no-pronouns.webp create mode 100644 data/static/badges/pronoun-she-her.webp create mode 100644 data/static/badges/pronoun-they-them.webp create mode 100644 data/static/badges/pronoun-xe-xem.webp create mode 100644 data/static/badges/pronoun-xe-xir.webp diff --git a/.gitignore b/.gitignore index a80f74c..fbab352 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ data/db/* data/static/avatars/* !data/static/avatars/default.webp +data/static/badges/user config/secrets.prod.env config/pyrom_config.toml diff --git a/app/__init__.py b/app/__init__.py index d20c74d..aeb1545 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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,6 +100,31 @@ 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['SITE_NAME'] = 'Pyrom' @@ -123,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['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 = [] @@ -150,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 @@ -237,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/'): @@ -269,4 +304,8 @@ def create_app(): return f'{subject.removeprefix('theme-').replace('-', ' ').capitalize()} (beta)' + @app.template_filter('fromjson') + def fromjson(subject: str): + return json.loads(subject) + return app diff --git a/app/models.py b/app/models.py index 7554c69..509d260 100644 --- a/app/models.py +++ b/app/models.py @@ -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 diff --git a/app/routes/api.py b/app/routes/api.py index bfd671b..0b5eca6 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -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/") diff --git a/app/routes/hyperapi.py b/app/routes/hyperapi.py index f622cca..c9f6dff 100644 --- a/app/routes/hyperapi.py +++ b/app/routes/hyperapi.py @@ -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 "

forbidden

", 403 -@bp.get('bookmarks-dropdown/') +@bp.get('/bookmarks-dropdown/') @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) diff --git a/app/routes/users.py b/app/routes/users.py index fe16432..a9b08d4 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -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 @@ -21,6 +21,7 @@ import re import os AVATAR_MAX_SIZE = 1000 * 1000 +BADGE_MAX_SIZE = 1000 * 500 bp = Blueprint("users", __name__, url_prefix = "/users/") @@ -56,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 @@ -855,3 +871,96 @@ def delete_page_confirm(username): session.clear() target_user.delete() return redirect(url_for('topics.all_topics')) + + +@bp.post('//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)) diff --git a/app/schema.py b/app/schema.py index ce58870..5273002 100644 --- a/app/schema.py +++ b/app/schema.py @@ -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(): diff --git a/app/templates/common/macros.html b/app/templates/common/macros.html index 0c0ae13..5241971 100644 --- a/app/templates/common/macros.html +++ b/app/templates/common/macros.html @@ -138,6 +138,17 @@ {% endmacro %} +{% macro badge_button(badge) %} +{% set img_url = badge.file_path if badge.file_path else badge.get_image_url() %} +{% if badge.link %} + +{% endif %} +{{badge.label}} +{% if badge.link %} + +{% 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'] %} {{ post['status'] }} {% endif %} +
+ {% for badge_data in (post.badges_json | fromjson) %} + {{ badge_button(badge_data) }} + {% endfor %} +
@@ -306,3 +322,40 @@ {% 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 %} + +
+
+ + +
+
+ + +
+ + + +
+
+{% endmacro %} diff --git a/app/templates/components/badge_editor_badges.html b/app/templates/components/badge_editor_badges.html new file mode 100644 index 0000000..ddabdf5 --- /dev/null +++ b/app/templates/components/badge_editor_badges.html @@ -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 %} diff --git a/app/templates/components/badge_editor_template.html b/app/templates/components/badge_editor_template.html new file mode 100644 index 0000000..ef9f881 --- /dev/null +++ b/app/templates/components/badge_editor_template.html @@ -0,0 +1,2 @@ +{% from 'common/macros.html' import badge_editor_single with context %} +{{ badge_editor_single(options=uploads) }} diff --git a/app/templates/guides/user-guides/01-introduction.html b/app/templates/guides/user-guides/01-introduction.html index 1f711ee..d11e001 100644 --- a/app/templates/guides/user-guides/01-introduction.html +++ b/app/templates/guides/user-guides/01-introduction.html @@ -53,7 +53,7 @@

A post is split up into five sections:

  1. Usercard -
    • The post author's information shows up to the left of the post. This includes their avatar, display name, mention, and status.
    +
    • The post author's information shows up to the left of the post. This includes their avatar, display name, mention, status, and badges.
  2. Post actions
    • 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.
    diff --git a/app/templates/guides/user-guides/02-profiles.html b/app/templates/guides/user-guides/02-profiles.html index 55f2f1d..68937e9 100644 --- a/app/templates/guides/user-guides/02-profiles.html +++ b/app/templates/guides/user-guides/02-profiles.html @@ -10,6 +10,7 @@
  3. Their username and display name;
  4. Their status;
  5. Their signature;
  6. +
  7. Their badges;
  8. Their stats:
    • Their permission level (regular user, moderator, etc);
    • diff --git a/app/templates/guides/user-guides/03-settings.html b/app/templates/guides/user-guides/03-settings.html index 5170de1..cedd4c6 100644 --- a/app/templates/guides/user-guides/03-settings.html +++ b/app/templates/guides/user-guides/03-settings.html @@ -51,7 +51,35 @@

      Changing your password

      -

      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. The passwords in the two fields must match.

      +

      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. The passwords in the two fields must match.

      +
      +
      +

      Badges

      +

      Badges, also known as buttons, are 88x31 images that you can use to add more flair to your profile or link to other websites.

      +

      Badges you set will be shown on the usercard in threads and on your profile page. You can have up to 10 badges.

      +

      To add a badge, press the "Add badge" button. A badge editor will be added. A badge consists of three parts:

      +
        +
      1. Image +
          +
        • {{ 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.
        • +
        • Alternatively, you may upload your own image. To do so, select the "Upload…" option in the dropdown, under the "Your uploads" category. A button will appear, letting you select a file. The image must be exactly 88x31 pixels and may not be over 500KB in size.
          + 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.
          + Other users can not use images you've uploaded as part of their badges unless they download it manually.
        • +
        +
      2. +
      3. Label +
          +
        • The label will be shown when the badge is hovered over. It will also be the badge image's alt text.
        • +
        +
      4. +
      5. Link +
          +
        • 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 rel="me" so you can use your profile as verification on platforms like Mastodon.
        • +
        +
      6. +
      +

      If a badge is not valid, its editor and any invalid fields will have a dashed border.

      +

      You can delete a badge by pressing the button. Your changes are not saved until you press the button.

      Deleting your account

      diff --git a/app/templates/users/settings.html b/app/templates/users/settings.html index 056c8c4..ded0f40 100644 --- a/app/templates/users/settings.html +++ b/app/templates/users/settings.html @@ -53,6 +53,16 @@ +
      + Badges + Badges help + +
      +
      Loading badges…
      +
      If badges fail to load, JS may be disabled.
      +
      +
      +
      Delete account diff --git a/app/templates/users/user.html b/app/templates/users/user.html index 619d568..06032ea 100644 --- a/app/templates/users/user.html +++ b/app/templates/users/user.html @@ -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:
      {{ target_user.signature_rendered | safe }}
      {% endif %} +
      + {% for badge in target_user.get_badges() %} + {{ badge_button(badge) }} + {% endfor %} +
      diff --git a/data/static/badges/link-bsky.webp b/data/static/badges/link-bsky.webp new file mode 100644 index 0000000000000000000000000000000000000000..0897694771f5c32afbbab43ec3dacd0bcf28e844 GIT binary patch literal 1290 zcmV+l1@-z;Nk&Ej1pok7MM6+kP&gp=1ONaq9RQsHDp&v?06sAmi9;eGAr`5fBp?F> zvH)Ib*L|~O$Lt1I2CDsld;s+|_N%@__>Z6u)n7^v(!X#YsNdKBc0?SkFeF7dF^_TYpKzG(JtbRRcqW-=48Tog|bIHG-|Azdp{D9;;=3D){ z@>iMmfFF-P$9_P6fcUTfPx1rTU(SE>f5dzP|0MpO{!jBa{5SvpRDaiCPFdUB5mx!t z?2w-?{1;2u#}_Xvq?ER+tBi7-NX)O|_&~2SXNAX){IQH-+=m2<1YK*Ov8xPDwti~t zwx1uf?5omQo*C&H65|Q+0j$JpZYAe!6rg{H))$^E;f5z?h(GQ$O+;?83kY z#xt$HMXk(QFGt_biS}v6g>W2Hn=I0{;Y?vKB0u=MA&rrvu*yA|KMxkVk98Z;XY~j@9t|l*OTR)!TB~9XQ z=WZ$+T&;Y1D`@c5P;*9#2Sc_{M@t(W;%u}H==lNs41+U-yZd{+KUp7O7D4t4+df(Qv$S$hEqFKDx=8`1`Y&gr3ledWu z@UeF`iIB#%5|kEWLg9+*oOkCS0CbI!GAx@oFq>=%RVbH>BoPQn+F$*GzGH3m$oo7(BMxAKwwtMU+#9lz-o=vwTS%VxIT`H9C9Sv~ zb{bgbZvSDLw)eP@a1)jm?mT7#|6diFCau@|Pye*rZF9E?&|qb-VYIlMkC3%%Fa8|4 z86V1uTWpu5!#KqR$RyvWj6*vw2vcET>n>7vSoN}H2i78()w>%4toRG;yM?ta6!3Z& zZPNo}Ic4?c8NpbY)W^J+zTEz>N|4jk^WOE=i$0+Xp?Zj0l{{z>(XT|{L+^Iv`?Je3 zIf+6uB)hQ(AbG{0YTU5(Ybs(`kEr>uzsD5@d(ThMcz`!q7tcyl<|tuRN|mek&aeF% z$jAuiZpbZ>+v~PvlYP=CQ##RICy;w>weleEPWqGfvZy zjgEH$11^?-|JnKf<)xSz(=9LwEO5DHjPT36I}CPLs}J3=!=sDLMfE8cgAOANf&0u1 zzG5Sv|JQi0GP?n%+poQdlF4$2B$!+J4ENIkIL2{P;WHA7+lA2_ZvZXo3q6>h=RWNs zyu>_}FfS5now$k-N$|xV8E7uVBINk2uwiVh)@ffC)4P_!g4k4l$LN0UDb=CuFKBvGjtuFn<_k zN>V#JmU8iLO{K>hL5sJNNio0m_A;-o3hsytfJd^D_c^>@lXz>Ik8F~I8aRUh0O;V6 Aw*UYD literal 0 HcmV?d00001 diff --git a/data/static/badges/link-itch-io.webp b/data/static/badges/link-itch-io.webp new file mode 100644 index 0000000000000000000000000000000000000000..ff109775bf30769741a2256f9ad820e926b400ea GIT binary patch literal 1358 zcmV-U1+n^4Nk&FS1pok7MM6+kP&gnu1polB8~~jGDp&v?06sAmi9@0xArV#3AOi%m zfLt;3WRgR@$0s3}-+RypZhfmXuzREcZ`NAwdDDeUq8S~hu4 zBVSQSumJx57d6CAHZ=-SEdjo=Q<{^Q&tIGpouuVkn$LYkNoHHQ=G^d4ZB0l~@YMz^EtUl$cpDiuz7b8_IxqkKM}n|*&Lm>% z*SeOeB@@5bQ5gP6UORfVM;Z@u2}L%3Qh`L6^#P?Gp?(74Z_(2EY)FSxw1znrO~A~z z2lT(1oR}oVw=*KCtRHiatTb*uZ*kje+L*H_>j3L3s<`&Pky}vLKgWs5H|>!Sm!mW& zKm8FjHw})NbwSfOUHdIa=dJwF#?x16W9zlHPG zj<7UUkC)Zh|2y2EiNnQJ)P-3effVKX43Fw>Gz%hrwnQ%Gb*|n?CC&7yr-mL#MKv)f z{m&{ONCrCxrP@#bG+%?hjzeE}zsLibbJ5G3J=A1gj@3@IbFeFKl7DUeM=FyRHW-{p zu&zi!_AN#K^?Th{XK5|AvkHFWBltPBU;WpSQk{vU`u~DT85;Pg2&eBB>6Tu28=VNM z5xP2dL(qyZ8V;I4Wn+VOFg7QEf#aaj4IXk zOjs*4_g`zEyA8lgTTf!}kQB4+j@e#)m@vrqLFBFL-!ZPt)>Qx#=jkqTY7CIYG#J?X zVrwa(CqvI-$>k_tMmL$T+y4Xo$6xg3)DYMshXFPjV<~SU3y32;1^s)Sa+e{-ng4fZ zVh7(8skIU}>wjg)<`T?2dDrA+*<6?g)@z(#)jlrgq_$3hqBA8xojDFZrPwr~JgRA$ zXiR=zLr=udnEMQsjs+RHSYHx3bME^bHk;(V(Dudsyk}{(!P+nLu<}hTa&;7*f9RPx zHJ3n9H+)wkk+8z?)wD@a`5iL%R~$<#PlHZD6RED>IXjBx$bTfxW-p%_L`n>NyXUv^%faWe87Np4Qm43F?%x9w_DQzZuHE}3W&^Gd z8)UM0;nhjOXw_ z0@!(`TRCfamh+zjr475VlHZ$SGg@z3M|$79VNSHq)(zJ?%d%b;*ajDHbIc1UU zBJ~paIAY3IzL}!bDyAP^>8ueudvp3-|Gx{)c$YvWzB2=bJ;=o`WJzv6X|1J3XxE(? QM{d2Pn)L^h1j5-s01w2UHvj+t literal 0 HcmV?d00001 diff --git a/data/static/badges/link-mastodon.webp b/data/static/badges/link-mastodon.webp new file mode 100644 index 0000000000000000000000000000000000000000..2b882031224214fe0e6f9a9e08ed17717560fd81 GIT binary patch literal 1248 zcmV<61RwiSNk&H41ONb6MM6+kP&gpW1ONaq833IDDp&v?06sAmh(e+vAr`AOv>*cn zpbP>15UqQwyYV1YpP&cpcc#CxZ}~qnE59sD@{``5PCssP@V$relk$(9 zouhxQemv!!{PX-reQ)jGh#ae5lpM-)8~#WA=kd?`m(nNhzw$r9`WyU<{7?DsaDU_9 z&40xIZ2h?ZA^*d}Pn2;QXke}U<&T3La3SM=_Te5P&Ek#~F&sN|MITpnO^y|y`Xl3c zBI`U#s=_z>HoDZ;aB@36mLcNfviim$BD+Nz0092~Mv2=e;-+bALz=kcN4UZOok@v~vXLD`c8R1kwk)diPBSBFeba=->@(a!Up%gFQk#nW6Cg z)(sU6u_i8Zr1o5PyLzl%Iptv&NGJQb5B8l^`Qwxm=F@-Q?g!!UbbO%-2EX6opYZ+U zDLTO-P4aizEiXS14$M2C{rfM|71la{-9Jgil}f6UE;>aD%>jINDOh-vhU_5En2^Qb z4~h=B)i;dU=6ruKcO~^fCCN$cpa10B>0%8zv_5;OZ4K_d{PdLsobX@nC-ao;;{x4} zuCqgr&!=h7kF@GVRT5!;t0$$&Ah))-2BFQI1W8DO%Y_P81^{5Cz;7X=%Wv#g@Xsc{ z*E_W6`W!k=M8uhyzJRZ;qt!7&voakP(xH{yyeXQ?WFa#oh++pIK)4nq8?0HM4TZmy zz9s!coubPak*063sMv~tD@e-Y90AjY&H@+a=~llw1MM$Gx$x9{BO*bcWAyYyFa1OO z_uOv-UpP|83iEWiY;^v%l%c@Btm991*TE<{*jKJhAmGO+42U~zK{G)!yJ1KXudxcj z-^V;Qwst>)_xVHVLd>^^U;CoHt{?IzmHfT_E(gBs=e}GFzD>-@AG#k?o1X<(0CxFE zv-6m=Jv?e7923~C?+`!rO!kY$$RTcWk^FCuP+9Z0rEzQipwIyEd@|Gr^S>?B(jmi7 zN4+c57l|d30MI_qHeUq7k${eAybTd^YRRr&W0KOmz?mwoiP{c~L`!7yEGvHEtyG92 z@qS+F1IXQVcgQ=C`<9?>`5F~IADG$Ll44`QKd^t!`$2EyUL8%oppVts>8&#K4F22? z{oLx`NFT-D$-9c8O}YFdtuk%J*3Vh$TOQYUyI{?ASWd$Gt6FXz|HoZ$w%3O@BSIe-?lCW;6y0{0<9ic ztD5S*7Ox;wUY^F<^*5Mc;z8tQ_eL7MU?0A(sc|)e`0P8T;2pW(9eE|38p{{Ri_m1e K=r+_L()0iX34VnD literal 0 HcmV?d00001 diff --git a/data/static/badges/link-www.webp b/data/static/badges/link-www.webp new file mode 100644 index 0000000000000000000000000000000000000000..74b66396db50bbf5f69f9970f623976c71c872d6 GIT binary patch literal 1000 zcmVKNk&HC0{{S5MM6+kP&gpe0{{T969An7Dp&v?06sAmi9;eGAr(}BAOi%T z3$r%CS`+QJ+pVq$;|6VNL?q}f3 zL{s}G<#x!I^k44(jvnm)<@rzOzCI~z@Re!?d;A+cxhn){>nEr3ISCP zkS;0J9I;GOaRGEmw8#DD`$A8U{pnOoo^&VmfJn^UB~$`C<$BMB9a*PSbqW8i+lG&& z-1D$=6%id0e1F)pN4Np#{v()Ga| za~31$iQcK8*uO#g&dgu;MD@dIndx?$ax3kEJa^u_Zl{$NNWuJofDWaSsY^u@xrU z2=w>qLbloeOYaJYD>wZusllNAHkLEyO&@5b!*JpL3^}@@hY4Pi8vV=tJ%8pyc;N3q z7d?g19RBEm$a7V4Y--Q`Lm&D`NlV5w#&Zq#mjrqzws&yMQZi}T_aVp6#Duc{-JHoX2#dW{FY$`9UDjUJ6F0IkXY-1 zaYqh8JZ9Gu9y?b=zqUsc8cXHce``m>)e~%zO&fK_V8j2&R2jg9syJ7}3?N}E!;a9Q zTQ)|htUc`E96#*NtBlQ!uYw-rXGiie0GNNzN;o?s?rY{}0n|19pI0Y=XzXrSz^1L6 zn`!6&A?pY_K>X7_h|Dp&v?06sAmi9;eGAs7i<03ZVd zpbbo13qU^rC4gXne}Mi0@Ben?>~nwtC;-L){6N|O{Bls#whn0$ZH_*UPJqY%B0w?* zoB1My0REyM&SU?a$N%rCbwA%#i0rRB^)jMw39r@+n9={$_WY5d|M`Ki22Y2-#38@? zlyF49=%Q!wC;I$%|5t-+Usj1eIU!7~9D92s-}!OF+`1XM zz&!vzPJJSO-ha3FbN#CHTzH>t0XPH3Z_-|fU+Xvs3}&|v6yCIrTe_OsMzvgf0T)+& zl|CVq!ZDj;dl=mI*Pl{b?JoBK0RI1`{F};{-6TGZ5WDTn^x>}S{nRu&<6D5?kB$!! zbwFR8T-2)uBU5KhIiV`JvHkgJrF~s2(KxGdDNvaOw>I6I>f2YcL1D7i3QKS3cPiyW zf7g%lZLsl4tKEA$5)B;SLf#)0@}_V^C(+M`I|S;hEO=bj%Q@(LlbvYVtgIXMAKkkr zmCh6RmIH}rM36q}1)`sO9B$OWYMY3Ur3XOcBOE3G_XZqh^Bv|yu`7yoD^z|)|Jxkd z)uT6L)^q18fU3gt<<3^jU%J)Wr1$lh=RiW?0}+VCi6j`18N<21_S$^a-~Ud3{`2IK z-i9+=08sU?4xt6!6`h%=gQ9{x>|n8C4Gs z*JR8WTn+bFcmuwmwHcdyWG0`Y;6rzwPyeCPg)bh^`Zjq_Yu~@82AWJnTenAMJ?F=L z^5p&6eBi(Q`(B0!yq0tL_v$G6-M4PQ5nKmK(7A;S0K&+K>DJ~P%T2#HoNm26MI8QA Q5wr;KwcahpOI82?0Aq?>yZ`_I literal 0 HcmV?d00001 diff --git a/data/static/badges/pride-lesbian.webp b/data/static/badges/pride-lesbian.webp new file mode 100644 index 0000000000000000000000000000000000000000..6414cd023ed6f7463003b4b6ce64f3920db1d407 GIT binary patch literal 394 zcmV;50d@XTNk&G30RRA3MM6+kP&goV0RRAS3ILq}Dp&v?06sAki9;eGAr)Qx03ZVd zwE$(y);%Zi&xQ}VxgdOjV|PMNm=E9|(|vM%0B;rg0CdDRz+Hs@DGan6H&TFSJhF5V z3i88rcu+2YWe~i{o<5`i{_wZ#kA8o4uK(k=|I*=AFx0e+8f@?`t--1`7H~7$_B+UX z^^P!4e$4;%<%fVP{Zlc}ZCdNCwrCgs_#e2p0sVPV{Fg^sAd3u-ltMZvO2y>q^7loRr4D-UGTubcwpsnTN9#4(rixiIlst*a@_o(6b zVdJ6BxWwZ?HDb;JbGFaZAC~i?P$VPmJT&9(+{ALdOtl#DqQ?VJm2zpS74G7SKWJRt zIzM&7?QPNw?3^>Q5o+y)`dqoJ|HgazGmF0RWjg=x2Ub!vhO&|Ag=5RAAGRzQ8!)!Kmh*#d#ozT z^ar7_KTc1*JD)w7`n5QrWM}QpmJM!?VSrGxv zDn`*O|NVzs$?z}V|3ClTe+cgHTkS(ZZuZ%1{u0$#SPJvzh4;Vb>f^s!iL`p{lbPFu zLi=?8)5JgdqWw*Nv%m0v-i7}cbv6;j9~K!l2&C*bOHj(aKegXYvQB$5NLMqP0D3D0 YB}~)XQo2f&Ew52QhCzAFf`YKUu$HJ?^?dKdygO z>j3ls@|^vo_&WWD{hszyyK(vjbAQhofSF7(P$D$u=EEK`4KeJc#h;GDdILiFe=?%^ z>FMh;xUSy+|1tpn`(*qc7=Q8RhOsaBYwP^F|1yZ{o0n8|`LfMptUn0YS<{_?=EDOe z`TG8E&hk9CxR8WD=?_rVzu5++{vN&`8||9~*`|)9zc?8)L|^xOf}7Eb&?e;FVF0D# z=9`l8ogkm&Kut8r=s%$?xE%NQxyWj(*k1j`Lo{tzxZpa70)lR_;W^ln4>n$;bMvG)e&cL+d1ogIa8mGGWTVkNwBix-|2S7 zcJoTb&g?KnEAzerPtF$ zQKhd2%z60EDR6u}4m32*)CxXFh m_>`xwt3@xGJ}YNXnbQ51wtk=TZgOK>L%M*&V*jkQfB*oX=!{VS literal 0 HcmV?d00001 diff --git a/data/static/badges/pride-six.webp b/data/static/badges/pride-six.webp new file mode 100644 index 0000000000000000000000000000000000000000..b9532b52ca96364cf6924e747a786f2f6791ec40 GIT binary patch literal 478 zcmV<40U`cUNk&H20RRA3MM6+kP&gpU0RR9{3;>-0Dp&v?06sAmheM(v3a$VU0fJi% zz#HHKNvA1FzDxLl`5n#~uU)UnC*#_BSaTR0e zzWrnrqZ=MeZCr4|b0)1E0oi*@M}D6fgVQ$PJ96LowU^Uc9ZTh*~b_>>F+4bJl}=P&K}DbbG! UB}ods5`qwSpLYF~s5X!Q0JmQH1poj5 literal 0 HcmV?d00001 diff --git a/data/static/badges/pride-trans.webp b/data/static/badges/pride-trans.webp new file mode 100644 index 0000000000000000000000000000000000000000..e6267d9733a789d62ded70004dbdb180fd5bc00f GIT binary patch literal 402 zcmV;D0d4+LNk&GB0RRA3MM6+kP&god0RRB-3ILq}Dp&v?06sAoibJ9yp%S!cAOi%o z51<4sQ-$r{h#aL+10)P!ALGAJJ@$UEe`fSe{@v<+kQ$_D?$b|3%%{{Ejcx#tM9Rk7+O^J&=wB6Ha4$v?xT>(3z)o=~^{ zCWZs9jHOL*KmMreg@-v_cQ$#PLG144py2yW^|;4>i_Jx+3ji~nNlv~nr%(#Ocm-9a z_VG2Wj)UfJ|06ocrvLtKKmOk5I5ss%zcVC@@gbT4H#a?L=BZ-AbKBg+r%viFEyJgx z-NH;UdCz(8gz~pruLA8nxAIBDWwf+eF>)`T9IJGxcI%E2>fL zvw&iT09hm2-stf&{XqBF(<|NJ$)xZHOx{h?&3-xfXZahYbMb>A<47<1uj7WukMS?? zzsY}Rf53Y{ejs|~`IG%8xIggUuE|K(l%Nhp>apJ3E|=B3h!@}=+hfpKp# zYi9xQrPCppWW6!V`yWjx+|7E@{yD1l75a=~=`d!_r_IaOg zgCDNi+RliY{r=|M;A+XYyJ23$bO!tw_c?l^T^Icu>i9|EGafpQU~(js{TI0TN_f!7 zR$^CNBgdQUDJG)XUCZO-m5!tFkPq+GBz8VrMcN7Yd*+zPF8BEH=~Vs+=aTu=7|cfb zDa-!kWQd;RX&MOB z*=Ip5t%nQ$KmVHE-uW6&^P<$<<;sE8x1%R5cyY(zHBn?UuFs|w?R{0h3-HF}m#^)^ z4Vw|mtdeFgo(LbtVTVmbG3HUO;pL^QfAT(yLjMf7)}x3(yAeBsYYnpA<|tOlcA z+M8XA}cwmQCH*_pMS`g{*Ux$=-+~coARPtCC7vZJ_bMoyuo(f&>#UG(eE@L z|7|b(yd$51P40x6-z#N3Z525eb6j&(g3zS{av>`D4-{?7dA&Vgg>5ZmlWiCoTfTPa Kn=5(zfB*miuVHrp literal 0 HcmV?d00001 diff --git a/data/static/badges/pronoun-fae-faer.webp b/data/static/badges/pronoun-fae-faer.webp new file mode 100644 index 0000000000000000000000000000000000000000..13c9638727d89f3abe6096e67fa5ae170d332277 GIT binary patch literal 772 zcmV+f1N;0^Nk&He0ssJ4MM6+kP&gp)0ssIo6abw8Dp&v?06sAmi9;eGAru9~AOi%m zfM6~O#QU*cV{d0Y4w~-sFZv9bWTpH-{f_C0Jiq22iT{(DgnyuYk$%8tF8*8gp{x`A zH}O|wBl-9CZ^@tPzYc%9d1Cn+{KvRA@h{`w*nh45;s0*`|J;G>$@*SpEIB*gs9}pH zks*4$asm<%xg!Tg_^KqzH@)8@&$T;Fh_1+Bkn*mr$xre`qx9WGh9zZO7WJKBy{A>nf=d|&g{U_Xne(GJ=}Ttkk2scCOo$E@_? z+V2=b;kjzjV-rwIv?I5L$uV=OVo5(B+i4nJOXqEvC)g*fP7I^}M@^^;we1`+4|RsQ zo&6nkxl7=zBXM`;d#|(y5wX{4_vzC_pV7nr z{?LT(iw)20$|coKPo;qTJeb?hbtRcRX);w4G0k(o4bRcZA-EXn?#}J%HvM%IJPKGk z0B-vFrzp4AT&>`$+5qrRz4IOo;%+d0Kg8h=dBn({JPn7=MIgCnMhxfnc}F&tcz2Ss93JkdP2Du z@{b}NcUc}(jC-FuPwhRNANJ}`H=^m(<1%wf6i(BoE7X$T)lcw(7hg8M%5ndpFjQx- z-TnF9=~Q3BjhM=#5Pkdvev{M+%HT|f7a$1iog z1^ko#H{2um&+;GaKbc%*f3O9)t;4hXbwdZ$qmu>2-YGB9>aHq7D~`{w`2M!{wc^jM zmF!l$=2d&&Ylf93f`M270RI2nxo`Mu+wK#(_bT7%cfOz2;E&!oSTr4C4)epe{Xg=& zfBy8H42XGG4dT5p=lej;!HAuHI7k0+cDj=4)(bC8>HG6F6G|m$j4c<{{2_NE$)v+y z`Ow8|-awc%!4`g-mOu+9-W>LFAy(J4Ii*tazyBfs%8P=(`G}Q~sHyrGWq&ncGK`y! znK0>P(V?dOPd?HA4o0(+2)esIJizwSW7f)sFx^K)fRxX^=%F^g#vGb8tfNz{M;R0I zOqC~@3KN7Gf531F`%=d(B|i}@bLtDwAD0hJ^fJDG|LOxDlW})4F=>Yn#EbvBxzjZ% z-RLU@-?JZ>9l=Woe&AHEDbgy&6F*BLsSV%Fxh_F%KdflJjjulL(n%CBbf!&Xdfq8JM;c7~i5 zWd^j%gL3%0mY^Y>wiI4uk4WHwpm-d0KXN=D8u3BvYU8Pq#^5k|2>|+q9M5t6L68cL z&Juj-BP^uPtJ>9ko}>=5m}0Yzar~wpGi&5Bc}dYq;W$2`wuh1f(ak8fTN6K}TYvyC C;yBCz literal 0 HcmV?d00001 diff --git a/data/static/badges/pronoun-it-its.webp b/data/static/badges/pronoun-it-its.webp new file mode 100644 index 0000000000000000000000000000000000000000..6bd33684b6acc2fc66226b3f79fe72c9fe436fa6 GIT binary patch literal 582 zcmV-M0=fNCNk&FK0ssJ4MM6+kP&gnm0ssKe5CEM4Dp&v?06r}eh(e*EAs7q?R2Txp zvw&fi3^)g}If)gnd7U)f=zsP(G>JjT1?sL$50w5WXSw-R%sc(VMBDNsT;KE0%OCOI z2;b59R`yHW6Zp6B@9;mFU);GDOIurqU=jA3aHTzC4ai&MyK&pcEYHH-n`02kX{=&K z8S0r!%^~~iZ+6hAEg%5?|K&2j$|82J=|Aka|8CsZ(96VgLIHyDH_6;+i~4^i6aVn5 zV?*3=9yPyRXC-w1W4B-kvK>6jQ}s+k9X@!ei=Y1$|B6)KoO1;K#cpQ5N$Ca!{>|}^ zcFA+$u(6Eg@{)YD{=PvC)p=5V`9FwxK79?fhpyfz&EM{~xgtN6wJ>F!N&8oh zmK6FWT+_~pE@ytX9mIL3{4L}YOqIs9r2jck*UfFWtb4FH2?h&4TZI)b|JwCwam^KO zDSwkZ7s6b1zwWSq^5eAo@0mQ3>+3^ZY_`Z3=rA^9r)LM;AQJhJ81RnCu@mK`E?myq_ocQjHcj@4OdwO=lnN2bgAgjfwMmg~PjKUFY%6ROIFH@90SJ05>69VCGnu}vT=N!AZ{l7Zi}SmMgb7ko UZ}CI0$LG)L@_NJ^4~2jL00u`S$p8QV literal 0 HcmV?d00001 diff --git a/data/static/badges/pronoun-no-pronouns.webp b/data/static/badges/pronoun-no-pronouns.webp new file mode 100644 index 0000000000000000000000000000000000000000..26f049b621e5783dc481e436ecc05bc5aec9bfc3 GIT binary patch literal 850 zcmV-Y1Fig0Nk&FW0{{S5MM6+kP&gny0{{T<6#$(9Dp&v?06sAoi9;eGp%e|9|6%~^}~I13`DP8cg`i$eD)h^+v`BHx-zaxMxZ+?!g^C@gbx*WtrDK% zz83%h{`)Q(HN#YmCU+3+H?#RpXaCm@p$RU(`5AwT;B0kbA_hzV=Q3AtxNJ)01+(4f z#DSFW>lhWyJfT^qV!=P`FR68bZYznu-R6y#WjH2p{ns;PfBZ{P+X(-2S(>c)>+K+A z%FqCfSV|Y5iphnNBgC^?+{(LtX{_g$EzpIr*TbC-0kzafsiifF70as{+Mm~7f^LkR z7h1Bzz>J8rSKc$eLRUgfGyBT1LmBsR3Vi?X`Y^fU@onb%x*(hjGP2Na;yP*uT^C?w zr+7TFygw{)_mU4S$VZD*E%yWMOW}Olz*1f_kIRj_EqcV?FfVe+9MDk10$yn__zIW= zyAMu-68rINB1-o+f}=Z1d60-AcX9j*Un9lX8E0N*VUZwkpxYLk7Be3pE?x6xES_{p z;7E1G16{wFac{Xoj(8CXg_n^cPW8QwY!d#05q92@6?`sT!yH!IQ(RCGIgSX^D-n#( zso}T1YB88JJoDRMN&4u}%bQ`%|4}v9YMo%6=Kkiun`;cdJ}-)=HDAUYT<#rN8a^b* z`~ONyD*NC6!X}l>D5cS;JNDng>iME0s4G8xFAJ(~uj-wv_bZS8y-YRNjK6gP#Trzh zaLtfX(CIRQ$k0AaoSMT6B!1;m=0igdAED>o_&@dO9KX;GtYvUd{}Q?CcfApQooZk) zBxe*~nRXydw*HF5exD;uHu%UVYF|81)gI&COBLxHli_Ue2J9_!q|yKG{*v$Nt)wx3Y{@%E{^8o=JZ75qg6Y)6ytW%BgDSD-3|1OMlTkQT&G-etwJ*=t8kfy}>g cWs)!G?fDI$8Ru`H1W`MU?XGzCA%I^10F-W}NdN!< literal 0 HcmV?d00001 diff --git a/data/static/badges/pronoun-she-her.webp b/data/static/badges/pronoun-she-her.webp new file mode 100644 index 0000000000000000000000000000000000000000..b17dfee6979c87df87e5ce1faacf242a3f783dca GIT binary patch literal 690 zcmV;j0!{r=Nk&Gh0ssJ4MM6+kP&go-0ssK;6abw8Dp&v?06sAoi9;eGp%S&2AOi%m zfMEaM7$@5P=jcao7d=LrHuMMlJep4ce`^Q^ zKEb32I>+si|B5(Vn8|@11#ITld>)+?H}*keh{$Ang_r~UygI>A3iwnRR&3l?CA9adiD_e`0+?R9t@!1Tj_psrW|w3 z-agT<)7EP&6#yoRoxrHpKF77ZYP;M7T%fzGW;*yF&xKPJ-*<{45*YXi@(xy~59p0( z@~spu_$DQ*KNiz2!d@wc2hW7|Oykr$^?9V;Oip+_ zsD=fizr77FN_yvCbg*m5#5+HGjR<$#U2?5xVE4WG}1|nFY0So8gUTAvea}##T|szyC`m&u+%^ z|KSbNO>)B(Dy^NEUi+L9Aflf%|iS%!0UEADo==pFgyIN__(V7XDZLqw??i$N&Ef z-hv;fhWPRXJiawn8$R8gkiR?bjM7TtEfZBXTO?J9lVOIceBUgPH&MlLOm3`utu2R} z-1BF#Z~*@QS7LWJIyMkwakZ(<;LvNN75+q$L*TN#iK3?)G6vEA@;3;Y9~i&wl<@oY z*)uryR}}DXzG&FkQlD&ZUX*`9iE^=ZgUWN=mqj`aL^k2v4QBuSa$0J6yDp-E3|!X# z{p*}lyWkgw;`g%PX(S)TFIR9S266`RX})?sdcTSv{*Ohc%AE|qkNx)R_cV6%Z!c~* z-!G5nUX?lqBRs%2U+f8yYTVM}sxbQh^w`zVKy!=R()#Buy5d>omksvx6s^<$i+w@$ zyJRCY<|r2c`}1zzN?eftr_zt;F)mgvuz5~OCE5>A`+mOMj=6GeLd zj_9wPgCXLgekE7;mBTxXgtEdH3*a1>qj}es(EiUuSR^XFC>CP%3(LuWL{ud8tt|J1QeAT=WBkN?Z|;?2V1(xvC2|9?xCvQ9*ec+>5U z-dQt_3TJ3@gIu9>qPk4+l;)K!;+`xC-WrTiU+Ip4n|SZYJ7d50I&@Zy5iTw6*0_)^ znx`&{9UQZpvE9zl0Nn0x!N9pu1z9&2o7%22ruDXQNF0#bX3i4qfL#DT9H}?n4;w4R Un|QIim$zh$;P;vjtq8yX0D$4M3;+NC literal 0 HcmV?d00001 diff --git a/data/static/badges/pronoun-xe-xem.webp b/data/static/badges/pronoun-xe-xem.webp new file mode 100644 index 0000000000000000000000000000000000000000..1bbf9c0b373e12653109a6087e7a4c35847338ab GIT binary patch literal 658 zcmV;D0&V?LNk&GB0ssJ4MM6+kP&god0ssKe5CEM4Dp&v?06sAoibJ9yp%GOWAOi%m zfM1SLgu~MS-Qmfy=m$}55)7Ml2! z_?PmJ?jM)`*Z=wWN%TDZV^tDj+~Pg19*b%`(;GctXsM*y9X*~AOXHBlm(^p>Am;^_ zcuA2vSD2+%Dz4vDdjJ6b|H1aEqUG+V*wB~bub_MWMCMqW-&@ChH>#vA=NzHk!G8(+ z?d`PsIN`Wdu(6~`IsUncFRDLB;hAIeV%R`C^{)aK|KuwH4gs#U>{V;4AsClaCqk z;CyWtiDh>m8rF#nK%_*M(%wIn_jYp*=kzEz0q5~(kRSaH{|T4_!SDdn47-a(*@+iE z&@$jgt0b|7q33<4@Id}Bbz|`e!4oQ;Az=V5X(ZbayQaN?0mLr(m*{Q%*r%V{UgW~g zwL;%P2_eVCp}!VcD2BE_!WUiK`UZDuyp$JD^iu2Re=|9l>7%UR++Y65$#+kTI3XY| z%F0)8&I`b<143P_5&Os_*t5cK*p{f4M3bx^0;V`k6I%oy2z|SKo{@$PIV{G(n%6hH z@GQ{~4^U*r*8LKEVGQSq6<{bJ{X|3Zx27yvMt*`JonpsQVL4_QldOp4$nT*=Lb6$2 sn42;Eh7TU)KS-a^NbEWWkU(JIvhCf#W(b!rUIyrc2t2k`ALal6007KJWB>pF literal 0 HcmV?d00001 diff --git a/data/static/badges/pronoun-xe-xir.webp b/data/static/badges/pronoun-xe-xir.webp new file mode 100644 index 0000000000000000000000000000000000000000..babe62b60e141c89756b1583cac62707e64058a7 GIT binary patch literal 620 zcmV-y0+anxNk&Fw0ssJ4MM6+kP&go10ssK85dfV5Dp&v?06sAih(e+vAr`5hY#;*! zvw&Y96(?hKO=Rb<(;(i5|ErT72tP)$X1wF@=kiNRt}^D3$MoOE?U29azv#ZuJ@x-~ z`$_hp@{jj#%YWlvV4uZ*nSXTrx&DvegV8j0*m8UWPz*THT!g^(-$L^G+kF)_MADbn z44FaK|0pc-UsAwrVjJJ+h^Em00092~;I-jJSSj?2Rc_WVrQ9C;QXij=6@=2K55?g@ z{!$+Le0NvK5{ZLfYw+6pe90;Qzp#Mjq#E_GJ-}I`9hb|yI}`p91TWOj97Lu6*t!zk z4hebWq(?vd58TxYidPBBjTM;#uD~xfiMsIryxa?Nz=N;OlAT1@o#lbgM4q3pYcMlG z#rvUew|_wv2ctO7Z#%fZ>COM?z7GHWR*b$N^jnKk@Vhy@FTd@Ms6R)2W?Uy$q__r% zmfv?{lXW3W;KcJ%*Xh?p|Jonpo+EB=2#xeg99y*-g+7DRB~os6>v4~r$Ly=XC;8Pg zAfFfvc#(*!t2uAH!6M3vy9)E>N7E*issk?P{1893&J?ShN;l868F9kq4QaQ~KYri2 z&VCdOv#3w@1gH_egUc1#fBR9&I16R_2Wbn?9pE_`6L=tjkpy6od_R3dCuk08ec$%W z+w<4qv*ho#;?4BEw}$m0lC7g;9nvxnl?u#{Q+1N9VeheV-~$8`q{5mOLSLG5Gz^!a zUd>aqVCLyLJvb7P4Ktw)|Ji={lLZile(d%6Z{ec&ZDuj9Bc*q&ZL?W&e84x!I>8ik GfB*mz+dWbM literal 0 HcmV?d00001 diff --git a/data/static/css/style.css b/data/static/css/style.css index 59de496..0dd3641 100644 --- a/data/static/css/style.css +++ b/data/static/css/style.css @@ -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); @@ -963,10 +964,6 @@ textarea { gap: 5px; } -.thread-info-bookmark-button { - margin-left: auto !important; -} - .thread-info-post-preview { overflow: hidden; text-overflow: ellipsis; @@ -1131,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); @@ -1292,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,3 +1470,56 @@ a.mention:hover, a.mention:visited:hover { 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; +} diff --git a/data/static/css/theme-otomotone.css b/data/static/css/theme-otomotone.css index c647df0..fbf958e 100644 --- a/data/static/css/theme-otomotone.css +++ b/data/static/css/theme-otomotone.css @@ -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); @@ -963,10 +964,6 @@ textarea { gap: 5px; } -.thread-info-bookmark-button { - margin-left: auto !important; -} - .thread-info-post-preview { overflow: hidden; text-overflow: ellipsis; @@ -1131,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); @@ -1292,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); @@ -1474,6 +1471,59 @@ 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); diff --git a/data/static/css/theme-peachy.css b/data/static/css/theme-peachy.css index ade51cb..840e6af 100644 --- a/data/static/css/theme-peachy.css +++ b/data/static/css/theme-peachy.css @@ -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); @@ -963,10 +964,6 @@ textarea { gap: 3px; } -.thread-info-bookmark-button { - margin-left: auto !important; -} - .thread-info-post-preview { overflow: hidden; text-overflow: ellipsis; @@ -1131,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); @@ -1292,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); @@ -1474,14 +1471,65 @@ 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; } @@ -1489,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; } diff --git a/data/static/css/theme-snow-white.css b/data/static/css/theme-snow-white.css index 87399c8..cce6fc6 100644 --- a/data/static/css/theme-snow-white.css +++ b/data/static/css/theme-snow-white.css @@ -48,7 +48,7 @@ font-family: "Cadman", sans-serif; text-decoration: none; border: 1px solid black; - border-radius: 8px; + border-radius: 4px; padding: 5px 20px; margin: 10px 0; } @@ -60,10 +60,11 @@ body { color: black; } -a:link { +:where(a:link) { color: #711579; } -a:visited { + +:where(a:visited) { color: #4a144f; } @@ -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; @@ -604,14 +605,14 @@ pre code { /* Literal.Number.Integer.Long */ } padding: 5px 10px; display: inline-block; margin: 4px; - border-radius: 8px; + border-radius: 4px; font-size: 1rem; white-space: pre; } #delete-dialog, .lightbox-dialog { padding: 0; - border-radius: 8px; + border-radius: 4px; border: 2px solid black; box-shadow: 0 0 30px rgba(0, 0, 0, 0.25); } @@ -648,7 +649,7 @@ pre code { /* Literal.Number.Integer.Long */ } blockquote { padding: 10px 20px; margin: 10px; - border-radius: 8px; + border-radius: 4px; border-left: 10px solid rgb(239.24, 241, 244.36); background-color: rgba(0, 0, 0, 0.1490196078); } @@ -710,7 +711,7 @@ blockquote { button, input[type=submit], .linkbutton { display: inline-block; background-color: #eecee9; - color: black !important; + color: black; } button:hover, input[type=submit]:hover, .linkbutton:hover { background-color: rgb(241.4, 215.8, 237.4); @@ -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: #eecee9; - color: black !important; + color: black; } input[type=file]::file-selector-button:hover { background-color: rgb(241.4, 215.8, 237.4); @@ -803,7 +804,7 @@ p { .pagebutton { background-color: #eecee9; - color: black !important; + color: black; } .pagebutton:hover { background-color: rgb(241.4, 215.8, 237.4); @@ -852,7 +853,7 @@ p { input[type=text], input[type=password], textarea, select { border: 1px solid black; - border-radius: 8px; + border-radius: 4px; padding: 7px 10px; width: 100%; resize: vertical; @@ -963,10 +964,6 @@ textarea { gap: 5px; } -.thread-info-bookmark-button { - margin-left: auto !important; -} - .thread-info-post-preview { overflow: hidden; text-overflow: ellipsis; @@ -1131,7 +1128,7 @@ textarea { .tab-button { background-color: #eecee9; - color: black !important; + color: black; } .tab-button:hover { background-color: rgb(241.4, 215.8, 237.4); @@ -1170,9 +1167,9 @@ textarea { background-color: rgb(231.4, 224.9375, 212.6); border: 1px solid black; padding: 10px; - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - border-bottom-left-radius: 8px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; } ul, ol { @@ -1199,7 +1196,7 @@ ul.horizontal li, ol.horizontal li { border: 1px solid black; background-color: #81a3e6; padding: 20px 15px; - border-radius: 8px; + border-radius: 4px; box-shadow: 0 0 30px rgba(0, 0, 0, 0.25); } @@ -1209,8 +1206,8 @@ ul.horizontal li, ol.horizontal li { } .accordion { - border-top-right-radius: 8px; - border-top-left-radius: 8px; + border-top-right-radius: 4px; + border-top-left-radius: 4px; border: 1px solid black; margin: 10px 5px; overflow: hidden; @@ -1281,7 +1278,7 @@ ul.horizontal li, ol.horizontal li { transform: translateX(-50%); margin: 0; border: none; - border-radius: 8px; + border-radius: 4px; background-color: rgba(0, 0, 0, 0.5019607843); padding: 5px 10px; } @@ -1292,7 +1289,7 @@ footer { .reaction-button.active { background-color: #eee3ce; - color: black !important; + color: black; } .reaction-button.active:hover { background-color: rgb(241.4, 232.6, 215.8); @@ -1316,7 +1313,7 @@ footer { position: relative; margin: 0; border: none; - border-radius: 8px; + border-radius: 4px; background-color: rgba(0, 0, 0, 0.5019607843); padding: 5px 10px; width: 250px; @@ -1341,7 +1338,7 @@ footer { .bookmarks-dropdown { background-color: #ced9ee; border: 1px solid black; - border-radius: 8px; + border-radius: 4px; box-shadow: 0 0 30px rgba(0, 0, 0, 0.25); position: absolute; margin: 0; @@ -1359,7 +1356,7 @@ footer { margin: 10px 0; cursor: pointer; border: 1px solid black; - border-radius: 8px; + border-radius: 4px; color: black; background-color: #eecee9; } @@ -1438,7 +1435,7 @@ a.mention, a.mention:visited { color: white; background-color: rgb(136.0836363636, 149.3636363636, 174.7163636364); padding: 5px; - border-radius: 8px; + border-radius: 4px; text-decoration: none; } a.mention.display, a.mention:visited.display { @@ -1462,7 +1459,7 @@ a.mention:hover, a.mention:visited:hover { } .settings-grid fieldset { border: 1px solid white; - border-radius: 8px; + border-radius: 4px; background-color: rgb(187.6595454545, 188.3232954545, 189.5904545455); } @@ -1473,3 +1470,56 @@ a.mention:hover, a.mention:visited:hover { 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; +} diff --git a/data/static/js/bitties/pyrom-bitty.js b/data/static/js/bitties/pyrom-bitty.js index c5312e2..e79cc8f 100644 --- a/data/static/js/bitties/pyrom-bitty.js +++ b/data/static/js/bitties/pyrom-bitty.js @@ -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 = ``; + const submitButton = ``; + const controls = `${addButton} ${submitButton} BADGECOUNT/10` + 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(''); + } +} diff --git a/sass/_default.scss b/sass/_default.scss index ad9de0f..7bba579 100644 --- a/sass/_default.scss +++ b/sass/_default.scss @@ -133,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 + color: $font_color; &:hover { background-color: color.scale($color, $lightness: 20%); @@ -180,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 { @@ -233,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; @@ -251,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; @@ -836,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; @@ -1446,3 +1440,74 @@ $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; +} diff --git a/sass/otomotone.scss b/sass/otomotone.scss index e5edbb1..bd92a76 100644 --- a/sass/otomotone.scss +++ b/sass/otomotone.scss @@ -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 { diff --git a/sass/peachy.scss b/sass/peachy.scss index 619b5fd..4dccf8f 100644 --- a/sass/peachy.scss +++ b/sass/peachy.scss @@ -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; }