Compare commits
46 Commits
2d3eef6531
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
508b313871
|
|||
|
db677abaa5
|
|||
|
65abea2093
|
|||
|
1533f82a6b
|
|||
|
35483c27aa
|
|||
|
005d2f3b6c
|
|||
|
265e249eaf
|
|||
|
b812e01473
|
|||
|
88f80c38cc
|
|||
|
c70f13d069
|
|||
|
73af2dc3b9
|
|||
|
062cab44bc
|
|||
|
3baccb87b1
|
|||
|
3742749cf6
|
|||
|
eb76338c4a
|
|||
|
9951ed3fae
|
|||
|
a7876ca410
|
|||
|
7c037d1593
|
|||
|
24fe0aba30
|
|||
|
a185208fc1
|
|||
|
1d5d5a8c64
|
|||
|
414298b4b4
|
|||
|
c3a3ead852
|
|||
|
54907db896
|
|||
|
db2d09cb03
|
|||
|
5c03ba3d3a
|
|||
|
3a9f8a111b
|
|||
|
bbe57d6e94
|
|||
|
0bed6b58ae
|
|||
|
8164e63b09
|
|||
|
8b5b38e38b
|
|||
|
d0dfd3a4c3
|
|||
|
fca214dfcf
|
|||
|
04fd3f5d20
|
|||
|
1a3c015612
|
|||
|
fc9ae63471
|
|||
|
4d88b5c24c
|
|||
|
fefdbdb493
|
|||
|
d0c82cf9a9
|
|||
|
90fe38497d
|
|||
|
97e2c041c9
|
|||
|
bbbe152ff8
|
|||
|
a3ad36e9a9
|
|||
|
48fcadf61e
|
|||
|
62e1724f6c
|
|||
|
19383a538d
|
@@ -23,11 +23,17 @@ 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)
|
||||
URL: https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license
|
||||
Copyright: Gabriele Malaspina
|
||||
Designers: Gabriele Malaspina
|
||||
License: CC0 1.0
|
||||
|
||||
@@ -74,8 +80,8 @@ Repo: https://github.com/emcconville/wand
|
||||
|
||||
## Bitty
|
||||
|
||||
Affected files: [`data/static/js/vnd/bitty-5.1.0-rc6.min.js`](./data/static/js/vnd/bitty-5.1.0-rc6.min.js)
|
||||
Affected files: [`data/static/js/vnd/bitty-6.0.0-rc3.min.js`](./data/static/js/vnd/bitty-6.0.0-rc3.min.js)
|
||||
URL: https://bitty.alanwsmith.com/
|
||||
Copyright: `Copyright (c) 2025 Alan Smith - https://www.alanwsmith.com/`
|
||||
License: MIT
|
||||
Repo: https://github.com/alanwsmith/bitty
|
||||
License: CC0 1.0
|
||||
Author: alan w smith https://www.alanwsmith.com/
|
||||
Repo: https://github.com/alanwsmith/bitty
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from flask import Flask, session, request
|
||||
from flask import Flask, session, request, render_template
|
||||
from dotenv import load_dotenv
|
||||
from .models import Avatars, Users, PostHistory, Posts
|
||||
from .models import Avatars, Users, PostHistory, Posts, MOTD
|
||||
from .auth import digest
|
||||
from .routes.users import is_logged_in, get_active_user, get_prefers_theme
|
||||
from .routes.threads import get_post_url
|
||||
from .constants import (
|
||||
PermissionLevel, permission_level_string,
|
||||
InfoboxKind, InfoboxHTMLClass,
|
||||
REACTION_EMOJI,
|
||||
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
|
||||
@@ -40,10 +41,11 @@ def create_admin():
|
||||
|
||||
def create_deleted_user():
|
||||
username = "DeletedUser"
|
||||
if Users.count({"username": username}) == 0:
|
||||
if Users.count({"username": username.lower()}) == 0:
|
||||
print("Creating DeletedUser")
|
||||
Users.create({
|
||||
"username": username,
|
||||
"username": username.lower(),
|
||||
"display_name": username,
|
||||
"password_hash": "",
|
||||
"permission": PermissionLevel.SYSTEM.value,
|
||||
})
|
||||
@@ -51,6 +53,7 @@ def create_deleted_user():
|
||||
def reparse_babycode():
|
||||
print('Re-parsing babycode, this may take a while...')
|
||||
from .db import db
|
||||
from .constants import MOTD_BANNED_TAGS
|
||||
post_histories = PostHistory.findall([
|
||||
('markup_language', '=', 'babycode'),
|
||||
('format_version', 'IS NOT', BABYCODE_VERSION)
|
||||
@@ -60,7 +63,7 @@ def reparse_babycode():
|
||||
with db.transaction():
|
||||
for ph in post_histories:
|
||||
ph.update({
|
||||
'content': babycode_to_html(ph['original_markup']),
|
||||
'content': babycode_to_html(ph['original_markup']).result,
|
||||
'format_version': BABYCODE_VERSION,
|
||||
})
|
||||
print('Re-parsing posts done.')
|
||||
@@ -75,11 +78,25 @@ def reparse_babycode():
|
||||
with db.transaction():
|
||||
for user in users_with_sigs:
|
||||
user.update({
|
||||
'signature_rendered': babycode_to_html(user['signature_original_markup']),
|
||||
'signature_rendered': babycode_to_html(user['signature_original_markup']).result,
|
||||
'signature_format_version': BABYCODE_VERSION,
|
||||
})
|
||||
print(f'Re-parsed {len(users_with_sigs)} user sigs.')
|
||||
|
||||
stale_motds = MOTD.findall([
|
||||
['markup_language', '=', 'babycode'],
|
||||
['format_version', 'IS NOT', BABYCODE_VERSION]
|
||||
])
|
||||
if stale_motds:
|
||||
print('Re-parsing MOTDs...')
|
||||
with db.transaction():
|
||||
for motd in stale_motds:
|
||||
motd.update({
|
||||
'body_rendered': babycode_to_html(motd['body_original_markup'], banned_tags=MOTD_BANNED_TAGS).result,
|
||||
'format_version': BABYCODE_VERSION,
|
||||
})
|
||||
print('Re-parsing MOTDs done.')
|
||||
|
||||
print('Re-parsing done.')
|
||||
|
||||
def create_app():
|
||||
@@ -132,6 +149,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)
|
||||
@@ -140,6 +158,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
|
||||
|
||||
@@ -160,6 +179,8 @@ def create_app():
|
||||
"__commit": commit,
|
||||
"__emoji": EMOJI,
|
||||
"REACTION_EMOJI": REACTION_EMOJI,
|
||||
"MOTD_BANNED_TAGS": MOTD_BANNED_TAGS,
|
||||
"SIG_BANNED_TAGS": SIG_BANNED_TAGS,
|
||||
}
|
||||
|
||||
@app.context_processor
|
||||
@@ -171,6 +192,7 @@ def create_app():
|
||||
return {
|
||||
'get_post_url': get_post_url,
|
||||
'get_prefers_theme': get_prefers_theme,
|
||||
'get_motds': MOTD.get_all,
|
||||
}
|
||||
|
||||
@app.template_filter("ts_datetime")
|
||||
@@ -190,7 +212,11 @@ def create_app():
|
||||
|
||||
@app.template_filter('babycode')
|
||||
def babycode_filter(markup):
|
||||
return babycode_to_html(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):
|
||||
@@ -209,7 +235,7 @@ def create_app():
|
||||
elif request.path.startswith('/api/'):
|
||||
return {'error': 'not found'}, e.code
|
||||
else:
|
||||
return e
|
||||
return render_template('common/404.html'), e.code
|
||||
|
||||
# this only happens at build time but
|
||||
# build time is when updates are done anyway
|
||||
|
||||
@@ -47,6 +47,19 @@ REACTION_EMOJI = [
|
||||
'scissors',
|
||||
]
|
||||
|
||||
MOTD_BANNED_TAGS = [
|
||||
'img', 'spoiler', '@mention',
|
||||
]
|
||||
|
||||
SIG_BANNED_TAGS = [
|
||||
'@mention',
|
||||
]
|
||||
|
||||
STRICT_BANNED_TAGS = [
|
||||
'img', 'spoiler', '@mention',
|
||||
'big', 'small', 'center', 'right', 'color',
|
||||
]
|
||||
|
||||
def permission_level_string(perm):
|
||||
return PermissionLevelString[PermissionLevel(int(perm))]
|
||||
|
||||
|
||||
26
app/db.py
26
app/db.py
@@ -28,7 +28,7 @@ class DB:
|
||||
|
||||
if in_transaction:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
if in_transaction and self._connection:
|
||||
conn.rollback()
|
||||
finally:
|
||||
@@ -189,6 +189,13 @@ class Model:
|
||||
raise AttributeError(f"No column '{key}'")
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
instance = cls(cls.table)
|
||||
instance._data = dict(data)
|
||||
return instance
|
||||
|
||||
|
||||
@classmethod
|
||||
def find(cls, condition):
|
||||
row = db.QueryBuilder(cls.table)\
|
||||
@@ -196,9 +203,7 @@ class Model:
|
||||
.first()
|
||||
if not row:
|
||||
return None
|
||||
instance = cls(cls.table)
|
||||
instance._data = dict(row)
|
||||
return instance
|
||||
return cls.from_data(row)
|
||||
|
||||
|
||||
@classmethod
|
||||
@@ -207,11 +212,7 @@ class Model:
|
||||
.where(condition, operator)\
|
||||
.all()
|
||||
res = []
|
||||
for row in rows:
|
||||
instance = cls(cls.table)
|
||||
instance._data = dict(row)
|
||||
res.append(instance)
|
||||
return res
|
||||
return [cls.from_data(row) for row in rows]
|
||||
|
||||
|
||||
@classmethod
|
||||
@@ -223,9 +224,7 @@ class Model:
|
||||
row = db.insert(cls.table, columns, *values.values())
|
||||
|
||||
if row:
|
||||
instance = cls(cls.table)
|
||||
instance._data = row
|
||||
return instance
|
||||
return cls.from_data(row)
|
||||
return None
|
||||
|
||||
|
||||
@@ -243,7 +242,8 @@ class Model:
|
||||
def select(cls, sel = "*"):
|
||||
qb = db.QueryBuilder(cls.table).select(sel)
|
||||
result = qb.all()
|
||||
return result if result else []
|
||||
# return result if result else []
|
||||
return [cls.from_data(data) for data in (result if result else [])]
|
||||
|
||||
|
||||
def update(self, data):
|
||||
|
||||
@@ -6,7 +6,17 @@ from pygments.lexers import get_lexer_by_name
|
||||
from pygments.util import ClassNotFound as PygmentsClassNotFound
|
||||
import re
|
||||
|
||||
BABYCODE_VERSION = 4
|
||||
class BabycodeParseResult:
|
||||
def __init__(self, result, mentions=[]):
|
||||
self.result = result
|
||||
self.mentions = mentions
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.result
|
||||
|
||||
|
||||
BABYCODE_VERSION = 5
|
||||
|
||||
NAMED_COLORS = [
|
||||
'black', 'silver', 'gray', 'white', 'maroon', 'red',
|
||||
@@ -61,7 +71,7 @@ def tag_code(children, attr, surrounding):
|
||||
return f"<code class=\"inline-code\">{children}</code>"
|
||||
else:
|
||||
input_code = children.strip()
|
||||
button = f"<button type=button class=\"copy-code\" value=\"{input_code}\">Copy</button>"
|
||||
button = f"<button type=button class=\"copy-code\" value=\"{input_code}\" data-send=\"copyCode\" data-receive=\"copyCode\">Copy</button>"
|
||||
unhighlighted = f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">code block</span>{button}</span><code>{input_code}</code></pre>"
|
||||
if not attr:
|
||||
return unhighlighted
|
||||
@@ -78,6 +88,9 @@ def tag_list(children):
|
||||
return " ".join([f"<li>{x}</li>" for x in list_body.split("\1") if x])
|
||||
|
||||
def tag_color(children, attr, surrounding):
|
||||
if not attr:
|
||||
return f"[color]{children}[/color]"
|
||||
|
||||
hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
|
||||
potential_color = attr.lower().strip()
|
||||
|
||||
@@ -94,7 +107,7 @@ def tag_color(children, attr, surrounding):
|
||||
def tag_spoiler(children, attr, surrounding):
|
||||
spoiler_name = attr if attr else "Spoiler"
|
||||
content = f"<div class='accordion-content post-accordion-content hidden'>{children}</div>"
|
||||
container = f"""<div class='accordion hidden'><div class='accordion-header'><button type='button' class='accordion-toggle'>+</button><span>{spoiler_name}</span></div>{content}</div>"""
|
||||
container = f"""<div class='accordion hidden' data-receive='toggleAccordion'><div class='accordion-header'><button type='button' class='accordion-toggle' data-send='toggleAccordion'>+</button><span>{spoiler_name}</span></div>{content}</div>"""
|
||||
return container
|
||||
|
||||
def tag_image(children, attr, surrounding):
|
||||
@@ -128,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().
|
||||
@@ -206,6 +225,24 @@ def is_inline(e):
|
||||
|
||||
return e['type'] != 'rule'
|
||||
|
||||
def make_mention(e, mentions):
|
||||
from ..models import Users
|
||||
from flask import url_for
|
||||
target_user = Users.find({'username': e['name'].lower()})
|
||||
if not target_user:
|
||||
return f"@{e['name']}"
|
||||
|
||||
mention_data = {
|
||||
'mention_text': f"@{e['name']}",
|
||||
'mentioned_user_id': int(target_user.id),
|
||||
"start": e['start'],
|
||||
"end": e['end'],
|
||||
}
|
||||
if mention_data not in mentions:
|
||||
mentions.append(mention_data)
|
||||
|
||||
return f"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.page', username=target_user.username)}' title='@{target_user.username}' data-init='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
|
||||
|
||||
def should_collapse(text, surrounding):
|
||||
if not isinstance(text, str):
|
||||
return False
|
||||
@@ -218,11 +255,20 @@ def should_collapse(text, surrounding):
|
||||
|
||||
return False
|
||||
|
||||
def babycode_to_html(s):
|
||||
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
||||
def sanitize(s):
|
||||
return escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
||||
|
||||
def babycode_to_html(s, banned_tags=[]):
|
||||
allowed_tags = set(TAGS.keys())
|
||||
if banned_tags is not None:
|
||||
for tag in banned_tags:
|
||||
allowed_tags.discard(tag)
|
||||
subj = sanitize(s)
|
||||
parser = Parser(subj)
|
||||
parser.valid_bbcode_tags = TAGS.keys()
|
||||
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()
|
||||
|
||||
uncollapsed = parser.parse()
|
||||
@@ -237,6 +283,7 @@ def babycode_to_html(s):
|
||||
elements.append(e)
|
||||
|
||||
out = ""
|
||||
mentions = []
|
||||
def fold(element, nobr, surrounding):
|
||||
if isinstance(element, str):
|
||||
if nobr:
|
||||
@@ -256,12 +303,16 @@ def babycode_to_html(s):
|
||||
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':
|
||||
return EMOJI[element['name']]
|
||||
case "rule":
|
||||
return "<hr>"
|
||||
case "mention":
|
||||
return make_mention(element, mentions)
|
||||
|
||||
for i in range(len(elements)):
|
||||
e = elements[i]
|
||||
@@ -270,4 +321,4 @@ def babycode_to_html(s):
|
||||
elements[i + 1] if i+1 < len(elements) else None
|
||||
)
|
||||
out = out + fold(e, False, surrounding)
|
||||
return out
|
||||
return BabycodeParseResult(out, mentions)
|
||||
|
||||
@@ -6,12 +6,15 @@ PAT_EMOTE = r"[^\s:]"
|
||||
PAT_BBCODE_TAG = r"\w"
|
||||
PAT_BBCODE_ATTR = r"[^\]]"
|
||||
PAT_LINK = r"https?:\/\/[\w\-_.?:\/=&~@#%]+[\w\-\/]"
|
||||
PAT_MENTION = r'[a-zA-Z0-9_-]'
|
||||
|
||||
class Parser:
|
||||
def __init__(self, src_str):
|
||||
self.valid_bbcode_tags = []
|
||||
self.valid_bbcode_tags = {}
|
||||
self.void_bbcode_tags = {}
|
||||
self.valid_emotes = []
|
||||
self.bbcode_tags_only_text_children = [],
|
||||
self.bbcode_tags_only_text_children = []
|
||||
self.mentions_allowed = True
|
||||
self.source = src_str
|
||||
self.position = 0
|
||||
self.position_stack = []
|
||||
@@ -206,15 +209,70 @@ class Parser:
|
||||
"url": word
|
||||
}
|
||||
|
||||
def parse_mention(self):
|
||||
if not self.mentions_allowed:
|
||||
return None
|
||||
|
||||
self.save_position()
|
||||
|
||||
if not self.check_char('@'):
|
||||
self.restore_position()
|
||||
return None
|
||||
|
||||
mention = self.match_pattern(PAT_MENTION)
|
||||
self.forget_position()
|
||||
return {
|
||||
"type": "mention",
|
||||
"name": mention,
|
||||
"start": self.position - len(mention) - 1,
|
||||
"end": self.position,
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
or self.parse_link() \
|
||||
or self.parse_mention()
|
||||
|
||||
if element is None:
|
||||
if len(siblings) > 0:
|
||||
|
||||
@@ -22,6 +22,18 @@ def create_default_bookmark_collections():
|
||||
for user in user_ids_without_default_collection:
|
||||
BookmarkCollections.create_default(user['id'])
|
||||
|
||||
def add_display_name():
|
||||
dq = 'ALTER TABLE "users" ADD COLUMN "display_name" TEXT NOT NULL DEFAULT ""'
|
||||
db.execute(dq)
|
||||
from .models import Users
|
||||
for user in Users.select():
|
||||
data = {
|
||||
'username': user.username.lower(),
|
||||
}
|
||||
if user.username.lower() != user.username:
|
||||
data['display_name'] = user.username
|
||||
user.update(data)
|
||||
|
||||
# format: [str|tuple(str, any...)|callable]
|
||||
MIGRATIONS = [
|
||||
migrate_old_avatars,
|
||||
@@ -30,6 +42,7 @@ MIGRATIONS = [
|
||||
'ALTER TABLE "post_history" ADD COLUMN "format_version" INTEGER DEFAULT NULL',
|
||||
add_signature_format,
|
||||
create_default_bookmark_collections,
|
||||
add_display_name,
|
||||
]
|
||||
|
||||
def run_migrations():
|
||||
|
||||
@@ -53,7 +53,8 @@ class Users(Model):
|
||||
COUNT(DISTINCT threads.id) AS thread_count,
|
||||
MAX(threads.title) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_title,
|
||||
MAX(threads.slug) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_slug,
|
||||
inviter.username AS inviter_username
|
||||
inviter.username AS inviter_username,
|
||||
inviter.display_name AS inviter_display_name
|
||||
FROM users
|
||||
LEFT JOIN posts ON posts.user_id = users.id
|
||||
LEFT JOIN threads ON threads.user_id = users.id
|
||||
@@ -106,6 +107,15 @@ class Users(Model):
|
||||
res = db.query(q, self.id)
|
||||
return [BookmarkCollections.find({'id': bc['id']}) for bc in res]
|
||||
|
||||
def get_readable_name(self):
|
||||
if self.display_name:
|
||||
return self.display_name
|
||||
|
||||
return self.username
|
||||
|
||||
def has_display_name(self):
|
||||
return self.display_name != ''
|
||||
|
||||
|
||||
class Topics(Model):
|
||||
table = "topics"
|
||||
@@ -116,6 +126,7 @@ class Topics(Model):
|
||||
SELECT
|
||||
topics.id, topics.name, topics.slug, topics.description, topics.is_locked,
|
||||
users.username AS latest_thread_username,
|
||||
users.display_name AS latest_thread_display_name,
|
||||
threads.title AS latest_thread_title,
|
||||
threads.slug AS latest_thread_slug,
|
||||
threads.created_at AS latest_thread_created_at
|
||||
@@ -141,7 +152,7 @@ class Topics(Model):
|
||||
SELECT
|
||||
threads.topic_id, threads.id AS thread_id, threads.title AS thread_title, threads.slug AS thread_slug,
|
||||
posts.id AS post_id, posts.created_at AS post_created_at,
|
||||
users.username,
|
||||
users.username, users.display_name,
|
||||
ROW_NUMBER() OVER (PARTITION BY threads.topic_id ORDER BY posts.created_at DESC) AS rn
|
||||
FROM
|
||||
threads
|
||||
@@ -154,7 +165,7 @@ class Topics(Model):
|
||||
topic_id,
|
||||
thread_id, thread_title, thread_slug,
|
||||
post_id, post_created_at,
|
||||
username
|
||||
username, display_name
|
||||
FROM
|
||||
ranked_threads
|
||||
WHERE
|
||||
@@ -170,6 +181,7 @@ class Topics(Model):
|
||||
'thread_slug': thread['thread_slug'],
|
||||
'post_id': thread['post_id'],
|
||||
'username': thread['username'],
|
||||
'display_name': thread['display_name'],
|
||||
'post_created_at': thread['post_created_at']
|
||||
}
|
||||
return active_threads
|
||||
@@ -185,7 +197,9 @@ class Topics(Model):
|
||||
SELECT
|
||||
threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
||||
users.username AS started_by,
|
||||
u.username AS latest_post_username,
|
||||
users.display_name AS started_by_display_name,
|
||||
u.username AS latest_post_username,
|
||||
u.display_name AS latest_post_display_name,
|
||||
ph.content AS latest_post_content,
|
||||
posts.created_at AS latest_post_created_at,
|
||||
posts.id AS latest_post_id
|
||||
@@ -230,7 +244,13 @@ class Threads(Model):
|
||||
class Posts(Model):
|
||||
FULL_POSTS_QUERY = """
|
||||
SELECT
|
||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, 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
|
||||
posts.id, posts.created_at,
|
||||
post_history.content, post_history.edited_at,
|
||||
users.username, users.display_name, users.status,
|
||||
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
|
||||
FROM
|
||||
posts
|
||||
JOIN
|
||||
@@ -394,3 +414,23 @@ class BookmarkedThreads(Model):
|
||||
|
||||
def get_thread(self):
|
||||
return Threads.find({'id': self.thread_id})
|
||||
|
||||
|
||||
class MOTD(Model):
|
||||
table = 'motd'
|
||||
|
||||
@classmethod
|
||||
def has_motd(cls):
|
||||
q = 'SELECT EXISTS(SELECT 1 FROM motd) as e'
|
||||
res = db.fetch_one(q)['e']
|
||||
return int(res) == 1
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
q = 'SELECT id FROM motd'
|
||||
res = db.query(q)
|
||||
return [MOTD.find({'id': i['id']}) for i in res]
|
||||
|
||||
|
||||
class Mentions(Model):
|
||||
table = 'mentions'
|
||||
|
||||
@@ -40,7 +40,8 @@ def babycode_preview():
|
||||
markup = request.json.get('markup')
|
||||
if not markup or not isinstance(markup, str):
|
||||
return {'error': 'markup field missing or invalid type'}, 400
|
||||
rendered = babycode_to_html(markup)
|
||||
banned_tags = request.json.get('banned_tags', [])
|
||||
rendered = babycode_to_html(markup, banned_tags).result
|
||||
return {'html': rendered}
|
||||
|
||||
|
||||
@@ -212,3 +213,16 @@ def bookmark_thread(thread_id):
|
||||
return {'error': 'bad request'}, 400
|
||||
|
||||
return {'status': 'ok'}, 200
|
||||
|
||||
@bp.get('/current-user')
|
||||
def get_current_user_info():
|
||||
if not is_logged_in():
|
||||
return {'user': None}
|
||||
|
||||
user = get_active_user()
|
||||
return {
|
||||
'user': {
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
111
app/routes/guides.py
Normal 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')
|
||||
@@ -1,25 +1,33 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for
|
||||
Blueprint, render_template, request, redirect, url_for,
|
||||
flash
|
||||
)
|
||||
from .users import login_required, mod_only, get_active_user, admin_only
|
||||
from ..models import Users, PasswordResetLinks
|
||||
from ..db import db, DB
|
||||
from .users import get_active_user, is_logged_in
|
||||
from ..models import Users, PasswordResetLinks, MOTD
|
||||
from ..constants import InfoboxKind, MOTD_BANNED_TAGS
|
||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||
from ..db import db
|
||||
import secrets
|
||||
import time
|
||||
|
||||
bp = Blueprint("mod", __name__, url_prefix = "/mod/")
|
||||
|
||||
@bp.before_request
|
||||
def _before_request():
|
||||
if not is_logged_in():
|
||||
return redirect(url_for("users.log_in"))
|
||||
|
||||
if not get_active_user().is_mod():
|
||||
return redirect(url_for("topics.all_topics"))
|
||||
|
||||
|
||||
@bp.get("/sort-topics")
|
||||
@login_required
|
||||
@mod_only("topics.all_topics")
|
||||
def sort_topics():
|
||||
topics = db.query("SELECT * FROM topics ORDER BY sort_order ASC")
|
||||
return render_template("mod/sort-topics.html", topics = topics)
|
||||
|
||||
|
||||
@bp.post("/sort-topics")
|
||||
@login_required
|
||||
@mod_only("topics.all_topics")
|
||||
def sort_topics_post():
|
||||
with db.transaction():
|
||||
for topic_id, new_order in request.form.items():
|
||||
@@ -29,16 +37,12 @@ def sort_topics_post():
|
||||
|
||||
|
||||
@bp.get("/user-list")
|
||||
@login_required
|
||||
@mod_only("users.page", username = lambda: get_active_user().username)
|
||||
def user_list():
|
||||
users = Users.select()
|
||||
return render_template("mod/user-list.html", users = users)
|
||||
|
||||
|
||||
@bp.post("/reset-pass/<user_id>")
|
||||
@login_required
|
||||
@mod_only("topics.all_topics")
|
||||
def create_reset_pass(user_id):
|
||||
now = int(time.time())
|
||||
key = secrets.token_urlsafe(20)
|
||||
@@ -49,3 +53,52 @@ def create_reset_pass(user_id):
|
||||
})
|
||||
|
||||
return redirect(url_for('users.reset_link_login', key=key))
|
||||
|
||||
|
||||
@bp.get('/panel')
|
||||
def panel():
|
||||
return render_template('mod/panel.html')
|
||||
|
||||
|
||||
@bp.get('/motd')
|
||||
def motd_editor():
|
||||
current = MOTD.get_all()[0] if MOTD.has_motd() else None
|
||||
return render_template('mod/motd.html', current=current)
|
||||
|
||||
|
||||
@bp.post('/motd')
|
||||
def motd_editor_form():
|
||||
orig_body = request.form.get('body', default='')
|
||||
title = request.form.get('title', default='')
|
||||
data = {
|
||||
'title': title,
|
||||
'body_original_markup': orig_body,
|
||||
'body_rendered': babycode_to_html(orig_body, banned_tags=MOTD_BANNED_TAGS).result,
|
||||
'format_version': BABYCODE_VERSION,
|
||||
'edited_at': int(time.time()),
|
||||
}
|
||||
|
||||
if MOTD.has_motd():
|
||||
motd = MOTD.get_all()[0]
|
||||
motd.update(data)
|
||||
message = 'MOTD updated.'
|
||||
else:
|
||||
data['created_at'] = int(time.time())
|
||||
data['user_id'] = get_active_user().id
|
||||
motd = MOTD.create(data)
|
||||
message = 'MOTD created.'
|
||||
|
||||
flash(message, InfoboxKind.INFO)
|
||||
return redirect(url_for('.motd_editor'))
|
||||
|
||||
|
||||
@bp.post('/motd/delete')
|
||||
def motd_delete():
|
||||
if not MOTD.has_motd():
|
||||
flash('No MOTD to delete.', InfoboxKind.WARN)
|
||||
return redirect(url_for('.motd_editor'))
|
||||
|
||||
current = MOTD.get_all()[0]
|
||||
current.delete()
|
||||
flash('MOTD deleted.', InfoboxKind.INFO)
|
||||
return redirect(url_for('.motd_editor'))
|
||||
|
||||
@@ -5,7 +5,7 @@ from .users import login_required, get_active_user
|
||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||
from ..constants import InfoboxKind
|
||||
from ..db import db
|
||||
from ..models import Posts, PostHistory, Threads, Topics
|
||||
from ..models import Posts, PostHistory, Threads, Topics, Mentions
|
||||
|
||||
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
||||
|
||||
@@ -21,13 +21,22 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
|
||||
|
||||
revision = PostHistory.create({
|
||||
"post_id": post.id,
|
||||
"content": parsed_content,
|
||||
"content": parsed_content.result,
|
||||
"is_initial_revision": True,
|
||||
"original_markup": content,
|
||||
"markup_language": markup_language,
|
||||
"format_version": BABYCODE_VERSION,
|
||||
})
|
||||
|
||||
for mention in parsed_content.mentions:
|
||||
Mentions.create({
|
||||
'revision_id': revision.id,
|
||||
'mentioned_user_id': mention['mentioned_user_id'],
|
||||
'original_mention_text': mention['mention_text'],
|
||||
'start_index': mention['start'],
|
||||
'end_index': mention['end'],
|
||||
})
|
||||
|
||||
post.update({"current_revision_id": revision.id})
|
||||
return post
|
||||
|
||||
@@ -38,13 +47,22 @@ def update_post(post_id, new_content, markup_language='babycode'):
|
||||
post = Posts.find({'id': post_id})
|
||||
new_revision = PostHistory.create({
|
||||
'post_id': post.id,
|
||||
'content': parsed_content,
|
||||
'content': parsed_content.result,
|
||||
'is_initial_revision': False,
|
||||
'original_markup': new_content,
|
||||
'markup_language': markup_language,
|
||||
'format_version': BABYCODE_VERSION,
|
||||
})
|
||||
|
||||
for mention in parsed_content.mentions:
|
||||
Mentions.create({
|
||||
'revision_id': new_revision.id,
|
||||
'mentioned_user_id': mention['mentioned_user_id'],
|
||||
'original_mention_text': mention['mention_text'],
|
||||
'start_index': mention['start'],
|
||||
'end_index': mention['end'],
|
||||
})
|
||||
|
||||
post.update({'current_revision_id': new_revision.id})
|
||||
|
||||
|
||||
@@ -53,7 +71,8 @@ def update_post(post_id, new_content, markup_language='babycode'):
|
||||
def delete(post_id):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
user = get_active_user()
|
||||
@@ -85,13 +104,15 @@ def delete(post_id):
|
||||
def edit(post_id):
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
|
||||
user = get_active_user()
|
||||
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
||||
editing_post = db.fetch_one(q, post_id)
|
||||
if not editing_post:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
if editing_post['user_id'] != user.id:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
@@ -118,7 +139,8 @@ def edit_form(post_id):
|
||||
user = get_active_user()
|
||||
post = Posts.find({'id': post_id})
|
||||
if not post:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
if post.user_id != user.id:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for, flash
|
||||
Blueprint, render_template, request, redirect, url_for, flash,
|
||||
abort,
|
||||
)
|
||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||
from ..db import db
|
||||
@@ -32,7 +33,8 @@ def thread(slug):
|
||||
POSTS_PER_PAGE = 10
|
||||
thread = Threads.find({"slug": slug})
|
||||
if not thread:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
|
||||
post_count = Posts.count({"thread_id": thread.id})
|
||||
page_count = max(math.ceil(post_count / POSTS_PER_PAGE), 1)
|
||||
@@ -48,7 +50,6 @@ def thread(slug):
|
||||
page = math.ceil((post_position) / POSTS_PER_PAGE)
|
||||
else:
|
||||
page = max(1, min(page_count, int(request.args.get("page", default = 1))))
|
||||
|
||||
posts = thread.get_posts(POSTS_PER_PAGE, (page - 1) * POSTS_PER_PAGE)
|
||||
topic = Topics.find({"id": thread.topic_id})
|
||||
other_topics = Topics.select()
|
||||
@@ -87,7 +88,8 @@ def thread(slug):
|
||||
def reply(slug):
|
||||
thread = Threads.find({"slug": slug})
|
||||
if not thread:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
user = get_active_user()
|
||||
if user.is_guest():
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
@@ -149,7 +151,8 @@ def lock(slug):
|
||||
user = get_active_user()
|
||||
thread = Threads.find({'slug': slug})
|
||||
if not thread:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
if not ((thread.user_id == user.id) or user.is_mod()):
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
target_op = request.form.get('target_op')
|
||||
@@ -166,7 +169,8 @@ def sticky(slug):
|
||||
user = get_active_user()
|
||||
thread = Threads.find({'slug': slug})
|
||||
if not thread:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
if not ((thread.user_id == user.id) or user.is_mod()):
|
||||
return redirect(url_for('.thread', slug=slug))
|
||||
target_op = request.form.get('target_op')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
Blueprint, render_template, request, redirect, url_for, flash, session,
|
||||
abort,
|
||||
)
|
||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||
from ..models import Users, Topics, Threads, Subscriptions
|
||||
@@ -13,7 +14,6 @@ bp = Blueprint("topics", __name__, url_prefix = "/topics/")
|
||||
|
||||
@bp.get("/")
|
||||
def all_topics():
|
||||
admin = Users.find({"id": 1})
|
||||
return render_template("topics/topics.html", topic_list = Topics.get_list(), active_threads = Topics.get_active_threads())
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ def topic(slug):
|
||||
"slug": slug
|
||||
})
|
||||
if not target_topic:
|
||||
return redirect(url_for('.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
|
||||
threads_count = Threads.count({
|
||||
"topic_id": target_topic.id
|
||||
@@ -89,7 +90,8 @@ def topic(slug):
|
||||
def edit(slug):
|
||||
topic = Topics.find({"slug": slug})
|
||||
if not topic:
|
||||
return redirect(url_for('.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
return render_template("topics/edit.html", topic=topic)
|
||||
|
||||
|
||||
@@ -99,7 +101,8 @@ def edit(slug):
|
||||
def edit_post(slug):
|
||||
topic = Topics.find({"slug": slug})
|
||||
if not topic:
|
||||
return redirect(url_for('.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
|
||||
topic.update({
|
||||
"name": request.form.get('name', default = topic.name).strip(),
|
||||
@@ -116,7 +119,8 @@ def edit_post(slug):
|
||||
def delete(slug):
|
||||
topic = Topics.find({"slug": slug})
|
||||
if not topic:
|
||||
return redirect(url_for('.all_topics'))
|
||||
abort(404)
|
||||
return
|
||||
|
||||
topic.delete()
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from flask import (
|
||||
Blueprint, render_template, request, redirect, url_for, flash, session, current_app
|
||||
Blueprint, render_template, request, redirect, url_for, flash, session, current_app, abort
|
||||
)
|
||||
from functools import wraps
|
||||
from ..db import db
|
||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||
from ..models import Users, Sessions, Subscriptions, Avatars, PasswordResetLinks, InviteKeys, BookmarkCollections, BookmarkedThreads
|
||||
from ..constants import InfoboxKind, PermissionLevel
|
||||
from ..models import (
|
||||
Users, Sessions, Subscriptions,
|
||||
Avatars, PasswordResetLinks, InviteKeys,
|
||||
BookmarkCollections, BookmarkedThreads,
|
||||
Mentions, PostHistory,
|
||||
)
|
||||
from ..constants import InfoboxKind, PermissionLevel, SIG_BANNED_TAGS
|
||||
from ..auth import digest, verify
|
||||
from wand.image import Image
|
||||
from wand.exceptions import WandException
|
||||
@@ -62,11 +67,15 @@ 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):
|
||||
session_obj = Sessions.find({'key': session['pyrom_session_key']})
|
||||
@@ -90,6 +99,15 @@ def validate_username(username):
|
||||
return bool(re.fullmatch(pattern, username))
|
||||
|
||||
|
||||
def validate_display_name(display_name):
|
||||
if not display_name:
|
||||
return True
|
||||
|
||||
pattern = r'^[\w!#$%^*\(\)\-_=+\[\]\{\}\|;:,.?\s]{3,50}$'
|
||||
display_name = display_name.replace('@', '_')
|
||||
return bool(re.fullmatch(pattern, display_name))
|
||||
|
||||
|
||||
def redirect_if_logged_in(*args, **kwargs):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
@@ -112,6 +130,19 @@ def redirect_if_logged_in(*args, **kwargs):
|
||||
return decorator
|
||||
|
||||
|
||||
def redirect_to_own(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(username, *args, **kwargs):
|
||||
user = get_active_user()
|
||||
if username.lower() != user.username:
|
||||
view_args = dict(request.view_args)
|
||||
view_args.pop('username', None)
|
||||
new_args = {**view_args, 'username': user.username}
|
||||
return redirect(url_for(request.endpoint, **new_args))
|
||||
return view_func(username, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def login_required(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
@@ -174,6 +205,53 @@ def get_prefers_theme():
|
||||
|
||||
return session['theme']
|
||||
|
||||
|
||||
def anonymize_user(user_id):
|
||||
deleted_user = Users.find({'username': 'deleteduser'})
|
||||
|
||||
from ..models import Threads, Posts
|
||||
from ..lib.babycode import sanitize
|
||||
threads = Threads.findall({'user_id': user_id})
|
||||
posts = Posts.findall({'user_id': user_id})
|
||||
|
||||
revs_q = """SELECT DISTINCT m.revision_id FROM mentions m
|
||||
WHERE m.mentioned_user_id = ?"""
|
||||
|
||||
mentioned_revs = db.query(revs_q, int(user_id))
|
||||
with db.transaction():
|
||||
for thread in threads:
|
||||
thread.update({'user_id': int(deleted_user.id)})
|
||||
for post in posts:
|
||||
post.update({'user_id': int(deleted_user.id)})
|
||||
|
||||
revs = {}
|
||||
for rev in mentioned_revs:
|
||||
ph = PostHistory.find({'id': int(rev['revision_id'])})
|
||||
ms = Mentions.findall({
|
||||
'mentioned_user_id': int(user_id),
|
||||
'revision_id': int(rev['revision_id'])
|
||||
})
|
||||
data = {
|
||||
'text': sanitize(ph.original_markup),
|
||||
'mentions': ms,
|
||||
}
|
||||
data['mentions'] = sorted(data['mentions'], key=lambda x: int(x.end_index), reverse=True)
|
||||
revs[rev['revision_id']] = data
|
||||
|
||||
for rev_id, data in revs.items():
|
||||
text = data['text']
|
||||
for mention in data['mentions']:
|
||||
text = text[:mention.start_index] + '@deleteduser' + text[mention.end_index:]
|
||||
mention.delete()
|
||||
|
||||
res = babycode_to_html(text)
|
||||
ph = PostHistory.find({'id': int(rev_id)})
|
||||
ph.update({
|
||||
'original_markup': text.unescape(),
|
||||
'content': res.result,
|
||||
})
|
||||
|
||||
|
||||
@bp.get("/log_in")
|
||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||
def log_in():
|
||||
@@ -184,7 +262,7 @@ def log_in():
|
||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||
def log_in_post():
|
||||
target_user = Users.find({
|
||||
"username": request.form['username']
|
||||
"username": request.form['username'].lower()
|
||||
})
|
||||
if not target_user:
|
||||
flash("Incorrect username or password.", InfoboxKind.ERROR)
|
||||
@@ -239,7 +317,7 @@ def sign_up_post():
|
||||
flash("Invalid username.", InfoboxKind.ERROR)
|
||||
return redirect(url_for("users.sign_up", key=key))
|
||||
|
||||
user_exists = Users.count({"username": username}) > 0
|
||||
user_exists = Users.count({"username": username.lower()}) > 0
|
||||
if user_exists:
|
||||
flash(f"Username '{username}' is already taken.", InfoboxKind.ERROR)
|
||||
return redirect(url_for("users.sign_up", key=key))
|
||||
@@ -254,47 +332,54 @@ def sign_up_post():
|
||||
|
||||
hashed = digest(password)
|
||||
|
||||
new_user = Users.create({
|
||||
"username": username,
|
||||
"password_hash": hashed,
|
||||
"permission": PermissionLevel.GUEST.value,
|
||||
})
|
||||
if username.lower() != username:
|
||||
display_name = username
|
||||
else:
|
||||
display_name = ''
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
@bp.get("/<username>")
|
||||
def page(username):
|
||||
target_user = Users.find({"username": username})
|
||||
target_user = Users.find({"username": username.lower()})
|
||||
if not target_user:
|
||||
abort(404)
|
||||
return render_template("users/user.html", target_user = target_user)
|
||||
|
||||
|
||||
@bp.get("/<username>/settings")
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def settings(username):
|
||||
target_user = Users.find({'username': username})
|
||||
if target_user.id != get_active_user().id:
|
||||
return redirect('.settings', username = get_active_user().username)
|
||||
|
||||
return render_template('users/settings.html')
|
||||
|
||||
|
||||
@bp.post('/<username>/settings')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def settings_form(username):
|
||||
# we silently ignore the passed username
|
||||
# and grab the correct user from the session
|
||||
@@ -311,10 +396,16 @@ 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)
|
||||
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='')
|
||||
if not validate_display_name(display_name):
|
||||
flash('Invalid display name.', InfoboxKind.ERROR)
|
||||
return redirect('.settings', username=user.username)
|
||||
|
||||
old_dn = user.display_name
|
||||
|
||||
user.update({
|
||||
'status': status,
|
||||
@@ -322,13 +413,29 @@ def settings_form(username):
|
||||
'signature_rendered': rendered_sig,
|
||||
'signature_format_version': BABYCODE_VERSION,
|
||||
'signature_markup_language': 'babycode',
|
||||
'display_name': display_name,
|
||||
})
|
||||
|
||||
if old_dn != display_name:
|
||||
# re-parse mentions
|
||||
q = """SELECT DISTINCT m.revision_id FROM mentions m
|
||||
JOIN post_history ph ON m.revision_id = ph.id
|
||||
JOIN posts p ON p.current_revision_id = ph.id
|
||||
WHERE m.mentioned_user_id = ?"""
|
||||
mentions = db.query(q, int(user.id))
|
||||
with db.transaction():
|
||||
for mention in mentions:
|
||||
rev = PostHistory.find({'id': int(mention['revision_id'])})
|
||||
parsed_content = babycode_to_html(rev.original_markup).result
|
||||
rev.update({'content': parsed_content})
|
||||
|
||||
flash('Settings updated.', InfoboxKind.INFO)
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
|
||||
|
||||
@bp.post('/<username>/set_avatar')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def set_avatar(username):
|
||||
user = get_active_user()
|
||||
if user.is_guest():
|
||||
@@ -372,6 +479,7 @@ def set_avatar(username):
|
||||
|
||||
@bp.post('/<username>/change_password')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def change_password(username):
|
||||
user = get_active_user()
|
||||
password = request.form.get('new_password')
|
||||
@@ -394,6 +502,7 @@ def change_password(username):
|
||||
|
||||
@bp.post('/<username>/clear_avatar')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def clear_avatar(username):
|
||||
user = get_active_user()
|
||||
if user.is_default_avatar():
|
||||
@@ -486,11 +595,9 @@ def guest_user(user_id):
|
||||
|
||||
@bp.get("/<username>/inbox")
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def inbox(username):
|
||||
user = get_active_user()
|
||||
if username != user.username:
|
||||
return redirect(url_for(".inbox", username = user.username))
|
||||
|
||||
new_posts = []
|
||||
subscription = Subscriptions.find({"user_id": user.id})
|
||||
all_subscriptions = None
|
||||
@@ -628,16 +735,14 @@ def reset_link_login_form(key):
|
||||
|
||||
@bp.get('/<username>/invite-links/')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def invite_links(username):
|
||||
target_user = Users.find({
|
||||
'username': username
|
||||
'username': username.lower()
|
||||
})
|
||||
if not target_user or not target_user.can_invite():
|
||||
return redirect(url_for('.page', username=username))
|
||||
|
||||
if target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
invites = InviteKeys.findall({
|
||||
'created_by': target_user.id
|
||||
})
|
||||
@@ -647,15 +752,13 @@ def invite_links(username):
|
||||
|
||||
@bp.post('/<username>/invite-links/create')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def create_invite_link(username):
|
||||
target_user = Users.find({
|
||||
'username': username
|
||||
'username': username.lower()
|
||||
})
|
||||
if not target_user or not target_user.can_invite():
|
||||
return redirect(url_for('.page', username=username))
|
||||
|
||||
if target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
return redirect(url_for('.page', username=username.lower()))
|
||||
|
||||
invite = InviteKeys.create({
|
||||
'created_by': target_user.id,
|
||||
@@ -667,15 +770,13 @@ def create_invite_link(username):
|
||||
|
||||
@bp.post('/<username>/invite-links/revoke')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def revoke_invite_link(username):
|
||||
target_user = Users.find({
|
||||
'username': username
|
||||
'username': username.lower()
|
||||
})
|
||||
if not target_user or not target_user.can_invite():
|
||||
return redirect(url_for('.page', username=username))
|
||||
|
||||
if target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
return redirect(url_for('.page', username=username.lower()))
|
||||
|
||||
invite = InviteKeys.find({
|
||||
'key': request.form.get('key'),
|
||||
@@ -694,10 +795,9 @@ def revoke_invite_link(username):
|
||||
|
||||
@bp.get('/<username>/bookmarks')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def bookmarks(username):
|
||||
target_user = Users.find({'username': username})
|
||||
if not target_user or target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.bookmarks', username=get_active_user().username))
|
||||
target_user = get_active_user()
|
||||
|
||||
collections = target_user.get_bookmark_collections()
|
||||
|
||||
@@ -706,10 +806,40 @@ def bookmarks(username):
|
||||
|
||||
@bp.get('/<username>/bookmarks/collections')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def bookmark_collections(username):
|
||||
target_user = Users.find({'username': username})
|
||||
if not target_user or target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.bookmark_collections', username=get_active_user().username))
|
||||
target_user = get_active_user()
|
||||
|
||||
collections = target_user.get_bookmark_collections()
|
||||
return render_template('users/bookmark_collections.html', collections=collections)
|
||||
|
||||
|
||||
@bp.get('/<username>/delete-account')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def delete_page(username):
|
||||
target_user = get_active_user()
|
||||
|
||||
return render_template('users/delete_page.html')
|
||||
|
||||
|
||||
@bp.post('/<username>/delete-account')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def delete_page_confirm(username):
|
||||
target_user = get_active_user()
|
||||
|
||||
password = request.form.get('password', default='')
|
||||
|
||||
if not verify(target_user.password_hash, password):
|
||||
flash('Incorrect password.', InfoboxKind.ERROR)
|
||||
return redirect(url_for('.delete_page', username=username))
|
||||
|
||||
anonymize_user(target_user.id)
|
||||
sessions = Sessions.findall({'user_id': int(target_user.id)})
|
||||
for session_obj in sessions:
|
||||
session_obj.delete()
|
||||
|
||||
session.clear()
|
||||
target_user.delete()
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
@@ -120,6 +120,27 @@ SCHEMA = [
|
||||
UNIQUE(collection_id, thread_id)
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "motd" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"title" TEXT NOT NULL,
|
||||
"body_original_markup" TEXT NOT NULL,
|
||||
"body_rendered" TEXT NOT NULL,
|
||||
"markup_language" TEXT NOT NULL DEFAULT 'babycode',
|
||||
"format_version" INTEGER DEFAULT NULL,
|
||||
"created_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
|
||||
"edited_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
|
||||
"user_id" REFERENCES users(id) ON DELETE CASCADE
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "mentions" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"revision_id" REFERENCES post_history(id) ON DELETE CASCADE,
|
||||
"mentioned_user_id" REFERENCES users(id) ON DELETE CASCADE,
|
||||
"start_index" INTEGER NOT NULL,
|
||||
"end_index" INTEGER NOT NULL,
|
||||
"original_mention_text" TEXT NOT NULL
|
||||
)""",
|
||||
|
||||
# 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)",
|
||||
@@ -143,6 +164,9 @@ SCHEMA = [
|
||||
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_collection ON bookmarked_threads(collection_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_thread ON bookmarked_threads(thread_id)",
|
||||
|
||||
"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)",
|
||||
]
|
||||
|
||||
def create():
|
||||
|
||||
@@ -1,182 +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>Text inside the tag becomes the alt text. 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>
|
||||
{% 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 %}
|
||||
@@ -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-5.1.0-rc6.min.js" type="module"></script>
|
||||
<script src="{{ '/static/js/vnd/bitty-7.0.0-rc1.min.js' | cachebust }}" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<bitty-5-1 data-connect="/static/js/bitties/pyrom-bitty.js">
|
||||
<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,10 +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-5-1>
|
||||
{% include 'common/footer.html' %}
|
||||
</bitty-7-0>
|
||||
<script src="{{ "/static/js/ui.js" | cachebust }}"></script>
|
||||
<script src="{{ "/static/js/date-fmt.js" | cachebust }}"></script>
|
||||
</body>
|
||||
|
||||
8
app/templates/common/404.html
Normal file
8
app/templates/common/404.html
Normal file
@@ -0,0 +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>
|
||||
<p>The requested URL does not exist.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
7
app/templates/common/footer.html
Normal file
7
app/templates/common/footer.html
Normal 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>
|
||||
@@ -1,49 +1,55 @@
|
||||
{# https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license #}
|
||||
|
||||
{% macro icn_bookmark(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 6C6 4.89543 6.89543 4 8 4H16C17.1046 4 18 4.89543 18 6V18.7268C18 19.5969 16.9657 20.0519 16.3243 19.4639L12 15.5L7.67573 19.4639C7.03432 20.0519 6 19.5969 6 18.7268V6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_error(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.364 5.63604C19.9926 7.26472 21 9.51472 21 12C21 16.9706 16.9706 21 12 21C9.51472 21 7.26472 19.9926 5.63604 18.364M18.364 5.63604C16.7353 4.00736 14.4853 3 12 3C7.02944 3 3 7.02944 3 12C3 14.4853 4.00736 16.7353 5.63604 18.364M18.364 5.63604L5.63604 18.364" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_info(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8V8.5M12 12V16M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_lock(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 14V16M8 9V6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6V9M7 21H17C18.1046 21 19 20.1046 19 19V11C19 9.89543 18.1046 9 17 9H7C5.89543 9 5 9.89543 5 11V19C5 20.1046 5.89543 21 7 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_warn(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15H12.01M12 12V9M4.98207 19H19.0179C20.5615 19 21.5233 17.3256 20.7455 15.9923L13.7276 3.96153C12.9558 2.63852 11.0442 2.63852 10.2724 3.96153L3.25452 15.9923C2.47675 17.3256 3.43849 19 4.98207 19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_image(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 17L7.58959 13.7694C8.38025 13.0578 9.58958 13.0896 10.3417 13.8417L11.5 15L15.0858 11.4142C15.8668 10.6332 17.1332 10.6332 17.9142 11.4142L20 13.5M11 9C11 9.55228 10.5523 10 10 10C9.44772 10 9 9.55228 9 9C9 8.44772 9.44772 8 10 8C10.5523 8 11 8.44772 11 9ZM6 20H18C19.1046 20 20 19.1046 20 18V6C20 4.89543 19.1046 4 18 4H6C4.89543 4 4 4.89543 4 6V18C4 19.1046 4.89543 20 6 20Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_spoiler(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4L9.87868 9.87868M20 20L14.1213 14.1213M9.87868 9.87868C9.33579 10.4216 9 11.1716 9 12C9 13.6569 10.3431 15 12 15C12.8284 15 13.5784 14.6642 14.1213 14.1213M9.87868 9.87868L14.1213 14.1213M6.76821 6.76821C4.72843 8.09899 2.96378 10.026 2 11.9998C3.74646 15.5764 8.12201 19 11.9998 19C13.7376 19 15.5753 18.3124 17.2317 17.2317M9.76138 5.34717C10.5114 5.12316 11.2649 5 12.0005 5C15.8782 5 20.2531 8.42398 22 12.0002C21.448 13.1302 20.6336 14.2449 19.6554 15.2412" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_sticky(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon" width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_megaphone(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark %}
|
||||
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone %}
|
||||
{% macro pager(current_page, page_count) %}
|
||||
{% set left_start = [1, current_page - 5] | max %}
|
||||
{% set right_end = [page_count, current_page + 5] | min %}
|
||||
@@ -39,49 +39,79 @@
|
||||
{% macro infobox(message, kind=InfoboxKind.INFO) %}
|
||||
<div class="{{ "infobox " + InfoboxHTMLClass[kind] }}">
|
||||
<span>
|
||||
<div class="infobox-icon-container">
|
||||
{%- if kind == InfoboxKind.INFO -%}
|
||||
{{- icn_info() -}}
|
||||
{%- elif kind == InfoboxKind.LOCK -%}
|
||||
{{- icn_lock() -}}
|
||||
{%- elif kind == InfoboxKind.WARN -%}
|
||||
{{- icn_warn() -}}
|
||||
{%- elif kind == InfoboxKind.ERROR -%}
|
||||
{{- icn_error() -}}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{{ message }}
|
||||
<div class="infobox-icon-container">
|
||||
{%- if kind == InfoboxKind.INFO -%}
|
||||
{{- icn_info() -}}
|
||||
{%- elif kind == InfoboxKind.LOCK -%}
|
||||
{{- icn_lock() -}}
|
||||
{%- elif kind == InfoboxKind.WARN -%}
|
||||
{{- icn_warn() -}}
|
||||
{%- elif kind == InfoboxKind.ERROR -%}
|
||||
{{- icn_error() -}}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<span>
|
||||
{% set m = message.split(';', maxsplit=1) %}
|
||||
<strong>{{ m[0] }}</strong>
|
||||
{%- if m[1] %}
|
||||
{{ m[1] -}}
|
||||
{%- endif -%}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro motd(motd_obj) %}
|
||||
<div class="motd">
|
||||
<div class="motd-icon-container contain-svg">
|
||||
{{ icn_megaphone(80) }}
|
||||
<i><abbr title="Message of the Day">MOTD</abbr></i>
|
||||
</div>
|
||||
<div class="motd-content-container">
|
||||
<div class="motd-title">{{ motd_obj.title }}</div>
|
||||
<div class="motd-body">{{ motd_obj.body_rendered | safe}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timestamp(unix_ts) -%}
|
||||
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
|
||||
<span class="timestamp" data-utc="{{ unix_ts }}" data-init="convertTimestamps">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="") %}
|
||||
<div class="babycode-editor-container">
|
||||
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="", banned_tags=[]) %}
|
||||
<div class="babycode-editor-container tab-bar" data-receive="toggleTab">
|
||||
<input type="hidden" id="babycode-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
|
||||
<div class="tab-buttons">
|
||||
<button type=button class="tab-button active" data-target-id="tab-edit">Write</button>
|
||||
<button type=button class="tab-button" data-target-id="tab-preview">Preview</button>
|
||||
<button data-send="toggleTab" type=button class="tab-button active" data-target-id="tab-edit">Write</button>
|
||||
<button data-send="babycodePreview toggleTab" type=button class="tab-button" data-target-id="tab-preview">Preview</button>
|
||||
</div>
|
||||
<div class="tab-content active" id="tab-edit">
|
||||
<span class="babycode-button-container">
|
||||
<button class="babycode-button" type=button id="post-editor-bold" title="Insert Bold"><strong>B</strong></button>
|
||||
<button class="babycode-button" type=button id="post-editor-italics" title="Insert Italics"><em>I</em></button>
|
||||
<button class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough"><del>S</del></button>
|
||||
<button class="babycode-button" type=button id="post-editor-underline" title="Insert Underline"><u>U</u></button>
|
||||
<button class="babycode-button" type=button id="post-editor-url" title="Insert Link"><code>://</code></button>
|
||||
<button class="babycode-button" type=button id="post-editor-code" title="Insert Code block"><code></></code></button>
|
||||
<button class="babycode-button contain-svg" type=button id="post-editor-img" title="Insert Image">{{ icn_image() }}</button>
|
||||
<button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list">1.</button>
|
||||
<button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list">•</button>
|
||||
<button class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler">{{ icn_spoiler() }}</button>
|
||||
<button data-send="insertBabycodeTag" data-tag="b" class="babycode-button" type=button id="post-editor-bold" title="Insert Bold" {{"disabled" if "b" in banned_tags else ""}}><strong>B</strong></button>
|
||||
<button data-send="insertBabycodeTag" data-tag="i" class="babycode-button" type=button id="post-editor-italics" title="Insert Italics" {{"disabled" if "i" in banned_tags else ""}}><em>I</em></button>
|
||||
<button data-send="insertBabycodeTag" data-tag="s" class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough" {{"disabled" if "s" in banned_tags else ""}}><del>S</del></button>
|
||||
<button data-send="insertBabycodeTag" data-tag="u" class="babycode-button" type=button id="post-editor-underline" title="Insert Underline" {{"disabled" if "u" in banned_tags else ""}}><u>U</u></button>
|
||||
<button data-send="insertBabycodeTag" data-tag="url=" data-prefill="link label" class="babycode-button" type=button id="post-editor-url" title="Insert Link" {{"disabled" if "url" in banned_tags else ""}}><code>://</code></button>
|
||||
<button data-send="insertBabycodeTag" data-tag="code=" data-break-line="1" class="babycode-button" type=button id="post-editor-code" title="Insert Code block" {{"disabled" if "code" in banned_tags else ""}}><code></></code></button>
|
||||
<button data-send="insertBabycodeTag" data-tag="img=" data-prefill="alt text" class="babycode-button contain-svg" type=button id="post-editor-img" title="Insert Image" {{"disabled" if "img" in banned_tags else ""}}>{{ icn_image() }}</button>
|
||||
<button data-send="insertBabycodeTag" data-tag="ol" data-break-line="1" class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list" {{"disabled" if "ol" in banned_tags else ""}}>1.</button>
|
||||
<button data-send="insertBabycodeTag" data-tag="ul" data-break-line="1" class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list" {{"disabled" if "u;" in banned_tags else ""}}>•</button>
|
||||
<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 "" }}>{{ prefill }}</textarea>
|
||||
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
|
||||
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }} autocomplete="off" data-receive="insertBabycodeTag addQuote">{{ prefill }}</textarea>
|
||||
<a href="{{ url_for("guides.guide_page", category='user-guides', slug='babycode') }}" target="_blank">babycode guide</a>
|
||||
{% if banned_tags %}
|
||||
<div>Forbidden tags:</div>
|
||||
<div>
|
||||
<ul class="horizontal">
|
||||
{% for tag in banned_tags | unique %}
|
||||
<li><code class="inline-code">{{ tag }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-content" id="tab-preview">
|
||||
<div class="tab-content" id="tab-preview" data-receive="babycodePreview">
|
||||
<div id="babycode-preview-errors-container">Type something!</div>
|
||||
<div id="babycode-preview-container"></div>
|
||||
</div>
|
||||
@@ -91,7 +121,7 @@
|
||||
|
||||
{% macro babycode_editor_form(ta_name, prefill = "", cancel_url="", endpoint="") %}
|
||||
{% set save_button_text = "Post reply" if not cancel_url else "Save" %}
|
||||
<form class="post-edit-form" method="post" action={{ endpoint }}>
|
||||
<form class="post-edit-form" method="post" {%- if endpoint %}action={{ endpoint }}{% endif %}>
|
||||
{{babycode_editor_component(ta_name, prefill = prefill)}}
|
||||
{% if not cancel_url %}
|
||||
<span>
|
||||
@@ -126,7 +156,8 @@
|
||||
<a href="{{ url_for("users.page", username=post['username']) }}" style="display: contents;">
|
||||
<img src="{{ post['avatar_path'] }}" class="avatar">
|
||||
</a>
|
||||
<a href="{{ url_for("users.page", username=post['username']) }}" class="username-link">{{ post['username'] }}</a>
|
||||
<a href="{{ url_for("users.page", username=post['username']) }}" class="username-link">{{ post['display_name'] or post['username'] }}</a>
|
||||
<em><abbr title="Mention">@{{ post.username }}</abbr></em>
|
||||
{% if post['status'] %}
|
||||
<em class="user-status">{{ post['status'] }}</em>
|
||||
{% endif %}
|
||||
@@ -175,9 +206,9 @@
|
||||
{% endif %}
|
||||
|
||||
{% if show_reply %}
|
||||
{% set qtext = "[url=%s]%s said:[/url]" | format(post_permalink, post['username']) %}
|
||||
{% set qtext = "@%s [url=%s]said:[/url]" | format(post['username'], post_permalink) %}
|
||||
{% set reply_text = "%s\n[quote]\n%s\n[/quote]\n" | format(qtext, post['original_markup']) %}
|
||||
<button value="{{ reply_text }}" class="reply-button">Quote</button>
|
||||
<button data-send="addQuote" value="{{ reply_text }}" class="reply-button">Quote</button>
|
||||
{% endif %}
|
||||
|
||||
{% set show_delete = false %}
|
||||
@@ -242,13 +273,13 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro accordion(hidden=false, style="", disabled=false) %}
|
||||
{% macro accordion(hidden=false, disabled=false) %}
|
||||
{% if disabled %}
|
||||
{% set hidden = true %}
|
||||
{% endif %}
|
||||
<div class="accordion {{ "hidden" if hidden else ""}}" style="{{style}}">
|
||||
<div class="accordion {{ "hidden" if hidden else ""}}" data-receive="toggleAccordion">
|
||||
<div class="accordion-header">
|
||||
<button type="button" class="accordion-toggle" {{"disabled" if disabled else ""}}>{{ "+" if hidden else "-" }}</button>
|
||||
<button type="button" class="accordion-toggle" {{"disabled" if disabled else ""}} data-send="toggleAccordion">{{ "+" if hidden else "-" }}</button>
|
||||
{{ caller('header') }}
|
||||
</div>
|
||||
<div class="accordion-content {{ "hidden" if hidden else "" }}">
|
||||
@@ -256,3 +287,22 @@
|
||||
</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 %}
|
||||
|
||||
@@ -11,23 +11,20 @@
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% with user = get_active_user() %}
|
||||
Welcome, <a href="{{ url_for("users.page", username = user.username) }}">{{user.username}}</a>
|
||||
•
|
||||
<a href="{{ url_for("users.settings", username = user.username) }}">Settings</a>
|
||||
•
|
||||
<a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</a>
|
||||
{% if config.DISABLE_SIGNUP and user.can_invite() %}
|
||||
•
|
||||
<a href="{{ url_for('users.invite_links', username=user.username )}}">Invite to {{ config.SITE_NAME }}</a>
|
||||
{% endif %}
|
||||
{% if not user.is_guest() %}
|
||||
•
|
||||
<a href="{{ url_for('users.bookmarks', username=user.username) }}">Bookmarks</a>
|
||||
{% endif %}
|
||||
{% if user.is_mod() %}
|
||||
•
|
||||
<a href="{{ url_for("mod.user_list") }}">User list</a>
|
||||
{% endif %}
|
||||
Welcome, <a href="{{ url_for("users.page", username = user.username) }}">{{user.get_readable_name()}}</a>
|
||||
<ul class="horizontal">
|
||||
<li><a href="{{ url_for("users.settings", username = user.username) }}">Settings</a></li>
|
||||
<li><a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</a></li>
|
||||
{% if config.DISABLE_SIGNUP and user.can_invite() %}
|
||||
<li><a href="{{ url_for('users.invite_links', username=user.username )}}">Invite to {{ config.SITE_NAME }}</a></li>
|
||||
{% endif %}
|
||||
{% if not user.is_guest() %}
|
||||
<li><a href="{{ url_for('users.bookmarks', username=user.username) }}">Bookmarks</a></li>
|
||||
{% endif %}
|
||||
{% if user.is_mod() %}
|
||||
<li><a href="{{ url_for("mod.panel") }}">Moderation</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% else %}
|
||||
{% set bookmark_url = url_for('api.bookmark_thread', thread_id=id) %}
|
||||
{% endif %}
|
||||
<div class="bookmarks-dropdown" data-bookmark-type="{{type}}" data-receive="saveBookmarks" data-bookmark-endpoint="{{bookmark_url}}" data-originally-contained-in="{{ selected.id if selected else ""}}" data-require-reload={{require_reload | int}}>
|
||||
<div class="bookmarks-dropdown" data-bookmark-type="{{type}}" data-receive="saveBookmarks" data-bookmark-endpoint="{{bookmark_url}}" data-originally-contained-in="{{ selected.id if selected else ""}}" data-require-reload={{require_reload | int}} popover=auto>
|
||||
<div class="bookmarks-dropdown-header">
|
||||
<span>Bookmark collections</span>
|
||||
{% if not require_reload %}
|
||||
@@ -12,9 +12,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="bookmark-dropdown-items-container">
|
||||
{% for collection in collections %}
|
||||
<div class="bookmark-dropdown-item {{ "selected" if selected and (selected.id | int) == (collection.id | int) else ""}}" data-send="selectBookmarkCollection" data-receive="selectBookmarkCollection" data-collection-id="{{collection.id}}">{{collection.name}} ({{ collection.get_posts_count() }}p, {{ collection.get_threads_count() }}t)</div>
|
||||
{% endfor %}
|
||||
{%- for collection in collections -%}
|
||||
{%- set pc = collection.get_posts_count() -%}
|
||||
{%- set tc = collection.get_threads_count() -%}
|
||||
<div class="bookmark-dropdown-item {{ "selected" if selected and (selected.id | int) == (collection.id | int) else ""}}" data-send="selectBookmarkCollection" data-receive="selectBookmarkCollection" data-collection-id="{{collection.id}}">
|
||||
<span class="bookmark-dropdown-item-name">{{collection.name}}</span>
|
||||
<span class="bookmark-dropdown-item-stats"><abbr title="{{ pc }} {{('post' | pluralize(pc))}}">{{ pc }}p</abbr>, <abbr title="{{ tc }} {{('thread' | pluralize(tc))}}">{{ tc }}t</abbr></span>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
<span>
|
||||
<input type="text" placeholder="Memo" class="bookmark-memo-input" value="{{memo}}"></input>
|
||||
|
||||
21
app/templates/guides/_layout.html
Normal file
21
app/templates/guides/_layout.html
Normal 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 class="thread-title">Guide: {{ guide.title }}</h1>
|
||||
<ul class="horizontal">
|
||||
<li><a href="{{ url_for('guides.category_index', category=category) }}">↑ Back to category</a></li>
|
||||
{% if prev_guide %}
|
||||
<li><a href="{{ prev_guide.url }}">← Previous: {{ prev_guide.title }}</a></li>
|
||||
{% endif %}
|
||||
{% if next_guide %}
|
||||
<li><a href="{{ next_guide.url }}">→ Next: {{ next_guide.title }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% call() guide_sections() %}
|
||||
{% block guide_content %}
|
||||
{% endblock %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
15
app/templates/guides/category_index.html
Normal file
15
app/templates/guides/category_index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}guides - {{ category | title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<h1 class="thread-title">All guides in category "{{ category | replace('-', ' ') | title }}"</h1>
|
||||
<ul>
|
||||
{% for page in pages %}
|
||||
<li><a href="{{ page.url }}">{{ page.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div>
|
||||
<a href="{{ url_for('guides.guides_index') }}">← All guide categories</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
13
app/templates/guides/contact.html
Normal file
13
app/templates/guides/contact.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}contact us{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<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 %}
|
||||
19
app/templates/guides/guides_index.html
Normal file
19
app/templates/guides/guides_index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}guides{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<h1 class="thread-title">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 %}
|
||||
76
app/templates/guides/user-guides/01-introduction.html
Normal file
76
app/templates/guides/user-guides/01-introduction.html
Normal file
@@ -0,0 +1,76 @@
|
||||
# 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>
|
||||
<p>You can</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, and status.</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 %}
|
||||
24
app/templates/guides/user-guides/02-profiles.html
Normal file
24
app/templates/guides/user-guides/02-profiles.html
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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 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 %}
|
||||
64
app/templates/guides/user-guides/03-settings.html
Normal file
64
app/templates/guides/user-guides/03-settings.html
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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…</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 it in the "New password" field and again in the "Confirm new password" field, then pressing the <button class="warn">Change password</button> button. The passwords in the two fields must match.</p>
|
||||
</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 %}
|
||||
29
app/templates/guides/user-guides/04-inbox.html
Normal file
29
app/templates/guides/user-guides/04-inbox.html
Normal 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 %}
|
||||
39
app/templates/guides/user-guides/05-bookmarks.html
Normal file
39
app/templates/guides/user-guides/05-bookmarks.html
Normal 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…</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…</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…</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…</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 %}
|
||||
184
app/templates/guides/user-guides/99-babycode.html
Normal file
184
app/templates/guides/user-guides/99-babycode.html
Normal 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 %}
|
||||
17
app/templates/mod/motd.html
Normal file
17
app/templates/mod/motd.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% from 'common/macros.html' import babycode_editor_component %}
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}editing MOTD{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<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">
|
||||
<label for="title">Title</label>
|
||||
<input name="title" id="title" type="text" required autocomplete="off" placeholder="Required" value="{{ current.title }}"><br>
|
||||
<label for="body">Body</label>
|
||||
{{ babycode_editor_component('body', ta_placeholder='MOTD body (required)', banned_tags=MOTD_BANNED_TAGS, prefill=current.body_original_markup) }}
|
||||
<input type="submit" value="Save">
|
||||
<input class="critical" type="submit" formaction="{{ url_for('mod.motd_delete') }}" value="Delete MOTD" formnovalidate {{"disabled" if not current else ""}}>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
12
app/templates/mod/panel.html
Normal file
12
app/templates/mod/panel.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}moderation{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<h1>Moderation actions</h1>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('mod.user_list') }}">User list</a></li>
|
||||
<li><a href="{{ url_for('mod.sort_topics') }}">Sort topics</a></li>
|
||||
<li><a href="{{ url_for('mod.motd_editor') }}">Message of the Day</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% from 'common/macros.html' import pager, timestamp %}
|
||||
{% from 'common/macros.html' import pager, timestamp, motd %}
|
||||
{% from 'common/icons.html' import icn_lock, icn_sticky %}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}browsing topic {{ topic['name'] }}{% endblock %}
|
||||
@@ -24,9 +24,17 @@
|
||||
</nav>
|
||||
|
||||
{% if topic['is_locked'] %}
|
||||
{{ infobox("This topic is locked. Only moderators can create new threads.", InfoboxKind.INFO) }}
|
||||
{{ infobox("This topic is locked.;Only moderators can create new threads.", InfoboxKind.INFO) }}
|
||||
{% endif %}
|
||||
|
||||
{%- with motds = get_motds() -%}
|
||||
{%- if motds -%}
|
||||
{%- for motd_obj in motds -%}
|
||||
{{- motd(motd_obj) -}}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- endwith -%}
|
||||
|
||||
{% if threads_list | length == 0 %}
|
||||
<p>There are no threads in this topic.</p>
|
||||
{% else %}
|
||||
@@ -48,12 +56,12 @@
|
||||
</span>
|
||||
•
|
||||
<span>
|
||||
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by'] }}</a> on {{ timestamp(thread['created_at']) }}
|
||||
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by_display_name'] or thread['started_by'] }}</a> on {{ timestamp(thread['created_at']) }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
Latest post by <a href="{{ url_for("users.page", username=thread['latest_post_username']) }}">{{ thread['latest_post_username'] }}</a>
|
||||
Latest post by <a href="{{ url_for("users.page", username=thread['latest_post_username']) }}">{{ thread['latest_post_display_name'] or thread['latest_post_username'] }}</a>
|
||||
on <a href="{{ url_for("threads.thread", slug=thread['slug'], after=thread['latest_post_id']) }}">on {{ timestamp(thread['latest_post_created_at']) }}</a>:
|
||||
</span>
|
||||
<span class="thread-info-post-preview">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% from 'common/icons.html' import icn_lock %}
|
||||
{% from 'common/macros.html' import timestamp %}
|
||||
{% from 'common/macros.html' import timestamp, motd %}
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<nav class="darkbg">
|
||||
@@ -9,6 +9,13 @@
|
||||
<a class="linkbutton" href={{ url_for("mod.sort_topics") }}>Sort topics</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{%- with motds = get_motds() -%}
|
||||
{%- if motds -%}
|
||||
{%- for motd_obj in motds -%}
|
||||
{{- motd(motd_obj) -}}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- endwith -%}
|
||||
{% if topic_list | length == 0 %}
|
||||
<p>There are no topics.</p>
|
||||
{% else %}
|
||||
@@ -19,12 +26,12 @@
|
||||
{{ topic['description'] }}
|
||||
{% if topic['latest_thread_username'] %}
|
||||
<span>
|
||||
Latest thread: <a href="{{ url_for("threads.thread", slug=topic['latest_thread_slug'])}}">{{topic['latest_thread_title']}}</a> by <a href="{{url_for("users.page", username=topic['latest_thread_username'])}}">{{topic['latest_thread_username']}}</a> on {{ timestamp(topic['latest_thread_created_at']) }}
|
||||
Latest thread: <a href="{{ url_for("threads.thread", slug=topic['latest_thread_slug'])}}">{{topic['latest_thread_title']}}</a> by <a href="{{url_for("users.page", username=topic['latest_thread_username'])}}">{{topic['latest_thread_display_name'] or topic['latest_thread_username']}}</a> on {{ timestamp(topic['latest_thread_created_at']) }}
|
||||
</span>
|
||||
{% if topic['id'] in active_threads %}
|
||||
{% with thread=active_threads[topic['id']] %}
|
||||
<span>
|
||||
Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['username'] }}</a> at <a href="{{ get_post_url(thread.post_id, _anchor=true) }}">{{ timestamp(thread['post_created_at']) }}</a>
|
||||
Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['display_name'] or thread['username'] }}</a> at <a href="{{ get_post_url(thread.post_id, _anchor=true) }}">{{ timestamp(thread['post_created_at']) }}</a>
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
15
app/templates/users/delete_page.html
Normal file
15
app/templates/users/delete_page.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}delete confirmation{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg login-container">
|
||||
<h1>Confirm account deletion</h1>
|
||||
<p>Are you sure you want to delete your account on {{ config.SITE_NAME }}? <strong>This action is irreversible.</strong> Your posts and threads will remain accessible to preserve history but will be de-personalized, showing up as authored by a system user. Posts that @mention you will also mention the system user instead.</p>
|
||||
<p>If you wish for any and all content relating to you to be removed, you will have to <a href="{{url_for("guides.contact")}}" target="_blank">contact {{ config.SITE_NAME }}'s administrators separately.</a></p>
|
||||
<p>If you are sure, please confirm your current password below.</p>
|
||||
<form method="post">
|
||||
<label for="password">Confirm password</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
<input class="critical" type="submit" value="Delete account">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,41 +5,57 @@
|
||||
{% set disable_avatar = not is_logged_in() %}
|
||||
<div class='darkbg settings-container'>
|
||||
<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='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 scaled down to fit a 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>
|
||||
</div>
|
||||
<div>
|
||||
<a class="linkbutton critical" href="{{ url_for('users.delete_page', username=active_user.username) }}">Delete account</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="darkbg login-container">
|
||||
<h1>Sign up</h1>
|
||||
{% if inviter %}
|
||||
<p>You have been invited by <a href="{{ url_for('users.page', username=inviter.username) }}">{{ inviter.username }}</a> to join {{ config.SITE_NAME }}. Create an identity below.</p>
|
||||
<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>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% if key %}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% from 'common/macros.html' import timestamp %}
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ target_user.username }}'s profile{% endblock %}
|
||||
{% block title %}{{ target_user.get_readable_name() }}'s profile{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg">
|
||||
<h1 class="thread-title"><i>{{ target_user.username }}</i>'s profile</h1>
|
||||
<h1 class="thread-title"><i>{{ target_user.get_readable_name() }}</i>'s profile</h1>
|
||||
{% if active_user.id == target_user.id %}
|
||||
<div class="user-actions">
|
||||
<a class="linkbutton" href="{{ url_for("users.settings", username = active_user.username) }}">Settings</a>
|
||||
@@ -45,7 +45,8 @@
|
||||
<div class="user-page-usercard">
|
||||
<div class="usercard-inner">
|
||||
<img class="avatar" src="{{ target_user.get_avatar_url() }}">
|
||||
<strong class="big">{{ target_user.username }}</strong>
|
||||
<strong class="big">{{ target_user.get_readable_name() }}</strong>
|
||||
<em><abbr title="Mention">@{{ target_user.username }}</abbr></em>
|
||||
{% if target_user.status %}
|
||||
<em class="user-status">{{ target_user.status }}</em>
|
||||
{% endif %}
|
||||
@@ -65,7 +66,7 @@
|
||||
<li>Latest started thread: <a href="{{ url_for("threads.thread", slug = stats.latest_thread_slug) }}">{{ stats.latest_thread_title }}</a>
|
||||
{% endif %}
|
||||
{% if stats.inviter_username %}
|
||||
<li>Invited by <a href="{{ url_for('users.page', username=stats.inviter_username) }}">{{ stats.inviter_username }}</a></li>
|
||||
<li>Invited by <a href="{{ url_for('users.page', username=stats.inviter_username) }}">{{ stats.inviter_display_name or stats.inviter_username }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endwith %}
|
||||
|
||||
@@ -6,3 +6,13 @@ DISABLE_SIGNUP = false # if true, no one can sign up.
|
||||
|
||||
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 = ""
|
||||
|
||||
@@ -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,7 +54,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Cadman";
|
||||
font-family: "Cadman", sans-serif;
|
||||
margin: 20px 100px;
|
||||
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||
color: black;
|
||||
@@ -73,6 +89,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;
|
||||
@@ -188,6 +214,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 +580,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;
|
||||
@@ -814,12 +844,12 @@ p {
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 40%;
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.settings-container > * {
|
||||
width: 40%;
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -835,15 +865,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;
|
||||
@@ -949,14 +984,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;
|
||||
@@ -1094,7 +1129,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
|
||||
.babycode-editor {
|
||||
height: 150px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.babycode-editor-container {
|
||||
@@ -1156,6 +1190,14 @@ ul, ol {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.horizontal, ol.horizontal {
|
||||
display: inline;
|
||||
margin-left: 0;
|
||||
}
|
||||
ul.horizontal li, ol.horizontal li {
|
||||
display: inline list-item;
|
||||
}
|
||||
|
||||
.new-concept-notification.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -1179,9 +1221,9 @@ ul, ol {
|
||||
.accordion {
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1298,7 +1340,7 @@ footer {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.babycode-guide-list {
|
||||
.guide-list {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
@@ -1312,14 +1354,17 @@ footer {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
padding: 10px;
|
||||
z-index: 100;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
@@ -1332,10 +1377,13 @@ footer {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
}
|
||||
.bookmark-dropdown-item::before {
|
||||
content: 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");
|
||||
content: "";
|
||||
background-color: currentColor;
|
||||
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 {
|
||||
background-color: #beb1ce;
|
||||
@@ -1344,7 +1392,7 @@ footer {
|
||||
background-color: rgb(203, 192.6, 215.8);
|
||||
}
|
||||
.bookmark-dropdown-item.selected::before {
|
||||
content: 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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E");
|
||||
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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
|
||||
}
|
||||
|
||||
.bookmarks-dropdown-header {
|
||||
@@ -1352,7 +1400,81 @@ footer {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item-stats {
|
||||
padding: 0 10px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.motd {
|
||||
display: flex;
|
||||
background-color: #c1ceb1;
|
||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||
padding: 10px 15px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.motd-icon-container {
|
||||
display: flex;
|
||||
min-width: 80px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.motd-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
flex-grow: 1;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
||||
.motd-title {
|
||||
font-weight: bold;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
a.mention, a.mention:visited {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background-color: rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.mention.display, a.mention:visited.display {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
}
|
||||
a.mention.me, a.mention:visited.me {
|
||||
background-color: rgb(123.0025984252, 145.0974015748, 143.9545669291);
|
||||
border: 1px dashed;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@@ -26,19 +26,35 @@
|
||||
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;
|
||||
border-radius: 8px;
|
||||
padding: 5px 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Cadman";
|
||||
font-family: "Cadman", sans-serif;
|
||||
margin: 20px 100px;
|
||||
background-color: #220d16;
|
||||
color: #e6e6e6;
|
||||
@@ -73,6 +89,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;
|
||||
@@ -188,6 +214,10 @@ a:visited {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Atkinson Hyperlegible Mono", monospace;
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
background-color: #302731;
|
||||
@@ -550,7 +580,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;
|
||||
@@ -574,14 +604,14 @@ pre code { /* Literal.Number.Integer.Long */ }
|
||||
padding: 5px 10px;
|
||||
display: inline-block;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid black;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
@@ -618,7 +648,7 @@ pre code { /* Literal.Number.Integer.Long */ }
|
||||
blockquote {
|
||||
padding: 10px 20px;
|
||||
margin: 10px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
border-left: 10px solid #ae6bae;
|
||||
background-color: rgba(251, 175, 207, 0.0392156863);
|
||||
}
|
||||
@@ -814,12 +844,12 @@ p {
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 40%;
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.settings-container > * {
|
||||
width: 40%;
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -832,18 +862,23 @@ p {
|
||||
|
||||
input[type=text], input[type=password], textarea, select {
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
@@ -949,14 +984,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;
|
||||
@@ -1094,7 +1129,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
|
||||
.babycode-editor {
|
||||
height: 150px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.babycode-editor-container {
|
||||
@@ -1146,9 +1180,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
background-color: #503250;
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
@@ -1156,6 +1190,14 @@ ul, ol {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.horizontal, ol.horizontal {
|
||||
display: inline;
|
||||
margin-left: 0;
|
||||
}
|
||||
ul.horizontal li, ol.horizontal li {
|
||||
display: inline list-item;
|
||||
}
|
||||
|
||||
.new-concept-notification.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -1167,7 +1209,7 @@ ul, ol {
|
||||
border: 1px solid black;
|
||||
background-color: #775891;
|
||||
padding: 20px 15px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
@@ -1177,11 +1219,11 @@ ul, ol {
|
||||
}
|
||||
|
||||
.accordion {
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
border-top-right-radius: 8px;
|
||||
border-top-left-radius: 8px;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1249,7 +1291,7 @@ ul, ol {
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
@@ -1284,7 +1326,7 @@ footer {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||
padding: 5px 10px;
|
||||
width: 250px;
|
||||
@@ -1298,7 +1340,7 @@ footer {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.babycode-guide-list {
|
||||
.guide-list {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
@@ -1307,24 +1349,27 @@ footer {
|
||||
}
|
||||
|
||||
.bookmarks-dropdown {
|
||||
background-color: #9b649b;
|
||||
background-color: #503250;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
padding: 10px;
|
||||
z-index: 100;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
color: #e6e6e6;
|
||||
background-color: #3c283c;
|
||||
}
|
||||
@@ -1332,10 +1377,13 @@ footer {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
}
|
||||
.bookmark-dropdown-item::before {
|
||||
content: 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");
|
||||
content: "";
|
||||
background-color: currentColor;
|
||||
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 {
|
||||
background-color: #8a5584;
|
||||
@@ -1344,7 +1392,7 @@ footer {
|
||||
background-color: rgb(167.4843049327, 112.9156950673, 161.3067264574);
|
||||
}
|
||||
.bookmark-dropdown-item.selected::before {
|
||||
content: 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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E");
|
||||
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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
|
||||
}
|
||||
|
||||
.bookmarks-dropdown-header {
|
||||
@@ -1352,16 +1400,109 @@ footer {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item-stats {
|
||||
padding: 0 10px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.motd {
|
||||
display: flex;
|
||||
background-color: #503250;
|
||||
border: 2px outset #503250;
|
||||
padding: 10px 15px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.motd-icon-container {
|
||||
display: flex;
|
||||
min-width: 80px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.motd-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
flex-grow: 1;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
||||
.motd-title {
|
||||
font-weight: bold;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
a.mention, a.mention:visited {
|
||||
display: inline-block;
|
||||
color: #e6e6e6;
|
||||
background-color: rgb(96.95, 81.55, 96.95);
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.mention.display, a.mention:visited.display {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
}
|
||||
a.mention.me, a.mention:visited.me {
|
||||
background-color: rgb(96.95, 89.25, 81.55);
|
||||
border: 1px dashed;
|
||||
}
|
||||
a.mention:hover, a.mention:visited:hover {
|
||||
background-color: #ae6bae;
|
||||
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;
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
margin-bottom: 10px;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
margin-top: 10px;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.infobox, .motd {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.thread-sticky-container {
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.thread-locked-container {
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -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,7 +54,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Cadman";
|
||||
font-family: "Cadman", sans-serif;
|
||||
margin: 12px 50px;
|
||||
background-color: #c85d45;
|
||||
color: black;
|
||||
@@ -73,6 +89,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;
|
||||
@@ -188,6 +214,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 +580,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;
|
||||
@@ -814,12 +844,12 @@ p {
|
||||
}
|
||||
|
||||
.login-container > * {
|
||||
width: 60%;
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.settings-container > * {
|
||||
width: 60%;
|
||||
width: 85%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -835,15 +865,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;
|
||||
@@ -949,14 +984,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;
|
||||
@@ -1094,7 +1129,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
|
||||
.babycode-editor {
|
||||
height: 150px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.babycode-editor-container {
|
||||
@@ -1156,6 +1190,14 @@ ul, ol {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.horizontal, ol.horizontal {
|
||||
display: inline;
|
||||
margin-left: 0;
|
||||
}
|
||||
ul.horizontal li, ol.horizontal li {
|
||||
display: inline list-item;
|
||||
}
|
||||
|
||||
.new-concept-notification.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -1179,9 +1221,9 @@ ul, ol {
|
||||
.accordion {
|
||||
border-top-right-radius: 16px;
|
||||
border-top-left-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 6px 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1298,7 +1340,7 @@ footer {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.babycode-guide-list {
|
||||
.guide-list {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
@@ -1312,14 +1354,17 @@ footer {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
padding: 6px;
|
||||
z-index: 100;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
margin: 6px 0;
|
||||
cursor: pointer;
|
||||
@@ -1332,10 +1377,13 @@ footer {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
}
|
||||
.bookmark-dropdown-item::before {
|
||||
content: 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");
|
||||
content: "";
|
||||
background-color: currentColor;
|
||||
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 {
|
||||
background-color: #b54444;
|
||||
@@ -1344,7 +1392,7 @@ footer {
|
||||
background-color: rgb(197.978313253, 103.221686747, 103.221686747);
|
||||
}
|
||||
.bookmark-dropdown-item.selected::before {
|
||||
content: 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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E");
|
||||
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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
|
||||
}
|
||||
|
||||
.bookmarks-dropdown-header {
|
||||
@@ -1352,11 +1400,85 @@ footer {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item-stats {
|
||||
padding: 0 6px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.motd {
|
||||
display: flex;
|
||||
background-color: #f27a5a;
|
||||
border: 1px solid black;
|
||||
padding: 6px 8px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.motd-icon-container {
|
||||
display: flex;
|
||||
min-width: 80px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.motd-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
flex-grow: 1;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
||||
.motd-title {
|
||||
font-weight: bold;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
a.mention, a.mention:visited {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background-color: rgb(155.8907865169, 93.2211235955, 76.5092134831);
|
||||
padding: 3px;
|
||||
border-radius: 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.mention.display, a.mention:visited.display {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
}
|
||||
a.mention.me, a.mention:visited.me {
|
||||
background-color: rgb(99.4880898876, 155.8907865169, 76.5092134831);
|
||||
border: 1px dashed;
|
||||
}
|
||||
a.mention:hover, a.mention:visited:hover {
|
||||
background-color: rgb(231.56, 212.36, 207.24);
|
||||
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;
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
@@ -1378,10 +1500,3 @@ footer {
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
color: white;
|
||||
}
|
||||
.darkbg a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
data/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf
Normal file
BIN
data/static/fonts/AtkinsonHyperlegibleMono-VariableFont_wght.ttf
Normal file
Binary file not shown.
@@ -3,7 +3,6 @@
|
||||
|
||||
ta.addEventListener("keydown", (e) => {
|
||||
if(e.key === "Enter" && e.ctrlKey) {
|
||||
// console.log(e.target.form)
|
||||
if (inThread()) {
|
||||
localStorage.removeItem(window.location.pathname);
|
||||
}
|
||||
@@ -38,148 +37,4 @@
|
||||
if (!prevContent) return;
|
||||
ta.value = prevContent;
|
||||
})
|
||||
|
||||
const buttonBold = document.getElementById("post-editor-bold");
|
||||
const buttonItalics = document.getElementById("post-editor-italics");
|
||||
const buttonStrike = document.getElementById("post-editor-strike");
|
||||
const buttonUnderline = document.getElementById("post-editor-underline");
|
||||
const buttonUrl = document.getElementById("post-editor-url");
|
||||
const buttonCode = document.getElementById("post-editor-code");
|
||||
const buttonImg = document.getElementById("post-editor-img");
|
||||
const buttonOl = document.getElementById("post-editor-ol");
|
||||
const buttonUl = document.getElementById("post-editor-ul");
|
||||
const buttonSpoiler = document.getElementById("post-editor-spoiler");
|
||||
|
||||
function insertTag(tagStart, newline = false, prefill = "") {
|
||||
const hasAttr = tagStart[tagStart.length - 1] === "=";
|
||||
let tagEnd = tagStart;
|
||||
let tagInsertStart = `[${tagStart}]${newline ? "\n" : ""}`;
|
||||
if (hasAttr) {
|
||||
tagEnd = tagEnd.slice(0, -1);
|
||||
}
|
||||
const tagInsertEnd = `${newline ? "\n" : ""}[/${tagEnd}]`;
|
||||
const hasSelection = ta.selectionStart !== ta.selectionEnd;
|
||||
const text = ta.value;
|
||||
if (hasSelection) {
|
||||
const realStart = Math.min(ta.selectionStart, ta.selectionEnd);
|
||||
const realEnd = Math.max(ta.selectionStart, ta.selectionEnd);
|
||||
const selectionLength = realEnd - realStart;
|
||||
|
||||
const strStart = text.slice(0, realStart);
|
||||
const strEnd = text.substring(realEnd);
|
||||
const frag = `${tagInsertStart}${text.slice(realStart, realEnd)}${tagInsertEnd}`;
|
||||
const reconst = `${strStart}${frag}${strEnd}`;
|
||||
ta.value = reconst;
|
||||
if (!hasAttr){
|
||||
ta.setSelectionRange(realStart + tagInsertStart.length, realStart + tagInsertStart.length + selectionLength);
|
||||
} else {
|
||||
ta.setSelectionRange(realStart + tagInsertEnd.length - 1, realStart + tagInsertEnd.length - 1); // cursor on attr
|
||||
}
|
||||
ta.focus()
|
||||
} else {
|
||||
if (hasAttr) {
|
||||
tagInsertStart += prefill;
|
||||
}
|
||||
const cursor = ta.selectionStart;
|
||||
const strStart = text.slice(0, cursor);
|
||||
const strEnd = text.substr(cursor);
|
||||
|
||||
let newCursor = strStart.length + tagInsertStart.length;
|
||||
if (hasAttr) {
|
||||
newCursor = cursor + tagInsertStart.length - prefill.length - 1;
|
||||
}
|
||||
const reconst = `${strStart}${tagInsertStart}${tagInsertEnd}${strEnd}`;
|
||||
ta.value = reconst;
|
||||
ta.setSelectionRange(newCursor, newCursor);
|
||||
ta.focus()
|
||||
}
|
||||
}
|
||||
|
||||
buttonBold.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("b")
|
||||
})
|
||||
buttonItalics.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("i")
|
||||
})
|
||||
buttonStrike.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("s")
|
||||
})
|
||||
buttonUnderline.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("u")
|
||||
})
|
||||
buttonUrl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("url=", false, "link label");
|
||||
})
|
||||
buttonCode.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("code", true)
|
||||
})
|
||||
buttonImg.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("img=", false, "alt text");
|
||||
})
|
||||
buttonOl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("ol", true);
|
||||
})
|
||||
buttonUl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("ul", true);
|
||||
})
|
||||
buttonSpoiler.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("spoiler=", true, "hidden content");
|
||||
})
|
||||
|
||||
const previewEndpoint = "/api/babycode-preview";
|
||||
let previousMarkup = "";
|
||||
const previewTab = document.getElementById("tab-preview");
|
||||
previewTab.addEventListener("tab-activated", async () => {
|
||||
const previewContainer = document.getElementById("babycode-preview-container");
|
||||
const previewErrorsContainer = document.getElementById("babycode-preview-errors-container");
|
||||
// previewErrorsContainer.textContent = "";
|
||||
const markup = ta.value.trim();
|
||||
if (markup === "" || markup === previousMarkup) {
|
||||
return;
|
||||
}
|
||||
previousMarkup = markup;
|
||||
const req = await fetch(previewEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({markup: markup})
|
||||
})
|
||||
if (!req.ok) {
|
||||
switch (req.status) {
|
||||
case 429:
|
||||
previewErrorsContainer.textContent = "(Old preview, try again in a few seconds.)"
|
||||
previousMarkup = "";
|
||||
break;
|
||||
case 400:
|
||||
previewErrorsContainer.textContent = "(Request got malformed.)"
|
||||
break;
|
||||
case 401:
|
||||
previewErrorsContainer.textContent = "(You are not logged in.)"
|
||||
break;
|
||||
default:
|
||||
previewErrorsContainer.textContent = "(Error. Check console.)"
|
||||
console.error(req.error);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const json_resp = await req.json();
|
||||
previewContainer.innerHTML = json_resp.html;
|
||||
previewErrorsContainer.textContent = "";
|
||||
|
||||
const accordionRefreshEvt = new CustomEvent("refresh_accordions");
|
||||
document.body.dispatchEvent(accordionRefreshEvt);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown'
|
||||
const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown';
|
||||
const previewEndpoint = '/api/babycode-preview';
|
||||
const userEndpoint = '/api/current-user';
|
||||
|
||||
const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))}
|
||||
|
||||
export default class {
|
||||
async showBookmarkMenu(ev, el) {
|
||||
if ((ev.target.dataset.bookmarkId === el.dataset.bookmarkId) && el.childElementCount === 0) {
|
||||
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${ev.target.dataset.bookmarkType}?id=${ev.target.dataset.conceptId}&require_reload=${el.dataset.requireReload}`;
|
||||
if ((el.sender.dataset.bookmarkId === el.ds('bookmarkId')) && el.childElementCount === 0) {
|
||||
const searchParams = new URLSearchParams({
|
||||
'id': el.sender.dataset.conceptId,
|
||||
'require_reload': el.dataset.requireReload,
|
||||
});
|
||||
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${el.sender.dataset.bookmarkType}?${searchParams}`;
|
||||
const res = await this.api.getHTML(bookmarkMenuHref);
|
||||
if (res.error) {
|
||||
return;
|
||||
@@ -11,19 +19,39 @@ export default class {
|
||||
const frag = res.value;
|
||||
el.appendChild(frag);
|
||||
const menu = el.childNodes[0];
|
||||
const bRect = el.getBoundingClientRect()
|
||||
if (bRect.left < window.innerWidth - bRect.right) {
|
||||
menu.style.right = 'unset';
|
||||
menu.showPopover();
|
||||
|
||||
const bRect = el.getBoundingClientRect();
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const preferredLeft = bRect.right - menuRect.width;
|
||||
const preferredRight = bRect.right;
|
||||
const enoughSpace = preferredLeft >= 0;
|
||||
const scrollY = window.scrollY || window.pageYOffset;
|
||||
if (enoughSpace) {
|
||||
menu.style.left = `${preferredLeft}px`;
|
||||
} else {
|
||||
menu.style.left = `${bRect.left}px`;
|
||||
}
|
||||
menu.style.top = `${bRect.bottom + scrollY}px`;
|
||||
|
||||
menu.addEventListener('beforetoggle', (e) => {
|
||||
if (e.newState === 'closed') {
|
||||
// if it's still in the tree, remove it
|
||||
// the delay is required to make sure its removed instantly when
|
||||
// clicking the button when the menu is open
|
||||
setTimeout(() => {menu.remove()}, 100);
|
||||
};
|
||||
}, { once: true });
|
||||
|
||||
} else if (el.childElementCount > 0) {
|
||||
el.removeChild(el.childNodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
selectBookmarkCollection(ev, el) {
|
||||
const clicked = ev.target;
|
||||
const clicked = el.sender;
|
||||
|
||||
if (clicked === el) {
|
||||
if (el.sender === el) {
|
||||
if (clicked.classList.contains('selected')) {
|
||||
clicked.classList.remove('selected');
|
||||
} else {
|
||||
@@ -35,7 +63,7 @@ export default class {
|
||||
}
|
||||
|
||||
async saveBookmarks(ev, el) {
|
||||
const bookmarkHref = el.dataset.bookmarkEndpoint;
|
||||
const bookmarkHref = el.ds('bookmarkEndpoint');
|
||||
const collection = el.querySelector('.bookmark-dropdown-item.selected');
|
||||
let data = {};
|
||||
if (collection) {
|
||||
@@ -44,7 +72,7 @@ export default class {
|
||||
data['memo'] = el.querySelector('.bookmark-memo-input').value;
|
||||
} else {
|
||||
data['operation'] = 'remove';
|
||||
data['collection_id'] = el.dataset.originallyContainedIn;
|
||||
data['collection_id'] = el.ds('originallyContainedIn');
|
||||
}
|
||||
|
||||
const options = {
|
||||
@@ -54,11 +82,198 @@ export default class {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
const requireReload = parseInt(el.dataset.requireReload) !== 0;
|
||||
const requireReload = el.dsInt('requireReload') !== 0;
|
||||
el.remove();
|
||||
await fetch(bookmarkHref, options);
|
||||
if (requireReload) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async copyCode(ev, el) {
|
||||
if (!el.isSender) {
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(el.value);
|
||||
el.textContent = 'Copied!'
|
||||
await delay(1000);
|
||||
el.textContent = 'Copy';
|
||||
}
|
||||
|
||||
toggleAccordion(ev, el) {
|
||||
const accordion = el;
|
||||
const header = accordion.querySelector('.accordion-header');
|
||||
if (!header.contains(el.sender)){
|
||||
return;
|
||||
}
|
||||
const btn = el.sender;
|
||||
const content = el.querySelector('.accordion-content');
|
||||
// these are all meant to be in sync
|
||||
accordion.classList.toggle('hidden');
|
||||
content.classList.toggle('hidden');
|
||||
btn.textContent = accordion.classList.contains('hidden') ? '+' : '-';
|
||||
}
|
||||
|
||||
toggleTab(ev, el) {
|
||||
const tabButtonsContainer = el.querySelector('.tab-buttons');
|
||||
if (!el.contains(el.sender)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.sender.classList.contains('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = el.senderDs('targetId');
|
||||
const contents = el.querySelectorAll('.tab-content');
|
||||
for (let content of contents) {
|
||||
if (content.id === targetId) {
|
||||
content.classList.add('active');
|
||||
} else {
|
||||
content.classList.remove('active');
|
||||
}
|
||||
}
|
||||
for (let button of tabButtonsContainer.children) {
|
||||
if (button.dataset.targetId === targetId) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#previousMarkup = null;
|
||||
async babycodePreview(ev, el) {
|
||||
if (el.sender.classList.contains('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previewErrorsContainer = el.querySelector('#babycode-preview-errors-container');
|
||||
const previewContainer = el.querySelector('#babycode-preview-container');
|
||||
const ta = document.getElementById('babycode-content');
|
||||
const markup = ta.value.trim();
|
||||
if (markup === '') {
|
||||
previewErrorsContainer.textContent = 'Type something!';
|
||||
previewContainer.textContent = '';
|
||||
this.#previousMarkup = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (markup === this.#previousMarkup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bannedTags = JSON.parse(document.getElementById('babycode-banned-tags').value);
|
||||
this.#previousMarkup = markup;
|
||||
|
||||
const res = await this.api.getJSON(previewEndpoint, [], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
markup: markup,
|
||||
banned_tags: bannedTags,
|
||||
}),
|
||||
});
|
||||
if (res.error) {
|
||||
switch (res.error.status) {
|
||||
case 429:
|
||||
previewErrorsContainer.textContent = '(Old preview, try again in a few seconds.)'
|
||||
this.#previousMarkup = '';
|
||||
break;
|
||||
case 400:
|
||||
previewErrorsContainer.textContent = '(Request got malformed.)'
|
||||
break;
|
||||
case 401:
|
||||
previewErrorsContainer.textContent = '(You are not logged in.)'
|
||||
break;
|
||||
default:
|
||||
previewErrorsContainer.textContent = '(Error. Check console.)'
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
previewErrorsContainer.textContent = '';
|
||||
previewContainer.innerHTML = res.value.html;
|
||||
}
|
||||
}
|
||||
|
||||
insertBabycodeTag(ev, el) {
|
||||
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;
|
||||
let tagInsertStart = `[${tagStart}]${breakLine ? '\n' : ''}`;
|
||||
if (hasAttr) {
|
||||
tagEnd = tagEnd.slice(0, -1);
|
||||
}
|
||||
const tagInsertEnd = `${breakLine ? '\n' : ''}[/${tagEnd}]`;
|
||||
const hasSelection = el.selectionStart !== el.selectionEnd;
|
||||
const text = el.value;
|
||||
|
||||
if (hasSelection) {
|
||||
const realStart = Math.min(el.selectionStart, el.selectionEnd);
|
||||
const realEnd = Math.max(el.selectionStart, el.selectionEnd);
|
||||
const selectionLength = realEnd - realStart;
|
||||
|
||||
const strStart = text.slice(0, realStart);
|
||||
const strEnd = text.substring(realEnd);
|
||||
const frag = `${tagInsertStart}${text.slice(realStart, realEnd)}${tagInsertEnd}`;
|
||||
const reconst = `${strStart}${frag}${strEnd}`;
|
||||
el.value = reconst;
|
||||
if (!hasAttr) {
|
||||
el.setSelectionRange(realStart + tagInsertStart.length, realStart + tagInsertEnd.length + selectionLength - 1);
|
||||
} else {
|
||||
const attrCursor = realStart + tagInsertEnd.length - (1 + (breakLine ? 1 : 0))
|
||||
el.setSelectionRange(attrCursor, attrCursor); // cursor on attr
|
||||
}
|
||||
} else {
|
||||
if (hasAttr) {
|
||||
tagInsertStart += prefill;
|
||||
}
|
||||
const cursor = el.selectionStart;
|
||||
const strStart = text.slice(0, cursor);
|
||||
const strEnd = text.substr(cursor);
|
||||
|
||||
let newCursor = strStart.length + tagInsertStart.length;
|
||||
if (hasAttr) {
|
||||
newCursor = cursor + tagInsertStart.length - prefill.length - (1 + (breakLine ? 1 : 0)) //cursor on attr
|
||||
}
|
||||
const reconst = `${strStart}${tagInsertStart}${tagInsertEnd}${strEnd}`;
|
||||
el.value = reconst;
|
||||
el.setSelectionRange(newCursor, newCursor);
|
||||
}
|
||||
el.focus();
|
||||
}
|
||||
|
||||
addQuote(ev, el) {
|
||||
el.value += el.sender.value;
|
||||
el.scrollIntoView();
|
||||
el.focus();
|
||||
}
|
||||
|
||||
convertTimestamps(ev, el) {
|
||||
const timestamp = el.dsInt('utc');
|
||||
if (!isNaN(timestamp)) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
el.textContent = date.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
#currentUsername = undefined;
|
||||
async highlightMentions(ev, el) {
|
||||
if (this.#currentUsername === undefined) {
|
||||
const userInfo = await this.api.getJSON(userEndpoint);
|
||||
if (!userInfo.value) {
|
||||
return;
|
||||
}
|
||||
this.#currentUsername = userInfo.value.user.username;
|
||||
}
|
||||
|
||||
if (el.ds('username') === this.#currentUsername) {
|
||||
el.classList.add('me');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const timestampSpans = document.getElementsByClassName("timestamp");
|
||||
for (let timestampSpan of timestampSpans) {
|
||||
const timestamp = parseInt(timestampSpan.dataset.utc);
|
||||
if (!isNaN(timestamp)) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
timestampSpan.textContent = date.toLocaleString();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,13 +1,5 @@
|
||||
{
|
||||
const ta = document.getElementById("babycode-content");
|
||||
|
||||
for (let button of document.querySelectorAll(".reply-button")) {
|
||||
button.addEventListener("click", (e) => {
|
||||
ta.value += button.value;
|
||||
ta.scrollIntoView()
|
||||
ta.focus();
|
||||
})
|
||||
}
|
||||
|
||||
function supportsPopover() {
|
||||
return Object.hasOwn(HTMLElement.prototype, "popover");
|
||||
@@ -90,7 +82,6 @@
|
||||
quoteButton.textContent = "Quote fragment"
|
||||
quoteButton.className = "reduced"
|
||||
quotePopover.appendChild(quoteButton);
|
||||
|
||||
document.body.appendChild(quotePopover);
|
||||
return quoteButton;
|
||||
}
|
||||
@@ -106,7 +97,7 @@
|
||||
if (ta.value.trim() !== "") {
|
||||
ta.value += "\n"
|
||||
}
|
||||
ta.value += `[url=${postPermalink}]${authorUsername} said:[/url]\n[quote]< :scissors: > ${document.getSelection().toString()} < :scissors: >[/quote]\n`;
|
||||
ta.value += `@${authorUsername} [url=${postPermalink}]said:[/url]\n[quote]< :scissors: > ${document.getSelection().toString()} < :scissors: >[/quote]\n`;
|
||||
ta.scrollIntoView()
|
||||
ta.focus();
|
||||
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
function activateSelfDeactivateSibs(button) {
|
||||
if (button.classList.contains("active")) return;
|
||||
|
||||
Array.from(button.parentNode.children).forEach(s => {
|
||||
if (s === button){
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
s.classList.remove('active');
|
||||
}
|
||||
const targetId = s.dataset.targetId;
|
||||
const target = document.getElementById(targetId);
|
||||
|
||||
if (!target) return;
|
||||
|
||||
if (s.classList.contains('active')) {
|
||||
target.classList.add('active');
|
||||
target.dispatchEvent(new CustomEvent("tab-activated", {bubbles: false}))
|
||||
} else {
|
||||
target.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openLightbox(post, idx) {
|
||||
lightboxCurrentPost = post;
|
||||
lightboxCurrentIdx = idx;
|
||||
@@ -102,43 +79,6 @@ let lightboxCurrentPost = null;
|
||||
let lightboxCurrentIdx = -1;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// tabs
|
||||
document.querySelectorAll(".tab-button").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
activateSelfDeactivateSibs(button);
|
||||
});
|
||||
});
|
||||
|
||||
// accordions
|
||||
const handledAccordions = new Set();
|
||||
function attachAccordionHandlers(accordion){
|
||||
if(handledAccordions.has(accordion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handledAccordions.add(accordion)
|
||||
const header = accordion.querySelector(".accordion-header");
|
||||
const toggleButton = header.querySelector(".accordion-toggle");
|
||||
const content = accordion.querySelector(".accordion-content");
|
||||
|
||||
const toggle = (e) => {
|
||||
e.stopPropagation();
|
||||
accordion.classList.toggle("hidden");
|
||||
content.classList.toggle("hidden");
|
||||
toggleButton.textContent = content.classList.contains("hidden") ? "+" : "-"
|
||||
}
|
||||
|
||||
toggleButton.addEventListener("click", toggle);
|
||||
}
|
||||
|
||||
function refreshAccordions(){
|
||||
const accordions = document.querySelectorAll(".accordion");
|
||||
accordions.forEach(attachAccordionHandlers);
|
||||
}
|
||||
refreshAccordions()
|
||||
|
||||
document.body.addEventListener('refresh_accordions', refreshAccordions)
|
||||
|
||||
//lightboxes
|
||||
lightboxObj = constructLightbox();
|
||||
document.body.appendChild(lightboxObj.dialog);
|
||||
@@ -192,13 +132,4 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
image.addEventListener("load", () => setImageMaxSize(image));
|
||||
}
|
||||
})
|
||||
|
||||
// copy code blocks
|
||||
for (let button of document.querySelectorAll(".copy-code")) {
|
||||
button.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(button.value)
|
||||
button.textContent = "Copied!"
|
||||
setTimeout(() => {button.textContent = "Copy"}, 1000.0)
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
1
data/static/js/vnd/bitty-5.1.0-rc6.min.js
vendored
1
data/static/js/vnd/bitty-5.1.0-rc6.min.js
vendored
File diff suppressed because one or more lines are too long
1
data/static/js/vnd/bitty-7.0.0-rc1.min.js
vendored
Normal file
1
data/static/js/vnd/bitty-7.0.0-rc1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -51,7 +51,7 @@ $BIGGER_PADDING: 30px !default;
|
||||
|
||||
$PAGE_SIDE_MARGIN: 100px !default;
|
||||
|
||||
$SETTINGS_WIDTH: 40% !default;
|
||||
$SETTINGS_WIDTH: 85% !default;
|
||||
|
||||
// **************
|
||||
// BORDERS
|
||||
@@ -95,14 +95,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;
|
||||
@@ -154,7 +172,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;
|
||||
@@ -188,6 +206,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;
|
||||
@@ -340,6 +364,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;
|
||||
@@ -444,7 +472,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;
|
||||
@@ -475,12 +503,12 @@ $inline_code_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
$dialogs-padding: $ZERO_PADDING !default;
|
||||
$dialogs-border-radius: $DEFAULT_BORDER_RADIUS !default;
|
||||
$dialogs_padding: $ZERO_PADDING !default;
|
||||
$dialogs_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||
$dialog_border: 2px solid black !default;
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
padding: $dialogs-padding;
|
||||
border-radius: $dialogs-border-radius;
|
||||
padding: $dialogs_padding;
|
||||
border-radius: $dialogs_border_radius;
|
||||
border: $dialog_border;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
@@ -684,16 +712,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;
|
||||
@@ -768,6 +801,10 @@ $thread_locked_background: none !default;
|
||||
}
|
||||
}
|
||||
|
||||
svg.icon {
|
||||
// pointer-events: none;
|
||||
}
|
||||
|
||||
$post_img_container_gap: $SMALL_PADDING !default;
|
||||
.post-img-container {
|
||||
display: flex;
|
||||
@@ -824,24 +861,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";
|
||||
@@ -852,21 +889,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 {
|
||||
@@ -1016,7 +1053,6 @@ $post_editing_context_margin: $BIG_PADDING $ZERO_PADDING !default;
|
||||
|
||||
.babycode-editor {
|
||||
height: 150px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.babycode-editor-container {
|
||||
@@ -1069,6 +1105,15 @@ ul, ol {
|
||||
padding: $list_padding;
|
||||
}
|
||||
|
||||
$horizontal_list_margin_left: 0 !default;
|
||||
ul.horizontal, ol.horizontal {
|
||||
display: inline;
|
||||
margin-left: $horizontal_list_margin_left;
|
||||
& li {
|
||||
display: inline list-item;
|
||||
}
|
||||
}
|
||||
|
||||
.new-concept-notification.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -1100,10 +1145,9 @@ $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;
|
||||
overflow: hidden; // for border-radius clipping
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1225,9 +1269,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 {
|
||||
@@ -1238,7 +1282,7 @@ $bookmarks_dropdown_background_color: $ACCENT_COLOR !default;
|
||||
$bookmarks_dropdown_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||
$bookmarks_dropdown_border: $button_border !default;
|
||||
$bookmarks_dropdown_shadow: 0 0 30px rgba(0, 0, 0, 0.25) !default;
|
||||
$bookmarks_dropdown_min_width: 400px !default;
|
||||
$bookmarks_dropdown_width: 400px !default;
|
||||
$bookmarks_dropdown_padding: $MEDIUM_PADDING !default;
|
||||
.bookmarks-dropdown {
|
||||
background-color: $bookmarks_dropdown_background_color;
|
||||
@@ -1246,10 +1290,12 @@ $bookmarks_dropdown_padding: $MEDIUM_PADDING !default;
|
||||
border-radius: $bookmarks_dropdown_border_radius;
|
||||
box-shadow: $bookmarks_dropdown_shadow;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: $bookmarks_dropdown_min_width;
|
||||
margin: 0;
|
||||
min-width: $bookmarks_dropdown_width;
|
||||
max-width: $bookmarks_dropdown_width;
|
||||
padding: $bookmarks_dropdown_padding;
|
||||
z-index: 100;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
$bookmark_dropdown_item_padding: $MEDIUM_PADDING 0 !default;
|
||||
@@ -1260,9 +1306,10 @@ $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;
|
||||
padding: $bookmark_dropdown_item_padding;
|
||||
margin: $bookmark_dropdown_item_margin;
|
||||
cursor: pointer;
|
||||
@@ -1276,11 +1323,13 @@ $bookmark_dropdown_item_icon_padding: 0 $MEDIUM_PADDING !default;
|
||||
}
|
||||
|
||||
&::before {
|
||||
// TODO: un-inline this once the bitty bug is fixed
|
||||
content: 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');
|
||||
content: '';
|
||||
background-color: currentColor;
|
||||
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: $bookmark_dropdown_item_icon_size;
|
||||
height: $bookmark_dropdown_item_icon_size;
|
||||
padding: $bookmark_dropdown_item_icon_padding;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@@ -1290,7 +1339,7 @@ $bookmark_dropdown_item_icon_padding: 0 $MEDIUM_PADDING !default;
|
||||
}
|
||||
|
||||
&::before{
|
||||
content: 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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E');
|
||||
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%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E') center/contain no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1300,8 +1349,107 @@ $bookmark_dropdown_item_icon_padding: 0 $MEDIUM_PADDING !default;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
$bookmark_dropdown_item_stats_padding: 0 $MEDIUM_PADDING !default;
|
||||
.bookmark-dropdown-item-stats {
|
||||
padding: $bookmark_dropdown_item_stats_padding;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
$bookmark_dropdown_items_container_max_height: 300px !default;
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: $bookmark_dropdown_items_container_max_height;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
$motd_background_color: $ACCENT_COLOR !default;
|
||||
$motd_border: 2px outset $LIGHT !default;
|
||||
$motd_padding: $MEDIUM_PADDING $MEDIUM_BIG_PADDING !default;
|
||||
$motd_margin: $MEDIUM_PADDING 0 !default;
|
||||
.motd {
|
||||
display: flex;
|
||||
background-color: $motd_background_color;
|
||||
border: $motd_border;
|
||||
padding: $motd_padding;
|
||||
margin: $motd_margin;
|
||||
}
|
||||
|
||||
$motd_icon_min_width: 80px !default;
|
||||
$motd_icon_padding_right: $MEDIUM_BIG_PADDING !default;
|
||||
.motd-icon-container {
|
||||
display: flex;
|
||||
min-width: $motd_icon_min_width;
|
||||
padding-right: $motd_icon_padding_right;
|
||||
}
|
||||
|
||||
$motd_content_padding_right: 25% !default;
|
||||
.motd-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
flex-grow: 1;
|
||||
padding-right: $motd_content_padding_right;
|
||||
}
|
||||
|
||||
.motd-title {
|
||||
font-weight: bold;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
$mention_font_color: $DEFAULT_FONT_COLOR_INVERSE !default;
|
||||
$mention_font_color_hover: $DEFAULT_FONT_COLOR !default;
|
||||
$mention_background_color: $DARK_2 !default;
|
||||
$mention_background_color_me: color.adjust($DARK_2, $hue: 90) !default;
|
||||
$mention_background_color_hover: $LIGHT_2 !default;
|
||||
$mention_border_me: 1px dashed;
|
||||
$mention_padding: $SMALL_PADDING !default;
|
||||
$mention_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||
a.mention, a.mention:visited {
|
||||
display: inline-block;
|
||||
color: $mention_font_color;
|
||||
background-color: $mention_background_color;
|
||||
padding: $mention_padding;
|
||||
border-radius: $mention_border_radius;
|
||||
text-decoration: none;
|
||||
|
||||
&.display {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
}
|
||||
|
||||
&.me {
|
||||
background-color: $mention_background_color_me;
|
||||
border: $mention_border_me;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.hfc {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ $dark_accent: #231c23;
|
||||
$warn: #eaea6a;
|
||||
$crit: #d53232;
|
||||
|
||||
$br: 8px;
|
||||
|
||||
@use 'default' with (
|
||||
$ACCENT_COLOR: #9b649b,
|
||||
|
||||
@@ -30,6 +32,8 @@ $crit: #d53232;
|
||||
$BUTTON_CRITICAL_FONT_COLOR: $fc,
|
||||
$ACCORDION_COLOR: #7d467d,
|
||||
|
||||
$DEFAULT_BORDER_RADIUS: $br,
|
||||
|
||||
$bottomnav_color: $dark_accent,
|
||||
|
||||
$topic_info_background: $dark_accent,
|
||||
@@ -49,13 +53,14 @@ $crit: #d53232;
|
||||
$post_content_background: $dark_accent,
|
||||
|
||||
$thread_info_background_color: $dark_accent,
|
||||
$motd_background_color: $lightish_accent,
|
||||
|
||||
$post_reactions_background: $lightish_accent,
|
||||
|
||||
$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,
|
||||
@@ -73,6 +78,10 @@ $crit: #d53232;
|
||||
|
||||
$tab_content_background: $lightish_accent,
|
||||
$tab_button_active_color: #8a5584,
|
||||
|
||||
$bookmarks_dropdown_background_color: $lightish_accent,
|
||||
|
||||
$mention_font_color: $fc,
|
||||
);
|
||||
|
||||
#topnav {
|
||||
@@ -80,6 +89,25 @@ $crit: #d53232;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
margin-top: 10px;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.infobox, .motd {
|
||||
border-radius: $br;
|
||||
}
|
||||
|
||||
.thread-sticky-container {
|
||||
border-top-left-radius: $br;
|
||||
border-bottom-left-radius: $br;
|
||||
}
|
||||
|
||||
.thread-locked-container {
|
||||
border-top-right-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ $br: 16px;
|
||||
$usercard_border: none,
|
||||
$usercard_border_right: none,
|
||||
$thread_locked_border: 1px solid black,
|
||||
$motd_border: 1px solid black,
|
||||
|
||||
$SETTINGS_WIDTH: 60%,
|
||||
$PAGE_SIDE_MARGIN: 50px,
|
||||
|
||||
$link_color: black,
|
||||
@@ -63,6 +63,8 @@ $br: 16px;
|
||||
|
||||
$pagebutton_min_width: 36px,
|
||||
$quote_background_color: #0002,
|
||||
|
||||
$bookmark_dropdown_item_icon_padding: 0 24px,
|
||||
);
|
||||
|
||||
#topnav {
|
||||
@@ -87,11 +89,3 @@ footer {
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
color: white;
|
||||
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user