Compare commits
46 Commits
f08c60de75
...
main
Author | SHA1 | Date | |
---|---|---|---|
6cfc862d63
|
|||
70646ba381
|
|||
f04f0fb51b
|
|||
317182ae12
|
|||
751be27b52
|
|||
6dd9f5bf65
|
|||
1f80ed7ca5
|
|||
89817340c9
|
|||
fc80823713
|
|||
184472726e
|
|||
68cf5f7d57
|
|||
4ef7b0ba1e
|
|||
aaeb3a524b
|
|||
f1f62fa2c8
|
|||
8c917f6ae2
|
|||
4f88d14b45
|
|||
9238385244
|
|||
cf89070639
|
|||
4a8f87d64a
|
|||
2b1f52a99d
|
|||
d0b702e1e8
|
|||
14b96bf37e
|
|||
cf4bf3caa3
|
|||
382080ceaa
|
|||
304a862931
|
|||
348b782350
|
|||
aec4724e2f
|
|||
53d39d5a36
|
|||
05bd034b23
|
|||
033df03c49
|
|||
a0c86f33b4
|
|||
712782bc1c
|
|||
1c80777fe4
|
|||
4c2877403d
|
|||
cf2d605077
|
|||
c68ead85c0
|
|||
b0fd2a4f0c
|
|||
a529c1db65
|
|||
acac6ed778
|
|||
3699daa44a
|
|||
4bdd01569c
|
|||
33dc52342a
|
|||
d3f63c4120
|
|||
d36e94127e
|
|||
e33d26c6dc
|
|||
7702384c40
|
@ -1,11 +1,11 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.13-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
nginx \
|
nginx \
|
||||||
uwsgi \
|
uwsgi \
|
||||||
uwsgi-plugin-python3 \
|
uwsgi-plugin-python3 \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
libargon2-0 \
|
libargon2-1 \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
@ -25,9 +25,16 @@ Designers: Paul James Miller
|
|||||||
|
|
||||||
## ICONCINO
|
## ICONCINO
|
||||||
|
|
||||||
Affected files: [`data/static/misc/error.svg`](./data/static/misc/error.svg) [`data/static/misc/image.svg`](./data/static/misc/image.svg) [`data/static/misc/info.svg`](./data/static/misc/info.svg) [`data/static/misc/lock.svg`](./data/static/misc/lock.svg) [`data/static/misc/sticky.svg`](./data/static/misc/sticky.svg) [`data/static/misc/warn.svg`](./data/static/misc/warn.svg)
|
Affected files: [`data/static/misc/error.svg`](./data/static/misc/error.svg) [`data/static/misc/image.svg`](./data/static/misc/image.svg) [`data/static/misc/info.svg`](./data/static/misc/info.svg) [`data/static/misc/lock.svg`](./data/static/misc/lock.svg) [`data/static/misc/spoiler.svg`](./data/static/misc/spoiler.svg) [`data/static/misc/sticky.svg`](./data/static/misc/sticky.svg) [`data/static/misc/warn.svg`](./data/static/misc/warn.svg)
|
||||||
URL: https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license
|
URL: https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license
|
||||||
Copyright: Gabriele Malaspina
|
Copyright: Gabriele Malaspina
|
||||||
Designers: Gabriele Malaspina
|
Designers: Gabriele Malaspina
|
||||||
License: CC0 1.0/CC BY 4.0
|
License: CC0 1.0/CC BY 4.0
|
||||||
CC BY 4.0 compliance: Modified to indicate the URL. Modified size.
|
CC BY 4.0 compliance: Modified to indicate the URL. Modified size.
|
||||||
|
|
||||||
|
## Forumoji
|
||||||
|
|
||||||
|
Affected files: everything in [`data/static/emoji`](./data/static/emoji) except [`data/static/emoji/scissors.png`](data/static/emoji/scissors.png)
|
||||||
|
URL: https://gh.vercte.net/forumoji/
|
||||||
|
License: CC0 1.0
|
||||||
|
Designers: lolecksdeehaha; Scratch137; 64lu; stickfiregames; mybearworld (the project has many more contributors, but these are the people whose designs were reproduced here)
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
from flask import Flask, session
|
from flask import Flask, session
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from .models import Avatars, Users
|
from .models import Avatars, Users, PostHistory, Posts
|
||||||
from .auth import digest
|
from .auth import digest
|
||||||
from .routes.users import is_logged_in, get_active_user
|
from .routes.users import is_logged_in, get_active_user, get_prefers_theme
|
||||||
|
from .routes.threads import get_post_url
|
||||||
from .constants import (
|
from .constants import (
|
||||||
PermissionLevel, permission_level_string,
|
PermissionLevel, permission_level_string,
|
||||||
InfoboxKind, InfoboxIcons, InfoboxHTMLClass
|
InfoboxKind, InfoboxIcons, InfoboxHTMLClass,
|
||||||
|
REACTION_EMOJI,
|
||||||
)
|
)
|
||||||
from .lib.babycode import babycode_to_html, EMOJI
|
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import secrets
|
import secrets
|
||||||
|
import tomllib
|
||||||
|
|
||||||
def create_default_avatar():
|
def create_default_avatar():
|
||||||
if Avatars.count() == 0:
|
if Avatars.count() == 0:
|
||||||
@ -45,8 +48,26 @@ def create_deleted_user():
|
|||||||
"permission": PermissionLevel.SYSTEM.value,
|
"permission": PermissionLevel.SYSTEM.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def reparse_posts():
|
||||||
|
from .db import db
|
||||||
|
post_histories = PostHistory.findall([
|
||||||
|
('markup_language', '=', 'babycode'),
|
||||||
|
('format_version', 'IS NOT', BABYCODE_VERSION)
|
||||||
|
])
|
||||||
|
if len(post_histories) == 0:
|
||||||
|
return
|
||||||
|
print('Re-parsing babycode, this may take a while...')
|
||||||
|
with db.transaction():
|
||||||
|
for ph in post_histories:
|
||||||
|
ph.update({
|
||||||
|
'content': babycode_to_html(ph['original_markup']),
|
||||||
|
'format_version': BABYCODE_VERSION,
|
||||||
|
})
|
||||||
|
print('Re-parsing done.')
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
|
||||||
|
|
||||||
if os.getenv("PYROM_PROD") is None:
|
if os.getenv("PYROM_PROD") is None:
|
||||||
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
|
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
|
||||||
@ -62,6 +83,18 @@ def create_app():
|
|||||||
app.config['MAX_CONTENT_LENGTH'] = 1000 * 1000
|
app.config['MAX_CONTENT_LENGTH'] = 1000 * 1000
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
|
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
|
||||||
|
|
||||||
|
css_dir = 'data/static/css/'
|
||||||
|
allowed_themes = []
|
||||||
|
for f in os.listdir(css_dir):
|
||||||
|
if not os.path.isfile(os.path.join(css_dir, f)):
|
||||||
|
continue
|
||||||
|
theme_name = os.path.splitext(os.path.basename(f))[0]
|
||||||
|
allowed_themes.append(theme_name)
|
||||||
|
|
||||||
|
allowed_themes.sort(key=(lambda x: (x != 'style', x)))
|
||||||
|
app.config['allowed_themes'] = allowed_themes
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
from .schema import create as create_tables
|
from .schema import create as create_tables
|
||||||
from .migrations import run_migrations
|
from .migrations import run_migrations
|
||||||
@ -72,6 +105,8 @@ def create_app():
|
|||||||
create_admin()
|
create_admin()
|
||||||
create_deleted_user()
|
create_deleted_user()
|
||||||
|
|
||||||
|
reparse_posts()
|
||||||
|
|
||||||
from app.routes.app import bp as app_bp
|
from app.routes.app import bp as app_bp
|
||||||
from app.routes.topics import bp as topics_bp
|
from app.routes.topics import bp as topics_bp
|
||||||
from app.routes.threads import bp as threads_bp
|
from app.routes.threads import bp as threads_bp
|
||||||
@ -106,12 +141,20 @@ def create_app():
|
|||||||
"PermissionLevel": PermissionLevel,
|
"PermissionLevel": PermissionLevel,
|
||||||
"__commit": commit,
|
"__commit": commit,
|
||||||
"__emoji": EMOJI,
|
"__emoji": EMOJI,
|
||||||
|
"REACTION_EMOJI": REACTION_EMOJI,
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_auth():
|
def inject_auth():
|
||||||
return {"is_logged_in": is_logged_in, "get_active_user": get_active_user, "active_user": get_active_user()}
|
return {"is_logged_in": is_logged_in, "get_active_user": get_active_user, "active_user": get_active_user()}
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_funcs():
|
||||||
|
return {
|
||||||
|
'get_post_url': get_post_url,
|
||||||
|
'get_prefers_theme': get_prefers_theme,
|
||||||
|
}
|
||||||
|
|
||||||
@app.template_filter("ts_datetime")
|
@app.template_filter("ts_datetime")
|
||||||
def ts_datetime(ts, format):
|
def ts_datetime(ts, format):
|
||||||
return datetime.utcfromtimestamp(ts or int(time.time())).strftime(format)
|
return datetime.utcfromtimestamp(ts or int(time.time())).strftime(format)
|
||||||
@ -141,4 +184,18 @@ def create_app():
|
|||||||
for id_, text in matches
|
for id_, text in matches
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# this only happens at build time but
|
||||||
|
# build time is when updates are done anyway
|
||||||
|
# sooo... /shrug
|
||||||
|
@app.template_filter('cachebust')
|
||||||
|
def cachebust(subject):
|
||||||
|
return f"{subject}?v={str(int(time.time()))}"
|
||||||
|
|
||||||
|
@app.template_filter('theme_name')
|
||||||
|
def get_theme_name(subject: str):
|
||||||
|
if subject == 'style':
|
||||||
|
return 'Default'
|
||||||
|
|
||||||
|
return f'{subject.removeprefix('theme-').capitalize()} (beta)'
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
@ -15,6 +15,38 @@ PermissionLevelString = {
|
|||||||
PermissionLevel.ADMIN: 'Administrator',
|
PermissionLevel.ADMIN: 'Administrator',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REACTION_EMOJI = [
|
||||||
|
'smile',
|
||||||
|
'grin',
|
||||||
|
|
||||||
|
'neutral',
|
||||||
|
|
||||||
|
'wink',
|
||||||
|
|
||||||
|
'frown',
|
||||||
|
'angry',
|
||||||
|
|
||||||
|
'think',
|
||||||
|
|
||||||
|
'sob',
|
||||||
|
|
||||||
|
'surprised',
|
||||||
|
|
||||||
|
'smiletear',
|
||||||
|
|
||||||
|
'tongue',
|
||||||
|
|
||||||
|
'pensive',
|
||||||
|
'weary',
|
||||||
|
|
||||||
|
'imp',
|
||||||
|
'impangry',
|
||||||
|
|
||||||
|
'lobster',
|
||||||
|
|
||||||
|
'scissors',
|
||||||
|
]
|
||||||
|
|
||||||
def permission_level_string(perm):
|
def permission_level_string(perm):
|
||||||
return PermissionLevelString[PermissionLevel(int(perm))]
|
return PermissionLevelString[PermissionLevel(int(perm))]
|
||||||
|
|
||||||
|
40
app/db.py
40
app/db.py
@ -55,7 +55,7 @@ class DB:
|
|||||||
|
|
||||||
def insert(self, table, columns, *values):
|
def insert(self, table, columns, *values):
|
||||||
if isinstance(columns, (list, tuple)):
|
if isinstance(columns, (list, tuple)):
|
||||||
columns = ", ".join(columns)
|
columns = ", ".join([f'"{column}"' for column in columns])
|
||||||
|
|
||||||
placeholders = ", ".join(["?"] * len(values))
|
placeholders = ", ".join(["?"] * len(values))
|
||||||
sql = f"""
|
sql = f"""
|
||||||
@ -89,6 +89,9 @@ class DB:
|
|||||||
self.table = table
|
self.table = table
|
||||||
self._where = [] # list of tuples
|
self._where = [] # list of tuples
|
||||||
self._select = "*"
|
self._select = "*"
|
||||||
|
self._group_by = ""
|
||||||
|
self._order_by = ""
|
||||||
|
self._order_asc = True
|
||||||
|
|
||||||
|
|
||||||
def _build_where(self):
|
def _build_where(self):
|
||||||
@ -104,6 +107,17 @@ class DB:
|
|||||||
return " WHERE " + " AND ".join(conditions), params
|
return " WHERE " + " AND ".join(conditions), params
|
||||||
|
|
||||||
|
|
||||||
|
def group_by(self, stmt):
|
||||||
|
self._group_by = stmt
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def order_by(self, stmt, asc = True):
|
||||||
|
self._order_by = stmt
|
||||||
|
self._order_asc = asc
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
def select(self, columns = "*"):
|
def select(self, columns = "*"):
|
||||||
self._select = columns
|
self._select = columns
|
||||||
return self
|
return self
|
||||||
@ -122,7 +136,16 @@ class DB:
|
|||||||
def build_select(self):
|
def build_select(self):
|
||||||
sql = f"SELECT {self._select} FROM {self.table}"
|
sql = f"SELECT {self._select} FROM {self.table}"
|
||||||
where_clause, params = self._build_where()
|
where_clause, params = self._build_where()
|
||||||
return sql + where_clause, params
|
|
||||||
|
stmt = sql + where_clause
|
||||||
|
|
||||||
|
if self._group_by:
|
||||||
|
stmt += " GROUP BY " + self._group_by
|
||||||
|
|
||||||
|
if self._order_by:
|
||||||
|
stmt += " ORDER BY " + self._order_by + (" ASC" if self._order_asc else " DESC")
|
||||||
|
|
||||||
|
return stmt, params
|
||||||
|
|
||||||
|
|
||||||
def build_update(self, data):
|
def build_update(self, data):
|
||||||
@ -178,6 +201,19 @@ class Model:
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def findall(cls, condition, operator='='):
|
||||||
|
rows = db.QueryBuilder(cls.table)\
|
||||||
|
.where(condition, operator)\
|
||||||
|
.all()
|
||||||
|
res = []
|
||||||
|
for row in rows:
|
||||||
|
instance = cls(cls.table)
|
||||||
|
instance._data = dict(row)
|
||||||
|
res.append(instance)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, values):
|
def create(cls, values):
|
||||||
if not values:
|
if not values:
|
||||||
|
@ -1,14 +1,63 @@
|
|||||||
from .babycode_parser import Parser
|
from .babycode_parser import Parser
|
||||||
from markupsafe import escape
|
from markupsafe import Markup, escape
|
||||||
import re
|
import re
|
||||||
|
|
||||||
def tag_code(children, attr):
|
BABYCODE_VERSION = 3
|
||||||
|
|
||||||
|
NAMED_COLORS = [
|
||||||
|
'black', 'silver', 'gray', 'white', 'maroon', 'red',
|
||||||
|
'purple', 'fuchsia', 'green', 'lime', 'olive', 'yellow',
|
||||||
|
'navy', 'blue', 'teal', 'aqua', 'aliceblue', 'antiquewhite',
|
||||||
|
'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black',
|
||||||
|
'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue',
|
||||||
|
'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson',
|
||||||
|
'cyan', 'aqua', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray',
|
||||||
|
'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange',
|
||||||
|
'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray',
|
||||||
|
'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray',
|
||||||
|
'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia',
|
||||||
|
'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green',
|
||||||
|
'greenyellow', 'grey', 'gray', 'honeydew', 'hotpink', 'indianred',
|
||||||
|
'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen',
|
||||||
|
'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray',
|
||||||
|
'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue',
|
||||||
|
'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen',
|
||||||
|
'linen', 'magenta', 'fuchsia', 'maroon', 'mediumaquamarine', 'mediumblue',
|
||||||
|
'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
|
||||||
|
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite',
|
||||||
|
'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered',
|
||||||
|
'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip',
|
||||||
|
'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple',
|
||||||
|
'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
|
||||||
|
'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue',
|
||||||
|
'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue',
|
||||||
|
'tan', 'teal', 'thistle', 'tomato', 'transparent', 'turquoise',
|
||||||
|
'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen',
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_tag(e, tag=None):
|
||||||
|
if e is None:
|
||||||
|
return False
|
||||||
|
if isinstance(e, str):
|
||||||
|
return False
|
||||||
|
if e['type'] != 'bbcode':
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tag is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return e['name'] == tag
|
||||||
|
|
||||||
|
def is_text(e):
|
||||||
|
return isinstance(e, str)
|
||||||
|
|
||||||
|
def tag_code(children, attr, surrounding):
|
||||||
is_inline = children.find('\n') == -1
|
is_inline = children.find('\n') == -1
|
||||||
if is_inline:
|
if is_inline:
|
||||||
return f"<code class=\"inline-code\">{children}</code>"
|
return f"<code class=\"inline-code\">{children}</code>"
|
||||||
else:
|
else:
|
||||||
t = children.strip()
|
t = children.strip()
|
||||||
button = f"<button type=button class=\"copy-code\" value={t}>Copy</button>"
|
button = f"<button type=button class=\"copy-code\" value=\"{t}\">Copy</button>"
|
||||||
return f"<pre><span class=\"copy-code-container\">{button}</span><code>{t}</code></pre>"
|
return f"<pre><span class=\"copy-code-container\">{button}</span><code>{t}</code></pre>"
|
||||||
|
|
||||||
def tag_list(children):
|
def tag_list(children):
|
||||||
@ -16,16 +65,62 @@ def tag_list(children):
|
|||||||
list_body = re.sub(r"\n\n+", "\1", list_body)
|
list_body = re.sub(r"\n\n+", "\1", list_body)
|
||||||
return " ".join([f"<li>{x}</li>" for x in list_body.split("\1") if x])
|
return " ".join([f"<li>{x}</li>" for x in list_body.split("\1") if x])
|
||||||
|
|
||||||
|
def tag_color(children, attr, surrounding):
|
||||||
|
hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
|
||||||
|
potential_color = attr.lower().strip()
|
||||||
|
|
||||||
|
if potential_color in NAMED_COLORS:
|
||||||
|
return f"<span style='color: {potential_color};'>{children}</span>"
|
||||||
|
|
||||||
|
m = re.match(hex_re, potential_color)
|
||||||
|
if m:
|
||||||
|
return f"<span style='color: #{m.group(1)};'>{children}</span>"
|
||||||
|
|
||||||
|
# return just the way it was if we can't parse it
|
||||||
|
return f"[color={attr}]{children}[/color]"
|
||||||
|
|
||||||
|
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>"""
|
||||||
|
return container
|
||||||
|
|
||||||
|
def tag_image(children, attr, surrounding):
|
||||||
|
img = f"<img class=\"post-image\" src=\"{attr}\" alt=\"{children}\">"
|
||||||
|
if not is_tag(surrounding[0], 'img'):
|
||||||
|
img = f"<div class=post-img-container>{img}"
|
||||||
|
if not is_tag(surrounding[1], 'img'):
|
||||||
|
img = f"{img}</div>"
|
||||||
|
return img
|
||||||
|
|
||||||
TAGS = {
|
TAGS = {
|
||||||
"b": lambda children, attr: f"<strong>{children}</strong>",
|
"b": lambda children, attr, _: f"<strong>{children}</strong>",
|
||||||
"i": lambda children, attr: f"<em>{children}</em>",
|
"i": lambda children, attr, _: f"<em>{children}</em>",
|
||||||
"s": lambda children, attr: f"<del>{children}</del>",
|
"s": lambda children, attr, _: f"<del>{children}</del>",
|
||||||
"img": lambda children, attr: f"<div class=\"post-img-container\"><img class=\"block-img\" src=\"{attr}\" alt=\"{children}\"></div>",
|
"u": lambda children, attr, _: f"<u>{children}</u>",
|
||||||
"url": lambda children, attr: f"<a href={attr}>{children}</a>",
|
|
||||||
"quote": lambda children, attr: f"<blockquote>{children}</blockquote>",
|
"img": tag_image,
|
||||||
|
"url": lambda children, attr, _: f"<a href={attr}>{children}</a>",
|
||||||
|
"quote": lambda children, attr, _: f"<blockquote>{children}</blockquote>",
|
||||||
"code": tag_code,
|
"code": tag_code,
|
||||||
"ul": lambda children, attr: f"<ul>{tag_list(children)}</ul>",
|
"ul": lambda children, attr, _: f"<ul>{tag_list(children)}</ul>",
|
||||||
"ol": lambda children, attr: f"<ol>{tag_list(children)}</ol>",
|
"ol": lambda children, attr, _: f"<ol>{tag_list(children)}</ol>",
|
||||||
|
|
||||||
|
"big": lambda children, attr, _: f"<span style='font-size: 2rem;'>{children}</span>",
|
||||||
|
"small": lambda children, attr, _: f"<span style='font-size: 0.75rem;'>{children}</span>",
|
||||||
|
"color": tag_color,
|
||||||
|
|
||||||
|
"center": lambda children, attr, _: f"<div style='text-align: center;'>{children}</div>",
|
||||||
|
"right": lambda children, attr, _: f"<div style='text-align: right;'>{children}</div>",
|
||||||
|
|
||||||
|
"spoiler": tag_spoiler,
|
||||||
|
}
|
||||||
|
|
||||||
|
# [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().
|
||||||
|
INLINE_TAGS = {
|
||||||
|
'b', 'i', 's', 'u', 'color', 'big', 'small', 'url'
|
||||||
}
|
}
|
||||||
|
|
||||||
def make_emoji(name, code):
|
def make_emoji(name, code):
|
||||||
@ -49,6 +144,8 @@ EMOJI = {
|
|||||||
|
|
||||||
'pensive': make_emoji('pensive', 'pensive'),
|
'pensive': make_emoji('pensive', 'pensive'),
|
||||||
|
|
||||||
|
'scissors': make_emoji('scissors', 'scissors'),
|
||||||
|
|
||||||
')': make_emoji('smile', ')'),
|
')': make_emoji('smile', ')'),
|
||||||
|
|
||||||
'smiletear': make_emoji('smiletear', 'smiletear'),
|
'smiletear': make_emoji('smiletear', 'smiletear'),
|
||||||
@ -82,6 +179,33 @@ def break_lines(text):
|
|||||||
text = re.sub(r"\n\n+", "<br><br>", text)
|
text = re.sub(r"\n\n+", "<br><br>", text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
def is_inline(e):
|
||||||
|
if e is None:
|
||||||
|
return False # i think
|
||||||
|
|
||||||
|
if is_text(e):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if is_tag(e):
|
||||||
|
if is_tag(e, 'code'): # special case, since [code] can be inline OR block
|
||||||
|
return '\n' not in e['children']
|
||||||
|
|
||||||
|
return e['name'] in INLINE_TAGS
|
||||||
|
|
||||||
|
return e['type'] != 'rule'
|
||||||
|
|
||||||
|
def should_collapse(text, surrounding):
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not text.strip() and '\n' not in text:
|
||||||
|
return not is_inline(surrounding[0]) and not is_inline(surrounding[1])
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def babycode_to_html(s):
|
def babycode_to_html(s):
|
||||||
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
||||||
parser = Parser(subj)
|
parser = Parser(subj)
|
||||||
@ -89,10 +213,19 @@ def babycode_to_html(s):
|
|||||||
parser.bbcode_tags_only_text_children = TEXT_ONLY
|
parser.bbcode_tags_only_text_children = TEXT_ONLY
|
||||||
parser.valid_emotes = EMOJI.keys()
|
parser.valid_emotes = EMOJI.keys()
|
||||||
|
|
||||||
elements = parser.parse()
|
uncollapsed = parser.parse()
|
||||||
print(elements)
|
elements = []
|
||||||
|
for i in range(len(uncollapsed)):
|
||||||
|
e = uncollapsed[i]
|
||||||
|
surrounding = (
|
||||||
|
uncollapsed[i - 1] if i-1 >= 0 else None,
|
||||||
|
uncollapsed[i + 1] if i+1 < len(uncollapsed) else None
|
||||||
|
)
|
||||||
|
if not should_collapse(e, surrounding):
|
||||||
|
elements.append(e)
|
||||||
|
|
||||||
out = ""
|
out = ""
|
||||||
def fold(element, nobr):
|
def fold(element, nobr, surrounding):
|
||||||
if isinstance(element, str):
|
if isinstance(element, str):
|
||||||
if nobr:
|
if nobr:
|
||||||
return element
|
return element
|
||||||
@ -101,10 +234,15 @@ def babycode_to_html(s):
|
|||||||
match element['type']:
|
match element['type']:
|
||||||
case "bbcode":
|
case "bbcode":
|
||||||
c = ""
|
c = ""
|
||||||
for child in element['children']:
|
for i in range(len(element['children'])):
|
||||||
|
child = element['children'][i]
|
||||||
|
_surrounding = (
|
||||||
|
element['children'][i - 1] if i-1 >= 0 else None,
|
||||||
|
element['children'][i + 1] if i+1 < len(element['children']) else None
|
||||||
|
)
|
||||||
_nobr = element['name'] == "code" or element['name'] == "ul" or element['name'] == "ol"
|
_nobr = element['name'] == "code" or element['name'] == "ul" or element['name'] == "ol"
|
||||||
c = c + fold(child, _nobr)
|
c = c + Markup(fold(child, _nobr, _surrounding))
|
||||||
res = TAGS[element['name']](c, element['attr'])
|
res = TAGS[element['name']](c, element['attr'], surrounding)
|
||||||
return res
|
return res
|
||||||
case "link":
|
case "link":
|
||||||
return f"<a href=\"{element['url']}\">{element['url']}</a>"
|
return f"<a href=\"{element['url']}\">{element['url']}</a>"
|
||||||
@ -112,6 +250,12 @@ def babycode_to_html(s):
|
|||||||
return EMOJI[element['name']]
|
return EMOJI[element['name']]
|
||||||
case "rule":
|
case "rule":
|
||||||
return "<hr>"
|
return "<hr>"
|
||||||
for e in elements:
|
|
||||||
out = out + fold(e, False)
|
for i in range(len(elements)):
|
||||||
|
e = elements[i]
|
||||||
|
surrounding = (
|
||||||
|
elements[i - 1] if i-1 >= 0 else None,
|
||||||
|
elements[i + 1] if i+1 < len(elements) else None
|
||||||
|
)
|
||||||
|
out = out + fold(e, False, surrounding)
|
||||||
return out
|
return out
|
||||||
|
@ -4,7 +4,7 @@ import re
|
|||||||
|
|
||||||
PAT_EMOTE = r"[^\s:]"
|
PAT_EMOTE = r"[^\s:]"
|
||||||
PAT_BBCODE_TAG = r"\w"
|
PAT_BBCODE_TAG = r"\w"
|
||||||
PAT_BBCODE_ATTR = r"[^\s\]]"
|
PAT_BBCODE_ATTR = r"[^\]]"
|
||||||
PAT_LINK = r"https?:\/\/[\w\-_.?:\/=&~@#%]+[\w\-\/]"
|
PAT_LINK = r"https?:\/\/[\w\-_.?:\/=&~@#%]+[\w\-\/]"
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
@ -193,9 +193,10 @@ class Parser:
|
|||||||
self.save_position()
|
self.save_position()
|
||||||
|
|
||||||
# extract printable chars (extreme hack edition)
|
# extract printable chars (extreme hack edition)
|
||||||
word = self.match_pattern(r'[ -~]')
|
word = self.match_pattern(r'[!-~]')
|
||||||
|
|
||||||
if not re.match(PAT_LINK, word):
|
match = re.match(PAT_LINK, word)
|
||||||
|
if not match:
|
||||||
self.restore_position()
|
self.restore_position()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ def migrate_old_avatars():
|
|||||||
MIGRATIONS = [
|
MIGRATIONS = [
|
||||||
migrate_old_avatars,
|
migrate_old_avatars,
|
||||||
'DELETE FROM sessions', # delete old lua porom sessions
|
'DELETE FROM sessions', # delete old lua porom sessions
|
||||||
|
'ALTER TABLE "users" ADD COLUMN "invited_by" INTEGER REFERENCES users(id)', # invitation system
|
||||||
|
'ALTER TABLE "post_history" ADD COLUMN "format_version" INTEGER DEFAULT NULL',
|
||||||
]
|
]
|
||||||
|
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from .db import Model, db
|
from .db import Model, db
|
||||||
from .constants import PermissionLevel
|
from .constants import PermissionLevel
|
||||||
|
from flask import current_app
|
||||||
import time
|
import time
|
||||||
|
|
||||||
class Users(Model):
|
class Users(Model):
|
||||||
@ -17,6 +18,9 @@ class Users(Model):
|
|||||||
def is_mod(self):
|
def is_mod(self):
|
||||||
return self.permission >= PermissionLevel.MODERATOR.value
|
return self.permission >= PermissionLevel.MODERATOR.value
|
||||||
|
|
||||||
|
def is_mod_only(self):
|
||||||
|
return self.permission == PermissionLevel.MODERATOR.value
|
||||||
|
|
||||||
def is_admin(self):
|
def is_admin(self):
|
||||||
return self.permission == PermissionLevel.ADMIN.value
|
return self.permission == PermissionLevel.ADMIN.value
|
||||||
|
|
||||||
@ -48,7 +52,8 @@ class Users(Model):
|
|||||||
COUNT(DISTINCT posts.id) AS post_count,
|
COUNT(DISTINCT posts.id) AS post_count,
|
||||||
COUNT(DISTINCT threads.id) AS thread_count,
|
COUNT(DISTINCT threads.id) AS thread_count,
|
||||||
MAX(threads.title) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_title,
|
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
|
MAX(threads.slug) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_slug,
|
||||||
|
inviter.username AS inviter_username
|
||||||
FROM users
|
FROM users
|
||||||
LEFT JOIN posts ON posts.user_id = users.id
|
LEFT JOIN posts ON posts.user_id = users.id
|
||||||
LEFT JOIN threads ON threads.user_id = users.id
|
LEFT JOIN threads ON threads.user_id = users.id
|
||||||
@ -57,6 +62,7 @@ class Users(Model):
|
|||||||
FROM threads
|
FROM threads
|
||||||
GROUP BY user_id
|
GROUP BY user_id
|
||||||
) latest ON latest.user_id = users.id
|
) latest ON latest.user_id = users.id
|
||||||
|
LEFT JOIN users AS inviter ON inviter.id = users.invited_by
|
||||||
WHERE users.id = ?"""
|
WHERE users.id = ?"""
|
||||||
return db.fetch_one(q, self.id)
|
return db.fetch_one(q, self.id)
|
||||||
|
|
||||||
@ -83,6 +89,18 @@ class Users(Model):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def can_invite(self):
|
||||||
|
if not current_app.config['DISABLE_SIGNUP']:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if current_app.config['MODS_CAN_INVITE'] and self.is_mod():
|
||||||
|
return True
|
||||||
|
|
||||||
|
if current_app.config['USERS_CAN_INVITE'] and not self.is_guest():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Topics(Model):
|
class Topics(Model):
|
||||||
table = "topics"
|
table = "topics"
|
||||||
@ -160,7 +178,7 @@ class Topics(Model):
|
|||||||
|
|
||||||
q = """
|
q = """
|
||||||
SELECT
|
SELECT
|
||||||
threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
||||||
users.username AS started_by,
|
users.username AS started_by,
|
||||||
u.username AS latest_post_username,
|
u.username AS latest_post_username,
|
||||||
ph.content AS latest_post_content,
|
ph.content AS latest_post_content,
|
||||||
@ -233,6 +251,16 @@ class Avatars(Model):
|
|||||||
class Subscriptions(Model):
|
class Subscriptions(Model):
|
||||||
table = "subscriptions"
|
table = "subscriptions"
|
||||||
|
|
||||||
|
def get_unread_count(self):
|
||||||
|
q = """SELECT COUNT(*) AS unread_count
|
||||||
|
FROM posts
|
||||||
|
LEFT JOIN subscriptions ON subscriptions.thread_id = posts.thread_id
|
||||||
|
WHERE subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen AND posts.thread_id = ?"""
|
||||||
|
res = db.fetch_one(q, self.user_id, self.thread_id)
|
||||||
|
if res:
|
||||||
|
return res['unread_count']
|
||||||
|
return None
|
||||||
|
|
||||||
class APIRateLimits(Model):
|
class APIRateLimits(Model):
|
||||||
table = 'api_rate_limits'
|
table = 'api_rate_limits'
|
||||||
|
|
||||||
@ -256,3 +284,36 @@ class APIRateLimits(Model):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
class Reactions(Model):
|
||||||
|
table = "reactions"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_post(cls, post_id):
|
||||||
|
qb = db.QueryBuilder(cls.table)\
|
||||||
|
.select("reaction_text, COUNT(*) as c")\
|
||||||
|
.where({"post_id": post_id})\
|
||||||
|
.group_by("reaction_text")\
|
||||||
|
.order_by("c", False)
|
||||||
|
result = qb.all()
|
||||||
|
return result if result else []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_users(cls, post_id, reaction_text):
|
||||||
|
q = """
|
||||||
|
SELECT user_id, username FROM reactions
|
||||||
|
JOIN
|
||||||
|
users ON users.id = user_id
|
||||||
|
WHERE
|
||||||
|
post_id = ? AND reaction_text = ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
return db.query(q, post_id, reaction_text)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetLinks(Model):
|
||||||
|
table = "password_reset_links"
|
||||||
|
|
||||||
|
|
||||||
|
class InviteKeys(Model):
|
||||||
|
table = 'invite_keys'
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from flask import Blueprint, request, url_for
|
from flask import Blueprint, request, url_for
|
||||||
from ..lib.babycode import babycode_to_html
|
from ..lib.babycode import babycode_to_html
|
||||||
|
from ..constants import REACTION_EMOJI
|
||||||
from .users import is_logged_in, get_active_user
|
from .users import is_logged_in, get_active_user
|
||||||
from ..models import APIRateLimits, Threads
|
from ..models import APIRateLimits, Threads, Reactions
|
||||||
from ..db import db
|
from ..db import db
|
||||||
|
|
||||||
bp = Blueprint("api", __name__, url_prefix="/api/")
|
bp = Blueprint("api", __name__, url_prefix="/api/")
|
||||||
@ -41,3 +42,57 @@ def babycode_preview():
|
|||||||
return {'error': 'markup field missing or invalid type'}, 400
|
return {'error': 'markup field missing or invalid type'}, 400
|
||||||
rendered = babycode_to_html(markup)
|
rendered = babycode_to_html(markup)
|
||||||
return {'html': rendered}
|
return {'html': rendered}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/add-reaction/<post_id>')
|
||||||
|
def add_reaction(post_id):
|
||||||
|
if not is_logged_in():
|
||||||
|
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||||
|
user = get_active_user()
|
||||||
|
reaction_text = request.json.get('emoji')
|
||||||
|
if not reaction_text or not isinstance(reaction_text, str):
|
||||||
|
return {'error': 'emoji field missing or invalid type', 'error_code': 400}, 400
|
||||||
|
if reaction_text not in REACTION_EMOJI:
|
||||||
|
return {'error': 'unsupported reaction', 'error_code': 400}, 400
|
||||||
|
|
||||||
|
reaction = Reactions.find({
|
||||||
|
'user_id': user.id,
|
||||||
|
'post_id': int(post_id),
|
||||||
|
'reaction_text': reaction_text,
|
||||||
|
})
|
||||||
|
|
||||||
|
if reaction:
|
||||||
|
return {'error': 'reaction already exists', 'error_code': 409}, 409
|
||||||
|
|
||||||
|
reaction = Reactions.create({
|
||||||
|
'user_id': user.id,
|
||||||
|
'post_id': int(post_id),
|
||||||
|
'reaction_text': reaction_text,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {'status': 'added'}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/remove-reaction/<post_id>')
|
||||||
|
def remove_reaction(post_id):
|
||||||
|
if not is_logged_in():
|
||||||
|
return {'error': 'not authorized'}, 401
|
||||||
|
user = get_active_user()
|
||||||
|
reaction_text = request.json.get('emoji')
|
||||||
|
if not reaction_text or not isinstance(reaction_text, str):
|
||||||
|
return {'error': 'emoji field missing or invalid type'}, 400
|
||||||
|
if reaction_text not in REACTION_EMOJI:
|
||||||
|
return {'error': 'unsupported reaction'}, 400
|
||||||
|
|
||||||
|
reaction = Reactions.find({
|
||||||
|
'user_id': user.id,
|
||||||
|
'post_id': int(post_id),
|
||||||
|
'reaction_text': reaction_text,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not reaction:
|
||||||
|
return {'error': 'reaction does not exist'}, 404
|
||||||
|
|
||||||
|
reaction.delete()
|
||||||
|
|
||||||
|
return {'status': 'removed'}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for
|
Blueprint, render_template, request, redirect, url_for
|
||||||
)
|
)
|
||||||
from .users import login_required, mod_only, get_active_user
|
from .users import login_required, mod_only, get_active_user, admin_only
|
||||||
from ..models import Users
|
from ..models import Users, PasswordResetLinks
|
||||||
from ..db import db, DB
|
from ..db import db, DB
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
|
||||||
bp = Blueprint("mod", __name__, url_prefix = "/mod/")
|
bp = Blueprint("mod", __name__, url_prefix = "/mod/")
|
||||||
|
|
||||||
@bp.get("/sort-topics")
|
@bp.get("/sort-topics")
|
||||||
@ -31,3 +34,18 @@ def sort_topics_post():
|
|||||||
def user_list():
|
def user_list():
|
||||||
users = Users.select()
|
users = Users.select()
|
||||||
return render_template("mod/user-list.html", users = users)
|
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)
|
||||||
|
reset_link = PasswordResetLinks.create({
|
||||||
|
'user_id': int(user_id),
|
||||||
|
'expires_at': now + 24 * 60 * 60,
|
||||||
|
'key': key,
|
||||||
|
})
|
||||||
|
|
||||||
|
return redirect(url_for('users.reset_link_login', key=key))
|
||||||
|
@ -2,15 +2,16 @@ from flask import (
|
|||||||
Blueprint, redirect, url_for, flash, render_template, request
|
Blueprint, redirect, url_for, flash, render_template, request
|
||||||
)
|
)
|
||||||
from .users import login_required, get_active_user
|
from .users import login_required, get_active_user
|
||||||
from ..lib.babycode import babycode_to_html
|
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||||
from ..constants import InfoboxKind
|
from ..constants import InfoboxKind
|
||||||
from ..db import db
|
from ..db import db
|
||||||
from ..models import Posts, PostHistory, Threads
|
from ..models import Posts, PostHistory, Threads, Topics
|
||||||
|
|
||||||
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
||||||
|
|
||||||
|
|
||||||
def create_post(thread_id, user_id, content, markup_language="babycode"):
|
def create_post(thread_id, user_id, content, markup_language="babycode"):
|
||||||
|
parsed_content = babycode_to_html(content)
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
post = Posts.create({
|
post = Posts.create({
|
||||||
"thread_id": thread_id,
|
"thread_id": thread_id,
|
||||||
@ -20,10 +21,11 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
|
|||||||
|
|
||||||
revision = PostHistory.create({
|
revision = PostHistory.create({
|
||||||
"post_id": post.id,
|
"post_id": post.id,
|
||||||
"content": babycode_to_html(content),
|
"content": parsed_content,
|
||||||
"is_initial_revision": True,
|
"is_initial_revision": True,
|
||||||
"original_markup": content,
|
"original_markup": content,
|
||||||
"markup_language": markup_language,
|
"markup_language": markup_language,
|
||||||
|
"format_version": BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
|
|
||||||
post.update({"current_revision_id": revision.id})
|
post.update({"current_revision_id": revision.id})
|
||||||
@ -31,14 +33,16 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
|
|||||||
|
|
||||||
|
|
||||||
def update_post(post_id, new_content, markup_language='babycode'):
|
def update_post(post_id, new_content, markup_language='babycode'):
|
||||||
|
parsed_content = babycode_to_html(new_content)
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
new_revision = PostHistory.create({
|
new_revision = PostHistory.create({
|
||||||
'post_id': post.id,
|
'post_id': post.id,
|
||||||
'content': babycode_to_html(new_content),
|
'content': parsed_content,
|
||||||
'is_initial_revision': False,
|
'is_initial_revision': False,
|
||||||
'original_markup': new_content,
|
'original_markup': new_content,
|
||||||
'markup_language': markup_language,
|
'markup_language': markup_language,
|
||||||
|
'format_version': BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
|
|
||||||
post.update({'current_revision_id': new_revision.id})
|
post.update({'current_revision_id': new_revision.id})
|
||||||
@ -48,12 +52,30 @@ def update_post(post_id, new_content, markup_language='babycode'):
|
|||||||
@login_required
|
@login_required
|
||||||
def delete(post_id):
|
def delete(post_id):
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
thread = Threads.find({'id': post.thread_id})
|
thread = Threads.find({'id': post.thread_id})
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
|
if not user:
|
||||||
|
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||||
|
|
||||||
if user.is_mod() or post.user_id == user.id:
|
if user.is_mod() or post.user_id == user.id:
|
||||||
post.delete()
|
post.delete()
|
||||||
flash("Post deleted.", InfoboxKind.INFO)
|
|
||||||
|
post_count = Posts.count({
|
||||||
|
'thread_id': thread.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if post_count == 0:
|
||||||
|
topic = Topics.find({
|
||||||
|
'id': thread.topic_id,
|
||||||
|
})
|
||||||
|
thread.delete()
|
||||||
|
flash('Thread deleted.', InfoboxKind.INFO)
|
||||||
|
return redirect(url_for('topics.topic', slug=topic.slug))
|
||||||
|
|
||||||
|
flash('Post deleted.', InfoboxKind.INFO)
|
||||||
|
|
||||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||||
|
|
||||||
@ -61,6 +83,10 @@ def delete(post_id):
|
|||||||
@bp.get("/<post_id>/edit")
|
@bp.get("/<post_id>/edit")
|
||||||
@login_required
|
@login_required
|
||||||
def edit(post_id):
|
def edit(post_id):
|
||||||
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
||||||
editing_post = db.fetch_one(q, post_id)
|
editing_post = db.fetch_one(q, post_id)
|
||||||
@ -91,6 +117,8 @@ def edit(post_id):
|
|||||||
def edit_form(post_id):
|
def edit_form(post_id):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
if post.user_id != user.id:
|
if post.user_id != user.id:
|
||||||
return redirect(url_for('topics.all_topics'))
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||||
from ..db import db
|
from ..db import db
|
||||||
from ..models import Threads, Topics, Posts, Subscriptions
|
from ..models import Threads, Topics, Posts, Subscriptions, Reactions
|
||||||
from ..constants import InfoboxKind
|
from ..constants import InfoboxKind
|
||||||
from .posts import create_post
|
from .posts import create_post
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
@ -13,6 +13,20 @@ import time
|
|||||||
bp = Blueprint("threads", __name__, url_prefix = "/threads/")
|
bp = Blueprint("threads", __name__, url_prefix = "/threads/")
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_url(post_id, _anchor=False):
|
||||||
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
|
||||||
|
res = url_for('threads.thread', slug=thread.slug, after=post_id)
|
||||||
|
if not _anchor:
|
||||||
|
return res
|
||||||
|
|
||||||
|
return f"{res}#post-{post_id}"
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<slug>")
|
@bp.get("/<slug>")
|
||||||
def thread(slug):
|
def thread(slug):
|
||||||
POSTS_PER_PAGE = 10
|
POSTS_PER_PAGE = 10
|
||||||
@ -40,12 +54,14 @@ def thread(slug):
|
|||||||
other_topics = Topics.select()
|
other_topics = Topics.select()
|
||||||
|
|
||||||
is_subscribed = False
|
is_subscribed = False
|
||||||
|
unread_count = None
|
||||||
if is_logged_in():
|
if is_logged_in():
|
||||||
subscription = Subscriptions.find({
|
subscription = Subscriptions.find({
|
||||||
'thread_id': thread.id,
|
'thread_id': thread.id,
|
||||||
'user_id': get_active_user().id,
|
'user_id': get_active_user().id,
|
||||||
})
|
})
|
||||||
if subscription:
|
if subscription:
|
||||||
|
unread_count = subscription.get_unread_count()
|
||||||
if int(posts[-1]['created_at']) > int(subscription.last_seen):
|
if int(posts[-1]['created_at']) > int(subscription.last_seen):
|
||||||
subscription.update({
|
subscription.update({
|
||||||
'last_seen': int(posts[-1]['created_at'])
|
'last_seen': int(posts[-1]['created_at'])
|
||||||
@ -61,6 +77,8 @@ def thread(slug):
|
|||||||
topic = topic,
|
topic = topic,
|
||||||
topics = other_topics,
|
topics = other_topics,
|
||||||
is_subscribed = is_subscribed,
|
is_subscribed = is_subscribed,
|
||||||
|
Reactions = Reactions,
|
||||||
|
unread_count = unread_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for, flash, session
|
Blueprint, render_template, request, redirect, url_for, flash, session
|
||||||
)
|
)
|
||||||
from .users import login_required, mod_only
|
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||||
from ..models import Users, Topics, Threads
|
from ..models import Users, Topics, Threads, Subscriptions
|
||||||
from ..constants import InfoboxKind
|
from ..constants import InfoboxKind
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
import time
|
import time
|
||||||
@ -62,9 +62,21 @@ def topic(slug):
|
|||||||
page_count = max(math.ceil(threads_count / THREADS_PER_PAGE), 1)
|
page_count = max(math.ceil(threads_count / THREADS_PER_PAGE), 1)
|
||||||
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||||
|
|
||||||
|
threads_list = target_topic.get_threads(THREADS_PER_PAGE, page, sort_by)
|
||||||
|
subscriptions = {}
|
||||||
|
if is_logged_in():
|
||||||
|
for thread in threads_list:
|
||||||
|
subscription = Subscriptions.find({
|
||||||
|
'user_id': get_active_user().id,
|
||||||
|
'thread_id': thread['id'],
|
||||||
|
})
|
||||||
|
if subscription:
|
||||||
|
subscriptions[thread['id']] = subscription.get_unread_count()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"topics/topic.html",
|
"topics/topic.html",
|
||||||
threads_list = target_topic.get_threads(THREADS_PER_PAGE, page, sort_by),
|
threads_list = threads_list,
|
||||||
|
subscriptions = subscriptions,
|
||||||
topic = target_topic,
|
topic = target_topic,
|
||||||
current_page = page,
|
current_page = page,
|
||||||
page_count = page_count
|
page_count = page_count
|
||||||
|
@ -4,7 +4,7 @@ from flask import (
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from ..db import db
|
from ..db import db
|
||||||
from ..lib.babycode import babycode_to_html
|
from ..lib.babycode import babycode_to_html
|
||||||
from ..models import Users, Sessions, Subscriptions, Avatars
|
from ..models import Users, Sessions, Subscriptions, Avatars, PasswordResetLinks, InviteKeys
|
||||||
from ..constants import InfoboxKind, PermissionLevel
|
from ..constants import InfoboxKind, PermissionLevel
|
||||||
from ..auth import digest, verify
|
from ..auth import digest, verify
|
||||||
from wand.image import Image
|
from wand.image import Image
|
||||||
@ -165,6 +165,15 @@ def admin_only(*args, **kwargs):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def get_prefers_theme():
|
||||||
|
if not 'theme' in session:
|
||||||
|
return 'style'
|
||||||
|
|
||||||
|
if session['theme'] not in current_app.config['allowed_themes']:
|
||||||
|
return 'style'
|
||||||
|
|
||||||
|
return session['theme']
|
||||||
|
|
||||||
@bp.get("/log_in")
|
@bp.get("/log_in")
|
||||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||||
def log_in():
|
def log_in():
|
||||||
@ -195,32 +204,53 @@ def log_in_post():
|
|||||||
@bp.get("/sign_up")
|
@bp.get("/sign_up")
|
||||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||||
def sign_up():
|
def sign_up():
|
||||||
|
if current_app.config['DISABLE_SIGNUP']:
|
||||||
|
key = request.args.get('key', default=None)
|
||||||
|
if key is None:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
invite = InviteKeys.find({'key': key})
|
||||||
|
if not invite:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
inviter = Users.find({'id': invite.created_by})
|
||||||
|
return render_template("users/sign_up.html", inviter=inviter, key=key)
|
||||||
|
|
||||||
return render_template("users/sign_up.html")
|
return render_template("users/sign_up.html")
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/sign_up")
|
@bp.post("/sign_up")
|
||||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||||
def sign_up_post():
|
def sign_up_post():
|
||||||
|
key = request.form.get('key', default=None)
|
||||||
|
|
||||||
|
if current_app.config['DISABLE_SIGNUP']:
|
||||||
|
if not key:
|
||||||
|
return redirect(url_for("topics.all_topics"))
|
||||||
|
invite_key = InviteKeys.find({'key': key})
|
||||||
|
if not invite_key:
|
||||||
|
return redirect(url_for("topics.all_topics"))
|
||||||
|
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
password = request.form['password']
|
password = request.form['password']
|
||||||
password_confirm = request.form['password-confirm']
|
password_confirm = request.form['password-confirm']
|
||||||
|
|
||||||
if not validate_username(username):
|
if not validate_username(username):
|
||||||
flash("Invalid username.", InfoboxKind.ERROR)
|
flash("Invalid username.", InfoboxKind.ERROR)
|
||||||
return redirect(url_for("users.sign_up"))
|
return redirect(url_for("users.sign_up", key=key))
|
||||||
|
|
||||||
user_exists = Users.count({"username": username}) > 0
|
user_exists = Users.count({"username": username}) > 0
|
||||||
if user_exists:
|
if user_exists:
|
||||||
flash(f"Username '{username}' is already taken.", InfoboxKind.ERROR)
|
flash(f"Username '{username}' is already taken.", InfoboxKind.ERROR)
|
||||||
return redirect(url_for("users.sign_up"))
|
return redirect(url_for("users.sign_up", key=key))
|
||||||
|
|
||||||
if not validate_password(password):
|
if not validate_password(password):
|
||||||
flash("Invalid password.", InfoboxKind.ERROR)
|
flash("Invalid password.", InfoboxKind.ERROR)
|
||||||
return redirect(url_for("users.sign_up"))
|
return redirect(url_for("users.sign_up", key=key))
|
||||||
|
|
||||||
if password != password_confirm:
|
if password != password_confirm:
|
||||||
flash("Passwords do not match.", InfoboxKind.ERROR)
|
flash("Passwords do not match.", InfoboxKind.ERROR)
|
||||||
return redirect(url_for("users.sign_up"))
|
return redirect(url_for("users.sign_up", key=key))
|
||||||
|
|
||||||
hashed = digest(password)
|
hashed = digest(password)
|
||||||
|
|
||||||
@ -230,11 +260,19 @@ def sign_up_post():
|
|||||||
"permission": PermissionLevel.GUEST.value,
|
"permission": PermissionLevel.GUEST.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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_obj = create_session(new_user.id)
|
||||||
|
|
||||||
session['pyrom_session_key'] = session_obj.key
|
session['pyrom_session_key'] = session_obj.key
|
||||||
flash("Signed up successfully!", InfoboxKind.INFO)
|
flash("Signed up successfully!", InfoboxKind.INFO)
|
||||||
return redirect(url_for("users.sign_up"))
|
return redirect(url_for("topics.all_topics"))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<username>")
|
@bp.get("/<username>")
|
||||||
@ -259,6 +297,12 @@ def settings_form(username):
|
|||||||
# we silently ignore the passed username
|
# we silently ignore the passed username
|
||||||
# and grab the correct user from the session
|
# and grab the correct user from the session
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
|
theme = request.form.get('theme', default='style')
|
||||||
|
if theme == 'style':
|
||||||
|
if 'theme' in session:
|
||||||
|
session.pop('theme')
|
||||||
|
else:
|
||||||
|
session['theme'] = theme
|
||||||
topic_sort_by = request.form.get('topic_sort_by', default='activity')
|
topic_sort_by = request.form.get('topic_sort_by', default='activity')
|
||||||
if topic_sort_by == 'activity' or topic_sort_by == 'thread':
|
if topic_sort_by == 'activity' or topic_sort_by == 'thread':
|
||||||
sort_by = session['sort_by'] = topic_sort_by
|
sort_by = session['sort_by'] = topic_sort_by
|
||||||
@ -281,17 +325,17 @@ def settings_form(username):
|
|||||||
def set_avatar(username):
|
def set_avatar(username):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
if user.is_guest():
|
if user.is_guest():
|
||||||
flash('You must be logged in to perform this action.', InfoboxKind.ERROR)
|
flash('You are a guest. Your account must be confirmed by a moderator to perform this action.', InfoboxKind.ERROR)
|
||||||
return redirect(url_for('.settings', user.username))
|
return redirect(url_for('.settings', username=user.username))
|
||||||
if 'avatar' not in request.files:
|
if 'avatar' not in request.files:
|
||||||
flash('Avatar missing.', InfoboxKind.ERROR)
|
flash('Avatar missing.', InfoboxKind.ERROR)
|
||||||
return redirect(url_for('.settings', user.username))
|
return redirect(url_for('.settings', username=user.username))
|
||||||
|
|
||||||
file = request.files['avatar']
|
file = request.files['avatar']
|
||||||
|
|
||||||
if file.filename == '':
|
if file.filename == '':
|
||||||
flash('Avatar missing.', InfoboxKind.ERROR)
|
flash('Avatar missing.', InfoboxKind.ERROR)
|
||||||
return redirect(url_for('.settings', user.username))
|
return redirect(url_for('.settings', username=user.username))
|
||||||
|
|
||||||
file_bytes = file.read()
|
file_bytes = file.read()
|
||||||
|
|
||||||
@ -316,7 +360,7 @@ def set_avatar(username):
|
|||||||
return redirect(url_for('.settings', username=user.username))
|
return redirect(url_for('.settings', username=user.username))
|
||||||
else:
|
else:
|
||||||
flash('Something went wrong. Please try again later.', InfoboxKind.WARN)
|
flash('Something went wrong. Please try again later.', InfoboxKind.WARN)
|
||||||
return redirect(url_for('.settings', user.username))
|
return redirect(url_for('.settings', username=user.username))
|
||||||
|
|
||||||
|
|
||||||
@bp.post('/<username>/change_password')
|
@bp.post('/<username>/change_password')
|
||||||
@ -419,12 +463,12 @@ def demod_user(user_id):
|
|||||||
|
|
||||||
@bp.post("/guest_user/<user_id>")
|
@bp.post("/guest_user/<user_id>")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_only("topics.all_topics")
|
@mod_only("topics.all_topics")
|
||||||
def guest_user(user_id):
|
def guest_user(user_id):
|
||||||
target_user = Users.find({"id": user_id})
|
target_user = Users.find({"id": user_id})
|
||||||
if not target_user:
|
if not target_user:
|
||||||
return redirect(url_for('.all_topics'))
|
return redirect(url_for('.all_topics'))
|
||||||
if target_user.is_mod():
|
if get_active_user().is_mod_only() and target_user.is_mod():
|
||||||
return redirect(url_for('.page', username=target_user.username))
|
return redirect(url_for('.page', username=target_user.username))
|
||||||
|
|
||||||
target_user.update({
|
target_user.update({
|
||||||
@ -510,7 +554,132 @@ def inbox(username):
|
|||||||
'thread_id': row['thread_id'],
|
'thread_id': row['thread_id'],
|
||||||
'user_id': row['user_id'],
|
'user_id': row['user_id'],
|
||||||
'original_markup': row['original_markup'],
|
'original_markup': row['original_markup'],
|
||||||
'signature_rendered': row['signature_rendered']
|
'signature_rendered': row['signature_rendered'],
|
||||||
|
|
||||||
|
'thread_slug': row['thread_slug'],
|
||||||
})
|
})
|
||||||
|
|
||||||
return render_template("users/inbox.html", new_posts = new_posts, total_unreads_count = total_unreads_count, all_subscriptions = all_subscriptions)
|
return render_template("users/inbox.html", new_posts = new_posts, total_unreads_count = total_unreads_count, all_subscriptions = all_subscriptions)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get('/reset-link/<key>')
|
||||||
|
def reset_link_login(key):
|
||||||
|
reset_link = PasswordResetLinks.find({
|
||||||
|
'key': key
|
||||||
|
})
|
||||||
|
if not reset_link:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
if int(time.time()) > int(reset_link.expires_at):
|
||||||
|
reset_link.delete()
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
target_user = Users.find({
|
||||||
|
'id': reset_link.user_id
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('users/reset_link_login.html', username = target_user.username)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/reset-link/<key>')
|
||||||
|
def reset_link_login_form(key):
|
||||||
|
reset_link = PasswordResetLinks.find({
|
||||||
|
'key': key
|
||||||
|
})
|
||||||
|
if not reset_link:
|
||||||
|
return redirect('topics.all_topics')
|
||||||
|
|
||||||
|
if int(time.time()) > int(reset_link.expires_at):
|
||||||
|
reset_link.delete()
|
||||||
|
return redirect('topics.all_topics')
|
||||||
|
|
||||||
|
password = request.form.get('password')
|
||||||
|
password2 = request.form.get('password2')
|
||||||
|
|
||||||
|
if not validate_password(password):
|
||||||
|
flash("Invalid password.", InfoboxKind.ERROR)
|
||||||
|
return redirect(url_for('.reset_link_login', key=key))
|
||||||
|
|
||||||
|
if password != password2:
|
||||||
|
flash("Passwords do not match.", InfoboxKind.ERROR)
|
||||||
|
return redirect(url_for('.reset_link_login', key=key))
|
||||||
|
|
||||||
|
target_user = Users.find({
|
||||||
|
'id': reset_link.user_id
|
||||||
|
})
|
||||||
|
reset_link.delete()
|
||||||
|
|
||||||
|
hashed = digest(password)
|
||||||
|
target_user.update({'password_hash': hashed})
|
||||||
|
session_obj = create_session(target_user.id)
|
||||||
|
|
||||||
|
session['pyrom_session_key'] = session_obj.key
|
||||||
|
flash("Logged in!", InfoboxKind.INFO)
|
||||||
|
|
||||||
|
return redirect(url_for('.page', username=target_user.username))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get('/<username>/invite-links/')
|
||||||
|
@login_required
|
||||||
|
def invite_links(username):
|
||||||
|
target_user = Users.find({
|
||||||
|
'username': username
|
||||||
|
})
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('users/invite_links.html', invites=invites)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/<username>/invite-links/create')
|
||||||
|
@login_required
|
||||||
|
def create_invite_link(username):
|
||||||
|
target_user = Users.find({
|
||||||
|
'username': username
|
||||||
|
})
|
||||||
|
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))
|
||||||
|
|
||||||
|
invite = InviteKeys.create({
|
||||||
|
'created_by': target_user.id,
|
||||||
|
'key': secrets.token_urlsafe(20),
|
||||||
|
})
|
||||||
|
|
||||||
|
return redirect(url_for('.invite_links', username=target_user.username))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/<username>/invite-links/revoke')
|
||||||
|
@login_required
|
||||||
|
def revoke_invite_link(username):
|
||||||
|
target_user = Users.find({
|
||||||
|
'username': username
|
||||||
|
})
|
||||||
|
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))
|
||||||
|
|
||||||
|
invite = InviteKeys.find({
|
||||||
|
'key': request.form.get('key'),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not invite:
|
||||||
|
return redirect(url_for('.invite_links', username=target_user.username))
|
||||||
|
|
||||||
|
if invite.created_by != target_user.id:
|
||||||
|
return redirect(url_for('.invite_links', username=target_user.username))
|
||||||
|
|
||||||
|
invite.delete()
|
||||||
|
|
||||||
|
return redirect(url_for('.invite_links', username=target_user.username))
|
||||||
|
@ -76,6 +76,26 @@ SCHEMA = [
|
|||||||
"signature_rendered" TEXT NOT NULL DEFAULT ''
|
"signature_rendered" TEXT NOT NULL DEFAULT ''
|
||||||
)""",
|
)""",
|
||||||
|
|
||||||
|
"""CREATE TABLE IF NOT EXISTS "reactions" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
"user_id" REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
"post_id" REFERENCES posts(id) ON DELETE CASCADE,
|
||||||
|
"reaction_text" TEXT NOT NULL DEFAULT ''
|
||||||
|
)""",
|
||||||
|
|
||||||
|
"""CREATE TABLE IF NOT EXISTS "password_reset_links" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
"user_id" REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
"expires_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
|
||||||
|
"key" TEXT NOT NULL UNIQUE
|
||||||
|
)""",
|
||||||
|
|
||||||
|
"""CREATE TABLE IF NOT EXISTS "invite_keys" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
"created_by" REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
"key" TEXT NOT NULL UNIQUE
|
||||||
|
)""",
|
||||||
|
|
||||||
# INDEXES
|
# INDEXES
|
||||||
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
|
"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)",
|
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",
|
||||||
@ -87,6 +107,9 @@ SCHEMA = [
|
|||||||
"CREATE INDEX IF NOT EXISTS idx_topics_slug ON topics(slug)",
|
"CREATE INDEX IF NOT EXISTS idx_topics_slug ON topics(slug)",
|
||||||
"CREATE INDEX IF NOT EXISTS session_keys ON sessions(key)",
|
"CREATE INDEX IF NOT EXISTS session_keys ON sessions(key)",
|
||||||
"CREATE INDEX IF NOT EXISTS sessions_user_id ON sessions(user_id)",
|
"CREATE INDEX IF NOT EXISTS sessions_user_id ON sessions(user_id)",
|
||||||
|
|
||||||
|
"CREATE INDEX IF NOT EXISTS reaction_post_text ON reactions(post_id, reaction_text)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS reaction_user_post_text ON reactions(user_id, post_id, reaction_text)",
|
||||||
]
|
]
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
|
@ -14,23 +14,68 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="babycode-guide-section">
|
<section class="babycode-guide-section">
|
||||||
<h2 id="text-formatting-tags">Text formatting tags</h2>
|
<h2 id="text-formatting-tags">Text formatting tags</h2>
|
||||||
<ul>
|
<ul class='babycode-guide-list'>
|
||||||
<li>To make some text <strong>bold</strong>, enclose it in <code class="inline-code">[b][/b]</code>:<br>
|
<li>To make some text <strong>bold</strong>, enclose it in <code class="inline-code">[b][/b]</code>:<br>
|
||||||
[b]Hello World[/b]<br>
|
[b]Hello World[/b]<br>
|
||||||
Will become<br>
|
Will become<br>
|
||||||
<strong>Hello World</strong>
|
<strong>Hello World</strong>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul class='babycode-guide-list'>
|
||||||
<li>To <em>italicize</em> text, enclose it in <code class="inline-code">[i][/i]</code>:<br>
|
<li>To <em>italicize</em> text, enclose it in <code class="inline-code">[i][/i]</code>:<br>
|
||||||
[i]Hello World[/i]<br>
|
[i]Hello World[/i]<br>
|
||||||
Will become<br>
|
Will become<br>
|
||||||
<em>Hello World</em>
|
<em>Hello World</em>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<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>
|
<li>To make some text <del>strikethrough</del>, enclose it in <code class="inline-code">[s][/s]</code>:<br>
|
||||||
[s]Hello World[/s]<br>
|
[s]Hello World[/s]<br>
|
||||||
Will become<br>
|
Will become<br>
|
||||||
<del>Hello World</del>
|
<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>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<section class="babycode-guide-section">
|
<section class="babycode-guide-section">
|
||||||
@ -60,6 +105,15 @@
|
|||||||
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
|
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
|
||||||
That will produce:<br>
|
That will produce:<br>
|
||||||
{{ 'paragraph 1 \nstill paragraph 1' | babycode | safe }}
|
{{ '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>
|
||||||
<section class="babycode-guide-section">
|
<section class="babycode-guide-section">
|
||||||
<h2 id="links">Links</h2>
|
<h2 id="links">Links</h2>
|
||||||
@ -72,8 +126,9 @@
|
|||||||
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code>
|
<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 }}
|
{{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }}
|
||||||
</p>
|
</p>
|
||||||
<p>Text inside the tag becomes the alt text. The attribute is the image URL.</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. 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>
|
||||||
<section class="babycode-guide-section">
|
<section class="babycode-guide-section">
|
||||||
<h2 id="adding-code-blocks">Adding code blocks</h2>
|
<h2 id="adding-code-blocks">Adding code blocks</h2>
|
||||||
@ -97,6 +152,15 @@
|
|||||||
Will produce the following list:
|
Will produce the following list:
|
||||||
{{ list | babycode | safe }}
|
{{ list | babycode | safe }}
|
||||||
</section>
|
</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 %}
|
{% endset %}
|
||||||
{{ sections | safe }}
|
{{ sections | safe }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
{% if self.title() %}
|
{% if self.title() %}
|
||||||
<title>Porom - {% block title %}{% endblock %}</title>
|
<title>{{config.SITE_NAME}} - {% block title %}{% endblock %}</title>
|
||||||
{% else %}
|
{% else %}
|
||||||
<title>Porom</title>
|
<title>{{config.SITE_NAME}}</title>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
|
||||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -24,7 +24,6 @@
|
|||||||
<footer class="darkbg">
|
<footer class="darkbg">
|
||||||
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
|
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/js/copy-code.js"></script>
|
<script src="{{ "/static/js/ui.js" | cachebust }}"></script>
|
||||||
<script src="/static/js/ui.js"></script>
|
<script src="{{ "/static/js/date-fmt.js" | cachebust }}"></script>
|
||||||
<script src="/static/js/date-fmt.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
@ -53,11 +53,13 @@
|
|||||||
<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-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-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-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-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" type=button id="post-editor-code" title="Insert Code block"><code></></code></button>
|
||||||
<button class="babycode-button contain-svg full" type=button id="post-editor-img" title="Insert Image"><img src="/static/misc/image.svg"></button>
|
<button class="babycode-button contain-svg full" type=button id="post-editor-img" title="Insert Image"><img src="/static/misc/image.svg"></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-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" type=button id="post-editor-ul" title="Insert Unordered list">•</button>
|
||||||
|
<button class="babycode-button contain-svg full" type=button id="post-editor-spoiler" title="Insert spoiler"><img src="/static/misc/spoiler.svg"></button>
|
||||||
</span>
|
</span>
|
||||||
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }}>{{ prefill }}</textarea>
|
<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>
|
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
|
||||||
@ -67,7 +69,7 @@
|
|||||||
<div id="babycode-preview-container"></div>
|
<div id="babycode-preview-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/babycode-editor.js?v=2"></script>
|
<script src="{{ "/static/js/babycode-editor.js" | cachebust }}"></script>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro babycode_editor_form(ta_name, prefill = "", cancel_url="", endpoint="") %}
|
{% macro babycode_editor_form(ta_name, prefill = "", cancel_url="", endpoint="") %}
|
||||||
@ -89,13 +91,13 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None, no_reply = false) %}
|
{% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None, no_reply = false, Reactions = none) %}
|
||||||
{% set postclass = "post" %}
|
{% set postclass = "post" %}
|
||||||
{% if editing %}
|
{% if editing %}
|
||||||
{% set postclass = postclass + " editing" %}
|
{% set postclass = postclass + " editing" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set post_permalink = url_for("threads.thread", slug = post['thread_slug'], after = post['id'], _anchor = ("post-" + (post['id'] | string))) %}
|
{% set post_permalink = url_for("threads.thread", slug = post['thread_slug'], after = post['id'], _anchor = ("post-" + (post['id'] | string))) %}
|
||||||
<div class=" {{ postclass }}" id="post-{{ post['id'] }}">
|
<div class=" {{ postclass }}" id="post-{{ post['id'] }}" data-post-id="{{ post['id'] }}">
|
||||||
<div class="usercard">
|
<div class="usercard">
|
||||||
<div class="usercard-inner">
|
<div class="usercard-inner">
|
||||||
<a href="{{ url_for("users.page", username=post['username']) }}" style="display: contents;">
|
<a href="{{ url_for("users.page", username=post['username']) }}" style="display: contents;">
|
||||||
@ -128,9 +130,11 @@
|
|||||||
|
|
||||||
{% set show_reply = true %}
|
{% set show_reply = true %}
|
||||||
|
|
||||||
{% if active_user and post['thread_is_locked'] and not active_user.is_mod() %}
|
{% if not active_user %}
|
||||||
{% set show_reply = false %}
|
{% set show_reply = false %}
|
||||||
{% elif active_user and active_user.is_guest() %}
|
{% elif post['thread_is_locked'] and not active_user.is_mod() %}
|
||||||
|
{% set show_reply = false %}
|
||||||
|
{% elif active_user.is_guest() %}
|
||||||
{% set show_reply = false %}
|
{% set show_reply = false %}
|
||||||
{% elif editing %}
|
{% elif editing %}
|
||||||
{% set show_reply = false %}
|
{% set show_reply = false %}
|
||||||
@ -140,7 +144,7 @@
|
|||||||
|
|
||||||
{% if show_reply %}
|
{% if show_reply %}
|
||||||
{% set qtext = "[url=%s]%s said:[/url]" | format(post_permalink, post['username']) %}
|
{% set qtext = "[url=%s]%s said:[/url]" | format(post_permalink, post['username']) %}
|
||||||
{% set reply_text = "%s\n[quote]%s[/quote]\n" | format(qtext, post['original_markup']) %}
|
{% 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 value="{{ reply_text }}" class="reply-button">Quote</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -160,7 +164,6 @@
|
|||||||
<div class="post-inner" data-post-permalink="{{ post_permalink }}" data-author-username="{{ post.username }}">{{ post['content'] | safe }}</div>
|
<div class="post-inner" data-post-permalink="{{ post_permalink }}" data-author-username="{{ post.username }}">{{ post['content'] | safe }}</div>
|
||||||
{% if render_sig and post['signature_rendered'] %}
|
{% if render_sig and post['signature_rendered'] %}
|
||||||
<div class="signature-container">
|
<div class="signature-container">
|
||||||
<hr>
|
|
||||||
{{ post['signature_rendered'] | safe }}
|
{{ post['signature_rendered'] | safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -168,6 +171,37 @@
|
|||||||
{{ babycode_editor_form(cancel_url = post_permalink, prefill = post['original_markup'], ta_name = "new_content") }}
|
{{ babycode_editor_form(cancel_url = post_permalink, prefill = post['original_markup'], ta_name = "new_content") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if Reactions -%}
|
||||||
|
{% set can_react = true -%}
|
||||||
|
|
||||||
|
{% if not active_user -%}
|
||||||
|
{% set can_react = false -%}
|
||||||
|
{% elif post['thread_is_locked'] and not active_user.is_mod() -%}
|
||||||
|
{% set can_react = false -%}
|
||||||
|
{% elif active_user.is_guest() -%}
|
||||||
|
{% set can_react = false -%}
|
||||||
|
{% elif editing -%}
|
||||||
|
{% set can_react = false -%}
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% set reactions = Reactions.for_post(post.id) -%}
|
||||||
|
<div class="post-reactions">
|
||||||
|
{% for reaction in reactions %}
|
||||||
|
{% set reactors = Reactions.get_users(post.id, reaction.reaction_text) | map(attribute='username') | list %}
|
||||||
|
{% set reactors_trimmed = reactors[:10] %}
|
||||||
|
{% set reactors_str = reactors_trimmed | join (',\n') %}
|
||||||
|
{% if reactors | count > 10 %}
|
||||||
|
{% set reactors_str = reactors_str + '\n...and many others' %}
|
||||||
|
{% endif %}
|
||||||
|
{% set has_reacted = active_user is not none and active_user.username in reactors %}
|
||||||
|
<span class="reaction-container" data-emoji="{{ reaction.reaction_text }}" data-post-id="{{ post.id }}"><button type="button" class="reduced reaction-button {{"active" if has_reacted else ""}}" {{ "disabled" if not can_react else ""}} title="{{reactors_str}}"><img class=emoji src="/static/emoji/{{reaction.reaction_text}}.png"> x<span class="reaction-count" data-emoji="{{ reaction.reaction_text }}">{{reaction.c}}</span></button>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% if can_react %}
|
||||||
|
<button type="button" class="reduced add-reaction-button" data-post-id="{{ post.id }}">Add reaction</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
<nav id="topnav">
|
<nav id="topnav">
|
||||||
<span>
|
<span>
|
||||||
<a class="site-title" href="{{url_for('topics.all_topics')}}">Porom</a>
|
<a class="site-title" href="{{url_for('topics.all_topics')}}">{{config.SITE_NAME}}</a>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{% if not is_logged_in() %}
|
{% if not is_logged_in() %}
|
||||||
|
{% if not config.DISABLE_SIGNUP %}
|
||||||
Welcome, guest. Please <a href="{{url_for('users.sign_up')}}">sign up</a> or <a href="{{url_for('users.log_in')}}">log in</a>
|
Welcome, guest. Please <a href="{{url_for('users.sign_up')}}">sign up</a> or <a href="{{url_for('users.log_in')}}">log in</a>
|
||||||
|
{% else %}
|
||||||
|
Welcome, guest. Please <a href="{{url_for('users.log_in')}}">log in</a>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with user = get_active_user() %}
|
{% with user = get_active_user() %}
|
||||||
Welcome, <a href="{{ url_for("users.page", username = user.username) }}">{{user.username}}</a>
|
Welcome, <a href="{{ url_for("users.page", username = user.username) }}">{{user.username}}</a>
|
||||||
@ -12,6 +16,10 @@
|
|||||||
<a href="{{ url_for("users.settings", username = user.username) }}">Settings</a>
|
<a href="{{ url_for("users.settings", username = user.username) }}">Settings</a>
|
||||||
•
|
•
|
||||||
<a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</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 user.is_mod() %}
|
{% if user.is_mod() %}
|
||||||
•
|
•
|
||||||
<a href="{{ url_for("mod.user_list") }}">User list</a>
|
<a href="{{ url_for("mod.user_list") }}">User list</a>
|
||||||
|
@ -14,5 +14,5 @@
|
|||||||
<input type=submit value="Save order">
|
<input type=submit value="Save order">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/sort-topics.js"></script>
|
<script src="{{ "/static/js/sort-topics.js" | cachebust }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -37,6 +37,9 @@
|
|||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th class="small">Permission</th>
|
<th class="small">Permission</th>
|
||||||
<th class="small">Signed up on</th>
|
<th class="small">Signed up on</th>
|
||||||
|
{% if active_user.is_admin() %}
|
||||||
|
<th class="small">Create password reset link</th>
|
||||||
|
{% endif %}
|
||||||
</thead>
|
</thead>
|
||||||
{% for user in not_guests %}
|
{% for user in not_guests %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -50,6 +53,13 @@
|
|||||||
<td>
|
<td>
|
||||||
{{ timestamp(user.created_at) }}
|
{{ timestamp(user.created_at) }}
|
||||||
</td>
|
</td>
|
||||||
|
{% if active_user.is_admin() %}
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{{url_for('mod.create_reset_pass', user_id=user.id)}}">
|
||||||
|
<input type="submit" class="warn" value="Create password reset link">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<main>
|
<main>
|
||||||
<nav class="darkbg">
|
<nav class="darkbg">
|
||||||
<h1 class="thread-title">{{ thread.title }}</h1>
|
<h1 class="thread-title">{{ thread.title }}{% if unread_count is not none %} ({{ unread_count }} unread){% endif %}</h1>
|
||||||
<span>Posted in <a href="{{ url_for("topics.topic", slug=topic.slug) }}">{{ topic.name }}</a>
|
<span>Posted in <a href="{{ url_for("topics.topic", slug=topic.slug) }}">{{ topic.name }}</a>
|
||||||
{% if thread.is_stickied %}
|
{% if thread.is_stickied %}
|
||||||
• <i>stickied, so it's probably important</i>
|
• <i>stickied, so it's probably important</i>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{{ full_post(post = post, active_user = active_user, is_latest = loop.index == (posts | length)) }}
|
{{ full_post(post = post, active_user = active_user, is_latest = loop.index == (posts | length), Reactions = Reactions) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -72,6 +72,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<input type='hidden' id='allowed-reaction-emoji' value='{{ REACTION_EMOJI | join(' ') }}'>
|
||||||
<input type='hidden' id='thread-subscribe-endpoint' value='{{ url_for('api.thread_updates', thread_id=thread.id) }}'>
|
<input type='hidden' id='thread-subscribe-endpoint' value='{{ url_for('api.thread_updates', thread_id=thread.id) }}'>
|
||||||
<div id="new-post-notification" class="new-concept-notification hidden">
|
<div id="new-post-notification" class="new-concept-notification hidden">
|
||||||
<div class="new-notification-content">
|
<div class="new-notification-content">
|
||||||
@ -83,5 +84,5 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/thread.js?v=1"></script>
|
<script src="{{ "/static/js/thread.js" | cachebust }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -40,6 +40,9 @@
|
|||||||
<div class="thread-info-container">
|
<div class="thread-info-container">
|
||||||
<span>
|
<span>
|
||||||
<span class="thread-title"><a href="{{ url_for("threads.thread", slug=thread['slug']) }}">{{thread['title']}}</a></span>
|
<span class="thread-title"><a href="{{ url_for("threads.thread", slug=thread['slug']) }}">{{thread['title']}}</a></span>
|
||||||
|
{% if thread['id'] in subscriptions %}
|
||||||
|
({{ subscriptions[thread['id']] }} unread)
|
||||||
|
{% endif %}
|
||||||
•
|
•
|
||||||
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'] }}</a> on {{ timestamp(thread['created_at'])}}
|
||||||
</span>
|
</span>
|
||||||
@ -75,5 +78,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="/static/js/topic.js"></script>
|
<script src="{{ "/static/js/topic.js" | cachebust }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
{% if topic['id'] in active_threads %}
|
{% if topic['id'] in active_threads %}
|
||||||
{% with thread=active_threads[topic['id']] %}
|
{% with thread=active_threads[topic['id']] %}
|
||||||
<span>
|
<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="">{{ 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['username'] }}</a> at <a href="{{ get_post_url(thread.post_id, _anchor=true) }}">{{ timestamp(thread['post_created_at']) }}</a>
|
||||||
</span>
|
</span>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
{% if section == "header" %}
|
{% if section == "header" %}
|
||||||
{% set latest_post_id = thread.posts[-1].id %}
|
{% set latest_post_id = thread.posts[-1].id %}
|
||||||
{% set unread_posts_text = " (" + (thread.unread_count | string) + (" unread post" | pluralize(num=thread.unread_count)) %}
|
{% set unread_posts_text = " (" + (thread.unread_count | string) + (" unread post" | pluralize(num=thread.unread_count)) %}
|
||||||
<a class="accordion-title" href="{{ url_for("threads.thread", slug=latest_post_slug, after=latest_post_id, _anchor="post-" + (latest_post_id | string)) }}" title="Jump to latest post">{{thread.thread_title + unread_posts_text}}, latest at {{ timestamp(thread.newest_post_time) }})</a>
|
<a class="accordion-title" href="{{ url_for("threads.thread", slug=thread.thread_slug, after=latest_post_id, _anchor="post-" + (latest_post_id | string)) }}" title="Jump to latest post">{{thread.thread_title + unread_posts_text}}, latest at {{ timestamp(thread.newest_post_time) }})</a>
|
||||||
<form class="modform" method="post" action="{{ url_for("threads.subscribe", slug = thread.thread_slug) }}">
|
<form class="modform" method="post" action="{{ url_for("threads.subscribe", slug = thread.thread_slug) }}">
|
||||||
<input type="hidden" name="subscribe" value="read">
|
<input type="hidden" name="subscribe" value="read">
|
||||||
<input type="submit" value="Mark thread as Read">
|
<input type="submit" value="Mark thread as Read">
|
||||||
|
36
app/templates/users/invite_links.html
Normal file
36
app/templates/users/invite_links.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% from 'common/macros.html' import accordion %}
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}invites{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="darkbg inbox-container">
|
||||||
|
<p>To manage growth, {{ config.SITE_NAME }} disallows direct sign ups. Instead, users already with an account may invite people they know. You can create invite links here. Once an invite link is used to sign up, it can no longer be used.</p>
|
||||||
|
{% call(section) accordion(disabled=invites | length == 0) %}
|
||||||
|
{% if section == 'header' %}
|
||||||
|
Your invites
|
||||||
|
{% else %}
|
||||||
|
{% if invites %}
|
||||||
|
<table class="colorful-table">
|
||||||
|
<thead>
|
||||||
|
<th class='small'>Link</th>
|
||||||
|
<th class='small'>Revoke</th>
|
||||||
|
</thead>
|
||||||
|
{% for invite in invites %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{url_for('users.sign_up', key=invite.key)}}">Link</a></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{{ url_for('users.revoke_invite_link', username=active_user.username) }}">
|
||||||
|
<input type=hidden value="{{ invite.key }}" name="key">
|
||||||
|
<input type=submit class=warn value="Revoke">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endcall %}
|
||||||
|
<form method="post" action="{{ url_for('users.create_invite_link', username=active_user.username) }}">
|
||||||
|
<input type=submit value="Create new invite">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
15
app/templates/users/reset_link_login.html
Normal file
15
app/templates/users/reset_link_login.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Reset password{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="darkbg login-container">
|
||||||
|
<h1>Reset password for {{username}}</h1>
|
||||||
|
<p>Send this link to {{username}} to allow them to reset their password.</p>
|
||||||
|
<form method="post">
|
||||||
|
<label for="password">New password</label><br>
|
||||||
|
<input type="password" id="password" name="password" autocomplete="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><br>
|
||||||
|
<label for="password2">Confirm password</label><br>
|
||||||
|
<input type="password" id="password2" name="password2" autocomplete="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><br>
|
||||||
|
<input type="submit" value="Reset password">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -15,6 +15,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form method='post'>
|
<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>
|
<label for='topic_sort_by'>Sort threads by:</label>
|
||||||
<select id='topic_sort_by' name='topic_sort_by'>
|
<select id='topic_sort_by' name='topic_sort_by'>
|
||||||
<option value='activity' {{ 'selected' if session['sort_by'] == 'activity' else '' }}>Latest activity</option>
|
<option value='activity' {{ 'selected' if session['sort_by'] == 'activity' else '' }}>Latest activity</option>
|
||||||
|
@ -3,7 +3,13 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="darkbg login-container">
|
<div class="darkbg login-container">
|
||||||
<h1>Sign up</h1>
|
<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>
|
||||||
|
{% endif %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
{% if key %}
|
||||||
|
<input type="hidden" value={{key}} name="key">
|
||||||
|
{% endif %}
|
||||||
<label for="username">Username</label><br>
|
<label for="username">Username</label><br>
|
||||||
<input type="text" id="username" name="username" pattern="[a-zA-Z0-9_-]{3,20}" title="3-20 characters. Only upper and lowercase letters, digits, hyphens, and underscores" required autocomplete="username"><br>
|
<input type="text" id="username" name="username" pattern="[a-zA-Z0-9_-]{3,20}" title="3-20 characters. Only upper and lowercase letters, digits, hyphens, and underscores" required autocomplete="username"><br>
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
@ -12,6 +18,8 @@
|
|||||||
<input type="password" id="password-confirm" name="password-confirm" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
<input type="password" id="password-confirm" name="password-confirm" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
||||||
<input type="submit" value="Sign up">
|
<input type="submit" value="Sign up">
|
||||||
</form>
|
</form>
|
||||||
|
{% if not inviter %}
|
||||||
<span>After you sign up, a moderator will need to confirm your account before you will be allowed to post.</span>
|
<span>After you sign up, a moderator will need to confirm your account before you will be allowed to post.</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -64,6 +64,9 @@
|
|||||||
{% if stats.latest_thread_title %}
|
{% if stats.latest_thread_title %}
|
||||||
<li>Latest started thread: <a href="{{ url_for("threads.thread", slug = stats.latest_thread_slug) }}">{{ stats.latest_thread_title }}</a>
|
<li>Latest started thread: <a href="{{ url_for("threads.thread", slug = stats.latest_thread_slug) }}">{{ stats.latest_thread_title }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if stats.inviter_username %}
|
||||||
|
<li>Invited by <a href="{{ url_for('users.page', username=stats.inviter_username) }}">{{ stats.inviter_username }}</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
Latest posts:
|
Latest posts:
|
||||||
@ -80,8 +83,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</i></a>
|
</i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content wider user-page-post-preview">
|
<div class="post-content user-page-post-preview">
|
||||||
<div class="post-inner">{{ post.content | safe }}</div>
|
<div class="post-inner wider">{{ post.content | safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
20
build-themes.sh
Executable file
20
build-themes.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
sass_dir="sass"
|
||||||
|
css_dir="data/static/css"
|
||||||
|
|
||||||
|
if [[ "$1" == "--watch" && -n "$2" ]]; then
|
||||||
|
file="$2"
|
||||||
|
[[ $(basename "$file") = _* ]] && exit 1
|
||||||
|
sass --no-source-map --watch "$file" "$css_dir/theme-$(basename "$file" .scss).css"
|
||||||
|
else
|
||||||
|
set -u
|
||||||
|
rm -r "$css_dir/"
|
||||||
|
#build default first
|
||||||
|
sass --no-source-map "$sass_dir/_default.scss" "$css_dir/style.css"
|
||||||
|
for file in "$sass_dir"/*.scss; do
|
||||||
|
[[ $(basename "$file") = _* ]] && continue
|
||||||
|
sass --no-source-map "$file" "$css_dir/theme-$(basename "$file" .scss).css"
|
||||||
|
done
|
||||||
|
fi
|
8
config/pyrom_config.toml
Normal file
8
config/pyrom_config.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
SITE_NAME = "Porom"
|
||||||
|
DISABLE_SIGNUP = false # if true, no one can sign up.
|
||||||
|
|
||||||
|
# if neither of the following two options is true,
|
||||||
|
# no one can sign up. this may be useful later when/if LDAP is implemented.
|
||||||
|
|
||||||
|
MODS_CAN_INVITE = true # if true, allows moderators to create invite links. useless unless DISABLE_SIGNUP to be true.
|
||||||
|
USERS_CAN_INVITE = false # if true, allows users to create invite links. useless unless DISABLE_SIGNUP to be true.
|
@ -26,14 +26,13 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.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 {
|
.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;
|
cursor: default;
|
||||||
color: black;
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
font-family: "Cadman";
|
font-family: "Cadman";
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
@ -42,6 +41,14 @@ body {
|
|||||||
font-family: "Cadman";
|
font-family: "Cadman";
|
||||||
margin: 20px 100px;
|
margin: 20px 100px;
|
||||||
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: #c11c1c;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #730c0c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
@ -50,6 +57,7 @@ body {
|
|||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
background-color: #c1ceb1;
|
background-color: #c1ceb1;
|
||||||
@ -59,6 +67,7 @@ body {
|
|||||||
|
|
||||||
#bottomnav {
|
#bottomnav {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
|
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
|
||||||
@ -81,7 +90,7 @@ body {
|
|||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
@ -119,38 +128,58 @@ body {
|
|||||||
.post-content-container {
|
.post-content-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 70px 2.5fr;
|
grid-template-rows: min-content 1fr min-content;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas: "post-info" "post-content";
|
grid-template-areas: "post-info" "post-content" "post-reactions";
|
||||||
grid-area: post-content-container;
|
grid-area: post-content-container;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-info {
|
.post-info {
|
||||||
grid-area: post-info;
|
grid-area: post-info;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-height: 70px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
|
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-content {
|
.post-content {
|
||||||
grid-area: post-content;
|
grid-area: post-content;
|
||||||
padding: 20px;
|
padding: 20px 20px 0 20px;
|
||||||
margin-right: 25%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background-color: #c1ceb1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-content.wider {
|
.post-reactions {
|
||||||
margin-right: 12.5%;
|
grid-area: post-reactions;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 20px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||||
|
border-top: 2px dotted gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-inner {
|
.post-inner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding-right: 25%;
|
||||||
|
}
|
||||||
|
.post-inner.wider {
|
||||||
|
padding-right: 12.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-container {
|
||||||
|
border-top: 2px dotted gray;
|
||||||
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code {
|
pre code {
|
||||||
@ -166,51 +195,6 @@ pre code {
|
|||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-code {
|
|
||||||
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
|
|
||||||
color: white;
|
|
||||||
padding: 5px 10px;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#delete-dialog, .lightbox-dialog {
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 2px solid black;
|
|
||||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-dialog-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightbox-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 20px;
|
|
||||||
min-width: 400px;
|
|
||||||
background-color: #c1ceb1;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightbox-image {
|
|
||||||
max-width: 70vw;
|
|
||||||
max-height: 70vh;
|
|
||||||
object-fit: scale-down;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightbox-nav {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-code-container {
|
.copy-code-container {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
width: calc(100% - 4px);
|
width: calc(100% - 4px);
|
||||||
@ -235,12 +219,59 @@ pre code {
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-code {
|
||||||
|
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
#delete-dialog, .lightbox-dialog {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid black;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-dialog-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 400px;
|
||||||
|
background-color: #c1ceb1;
|
||||||
|
color: black;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-image {
|
||||||
|
max-width: 70vw;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: scale-down;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-left: 10px solid rgb(229.84, 231.92, 227.28);
|
border-left: 10px solid rgb(229.84, 231.92, 227.28);
|
||||||
background-color: rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
background-color: rgba(0, 0, 0, 0.1490196078);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
@ -271,9 +302,9 @@ blockquote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-page-posts {
|
.user-page-posts {
|
||||||
border-left: solid 1px black;
|
border-left: 1px solid black;
|
||||||
border-right: solid 1px black;
|
border-right: 1px solid black;
|
||||||
border-bottom: solid 1px black;
|
border-bottom: 1px solid black;
|
||||||
background-color: #c1ceb1;
|
background-color: #c1ceb1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +331,7 @@ blockquote {
|
|||||||
button, input[type=submit], .linkbutton {
|
button, input[type=submit], .linkbutton {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
|
color: black !important;
|
||||||
}
|
}
|
||||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||||
background-color: rgb(192.6, 215.8, 214.6);
|
background-color: rgb(192.6, 215.8, 214.6);
|
||||||
@ -315,8 +347,8 @@ button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||||
color: white;
|
|
||||||
background-color: red;
|
background-color: red;
|
||||||
|
color: white !important;
|
||||||
}
|
}
|
||||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||||
background-color: #ff3333;
|
background-color: #ff3333;
|
||||||
@ -333,6 +365,7 @@ button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critic
|
|||||||
}
|
}
|
||||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||||
background-color: #fbfb8d;
|
background-color: #fbfb8d;
|
||||||
|
color: black !important;
|
||||||
}
|
}
|
||||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||||
background-color: rgb(251.8, 251.8, 163.8);
|
background-color: rgb(251.8, 251.8, 163.8);
|
||||||
@ -350,7 +383,8 @@ button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
|||||||
|
|
||||||
input[type=file]::file-selector-button {
|
input[type=file]::file-selector-button {
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
margin: 10px 10px;
|
color: black !important;
|
||||||
|
margin: 10px;
|
||||||
}
|
}
|
||||||
input[type=file]::file-selector-button:hover {
|
input[type=file]::file-selector-button:hover {
|
||||||
background-color: rgb(192.6, 215.8, 214.6);
|
background-color: rgb(192.6, 215.8, 214.6);
|
||||||
@ -372,6 +406,7 @@ p {
|
|||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
|
color: black !important;
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -406,7 +441,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-container > * {
|
.login-container > * {
|
||||||
width: 25%;
|
width: 40%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,11 +459,12 @@ p {
|
|||||||
|
|
||||||
input[type=text], input[type=password], textarea, select {
|
input[type=text], input[type=password], textarea, select {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
color: black;
|
||||||
background-color: rgb(217.8, 225.6, 208.2);
|
background-color: rgb(217.8, 225.6, 208.2);
|
||||||
}
|
}
|
||||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
@ -439,12 +475,15 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
background-color: #81a3e6;
|
background-color: #81a3e6;
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
.infobox.critical {
|
.infobox.critical {
|
||||||
background-color: rgb(237, 129, 129);
|
background-color: #ed8181;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
.infobox.warn {
|
.infobox.warn {
|
||||||
background-color: #fbfb8d;
|
background-color: #fbfb8d;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infobox > span {
|
.infobox > span {
|
||||||
@ -461,7 +500,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 96px 1.6fr 96px;
|
grid-template-columns: 96px 1.6fr 96px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
min-height: 96px;
|
min-height: 96px;
|
||||||
grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
|
grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
|
||||||
@ -470,11 +509,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
.thread-sticky-container {
|
.thread-sticky-container {
|
||||||
grid-area: thread-sticky-container;
|
grid-area: thread-sticky-container;
|
||||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||||
|
background-color: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-locked-container {
|
.thread-locked-container {
|
||||||
grid-area: thread-locked-container;
|
grid-area: thread-locked-container;
|
||||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||||
|
background-color: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contain-svg {
|
.contain-svg {
|
||||||
@ -492,10 +533,21 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-img {
|
.post-img-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
min-width: 200px;
|
||||||
|
min-height: 200px;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-info-container {
|
.thread-info-container {
|
||||||
@ -529,7 +581,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 300px;
|
grid-template-columns: 1.5fr 300px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas: "guide-topics guide-toc";
|
grid-template-areas: "guide-topics guide-toc";
|
||||||
}
|
}
|
||||||
@ -596,7 +648,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 96px;
|
grid-template-columns: 1.5fr 96px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas: "topic-info-container topic-locked-container";
|
grid-template-areas: "topic-info-container topic-locked-container";
|
||||||
}
|
}
|
||||||
@ -613,6 +665,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
.topic-locked-container {
|
.topic-locked-container {
|
||||||
grid-area: topic-locked-container;
|
grid-area: topic-locked-container;
|
||||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||||
|
background-color: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-topic {
|
.draggable-topic {
|
||||||
@ -620,9 +673,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: #c1ceb1;
|
background-color: #c1ceb1;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 12px 0;
|
margin: 15px 0;
|
||||||
border-top: 6px outset rgb(217.26, 220.38, 213.42);
|
border-top: 5px outset rgb(217.26, 220.38, 213.42);
|
||||||
border-bottom: 6px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||||
}
|
}
|
||||||
.draggable-topic.dragged {
|
.draggable-topic.dragged {
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
@ -660,6 +713,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
|
color: black !important;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
@ -692,9 +746,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
background-color: rgb(191.3137931034, 189.7, 193.3);
|
background-color: rgb(191.3137931034, 189.7, 193.3);
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 4px;
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 4px;
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol {
|
ul, ol {
|
||||||
@ -710,7 +764,7 @@ ul, ol {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
right: 80px;
|
right: 80px;
|
||||||
border: 2px solid black;
|
border: 1px solid black;
|
||||||
background-color: #81a3e6;
|
background-color: #81a3e6;
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -723,8 +777,8 @@ ul, ol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accordion {
|
.accordion {
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 4px;
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
margin: 10px 5px;
|
margin: 10px 5px;
|
||||||
@ -767,13 +821,20 @@ ul, ol {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-accordion-content {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||||
|
}
|
||||||
|
|
||||||
.inbox-container {
|
.inbox-container {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.babycode-button-container {
|
.babycode-button-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.babycode-button {
|
.babycode-button {
|
||||||
@ -797,3 +858,43 @@ ul, ol {
|
|||||||
footer {
|
footer {
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reaction-button.active {
|
||||||
|
background-color: #beb1ce;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
.reaction-button.active:hover {
|
||||||
|
background-color: rgb(203, 192.6, 215.8);
|
||||||
|
}
|
||||||
|
.reaction-button.active:active {
|
||||||
|
background-color: rgb(171.7642913386, 166.6881496063, 178.0118503937);
|
||||||
|
}
|
||||||
|
.reaction-button.active:disabled {
|
||||||
|
background-color: rgb(210.445, 209.535, 211.565);
|
||||||
|
}
|
||||||
|
.reaction-button.active.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-popover {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||||
|
padding: 5px 10px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-popover-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: scroll;
|
||||||
|
margin: auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-guide-list {
|
||||||
|
border-bottom: 1px dashed;
|
||||||
|
}
|
@ -1,99 +1,54 @@
|
|||||||
@use "sass:color";
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
src: url("/static/fonts/ChicagoFLF.woff2");
|
src: url("/static/fonts/ChicagoFLF.woff2");
|
||||||
}
|
}
|
||||||
|
@font-face {
|
||||||
@mixin cadman($var) {
|
|
||||||
font-family: "Cadman";
|
font-family: "Cadman";
|
||||||
src: url("/static/fonts/Cadman_#{$var}.woff2");
|
src: url("/static/fonts/Cadman_Roman.woff2");
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
@include cadman("Roman");
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@include cadman("Bold");
|
font-family: "Cadman";
|
||||||
|
src: url("/static/fonts/Cadman_Bold.woff2");
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@include cadman("Italic");
|
font-family: "Cadman";
|
||||||
|
src: url("/static/fonts/Cadman_Italic.woff2");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@include cadman("BoldItalic");
|
font-family: "Cadman";
|
||||||
|
src: url("/static/fonts/Cadman_BoldItalic.woff2");
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.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 {
|
||||||
$accent_color: #c1ceb1;
|
|
||||||
|
|
||||||
$dark_bg: color.scale($accent_color, $lightness: -25%, $saturation: -97%);
|
|
||||||
$dark2: color.scale($accent_color, $lightness: -30%, $saturation: -60%);
|
|
||||||
$verydark: color.scale($accent_color, $lightness: -80%, $saturation: -70%);
|
|
||||||
|
|
||||||
$light: color.scale($accent_color, $lightness: 40%, $saturation: -60%);
|
|
||||||
$lighter: color.scale($accent_color, $lightness: 60%, $saturation: -60%);
|
|
||||||
|
|
||||||
$main_bg: color.scale($accent_color, $lightness: -10%, $saturation: -40%);
|
|
||||||
$button_color: color.adjust($accent_color, $hue: 90);
|
|
||||||
$button_color2: color.adjust($accent_color, $hue: 180);
|
|
||||||
$accordion_color: color.adjust($accent_color, $hue: 140, $lightness: -10%, $saturation: -15%);
|
|
||||||
|
|
||||||
%button-base {
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: black;
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
font-family: "Cadman";
|
font-family: "Cadman";
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin button($color) {
|
|
||||||
@extend %button-base;
|
|
||||||
background-color: $color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: color.scale($color, $lightness: 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: color.scale($color, $lightness: -10%, $saturation: -70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: color.scale($color, $lightness: 30%, $saturation: -90%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reduced {
|
|
||||||
margin: 0;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin navbar($color) {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: end;
|
|
||||||
background-color: $color;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: "Cadman";
|
font-family: "Cadman";
|
||||||
// font-size: 18px;
|
|
||||||
margin: 20px 100px;
|
margin: 20px 100px;
|
||||||
background-color: $main_bg;
|
background-color: #220d16;
|
||||||
|
color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: #e87fe1;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #ed4fb1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
@ -101,20 +56,28 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
@include navbar($accent_color);
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
background-color: #303030;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#bottomnav {
|
#bottomnav {
|
||||||
@include navbar($dark_bg);
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
background-color: #231c23;
|
||||||
}
|
}
|
||||||
|
|
||||||
.darkbg {
|
.darkbg {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
background-color: $dark_bg;
|
background-color: #502d50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-actions {
|
.user-actions {
|
||||||
@ -127,7 +90,7 @@ body {
|
|||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
@ -142,16 +105,15 @@ body {
|
|||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas:
|
grid-template-areas: "usercard post-content-container";
|
||||||
"usercard post-content-container";
|
border: 2px outset rgb(96.95, 81.55, 96.95);
|
||||||
border: 2px outset $dark2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.usercard {
|
.usercard {
|
||||||
grid-area: usercard;
|
grid-area: usercard;
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
border: 4px outset $light;
|
border: 4px outset #503250;
|
||||||
background-color: $dark_bg;
|
background-color: #502d50;
|
||||||
border-right: solid 2px;
|
border-right: solid 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,63 +128,106 @@ body {
|
|||||||
.post-content-container {
|
.post-content-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 70px 2.5fr;
|
grid-template-rows: min-content 1fr min-content;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas:
|
grid-template-areas: "post-info" "post-content" "post-reactions";
|
||||||
"post-info"
|
|
||||||
"post-content";
|
|
||||||
grid-area: post-content-container;
|
grid-area: post-content-container;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-info {
|
.post-info {
|
||||||
grid-area: post-info;
|
grid-area: post-info;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-height: 70px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
|
background-color: #412841;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-content {
|
.post-content {
|
||||||
grid-area: post-content;
|
grid-area: post-content;
|
||||||
padding: 20px;
|
padding: 20px 20px 0 20px;
|
||||||
margin-right: 25%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background-color: #231c23;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-content.wider {
|
.post-reactions {
|
||||||
margin-right: 12.5%;
|
grid-area: post-reactions;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 20px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
background-color: #503250;
|
||||||
|
border-top: 2px dotted gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-inner {
|
.post-inner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding-right: 25%;
|
||||||
|
}
|
||||||
|
.post-inner.wider {
|
||||||
|
padding-right: 12.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-container {
|
||||||
|
border-top: 2px dotted gray;
|
||||||
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code {
|
pre code {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: $verydark;
|
background-color: #302731;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: white;
|
color: white;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
border-left: 10px solid $lighter;
|
border-left: 10px solid #ae6bae;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-code-container {
|
||||||
|
position: sticky;
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: last baseline;
|
||||||
|
font-family: "Cadman";
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
background-color: #9b649b;
|
||||||
|
border-left: 2px solid black;
|
||||||
|
border-right: 2px solid black;
|
||||||
|
border-top: 2px solid black;
|
||||||
|
}
|
||||||
|
.copy-code-container::before {
|
||||||
|
content: "code block";
|
||||||
|
font-style: italic;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-code {
|
.inline-code {
|
||||||
background-color: $verydark;
|
background-color: #302731;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
#delete-dialog, .lightbox-dialog {
|
#delete-dialog, .lightbox-dialog {
|
||||||
@ -244,7 +249,8 @@ pre code {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
background-color: $accent_color;
|
background-color: #503250;
|
||||||
|
color: #e6e6e6;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,38 +266,12 @@ pre code {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-code-container {
|
|
||||||
position: sticky;
|
|
||||||
// width: 100%;
|
|
||||||
width: calc(100% - 4px);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: last baseline;
|
|
||||||
font-family: "Cadman";
|
|
||||||
border-top-right-radius: 8px;
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
background-color: $accent_color;
|
|
||||||
border-left: 2px solid black;
|
|
||||||
border-right: 2px solid black;
|
|
||||||
border-top: 2px solid black;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "code block";
|
|
||||||
font-style: italic;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-code {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-left: 10px solid $lighter;
|
border-left: 10px solid #ae6bae;
|
||||||
background-color: $dark2;
|
background-color: rgba(251, 175, 207, 0.0392156863);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
@ -299,15 +279,14 @@ blockquote {
|
|||||||
grid-template-columns: 300px 1fr;
|
grid-template-columns: 300px 1fr;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
grid-template-areas:
|
grid-template-areas: "user-page-usercard user-page-stats";
|
||||||
"user-page-usercard user-page-stats";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-page-usercard {
|
.user-page-usercard {
|
||||||
grid-area: user-page-usercard;
|
grid-area: user-page-usercard;
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
border: 4px outset $light;
|
border: 4px outset #503250;
|
||||||
background-color: $dark_bg;
|
background-color: #502d50;
|
||||||
border-right: solid 2px;
|
border-right: solid 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,15 +302,15 @@ blockquote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-page-posts {
|
.user-page-posts {
|
||||||
border-left: solid 1px black;
|
border-left: 1px solid black;
|
||||||
border-right: solid 1px black;
|
border-right: 1px solid black;
|
||||||
border-bottom: solid 1px black;
|
border-bottom: 1px solid black;
|
||||||
background-color: $accent_color;
|
background-color: #9b649b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-page-post-preview {
|
.user-page-post-preview {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
mask-image: linear-gradient(180deg,#000 60%,transparent);
|
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@ -349,24 +328,76 @@ blockquote {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input[type="submit"], .linkbutton {
|
button, input[type=submit], .linkbutton {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@include button($button_color);
|
background-color: #3c283c;
|
||||||
|
color: #e6e6e6 !important;
|
||||||
&.critical {
|
}
|
||||||
color: white;
|
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||||
@include button(red);
|
background-color: rgb(109.2, 72.8, 109.2);
|
||||||
}
|
}
|
||||||
|
button:active, input[type=submit]:active, .linkbutton:active {
|
||||||
&.warn {
|
background-color: rgb(47.7, 42.3, 47.7);
|
||||||
@include button(#fbfb8d);
|
}
|
||||||
}
|
button:disabled, input[type=submit]:disabled, .linkbutton:disabled {
|
||||||
|
background-color: rgb(113.73, 109.27, 113.73);
|
||||||
|
}
|
||||||
|
button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||||
|
background-color: #d53232;
|
||||||
|
color: #e6e6e6 !important;
|
||||||
|
}
|
||||||
|
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||||
|
background-color: rgb(221.4, 91, 91);
|
||||||
|
}
|
||||||
|
button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active {
|
||||||
|
background-color: rgb(141.7804251012, 94.9195748988, 94.9195748988);
|
||||||
|
}
|
||||||
|
button.critical:disabled, input[type=submit].critical:disabled, .linkbutton.critical:disabled {
|
||||||
|
background-color: rgb(174.255, 162.845, 162.845);
|
||||||
|
}
|
||||||
|
button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critical.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||||
|
background-color: #eaea6a;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||||
|
background-color: rgb(238.2, 238.2, 135.8);
|
||||||
|
}
|
||||||
|
button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active {
|
||||||
|
background-color: rgb(176.04, 176.04, 129.96);
|
||||||
|
}
|
||||||
|
button.warn:disabled, input[type=submit].warn:disabled, .linkbutton.warn:disabled {
|
||||||
|
background-color: rgb(199.98, 199.98, 191.02);
|
||||||
|
}
|
||||||
|
button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// not sure why this one has to be separate, but if it's included in the rule above everything breaks
|
input[type=file]::file-selector-button {
|
||||||
input[type="file"]::file-selector-button {
|
background-color: #3c283c;
|
||||||
@include button($button_color);
|
color: #e6e6e6 !important;
|
||||||
margin: 10px 10px;
|
margin: 10px;
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button:hover {
|
||||||
|
background-color: rgb(109.2, 72.8, 109.2);
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button:active {
|
||||||
|
background-color: rgb(47.7, 42.3, 47.7);
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button:disabled {
|
||||||
|
background-color: rgb(113.73, 109.27, 113.73);
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@ -374,16 +405,29 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
@include button($button_color);
|
background-color: #3c283c;
|
||||||
|
color: #e6e6e6 !important;
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.pagebutton:hover {
|
||||||
|
background-color: rgb(109.2, 72.8, 109.2);
|
||||||
|
}
|
||||||
|
.pagebutton:active {
|
||||||
|
background-color: rgb(47.7, 42.3, 47.7);
|
||||||
|
}
|
||||||
|
.pagebutton:disabled {
|
||||||
|
background-color: rgb(113.73, 109.27, 113.73);
|
||||||
|
}
|
||||||
|
.pagebutton.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.currentpage {
|
.currentpage {
|
||||||
@extend %button-base;
|
|
||||||
border: none;
|
border: none;
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -397,7 +441,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-container > * {
|
.login-container > * {
|
||||||
width: 25%;
|
width: 40%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,32 +457,33 @@ p {
|
|||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"], input[type="password"], textarea, select {
|
input[type=text], input[type=password], textarea, select {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
background-color: color.scale($accent_color, $lightness: 40%);
|
color: #e6e6e6;
|
||||||
|
background-color: #371e37;
|
||||||
&:focus {
|
}
|
||||||
background-color: color.scale($accent_color, $lightness: 60%);
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
}
|
background-color: #514151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infobox {
|
.infobox {
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
background-color: #81a3e6;
|
background-color: #775891;
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
|
color: #e6e6e6;
|
||||||
&.critical {
|
}
|
||||||
background-color: rgb(237, 129, 129);
|
.infobox.critical {
|
||||||
}
|
background-color: #d53232;
|
||||||
|
color: #e6e6e6;
|
||||||
&.warn {
|
}
|
||||||
background-color: #fbfb8d;
|
.infobox.warn {
|
||||||
}
|
background-color: #eaea6a;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infobox > span {
|
.infobox > span {
|
||||||
@ -455,21 +500,22 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 96px 1.6fr 96px;
|
grid-template-columns: 96px 1.6fr 96px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
min-height: 96px;
|
min-height: 96px;
|
||||||
grid-template-areas:
|
grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
|
||||||
"thread-sticky-container thread-info-container thread-locked-container";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-sticky-container {
|
.thread-sticky-container {
|
||||||
grid-area: thread-sticky-container;
|
grid-area: thread-sticky-container;
|
||||||
border: 2px outset $light;
|
border: 2px outset #231c23;
|
||||||
|
background-color: #503250;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-locked-container {
|
.thread-locked-container {
|
||||||
grid-area: thread-locked-container;
|
grid-area: thread-locked-container;
|
||||||
border: 2px outset $light;
|
border: 2px outset #231c23;
|
||||||
|
background-color: #503250;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contain-svg {
|
.contain-svg {
|
||||||
@ -477,25 +523,36 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
&:not(.full) > svg, &:not(.full) > img {
|
}
|
||||||
height: 50%;
|
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||||
width: 50%;
|
height: 50%;
|
||||||
}
|
width: 50%;
|
||||||
&.full > svg, &.full > img {
|
}
|
||||||
height: 100%;
|
.contain-svg.full > svg, .contain-svg.full > img {
|
||||||
width: 100%;
|
height: 100%;
|
||||||
}
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-img {
|
.post-img-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
min-width: 200px;
|
||||||
|
min-height: 200px;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-info-container {
|
.thread-info-container {
|
||||||
grid-area: thread-info-container;
|
grid-area: thread-info-container;
|
||||||
background-color: $accent_color;
|
background-color: #231c23;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
@ -503,7 +560,7 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 110px;
|
max-height: 110px;
|
||||||
mask-image: linear-gradient(180deg,#000 60%,transparent);
|
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-info-post-preview {
|
.thread-info-post-preview {
|
||||||
@ -514,7 +571,7 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.babycode-guide-section {
|
.babycode-guide-section {
|
||||||
background-color: $accent_color;
|
background-color: #231c23;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding-right: 25%;
|
padding-right: 25%;
|
||||||
@ -524,10 +581,9 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 300px;
|
grid-template-columns: 1.5fr 300px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas:
|
grid-template-areas: "guide-topics guide-toc";
|
||||||
"guide-topics guide-toc";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-topics {
|
.guide-topics {
|
||||||
@ -541,9 +597,8 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
top: 100px;
|
top: 100px;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
// border-top-right-radius: 16px;
|
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
background-color: $button_color;
|
background-color: #3c233c;
|
||||||
border-right: 1px solid black;
|
border-right: 1px solid black;
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
@ -575,12 +630,12 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.colorful-table tr th {
|
.colorful-table tr th {
|
||||||
background-color: $button_color2;
|
background-color: #503250;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorful-table tr td {
|
.colorful-table tr td {
|
||||||
background-color: $button_color;
|
background-color: #231c23;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -593,15 +648,14 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 96px;
|
grid-template-columns: 1.5fr 96px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas:
|
grid-template-areas: "topic-info-container topic-locked-container";
|
||||||
"topic-info-container topic-locked-container";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-info-container {
|
.topic-info-container {
|
||||||
grid-area: topic-info-container;
|
grid-area: topic-info-container;
|
||||||
background-color: $accent_color;
|
background-color: #231c23;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -610,26 +664,25 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
|
|
||||||
.topic-locked-container {
|
.topic-locked-container {
|
||||||
grid-area: topic-locked-container;
|
grid-area: topic-locked-container;
|
||||||
border: 2px outset $light;
|
border: 2px outset #231c23;
|
||||||
|
background-color: #503250;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.draggable-topic {
|
.draggable-topic {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: $accent_color;
|
background-color: #9b649b;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 12px 0;
|
margin: 15px 0;
|
||||||
border-top: 6px outset $light;
|
border-top: 5px outset #503250;
|
||||||
border-bottom: 6px outset $dark2;
|
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
|
||||||
|
}
|
||||||
&.dragged {
|
.draggable-topic.dragged {
|
||||||
background-color: $button_color;
|
background-color: #3c283c;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editing {
|
.editing {
|
||||||
background-color: $light;
|
background-color: #503250;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-explain {
|
.context-explain {
|
||||||
@ -659,31 +712,43 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@include button($button_color);
|
background-color: #3c283c;
|
||||||
|
color: #e6e6e6 !important;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
&.active {
|
.tab-button:hover {
|
||||||
background-color: $button_color2;
|
background-color: rgb(109.2, 72.8, 109.2);
|
||||||
padding-top: 8px;
|
}
|
||||||
}
|
.tab-button:active {
|
||||||
|
background-color: rgb(47.7, 42.3, 47.7);
|
||||||
|
}
|
||||||
|
.tab-button:disabled {
|
||||||
|
background-color: rgb(113.73, 109.27, 113.73);
|
||||||
|
}
|
||||||
|
.tab-button.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
background-color: #8a5584;
|
||||||
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
display: none;
|
display: none;
|
||||||
|
}
|
||||||
&.active {
|
.tab-content.active {
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
display: block;
|
display: block;
|
||||||
background-color: color.adjust($button_color2, $saturation: -20%);
|
background-color: #503250;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 4px;
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 4px;
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 4px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol {
|
ul, ol {
|
||||||
@ -699,8 +764,8 @@ ul, ol {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
right: 80px;
|
right: 80px;
|
||||||
border: 2px solid black;
|
border: 1px solid black;
|
||||||
background-color: #81a3e6;
|
background-color: #775891;
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||||
@ -712,8 +777,8 @@ ul, ol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accordion {
|
.accordion {
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 4px;
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
margin: 10px 5px;
|
margin: 10px 5px;
|
||||||
@ -727,7 +792,7 @@ ul, ol {
|
|||||||
.accordion-header {
|
.accordion-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: $accordion_color;
|
background-color: #7d467d;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
@ -756,22 +821,28 @@ ul, ol {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-accordion-content {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
background-color: #2d212d;
|
||||||
|
}
|
||||||
|
|
||||||
.inbox-container {
|
.inbox-container {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.babycode-button-container {
|
.babycode-button-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.babycode-button {
|
.babycode-button {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
|
}
|
||||||
&> * {
|
.babycode-button > * {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-popover {
|
.quote-popover {
|
||||||
@ -780,10 +851,59 @@ ul, ol {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #00000080;
|
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reaction-button.active {
|
||||||
|
background-color: #8a5584;
|
||||||
|
color: #e6e6e6 !important;
|
||||||
|
}
|
||||||
|
.reaction-button.active:hover {
|
||||||
|
background-color: rgb(167.4843049327, 112.9156950673, 161.3067264574);
|
||||||
|
}
|
||||||
|
.reaction-button.active:active {
|
||||||
|
background-color: rgb(107.505, 93.195, 105.885);
|
||||||
|
}
|
||||||
|
.reaction-button.active:disabled {
|
||||||
|
background-color: rgb(156.9373766816, 152.1626233184, 156.396838565);
|
||||||
|
}
|
||||||
|
.reaction-button.active.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-popover {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||||
|
padding: 5px 10px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-popover-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: scroll;
|
||||||
|
margin: auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-guide-list {
|
||||||
|
border-bottom: 1px dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topnav {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 10px solid rgb(40, 40, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
929
data/static/css/theme-peachy.css
Normal file
929
data/static/css/theme-peachy.css
Normal file
@ -0,0 +1,929 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "site-title";
|
||||||
|
src: url("/static/fonts/ChicagoFLF.woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cadman";
|
||||||
|
src: url("/static/fonts/Cadman_Roman.woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cadman";
|
||||||
|
src: url("/static/fonts/Cadman_Bold.woff2");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cadman";
|
||||||
|
src: url("/static/fonts/Cadman_Italic.woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Cadman";
|
||||||
|
src: url("/static/fonts/Cadman_BoldItalic.woff2");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.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";
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Cadman";
|
||||||
|
margin: 12px 50px;
|
||||||
|
background-color: #c85d45;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topnav {
|
||||||
|
padding: 6px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottomnav {
|
||||||
|
padding: 6px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
background-color: #88486d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkbg {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
background-color: #88486d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-actions {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
font-family: "site-title";
|
||||||
|
font-size: 3rem;
|
||||||
|
margin: 0 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas: "usercard post-content-container";
|
||||||
|
border: 2px outset rgb(155.8907865169, 93.2211235955, 76.5092134831);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usercard {
|
||||||
|
grid-area: usercard;
|
||||||
|
padding: 12px 6px;
|
||||||
|
border: none;
|
||||||
|
background-color: #88486d;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usercard-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: min-content 1fr min-content;
|
||||||
|
gap: 0;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas: "post-info" "post-content" "post-reactions";
|
||||||
|
grid-area: post-content-container;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-info {
|
||||||
|
grid-area: post-info;
|
||||||
|
display: flex;
|
||||||
|
min-height: 35px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 3px 12px;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background-color: #c85d45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content {
|
||||||
|
grid-area: post-content;
|
||||||
|
padding: 12px 12px 0 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-reactions {
|
||||||
|
grid-area: post-reactions;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
background-color: #c85d45;
|
||||||
|
border-top: 2px dotted #f7bfdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-inner {
|
||||||
|
height: 100%;
|
||||||
|
padding-right: 25%;
|
||||||
|
}
|
||||||
|
.post-inner.wider {
|
||||||
|
padding-right: 12.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-container {
|
||||||
|
border-top: 2px dotted #f7bfdf;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
display: block;
|
||||||
|
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
|
||||||
|
font-size: 1rem;
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
border-bottom-left-radius: 16px;
|
||||||
|
border-left: 6px solid rgb(231.56, 212.36, 207.24);
|
||||||
|
padding: 12px;
|
||||||
|
overflow: scroll;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code-container {
|
||||||
|
position: sticky;
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: last baseline;
|
||||||
|
font-family: "Cadman";
|
||||||
|
border-top-right-radius: 16px;
|
||||||
|
border-top-left-radius: 16px;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
border-left: 2px solid black;
|
||||||
|
border-right: 2px solid black;
|
||||||
|
border-top: 2px solid black;
|
||||||
|
}
|
||||||
|
.copy-code-container::before {
|
||||||
|
content: "code block";
|
||||||
|
font-style: italic;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-code {
|
||||||
|
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
|
||||||
|
color: white;
|
||||||
|
padding: 3px 6px;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 4px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 1rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
#delete-dialog, .lightbox-dialog {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 2px solid black;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-dialog-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
min-width: 400px;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
color: black;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-image {
|
||||||
|
max-width: 70vw;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: scale-down;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin: 6px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border-left: 6px solid rgb(231.56, 212.36, 207.24);
|
||||||
|
background-color: rgba(0, 0, 0, 0.1333333333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
grid-template-areas: "user-page-usercard user-page-stats";
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page-usercard {
|
||||||
|
grid-area: user-page-usercard;
|
||||||
|
padding: 12px 6px;
|
||||||
|
border: none;
|
||||||
|
background-color: #88486d;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page-stats {
|
||||||
|
grid-area: user-page-stats;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page-posts {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-page-post-preview {
|
||||||
|
max-height: 200px;
|
||||||
|
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-link {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-status {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, input[type=submit], .linkbutton {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||||
|
background-color: rgb(244.6, 148.6, 123);
|
||||||
|
}
|
||||||
|
button:active, input[type=submit]:active, .linkbutton:active {
|
||||||
|
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||||
|
}
|
||||||
|
button:disabled, input[type=submit]:disabled, .linkbutton:disabled {
|
||||||
|
background-color: rgb(198.02, 189.62, 187.38);
|
||||||
|
}
|
||||||
|
button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||||
|
background-color: #f73030;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||||
|
background-color: rgb(248.6, 89.4, 89.4);
|
||||||
|
}
|
||||||
|
button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active {
|
||||||
|
background-color: rgb(166.6956976744, 98.8043023256, 98.8043023256);
|
||||||
|
}
|
||||||
|
button.critical:disabled, input[type=submit].critical:disabled, .linkbutton.critical:disabled {
|
||||||
|
background-color: rgb(186.715, 172.785, 172.785);
|
||||||
|
}
|
||||||
|
button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critical.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||||
|
background-color: #fbfb8d;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||||
|
background-color: rgb(251.8, 251.8, 163.8);
|
||||||
|
}
|
||||||
|
button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active {
|
||||||
|
background-color: rgb(198.3813559322, 198.3813559322, 154.4186440678);
|
||||||
|
}
|
||||||
|
button.warn:disabled, input[type=submit].warn:disabled, .linkbutton.warn:disabled {
|
||||||
|
background-color: rgb(217.55, 217.55, 209.85);
|
||||||
|
}
|
||||||
|
button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=file]::file-selector-button {
|
||||||
|
background-color: #f27a5a;
|
||||||
|
color: black !important;
|
||||||
|
margin: 6px;
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button:hover {
|
||||||
|
background-color: rgb(244.6, 148.6, 123);
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button:active {
|
||||||
|
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button:disabled {
|
||||||
|
background-color: rgb(198.02, 189.62, 187.38);
|
||||||
|
}
|
||||||
|
input[type=file]::file-selector-button.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagebutton {
|
||||||
|
background-color: #f27a5a;
|
||||||
|
color: black !important;
|
||||||
|
padding: 3px 3px;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.pagebutton:hover {
|
||||||
|
background-color: rgb(244.6, 148.6, 123);
|
||||||
|
}
|
||||||
|
.pagebutton:active {
|
||||||
|
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||||
|
}
|
||||||
|
.pagebutton:disabled {
|
||||||
|
background-color: rgb(198.02, 189.62, 187.38);
|
||||||
|
}
|
||||||
|
.pagebutton.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currentpage {
|
||||||
|
border: none;
|
||||||
|
padding: 3px 3px;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modform {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container > * {
|
||||||
|
width: 60%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container > * {
|
||||||
|
width: 60%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text], input[type=password], textarea, select {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
color: black;
|
||||||
|
background-color: rgb(247.2, 175.2, 156);
|
||||||
|
}
|
||||||
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
|
background-color: rgb(249.8, 201.8, 189);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox {
|
||||||
|
border: 2px solid black;
|
||||||
|
background-color: #81a3e6;
|
||||||
|
padding: 12px 8px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.infobox.critical {
|
||||||
|
background-color: #f73030;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.infobox.warn {
|
||||||
|
background-color: #fbfb8d;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox > span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox-icon-container {
|
||||||
|
min-width: 60px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 96px 1.6fr 96px;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
min-height: 96px;
|
||||||
|
grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-sticky-container {
|
||||||
|
grid-area: thread-sticky-container;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-locked-container {
|
||||||
|
grid-area: thread-locked-container;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contain-svg {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||||
|
height: 50%;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
.contain-svg.full > svg, .contain-svg.full > img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-img-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
|
object-fit: contain;
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 400px;
|
||||||
|
min-width: 200px;
|
||||||
|
min-height: 200px;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-info-container {
|
||||||
|
grid-area: thread-info-container;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 110px;
|
||||||
|
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-info-post-preview {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-guide-section {
|
||||||
|
background-color: #f27a5a;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border: 1px solid black;
|
||||||
|
padding-right: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-guide-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5fr 300px;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas: "guide-topics guide-toc";
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-topics {
|
||||||
|
grid-area: guide-topics;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc {
|
||||||
|
grid-area: guide-toc;
|
||||||
|
position: sticky;
|
||||||
|
top: 100px;
|
||||||
|
align-self: start;
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-table tr td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-table tr th {
|
||||||
|
padding-left: 50px;
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-table {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-table, th, td {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorful-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 6px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorful-table tr th {
|
||||||
|
background-color: #b54444;
|
||||||
|
padding: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorful-table tr td {
|
||||||
|
background-color: #f27a5a;
|
||||||
|
padding: 3px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorful-table .small {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5fr 96px;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas: "topic-info-container topic-locked-container";
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-info-container {
|
||||||
|
grid-area: topic-info-container;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border: 1px solid black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-locked-container {
|
||||||
|
grid-area: topic-locked-container;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-topic {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
background-color: #f27a5a;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
border-top: 5px outset rgb(219.84, 191.04, 183.36);
|
||||||
|
border-bottom: 5px outset rgb(155.8907865169, 93.2211235955, 76.5092134831);
|
||||||
|
}
|
||||||
|
.draggable-topic.dragged {
|
||||||
|
background-color: #f27a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing {
|
||||||
|
background-color: rgb(219.84, 191.04, 183.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-explain {
|
||||||
|
margin: 12px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-edit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: baseline;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-editor {
|
||||||
|
height: 150px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-preview-errors-container {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
background-color: #f27a5a;
|
||||||
|
color: black !important;
|
||||||
|
border-bottom: none;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.tab-button:hover {
|
||||||
|
background-color: rgb(244.6, 148.6, 123);
|
||||||
|
}
|
||||||
|
.tab-button:active {
|
||||||
|
background-color: rgb(176.4525842697, 133.7379775281, 122.3474157303);
|
||||||
|
}
|
||||||
|
.tab-button:disabled {
|
||||||
|
background-color: rgb(198.02, 189.62, 187.38);
|
||||||
|
}
|
||||||
|
.tab-button.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
background-color: #b54444;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-content.active {
|
||||||
|
min-height: 250px;
|
||||||
|
display: block;
|
||||||
|
background-color: rgb(156.1, 92.9, 92.9);
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 6px;
|
||||||
|
border-top-right-radius: 16px;
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
border-bottom-left-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin: 6px 0 6px 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-concept-notification.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-concept-notification {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 80px;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #81a3e6;
|
||||||
|
padding: 12px 8px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
max-width: 15px;
|
||||||
|
max-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #c6655b;
|
||||||
|
padding: 0 6px;
|
||||||
|
gap: 6px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-toggle {
|
||||||
|
padding: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-title {
|
||||||
|
margin-right: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-accordion-content {
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
background-color: #c85d45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inbox-container {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-button-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-button {
|
||||||
|
padding: 3px 6px;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
.babycode-button > * {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-popover {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-button.active {
|
||||||
|
background-color: #b54444;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.reaction-button.active:hover {
|
||||||
|
background-color: rgb(197.978313253, 103.221686747, 103.221686747);
|
||||||
|
}
|
||||||
|
.reaction-button.active:active {
|
||||||
|
background-color: rgb(127.305, 96.795, 96.795);
|
||||||
|
}
|
||||||
|
.reaction-button.active:disabled {
|
||||||
|
background-color: rgb(167.7956024096, 159.5043975904, 159.5043975904);
|
||||||
|
}
|
||||||
|
.reaction-button.active.reduced {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-popover {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5019607843);
|
||||||
|
padding: 3px 6px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-popover-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: scroll;
|
||||||
|
margin: auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.babycode-guide-list {
|
||||||
|
border-bottom: 1px dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topnav {
|
||||||
|
border-top-left-radius: 16px;
|
||||||
|
border-top-right-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottomnav {
|
||||||
|
border-bottom-left-radius: 16px;
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkbg {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.darkbg a {
|
||||||
|
color: white;
|
||||||
|
}
|
BIN
data/static/emoji/scissors.png
Normal file
BIN
data/static/emoji/scissors.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 236 B |
@ -42,11 +42,13 @@
|
|||||||
const buttonBold = document.getElementById("post-editor-bold");
|
const buttonBold = document.getElementById("post-editor-bold");
|
||||||
const buttonItalics = document.getElementById("post-editor-italics");
|
const buttonItalics = document.getElementById("post-editor-italics");
|
||||||
const buttonStrike = document.getElementById("post-editor-strike");
|
const buttonStrike = document.getElementById("post-editor-strike");
|
||||||
|
const buttonUnderline = document.getElementById("post-editor-underline");
|
||||||
const buttonUrl = document.getElementById("post-editor-url");
|
const buttonUrl = document.getElementById("post-editor-url");
|
||||||
const buttonCode = document.getElementById("post-editor-code");
|
const buttonCode = document.getElementById("post-editor-code");
|
||||||
const buttonImg = document.getElementById("post-editor-img");
|
const buttonImg = document.getElementById("post-editor-img");
|
||||||
const buttonOl = document.getElementById("post-editor-ol");
|
const buttonOl = document.getElementById("post-editor-ol");
|
||||||
const buttonUl = document.getElementById("post-editor-ul");
|
const buttonUl = document.getElementById("post-editor-ul");
|
||||||
|
const buttonSpoiler = document.getElementById("post-editor-spoiler");
|
||||||
|
|
||||||
function insertTag(tagStart, newline = false, prefill = "") {
|
function insertTag(tagStart, newline = false, prefill = "") {
|
||||||
const hasAttr = tagStart[tagStart.length - 1] === "=";
|
const hasAttr = tagStart[tagStart.length - 1] === "=";
|
||||||
@ -105,6 +107,10 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
insertTag("s")
|
insertTag("s")
|
||||||
})
|
})
|
||||||
|
buttonUnderline.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
insertTag("u")
|
||||||
|
})
|
||||||
buttonUrl.addEventListener("click", (e) => {
|
buttonUrl.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
insertTag("url=", false, "link label");
|
insertTag("url=", false, "link label");
|
||||||
@ -125,6 +131,10 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
insertTag("ul", true);
|
insertTag("ul", true);
|
||||||
})
|
})
|
||||||
|
buttonSpoiler.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
insertTag("spoiler=", true, "hidden content");
|
||||||
|
})
|
||||||
|
|
||||||
const previewEndpoint = "/api/babycode-preview";
|
const previewEndpoint = "/api/babycode-preview";
|
||||||
let previousMarkup = "";
|
let previousMarkup = "";
|
||||||
@ -168,5 +178,8 @@
|
|||||||
const json_resp = await req.json();
|
const json_resp = await req.json();
|
||||||
previewContainer.innerHTML = json_resp.html;
|
previewContainer.innerHTML = json_resp.html;
|
||||||
previewErrorsContainer.textContent = "";
|
previewErrorsContainer.textContent = "";
|
||||||
|
|
||||||
|
const accordionRefreshEvt = new CustomEvent("refresh_accordions");
|
||||||
|
document.body.dispatchEvent(accordionRefreshEvt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
@ -62,26 +62,26 @@
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const valid = isQuoteSelectionValid();
|
const valid = isQuoteSelectionValid();
|
||||||
if (isSelecting || !valid) {
|
if (isSelecting || !valid) {
|
||||||
removePopover();
|
removeQuotePopover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
const selectionStr = selection.toString().trim();
|
const selectionStr = selection.toString().trim();
|
||||||
if (selection.isCollapsed || selectionStr === "") {
|
if (selection.isCollapsed || selectionStr === "") {
|
||||||
removePopover();
|
removeQuotePopover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showPopover();
|
showQuotePopover();
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePopover() {
|
function removeQuotePopover() {
|
||||||
quotePopover?.hidePopover();
|
quotePopover?.hidePopover();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPopover() {
|
function createQuotePopover() {
|
||||||
quotePopover = document.createElement("div");
|
quotePopover = document.createElement("div");
|
||||||
quotePopover.popover = "auto";
|
quotePopover.popover = "auto";
|
||||||
quotePopover.className = "quote-popover";
|
quotePopover.className = "quote-popover";
|
||||||
@ -95,9 +95,9 @@
|
|||||||
return quoteButton;
|
return quoteButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPopover() {
|
function showQuotePopover() {
|
||||||
if (!quotePopover) {
|
if (!quotePopover) {
|
||||||
const quoteButton = createPopover();
|
const quoteButton = createQuotePopover();
|
||||||
quoteButton.addEventListener("click", () => {
|
quoteButton.addEventListener("click", () => {
|
||||||
console.log("Quoting:", document.getSelection().toString());
|
console.log("Quoting:", document.getSelection().toString());
|
||||||
const postPermalink = quotedPostContainer.dataset.postPermalink;
|
const postPermalink = quotedPostContainer.dataset.postPermalink;
|
||||||
@ -106,12 +106,12 @@
|
|||||||
if (ta.value.trim() !== "") {
|
if (ta.value.trim() !== "") {
|
||||||
ta.value += "\n"
|
ta.value += "\n"
|
||||||
}
|
}
|
||||||
ta.value += `[url=${postPermalink}]${authorUsername} said:[/url]\n[quote]${document.getSelection().toString()}[/quote]\n`;
|
ta.value += `[url=${postPermalink}]${authorUsername} said:[/url]\n[quote]< :scissors: > ${document.getSelection().toString()} < :scissors: >[/quote]\n`;
|
||||||
ta.scrollIntoView()
|
ta.scrollIntoView()
|
||||||
ta.focus();
|
ta.focus();
|
||||||
|
|
||||||
document.getSelection().empty();
|
document.getSelection().empty();
|
||||||
removePopover();
|
removeQuotePopover();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,4 +196,174 @@
|
|||||||
.catch(error => console.log(error))
|
.catch(error => console.log(error))
|
||||||
}
|
}
|
||||||
tryFetchUpdate();
|
tryFetchUpdate();
|
||||||
|
|
||||||
|
if (supportsPopover()){
|
||||||
|
const reactionEmoji = document.getElementById("allowed-reaction-emoji").value.split(" ");
|
||||||
|
let reactionPopover = null;
|
||||||
|
let reactionTargetPostId = null;
|
||||||
|
|
||||||
|
function tryAddReaction(emoji, postId = reactionTargetPostId) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
"emoji": emoji,
|
||||||
|
});
|
||||||
|
fetch(`/api/add-reaction/${postId}`, {method: "POST", headers: {"Content-Type": "application/json"}, body: body})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
if (json.status === "added") {
|
||||||
|
const post = document.getElementById(`post-${postId}`);
|
||||||
|
const spans = Array.from(post.querySelectorAll(".reaction-count")).filter((span) => {
|
||||||
|
return span.dataset.emoji === emoji
|
||||||
|
});
|
||||||
|
if (spans.length > 0) {
|
||||||
|
const currentValue = spans[0].textContent;
|
||||||
|
spans[0].textContent = `${parseInt(currentValue) + 1}`;
|
||||||
|
const button = spans[0].closest(".reaction-button");
|
||||||
|
button.classList.add("active");
|
||||||
|
} else {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.classList = "reaction-container";
|
||||||
|
span.dataset.emoji = emoji;
|
||||||
|
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "reduced reaction-button active";
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
tryAddReaction(emoji, postId);
|
||||||
|
})
|
||||||
|
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = `/static/emoji/${emoji}.png`;
|
||||||
|
|
||||||
|
button.textContent = " x";
|
||||||
|
|
||||||
|
const reactionCountSpan = document.createElement("span")
|
||||||
|
reactionCountSpan.className = "reaction-count"
|
||||||
|
reactionCountSpan.textContent = "1"
|
||||||
|
|
||||||
|
button.insertAdjacentElement("afterbegin", img);
|
||||||
|
button.appendChild(reactionCountSpan);
|
||||||
|
|
||||||
|
span.appendChild(button);
|
||||||
|
|
||||||
|
const post = document.getElementById(`post-${postId}`);
|
||||||
|
post.querySelector(".post-reactions").insertBefore(span, post.querySelector(".add-reaction-button"));
|
||||||
|
}
|
||||||
|
} else if (json.error_code === 409) {
|
||||||
|
console.log("reaction exists, gonna try and remove");
|
||||||
|
tryRemoveReaction(emoji, postId);
|
||||||
|
} else {
|
||||||
|
console.warn(json)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryRemoveReaction(emoji, postId = reactionTargetPostId) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
"emoji": emoji,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`/api/remove-reaction/${postId}`, {method: "POST", headers: {"Content-Type": "application/json"}, body: body})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
if (json.status === "removed") {
|
||||||
|
const post = document.getElementById(`post-${postId}`);
|
||||||
|
const spans = Array.from(post.querySelectorAll(".reaction-container")).filter((span) => {
|
||||||
|
return span.dataset.emoji === emoji
|
||||||
|
});
|
||||||
|
if (spans.length > 0) {
|
||||||
|
const reactionCountSpan = spans[0].querySelector(".reaction-count");
|
||||||
|
const currentValue = parseInt(reactionCountSpan.textContent);
|
||||||
|
if (currentValue - 1 === 0) {
|
||||||
|
spans[0].remove();
|
||||||
|
} else {
|
||||||
|
reactionCountSpan.textContent = `${parseInt(currentValue) - 1}`;
|
||||||
|
const button = reactionCountSpan.closest(".reaction-button");
|
||||||
|
button.classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(json)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReactionPopover() {
|
||||||
|
reactionPopover = document.createElement("div");
|
||||||
|
reactionPopover.className = "reaction-popover";
|
||||||
|
reactionPopover.popover = "auto";
|
||||||
|
|
||||||
|
const inner = document.createElement("div");
|
||||||
|
inner.className = "reaction-popover-inner";
|
||||||
|
|
||||||
|
reactionPopover.appendChild(inner);
|
||||||
|
|
||||||
|
for (let emoji of reactionEmoji) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = `/static/emoji/${emoji}.png`;
|
||||||
|
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "reduced";
|
||||||
|
button.appendChild(img);
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
tryAddReaction(emoji);
|
||||||
|
})
|
||||||
|
|
||||||
|
button.dataset.emojiName = emoji;
|
||||||
|
|
||||||
|
inner.appendChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
reactionPopover.addEventListener("beforetoggle", (e) => {
|
||||||
|
if (e.newState === "closed") {
|
||||||
|
reactionTargetPostId = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.appendChild(reactionPopover);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showReactionPopover() {
|
||||||
|
if (!reactionPopover) {
|
||||||
|
createReactionPopover();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reactionPopover.matches(':popover-open')) {
|
||||||
|
reactionPopover.showPopover();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let button of document.querySelectorAll(".add-reaction-button")) {
|
||||||
|
button.addEventListener("click", (e) => {
|
||||||
|
showReactionPopover();
|
||||||
|
reactionTargetPostId = e.target.dataset.postId;
|
||||||
|
|
||||||
|
const rect = e.target.getBoundingClientRect();
|
||||||
|
const popoverRect = reactionPopover.getBoundingClientRect();
|
||||||
|
const scrollY = window.scrollY || window.pageYOffset;
|
||||||
|
|
||||||
|
reactionPopover.style.setProperty("top", `${rect.top + scrollY + rect.height}px`)
|
||||||
|
reactionPopover.style.setProperty("left", `${rect.left + rect.width/2 - popoverRect.width/2}px`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let button of document.querySelectorAll(".reaction-button")) {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const reactionContainer = button.closest(".reaction-container")
|
||||||
|
const emoji = reactionContainer.dataset.emoji;
|
||||||
|
const postId = reactionContainer.dataset.postId;
|
||||||
|
console.log(reactionContainer);
|
||||||
|
tryAddReaction(emoji, postId);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for (let button of document.querySelectorAll(".add-reaction-button")) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.title = "Enable JS to add reactions."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,26 +110,66 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// accordions
|
// accordions
|
||||||
const accordions = document.querySelectorAll(".accordion");
|
const handledAccordions = new Set();
|
||||||
accordions.forEach(accordion => {
|
function attachAccordionHandlers(accordion){
|
||||||
|
if(handledAccordions.has(accordion)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handledAccordions.add(accordion)
|
||||||
const header = accordion.querySelector(".accordion-header");
|
const header = accordion.querySelector(".accordion-header");
|
||||||
const toggleButton = header.querySelector(".accordion-toggle");
|
const toggleButton = header.querySelector(".accordion-toggle");
|
||||||
const content = accordion.querySelector(".accordion-content");
|
const content = accordion.querySelector(".accordion-content");
|
||||||
|
|
||||||
const toggle = (e) => {
|
const toggle = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
accordion.classList.toggle("hidden");
|
accordion.classList.toggle("hidden");
|
||||||
content.classList.toggle("hidden");
|
content.classList.toggle("hidden");
|
||||||
toggleButton.textContent = content.classList.contains("hidden") ? "+" : "-"
|
toggleButton.textContent = content.classList.contains("hidden") ? "+" : "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleButton.addEventListener("click", toggle);
|
toggleButton.addEventListener("click", toggle);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function refreshAccordions(){
|
||||||
|
const accordions = document.querySelectorAll(".accordion");
|
||||||
|
accordions.forEach(attachAccordionHandlers);
|
||||||
|
}
|
||||||
|
refreshAccordions()
|
||||||
|
|
||||||
|
document.body.addEventListener('refresh_accordions', refreshAccordions)
|
||||||
|
|
||||||
//lightboxes
|
//lightboxes
|
||||||
lightboxObj = constructLightbox();
|
lightboxObj = constructLightbox();
|
||||||
document.body.appendChild(lightboxObj.dialog);
|
document.body.appendChild(lightboxObj.dialog);
|
||||||
const postImages = document.querySelectorAll(".post-inner img.block-img");
|
|
||||||
|
function setImageMaxSize(img) {
|
||||||
|
const {
|
||||||
|
maxWidth: origMaxWidth,
|
||||||
|
maxHeight: origMaxHeight,
|
||||||
|
minWidth: origMinWidth,
|
||||||
|
minHeight: origMinHeight,
|
||||||
|
} = getComputedStyle(img);
|
||||||
|
console.log(img, img.naturalWidth, img.naturalHeight, origMinWidth, origMinHeight, origMaxWidth, origMaxHeight)
|
||||||
|
if (img.naturalWidth < parseInt(origMinWidth)) {
|
||||||
|
console.log(1)
|
||||||
|
img.style.minWidth = img.naturalWidth + "px";
|
||||||
|
}
|
||||||
|
if (img.naturalHeight < parseInt(origMinHeight)) {
|
||||||
|
console.log(2)
|
||||||
|
img.style.minHeight = img.naturalHeight + "px";
|
||||||
|
}
|
||||||
|
if (img.naturalWidth < parseInt(origMaxWidth)) {
|
||||||
|
console.log(3)
|
||||||
|
img.style.maxWidth = img.naturalWidth + "px";
|
||||||
|
}
|
||||||
|
if (img.naturalHeight < parseInt(origMaxHeight)) {
|
||||||
|
console.log(4)
|
||||||
|
img.style.maxHeight = img.naturalHeight + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const postImages = document.querySelectorAll(".post-inner img.post-image");
|
||||||
postImages.forEach(postImage => {
|
postImages.forEach(postImage => {
|
||||||
const belongingTo = postImage.closest(".post-inner");
|
const belongingTo = postImage.closest(".post-inner");
|
||||||
const images = lightboxImages.get(belongingTo) ?? [];
|
const images = lightboxImages.get(belongingTo) ?? [];
|
||||||
@ -144,4 +184,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
openLightbox(belongingTo, idx);
|
openLightbox(belongingTo, idx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const postAndSigImages = document.querySelectorAll("img.post-image");
|
||||||
|
postAndSigImages.forEach(image => {
|
||||||
|
if (image.complete) {
|
||||||
|
setImageMaxSize(image);
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
5
data/static/misc/spoiler.svg
Normal file
5
data/static/misc/spoiler.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width="24px" height="24px" 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="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
After Width: | Height: | Size: 814 B |
1097
sass/_default.scss
Normal file
1097
sass/_default.scss
Normal file
File diff suppressed because it is too large
Load Diff
85
sass/otomotone.scss
Normal file
85
sass/otomotone.scss
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
$fc: #e6e6e6;
|
||||||
|
$fci: black;
|
||||||
|
|
||||||
|
$lightish_accent: #503250;
|
||||||
|
$lightish_accent2: #502d50;
|
||||||
|
$dark_accent: #231c23;
|
||||||
|
|
||||||
|
$warn: #eaea6a;
|
||||||
|
$crit: #d53232;
|
||||||
|
|
||||||
|
@use 'default' with (
|
||||||
|
$ACCENT_COLOR: #9b649b,
|
||||||
|
|
||||||
|
$MAIN_BG: #220d16,
|
||||||
|
$DARK_1: $lightish_accent2,
|
||||||
|
$DARK_3: #302731,
|
||||||
|
|
||||||
|
$LIGHT_2: #ae6bae,
|
||||||
|
$LIGHT: $lightish_accent,
|
||||||
|
|
||||||
|
$DEFAULT_FONT_COLOR: $fc,
|
||||||
|
$DEFAULT_FONT_COLOR_INVERSE: $fci,
|
||||||
|
|
||||||
|
$BUTTON_COLOR: #3c283c,
|
||||||
|
$BUTTON_COLOR_2: #8a5584,
|
||||||
|
$BUTTON_FONT_COLOR: $fc,
|
||||||
|
$BUTTON_COLOR_WARN: $warn,
|
||||||
|
$BUTTON_WARN_FONT_COLOR: $fci,
|
||||||
|
$BUTTON_COLOR_CRITICAL: $crit,
|
||||||
|
$BUTTON_CRITICAL_FONT_COLOR: $fc,
|
||||||
|
$ACCORDION_COLOR: #7d467d,
|
||||||
|
|
||||||
|
$bottomnav_color: $dark_accent,
|
||||||
|
|
||||||
|
$topic_info_background: $dark_accent,
|
||||||
|
$topic_locked_background: $lightish_accent,
|
||||||
|
$thread_locked_background: $lightish_accent,
|
||||||
|
$thread_locked_border: 2px outset $dark_accent,
|
||||||
|
|
||||||
|
$site_title_color: white,
|
||||||
|
$topnav_color: #303030,
|
||||||
|
|
||||||
|
$quote_background_color: #fbafcf0a,
|
||||||
|
|
||||||
|
$link_color: #e87fe1,
|
||||||
|
$link_color_visited: #ed4fb1,
|
||||||
|
|
||||||
|
$post_info_background: #412841,
|
||||||
|
$post_content_background: $dark_accent,
|
||||||
|
|
||||||
|
$thread_info_background_color: $dark_accent,
|
||||||
|
|
||||||
|
$post_reactions_background: $lightish_accent,
|
||||||
|
|
||||||
|
$post_accordion_content_background: #2d212d,
|
||||||
|
|
||||||
|
$babycode_guide_toc_background: #3c233c,
|
||||||
|
$babycode_guide_section_background: $dark_accent,
|
||||||
|
|
||||||
|
$text_input_background: #371e37,
|
||||||
|
$text_input_background_focus: #514151,
|
||||||
|
$text_input_font_color: $fc,
|
||||||
|
|
||||||
|
$colorful_table_th_color: $lightish_accent,
|
||||||
|
$colorful_table_td_color: $dark_accent,
|
||||||
|
|
||||||
|
$lightbox_background: $lightish_accent,
|
||||||
|
|
||||||
|
$infobox_info_color: #775891,
|
||||||
|
$infobox_warn_color: $warn,
|
||||||
|
$infobox_warn_font_color: $fci,
|
||||||
|
$infobox_critical_color: $crit,
|
||||||
|
|
||||||
|
$tab_content_background: $lightish_accent,
|
||||||
|
$tab_button_active_color: #8a5584,
|
||||||
|
);
|
||||||
|
|
||||||
|
#topnav {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 10px solid rgb(40, 40, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
97
sass/peachy.scss
Normal file
97
sass/peachy.scss
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// $accent: #dd5536;
|
||||||
|
$accent: #f27a5a;
|
||||||
|
|
||||||
|
$br: 16px;
|
||||||
|
|
||||||
|
@use 'default' with (
|
||||||
|
$ACCENT_COLOR: $accent,
|
||||||
|
$thread_locked_background: $accent,
|
||||||
|
$topic_locked_background: $accent,
|
||||||
|
|
||||||
|
// $DARK_1: #e36286,
|
||||||
|
$DARK_1: #88486d,
|
||||||
|
$MAIN_BG: #c85d45,
|
||||||
|
|
||||||
|
$usercard_border: none,
|
||||||
|
$usercard_border_right: none,
|
||||||
|
$thread_locked_border: 1px solid black,
|
||||||
|
|
||||||
|
$SETTINGS_WIDTH: 60%,
|
||||||
|
$PAGE_SIDE_MARGIN: 50px,
|
||||||
|
|
||||||
|
$link_color: black,
|
||||||
|
$link_color_visited: black,
|
||||||
|
$reaction_button_active_font_color: white,
|
||||||
|
// $DEFAULT_FONT_COLOR: white,
|
||||||
|
// $DEFAULT_FONT_COLOR_INVERSE: black,
|
||||||
|
|
||||||
|
$text_input_font_color: black,
|
||||||
|
|
||||||
|
$BUTTON_COLOR: $accent,
|
||||||
|
$BUTTON_COLOR_2: #b54444,
|
||||||
|
$BUTTON_COLOR_CRITICAL: #f73030,
|
||||||
|
$ACCORDION_COLOR: #c6655b,
|
||||||
|
$BUTTON_WARN_FONT_COLOR: black,
|
||||||
|
$BUTTON_CRITICAL_FONT_COLOR: white,
|
||||||
|
|
||||||
|
$SMALL_PADDING: 3px,
|
||||||
|
$MEDIUM_PADDING: 6px,
|
||||||
|
$MEDIUM_BIG_PADDING: 8px,
|
||||||
|
$BIG_PADDING: 12px,
|
||||||
|
$BIGGER_PADDING: 16px,
|
||||||
|
|
||||||
|
$DEFAULT_BORDER_RADIUS: $br,
|
||||||
|
$code_border_radius: $br,
|
||||||
|
$button_padding: 8px 12px,
|
||||||
|
$reduced_button_padding: 6px,
|
||||||
|
|
||||||
|
$post_reactions_border_top: 2px dotted #f7bfdf,
|
||||||
|
|
||||||
|
$post_info_min_height: 35px,
|
||||||
|
$post_reactions_padding: 6px 12px,
|
||||||
|
$post_reactions_gap: 6px,
|
||||||
|
|
||||||
|
$text_input_padding: 8px,
|
||||||
|
|
||||||
|
$infobox_info_color: #81a3e6,
|
||||||
|
$infobox_critical_color: #f73030,
|
||||||
|
$infobox_warn_color: #fbfb8d,
|
||||||
|
|
||||||
|
$infobox_info_font_color: black,
|
||||||
|
$infobox_critical_font_color: white,
|
||||||
|
$infobox_warn_font_color: black,
|
||||||
|
|
||||||
|
$pagebutton_min_width: 36px,
|
||||||
|
$quote_background_color: #0002,
|
||||||
|
);
|
||||||
|
|
||||||
|
#topnav {
|
||||||
|
border-top-left-radius: $br;
|
||||||
|
border-top-right-radius: $br;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottomnav {
|
||||||
|
border-bottom-left-radius: $br;
|
||||||
|
border-bottom-right-radius: $br;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: $br;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkbg {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
& a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user