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 0000000..0897694 Binary files /dev/null and b/data/static/badges/link-bsky.webp differ diff --git a/data/static/badges/link-itch-io.webp b/data/static/badges/link-itch-io.webp new file mode 100644 index 0000000..ff10977 Binary files /dev/null and b/data/static/badges/link-itch-io.webp differ diff --git a/data/static/badges/link-mastodon.webp b/data/static/badges/link-mastodon.webp new file mode 100644 index 0000000..2b88203 Binary files /dev/null and b/data/static/badges/link-mastodon.webp differ diff --git a/data/static/badges/link-www.webp b/data/static/badges/link-www.webp new file mode 100644 index 0000000..74b6639 Binary files /dev/null and b/data/static/badges/link-www.webp differ diff --git a/data/static/badges/pride-asexual.webp b/data/static/badges/pride-asexual.webp new file mode 100644 index 0000000..7f68ac5 Binary files /dev/null and b/data/static/badges/pride-asexual.webp differ diff --git a/data/static/badges/pride-intersex.webp b/data/static/badges/pride-intersex.webp new file mode 100644 index 0000000..bb30453 Binary files /dev/null and b/data/static/badges/pride-intersex.webp differ diff --git a/data/static/badges/pride-lesbian.webp b/data/static/badges/pride-lesbian.webp new file mode 100644 index 0000000..6414cd0 Binary files /dev/null and b/data/static/badges/pride-lesbian.webp differ diff --git a/data/static/badges/pride-nonbinary.webp b/data/static/badges/pride-nonbinary.webp new file mode 100644 index 0000000..4714289 Binary files /dev/null and b/data/static/badges/pride-nonbinary.webp differ diff --git a/data/static/badges/pride-progress.webp b/data/static/badges/pride-progress.webp new file mode 100644 index 0000000..1f3320b Binary files /dev/null and b/data/static/badges/pride-progress.webp differ diff --git a/data/static/badges/pride-six.webp b/data/static/badges/pride-six.webp new file mode 100644 index 0000000..b9532b5 Binary files /dev/null and b/data/static/badges/pride-six.webp differ diff --git a/data/static/badges/pride-trans.webp b/data/static/badges/pride-trans.webp new file mode 100644 index 0000000..e6267d9 Binary files /dev/null and b/data/static/badges/pride-trans.webp differ diff --git a/data/static/badges/pronoun-any-all.webp b/data/static/badges/pronoun-any-all.webp new file mode 100644 index 0000000..62893e5 Binary files /dev/null and b/data/static/badges/pronoun-any-all.webp differ diff --git a/data/static/badges/pronoun-fae-faer.webp b/data/static/badges/pronoun-fae-faer.webp new file mode 100644 index 0000000..13c9638 Binary files /dev/null and b/data/static/badges/pronoun-fae-faer.webp differ diff --git a/data/static/badges/pronoun-he-him.webp b/data/static/badges/pronoun-he-him.webp new file mode 100644 index 0000000..455de56 Binary files /dev/null and b/data/static/badges/pronoun-he-him.webp differ diff --git a/data/static/badges/pronoun-it-its.webp b/data/static/badges/pronoun-it-its.webp new file mode 100644 index 0000000..6bd3368 Binary files /dev/null and b/data/static/badges/pronoun-it-its.webp differ diff --git a/data/static/badges/pronoun-no-pronouns.webp b/data/static/badges/pronoun-no-pronouns.webp new file mode 100644 index 0000000..26f049b Binary files /dev/null and b/data/static/badges/pronoun-no-pronouns.webp differ diff --git a/data/static/badges/pronoun-she-her.webp b/data/static/badges/pronoun-she-her.webp new file mode 100644 index 0000000..b17dfee Binary files /dev/null and b/data/static/badges/pronoun-she-her.webp differ diff --git a/data/static/badges/pronoun-they-them.webp b/data/static/badges/pronoun-they-them.webp new file mode 100644 index 0000000..5c8f42c Binary files /dev/null and b/data/static/badges/pronoun-they-them.webp differ diff --git a/data/static/badges/pronoun-xe-xem.webp b/data/static/badges/pronoun-xe-xem.webp new file mode 100644 index 0000000..1bbf9c0 Binary files /dev/null and b/data/static/badges/pronoun-xe-xem.webp differ diff --git a/data/static/badges/pronoun-xe-xir.webp b/data/static/badges/pronoun-xe-xir.webp new file mode 100644 index 0000000..babe62b Binary files /dev/null and b/data/static/badges/pronoun-xe-xir.webp differ 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; }