Compare commits

..

29 Commits

Author SHA1 Message Date
dc0aa0dba7 ascii... 2025-12-09 13:40:25 +03:00
dbf0150a5e add badges 2025-12-09 03:33:27 +03:00
1539486456 undo the roundness in snow white theme 2025-12-07 20:22:21 +03:00
c18dad4a77 raise on exception in connection, fix operator not being considered in QueryBuilder 2025-12-06 22:19:12 +03:00
2b45cab4e8 actually disallow @ in display name 2025-12-06 19:08:35 +03:00
37c1ffc2a1 strip animation from uploaded avatar 2025-12-06 18:09:15 +03:00
09a19b5352 raise overall content body size, routes will implement stricter limits 2025-12-06 10:18:32 +03:00
6c96563a0e fix title in 413 template 2025-12-06 06:15:51 +03:00
77677eef6d new theme: snow white 2025-12-05 18:22:25 +03:00
f99ae75503 excise settings-container and login-container outright. full width babey 2025-12-05 17:53:24 +03:00
552fb67c6c settings width is now 95% 2025-12-05 17:26:05 +03:00
e9c03b9046 add a background color to fieldset in settings grid 2025-12-05 17:23:35 +03:00
f0b0fb8909 handle 413 2025-12-05 17:00:32 +03:00
9ae4e376b8 load default site configuration first before merging file config 2025-12-05 16:55:13 +03:00
d1bc1c644b remove errant paragraph from introduction guide 2025-12-05 16:53:36 +03:00
7840399d01 untrack pyrom_config.toml, provide example with default values instead 2025-12-05 14:16:11 +03:00
508b313871 fix babycode guide links 2025-12-05 13:58:13 +03:00
db677abaa5 add a guide topics system 2025-12-05 13:53:21 +03:00
65abea2093 port to bitty 7.0.0-rc1 2025-12-05 04:31:03 +03:00
1533f82a6b oops 2025-12-04 10:27:49 +03:00
35483c27aa null is not a thing in python lol 2025-12-04 10:22:03 +03:00
005d2f3b6c fix signup 2025-12-04 10:19:49 +03:00
265e249eaf make guide sections a macro 2025-12-04 08:55:08 +03:00
b812e01473 add [lb], [rb], and [@] tags 2025-12-04 08:31:49 +03:00
88f80c38cc make babycode guide use generic class names 2025-12-04 06:24:24 +03:00
c70f13d069 add contact information to config 2025-12-04 06:16:37 +03:00
73af2dc3b9 forbid mentions in sigs 2025-12-04 05:36:57 +03:00
062cab44bc add Atkinson Hyperlegible Mono font for textarea, normalize css a bit 2025-12-04 05:21:19 +03:00
3baccb87b1 new settings page 2025-12-04 02:39:24 +03:00
80 changed files with 3523 additions and 519 deletions

2
.gitignore vendored
View File

@@ -4,7 +4,9 @@
data/db/*
data/static/avatars/*
!data/static/avatars/default.webp
data/static/badges/user
config/secrets.prod.env
config/pyrom_config.toml
.local/

View File

@@ -23,6 +23,13 @@ Copyright: `© 2017-2020 by Paul James Miller. All rights reserved.`
License: SIL Open Font License 1.1
Designers: Paul James Miller
## Atkinson Hyperlegible Mono
Affected files: [`data/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf`](./data/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf) [`data/static/fonts/AtkinsonHyperlegibleMono-Italic-VariableFont_wght.ttf`](./data/static/fonts/AtkinsonHyperlegibleMono-Italic-VariableFont_wght.ttf)
URL: https://www.brailleinstitute.org/freefont/
Copyright: Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono)
License: SIL Open Font License 1.1
Designers: Elliott Scott, Megan Eiswerth, Braille Institute, Applied Design Works, Letters From Sweden
## ICONCINO
Affected files: [`app/templates/common/icons.html`](./app/templates/common/icons.html)

View File

@@ -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
@@ -8,6 +8,7 @@ from .constants import (
PermissionLevel, permission_level_string,
InfoboxKind, InfoboxHTMLClass,
REACTION_EMOJI, MOTD_BANNED_TAGS,
SIG_BANNED_TAGS, STRICT_BANNED_TAGS,
)
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION
from datetime import datetime
@@ -15,6 +16,7 @@ import os
import time
import secrets
import tomllib
import json
def create_default_avatar():
if Avatars.count() == 0:
@@ -98,9 +100,43 @@ def reparse_babycode():
print('Re-parsing done.')
def bind_default_badges(path):
from .db import db
with db.transaction():
potential_stales = BadgeUploads.get_default()
d = os.listdir(path)
for bu in potential_stales:
if os.path.basename(bu.file_path) not in d:
print(f'Deleted stale default badge{os.path.basename(bu.file_path)}')
bu.delete()
for f in d:
real_path = os.path.join(path, f)
if not os.path.isfile(real_path):
continue
if not f.endswith('.webp'):
continue
proxied_path = f'/static/badges/{f}'
bu = BadgeUploads.find({'file_path': proxied_path})
if not bu:
BadgeUploads.create({
'file_path': proxied_path,
'uploaded_at': int(os.path.getmtime(real_path)),
})
def create_app():
app = Flask(__name__)
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
app.config['SITE_NAME'] = 'Pyrom'
app.config['DISABLE_SIGNUP'] = False
app.config['MODS_CAN_INVITE'] = True
app.config['USERS_CAN_INVITE'] = False
app.config['ADMIN_CONTACT_INFO'] = ''
app.config['GUIDE_DESCRIPTION'] = ''
try:
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
except FileNotFoundError:
print('No configuration file found, leaving defaults.')
if os.getenv("PYROM_PROD") is None:
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
@@ -113,9 +149,12 @@ def create_app():
app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY")
app.config['AVATAR_UPLOAD_PATH'] = 'data/static/avatars/'
app.config['MAX_CONTENT_LENGTH'] = 1000 * 1000
app.config['BADGES_PATH'] = 'data/static/badges/'
app.config['BADGES_UPLOAD_PATH'] = 'data/static/badges/user/'
app.config['MAX_CONTENT_LENGTH'] = 3 * 1000 * 1000 # 3M total, subject to further limits per route
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True)
css_dir = 'data/static/css/'
allowed_themes = []
@@ -140,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
@@ -148,6 +189,7 @@ def create_app():
from app.routes.api import bp as api_bp
from app.routes.posts import bp as posts_bp
from app.routes.hyperapi import bp as hyperapi_bp
from app.routes.guides import bp as guides_bp
app.register_blueprint(app_bp)
app.register_blueprint(topics_bp)
app.register_blueprint(threads_bp)
@@ -156,6 +198,7 @@ def create_app():
app.register_blueprint(api_bp)
app.register_blueprint(posts_bp)
app.register_blueprint(hyperapi_bp)
app.register_blueprint(guides_bp)
app.config['SESSION_COOKIE_SECURE'] = True
@@ -177,6 +220,7 @@ def create_app():
"__emoji": EMOJI,
"REACTION_EMOJI": REACTION_EMOJI,
"MOTD_BANNED_TAGS": MOTD_BANNED_TAGS,
"SIG_BANNED_TAGS": SIG_BANNED_TAGS,
}
@app.context_processor
@@ -210,6 +254,10 @@ def create_app():
def babycode_filter(markup):
return babycode_to_html(markup).result
@app.template_filter('babycode_strict')
def babycode_strict_filter(markup):
return babycode_to_html(markup, STRICT_BANNED_TAGS).result
@app.template_filter('extract_h2')
def extract_h2(content):
import re
@@ -220,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/'):
@@ -229,6 +281,15 @@ def create_app():
else:
return render_template('common/404.html'), e.code
@app.errorhandler(413)
def _handle_413(e):
if request.path.startswith('/hyperapi/'):
return '<h1>request body too large</h1>', e.code
elif request.path.startswith('/api/'):
return {'error': 'body too large'}, e.code
else:
return render_template('common/413.html'), e.code
# this only happens at build time but
# build time is when updates are done anyway
# sooo... /shrug
@@ -241,6 +302,10 @@ def create_app():
if subject == 'style':
return 'Default'
return f'{subject.removeprefix('theme-').capitalize()} (beta)'
return f'{subject.removeprefix('theme-').replace('-', ' ').capitalize()} (beta)'
@app.template_filter('fromjson')
def fromjson(subject: str):
return json.loads(subject)
return app

View File

@@ -48,7 +48,16 @@ REACTION_EMOJI = [
]
MOTD_BANNED_TAGS = [
'img', 'spoiler', '@mention'
'img', 'spoiler', '@mention',
]
SIG_BANNED_TAGS = [
'@mention',
]
STRICT_BANNED_TAGS = [
'img', 'spoiler', '@mention',
'big', 'small', 'center', 'right', 'color',
]
def permission_level_string(perm):

View File

@@ -28,9 +28,10 @@ class DB:
if in_transaction:
conn.commit()
except Exception:
except Exception as e:
if in_transaction and self._connection:
conn.rollback()
raise
finally:
if in_transaction:
self._transaction_depth -= 1
@@ -126,7 +127,7 @@ class DB:
def where(self, condition, operator = "="):
if isinstance(condition, dict):
for key, value in condition.items():
self._where.append((key, "=", value))
self._where.append((key, operator, value))
elif isinstance(condition, list):
for c in condition:
self._where.append(c)

View File

@@ -141,6 +141,12 @@ TAGS = {
"spoiler": tag_spoiler,
}
VOID_TAGS = {
'lb': lambda attr: '[',
'rb': lambda attr: ']',
'@': lambda attr: '@',
}
# [img] is considered block for the purposes of collapsing whitespace,
# despite being potentially inline (since the resulting <img> tag is inline, but creates a block container around itself and sibling images).
# [code] has a special case in is_inline().
@@ -252,7 +258,7 @@ def should_collapse(text, surrounding):
def sanitize(s):
return escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
def babycode_to_html(s, banned_tags={}):
def babycode_to_html(s, banned_tags=[]):
allowed_tags = set(TAGS.keys())
if banned_tags is not None:
for tag in banned_tags:
@@ -260,6 +266,7 @@ def babycode_to_html(s, banned_tags={}):
subj = sanitize(s)
parser = Parser(subj)
parser.valid_bbcode_tags = allowed_tags
parser.void_bbcode_tags = set(VOID_TAGS)
parser.bbcode_tags_only_text_children = TEXT_ONLY
parser.mentions_allowed = '@mention' not in banned_tags
parser.valid_emotes = EMOJI.keys()
@@ -296,6 +303,8 @@ def babycode_to_html(s, banned_tags={}):
c = c + Markup(fold(child, _nobr, _surrounding))
res = TAGS[element['name']](c, element['attr'], surrounding)
return res
case "bbcode_void":
return VOID_TAGS[element['name']](element['attr'])
case "link":
return f"<a href=\"{element['url']}\">{element['url']}</a>"
case 'emote':

View File

@@ -11,6 +11,7 @@ PAT_MENTION = r'[a-zA-Z0-9_-]'
class Parser:
def __init__(self, src_str):
self.valid_bbcode_tags = {}
self.void_bbcode_tags = {}
self.valid_emotes = []
self.bbcode_tags_only_text_children = []
self.mentions_allowed = True
@@ -228,11 +229,46 @@ class Parser:
}
def parse_bbcode_void(self):
self.save_position()
if not self.check_char("["):
self.restore_position()
return None
name = self.match_pattern(PAT_BBCODE_TAG)
if name == "":
self.restore_position()
return None
attr = None
if self.check_char("="):
attr = self.match_pattern(PAT_BBCODE_ATTR)
if not self.check_char("]"):
self.restore_position()
return None
if not name in self.void_bbcode_tags:
self.restore_position()
return None
self.forget_position()
return {
'type': 'bbcode_void',
'name': name,
'attr': attr,
}
def parse_element(self, siblings):
if self.is_end_of_source():
return None
element = self.parse_emote() \
or self.parse_bbcode_void() \
or self.parse_bbcode() \
or self.parse_rule() \
or self.parse_link() \

View File

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

View File

@@ -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/")
@@ -217,7 +217,7 @@ def bookmark_thread(thread_id):
@bp.get('/current-user')
def get_current_user_info():
if not is_logged_in():
return {'user': null}
return {'user': None}
user = get_active_user()
return {

View File

@@ -5,8 +5,3 @@ bp = Blueprint("app", __name__, url_prefix = "/")
@bp.route("/")
def index():
return redirect(url_for("topics.all_topics"))
@bp.route("/babycode")
def babycode_guide():
return render_template('babycode.html')

111
app/routes/guides.py Normal file
View File

@@ -0,0 +1,111 @@
from flask import Blueprint, render_template, render_template_string, current_app, abort
from pathlib import Path
import re
bp = Blueprint('guides', __name__, url_prefix='/guides/')
def parse_guide_title(content: str):
lines = content.strip().split('\n', 1)
fline = lines[0].strip()
if fline.startswith('#'):
title = fline[2:].strip()
content = lines[1] if len(lines) > 1 else ''
return title, content.strip()
return None, content.strip()
def get_guides_by_category():
guides_dir = Path(current_app.root_path) / 'templates' / 'guides'
categories = {}
for item in guides_dir.iterdir():
if item.is_dir() and not item.name.startswith('_'):
category = item.name
categories[category] = []
for guide_file in sorted(item.glob('*.html')):
if guide_file.name.startswith('_'):
continue
m = re.match(r'(\d+)-(.+)\.html', guide_file.name)
if not m:
continue
sort_num = int(m.group(1))
slug = m.group(2)
try:
with open(guide_file, 'r') as f:
content = f.read()
title, template = parse_guide_title(content)
if not title:
title = slug.replace('-', ' ').title()
categories[category].append(({
'sort': sort_num,
'slug': slug,
'filename': guide_file.name,
'title': title,
'path': f'{category}/{guide_file.name}',
'url': f'/guides/{category}/{slug}',
'template': template,
}))
except Exception as e:
current_app.logger.warning(f'failed to read {guide_file}: {e}')
continue
categories[category].sort(key=lambda x: x['sort'])
return categories
@bp.get('/babycode')
def babycode():
# print(get_guides_by_category())
return '<h1>no</h1>'
@bp.get('/<category>/<slug>')
def guide_page(category, slug):
categories = get_guides_by_category()
if category not in categories:
abort(404)
return
for i, guide in enumerate(categories[category]):
if guide['slug'] != slug:
continue
next_guide = None
prev_guide = None
if i != 0:
prev_guide = categories[category][i - 1]
if i + 1 < len(categories[category]):
next_guide = categories[category][i + 1]
return render_template_string(guide['template'], next_guide=next_guide, prev_guide=prev_guide, category=category, guide=guide)
abort(404)
@bp.get('/<category>/')
def category_index(category):
categories = get_guides_by_category()
if category not in categories:
abort(404)
return
return render_template('guides/category_index.html', category=category, pages=categories[category])
@bp.get('/')
def guides_index():
return render_template('guides/guides_index.html', categories=get_guides_by_category())
@bp.get('/contact')
def contact():
return render_template('guides/contact.html')

View File

@@ -1,6 +1,6 @@
from flask import Blueprint, render_template, abort, request
from .users import get_active_user, is_logged_in
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, BadgeUploads, Badges
from functools import wraps
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
@@ -26,7 +26,7 @@ def handle_403(e):
return "<h1>forbidden</h1>", 403
@bp.get('bookmarks-dropdown/<bookmark_type>')
@bp.get('/bookmarks-dropdown/<bookmark_type>')
@login_required
@account_required
def bookmarks_dropdown(bookmark_type):
@@ -51,3 +51,20 @@ def bookmarks_dropdown(bookmark_type):
return render_template('components/bookmarks_dropdown.html', collections=collections, id=concept_id, selected=selected, type=bookmark_type, memo=memo, require_reload=require_reload)
@bp.get('/badge-editor')
@login_required
@account_required
def get_badges():
uploads = BadgeUploads.get_for_user(get_active_user().id)
badges = sorted(Badges.findall({'user_id': int(get_active_user().id)}), key=lambda x: x['sort_order'])
return render_template('components/badge_editor_badges.html', uploads=uploads, badges=badges)
@bp.get('/badge-editor/template')
@login_required
@account_required
def get_badge_template():
uploads = BadgeUploads.get_for_user(get_active_user().id)
return render_template('components/badge_editor_template.html', uploads=uploads)

View File

@@ -8,9 +8,9 @@ from ..models import (
Users, Sessions, Subscriptions,
Avatars, PasswordResetLinks, InviteKeys,
BookmarkCollections, BookmarkedThreads,
Mentions, PostHistory,
Mentions, PostHistory, Badges, BadgeUploads,
)
from ..constants import InfoboxKind, PermissionLevel
from ..constants import InfoboxKind, PermissionLevel, SIG_BANNED_TAGS
from ..auth import digest, verify
from wand.image import Image
from wand.exceptions import WandException
@@ -20,12 +20,17 @@ import time
import re
import os
AVATAR_MAX_SIZE = 1000 * 1000
BADGE_MAX_SIZE = 1000 * 500
bp = Blueprint("users", __name__, url_prefix = "/users/")
def validate_and_create_avatar(input_image, filename):
try:
with Image(blob=input_image) as img:
if hasattr(img, 'sequence') and len(img.sequence) > 1:
img = Image(image=img.sequence[0])
img.strip()
img.gravity = 'center'
@@ -52,6 +57,21 @@ def validate_and_create_avatar(input_image, filename):
except WandException:
return False
def validate_and_create_badge(input_image, filename):
try:
with Image(blob=input_image) as img:
if img.width != 88 or img.height != 31:
return False
if hasattr(img, 'sequence') and len(img.sequence) > 1:
img = Image(image=img.sequence[0])
img.strip()
img.format = 'webp'
img.compression_quality = 90
img.save(filename=filename)
return True
except WandException:
return False
def is_logged_in():
return "pyrom_session_key" in session
@@ -67,11 +87,14 @@ def get_active_user():
def create_session(user_id):
return Sessions.create({
"key": secrets.token_hex(16),
key = secrets.token_hex(16)
expires_at = int(time.time()) + 31 * 24 * 60 * 60
s = Sessions.create({
"key": key,
"user_id": user_id,
"expires_at": int(time.time()) + 31 * 24 * 60 * 60,
"expires_at": expires_at,
})
return s
def extend_session(user_id):
@@ -334,26 +357,27 @@ def sign_up_post():
else:
display_name = ''
new_user = Users.create({
"username": username,
'display_name': display_name,
"password_hash": hashed,
"permission": PermissionLevel.GUEST.value,
})
BookmarkCollections.create_default(new_user.id)
if current_app.config['DISABLE_SIGNUP']:
invite_key = InviteKeys.find({'key': key})
new_user.update({
'invited_by': invite_key.created_by,
'permission': PermissionLevel.USER.value,
with db.transaction():
new_user = Users.create({
"username": username.lower(),
'display_name': display_name,
"password_hash": hashed,
"permission": PermissionLevel.GUEST.value,
})
invite_key.delete()
session_obj = create_session(new_user.id)
BookmarkCollections.create_default(new_user.id)
session['pyrom_session_key'] = session_obj.key
if current_app.config['DISABLE_SIGNUP']:
invite_key = InviteKeys.find({'key': key})
new_user.update({
'invited_by': invite_key.created_by,
'permission': PermissionLevel.USER.value,
})
invite_key.delete()
session_obj = create_session(new_user.id)
session['pyrom_session_key'] = session_obj.key
flash("Signed up successfully!", InfoboxKind.INFO)
return redirect(url_for("topics.all_topics"))
@@ -392,11 +416,11 @@ def settings_form(username):
status = request.form.get('status', default="")[:100]
original_sig = request.form.get('signature', default='').strip()
if original_sig:
rendered_sig = babycode_to_html(original_sig).result
rendered_sig = babycode_to_html(original_sig, SIG_BANNED_TAGS).result
else:
rendered_sig = ''
session['subscribe_by_default'] = request.form.get('subscribe_by_default', default='off') == 'on'
display_name = request.form.get('display_name', default='')
display_name = request.form.get('display_name', default='').replace('@', '_')
if not validate_display_name(display_name):
flash('Invalid display name.', InfoboxKind.ERROR)
return redirect('.settings', username=user.username)
@@ -447,6 +471,14 @@ def set_avatar(username):
flash('Avatar missing.', InfoboxKind.ERROR)
return redirect(url_for('.settings', username=user.username))
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0, os.SEEK_SET)
if file_size > AVATAR_MAX_SIZE:
flash('Avatar image is over 1MB.', InfoboxKind.ERROR)
return redirect(url_for('.settings', username=user.username))
file_bytes = file.read()
now = int(time.time())
@@ -839,3 +871,96 @@ def delete_page_confirm(username):
session.clear()
target_user.delete()
return redirect(url_for('topics.all_topics'))
@bp.post('/<username>/save-badges')
@login_required
@redirect_to_own
def save_badges(username):
user = get_active_user()
badge_choices = request.form.getlist('badge_choice[]')
badge_files = request.files.getlist('badge_file[]')
badge_labels = request.form.getlist('badge_label[]')
badge_links = request.form.getlist('badge_link[]')
if not (len(badge_choices) == len(badge_files) == len(badge_labels) == len(badge_links)):
return 'nope'
pending_badges = []
rejected_filenames = []
# lack of file can be checked with a simple `if not file`
for i in range(len(badge_choices)):
is_custom = badge_choices[i] == 'custom'
file = badge_files[i]
pending_badge = {
'upload': badge_choices[i],
'is_custom': is_custom,
'label': badge_labels[i],
'link': badge_links[i],
'sort_order': i,
}
if is_custom:
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0, os.SEEK_SET)
if file_size >= BADGE_MAX_SIZE:
rejected_filenames.append(file.filename)
continue
file_bytes = file.read()
pending_badge['original_filename'] = file.filename
now = int(time.time())
filename = f'u{user.id}d{now}s{i}.webp'
output_path = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], filename)
proxied_filename = f'/static/badges/user/{filename}'
res = validate_and_create_badge(file_bytes, output_path)
if not res:
rejected_filenames.append(file.filename)
continue
pending_badge['proxied_filename'] = proxied_filename
pending_badge['uploaded_at'] = now
pending_badges.append(pending_badge)
if rejected_filenames:
flash(f'Invalid badges.;Some of your uploaded badges are incorrect: {", ".join(rejected_filenames)}. Your badges have not been modified.', InfoboxKind.ERROR)
return redirect(url_for('.settings', username=user.username))
with db.transaction():
existing_badges = Badges.findall({'user_id': int(user.id)})
for badge in existing_badges:
badge.delete()
with db.transaction():
for pending_badge in pending_badges:
if pending_badge['is_custom']:
bu = BadgeUploads.create({
'file_path': pending_badge['proxied_filename'],
'uploaded_at': pending_badge['uploaded_at'],
'original_filename': pending_badge['original_filename'],
'user_id': int(user.id),
})
else:
bu = BadgeUploads.find({
'id': int(pending_badge['upload'])
})
badge = Badges.create({
'user_id': int(user.id),
'upload': int(bu.id),
'label': pending_badge['label'],
'link': pending_badge['link'],
'sort_order': pending_badge['sort_order']
})
for stale_upload in BadgeUploads.get_unused_for_user(user.id):
filename = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], os.path.basename(stale_upload.file_path))
os.remove(filename)
stale_upload.delete()
flash('Badges saved.', InfoboxKind.INFO)
return redirect(url_for('.settings', username=user.username))

View File

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

View File

@@ -1,191 +0,0 @@
<!-- kate: remove-trailing-space off; -->
{% extends 'base.html' %}
{% block title %}babycode guide{% endblock %}
{% block content %}
<div class=darkbg>
<h1 class="thread-title">Babycode guide</h1>
</div>
<div class="babycode-guide-container">
<div class="guide-topics">
{% set sections %}
<section class="babycode-guide-section">
<h2 id="what-is-babycode">What is babycode?</h2>
<p>You may be familiar with BBCode, a loosely related family of markup languages popular on forums. Babycode is another, simplified, dialect of those languages. It is a way of formatting text by enclosing parts of it in special tags.</p>
<p>A <b>tag</b> is a short name enclosed in square brackets. Tags can be opening tags, like <code class="inline-code">[b]</code> or closing tags, like <code class="inline-code">[/b]</code>. Anything inserted between matching opening and closing tags is known as the tag's content.</p>
<p>Some tags can provide more specific instructions using an <b>attribute</b>. An attribute is added to the opening tag with an equals sign (<code class="inline-code">=</code>). This allows you to specify details like a particular color or a link's address.</p>
</section>
<section class="babycode-guide-section">
<h2 id="text-formatting-tags">Text formatting tags</h2>
<ul class='babycode-guide-list'>
<li>To make some text <strong>bold</strong>, enclose it in <code class="inline-code">[b][/b]</code>:<br>
[b]Hello World[/b]<br>
Will become<br>
<strong>Hello World</strong>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>To <em>italicize</em> text, enclose it in <code class="inline-code">[i][/i]</code>:<br>
[i]Hello World[/i]<br>
Will become<br>
<em>Hello World</em>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>To make some text <del>strikethrough</del>, enclose it in <code class="inline-code">[s][/s]</code>:<br>
[s]Hello World[/s]<br>
Will become<br>
<del>Hello World</del>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>To <u>underline</u> some text, enclose it in <code class="inline-code">[u][/u]</code>:<br>
[u]Hello World[/u]<br>
Will become<br>
<u>Hello World</u>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>To make some text {{ "[big]big[/big]" | babycode | safe }}, enclose it in <code class="inline-code">[big][/big]</code>:<br>
[big]Hello World[/big]<br>
Will become<br>
{{ "[big]Hello World[/big]" | babycode | safe }}
<li>Similarly, you can make text {{ "[small]small[/small]" | babycode | safe }} with <code class="inline-code">[small][/small]</code>:<br>
[small]Hello World[/small]<br>
Will become<br>
{{ "[small]Hello World[/small]" | babycode | safe }}
</li>
</ul>
<ul class='babycode-guide-list'>
<li>You can change the text color by using <code class="inline-code">[color][/color]</code>:<br>
[color=red]Red text[/color]<br>
[color=white]White text[/color]<br>
[color=#3b08f0]Blueish text[/color]<br>
Will become<br>
{{ "[color=red]Red text[/color]" | babycode | safe }}<br>
{{ "[color=white]White text[/color]" | babycode | safe }}<br>
{{ "[color=#3b08f0]Blueish text[/color]" | babycode | safe }}<br>
</li>
</ul>
<ul class='babycode-guide-list'>
<li>You can center text by enclosing it in <code class="inline-code">[center][/center]</code>:<br>
[center]Hello World[/center]<br>
Will become<br>
{{ "[center]Hello World[/center]" | babycode | safe }}
</li>
<li>You can right-align text by enclosing it in <code class="inline-code">[right][/right]</code>:<br>
[right]Hello World[/right]<br>
Will become<br>
{{ "[right]Hello World[/right]" | babycode | safe }}
</li>
Note: the center and right tags will break the paragraph. See <a href="#paragraph-rules">Paragraph rules</a> for more details.
</ul>
</section>
<section class="babycode-guide-section">
<h2 id="emoji">Emoji</h2>
<p>There are a few emoji in the style of old forum emotes:</p>
<table class="emoji-table">
<tr>
<th>Short code</th>
<th>Emoji result</th>
</tr>
{% for emoji in __emoji %}
<tr>
<td>{{ ("[code]:%s:[/code]" % emoji) | babycode | safe }}</td>
<td>{{ __emoji[emoji] | safe }}</td>
</tr>
{% endfor %}
</table>
<p>Special thanks to the <a href="https://gh.vercte.net/forumoji/">Forumoji project</a> and its contributors for these graphics.</p>
</section>
<section class="babycode-guide-section">
<h2 id="paragraph-rules">Paragraph rules</h2>
<p>Line breaks in babycode work like Markdown: to start a new paragraph, use two line breaks:</p>
{{ '[code]paragraph 1\n\nparagraph 2[/code]' | babycode | safe }}
Will produce:<br>
{{ 'paragraph 1\n\nparagraph 2' | babycode | safe }}
<p>To break a line without starting a new paragraph, end a line with two spaces:</p>
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
That will produce:<br>
{{ 'paragraph 1 \nstill paragraph 1' | babycode | safe }}
<p>Additionally, the following tags will break into a new paragraph:</p>
<ul>
<li><code class="inline-code">[code]</code> (code block, not inline);</li>
<li><code class="inline-code">[img]</code>;</li>
<li><code class="inline-code">[center]</code>;</li>
<li><code class="inline-code">[right]</code>;</li>
<li><code class="inline-code">[ul]</code> and <code class="inline-code">[ol]</code>;</li>
<li><code class="inline-code">[quote]</code>.</li>
</ul>
</section>
<section class="babycode-guide-section">
<h2 id="links">Links</h2>
<p>Loose links (starting with http:// or https://) will automatically get converted to clickable links. To add a label to a link, use<br><code class="inline-code">[url=https://example.com]Link label[/url]</code>:<br>
<a href="https://example.com">Link label</a></p>
</section>
<section class="babycode-guide-section">
<h2 id="attaching-an-image">Attaching an image</h2>
<p>To add an image to your post, use the <code class="inline-code">[img]</code> tag:<br>
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code>
{{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }}
</p>
<p>The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p>
<p>Multiple images attached to a post can be clicked to open a dialog to view them.</p>
</section>
<section class="babycode-guide-section">
<h2 id="adding-code-blocks">Adding code blocks</h2>
{% set code = 'func _ready() -> void:\n\tprint("hello world!")' %}
<p>There are two kinds of code blocks recognized by babycode: inline and block. Inline code blocks do not break a paragraph. They can be added with <code class="inline-code">[code]your code here[/code]</code>. As long as there are no line breaks inside the code block, it is considered inline. If there are any, it will produce this:</p>
{{ ('[code]%s[/code]' % code) | babycode | safe }}
<p>Optionally, you can enable syntax highlighting by specifying the language in the attribute like this: <code class="inline-code">[code=gdscript]</code></p>
{{ ('[code=gdscript]%s[/code]' % code) | babycode | safe}}
<p>A full list of languages that can be highlighted is available <a href="https://pygments.org/languages/" target=_blank>here</a> (the short names column).</p>
<p>Inline code tags look like this: {{ '[code]Inline code[/code]' | babycode | safe }}</p>
<p>Babycodes are not parsed inside code blocks.</p>
</section>
<section class="babycode-guide-section">
<h2 id="quoting">Quoting</h2>
<p>Text enclosed within <code class="inline-code">[quote][/quote]</code> will look like a quote:</p>
<blockquote>A man provided with paper, pencil, and rubber, and subject to strict discipline, is in effect a universal machine.</blockquote>
</section>
<section class="babycode-guide-section">
<h2 id="lists">Lists</h2>
{% set list = '[ul]\nitem 1\n\nitem 2\n\nitem 3 \nstill item 3 (break line without inserting a new item by using two spaces at the end of a line)\n[/ul]' %}
<p>There are two kinds of lists, ordered (1, 2, 3, ...) and unordered (bullet points). Ordered lists are made with <code class="inline-code">[ol][/ol]</code> tags, and unordered with <code class="inline-code">[ul][/ul]</code>. Every new paragraph according to the <a href="#paragraph-rules">usual paragraph rules</a> will create a new list item. For example:</p>
{{ ('[code]%s[/code]' % list) | babycode | safe }}
Will produce the following list:
{{ list | babycode | safe }}
</section>
<section class="babycode-guide-section">
<h2 id="spoilers">Spoilers</h2>
{% set spoiler = "[spoiler=Major Metal Gear Spoilers]Snake dies[/spoiler]" %}
<p>You can make a section collapsible by using the <code class="inline-code">[spoiler]</code> tag:</p>
{{ ("[code]\n%s[/code]" % spoiler) | babycode | safe }}
Will produce:
{{ spoiler | babycode | safe }}
All other tags are supported inside spoilers.
</section>
<section class="babycode-guide-section">
<h2 id="mentions">Mentioning users</h2>
<p>You can mention users by their username (<em>not</em> their display name) by using <code class="inline-code">@username</code>. A user's username is always shown below their avatar and display name on their posts and their user page.</p>
<p>A mention will show up on your post as a clickable box with the user's display name if they have one set or their username with an <code class="inline-code">@</code> symbol if they don't:</p>
<a class="mention" href="#mentions" title="@user-without-display-name">@user-without-display-name</a>
<a class="mention display" href="#mentions" title="@user-with-display-name">User with display name</a>
<a class="mention display me" href="#mentions" title="@your-username">Your display name</a>
<p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p>
</section>
{% endset %}
{{ sections | safe }}
</div>
<div class="guide-toc">
<h2>Table of contents</h2>
{% set toc = sections | extract_h2 %}
<ul>
{% for heading in toc %}
<li><a href='#{{ heading.id }}'>{{ heading.text }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@@ -10,10 +10,10 @@
{% endif %}
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
<link rel="icon" type="image/png" href="/static/favicon.png">
<script src="{{ '/static/js/vnd/bitty-6.0.0-rc3.min.js' | cachebust }}" type="module"></script>
<script src="{{ '/static/js/vnd/bitty-7.0.0-rc1.min.js' | cachebust }}" type="module"></script>
</head>
<body>
<bitty-6-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}">
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}">
{% include 'common/topnav.html' %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
@@ -23,9 +23,7 @@
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
<footer class="darkbg">
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
</footer>
</bitty-6-0>
{% include 'common/footer.html' %}
</bitty-7-0>
<script src="{{ "/static/js/ui.js" | cachebust }}"></script>
</body>

View File

@@ -1,8 +1,8 @@
{% extends 'base.html' %}
{% block title %}not found{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<h1 class="thread-title">404 Not Found</h1>
<div class="darkbg">
<h1>404 Not Found</h1>
<p>The requested URL does not exist.</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% block title %}request entity too large{% endblock %}
{% block content %}
<div class="darkbg">
<h1>413 Request Entity Too Large</h1>
<p>The file(s) you tried to upload are too large.</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,7 @@
<footer id="footer">
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
<ul class="horizontal">
<li><a href="{{ url_for('guides.contact') }}">Contact</a></li>
<li><a href="{{ url_for('guides.guides_index') }}">Guides</a></li>
</ul>
</footer>

View File

@@ -99,7 +99,7 @@
<button data-send="insertBabycodeTag" data-tag="spoiler=" data-break-line="1" data-prefill="hidden content" class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler" {{"disabled" if "spoiler" in banned_tags else ""}}>{{ icn_spoiler() }}</button>
</span>
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }} autocomplete="off" data-receive="insertBabycodeTag addQuote">{{ prefill }}</textarea>
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
<a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}" target="_blank">babycode guide</a>
{% if banned_tags %}
<div>Forbidden tags:</div>
<div>
@@ -138,6 +138,17 @@
</form>
{% endmacro %}
{% macro badge_button(badge) %}
{% set img_url = badge.file_path if badge.file_path else badge.get_image_url() %}
{% if badge.link %}
<a href="{{badge.link}}" rel="noopener noreferrer me" target="_blank">
{% endif %}
<img class="badge-button" src="{{img_url}}" alt="{{badge.label}}" title="{{badge.label}}"></img>
{% if badge.link %}
</a>
{% endif %}
{% endmacro %}
{% macro full_post(
post, render_sig = True, is_latest = False,
editing = False, active_user = None, no_reply = false,
@@ -161,6 +172,11 @@
{% if post['status'] %}
<em class="user-status">{{ post['status'] }}</em>
{% endif %}
<div class="badges-container">
{% for badge_data in (post.badges_json | fromjson) %}
{{ badge_button(badge_data) }}
{% endfor %}
</div>
</div>
</div>
@@ -287,3 +303,59 @@
</div>
</div>
{% endmacro %}
{% macro guide_sections() %}
<div class="guide-container">
<div class="guide-topics">
{% set sections %}{{ caller() }}{% endset %}
{{ sections | safe }}
</div>
<div class="guide-toc">
<h2>Table of contents</h2>
<a href="#top">(top)</a>
{% set toc = sections | extract_h2 %}
<ul>
{% for heading in toc %}
<li><a href='#{{ heading.id }}'>{{ heading.text }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endmacro %}
{% macro badge_editor_single(options={}, selected=none, fp_hidden=true, badge=none) %}
{% set defaults = options | selectattr('user_id', 'none') | list | sort(attribute='file_path') %}
{% set uploads = options | selectattr('user_id') | list %}
{% if selected is not none %}
{% set selected_href = (options | selectattr('id', 'equalto', selected) | list)[0].file_path %}
{% else %}
{% set selected_href = defaults[0].file_path %}
{% endif %}
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorBadge" data-listeners="click input submit change">
<div class="settings-badge-container" data-receive="deleteBadge">
<div class="settings-badge-select">
<select data-send="badgeUpdatePreview badgeToggleFilePicker" name="badge_choice[]" required>
<optgroup label="Default">
{% for option in defaults %}
<option data-file-path="{{ option.file_path }}" value="{{ option.id }}" {{ "selected" if selected==option.id else "" }}>{{option.file_path | basename_noext}}</option>
{% endfor %}
</optgroup>
<optgroup label="Your uploads">
{% for option in uploads %}
<option data-file-path="{{ option.file_path }}" value="{{ option.id }}" {{ "selected" if selected==option.id else "" }}>{{option.original_filename | basename_noext}}</option>
{% endfor %}
<option value="custom">Upload new...</option>
</optgroup>
</select>
<img class="badge-button" data-receive="badgeUpdatePreview badgeUpdatePreviewCustom" src="{{ selected_href }}"></img>
</div>
<div class="settings-badge-file-picker{{ " hidden" if fp_hidden else ""}}" data-receive="badgeToggleFilePicker">
<button data-send="openBadgeFilePicker" type=button>Browse&hellip;</button>
<input data-receive="openBadgeFilePicker badgeErrorSize badgeErrorDim badgeHideErrors" data-send="badgeUpdatePreviewCustom" type="file" accept="image/*" class="hidden" name="badge_file[]">
</div>
<input type="text" required placeholder="Label" name="badge_label[]" value="{{badge.label}}">
<input type="text" placeholder="(Optional) Link" name="badge_link[]" value="{{badge.link}}">
<button data-send="deleteBadge" type="button" class="critical" title="Delete">X</button>
</div>
</bitty-7-0>
{% endmacro %}

View File

@@ -0,0 +1,4 @@
{% from 'common/macros.html' import badge_editor_single with context %}
{% for badge in badges %}
{{ badge_editor_single(options=uploads, selected=badge.upload, badge=badge) }}
{% endfor %}

View File

@@ -0,0 +1,2 @@
{% from 'common/macros.html' import badge_editor_single with context %}
{{ badge_editor_single(options=uploads) }}

View File

@@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% from 'common/macros.html' import guide_sections with context %}
{% block title %}guide - {{ guide.title }}{% endblock %}
{% block content %}
<div class="darkbg" id="top">
<h1>Guide: {{ guide.title }}</h1>
<ul class="horizontal">
<li><a href="{{ url_for('guides.category_index', category=category) }}">&uarr; Back to category</a></li>
{% if prev_guide %}
<li><a href="{{ prev_guide.url }}">&larr; Previous: {{ prev_guide.title }}</a></li>
{% endif %}
{% if next_guide %}
<li><a href="{{ next_guide.url }}">&rarr; Next: {{ next_guide.title }}</a></li>
{% endif %}
</ul>
</div>
{% call() guide_sections() %}
{% block guide_content %}
{% endblock %}
{% endcall %}
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block title %}guides - {{ category | title }}{% endblock %}
{% block content %}
<div class="darkbg">
<h1>All guides in category "{{ category | replace('-', ' ') | title }}"</h1>
<ul>
{% for page in pages %}
<li><a href="{{ page.url }}">{{ page.title }}</a></li>
{% endfor %}
</ul>
<div>
<a href="{{ url_for('guides.guides_index') }}">&larr; All guide categories</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block title %}contact us{% endblock %}
{% block content %}
<div class="darkbg">
<h1>Contact</h1>
{% if config.ADMIN_CONTACT_INFO %}
<p>The administrators of {{ config.SITE_NAME }} provide the following contact information:</p>
<div>{{ config.ADMIN_CONTACT_INFO | babycode_strict | safe }}</div>
{% else %}
<p>The administrators of {{ config.SITE_NAME }} did not provide any contact information.</p>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block title %}guides{% endblock %}
{% block content %}
<div class="darkbg">
<h1>Guides index</h1>
<ul>
{% for category in categories %}
<li>
<h2><a href="{{ url_for('guides.category_index', category=category )}}">{{ category | replace('-', ' ') | title }}</a></h2>
<ul>
{% for page in categories[category] %}
<li><a href="{{ page.url }}">{{ page.title }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@@ -0,0 +1,75 @@
# Introduction
{% extends 'guides/_layout.html' %}
{% block guide_content %}
<section class="guide-section">
<h2 id="pyrom">What is {{config.SITE_NAME}}?</h2>
{% if config.GUIDE_DESCRIPTION %}
<div>{{ config.GUIDE_DESCRIPTION | babycode_strict | safe }}</div>
{% endif %}
<p>{{ config.SITE_NAME }} is powered by <a href="https://git.poto.cafe/yagich/pyrom/" target="_blank">Pyrom</a>, a home-grown forum software for the indie web. This guide will help you get started, from creating an account to understanding how discussions are organized.</p>
</section>
<section class="guide-section">
<h2 id="signing-up">Signing up</h2>
<p>Most interactive features on {{ config.SITE_NAME }} such as creating threads or posting replies require a user account. While browsing is open to all, you must be signed in to participate.</p>
{% if config.DISABLE_SIGNUP %}
<p>{{ config.SITE_NAME }} uses an invite-only sign-up system. You cannot sign up directly. Instead, {% if config.USERS_CAN_INVITE %}an existing user{% elif config.MODS_CAN_INVITE %}a moderator{% endif %} can generate an invite link containing an invite key. Using that link will take you to the sign up page.</p>
{% else %}
<p>You can create an account on the <a href="{{url_for('users.sign_up')}}">sign-up page.</a> After you create your account, a moderator will need to manually approve your account before you can post. This may take some time.</p>
{% endif %}
<p>To create an account, you will need to provide:</p>
<ul>
<li>A username:
<ul>
<li>Your username must be 3-20 characters long and only include lowercase letters (a-z), numbers (0-9), hyphens (-) and underscores (_).</li>
<li>If your username includes uppercase characters, the system will automatically convert them to lowercase. Your original capitalization will be preserved as part of your display name.</li>
</ul>
</li>
<li>A strong password:
<ul>
<li>Your password must be at least 10 characters long and include at least one uppercase letter, at least one lowercase letter, at least one number, at least one special character, and no spaces.</li>
</ul>
</li>
</ul>
<p>Once your account is {% if not config.DISABLE_SIGNUP %}confirmed and{% endif %} active, you will have full access to create threads, post replies, and use other features.</p>
</section>
<section class="guide-section">
<h2 id="topics">Topics</h2>
<p>The front page of {{ config.SITE_NAME }} is an overview of topics that you can create threads in. A topic is a broad category created by moderators to organize discussions.</p>
<p>Topics usually have a description that is shown under its name. Use it as guidance for where to post a thread. The order topics show up in is chosen by moderators.</p>
<p>A topic may be locked by a moderator. This will prevent threads from being created or moved into it by non-moderators. These topics are usually used for forum-wide information, but it is of course up to the moderators.</p>
<p>By default, threads in a topic are sorted by activity, but this can be changed in your user settings.</p>
</section>
<section class="guide-section">
<h2 id="threads">Threads</h2>
<p>Threads are where discussion happens. A thread is started by a user with a name and an initial post, and other users may add their own posts to it (known as replying to the thread).</p>
<p>When drafting a thread, you have to choose which topic to post it under. Be sure to follow the posting rules. If you created a thread under the wrong topic, you may move it when viewing replies. Moderators have the option to move any thread.</p>
<p>A thread may be locked by the original poster (OP) or a moderator. When a thread is locked, nobody except moderators can reply to it.</p>
<p>A thread may also be stickied by a moderator. Stickied threads show above all other threads in the topic it's under. If a thread is stickied, it is likely important.</p>
<p>You can subscribe to any thread. When subscribed, you will receive an overview of posts you missed for that thread in your inbox.</p>
</section>
<section class="guide-section">
<h2 id="posts">Posts</h2>
<p>Posts are the individual messages that make up a thread. The first post starts the thread, and every subsequent message is a post in reply.</p>
<p>A post is split up into five sections:</p>
<ol>
<li>Usercard
<ul><li>The post author's information shows up to the left of the post. This includes their avatar, display name, mention, status, and badges.</li></ul>
</li>
<li>Post actions
<ul><li>This shows the time and date when the post has been made (or edited), and buttons for actions you can perform on the post, such as quoting or editing.</li></ul>
</li>
<li>Post content
<ul><li>The actual message of the post.</li></ul>
</li>
<li>Signature (sig)
<ul><li>If the user has a signature set, it will show under each of their posts.</li></ul>
</li>
<li>Reactions
<ul><li>This shows the emoji reactions users have added to the post, and you can add your own.</li></ul>
</li>
</ol>
<p>You may edit or delete your posts after creating them. Edited posts will show when they were edited instead of when they were posted.</p>
<p>You can quote another user's post by pressing the <button>Quote</button> button on that post. This will copy the contents of it into the reply box and mention the user.</p>
<p>Posts are written in a markup language called Babycode. <a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}">More information on it can be found in a separate guide.</a></p>
</section>
{% endblock %}

View File

@@ -0,0 +1,25 @@
# User profiles
{% extends 'guides/_layout.html' %}
{% block guide_content %}
<section class="guide-section">
<h2 id="profile">User profiles</h2>
<p>Each user on {{ config.SITE_NAME }} has a profile.</p>
<p>A user's profile shows:</p>
<ul>
<li>Their avatar;</li>
<li>Their username and display name;</li>
<li>Their status;</li>
<li>Their signature;</li>
<li>Their badges;</li>
<li>Their stats:
<ul>
<li>Their permission level (regular user, moderator, etc);</li>
<li>The number of posts they've created;</li>
<li>The number of threads they've started;</li>
<li>A link to their latest started thread;</li>
<li>A few of their latest posts.</li>
</ul>
</li>
</ul>
</section>
{% endblock %}

View File

@@ -0,0 +1,92 @@
# User settings
{% extends 'guides/_layout.html' %}
{% block guide_content %}
<section class="guide-section">
<h2 id="settings">Settings</h2>
<p>You can access your settings by following the "Settings" link in the top navigation bar or from the <button>Settings</button> button in your profile.</p>
<p>The settings page lets you set your avatar, change your personalization options, and change your password.</p>
</section>
<section class="guide-section">
<h2 id="avatar">Avatar</h2>
<p>When you upload an avatar, keep in mind that it will be cropped to a square and resized to fit 256 by 256 pixels. It's best to upload an image that is already square. Avatars must be no more than 1 MB in size. You can also clear your avatar, resetting it to the default one.</p>
<p>To change your avatar, first press the <button>Browse&hellip;</button> button and select a file from your device, then press the <button>Save avatar</button> button to upload it.</p>
</section>
<section class="guide-section">
<h2 id="personalization">Personalization</h2>
<p>The personalization section of the settings lets you change the following settings:</p>
<ul>
<li>Theme
<ul>
<li>Set the appearance of the forum. This feature is still in beta and themes may change unexpectedly.</li>
</ul>
</li>
<li>Thread sorting
<ul>
<li>Set the sorting of threads when viewing a single topic. "Latest activity" will put threads which had a response more recently closer to the top. "Thread creation date" will put threads which were created more recently closer to the top.</li>
</ul>
</li>
<li>Display name
<ul>
<li>If set, your display name will show up instead of your username in most places. In a post's usercard, the display name will show up above your username/mention. In posts that have mentioned you, your display name will be shown instead of your @username.</li>
<li>Display names have fewer restrictions than usernames, and most characters are allowed.</li>
<li>If you do not wish to use a display name, you can leave the field blank.</li>
</ul>
</li>
<li>Status
<ul>
<li>If set, your status will show up below your @mention in a post's usercard. 100 characters limit, no <a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}">Babycodes.</a></li>
</ul>
</li>
<li>Subscribe by default
<ul>
<li>If checked, you will automatically add the thread to your subscriptions when replying to it. You can override this setting by checking or unchecking "Subscribe to thread" before posting your reply in the reply box.</li>
</ul>
</li>
<li>Signature
<ul>
<li>If set, this signature will appear under all of your posts. <a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}">Babycode</a> is allowed (except @mentions).</li>
</ul>
</li>
</ul>
</section>
<section class="guide-section">
<h2 id="password">Changing your password</h2>
<p>You can change your password by typing your desired new password in the "New password" field and again in the "Confirm new password" field, then pressing the <button class="warn">Change password</button> button. The passwords in the two fields must match.</p>
</section>
<section class="guide-section">
<h2 id="badges">Badges</h2>
<p>Badges, also known as buttons, are 88x31 images that you can use to add more flair to your profile or link to other websites.</p>
<p>Badges you set will be shown on the usercard in threads and on your profile page. You can have up to 10 badges.</p>
<p>To add a badge, press the "Add badge" button. A badge editor will be added. A badge consists of three parts:</p>
<ol>
<li>Image
<ul>
<li>{{ config.SITE_NAME }} provides a selection of default badges that you can use. You may select one by using the dropdown, under the "Default" category.</li>
<li>Alternatively, you may upload your own image. To do so, select the "Upload&hellip;" option in the dropdown, under the "Your uploads" category. A <button>Browse&hellip;</button> button will appear, letting you select a file. The image must be exactly 88x31 pixels and may not be over 500KB in size.<br>
Custom badge images that you have uploaded before can be reused and will appear under "Your uploads". If a badge image you've uploaded before is not used by any of your badges, it will be deleted and you will have to upload it again if you wish to reuse it.<br>
Other users can not use images you've uploaded as part of their badges unless they download it manually.</li>
</ul>
</li>
<li>Label
<ul>
<li>The label will be shown when the badge is hovered over. It will also be the badge image's alt text.</li>
</ul>
</li>
<li>Link
<ul>
<li>Optionally, you may turn the badge into a clickable button by providing a link. The link will open in a new tab when pressed, and will use <code class="inline-code">rel="me"</code> so you can use your profile as verification on platforms like Mastodon.</li>
</ul>
</li>
</ol>
<p>If a badge is not valid, its editor and any invalid fields will have a dashed border.</p>
<p>You can delete a badge by pressing the <button class="critical">X</button> button. Your changes are not saved until you press the <button>Save badges</button> button.</p>
</section>
<section class="guide-section">
<h2 id="deleting">Deleting your account</h2>
<p>If you no longer wish to participate in {{ config.SITE_NAME }}, you may delete your account. To do so, press the <button class="critical">Delete account</button> button in your settings, which will take you to a confirmation page.</p>
<p>Deleting your account is an irreversible action. Once you delete your account, <strong>you will not be able to log back in to it.</strong></p>
<p>Deleting your account <strong>does not delete your threads and posts.</strong> They will be kept intact to preserve history. They will be altered to show up as if a system user authored them.</p>
<p>If you want your posts and threads to be deleted as well, you may either delete them yourself or <a href="{{url_for("guides.contact")}}" target="_blank">contact the administrators</a> for help.</p>
<p>If you're sure you want to delete your account, you will need to type your password on the confirmation page before pressing the <button class="critical">Delete account</button> button.</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,29 @@
# Inbox/subscriptions
{% extends 'guides/_layout.html' %}
{% block guide_content %}
<section class="guide-section">
<h2 id="overview">Subscriptions</h2>
<p>You can subscribe to any thread. When subscribed, you will receive an overview of posts you missed for that thread in your inbox.</p>
<p>Additionally, when browsing a topic, threads you've subscribed to will show the number of posts you haven't read in it next to the thread's name.</p>
</section>
<section class="guide-section">
<h2 id="inbox">Inbox</h2>
<p>You can access your Inbox by following the "Inbox" link in the top navigation bar.</p>
<p>The Inbox is split into two parts:</p>
<ol>
<li>Subscriptions
<ul>
<li>A table of all threads that you've subscribed to. You can unsubscribe from each thread by pressing the <button class="warn">Unsubscribe</button> button for that thread's row.</li>
<li>This section is collapsible, like a Babycode spoiler - press the "-" button to collapse it.</li>
</ul>
</li>
<li>Unread posts
<ul>
<li>These are the posts you missed in each thread, if any. They are sorted in reverse chronological order and are grouped by thread. Each thread is collapsible.</li>
<li>You can mark an entire thread as read by pressing the <button>Mark thread as Read</button> button, or unsubscribe from it by pressing the <button class="warn">Unsubscribe</button> button.</li>
</ul>
</li>
</ol>
<p>Your inbox is updated automatically when you browse a subscribed thread. Once you've seen the posts you missed, they will disappear from your Inbox.</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,39 @@
# Bookmarks
{% from 'common/icons.html' import icn_bookmark %}
{% extends 'guides/_layout.html' %}
{% block guide_content %}
<section class="guide-section">
<h2 id="bookmarks">Bookmarks</h2>
<p>You can bookmark whole threads or individual posts to save them for later reading. Bookmarks are split into collections.</p>
<p>Each post or thread you bookmark (collectively bookmarks) can only belong to one collection.</p>
<p>Each bookmark can have a short memo that you can add to it, for example to help you remember why you saved it.</p>
</section>
<section class="guide-section">
<h2 id="collections">Bookmark collections</h2>
<p>Bookmarks are organized into collections. There is always at least one collection you have access to, called "Bookmarks" by default.</p>
<p>You can add any number of collections by pressing the <button>Manage collections</button> button on the your bookmarks page.</p>
<p>On the "Manage bookmark collections" page, you can add, delete, rename, and reorder your collections.</p>
<p>To add a new collection, press the <button>Add new collection</button> button. A new item will be added.</p>
<p>You can name any collection by using the input box.</p>
<p>You can reorder collections by dragging and dropping it above or below others. The default collection is always at the top and can not be moved.</p>
<p>You can delete a collection by pressing the <button class="critical">Delete</button> button on that collection. Deleting a collection will remove all bookmarks from it. If you delete a collection by accident, simply refresh the page. Your changes are not saved until you press the <button>Save</button> button.</p>
</section>
<section class="guide-section">
<h2 id="bookmarking">Bookmarking posts and threads</h2>
<ul>
<li>To bookmark a thread, press the <button class="contain-svg inline icon">{{ icn_bookmark(20) }}Bookmark&hellip;</button> button at the top of the thread.</li>
<li>To bookmark a post, press the <button class="contain-svg inline icon">{{ icn_bookmark(20) }}Bookmark&hellip;</button> button on that post.</li>
</ul>
<p>Both will bring up a popup with your bookmark collections. You can select the collection to save the bookmark into by pressing its name and add an optional memo. To save the bookmark, press the <button>Save</button> button. Each bookmark can only belong to one collection.</p>
<p>If the post or thread is already bookmarked, the name of the collection will be highlighed and the box to its name will be filled. If you wish to remove the bookmark, press the name of the collection it's in and press the <button>Save</button> button.</p>
</section>
<section class="guide-section">
<h2 id="browsing-bookmarks">Browsing and managing bookmarks</h2>
<p>You can access your bookmarks by following the "Bookmarks" link in the top navigation bar.</p>
<p>On the Bookmarks page, each collection will show up as a collapsible section. If a collection is empty, it will be collapsed.</p>
<p>If a collection has any bookmarks, it will have two collapsibe sections nested inside it: one for threads and one for posts.</p>
<p>Bookmarked threads are shown as a table with their title, the memo, and a <button class="contain-svg inline icon">{{ icn_bookmark(20) }}Manage&hellip;</button> button. Pressing this button will show the same popup, letting you move or remove the bookmark.</p>
<p>Bookmarked posts are shown in the same way they're shown in a thread. The memo will be shown before the post's time stamp. To the right of the post is the same <button class="contain-svg inline icon">{{ icn_bookmark(20) }}Manage&hellip;</button> button.</p>
<p>If you made changes to bookmarked threads or posts on this page, you will have to refresh to save those changes.</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,184 @@
# Babycode
{% extends 'guides/_layout.html' %}
{% block guide_content %}
<section class="guide-section">
<h2 id="what-is-babycode">What is babycode?</h2>
<p>You may be familiar with BBCode, a loosely related family of markup languages popular on forums. Babycode is another, simplified, dialect of those languages. It is a way of formatting text by enclosing parts of it in special tags.</p>
<p>A <b>tag</b> is a short name enclosed in square brackets. Tags can be opening tags, like <code class="inline-code">[b]</code> or closing tags, like <code class="inline-code">[/b]</code>. Anything inserted between matching opening and closing tags is known as the tag's content.</p>
<p>Some tags can provide more specific instructions using an <b>attribute</b>. An attribute is added to the opening tag with an equals sign (<code class="inline-code">=</code>). This allows you to specify details like a particular color or a link's address.</p>
<p>Babycode is used in posts, user signatures, and MOTDs.</p>
</section>
<section class="guide-section">
<h2 id="text-formatting-tags">Text formatting tags</h2>
<ul class='guide-list'>
<li>To make some text <strong>bold</strong>, enclose it in <code class="inline-code">[b][/b]</code>:<br>
[b]Hello World[/b]<br>
Will become<br>
<strong>Hello World</strong>
</li>
</ul>
<ul class='guide-list'>
<li>To <em>italicize</em> text, enclose it in <code class="inline-code">[i][/i]</code>:<br>
[i]Hello World[/i]<br>
Will become<br>
<em>Hello World</em>
</li>
</ul>
<ul class='guide-list'>
<li>To make some text <del>strikethrough</del>, enclose it in <code class="inline-code">[s][/s]</code>:<br>
[s]Hello World[/s]<br>
Will become<br>
<del>Hello World</del>
</li>
</ul>
<ul class='guide-list'>
<li>To <u>underline</u> some text, enclose it in <code class="inline-code">[u][/u]</code>:<br>
[u]Hello World[/u]<br>
Will become<br>
<u>Hello World</u>
</li>
</ul>
<ul class='guide-list'>
<li>To make some text {{ "[big]big[/big]" | babycode | safe }}, enclose it in <code class="inline-code">[big][/big]</code>:<br>
[big]Hello World[/big]<br>
Will become<br>
{{ "[big]Hello World[/big]" | babycode | safe }}
<li>Similarly, you can make text {{ "[small]small[/small]" | babycode | safe }} with <code class="inline-code">[small][/small]</code>:<br>
[small]Hello World[/small]<br>
Will become<br>
{{ "[small]Hello World[/small]" | babycode | safe }}
</li>
</ul>
<ul class='guide-list'>
<li>You can change the text color by using <code class="inline-code">[color][/color]</code>:<br>
[color=red]Red text[/color]<br>
[color=white]White text[/color]<br>
[color=#3b08f0]Blueish text[/color]<br>
Will become<br>
{{ "[color=red]Red text[/color]" | babycode | safe }}<br>
{{ "[color=white]White text[/color]" | babycode | safe }}<br>
{{ "[color=#3b08f0]Blueish text[/color]" | babycode | safe }}<br>
</li>
</ul>
<ul class='guide-list'>
<li>You can center text by enclosing it in <code class="inline-code">[center][/center]</code>:<br>
[center]Hello World[/center]<br>
Will become<br>
{{ "[center]Hello World[/center]" | babycode | safe }}
</li>
<li>You can right-align text by enclosing it in <code class="inline-code">[right][/right]</code>:<br>
[right]Hello World[/right]<br>
Will become<br>
{{ "[right]Hello World[/right]" | babycode | safe }}
</li>
Note: the center and right tags will break the paragraph. See <a href="#paragraph-rules">Paragraph rules</a> for more details.
</ul>
</section>
<section class="guide-section">
<h2 id="emoji">Emoji</h2>
<p>There are a few emoji in the style of old forum emotes:</p>
<table class="emoji-table">
<tr>
<th>Short code</th>
<th>Emoji result</th>
</tr>
{% for emoji in __emoji %}
<tr>
<td>{{ ("[code]:%s:[/code]" % emoji) | babycode | safe }}</td>
<td>{{ __emoji[emoji] | safe }}</td>
</tr>
{% endfor %}
</table>
<p>Special thanks to the <a href="https://gh.vercte.net/forumoji/">Forumoji project</a> and its contributors for these graphics.</p>
</section>
<section class="guide-section">
<h2 id="paragraph-rules">Paragraph rules</h2>
<p>Line breaks in babycode work like Markdown: to start a new paragraph, use two line breaks:</p>
{{ '[code]paragraph 1\n\nparagraph 2[/code]' | babycode | safe }}
Will produce:<br>
{{ 'paragraph 1\n\nparagraph 2' | babycode | safe }}
<p>To break a line without starting a new paragraph, end a line with two spaces:</p>
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
That will produce:<br>
{{ 'paragraph 1 \nstill paragraph 1' | babycode | safe }}
<p>Additionally, the following tags will break into a new paragraph:</p>
<ul>
<li><code class="inline-code">[code]</code> (code block, not inline);</li>
<li><code class="inline-code">[img]</code>;</li>
<li><code class="inline-code">[center]</code>;</li>
<li><code class="inline-code">[right]</code>;</li>
<li><code class="inline-code">[ul]</code> and <code class="inline-code">[ol]</code>;</li>
<li><code class="inline-code">[quote]</code>.</li>
</ul>
</section>
<section class="guide-section">
<h2 id="links">Links</h2>
<p>Loose links (starting with http:// or https://) will automatically get converted to clickable links. To add a label to a link, use<br><code class="inline-code">[url=https://example.com]Link label[/url]</code>:<br>
<a href="https://example.com">Link label</a></p>
</section>
<section class="guide-section">
<h2 id="attaching-an-image">Attaching an image</h2>
<p>To add an image to your post, use the <code class="inline-code">[img]</code> tag:<br>
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code>
{{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }}
</p>
<p>The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p>
<p>Multiple images attached to a post can be clicked to open a dialog to view them.</p>
</section>
<section class="guide-section">
<h2 id="adding-code-blocks">Adding code blocks</h2>
{% set code = 'func _ready() -> void:\n\tprint("hello world!")' %}
<p>There are two kinds of code blocks recognized by babycode: inline and block. Inline code blocks do not break a paragraph. They can be added with <code class="inline-code">[code]your code here[/code]</code>. As long as there are no line breaks inside the code block, it is considered inline. If there are any, it will produce this:</p>
{{ ('[code]%s[/code]' % code) | babycode | safe }}
<p>Optionally, you can enable syntax highlighting by specifying the language in the attribute like this: <code class="inline-code">[code=gdscript]</code></p>
{{ ('[code=gdscript]%s[/code]' % code) | babycode | safe}}
<p>A full list of languages that can be highlighted is available <a href="https://pygments.org/languages/" target=_blank>here</a> (the short names column).</p>
<p>Inline code tags look like this: {{ '[code]Inline code[/code]' | babycode | safe }}</p>
<p>Babycodes are not parsed inside code blocks.</p>
</section>
<section class="guide-section">
<h2 id="quoting">Quoting</h2>
<p>Text enclosed within <code class="inline-code">[quote][/quote]</code> will look like a quote:</p>
<blockquote>A man provided with paper, pencil, and rubber, and subject to strict discipline, is in effect a universal machine.</blockquote>
</section>
<section class="guide-section">
<h2 id="lists">Lists</h2>
{% set list = '[ul]\nitem 1\n\nitem 2\n\nitem 3 \nstill item 3 (break line without inserting a new item by using two spaces at the end of a line)\n[/ul]' %}
<p>There are two kinds of lists, ordered (1, 2, 3, ...) and unordered (bullet points). Ordered lists are made with <code class="inline-code">[ol][/ol]</code> tags, and unordered with <code class="inline-code">[ul][/ul]</code>. Every new paragraph according to the <a href="#paragraph-rules">usual paragraph rules</a> will create a new list item. For example:</p>
{{ ('[code]%s[/code]' % list) | babycode | safe }}
Will produce the following list:
{{ list | babycode | safe }}
</section>
<section class="guide-section">
<h2 id="spoilers">Spoilers</h2>
{% set spoiler = "[spoiler=Major Metal Gear Spoilers]Snake dies[/spoiler]" %}
<p>You can make a section collapsible by using the <code class="inline-code">[spoiler]</code> tag:</p>
{{ ("[code]\n%s[/code]" % spoiler) | babycode | safe }}
Will produce:
{{ spoiler | babycode | safe }}
All other tags are supported inside spoilers.
</section>
<section class="guide-section">
<h2 id="mentions">Mentioning users</h2>
<p>You can mention users by their username (<em>not</em> their display name) by using <code class="inline-code">@username</code>. A user's username is always shown below their avatar and display name on their posts and their user page.</p>
<p>A mention will show up on your post as a clickable box with the user's display name if they have one set or their username with an <code class="inline-code">@</code> symbol if they don't:</p>
<a class="mention" href="#mentions" title="@user-without-display-name">@user-without-display-name</a>
<a class="mention display" href="#mentions" title="@user-with-display-name">User with display name</a>
<a class="mention display me" href="#mentions" title="@your-username">Your display name</a>
<p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p>
</section>
<section class="guide-section">
<h2 id="void-tags">Void tags</h2>
<p>The special void tags <code class="inline-code">[lb]</code>, <code class="inline-code">[rb]</code>, and <code class="inline-code">[@]</code> will appear as the literal characters <code class="inline-code">[</code>, <code class="inline-code">]</code>, and <code class="inline-code">@</code> respectively. Unlike other tags, they are self-contained and have no closing equivalent.</p>
<ul class="guide-list">
{% set lbrb = "[color=red]This text will be red[/color]\n\n[lb]color=red[rb]This text won't be red[lb]/color[rb]" %}
<li><code class="inline-code">[lb]</code> and <code class="inline-code">[rb]</code> allow you to use square brackets without them being interpreted as Babycode:
{{ ("[code]" + lbrb + "[/code]") | babycode | safe }}
Will result in:<br>
{{ lbrb | babycode | safe }}
</li>
<li>The <code class="inline-code">[@]</code> tag allows you to use the @ symbol without it being turned into a mention.</li>
</ul>
</section>
{% endblock %}

View File

@@ -2,7 +2,7 @@
{% extends 'base.html' %}
{% block title %}editing MOTD{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<div class="darkbg">
<h1>Edit Message of the Day</h1>
<p>The Message of the Day will show up on the main page and in every topic.</p>
<form method="POST">

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}moderation{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<div class="darkbg">
<h1>Moderation actions</h1>
<ul>
<li><a href="{{ url_for('mod.user_list') }}">User list</a></li>

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block content %}
<div class="darkbg settings-container">
<div class="darkbg">
<h1>Change topics order</h1>
<p>Drag topic titles to reoder them. Press submit when done. The topics will appear to users in the order set here.</p>
<form method="post" id=topics-container>

View File

@@ -2,7 +2,7 @@
{% extends "base.html" %}
{% block title %}drafting a thread{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<div class="darkbg">
<h1>New thread</h1>
<form method="post">
<label for="topic_id">Topic</label>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}creating a topic{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<div class="darkbg">
<h1>Create topic</h1>
<form method="post">
<label for=name>Name</label>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}creating a topic{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<div class="darkbg">
<h1>Editing topic {{ topic['name'] }}</h1>
<form method="post">
<label for=name>Name</label>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}managing bookmark collections{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<div class="darkbg">
<h1>Manage bookmark collections</h1>
<p>Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.</p>
<div>

View File

@@ -1,10 +1,10 @@
{% extends 'base.html' %}
{% block title %}delete confirmation{% endblock %}
{% block content %}
<div class="darkbg login-container">
<div class="darkbg">
<h1>Confirm account deletion</h1>
<p>Are you sure you want to delete your account on {{ config.SITE_NAME }}? <strong>This action is irreversible.</strong> Your posts and threads will remain accessible to preserve history but will be de-personalized, showing up as authored by a system user. Posts that @mention you will also mention the system user instead.</p>
<p>If you wish for any and all content relating to you to be removed, you will have to ask {{ config.SITE_NAME }}'s administrators separately.</p>
<p>If you wish for any and all content relating to you to be removed, you will have to <a href="{{url_for("guides.contact")}}" target="_blank">contact {{ config.SITE_NAME }}'s administrators separately.</a></p>
<p>If you are sure, please confirm your current password below.</p>
<form method="post">
<label for="password">Confirm password</label>

View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block title %}Log in{% endblock %}
{% block content %}
<div class="darkbg login-container">
<div class="darkbg">
<h1>Log in</h1>
<form method="post">
<label for="username">Username</label><br>

View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block title %}Reset password{% endblock %}
{% block content %}
<div class="darkbg login-container">
<div class="darkbg">
<h1>Reset password for {{username}}</h1>
<p>Send this link to {{username}} to allow them to reset their password.</p>
<form method="post">

View File

@@ -3,46 +3,67 @@
{% block title %}settings{% endblock %}
{% block content %}
{% set disable_avatar = not is_logged_in() %}
<div class='darkbg settings-container'>
<div class='darkbg'>
<h1>User settings</h1>
<form class='avatar-form' method='post' action='{{ url_for('users.set_avatar', username=active_user.username) }}' enctype='multipart/form-data'>
<span>Set avatar (1mb max)</span>
<img src='{{ active_user.get_avatar_url() }}'>
<input id='file' type='file' name='avatar' accept='image/*' required>
<div>
<input type='submit' value='Upload avatar' {{ 'disabled' if disable_avatar else '' }}>
<input type='submit' value='Clear avatar' formaction='{{ url_for('users.clear_avatar', username=active_user.username) }}' formnovalidate {{ 'disabled' if active_user.is_default_avatar() else '' }}>
</div>
</form>
<form method='post'>
<label for='theme'>Theme (beta)</label>
<select autocomplete='off' id='theme' name='theme'>
{% for theme in config.allowed_themes %}
<option value="{{ theme }}" {{ 'selected' if get_prefers_theme() == theme }}>{{ theme | theme_name }}</option>
{% endfor %}
</select>
<label for='topic_sort_by'>Sort threads by:</label>
<select id='topic_sort_by' name='topic_sort_by'>
<option value='activity' {{ 'selected' if session['sort_by'] == 'activity' else '' }}>Latest activity</option>
<option value='thread' {{ 'selected' if session['sort_by'] == 'thread' else '' }}>Thread creation date</option>
</select>
<label for='display_name'>Display name</label>
<input type='text' id='display_name' name='display_name' value='{{ active_user.display_name }}' pattern="(?:[\w!#$%^*\(\)\-_=+\[\]\{\}\|;:,.?\s]{3,50})?" title='3-50 characters, no @, no <>, no &' placeholder='Optional. Will be shown in place of username.' autocomplete='off'></input>
<label for='status'>Status</label>
<input type='text' id='status' name='status' value='{{ active_user.status }}' maxlength=100 placeholder='Will be shown under your name. Max 100 characters.'>
<label for='babycode-content'>Signature</label>
{{ babycode_editor_component(ta_name='signature', prefill=active_user.signature_original_markup, ta_placeholder='Will be shown under each of your posts', optional=true) }}
<input autocomplete='off' type='checkbox' id='subscribe_by_default' name='subscribe_by_default' {{ 'checked' if session.get('subscribe_by_default', default=true) else '' }}>
<label for='subscribe_by_default'>Subscribe to thread by default when responding</label><br>
<input type='submit' value='Save settings'>
</form>
<form method='post' action='{{ url_for('users.change_password', username=active_user.username) }}'>
<label for="new_password">Change password</label><br>
<input type="password" id="new_password" name="new_password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<label for="new_password2">Confirm new password</label><br>
<input type="password" id="new_password2" name="new_password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<input class="warn" type="submit" value="Change password">
</form>
<div class="settings-grid">
<fieldset class="hfc">
<legend>Set avatar</legend>
<form class='avatar-form' method='post' action='{{ url_for('users.set_avatar', username=active_user.username) }}' enctype='multipart/form-data'>
<img src='{{ active_user.get_avatar_url() }}'>
<input id='file' type='file' name='avatar' accept='image/*' required>
<div>
<input type='submit' value='Save avatar' {{ 'disabled' if disable_avatar else '' }}>
<input type='submit' value='Clear avatar' formaction='{{ url_for('users.clear_avatar', username=active_user.username) }}' formnovalidate {{ 'disabled' if active_user.is_default_avatar() else '' }}>
</div>
<span>1MB maximum size. Avatar will be cropped to square.</span>
</form>
</fieldset>
<fieldset class="hfc">
<legend>Personalization</legend>
<form method='post'>
<label for='theme'>Theme (beta)</label>
<select autocomplete='off' id='theme' name='theme'>
{% for theme in config.allowed_themes %}
<option value="{{ theme }}" {{ 'selected' if get_prefers_theme() == theme }}>{{ theme | theme_name }}</option>
{% endfor %}
</select>
<label for='topic_sort_by'>Sort threads by:</label>
<select id='topic_sort_by' name='topic_sort_by'>
<option value='activity' {{ 'selected' if session['sort_by'] == 'activity' else '' }}>Latest activity</option>
<option value='thread' {{ 'selected' if session['sort_by'] == 'thread' else '' }}>Thread creation date</option>
</select>
<label for='display_name'>Display name</label>
<input type='text' id='display_name' name='display_name' value='{{ active_user.display_name }}' pattern="(?:[\w!#$%^*\(\)\-_=+\[\]\{\}\|;:,.?\s]{3,50})?" title='3-50 characters, no @, no <>, no &' placeholder='Optional. Will be shown in place of username.' autocomplete='off'></input>
<label for='status'>Status</label>
<input type='text' id='status' name='status' value='{{ active_user.status }}' maxlength=100 placeholder='Will be shown under your name. Max 100 characters.'>
<input autocomplete='off' type='checkbox' id='subscribe_by_default' name='subscribe_by_default' {{ 'checked' if session.get('subscribe_by_default', default=true) else '' }}>
<label for='subscribe_by_default'>Subscribe to thread by default when responding</label><br>
<label for='babycode-content'>Signature</label>
{{ babycode_editor_component(ta_name='signature', prefill=active_user.signature_original_markup, ta_placeholder='Will be shown under each of your posts', optional=true, banned_tags=SIG_BANNED_TAGS) }}
<input type='submit' value='Save settings'>
</form>
</fieldset>
<fieldset class="hfc">
<legend>Change password</legend>
<form method='post' action='{{ url_for('users.change_password', username=active_user.username) }}'>
<label for="new_password">New password</label><br>
<input type="password" id="new_password" name="new_password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<label for="new_password2">Confirm new password</label><br>
<input type="password" id="new_password2" name="new_password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<input class="warn" type="submit" value="Change password">
</form>
</fieldset>
<fieldset>
<legend>Badges</legend>
<a href="{{ url_for('guides.guide_page', category='user-guides', slug='settings', _anchor='badges')}}">Badges help</a>
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorForm" data-listeners="click input submit change">
<form data-use="badgeEditorPrepareSubmit" data-init='loadBadgeEditor' data-receive='addBadge' method='post' enctype='multipart/form-data' action='{{ url_for('users.save_badges', username=active_user.username) }}'>
<div>Loading badges&hellip;</div>
<div>If badges fail to load, JS may be disabled.</div>
</form>
</bitty-7-0>
</fieldset>
</div>
<div>
<a class="linkbutton critical" href="{{ url_for('users.delete_page', username=active_user.username) }}">Delete account</a>
</div>

View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block title %}Sign up{% endblock %}
{% block content %}
<div class="darkbg login-container">
<div class="darkbg">
<h1>Sign up</h1>
{% if inviter %}
<p>You have been invited by <a href="{{ url_for('users.page', username=inviter.username) }}">{{ inviter.get_readable_name() }}</a> to join {{ config.SITE_NAME }}. Create an identity below.</p>

View File

@@ -1,4 +1,4 @@
{% from 'common/macros.html' import timestamp %}
{% from 'common/macros.html' import timestamp, badge_button %}
{% extends 'base.html' %}
{% block title %}{{ target_user.get_readable_name() }}'s profile{% endblock %}
{% block content %}
@@ -54,6 +54,11 @@
Signature:
<div>{{ target_user.signature_rendered | safe }}</div>
{% endif %}
<div class="badges-container">
{% for badge in target_user.get_badges() %}
{{ badge_button(badge) }}
{% endfor %}
</div>
</div>
</div>
<div class="user-page-stats">

View File

@@ -1,8 +0,0 @@
SITE_NAME = "Porom"
DISABLE_SIGNUP = false # if true, no one can sign up.
# if neither of the following two options is true,
# no one can sign up. this may be useful later when/if LDAP is implemented.
MODS_CAN_INVITE = true # if true, allows moderators to create invite links. useless unless DISABLE_SIGNUP to be true.
USERS_CAN_INVITE = false # if true, allows users to create invite links. useless unless DISABLE_SIGNUP to be true.

View File

@@ -0,0 +1,18 @@
SITE_NAME = "Pyrom"
DISABLE_SIGNUP = false # if true, no one can sign up.
# if neither of the following two options is true,
# no one can sign up. this may be useful later when/if LDAP is implemented.
MODS_CAN_INVITE = true # if true, allows moderators to create invite links. useless unless DISABLE_SIGNUP to be true.
USERS_CAN_INVITE = false # if true, allows users to create invite links. useless unless DISABLE_SIGNUP to be true.
# contact information, will be shown in /guides/contact
# some babycodes allowed
# forbidden tags: [spoiler], [img], @mention, [big], [small], [center], [right], [color]
ADMIN_CONTACT_INFO = ""
# forum information. shown in the introduction guide at /guides/user/introduction
# some babycodes allowed
# forbidden tags: [spoiler], [img], @mention, [big], [small], [center], [right], [color]
GUIDE_DESCRIPTION = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

View File

@@ -26,10 +26,26 @@
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: normal;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-Italic-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: italic;
}
*, ::before, ::after {
box-sizing: border-box;
}
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default;
font-size: 0.9em;
font-family: "Cadman";
font-size: 1rem;
font-family: "Cadman", sans-serif;
text-decoration: none;
border: 1px solid black;
border-radius: 4px;
@@ -38,16 +54,17 @@
}
body {
font-family: "Cadman";
font-family: "Cadman", sans-serif;
margin: 20px 100px;
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
color: black;
}
a:link {
:where(a:link) {
color: #c11c1c;
}
a:visited {
:where(a:visited) {
color: #730c0c;
}
@@ -73,6 +90,16 @@ a:visited {
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
}
#footer {
padding: 10px;
margin: 0;
display: flex;
justify-content: end;
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
justify-content: space-between;
align-items: baseline;
}
.darkbg {
padding-bottom: 10px;
padding-left: 10px;
@@ -90,7 +117,7 @@ a:visited {
font-size: 3rem;
margin: 0 20px;
text-decoration: none;
color: black !important;
color: black;
}
.thread-title {
@@ -107,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;
@@ -188,6 +215,10 @@ a:visited {
padding: 10px 0;
}
code {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
pre code {
display: block;
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
@@ -550,7 +581,7 @@ pre code { /* Literal.Number.Integer.Long */ }
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: "Cadman";
font-family: "Cadman", sans-serif;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
background-color: #c1ceb1;
@@ -680,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);
@@ -701,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;
@@ -722,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);
@@ -744,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);
@@ -773,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);
@@ -813,18 +844,6 @@ p {
display: inline;
}
.login-container > * {
width: 70%;
margin: auto;
max-width: 1000px;
}
.settings-container > * {
width: 70%;
margin: auto;
max-width: 1000px;
}
.avatar-form {
display: flex;
flex-direction: column;
@@ -837,15 +856,20 @@ input[type=text], input[type=password], textarea, select {
border-radius: 4px;
padding: 7px 10px;
width: 100%;
box-sizing: border-box;
resize: vertical;
color: black;
background-color: rgb(217.8, 225.6, 208.2);
font-size: 100%;
font-family: inherit;
}
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
background-color: rgb(230.2, 235.4, 223.8);
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
.infobox {
border: 2px solid black;
background-color: #81a3e6;
@@ -940,10 +964,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
gap: 5px;
}
.thread-info-bookmark-button {
margin-left: auto !important;
}
.thread-info-post-preview {
overflow: hidden;
text-overflow: ellipsis;
@@ -951,14 +971,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
margin-right: 25%;
}
.babycode-guide-section {
.guide-section {
background-color: #c1ceb1;
padding: 5px 20px;
border: 1px solid black;
padding-right: 25%;
}
.babycode-guide-container {
.guide-container {
display: grid;
grid-template-columns: 1.5fr 300px;
grid-template-rows: 1fr;
@@ -1096,7 +1116,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
.babycode-editor {
height: 150px;
font-size: 1rem;
}
.babycode-editor-container {
@@ -1109,7 +1128,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
.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);
@@ -1189,7 +1208,6 @@ ul.horizontal li, ol.horizontal li {
.accordion {
border-top-right-radius: 4px;
border-top-left-radius: 4px;
box-sizing: border-box;
border: 1px solid black;
margin: 10px 5px;
overflow: hidden;
@@ -1271,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);
@@ -1309,7 +1327,7 @@ footer {
justify-content: center;
}
.babycode-guide-list {
.guide-list {
border-bottom: 1px dashed;
}
@@ -1351,7 +1369,7 @@ footer {
mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
width: 24px;
height: 24px;
padding: 0 10px;
padding: 0 20px;
flex-shrink: 0;
}
.bookmark-dropdown-item.selected {
@@ -1432,3 +1450,76 @@ a.mention:hover, a.mention:visited:hover {
background-color: rgb(229.84, 231.92, 227.28);
color: black;
}
.settings-grid {
display: grid;
gap: 10px;
--grid-item-max-width: calc((100% - 10px) / 2);
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr));
}
.settings-grid fieldset {
border: 1px solid white;
border-radius: 4px;
background-color: rgb(171.527945374, 172.0409719488, 170.8965280512);
}
.hfc {
height: fit-content;
}
h1 {
margin: 0;
}
.settings-badge-container {
display: flex;
align-items: baseline;
gap: 5px;
border: 1px solid black;
border-radius: 4px;
padding: 5px 10px;
margin: 10px 0;
}
.settings-badge-container:has(input:invalid) {
border: 2px dashed red;
}
.settings-badge-container input:invalid {
border: 2px dashed red;
}
.settings-badge-file-picker {
display: flex;
flex-direction: column;
align-items: center;
}
.settings-badge-file-picker input.hidden[type=file] {
width: 100%;
}
.settings-badge-file-picker input.hidden[type=file]::file-selector-button {
display: none;
}
.settings-badge-file-picker.hidden {
display: none;
}
.settings-badge-select {
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
min-width: 200px;
}
img.badge-button {
min-width: 88px;
min-height: 31px;
max-width: 88px;
max-height: 31px;
}
.badges-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 5px;
}

View File

@@ -26,10 +26,26 @@
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: normal;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-Italic-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: italic;
}
*, ::before, ::after {
box-sizing: border-box;
}
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default;
font-size: 0.9em;
font-family: "Cadman";
font-size: 1rem;
font-family: "Cadman", sans-serif;
text-decoration: none;
border: 1px solid black;
border-radius: 8px;
@@ -38,16 +54,17 @@
}
body {
font-family: "Cadman";
font-family: "Cadman", sans-serif;
margin: 20px 100px;
background-color: #220d16;
color: #e6e6e6;
}
a:link {
:where(a:link) {
color: #e87fe1;
}
a:visited {
:where(a:visited) {
color: #ed4fb1;
}
@@ -73,6 +90,16 @@ a:visited {
background-color: #231c23;
}
#footer {
padding: 10px;
margin: 0;
display: flex;
justify-content: end;
background-color: #231c23;
justify-content: space-between;
align-items: baseline;
}
.darkbg {
padding-bottom: 10px;
padding-left: 10px;
@@ -90,7 +117,7 @@ a:visited {
font-size: 3rem;
margin: 0 20px;
text-decoration: none;
color: white !important;
color: white;
}
.thread-title {
@@ -107,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;
@@ -188,6 +215,10 @@ a:visited {
padding: 10px 0;
}
code {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
pre code {
display: block;
background-color: #302731;
@@ -550,7 +581,7 @@ pre code { /* Literal.Number.Integer.Long */ }
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: "Cadman";
font-family: "Cadman", sans-serif;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
background-color: #9b649b;
@@ -680,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);
@@ -701,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);
@@ -722,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);
@@ -744,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);
@@ -773,7 +804,7 @@ p {
.pagebutton {
background-color: #3c283c;
color: #e6e6e6 !important;
color: #e6e6e6;
}
.pagebutton:hover {
background-color: rgb(109.2, 72.8, 109.2);
@@ -813,18 +844,6 @@ p {
display: inline;
}
.login-container > * {
width: 70%;
margin: auto;
max-width: 1000px;
}
.settings-container > * {
width: 70%;
margin: auto;
max-width: 1000px;
}
.avatar-form {
display: flex;
flex-direction: column;
@@ -837,15 +856,20 @@ input[type=text], input[type=password], textarea, select {
border-radius: 8px;
padding: 7px 10px;
width: 100%;
box-sizing: border-box;
resize: vertical;
color: #e6e6e6;
background-color: #371e37;
font-size: 100%;
font-family: inherit;
}
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
background-color: #514151;
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
.infobox {
border: 2px solid black;
background-color: #775891;
@@ -940,10 +964,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
gap: 5px;
}
.thread-info-bookmark-button {
margin-left: auto !important;
}
.thread-info-post-preview {
overflow: hidden;
text-overflow: ellipsis;
@@ -951,14 +971,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
margin-right: 25%;
}
.babycode-guide-section {
.guide-section {
background-color: #231c23;
padding: 5px 20px;
border: 1px solid black;
padding-right: 25%;
}
.babycode-guide-container {
.guide-container {
display: grid;
grid-template-columns: 1.5fr 300px;
grid-template-rows: 1fr;
@@ -1096,7 +1116,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
.babycode-editor {
height: 150px;
font-size: 1rem;
}
.babycode-editor-container {
@@ -1109,7 +1128,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
.tab-button {
background-color: #3c283c;
color: #e6e6e6 !important;
color: #e6e6e6;
}
.tab-button:hover {
background-color: rgb(109.2, 72.8, 109.2);
@@ -1189,7 +1208,6 @@ ul.horizontal li, ol.horizontal li {
.accordion {
border-top-right-radius: 8px;
border-top-left-radius: 8px;
box-sizing: border-box;
border: 1px solid black;
margin: 10px 5px;
overflow: hidden;
@@ -1271,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);
@@ -1309,7 +1327,7 @@ footer {
justify-content: center;
}
.babycode-guide-list {
.guide-list {
border-bottom: 1px dashed;
}
@@ -1351,7 +1369,7 @@ footer {
mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
width: 24px;
height: 24px;
padding: 0 10px;
padding: 0 20px;
flex-shrink: 0;
}
.bookmark-dropdown-item.selected {
@@ -1433,6 +1451,79 @@ a.mention:hover, a.mention:visited:hover {
color: #e6e6e6;
}
.settings-grid {
display: grid;
gap: 10px;
--grid-item-max-width: calc((100% - 10px) / 2);
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr));
}
.settings-grid fieldset {
border: 1px solid black;
border-radius: 8px;
background-color: rgb(141.6, 79.65, 141.6);
}
.hfc {
height: fit-content;
}
h1 {
margin: 0;
}
.settings-badge-container {
display: flex;
align-items: baseline;
gap: 5px;
border: 1px solid black;
border-radius: 8px;
padding: 5px 10px;
margin: 10px 0;
}
.settings-badge-container:has(input:invalid) {
border: 2px dashed red;
}
.settings-badge-container input:invalid {
border: 2px dashed red;
}
.settings-badge-file-picker {
display: flex;
flex-direction: column;
align-items: center;
}
.settings-badge-file-picker input.hidden[type=file] {
width: 100%;
}
.settings-badge-file-picker input.hidden[type=file]::file-selector-button {
display: none;
}
.settings-badge-file-picker.hidden {
display: none;
}
.settings-badge-select {
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
min-width: 200px;
}
img.badge-button {
min-width: 88px;
min-height: 31px;
max-width: 88px;
max-height: 31px;
}
.badges-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 5px;
}
#topnav {
margin-bottom: 10px;
border: 10px solid rgb(40, 40, 40);

View File

@@ -26,10 +26,26 @@
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: normal;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-Italic-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: italic;
}
*, ::before, ::after {
box-sizing: border-box;
}
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default;
font-size: 0.9em;
font-family: "Cadman";
font-size: 1rem;
font-family: "Cadman", sans-serif;
text-decoration: none;
border: 1px solid black;
border-radius: 16px;
@@ -38,16 +54,17 @@
}
body {
font-family: "Cadman";
font-family: "Cadman", sans-serif;
margin: 12px 50px;
background-color: #c85d45;
color: black;
}
a:link {
:where(a:link) {
color: black;
}
a:visited {
:where(a:visited) {
color: black;
}
@@ -73,6 +90,16 @@ a:visited {
background-color: #88486d;
}
#footer {
padding: 6px;
margin: 0;
display: flex;
justify-content: end;
background-color: #88486d;
justify-content: space-between;
align-items: baseline;
}
.darkbg {
padding-bottom: 6px;
padding-left: 6px;
@@ -90,7 +117,7 @@ a:visited {
font-size: 3rem;
margin: 0 12px;
text-decoration: none;
color: black !important;
color: black;
}
.thread-title {
@@ -107,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;
@@ -188,6 +215,10 @@ a:visited {
padding: 6px 0;
}
code {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
pre code {
display: block;
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
@@ -550,7 +581,7 @@ pre code { /* Literal.Number.Integer.Long */ }
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: "Cadman";
font-family: "Cadman", sans-serif;
border-top-right-radius: 16px;
border-top-left-radius: 16px;
background-color: #f27a5a;
@@ -680,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);
@@ -701,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);
@@ -722,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);
@@ -744,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);
@@ -773,7 +804,7 @@ p {
.pagebutton {
background-color: #f27a5a;
color: black !important;
color: black;
}
.pagebutton:hover {
background-color: rgb(244.6, 148.6, 123);
@@ -813,18 +844,6 @@ p {
display: inline;
}
.login-container > * {
width: 70%;
margin: auto;
max-width: 1000px;
}
.settings-container > * {
width: 70%;
margin: auto;
max-width: 1000px;
}
.avatar-form {
display: flex;
flex-direction: column;
@@ -837,15 +856,20 @@ input[type=text], input[type=password], textarea, select {
border-radius: 16px;
padding: 8px;
width: 100%;
box-sizing: border-box;
resize: vertical;
color: black;
background-color: rgb(247.2, 175.2, 156);
font-size: 100%;
font-family: inherit;
}
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
background-color: rgb(249.8, 201.8, 189);
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
.infobox {
border: 2px solid black;
background-color: #81a3e6;
@@ -940,10 +964,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
gap: 3px;
}
.thread-info-bookmark-button {
margin-left: auto !important;
}
.thread-info-post-preview {
overflow: hidden;
text-overflow: ellipsis;
@@ -951,14 +971,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
margin-right: 25%;
}
.babycode-guide-section {
.guide-section {
background-color: #f27a5a;
padding: 3px 12px;
border: 1px solid black;
padding-right: 25%;
}
.babycode-guide-container {
.guide-container {
display: grid;
grid-template-columns: 1.5fr 300px;
grid-template-rows: 1fr;
@@ -1096,7 +1116,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
.babycode-editor {
height: 150px;
font-size: 1rem;
}
.babycode-editor-container {
@@ -1109,7 +1128,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
.tab-button {
background-color: #f27a5a;
color: black !important;
color: black;
}
.tab-button:hover {
background-color: rgb(244.6, 148.6, 123);
@@ -1189,7 +1208,6 @@ ul.horizontal li, ol.horizontal li {
.accordion {
border-top-right-radius: 16px;
border-top-left-radius: 16px;
box-sizing: border-box;
border: 1px solid black;
margin: 6px 3px;
overflow: hidden;
@@ -1271,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);
@@ -1309,7 +1327,7 @@ footer {
justify-content: center;
}
.babycode-guide-list {
.guide-list {
border-bottom: 1px dashed;
}
@@ -1351,7 +1369,7 @@ footer {
mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
width: 24px;
height: 24px;
padding: 0 6px;
padding: 0 24px;
flex-shrink: 0;
}
.bookmark-dropdown-item.selected {
@@ -1433,14 +1451,85 @@ a.mention:hover, a.mention:visited:hover {
color: black;
}
.settings-grid {
display: grid;
gap: 6px;
--grid-item-max-width: calc((100% - 6px) / 2);
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr));
}
.settings-grid fieldset {
border: 1px solid white;
border-radius: 16px;
background-color: rgb(176.5961538462, 106.9038461538, 147.1947115385);
}
.hfc {
height: fit-content;
}
h1 {
margin: 0;
}
.settings-badge-container {
display: flex;
align-items: baseline;
gap: 3px;
border: 1px solid black;
border-radius: 16px;
padding: 3px 6px;
margin: 6px 0;
}
.settings-badge-container:has(input:invalid) {
border: 2px dashed red;
}
.settings-badge-container input:invalid {
border: 2px dashed red;
}
.settings-badge-file-picker {
display: flex;
flex-direction: column;
align-items: center;
}
.settings-badge-file-picker input.hidden[type=file] {
width: 100%;
}
.settings-badge-file-picker input.hidden[type=file]::file-selector-button {
display: none;
}
.settings-badge-file-picker.hidden {
display: none;
}
.settings-badge-select {
display: flex;
flex-direction: column;
gap: 3px;
align-items: center;
min-width: 200px;
}
img.badge-button {
min-width: 88px;
min-height: 31px;
max-width: 88px;
max-height: 31px;
}
.badges-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 3px;
}
#topnav {
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
#bottomnav {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
color: white;
}
@@ -1448,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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown';
const badgeEditorEndpoint = '/hyperapi/badge-editor';
const previewEndpoint = '/api/babycode-preview';
const userEndpoint = '/api/current-user';
@@ -6,12 +7,12 @@ const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))}
export default class {
async showBookmarkMenu(ev, el) {
if ((ev.sender.dataset.bookmarkId === el.getString('bookmarkId')) && el.childElementCount === 0) {
if ((el.sender.dataset.bookmarkId === el.ds('bookmarkId')) && el.childElementCount === 0) {
const searchParams = new URLSearchParams({
'id': ev.sender.dataset.conceptId,
'id': el.sender.dataset.conceptId,
'require_reload': el.dataset.requireReload,
});
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${ev.sender.dataset.bookmarkType}?${searchParams}`;
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${el.sender.dataset.bookmarkType}?${searchParams}`;
const res = await this.api.getHTML(bookmarkMenuHref);
if (res.error) {
return;
@@ -49,9 +50,9 @@ export default class {
}
selectBookmarkCollection(ev, el) {
const clicked = ev.sender;
const clicked = el.sender;
if (ev.sender === el) {
if (el.sender === el) {
if (clicked.classList.contains('selected')) {
clicked.classList.remove('selected');
} else {
@@ -63,7 +64,7 @@ export default class {
}
async saveBookmarks(ev, el) {
const bookmarkHref = el.getString('bookmarkEndpoint');
const bookmarkHref = el.ds('bookmarkEndpoint');
const collection = el.querySelector('.bookmark-dropdown-item.selected');
let data = {};
if (collection) {
@@ -72,7 +73,7 @@ export default class {
data['memo'] = el.querySelector('.bookmark-memo-input').value;
} else {
data['operation'] = 'remove';
data['collection_id'] = el.getString('originallyContainedIn');
data['collection_id'] = el.ds('originallyContainedIn');
}
const options = {
@@ -82,7 +83,7 @@ export default class {
'Content-Type': 'application/json',
},
}
const requireReload = el.getInt('requireReload') !== 0;
const requireReload = el.dsInt('requireReload') !== 0;
el.remove();
await fetch(bookmarkHref, options);
if (requireReload) {
@@ -103,10 +104,10 @@ export default class {
toggleAccordion(ev, el) {
const accordion = el;
const header = accordion.querySelector('.accordion-header');
if (!header.contains(ev.sender)){
if (!header.contains(el.sender)){
return;
}
const btn = ev.sender;
const btn = el.sender;
const content = el.querySelector('.accordion-content');
// these are all meant to be in sync
accordion.classList.toggle('hidden');
@@ -116,15 +117,15 @@ export default class {
toggleTab(ev, el) {
const tabButtonsContainer = el.querySelector('.tab-buttons');
if (!el.contains(ev.sender)) {
if (!el.contains(el.sender)) {
return;
}
if (ev.sender.classList.contains('active')) {
if (el.sender.classList.contains('active')) {
return;
}
const targetId = ev.sender.getString('targetId');
const targetId = el.senderDs('targetId');
const contents = el.querySelectorAll('.tab-content');
for (let content of contents) {
if (content.id === targetId) {
@@ -144,7 +145,7 @@ export default class {
#previousMarkup = null;
async babycodePreview(ev, el) {
if (ev.sender.classList.contains('active')) {
if (el.sender.classList.contains('active')) {
return;
}
@@ -199,9 +200,9 @@ export default class {
}
insertBabycodeTag(ev, el) {
const tagStart = ev.sender.getString('tag');
const breakLine = 'breakLine' in ev.sender.dataset;
const prefill = 'prefill' in ev.sender.dataset ? ev.sender.dataset.prefill : '';
const tagStart = el.senderDs('tag');
const breakLine = 'breakLine' in el.sender.dataset;
const prefill = 'prefill' in el.sender.dataset ? el.sender.dataset.prefill : '';
const hasAttr = tagStart[tagStart.length - 1] === '=';
let tagEnd = tagStart;
@@ -249,13 +250,13 @@ export default class {
}
addQuote(ev, el) {
el.value += ev.sender.value;
el.value += el.sender.value;
el.scrollIntoView();
el.focus();
}
convertTimestamps(ev, el) {
const timestamp = el.getInt('utc');
const timestamp = el.dsInt('utc');
if (!isNaN(timestamp)) {
const date = new Date(timestamp * 1000);
el.textContent = date.toLocaleString();
@@ -272,8 +273,205 @@ export default class {
this.#currentUsername = userInfo.value.user.username;
}
if (el.getString('username') === this.#currentUsername) {
if (el.ds('username') === this.#currentUsername) {
el.classList.add('me');
}
}
}
export class BadgeEditorForm {
#badgeTemplate = undefined;
async loadBadgeEditor(ev, el) {
const badges = await this.api.getHTML(badgeEditorEndpoint);
if (!badges.value) {
return;
}
if (this.#badgeTemplate === undefined){
const badge = await this.api.getHTML(`${badgeEditorEndpoint}/template`)
if (!badge.value){
return;
}
this.#badgeTemplate= badge.value;
}
el.replaceChildren();
const addButton = `<button data-disable-if-max="1" data-receive="updateBadgeCount" DISABLE_IF_MAX type="button" data-send="addBadge">Add badge</button>`;
const submitButton = `<input data-receive="updateBadgeCount" type="submit" value="Save badges">`;
const controls = `<span>${addButton} ${submitButton} <span data-count="1" data-receive="updateBadgeCount">BADGECOUNT/10</span></span>`
const badgeCount = badges.value.querySelectorAll('.settings-badge-container').length;
const subs = [
['BADGECOUNT', badgeCount],
['DISABLE_IF_MAX', badgeCount === 10 ? 'disabled' : ''],
];
el.appendChild(this.api.makeHTML(controls, subs));
el.appendChild(badges.value);
}
addBadge(ev, el) {
if (this.#badgeTemplate === undefined) {
return;
}
const badge = this.#badgeTemplate.cloneNode(true);
el.appendChild(badge);
this.api.localTrigger('updateBadgeCount');
}
deleteBadge(ev, el) {
if (!el.contains(el.sender)) {
return;
}
el.remove();
this.api.localTrigger('updateBadgeCount');
}
updateBadgeCount(_ev, el) {
const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length;
if (el.dsInt('disableIfMax') === 1) {
el.disabled = badgeCount === 10;
} else if (el.dsInt('count') === 1) {
el.textContent = `${badgeCount}/10`;
}
}
badgeEditorPrepareSubmit(ev, el) {
if (ev.type !== 'submit') {
return;
}
ev.preventDefault();
const badges = el.querySelectorAll('.settings-badge-container').length;
const noUploads = el.querySelectorAll('.settings-badge-file-picker.hidden input[type=file]');
noUploads.forEach(e => {
e.value = null;
})
// console.log(noUploads);
el.submit();
// console.log('would submit now');
}
}
const validateBase64Img = dataURL => new Promise(resolve => {
const img = new Image();
img.onload = () => {
resolve(img.width === 88 && img.height === 31);
};
img.src = dataURL;
});
export class BadgeEditorBadge {
#badgeCustomImageData = null;
badgeUpdatePreview(ev, el) {
if (ev.type !== 'change') {
return;
}
// TODO: el.sender doesn't have a bittyParentBittyId
const selectBittyParent = el.sender.closest('bitty-7-0');
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
return;
}
if (ev.val === 'custom') {
if (this.#badgeCustomImageData) {
el.src = this.#badgeCustomImageData;
} else {
el.removeAttribute('src');
}
return;
}
const option = el.sender.selectedOptions[0];
el.src = option.dataset.filePath;
}
async badgeUpdatePreviewCustom(ev, el) {
if (ev.type !== 'change') {
return;
}
if (el.bittyParentBittyId !== el.sender.bittyParentBittyId) {
return;
}
const file = ev.target.files[0];
if (file.size >= 1000 * 500) {
this.api.trigger('badgeErrorSize');
this.#badgeCustomImageData = null;
el.removeAttribute('src');
return;
}
const reader = new FileReader();
reader.onload = async e => {
const dimsValid = await validateBase64Img(e.target.result);
if (!dimsValid) {
this.api.trigger('badgeErrorDim');
this.#badgeCustomImageData = null;
el.removeAttribute('src');
return;
}
this.#badgeCustomImageData = e.target.result;
el.src = this.#badgeCustomImageData;
this.api.trigger('badgeHideErrors');
}
reader.readAsDataURL(file);
}
badgeToggleFilePicker(ev, el) {
if (ev.type !== 'change') {
return;
}
// TODO: el.sender doesn't have a bittyParentBittyId
const selectBittyParent = el.sender.closest('bitty-7-0');
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
return;
}
const filePicker = el.querySelector('input[type=file]');
if (ev.val === 'custom') {
el.classList.remove('hidden');
if (filePicker.dataset.validity) {
filePicker.setCustomValidity(filePicker.dataset.validity);
}
filePicker.required = true;
} else {
el.classList.add('hidden');
filePicker.setCustomValidity('');
filePicker.required = false;
}
}
openBadgeFilePicker(ev, el) {
// TODO: el.sender doesn't have a bittyParentBittyId
if (el.sender.parentNode !== el.parentNode) {
return;
}
el.click();
}
badgeErrorSize(_ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
const validity = "Image can't be over 500KB."
el.dataset.validity = validity;
el.setCustomValidity(validity);
el.reportValidity();
}
badgeErrorDim(_ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
const validity = "Image must be exactly 88x31 pixels."
el.dataset.validity = validity;
el.setCustomValidity(validity);
el.reportValidity();
}
badgeHideErrors(_ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
delete el.dataset.validity;
el.setCustomValidity('');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@
$ACCENT_COLOR: #c1ceb1 !default;
$DARK_1: color.scale($ACCENT_COLOR, $lightness: -25%, $saturation: -97%) !default;
$DARK_1_LIGHTER: color.scale($DARK_1, $lightness: 25%);
$DARK_2: color.scale($ACCENT_COLOR, $lightness: -30%, $saturation: -60%) !default;
$DARK_3: color.scale($ACCENT_COLOR, $lightness: -80%, $saturation: -70%) !default;
@@ -51,9 +52,6 @@ $BIGGER_PADDING: 30px !default;
$PAGE_SIDE_MARGIN: 100px !default;
$SETTINGS_WIDTH: 70% !default;
$SETTINGS_MAX_WIDTH: 1000px !default;
// **************
// BORDERS
// **************
@@ -96,14 +94,32 @@ $DEFAULT_BORDER_RADIUS: 4px !default;
font-style: italic;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: normal;
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url("/static/fonts/AtkinsonHyperlegibleMono-Italic-VariableFont_wght.ttf");
font-weight: 125 950;
font-style: italic;
}
*, ::before, ::after {
box-sizing: border-box;
}
$button_border: $DEFAULT_BORDER !default;
$button_padding: $SMALL_PADDING $BIG_PADDING !default;
$button_border_radius: $DEFAULT_BORDER_RADIUS !default;
$button_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
%button-base {
cursor: default;
font-size: 0.9em;
font-family: "Cadman";
font-size: 1rem;
font-family: "Cadman", sans-serif;
text-decoration: none;
border: $button_border;
border-radius: $button_border_radius;
@@ -117,7 +133,7 @@ $icon_button_padding_left: $BIG_PADDING - 4px !default;
@mixin button($color, $font_color) {
@extend %button-base;
background-color: $color;
color: $font_color !important; //!important because linkbutton is an <a>
color: $font_color;
&:hover {
background-color: color.scale($color, $lightness: 20%);
@@ -155,7 +171,7 @@ $navbar_margin: 0 !default;
$body_margin: $BIG_PADDING $PAGE_SIDE_MARGIN !default;
body {
font-family: "Cadman";
font-family: "Cadman", sans-serif;
// font-size: 18px;
margin: $body_margin;
background-color: $MAIN_BG;
@@ -164,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 {
@@ -189,6 +203,12 @@ $bottomnav_color: $DARK_1 !default;
@include navbar($bottomnav_color);
}
#footer {
@include navbar($bottomnav_color);
justify-content: space-between;
align-items: baseline;
}
$darkbg_color: $DARK_1 !default;
.darkbg {
padding-bottom: $MEDIUM_PADDING;
@@ -211,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;
@@ -229,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;
@@ -341,6 +361,10 @@ $signature_container_padding: $MEDIUM_PADDING $ZERO_PADDING !default;
padding: $signature_container_padding;
}
code {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
$code_background_color: $DARK_3 !default;
$code_font_color: white !default;
$code_border_radius: 8px !default;
@@ -445,7 +469,7 @@ $copy_code_border: 2px solid black !default;
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: "Cadman";
font-family: "Cadman", sans-serif;
border-top-right-radius: $code_border_radius;
border-top-left-radius: $code_border_radius;
background-color: $copy_code_header_background;
@@ -654,22 +678,6 @@ $pagebutton_min_width: $BIG_PADDING !default;
display: inline;
}
$login_container_width: $SETTINGS_WIDTH !default;
$login_container_max_width: $SETTINGS_MAX_WIDTH !default;
.login-container > * {
width: $login_container_width;
margin: auto;
max-width: $login_container_max_width;
}
$settings_container_width: $SETTINGS_WIDTH !default;
$settings_container_max_width: $SETTINGS_MAX_WIDTH !default;
.settings-container > * {
width: $settings_container_width;
margin: auto;
max-width: $settings_container_max_width
}
$avatar_form_padding: $BIG_PADDING $ZERO_PADDING !default;
.avatar-form {
display: flex;
@@ -689,16 +697,21 @@ input[type="text"], input[type="password"], textarea, select {
border-radius: $text_input_border_radius;
padding: $text_input_padding;
width: 100%;
box-sizing: border-box;
resize: vertical;
color: $text_input_font_color;
background-color: $text_input_background;
font-size: 100%;
font-family: inherit;
&:focus {
background-color: $text_input_background_focus;
}
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
$infobox_info_color: #81a3e6 !default;
$infobox_critical_color: #ed8181 !default;
$infobox_warn_color: #fbfb8d !default;
@@ -821,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;
@@ -833,24 +842,24 @@ $thread_info_post_preview_margin_right: $post_inner_padding_right !default;
margin-right: $thread_info_post_preview_margin_right;
}
$babycode_guide_section_background: $ACCENT_COLOR !default;
$babycode_guide_section_padding: $SMALL_PADDING $BIG_PADDING !default;
$babycode_guide_section_border: $DEFAULT_BORDER !default;
$babycode_guide_section_padding_right: $post_inner_padding_right !default;
.babycode-guide-section {
background-color: $babycode_guide_section_background;
padding: $babycode_guide_section_padding;
border: $babycode_guide_section_border;
padding-right: $babycode_guide_section_padding_right;
$guide_section_background: $ACCENT_COLOR !default;
$guide_section_padding: $SMALL_PADDING $BIG_PADDING !default;
$guide_section_border: $DEFAULT_BORDER !default;
$guide_section_padding_right: $post_inner_padding_right !default;
.guide-section {
background-color: $guide_section_background;
padding: $guide_section_padding;
border: $guide_section_border;
padding-right: $guide_section_padding_right;
}
$babycode_guide_toc_width: 300px !default;
$babycode_guide_column_guide: $ZERO_PADDING !default;
.babycode-guide-container {
$guide_toc_width: 300px !default;
$guide_column_guide: $ZERO_PADDING !default;
.guide-container {
display: grid;
grid-template-columns: 1.5fr $babycode_guide_toc_width;
grid-template-columns: 1.5fr $guide_toc_width;
grid-template-rows: 1fr;
gap: $babycode_guide_column_guide;
gap: $guide_column_guide;
grid-auto-flow: row;
grid-template-areas:
"guide-topics guide-toc";
@@ -861,21 +870,21 @@ $babycode_guide_column_guide: $ZERO_PADDING !default;
overflow: hidden;
}
$babycode_guide_toc_padding: $MEDIUM_PADDING !default;
$babycode_guide_toc_background: $BUTTON_COLOR !default;
$babycode_guide_toc_border_radius: 8px !default;
$babycode_guide_toc_border: $DEFAULT_BORDER !default;
$guide_toc_padding: $MEDIUM_PADDING !default;
$guide_toc_background: $BUTTON_COLOR !default;
$guide_toc_border_radius: 8px !default;
$guide_toc_border: $DEFAULT_BORDER !default;
.guide-toc {
grid-area: guide-toc;
position: sticky;
top: 100px;
align-self: start;
padding: $babycode_guide_toc_padding;
border-bottom-right-radius: $babycode_guide_toc_border_radius;
background-color: $babycode_guide_toc_background;
border-right: $babycode_guide_toc_border;
border-top: $babycode_guide_toc_border;
border-bottom: $babycode_guide_toc_border;
padding: $guide_toc_padding;
border-bottom-right-radius: $guide_toc_border_radius;
background-color: $guide_toc_background;
border-right: $guide_toc_border;
border-top: $guide_toc_border;
border-bottom: $guide_toc_border;
}
.emoji-table tr td {
@@ -1025,7 +1034,6 @@ $post_editing_context_margin: $BIG_PADDING $ZERO_PADDING !default;
.babycode-editor {
height: 150px;
font-size: 1rem;
}
.babycode-editor-container {
@@ -1118,7 +1126,6 @@ $accordion_margin: $MEDIUM_PADDING $SMALL_PADDING !default;
.accordion {
border-top-right-radius: $accordion_border_radius;
border-top-left-radius: $accordion_border_radius;
box-sizing: border-box;
border: $accordion_border;
margin: $accordion_margin;
overflow: hidden; // for border-radius clipping
@@ -1243,9 +1250,9 @@ $reaction_popover_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
justify-content: center;
}
$babycode_guide_list_border: 1px dashed !default;
.babycode-guide-list {
border-bottom: $babycode_guide_list_border;
$guide_list_border: 1px dashed !default;
.guide-list {
border-bottom: $guide_list_border;
}
.bookmark-dropdown-inner {
@@ -1280,7 +1287,7 @@ $bookmark_dropdown_item_background_hover: $BUTTON_COLOR_HOVER !default;
$bookmark_dropdown_item_background_selected: $BUTTON_COLOR_2 !default;
$bookmark_dropdown_item_background_selected_hover: $BUTTON_COLOR_2_HOVER !default;
$bookmark_dropdown_item_icon_size: 24px !default;
$bookmark_dropdown_item_icon_padding: 0 $MEDIUM_PADDING !default;
$bookmark_dropdown_item_icon_padding: 0 $BIG_PADDING !default;
.bookmark-dropdown-item {
display: flex;
justify-content: space-between;
@@ -1405,5 +1412,102 @@ a.mention, a.mention:visited {
background-color: $mention_background_color_hover;
color: $mention_font_color_hover;
}
}
$settings_grid_gap: $MEDIUM_PADDING !default;
$settings_grid_item_min_width: 400px !default;
$settings_grid_fieldset_border: 1px solid $DEFAULT_FONT_COLOR_INVERSE !default;
$settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
.settings-grid {
display: grid;
gap: $settings_grid_gap;
--grid-item-max-width: calc((100% - #{$settings_grid_gap}) / 2);
grid-template-columns: repeat(auto-fill, minmax(max($settings_grid_item_min_width, var(--grid-item-max-width)), 1fr));
& fieldset {
border: $settings_grid_fieldset_border;
border-radius: $settings_grid_fieldset_border_radius;
background-color: $DARK_1_LIGHTER;
}
}
.hfc {
height: fit-content;
}
$compact_h1_margin: $ZERO_PADDING !default;
h1 {
margin: $compact_h1_margin;
}
$settings_badge_container_gap: $SMALL_PADDING !default;
$settings_badge_container_border: $DEFAULT_BORDER !default;
$settings_badge_container_border_invalid: 2px dashed red !default;
$settings_badge_container_border_radius: $DEFAULT_BORDER_RADIUS !default;
$settings_badge_container_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
$settings_badge_container_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
.settings-badge-container {
display: flex;
align-items: baseline;
gap: $settings_badge_container_gap;
border: $settings_badge_container_border;
border-radius: $settings_badge_container_border_radius;
padding: $settings_badge_container_padding;
margin: $settings_badge_container_margin;
// the file picker's validity is managed by js
// so we got lucky here. when the file picker
// is hidden, its set to be valid. it's only invalid
// when, well, invalid.
&:has(input:invalid) {
border: $settings_badge_container_border_invalid;
}
input:invalid {
border: $settings_badge_container_border_invalid;
}
}
.settings-badge-file-picker {
display: flex;
flex-direction: column;
align-items: center;
& input.hidden[type=file] {
width: 100%;
&::file-selector-button {
display: none;
}
}
&.hidden {
display: none;
}
}
$settings_badge_select_gap: $SMALL_PADDING !default;
$settings_badge_select_min_width: 200px !default;
.settings-badge-select {
display: flex;
flex-direction: column;
gap: $settings_badge_select_gap;
align-items: center;
min-width: $settings_badge_select_min_width;
}
img.badge-button {
min-width: 88px;
min-height: 31px;
max-width: 88px;
max-height: 31px;
}
$badges_container_gap: $SMALL_PADDING !default;
.badges-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: $badges_container_gap;
}

View File

@@ -59,8 +59,8 @@ $br: 8px;
$post_accordion_content_background: #2d212d,
$babycode_guide_toc_background: #3c233c,
$babycode_guide_section_background: $dark_accent,
$guide_toc_background: #3c233c,
$guide_section_background: $dark_accent,
$text_input_background: #371e37,
$text_input_background_focus: #514151,
@@ -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 {

View File

@@ -63,6 +63,8 @@ $br: 16px;
$pagebutton_min_width: 36px,
$quote_background_color: #0002,
$bookmark_dropdown_item_icon_padding: 0 24px,
);
#topnav {
@@ -71,9 +73,6 @@ $br: 16px;
}
#bottomnav {
border-bottom-left-radius: $br;
border-bottom-right-radius: $br;
color: white;
}
@@ -81,9 +80,10 @@ textarea {
padding: 12px 16px;
}
footer {
margin-top: 10px;
#footer {
border-radius: $br;
border-top-left-radius: 0;
border-top-right-radius: 0;
border: none;
text-align: center;
}

7
sass/snow-white.scss Normal file
View File

@@ -0,0 +1,7 @@
// a simple light theme
@use 'default' with (
$ACCENT_COLOR: #ced9ee,
$link_color: #711579,
$link_color_visited: #4a144f,
)