Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
40219f2b54
|
|||
|
4a45b62521
|
|||
|
fc55aaf87a
|
|||
|
db68ef2c3d
|
|||
|
a808137e5b
|
|||
|
a93a89f0df
|
|||
|
7aa3a9382e
|
|||
|
46704df7d9
|
|||
|
98bf430604
|
|||
|
21ace9299f
|
|||
|
122b706350
|
|||
|
c655caab9e
|
|||
|
b2d16e305d
|
|||
|
a8398cad51
|
|||
|
f27d8eaf7e
|
|||
|
36e17c6677
|
|||
|
d7a90745f6
|
|||
|
d90b4643cb
|
|||
|
d82f25471d
|
|||
|
791911b416
|
|||
|
ba2c9132f6
|
|||
|
d4e3d7cded
|
|||
|
0898c56a51
|
|||
|
96c37f9081
|
|||
|
94a4be8b97
|
|||
|
fa1140895a
|
|||
|
fc6c5d46e1
|
@@ -4,5 +4,7 @@
|
|||||||
data/db/*
|
data/db/*
|
||||||
data/static/avatars/*
|
data/static/avatars/*
|
||||||
!data/static/avatars/default.webp
|
!data/static/avatars/default.webp
|
||||||
|
data/static/badges/user
|
||||||
|
data/_cached
|
||||||
|
|
||||||
.local/
|
.local/
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ data/db/*
|
|||||||
data/static/avatars/*
|
data/static/avatars/*
|
||||||
!data/static/avatars/default.webp
|
!data/static/avatars/default.webp
|
||||||
data/static/badges/user
|
data/static/badges/user
|
||||||
|
data/_cached
|
||||||
|
|
||||||
config/secrets.prod.env
|
config/secrets.prod.env
|
||||||
config/pyrom_config.toml
|
config/pyrom_config.toml
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -1,18 +1,70 @@
|
|||||||
# Pyrom
|
# Pyrom
|
||||||
python/flask port of [porom](https://git.poto.cafe/yagich/porom)
|
pyrom is a playful home-grown forum software for the indie web borne out of frustration with social media and modern forums imitating it.
|
||||||
|
|
||||||
this is now the canonical implementation of porom. it's compatible with the database of porom.
|
the aim is not to recreate the feeling of forums from any time period. rather, it aims to serve as a lightweight alternative to other forum software packages. pyrom is lean and "fire-and-forget"; there is little necessary configuration, making it a great fit for smaller communities (though nothing prevents it from being used in larger ones.)
|
||||||
|
|
||||||
# License
|
a live example can be seen in action over at [Porom](https://forum.poto.cafe/).
|
||||||
Released under [CNPLv7+](https://thufie.lain.haus/NPL.html).
|
|
||||||
Please read the [full terms](./LICENSE.md) for proper wording.
|
## stack & structure
|
||||||
|
on the server side, pyrom is built in Python using the Flask framework. content is rendered mostly server-side with Jinja templates. the database used is SQLite.
|
||||||
|
|
||||||
|
on the client side, JS with only one library ([Bitty](https://bitty-js.com)) is used. for CSS, pyrom uses Sass.
|
||||||
|
|
||||||
|
below is an explanation of the folder structure:
|
||||||
|
|
||||||
|
- `/`
|
||||||
|
- `app/`
|
||||||
|
- `lib/` - utility libraries
|
||||||
|
- `routes/` - each `.py` file represents a "sub-app", usually the first part of the URL
|
||||||
|
- `templates/` - Jinja templates used by the routes. each subfolder corresponds to the "sub-app" that uses that template.
|
||||||
|
- `__init__.py` - creates the app
|
||||||
|
- `auth.py` - authentication helper
|
||||||
|
- `constants.py` - constant values used throughout the forum
|
||||||
|
- `db.py` - database abstraction layer and ORM library
|
||||||
|
- `migrations.py` - database migrations
|
||||||
|
- `models.py` - ORM model definitions
|
||||||
|
- `run.py` - runner script for development
|
||||||
|
- `schema.py` - database schema definition
|
||||||
|
- `config/` - configuration for the forum
|
||||||
|
- `data/`
|
||||||
|
- `_cached/` - cached versions of certain endpoints are stored here
|
||||||
|
- `db/` - the SQLite database is stored here
|
||||||
|
- `static/` - static files
|
||||||
|
- `avatars/` - user avatar uploads
|
||||||
|
- `badges/` - user badge uploads
|
||||||
|
- `css/` - CSS files generated from Sass sources
|
||||||
|
- `emoji/` - emoji images used on the forum
|
||||||
|
- `fonts/`
|
||||||
|
- `js/`
|
||||||
|
- `sass/`
|
||||||
|
- `_default.scss` - the default theme. Sass variables that other themes modify are defined here, along with the default styles. other files define the available themes.
|
||||||
|
- `build-themes.sh` - script for building Sass files into CSS
|
||||||
|
- `nginx.conf` - nginx config (production only)
|
||||||
|
- `uwsgi.ini` - uwsgi config (production only)
|
||||||
|
|
||||||
|
# license
|
||||||
|
released under [CNPLv7+](https://thufie.lain.haus/NPL.html).
|
||||||
|
please read the [full terms](./LICENSE.md) for proper wording.
|
||||||
|
|
||||||
|
# acknowledgments
|
||||||
|
|
||||||
|
pyrom uses many open-source and otherwise free-culture components. see the [THIRDPARTY](./THIRDPARTY.md) file for full credit.
|
||||||
|
|
||||||
# installing & first time setup
|
# installing & first time setup
|
||||||
## docker (production)
|
## docker (production)
|
||||||
create `config/secrets.prod.env` according to `config/secrets.prod.env.example`
|
1. clone the repo
|
||||||
|
2. create `config/secrets.prod.env` according to `config/secrets.prod.env.example`
|
||||||
|
3. create `config/pyrom_config.toml` according to `config/pyrom_config.toml.example` and modify as needed
|
||||||
|
4. make sure the `data/` folder is writable by the app:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker compose up
|
$ chmod -R 777 data/
|
||||||
|
```
|
||||||
|
|
||||||
|
5. bring up the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
- opens port 8080
|
- opens port 8080
|
||||||
@@ -20,10 +72,10 @@ $ docker compose up
|
|||||||
|
|
||||||
make sure to run it in an interactive session the first time, because it will spit out the password to the auto-created admin account.
|
make sure to run it in an interactive session the first time, because it will spit out the password to the auto-created admin account.
|
||||||
|
|
||||||
alternatively, if you already had porom running before, put the db file (`db.prod.sqlite`) in `data/db` and it will Just Work.
|
6. point your favorite proxy at `localhost:8080`
|
||||||
|
|
||||||
## manual (development)
|
## manual (development)
|
||||||
1. install python >= 3.11, sqlite3, libargon2, and imagemagick & clone repo
|
1. install python >= 3.13, sqlite3, libargon2, and imagemagick & clone repo
|
||||||
2. create a venv:
|
2. create a venv:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -59,6 +111,3 @@ $ source .venv/bin/activate
|
|||||||
$ python -m app.run
|
$ python -m app.run
|
||||||
```
|
```
|
||||||
|
|
||||||
# acknowledgments
|
|
||||||
|
|
||||||
pyrom uses many open-source and otherwise free-culture components. see the [THIRDPARTY](./THIRDPARTY.md) file for full credit.
|
|
||||||
|
|||||||
@@ -80,8 +80,23 @@ Repo: https://github.com/emcconville/wand
|
|||||||
|
|
||||||
## Bitty
|
## Bitty
|
||||||
|
|
||||||
Affected files: [`data/static/js/vnd/bitty-6.0.0-rc3.min.js`](./data/static/js/vnd/bitty-6.0.0-rc3.min.js)
|
Affected files: [`data/static/js/vnd/bitty-7.0.0.js`](./data/static/js/vnd/bitty-7.0.0.js)
|
||||||
URL: https://bitty.alanwsmith.com/
|
URL: https://bitty-js.com/
|
||||||
License: CC0 1.0
|
License: CC0 1.0
|
||||||
Author: alan w smith https://www.alanwsmith.com/
|
Author: alan w smith https://www.alanwsmith.com/
|
||||||
Repo: https://github.com/alanwsmith/bitty
|
Repo: https://github.com/alanwsmith/bitty
|
||||||
|
|
||||||
|
## Flask-Caching
|
||||||
|
|
||||||
|
URL: https://flask-caching.readthedocs.io/
|
||||||
|
Copyright:
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyright (c) 2010 by Thadeus Burgess.
|
||||||
|
Copyright (c) 2016 by Peter Justin.
|
||||||
|
|
||||||
|
Some rights reserved.
|
||||||
|
```
|
||||||
|
|
||||||
|
License: BSD-3-Clause ([see more](https://github.com/pallets-eco/flask-caching/blob/e59bc040cd47cd2b43e501d636d43d442c50b3ff/LICENSE))
|
||||||
|
Repo: https://github.com/pallets-eco/flask-caching
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
from flask import Flask, session, request, render_template
|
from flask import Flask, session, request, render_template
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads
|
from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads, Sessions
|
||||||
from .auth import digest
|
from .auth import digest
|
||||||
from .routes.users import is_logged_in, get_active_user, get_prefers_theme
|
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, InfoboxHTMLClass,
|
InfoboxKind, InfoboxHTMLClass,
|
||||||
REACTION_EMOJI, MOTD_BANNED_TAGS,
|
REACTION_EMOJI, MOTD_BANNED_TAGS,
|
||||||
SIG_BANNED_TAGS, STRICT_BANNED_TAGS,
|
SIG_BANNED_TAGS, STRICT_BANNED_TAGS,
|
||||||
)
|
)
|
||||||
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION
|
from .lib.babycode import babycode_to_html, babycode_to_rssxml, EMOJI, BABYCODE_VERSION
|
||||||
from datetime import datetime
|
from .lib.exceptions import SiteNameMissingException
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from flask_caching import Cache
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import secrets
|
import secrets
|
||||||
@@ -55,6 +56,18 @@ def reparse_babycode():
|
|||||||
print('Re-parsing babycode, this may take a while...')
|
print('Re-parsing babycode, this may take a while...')
|
||||||
from .db import db
|
from .db import db
|
||||||
from .constants import MOTD_BANNED_TAGS
|
from .constants import MOTD_BANNED_TAGS
|
||||||
|
|
||||||
|
post_histories_without_rss = PostHistory.findall([
|
||||||
|
('markup_language', '=', 'babycode'),
|
||||||
|
('content_rss', 'IS', None),
|
||||||
|
])
|
||||||
|
|
||||||
|
with db.transaction():
|
||||||
|
for ph in post_histories_without_rss:
|
||||||
|
ph.update({
|
||||||
|
'content_rss': babycode_to_rssxml(ph['original_markup']),
|
||||||
|
})
|
||||||
|
|
||||||
post_histories = PostHistory.findall([
|
post_histories = PostHistory.findall([
|
||||||
('markup_language', '=', 'babycode'),
|
('markup_language', '=', 'babycode'),
|
||||||
('format_version', 'IS NOT', BABYCODE_VERSION)
|
('format_version', 'IS NOT', BABYCODE_VERSION)
|
||||||
@@ -65,6 +78,7 @@ def reparse_babycode():
|
|||||||
for ph in post_histories:
|
for ph in post_histories:
|
||||||
ph.update({
|
ph.update({
|
||||||
'content': babycode_to_html(ph['original_markup']).result,
|
'content': babycode_to_html(ph['original_markup']).result,
|
||||||
|
'content_rss': babycode_to_rssxml(ph['original_markup']),
|
||||||
'format_version': BABYCODE_VERSION,
|
'format_version': BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
print('Re-parsing posts done.')
|
print('Re-parsing posts done.')
|
||||||
@@ -124,6 +138,18 @@ def bind_default_badges(path):
|
|||||||
'uploaded_at': int(os.path.getmtime(real_path)),
|
'uploaded_at': int(os.path.getmtime(real_path)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def clear_stale_sessions():
|
||||||
|
from .db import db
|
||||||
|
with db.transaction():
|
||||||
|
now = int(time.time())
|
||||||
|
stale_sessions = Sessions.findall([
|
||||||
|
('expires_at', '<', now)
|
||||||
|
])
|
||||||
|
for sess in stale_sessions:
|
||||||
|
sess.delete()
|
||||||
|
|
||||||
|
|
||||||
|
cache = Cache()
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -133,6 +159,10 @@ def create_app():
|
|||||||
app.config['USERS_CAN_INVITE'] = False
|
app.config['USERS_CAN_INVITE'] = False
|
||||||
app.config['ADMIN_CONTACT_INFO'] = ''
|
app.config['ADMIN_CONTACT_INFO'] = ''
|
||||||
app.config['GUIDE_DESCRIPTION'] = ''
|
app.config['GUIDE_DESCRIPTION'] = ''
|
||||||
|
|
||||||
|
app.config['CACHE_TYPE'] = 'FileSystemCache'
|
||||||
|
app.config['CACHE_DEFAULT_TIMEOUT'] = 300
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
|
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -142,9 +172,12 @@ def create_app():
|
|||||||
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
|
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
|
||||||
app.debug = True
|
app.debug = True
|
||||||
app.config["DB_PATH"] = "data/db/db.dev.sqlite"
|
app.config["DB_PATH"] = "data/db/db.dev.sqlite"
|
||||||
|
app.config["SERVER_NAME"] = "localhost:8080"
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
else:
|
else:
|
||||||
app.config["DB_PATH"] = "data/db/db.prod.sqlite"
|
app.config["DB_PATH"] = "data/db/db.prod.sqlite"
|
||||||
|
if not app.config["SERVER_NAME"]:
|
||||||
|
raise SiteNameMissingException()
|
||||||
|
|
||||||
app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY")
|
app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY")
|
||||||
|
|
||||||
@@ -156,6 +189,13 @@ def create_app():
|
|||||||
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
|
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
|
||||||
os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True)
|
os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True)
|
||||||
|
|
||||||
|
if app.config['CACHE_TYPE'] == 'FileSystemCache':
|
||||||
|
cache_dir = app.config.get('CACHE_DIR', 'data/_cached')
|
||||||
|
os.makedirs(cache_dir, exist_ok = True)
|
||||||
|
app.config['CACHE_DIR'] = cache_dir
|
||||||
|
|
||||||
|
cache.init_app(app)
|
||||||
|
|
||||||
css_dir = 'data/static/css/'
|
css_dir = 'data/static/css/'
|
||||||
allowed_themes = []
|
allowed_themes = []
|
||||||
for f in os.listdir(css_dir):
|
for f in os.listdir(css_dir):
|
||||||
@@ -167,20 +207,6 @@ def create_app():
|
|||||||
allowed_themes.sort(key=(lambda x: (x != 'style', x)))
|
allowed_themes.sort(key=(lambda x: (x != 'style', x)))
|
||||||
app.config['allowed_themes'] = allowed_themes
|
app.config['allowed_themes'] = allowed_themes
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
from .schema import create as create_tables
|
|
||||||
from .migrations import run_migrations
|
|
||||||
create_tables()
|
|
||||||
run_migrations()
|
|
||||||
|
|
||||||
create_default_avatar()
|
|
||||||
create_admin()
|
|
||||||
create_deleted_user()
|
|
||||||
|
|
||||||
reparse_babycode()
|
|
||||||
|
|
||||||
bind_default_badges(app.config['BADGES_PATH'])
|
|
||||||
|
|
||||||
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
|
||||||
@@ -200,6 +226,22 @@ def create_app():
|
|||||||
app.register_blueprint(hyperapi_bp)
|
app.register_blueprint(hyperapi_bp)
|
||||||
app.register_blueprint(guides_bp)
|
app.register_blueprint(guides_bp)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
from .schema import create as create_tables
|
||||||
|
from .migrations import run_migrations
|
||||||
|
create_tables()
|
||||||
|
run_migrations()
|
||||||
|
|
||||||
|
create_default_avatar()
|
||||||
|
create_admin()
|
||||||
|
create_deleted_user()
|
||||||
|
|
||||||
|
clear_stale_sessions()
|
||||||
|
|
||||||
|
reparse_babycode()
|
||||||
|
|
||||||
|
bind_default_badges(app.config['BADGES_PATH'])
|
||||||
|
|
||||||
app.config['SESSION_COOKIE_SECURE'] = True
|
app.config['SESSION_COOKIE_SECURE'] = True
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
@@ -229,10 +271,12 @@ def create_app():
|
|||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_funcs():
|
def inject_funcs():
|
||||||
|
from .routes.threads import get_post_url
|
||||||
return {
|
return {
|
||||||
'get_post_url': get_post_url,
|
'get_post_url': get_post_url,
|
||||||
'get_prefers_theme': get_prefers_theme,
|
'get_prefers_theme': get_prefers_theme,
|
||||||
'get_motds': MOTD.get_all,
|
'get_motds': MOTD.get_all,
|
||||||
|
'get_time_now': lambda: int(time.time()),
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.template_filter("ts_datetime")
|
@app.template_filter("ts_datetime")
|
||||||
@@ -251,12 +295,12 @@ def create_app():
|
|||||||
return permission_level_string(term)
|
return permission_level_string(term)
|
||||||
|
|
||||||
@app.template_filter('babycode')
|
@app.template_filter('babycode')
|
||||||
def babycode_filter(markup):
|
def babycode_filter(markup, nofrag=False):
|
||||||
return babycode_to_html(markup).result
|
return babycode_to_html(markup, fragment=not nofrag).result
|
||||||
|
|
||||||
@app.template_filter('babycode_strict')
|
@app.template_filter('babycode_strict')
|
||||||
def babycode_strict_filter(markup):
|
def babycode_strict_filter(markup, nofrag=False):
|
||||||
return babycode_to_html(markup, STRICT_BANNED_TAGS).result
|
return babycode_to_html(markup, banned_tags=STRICT_BANNED_TAGS, fragment=not nofrag).result
|
||||||
|
|
||||||
@app.template_filter('extract_h2')
|
@app.template_filter('extract_h2')
|
||||||
def extract_h2(content):
|
def extract_h2(content):
|
||||||
@@ -308,4 +352,8 @@ def create_app():
|
|||||||
def fromjson(subject: str):
|
def fromjson(subject: str):
|
||||||
return json.loads(subject)
|
return json.loads(subject)
|
||||||
|
|
||||||
|
@app.template_filter('iso8601')
|
||||||
|
def unix_to_iso8601(subject: str):
|
||||||
|
return datetime.fromtimestamp(int(subject), timezone.utc).isoformat()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -6,17 +6,203 @@ from pygments.lexers import get_lexer_by_name
|
|||||||
from pygments.util import ClassNotFound as PygmentsClassNotFound
|
from pygments.util import ClassNotFound as PygmentsClassNotFound
|
||||||
import re
|
import re
|
||||||
|
|
||||||
class BabycodeParseResult:
|
BABYCODE_VERSION = 8
|
||||||
|
|
||||||
|
|
||||||
|
class BabycodeError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BabycodeRenderError(BabycodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownASTElementError(BabycodeRenderError):
|
||||||
|
def __init__(self, element_type, element=None):
|
||||||
|
self.element_type = element_type
|
||||||
|
self.element = element
|
||||||
|
|
||||||
|
message = f'Unknown AST element: {element_type}'
|
||||||
|
if element:
|
||||||
|
message += f' (element: {element})'
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class BabycodeRenderResult:
|
||||||
def __init__(self, result, mentions=[]):
|
def __init__(self, result, mentions=[]):
|
||||||
self.result = result
|
self.result = result
|
||||||
self.mentions = mentions
|
self.mentions = mentions
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
BABYCODE_VERSION = 5
|
class BabycodeRenderer:
|
||||||
|
def __init__(self, tag_map, void_tag_map, emote_map, fragment=False):
|
||||||
|
self.tag_map = tag_map
|
||||||
|
self.void_tag_map = void_tag_map
|
||||||
|
self.emote_map = emote_map
|
||||||
|
self.fragment = fragment
|
||||||
|
|
||||||
|
def make_mention(self, element):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def transform_para_whitespace(self, text):
|
||||||
|
# markdown rules:
|
||||||
|
# two spaces at end of line -> <br>
|
||||||
|
text = re.sub(r' +\n', '<br>', text)
|
||||||
|
# single newlines -> space (collapsed)
|
||||||
|
text = re.sub(r'\n', ' ', text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
def wrap_in_paragraphs(self, nodes, context_is_block=True, is_root=False):
|
||||||
|
result = []
|
||||||
|
current_paragraph = []
|
||||||
|
is_first_para = is_root and self.fragment
|
||||||
|
|
||||||
|
def flush_paragraph():
|
||||||
|
# TIL nonlocal exists
|
||||||
|
nonlocal result, current_paragraph, is_first_para
|
||||||
|
if not current_paragraph:
|
||||||
|
return
|
||||||
|
|
||||||
|
para_content = ''.join(current_paragraph)
|
||||||
|
if para_content.strip(): # skip empty paragraphs
|
||||||
|
if is_first_para:
|
||||||
|
result.append(para_content)
|
||||||
|
is_first_para = False
|
||||||
|
else:
|
||||||
|
result.append(f"<p>{para_content}</p>")
|
||||||
|
current_paragraph.clear()
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
if isinstance(node, str):
|
||||||
|
paras = re.split(r'\n\n+', node)
|
||||||
|
for i, para in enumerate(paras):
|
||||||
|
if i > 0 and context_is_block:
|
||||||
|
flush_paragraph()
|
||||||
|
|
||||||
|
if para:
|
||||||
|
processed = self.transform_para_whitespace(para)
|
||||||
|
current_paragraph.append(processed)
|
||||||
|
else:
|
||||||
|
inline = is_inline(node)
|
||||||
|
|
||||||
|
if inline and context_is_block:
|
||||||
|
# inline child within a paragraph context
|
||||||
|
current_paragraph.append(self.fold(node))
|
||||||
|
elif not inline and context_is_block:
|
||||||
|
# block child within a block context
|
||||||
|
flush_paragraph()
|
||||||
|
if is_root:
|
||||||
|
# this is relevant for fragment.
|
||||||
|
# fragment only applies to the first inline node(s).
|
||||||
|
# if the first element is a block, reset "fragment mode".
|
||||||
|
is_first_para = False
|
||||||
|
result.append(self.fold(node))
|
||||||
|
else:
|
||||||
|
# either inline in inline context, or block in inline context
|
||||||
|
current_paragraph.append(self.fold(node))
|
||||||
|
|
||||||
|
if context_is_block:
|
||||||
|
# flush final para if we're in a block context
|
||||||
|
flush_paragraph()
|
||||||
|
elif current_paragraph:
|
||||||
|
# inline context - just append whatever we collected
|
||||||
|
result.append(''.join(current_paragraph))
|
||||||
|
|
||||||
|
return ''.join(result)
|
||||||
|
|
||||||
|
def fold(self, element):
|
||||||
|
if isinstance(element, str):
|
||||||
|
return element
|
||||||
|
|
||||||
|
match element['type']:
|
||||||
|
case 'bbcode':
|
||||||
|
tag_name = element['name']
|
||||||
|
|
||||||
|
if is_inline(element):
|
||||||
|
# inline tag
|
||||||
|
# since its inline, all children should be processed inline
|
||||||
|
content = "".join(self.fold(child) for child in element['children'])
|
||||||
|
return self.tag_map[tag_name](content, element['attr'])
|
||||||
|
else:
|
||||||
|
# block tag
|
||||||
|
if tag_name in {'ul', 'ol', 'code', 'img'}:
|
||||||
|
# these handle their own internal structure
|
||||||
|
content = ''.join(
|
||||||
|
child if isinstance(child, str) else self.fold(child)
|
||||||
|
for child in element['children']
|
||||||
|
)
|
||||||
|
return self.tag_map[tag_name](content, element['attr'])
|
||||||
|
else:
|
||||||
|
# block elements that can contain paragraphs
|
||||||
|
content = self.wrap_in_paragraphs(element['children'], context_is_block=True, is_root=False)
|
||||||
|
return self.tag_map[tag_name](content, element['attr'])
|
||||||
|
case 'bbcode_void':
|
||||||
|
return self.void_tag_map[element['name']](element['attr'])
|
||||||
|
case 'link':
|
||||||
|
return f"<a href=\"{element['url']}\">{element['url']}</a>"
|
||||||
|
case 'emote':
|
||||||
|
return self.emote_map[element['name']]
|
||||||
|
case 'rule':
|
||||||
|
return '<hr>'
|
||||||
|
case 'mention':
|
||||||
|
return self.make_mention(element)
|
||||||
|
case _:
|
||||||
|
raise UnknownASTElementError(
|
||||||
|
element_type=element['type'],
|
||||||
|
element=element
|
||||||
|
)
|
||||||
|
|
||||||
|
def render(self, ast):
|
||||||
|
out = self.wrap_in_paragraphs(ast, context_is_block=True, is_root=True)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLRenderer(BabycodeRenderer):
|
||||||
|
def __init__(self, fragment=False):
|
||||||
|
super().__init__(TAGS, VOID_TAGS, EMOJI, fragment)
|
||||||
|
|
||||||
|
self.mentions = []
|
||||||
|
|
||||||
|
def make_mention(self, e):
|
||||||
|
from ..models import Users
|
||||||
|
from flask import url_for, current_app
|
||||||
|
with current_app.test_request_context('/'):
|
||||||
|
target_user = Users.find({'username': e['name'].lower()})
|
||||||
|
if not target_user:
|
||||||
|
return f"@{e['name']}"
|
||||||
|
|
||||||
|
mention_data = {
|
||||||
|
'mention_text': f"@{e['name']}",
|
||||||
|
'mentioned_user_id': int(target_user.id),
|
||||||
|
"start": e['start'],
|
||||||
|
"end": e['end'],
|
||||||
|
}
|
||||||
|
if mention_data not in self.mentions:
|
||||||
|
self.mentions.append(mention_data)
|
||||||
|
|
||||||
|
return f"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.page', username=target_user.username)}' title='@{target_user.username}' data-init='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
|
||||||
|
|
||||||
|
def render(self, ast):
|
||||||
|
out = super().render(ast)
|
||||||
|
return BabycodeRenderResult(out, self.mentions)
|
||||||
|
|
||||||
|
|
||||||
|
class RSSXMLRenderer(BabycodeRenderer):
|
||||||
|
def __init__(self, fragment=False):
|
||||||
|
super().__init__(RSS_TAGS, VOID_TAGS, RSS_EMOJI, fragment)
|
||||||
|
|
||||||
|
def make_mention(self, e):
|
||||||
|
from ..models import Users
|
||||||
|
from flask import url_for
|
||||||
|
target_user = Users.find({'username': e['name'].lower()})
|
||||||
|
if not target_user:
|
||||||
|
return f"@{e['name']}"
|
||||||
|
|
||||||
|
return f'<a href="{url_for('users.page', username=target_user.username, _external=True)}" title="@{target_user.username}">{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>'
|
||||||
|
|
||||||
|
|
||||||
NAMED_COLORS = [
|
NAMED_COLORS = [
|
||||||
'black', 'silver', 'gray', 'white', 'maroon', 'red',
|
'black', 'silver', 'gray', 'white', 'maroon', 'red',
|
||||||
@@ -49,114 +235,11 @@ NAMED_COLORS = [
|
|||||||
'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen',
|
'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
|
|
||||||
if is_inline:
|
|
||||||
return f"<code class=\"inline-code\">{children}</code>"
|
|
||||||
else:
|
|
||||||
input_code = children.strip()
|
|
||||||
button = f"<button type=button class=\"copy-code\" value=\"{input_code}\" data-send=\"copyCode\" data-receive=\"copyCode\">Copy</button>"
|
|
||||||
unhighlighted = f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">code block</span>{button}</span><code>{input_code}</code></pre>"
|
|
||||||
if not attr:
|
|
||||||
return unhighlighted
|
|
||||||
try:
|
|
||||||
lexer = get_lexer_by_name(attr.strip())
|
|
||||||
formatter = HtmlFormatter(nowrap=True)
|
|
||||||
return f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">{lexer.name}</span>{button}</span><code>{highlight(input_code.unescape(), lexer, formatter)}</code></pre>"
|
|
||||||
except PygmentsClassNotFound:
|
|
||||||
return unhighlighted
|
|
||||||
|
|
||||||
def tag_list(children):
|
|
||||||
list_body = re.sub(r" +\n", "<br>", children.strip())
|
|
||||||
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])
|
|
||||||
|
|
||||||
def tag_color(children, attr, surrounding):
|
|
||||||
if not attr:
|
|
||||||
return f"[color]{children}[/color]"
|
|
||||||
|
|
||||||
hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
|
|
||||||
potential_color = attr.lower().strip()
|
|
||||||
|
|
||||||
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' data-receive='toggleAccordion'><div class='accordion-header'><button type='button' class='accordion-toggle' data-send='toggleAccordion'>+</button><span>{spoiler_name}</span></div>{content}</div>"""
|
|
||||||
return container
|
|
||||||
|
|
||||||
def tag_image(children, attr, surrounding):
|
|
||||||
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 = {
|
|
||||||
"b": lambda children, attr, _: f"<strong>{children}</strong>",
|
|
||||||
"i": lambda children, attr, _: f"<em>{children}</em>",
|
|
||||||
"s": lambda children, attr, _: f"<del>{children}</del>",
|
|
||||||
"u": lambda children, attr, _: f"<u>{children}</u>",
|
|
||||||
|
|
||||||
"img": tag_image,
|
|
||||||
"url": lambda children, attr, _: f"<a href={attr}>{children}</a>",
|
|
||||||
"quote": lambda children, attr, _: f"<blockquote>{children}</blockquote>",
|
|
||||||
"code": tag_code,
|
|
||||||
"ul": lambda children, attr, _: f"<ul>{tag_list(children)}</ul>",
|
|
||||||
"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,
|
|
||||||
}
|
|
||||||
|
|
||||||
VOID_TAGS = {
|
|
||||||
'lb': lambda attr: '[',
|
|
||||||
'rb': lambda attr: ']',
|
|
||||||
'@': lambda attr: '@',
|
|
||||||
}
|
|
||||||
|
|
||||||
# [img] is considered block for the purposes of collapsing whitespace,
|
|
||||||
# despite being potentially inline (since the resulting <img> tag is inline, but creates a block container around itself and sibling images).
|
|
||||||
# [code] has a special case in is_inline().
|
|
||||||
INLINE_TAGS = {
|
|
||||||
'b', 'i', 's', 'u', 'color', 'big', 'small', 'url'
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_emoji(name, code):
|
def make_emoji(name, code):
|
||||||
return f'<img class=emoji src="/static/emoji/{name}.png" alt="{name}" title=":{code}:">'
|
return f'<img class=emoji src="/static/emoji/{name}.png" alt="{name}" title=":{code}:">'
|
||||||
|
|
||||||
|
|
||||||
EMOJI = {
|
EMOJI = {
|
||||||
'angry': make_emoji('angry', 'angry'),
|
'angry': make_emoji('angry', 'angry'),
|
||||||
|
|
||||||
@@ -203,12 +286,208 @@ EMOJI = {
|
|||||||
'wink': make_emoji('wink', 'wink'),
|
'wink': make_emoji('wink', 'wink'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RSS_EMOJI = {
|
||||||
|
**EMOJI,
|
||||||
|
|
||||||
|
'angry': '😡',
|
||||||
|
|
||||||
|
'(': '🙁',
|
||||||
|
|
||||||
|
'D': '😃',
|
||||||
|
|
||||||
|
'imp': '😈',
|
||||||
|
|
||||||
|
'angryimp': '👿',
|
||||||
|
'impangry': '👿',
|
||||||
|
|
||||||
|
'lobster': '🦞',
|
||||||
|
|
||||||
|
'|': '😐',
|
||||||
|
|
||||||
|
'pensive': '😔',
|
||||||
|
|
||||||
|
'scissors': '✂️',
|
||||||
|
|
||||||
|
')': '🙂',
|
||||||
|
|
||||||
|
'smiletear': '🥲',
|
||||||
|
'crytear': '🥲',
|
||||||
|
|
||||||
|
',': '😭',
|
||||||
|
'T': '😭',
|
||||||
|
'cry': '😭',
|
||||||
|
'sob': '😭',
|
||||||
|
|
||||||
|
'o': '😮',
|
||||||
|
'O': '😮',
|
||||||
|
|
||||||
|
'hmm': '🤔',
|
||||||
|
'think': '🤔',
|
||||||
|
'thinking': '🤔',
|
||||||
|
|
||||||
|
'P': '😛',
|
||||||
|
'p': '😛',
|
||||||
|
|
||||||
|
'weary': '😩',
|
||||||
|
|
||||||
|
';': '😉',
|
||||||
|
'wink': '😉',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TEXT_ONLY = ["code"]
|
TEXT_ONLY = ["code"]
|
||||||
|
|
||||||
def break_lines(text):
|
|
||||||
text = re.sub(r" +\n", "<br>", text)
|
def tag_code(children, attr):
|
||||||
text = re.sub(r"\n\n+", "<br><br>", text)
|
is_inline = children.find('\n') == -1
|
||||||
return text
|
if is_inline:
|
||||||
|
return f"<code class=\"inline-code\">{children}</code>"
|
||||||
|
else:
|
||||||
|
input_code = children.strip()
|
||||||
|
button = f"<button type=button class=\"copy-code\" value=\"{input_code}\" data-send=\"copyCode\" data-receive=\"copyCode\">Copy</button>"
|
||||||
|
unhighlighted = f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">code block</span>{button}</span><code>{input_code}</code></pre>"
|
||||||
|
if not attr:
|
||||||
|
return unhighlighted
|
||||||
|
try:
|
||||||
|
lexer = get_lexer_by_name(attr.strip())
|
||||||
|
formatter = HtmlFormatter(nowrap=True)
|
||||||
|
return f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">{lexer.name}</span>{button}</span><code>{highlight(Markup(input_code).unescape(), lexer, formatter)}</code></pre>"
|
||||||
|
except PygmentsClassNotFound:
|
||||||
|
return unhighlighted
|
||||||
|
|
||||||
|
|
||||||
|
def tag_list(children):
|
||||||
|
list_body = re.sub(r" +\n", "<br>", children.strip())
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
def tag_color(children, attr):
|
||||||
|
if not attr:
|
||||||
|
return f"[color]{children}[/color]"
|
||||||
|
|
||||||
|
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):
|
||||||
|
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' data-receive='toggleAccordion'><div class='accordion-header'><button type='button' class='accordion-toggle' data-send='toggleAccordion'>+</button><span>{spoiler_name}</span></div>{content}</div>"""
|
||||||
|
return container
|
||||||
|
|
||||||
|
|
||||||
|
def tag_image(children, attr):
|
||||||
|
img = f"<img class=\"post-image\" src=\"{attr}\" alt=\"{children}\">"
|
||||||
|
return f"<div class=post-img-container>{img}</div>"
|
||||||
|
|
||||||
|
|
||||||
|
TAGS = {
|
||||||
|
"b": lambda children, attr: f"<strong>{children}</strong>",
|
||||||
|
"i": lambda children, attr: f"<em>{children}</em>",
|
||||||
|
"s": lambda children, attr: f"<del>{children}</del>",
|
||||||
|
"u": lambda children, attr: f"<u>{children}</u>",
|
||||||
|
|
||||||
|
"img": tag_image,
|
||||||
|
"url": lambda children, attr: f"<a href={attr}>{children}</a>",
|
||||||
|
"quote": lambda children, attr: f"<blockquote>{children}</blockquote>",
|
||||||
|
"code": tag_code,
|
||||||
|
"ul": lambda children, attr: f"<ul>{tag_list(children)}</ul>",
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def tag_code_rss(children, attr):
|
||||||
|
is_inline = children.find('\n') == -1
|
||||||
|
if is_inline:
|
||||||
|
return f'<code>{children}</code>'
|
||||||
|
else:
|
||||||
|
return f'<pre><code>{children}</code></pre>'
|
||||||
|
|
||||||
|
|
||||||
|
def tag_url_rss(children, attr):
|
||||||
|
if attr.startswith('/'):
|
||||||
|
from flask import current_app
|
||||||
|
uri = f"{current_app.config['PREFERRED_URL_SCHEME']}://{current_app.config['SERVER_NAME']}{attr}"
|
||||||
|
return f"<a href={uri}>{children}</a>"
|
||||||
|
|
||||||
|
return f"<a href={attr}>{children}</a>"
|
||||||
|
|
||||||
|
|
||||||
|
def tag_image_rss(children, attr):
|
||||||
|
if attr.startswith('/'):
|
||||||
|
from flask import current_app
|
||||||
|
uri = f"{current_app.config['PREFERRED_URL_SCHEME']}://{current_app.config['SERVER_NAME']}{attr}"
|
||||||
|
return f'<img src="{uri}" alt={children} />'
|
||||||
|
|
||||||
|
return f'<img src="{attr}" alt={children} />'
|
||||||
|
|
||||||
|
|
||||||
|
RSS_TAGS = {
|
||||||
|
**TAGS,
|
||||||
|
'img': tag_image_rss,
|
||||||
|
'url': tag_url_rss,
|
||||||
|
'spoiler': lambda children, attr: f'<details><summary>{attr or "Spoiler"} (click to reveal)</summary>{children}</details>',
|
||||||
|
'code': tag_code_rss,
|
||||||
|
|
||||||
|
'big': lambda children, attr: f'<span style="font-size: 1.2em">{children}</span>',
|
||||||
|
'small': lambda children, attr: f'<small>{children}</small>'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
VOID_TAGS = {
|
||||||
|
'lb': lambda attr: '[',
|
||||||
|
'rb': lambda attr: ']',
|
||||||
|
'at': lambda attr: '@',
|
||||||
|
'd': lambda attr: '-',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# [img] is considered block for the purposes of collapsing whitespace,
|
||||||
|
# despite being potentially inline (since the resulting <img> tag is inline, but creates a block container around itself and sibling images).
|
||||||
|
# [code] has a special case in is_inline().
|
||||||
|
INLINE_TAGS = {
|
||||||
|
'b', 'i', 's', 'u', 'color', 'big', 'small', 'url', 'lb', 'rb', 'at', 'd'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_tag(e, tag=None):
|
||||||
|
if e is None:
|
||||||
|
return False
|
||||||
|
if isinstance(e, str):
|
||||||
|
return False
|
||||||
|
if e['type'] != 'bbcode' and e['type'] != 'bbcode_void':
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tag is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return e['name'] == tag
|
||||||
|
|
||||||
|
|
||||||
|
def is_text(e):
|
||||||
|
return isinstance(e, str)
|
||||||
|
|
||||||
|
|
||||||
def is_inline(e):
|
def is_inline(e):
|
||||||
if e is None:
|
if e is None:
|
||||||
@@ -219,29 +498,12 @@ def is_inline(e):
|
|||||||
|
|
||||||
if is_tag(e):
|
if is_tag(e):
|
||||||
if is_tag(e, 'code'): # special case, since [code] can be inline OR block
|
if is_tag(e, 'code'): # special case, since [code] can be inline OR block
|
||||||
return '\n' not in e['children']
|
return '\n' not in e['children'][0]
|
||||||
|
|
||||||
return e['name'] in INLINE_TAGS
|
return e['name'] in INLINE_TAGS
|
||||||
|
|
||||||
return e['type'] != 'rule'
|
return e['type'] != 'rule'
|
||||||
|
|
||||||
def make_mention(e, mentions):
|
|
||||||
from ..models import Users
|
|
||||||
from flask import url_for
|
|
||||||
target_user = Users.find({'username': e['name'].lower()})
|
|
||||||
if not target_user:
|
|
||||||
return f"@{e['name']}"
|
|
||||||
|
|
||||||
mention_data = {
|
|
||||||
'mention_text': f"@{e['name']}",
|
|
||||||
'mentioned_user_id': int(target_user.id),
|
|
||||||
"start": e['start'],
|
|
||||||
"end": e['end'],
|
|
||||||
}
|
|
||||||
if mention_data not in mentions:
|
|
||||||
mentions.append(mention_data)
|
|
||||||
|
|
||||||
return f"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.page', username=target_user.username)}' title='@{target_user.username}' data-init='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
|
|
||||||
|
|
||||||
def should_collapse(text, surrounding):
|
def should_collapse(text, surrounding):
|
||||||
if not isinstance(text, str):
|
if not isinstance(text, str):
|
||||||
@@ -255,10 +517,30 @@ def should_collapse(text, surrounding):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def sanitize(s):
|
def sanitize(s):
|
||||||
return escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
return escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
||||||
|
|
||||||
def babycode_to_html(s, banned_tags=[]):
|
|
||||||
|
def babycode_ast(s: str, banned_tags=[]):
|
||||||
|
"""
|
||||||
|
transforms a string of babycode into an AST.
|
||||||
|
the AST is a list of strings or dicts.
|
||||||
|
|
||||||
|
a string element is plain unformatted text.
|
||||||
|
|
||||||
|
a dict element is a node that contains at least the key `type`.
|
||||||
|
|
||||||
|
possible types are:
|
||||||
|
- bbcode
|
||||||
|
- bbcode_void
|
||||||
|
- link
|
||||||
|
- emote
|
||||||
|
- rule
|
||||||
|
- mention
|
||||||
|
|
||||||
|
bbcode type elements have a children key that is a list of children of that node. the children are themselves elements (string or dict).
|
||||||
|
"""
|
||||||
allowed_tags = set(TAGS.keys())
|
allowed_tags = set(TAGS.keys())
|
||||||
if banned_tags is not None:
|
if banned_tags is not None:
|
||||||
for tag in banned_tags:
|
for tag in banned_tags:
|
||||||
@@ -281,44 +563,38 @@ def babycode_to_html(s, banned_tags=[]):
|
|||||||
)
|
)
|
||||||
if not should_collapse(e, surrounding):
|
if not should_collapse(e, surrounding):
|
||||||
elements.append(e)
|
elements.append(e)
|
||||||
|
return elements
|
||||||
|
|
||||||
out = ""
|
|
||||||
mentions = []
|
|
||||||
def fold(element, nobr, surrounding):
|
|
||||||
if isinstance(element, str):
|
|
||||||
if nobr:
|
|
||||||
return element
|
|
||||||
return break_lines(element)
|
|
||||||
|
|
||||||
match element['type']:
|
def babycode_to_html(s: str, banned_tags=[], fragment=False) -> BabycodeRenderResult:
|
||||||
case "bbcode":
|
"""
|
||||||
c = ""
|
transforms a string of babycode into html.
|
||||||
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"
|
|
||||||
c = c + Markup(fold(child, _nobr, _surrounding))
|
|
||||||
res = TAGS[element['name']](c, element['attr'], surrounding)
|
|
||||||
return res
|
|
||||||
case "bbcode_void":
|
|
||||||
return VOID_TAGS[element['name']](element['attr'])
|
|
||||||
case "link":
|
|
||||||
return f"<a href=\"{element['url']}\">{element['url']}</a>"
|
|
||||||
case 'emote':
|
|
||||||
return EMOJI[element['name']]
|
|
||||||
case "rule":
|
|
||||||
return "<hr>"
|
|
||||||
case "mention":
|
|
||||||
return make_mention(element, mentions)
|
|
||||||
|
|
||||||
for i in range(len(elements)):
|
parameters:
|
||||||
e = elements[i]
|
|
||||||
surrounding = (
|
s (str) - babycode string
|
||||||
elements[i - 1] if i-1 >= 0 else None,
|
|
||||||
elements[i + 1] if i+1 < len(elements) else None
|
banned_tags (list) - babycode tags to exclude from being parsed. they will remain as plain text in the transformation.
|
||||||
)
|
|
||||||
out = out + fold(e, False, surrounding)
|
fragment (bool) - skip adding an html p tag to the first element if it is inline.
|
||||||
return BabycodeParseResult(out, mentions)
|
"""
|
||||||
|
ast = babycode_ast(s, banned_tags)
|
||||||
|
r = HTMLRenderer(fragment=fragment)
|
||||||
|
return r.render(ast)
|
||||||
|
|
||||||
|
|
||||||
|
def babycode_to_rssxml(s: str, banned_tags=[], fragment=False) -> str:
|
||||||
|
"""
|
||||||
|
transforms a string of babycode into rss-compatible x/html.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
|
||||||
|
s (str) - babycode string
|
||||||
|
|
||||||
|
banned_tags (list) - babycode tags to exclude from being parsed. they will remain as plain text in the transformation.
|
||||||
|
|
||||||
|
fragment (bool) - skip adding an html p tag to the first element if it is inline.
|
||||||
|
"""
|
||||||
|
ast = babycode_ast(s, banned_tags)
|
||||||
|
r = RSSXMLRenderer(fragment=fragment)
|
||||||
|
return r.render(ast)
|
||||||
|
|||||||
9
app/lib/exceptions.py
Normal file
9
app/lib/exceptions.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class MissingConfigurationException(Exception):
|
||||||
|
def __init__(self, configuration_field: str):
|
||||||
|
message = f"Missing configuration field '{configuration_field}'"
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class SiteNameMissingException(MissingConfigurationException):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('SITE_NAME')
|
||||||
10
app/lib/render_atom.py
Normal file
10
app/lib/render_atom.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from flask import make_response, render_template, request
|
||||||
|
|
||||||
|
def render_atom_template(template, *args, **kwargs):
|
||||||
|
injects = {
|
||||||
|
**kwargs,
|
||||||
|
'__current_page': request.url,
|
||||||
|
}
|
||||||
|
r = make_response(render_template(template, *args, **injects))
|
||||||
|
r.mimetype = 'application/xml'
|
||||||
|
return r
|
||||||
@@ -43,6 +43,7 @@ MIGRATIONS = [
|
|||||||
add_signature_format,
|
add_signature_format,
|
||||||
create_default_bookmark_collections,
|
create_default_bookmark_collections,
|
||||||
add_display_name,
|
add_display_name,
|
||||||
|
'ALTER TABLE "post_history" ADD COLUMN "content_rss" STRING DEFAULT NULL'
|
||||||
]
|
]
|
||||||
|
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
|
|||||||
@@ -230,6 +230,38 @@ class Topics(Model):
|
|||||||
|
|
||||||
return db.query(q, self.id, per_page, (page - 1) * per_page)
|
return db.query(q, self.id, per_page, (page - 1) * per_page)
|
||||||
|
|
||||||
|
def get_threads_with_op_rss(self):
|
||||||
|
q = """
|
||||||
|
SELECT
|
||||||
|
threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
||||||
|
users.username AS started_by,
|
||||||
|
users.display_name AS started_by_display_name,
|
||||||
|
ph.content_rss AS original_post_content,
|
||||||
|
posts.id AS original_post_id
|
||||||
|
FROM
|
||||||
|
threads
|
||||||
|
JOIN users ON users.id = threads.user_id
|
||||||
|
JOIN (
|
||||||
|
SELECT
|
||||||
|
posts.thread_id,
|
||||||
|
posts.id,
|
||||||
|
posts.user_id,
|
||||||
|
posts.created_at,
|
||||||
|
posts.current_revision_id,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY posts.thread_id ORDER BY posts.created_at ASC) AS rn
|
||||||
|
FROM
|
||||||
|
posts
|
||||||
|
) posts ON posts.thread_id = threads.id AND posts.rn = 1
|
||||||
|
JOIN
|
||||||
|
post_history ph ON ph.id = posts.current_revision_id
|
||||||
|
JOIN
|
||||||
|
users u ON u.id = posts.user_id
|
||||||
|
WHERE
|
||||||
|
threads.topic_id = ?
|
||||||
|
ORDER BY threads.created_at DESC"""
|
||||||
|
|
||||||
|
return db.query(q, self.id)
|
||||||
|
|
||||||
|
|
||||||
class Threads(Model):
|
class Threads(Model):
|
||||||
table = "threads"
|
table = "threads"
|
||||||
@@ -238,6 +270,10 @@ class Threads(Model):
|
|||||||
q = Posts.FULL_POSTS_QUERY + " WHERE posts.thread_id = ? ORDER BY posts.created_at ASC LIMIT ? OFFSET ?"
|
q = Posts.FULL_POSTS_QUERY + " WHERE posts.thread_id = ? ORDER BY posts.created_at ASC LIMIT ? OFFSET ?"
|
||||||
return db.query(q, self.id, limit, offset)
|
return db.query(q, self.id, limit, offset)
|
||||||
|
|
||||||
|
def get_posts_rss(self):
|
||||||
|
q = Posts.FULL_POSTS_QUERY + ' WHERE posts.thread_id = ?'
|
||||||
|
return db.query(q, self.id)
|
||||||
|
|
||||||
def locked(self):
|
def locked(self):
|
||||||
return bool(self.is_locked)
|
return bool(self.is_locked)
|
||||||
|
|
||||||
@@ -265,7 +301,7 @@ class Posts(Model):
|
|||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
posts.id, posts.created_at,
|
posts.id, posts.created_at,
|
||||||
post_history.content, post_history.edited_at,
|
post_history.content, post_history.edited_at, post_history.content_rss,
|
||||||
users.username, users.display_name, users.status,
|
users.username, users.display_name, users.status,
|
||||||
avatars.file_path AS avatar_path, posts.thread_id,
|
avatars.file_path AS avatar_path, posts.thread_id,
|
||||||
users.id AS user_id, post_history.original_markup,
|
users.id AS user_id, post_history.original_markup,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from flask import Blueprint, redirect, url_for, render_template
|
from flask import Blueprint, redirect, url_for, render_template
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
bp = Blueprint("app", __name__, url_prefix = "/")
|
bp = Blueprint("app", __name__, url_prefix = "/")
|
||||||
|
|
||||||
|
|||||||
@@ -60,11 +60,3 @@ def get_badges():
|
|||||||
uploads = BadgeUploads.get_for_user(get_active_user().id)
|
uploads = BadgeUploads.get_for_user(get_active_user().id)
|
||||||
badges = sorted(Badges.findall({'user_id': int(get_active_user().id)}), key=lambda x: x['sort_order'])
|
badges = sorted(Badges.findall({'user_id': int(get_active_user().id)}), key=lambda x: x['sort_order'])
|
||||||
return render_template('components/badge_editor_badges.html', uploads=uploads, badges=badges)
|
return render_template('components/badge_editor_badges.html', uploads=uploads, badges=badges)
|
||||||
|
|
||||||
|
|
||||||
@bp.get('/badge-editor/template')
|
|
||||||
@login_required
|
|
||||||
@account_required
|
|
||||||
def get_badge_template():
|
|
||||||
uploads = BadgeUploads.get_for_user(get_active_user().id)
|
|
||||||
return render_template('components/badge_editor_template.html', uploads=uploads)
|
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ def sort_topics():
|
|||||||
|
|
||||||
@bp.post("/sort-topics")
|
@bp.post("/sort-topics")
|
||||||
def sort_topics_post():
|
def sort_topics_post():
|
||||||
|
topics_list = request.form.getlist('topics[]')
|
||||||
|
print(topics_list)
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
for topic_id, new_order in request.form.items():
|
for new_order, topic_id in enumerate(topics_list):
|
||||||
db.execute("UPDATE topics SET sort_order = ? WHERE id = ?", new_order, topic_id)
|
db.execute("UPDATE topics SET sort_order = ? WHERE id = ?", new_order, topic_id)
|
||||||
|
|
||||||
return redirect(url_for(".sort_topics"))
|
return redirect(url_for(".sort_topics"))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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, BABYCODE_VERSION
|
from ..lib.babycode import babycode_to_html, babycode_to_rssxml, BABYCODE_VERSION
|
||||||
from ..constants import InfoboxKind
|
from ..constants import InfoboxKind
|
||||||
from ..db import db
|
from ..db import db
|
||||||
from ..models import Posts, PostHistory, Threads, Topics, Mentions
|
from ..models import Posts, PostHistory, Threads, Topics, Mentions
|
||||||
@@ -12,6 +12,7 @@ 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)
|
parsed_content = babycode_to_html(content)
|
||||||
|
parsed_rss = babycode_to_rssxml(content)
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
post = Posts.create({
|
post = Posts.create({
|
||||||
"thread_id": thread_id,
|
"thread_id": thread_id,
|
||||||
@@ -22,6 +23,7 @@ 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": parsed_content.result,
|
"content": parsed_content.result,
|
||||||
|
"content_rss": parsed_rss,
|
||||||
"is_initial_revision": True,
|
"is_initial_revision": True,
|
||||||
"original_markup": content,
|
"original_markup": content,
|
||||||
"markup_language": markup_language,
|
"markup_language": markup_language,
|
||||||
@@ -43,11 +45,13 @@ 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)
|
parsed_content = babycode_to_html(new_content)
|
||||||
|
parsed_rss = babycode_to_rssxml(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': parsed_content.result,
|
'content': parsed_content.result,
|
||||||
|
"content_rss": parsed_rss,
|
||||||
'is_initial_revision': False,
|
'is_initial_revision': False,
|
||||||
'original_markup': new_content,
|
'original_markup': new_content,
|
||||||
'markup_language': markup_language,
|
'markup_language': markup_language,
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for, flash,
|
Blueprint, render_template, request, redirect, url_for, flash,
|
||||||
abort,
|
abort, current_app,
|
||||||
)
|
)
|
||||||
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, Reactions
|
from ..models import Threads, Topics, Posts, Subscriptions, Reactions
|
||||||
from ..constants import InfoboxKind
|
from ..constants import InfoboxKind
|
||||||
|
from ..lib.render_atom import render_atom_template
|
||||||
from .posts import create_post
|
from .posts import create_post
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
from app import cache
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
|
||||||
bp = Blueprint("threads", __name__, url_prefix = "/threads/")
|
bp = Blueprint("threads", __name__, url_prefix = "/threads/")
|
||||||
|
|
||||||
|
|
||||||
def get_post_url(post_id, _anchor=False):
|
def get_post_url(post_id, _anchor=False, external=False):
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
if not post:
|
if not post:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
thread = Threads.find({'id': post.thread_id})
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
|
||||||
res = url_for('threads.thread', slug=thread.slug, after=post_id)
|
anchor = None if not _anchor else f'post-{post_id}'
|
||||||
if not _anchor:
|
|
||||||
return res
|
|
||||||
|
|
||||||
return f"{res}#post-{post_id}"
|
return url_for('threads.thread', slug=thread.slug, after=post_id, _external=external, _anchor=anchor)
|
||||||
|
# if not _anchor:
|
||||||
|
# return res
|
||||||
|
|
||||||
|
# return f"{res}#post-{post_id}"
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<slug>")
|
@bp.get("/<slug>")
|
||||||
@@ -80,9 +84,25 @@ def thread(slug):
|
|||||||
is_subscribed = is_subscribed,
|
is_subscribed = is_subscribed,
|
||||||
Reactions = Reactions,
|
Reactions = Reactions,
|
||||||
unread_count = unread_count,
|
unread_count = unread_count,
|
||||||
|
__feedlink = url_for('.thread_atom', slug=slug, _external=True),
|
||||||
|
__feedtitle = f'replies to {thread.title}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/<slug>/feed.atom")
|
||||||
|
@cache.cached(timeout=5 * 60, unless=lambda: current_app.config.get('DEBUG', False))
|
||||||
|
def thread_atom(slug):
|
||||||
|
thread = Threads.find({"slug": slug})
|
||||||
|
if not thread:
|
||||||
|
abort(404) # TODO throw an atom friendly 404
|
||||||
|
return
|
||||||
|
|
||||||
|
topic = Topics.find({'id': thread.topic_id})
|
||||||
|
posts = thread.get_posts_rss()
|
||||||
|
|
||||||
|
return render_atom_template('threads/thread.atom', thread=thread, topic=topic, posts=posts, get_post_url=get_post_url)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<slug>")
|
@bp.post("/<slug>")
|
||||||
@login_required
|
@login_required
|
||||||
def reply(slug):
|
def reply(slug):
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for, flash, session,
|
Blueprint, render_template, request, redirect, url_for, flash, session,
|
||||||
abort,
|
abort, current_app
|
||||||
)
|
)
|
||||||
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 ..models import Users, Topics, Threads, Subscriptions
|
from ..models import Users, Topics, Threads, Subscriptions
|
||||||
from ..constants import InfoboxKind
|
from ..constants import InfoboxKind
|
||||||
|
from ..lib.render_atom import render_atom_template
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
from app import cache
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
|
||||||
@@ -80,10 +82,27 @@ def topic(slug):
|
|||||||
subscriptions = subscriptions,
|
subscriptions = subscriptions,
|
||||||
topic = target_topic,
|
topic = target_topic,
|
||||||
current_page = page,
|
current_page = page,
|
||||||
page_count = page_count
|
page_count = page_count,
|
||||||
|
__feedlink = url_for('.topic_atom', slug=slug, _external=True),
|
||||||
|
__feedtitle = f'latest threads in {target_topic.name}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get('/<slug>/feed.atom')
|
||||||
|
@cache.cached(timeout=10 * 60, unless=lambda: current_app.config.get('DEBUG', False))
|
||||||
|
def topic_atom(slug):
|
||||||
|
target_topic = Topics.find({
|
||||||
|
"slug": slug
|
||||||
|
})
|
||||||
|
if not target_topic:
|
||||||
|
abort(404) # TODO throw an atom friendly 404
|
||||||
|
return
|
||||||
|
|
||||||
|
threads_list = target_topic.get_threads_with_op_rss()
|
||||||
|
|
||||||
|
return render_atom_template('topics/topic.atom', threads_list=threads_list, target_topic=target_topic)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<slug>/edit")
|
@bp.get("/<slug>/edit")
|
||||||
@login_required
|
@login_required
|
||||||
@mod_only(".topic", slug = lambda slug: slug)
|
@mod_only(".topic", slug = lambda slug: slug)
|
||||||
|
|||||||
@@ -74,7 +74,17 @@ def validate_and_create_badge(input_image, filename):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def is_logged_in():
|
def is_logged_in():
|
||||||
return "pyrom_session_key" in session
|
if "pyrom_session_key" not in session:
|
||||||
|
return False
|
||||||
|
sess = Sessions.find({"key": session["pyrom_session_key"]})
|
||||||
|
if not sess:
|
||||||
|
return False
|
||||||
|
if sess.expires_at < int(time.time()):
|
||||||
|
session.clear()
|
||||||
|
sess.delete()
|
||||||
|
flash('Your session expired.;Please log in again.', InfoboxKind.INFO)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_active_user():
|
def get_active_user():
|
||||||
@@ -83,6 +93,8 @@ def get_active_user():
|
|||||||
sess = Sessions.find({"key": session["pyrom_session_key"]})
|
sess = Sessions.find({"key": session["pyrom_session_key"]})
|
||||||
if not sess:
|
if not sess:
|
||||||
return None
|
return None
|
||||||
|
if sess.expires_at < int(time.time()):
|
||||||
|
return None
|
||||||
return Users.find({"id": sess.user_id})
|
return Users.find({"id": sess.user_id})
|
||||||
|
|
||||||
|
|
||||||
@@ -394,7 +406,8 @@ def page(username):
|
|||||||
@login_required
|
@login_required
|
||||||
@redirect_to_own
|
@redirect_to_own
|
||||||
def settings(username):
|
def settings(username):
|
||||||
return render_template('users/settings.html')
|
uploads = BadgeUploads.get_for_user(get_active_user().id)
|
||||||
|
return render_template('users/settings.html', uploads=uploads)
|
||||||
|
|
||||||
|
|
||||||
@bp.post('/<username>/settings')
|
@bp.post('/<username>/settings')
|
||||||
@@ -644,12 +657,29 @@ def inbox(username):
|
|||||||
subscriptions ON subscriptions.thread_id = posts.thread_id
|
subscriptions ON subscriptions.thread_id = posts.thread_id
|
||||||
WHERE subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen
|
WHERE subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen
|
||||||
GROUP BY posts.thread_id
|
GROUP BY posts.thread_id
|
||||||
|
),
|
||||||
|
user_badges AS (
|
||||||
|
SELECT
|
||||||
|
b.user_id,
|
||||||
|
json_group_array(
|
||||||
|
json_object(
|
||||||
|
'label', b.label,
|
||||||
|
'link', b.link,
|
||||||
|
'sort_order', b.sort_order,
|
||||||
|
'file_path', bu.file_path
|
||||||
|
)
|
||||||
|
) AS badges_json
|
||||||
|
FROM badges b
|
||||||
|
LEFT JOIN badge_uploads bu ON b.upload = bu.id
|
||||||
|
GROUP BY b.user_id
|
||||||
|
ORDER BY b.sort_order
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
tm.thread_id, tm.thread_slug, tm.thread_title, tm.unread_count, tm.newest_post_time,
|
tm.thread_id, tm.thread_slug, tm.thread_title, tm.unread_count, tm.newest_post_time,
|
||||||
|
|
||||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered
|
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered,
|
||||||
|
COALESCE(user_badges.badges_json, '[]') AS badges_json
|
||||||
FROM
|
FROM
|
||||||
thread_metadata tm
|
thread_metadata tm
|
||||||
JOIN
|
JOIN
|
||||||
@@ -664,6 +694,8 @@ def inbox(username):
|
|||||||
avatars ON users.avatar_id = avatars.id
|
avatars ON users.avatar_id = avatars.id
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
subscriptions ON subscriptions.thread_id = posts.thread_id
|
subscriptions ON subscriptions.thread_id = posts.thread_id
|
||||||
|
LEFT JOIN
|
||||||
|
user_badges ON users.id = user_badges.user_id
|
||||||
WHERE
|
WHERE
|
||||||
subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen
|
subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen
|
||||||
ORDER BY
|
ORDER BY
|
||||||
@@ -697,6 +729,7 @@ def inbox(username):
|
|||||||
'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'],
|
||||||
|
'badges_json': row['badges_json'],
|
||||||
|
|
||||||
'thread_slug': row['thread_slug'],
|
'thread_slug': row['thread_slug'],
|
||||||
})
|
})
|
||||||
@@ -863,6 +896,10 @@ def delete_page_confirm(username):
|
|||||||
flash('Incorrect password.', InfoboxKind.ERROR)
|
flash('Incorrect password.', InfoboxKind.ERROR)
|
||||||
return redirect(url_for('.delete_page', username=username))
|
return redirect(url_for('.delete_page', username=username))
|
||||||
|
|
||||||
|
if target_user.is_admin():
|
||||||
|
flash('You cannot delete the admin account.', InfoboxKind.ERROR)
|
||||||
|
return redirect(url_for('.delete_page', username=username))
|
||||||
|
|
||||||
anonymize_user(target_user.id)
|
anonymize_user(target_user.id)
|
||||||
sessions = Sessions.findall({'user_id': int(target_user.id)})
|
sessions = Sessions.findall({'user_id': int(target_user.id)})
|
||||||
for session_obj in sessions:
|
for session_obj in sessions:
|
||||||
|
|||||||
20
app/templates/base.atom
Normal file
20
app/templates/base.atom
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
{% if self.title() %}
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
{% else %}
|
||||||
|
<title>{{ config.SITE_NAME }}</title>
|
||||||
|
{% endif %}
|
||||||
|
{% if self.feed_updated() %}
|
||||||
|
<updated>{% block feed_updated %}{% endblock %}</updated>
|
||||||
|
{% else %}
|
||||||
|
<updated>{{ get_time_now() | iso8601 }}</updated>
|
||||||
|
{% endif %}
|
||||||
|
<id>{{ __current_page }}</id>
|
||||||
|
<link rel="self" href="{{ __current_page }}" />
|
||||||
|
<link href="{% block canonical_link %}{% endblock %}" />
|
||||||
|
{% if self.feed_author() %}
|
||||||
|
<author>{% block feed_author %}{% endblock %}</author>
|
||||||
|
{% endif %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</feed>
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
|
<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">
|
||||||
<script src="{{ '/static/js/vnd/bitty-7.0.0-rc1.min.js' | cachebust }}" type="module"></script>
|
<script src="{{ '/static/js/vnd/bitty-7.0.0.js' | cachebust }}" type="module"></script>
|
||||||
|
{% if __feedlink %}
|
||||||
|
<link rel="alternate" type="application/atom+xml" href="{{ __feedlink }}" title="{{ __feedtitle }}">
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}">
|
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}">
|
||||||
|
|||||||
@@ -53,3 +53,15 @@
|
|||||||
<path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro icn_rss(width=24) %}
|
||||||
|
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 11C9.41828 11 13 14.5817 13 19M5 5C12.732 5 19 11.268 19 19M7 18C7 18.5523 6.55228 19 6 19C5.44772 19 5 18.5523 5 18C5 17.4477 5.44772 17 6 17C6.55228 17 7 17.4477 7 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro icn_drag(width=24) %}
|
||||||
|
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 10H19M14 19L12 21L10 19M14 5L12 3L10 5M5 14H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone %}
|
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone, icn_rss, icn_drag %}
|
||||||
|
{%- macro dict_to_attr(attrs) -%}
|
||||||
|
{%- for key, value in attrs.items() if value is not none -%}{{' '}}{{key}}="{{value}}"{%- endfor -%}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
{% macro pager(current_page, page_count) %}
|
{% macro pager(current_page, page_count) %}
|
||||||
{% set left_start = [1, current_page - 5] | max %}
|
{% set left_start = [1, current_page - 5] | max %}
|
||||||
{% set right_end = [page_count, current_page + 5] | min %}
|
{% set right_end = [page_count, current_page + 5] | min %}
|
||||||
@@ -331,8 +335,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set selected_href = defaults[0].file_path %}
|
{% set selected_href = defaults[0].file_path %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorBadge" data-listeners="click input submit change">
|
<li class="sortable-item" data-sortable-list-key="" data-receive="deleteBadge"> {# breaking convention on purpose since this one is special #}
|
||||||
<div class="settings-badge-container" data-receive="deleteBadge">
|
<span class="dragger" draggable="true">{{ icn_drag(24) }}</span>
|
||||||
|
<bitty-7-0 class="fg" data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorBadge" data-listeners="click input submit change">
|
||||||
|
<div class="settings-badge-container">
|
||||||
<div class="settings-badge-select">
|
<div class="settings-badge-select">
|
||||||
<select data-send="badgeUpdatePreview badgeToggleFilePicker" name="badge_choice[]" required>
|
<select data-send="badgeUpdatePreview badgeToggleFilePicker" name="badge_choice[]" required>
|
||||||
<optgroup label="Default">
|
<optgroup label="Default">
|
||||||
@@ -358,4 +364,30 @@
|
|||||||
<button data-send="deleteBadge" type="button" class="critical" title="Delete">X</button>
|
<button data-send="deleteBadge" type="button" class="critical" title="Delete">X</button>
|
||||||
</div>
|
</div>
|
||||||
</bitty-7-0>
|
</bitty-7-0>
|
||||||
|
</li>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro rss_html_content(html) %}
|
||||||
|
<content type="html">{{ html }}</content>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro rss_button(feed) %}
|
||||||
|
<a class="linkbutton contain-svg inline icon rss-button" href="{{feed}}" title="it's actually atom, don't tell anyone ;)">{{ icn_rss(20) }} Subscribe via RSS</a>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro sortable_list(attr=none) %}
|
||||||
|
<ol class="sortable-list" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||||
|
{% if caller %}
|
||||||
|
{{ caller() }}
|
||||||
|
{% endif %}
|
||||||
|
</ol>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro sortable_list_item(key, immovable=false, attr=none) %}
|
||||||
|
<li class="sortable-item{{' immovable' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||||
|
<span class="dragger" draggable="{{ 'true' if not immovable else 'false'}}">{{ icn_drag(24) }}</span>
|
||||||
|
<div class="sortable-item-inner">
|
||||||
|
{{ caller() }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% from 'common/macros.html' import badge_editor_single with context %}
|
{% from 'common/macros.html' import badge_editor_single %}
|
||||||
{% for badge in badges %}
|
{% for badge in badges %}
|
||||||
{{ badge_editor_single(options=uploads, selected=badge.upload, badge=badge) }}
|
{{ badge_editor_single(options=uploads, selected=badge.upload, badge=badge) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
{% from 'common/macros.html' import badge_editor_single with context %}
|
{% from 'common/macros.html' import badge_editor_single %}
|
||||||
{{ badge_editor_single(options=uploads) }}
|
{{ badge_editor_single(options=uploads) }}
|
||||||
|
|||||||
@@ -95,12 +95,12 @@
|
|||||||
<h2 id="paragraph-rules">Paragraph rules</h2>
|
<h2 id="paragraph-rules">Paragraph rules</h2>
|
||||||
<p>Line breaks in babycode work like Markdown: to start a new paragraph, use two line breaks:</p>
|
<p>Line breaks in babycode work like Markdown: to start a new paragraph, use two line breaks:</p>
|
||||||
{{ '[code]paragraph 1\n\nparagraph 2[/code]' | babycode | safe }}
|
{{ '[code]paragraph 1\n\nparagraph 2[/code]' | babycode | safe }}
|
||||||
Will produce:<br>
|
Will produce:
|
||||||
{{ 'paragraph 1\n\nparagraph 2' | babycode | safe }}
|
{{ 'paragraph 1\n\nparagraph 2' | babycode(true) | safe }}
|
||||||
<p>To break a line without starting a new paragraph, end a line with two spaces:</p>
|
<p>To break a line without starting a new paragraph, end a line with two spaces:</p>
|
||||||
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
|
{{ '[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(true) | safe }}
|
||||||
<p>Additionally, the following tags will break into a new paragraph:</p>
|
<p>Additionally, the following tags will break into a new paragraph:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code class="inline-code">[code]</code> (code block, not inline);</li>
|
<li><code class="inline-code">[code]</code> (code block, not inline);</li>
|
||||||
@@ -113,21 +113,20 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="guide-section">
|
<section class="guide-section">
|
||||||
<h2 id="links">Links</h2>
|
<h2 id="links">Links</h2>
|
||||||
<p>Loose links (starting with http:// or https://) will automatically get converted to clickable links. To add a label to a link, use<br><code class="inline-code">[url=https://example.com]Link label[/url]</code>:<br>
|
<p>Loose links (starting with http:// or https://) will automatically get converted to clickable links. To add a label to a link, use <code class="inline-code">[url=https://example.com]Link label[/url]</code>:<br>
|
||||||
<a href="https://example.com">Link label</a></p>
|
<a href="https://example.com">Link label</a></p>
|
||||||
</section>
|
</section>
|
||||||
<section class="guide-section">
|
<section class="guide-section">
|
||||||
<h2 id="attaching-an-image">Attaching an image</h2>
|
<h2 id="attaching-an-image">Attaching an image</h2>
|
||||||
<p>To add an image to your post, use the <code class="inline-code">[img]</code> tag:<br>
|
<p>To add an image to your post, use the <code class="inline-code">[img]</code> tag:<br>
|
||||||
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code>
|
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code></p>
|
||||||
{{ '[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>The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
|
<p>The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
|
||||||
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p>
|
<p>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>
|
<p>Multiple images attached to a post can be clicked to open a dialog to view them.</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="guide-section">
|
<section class="guide-section">
|
||||||
<h2 id="adding-code-blocks">Adding code blocks</h2>
|
<h2 id="adding-code-blocks">Adding code blocūs</h2>
|
||||||
{% set code = 'func _ready() -> void:\n\tprint("hello world!")' %}
|
{% set code = 'func _ready() -> void:\n\tprint("hello world!")' %}
|
||||||
<p>There are two kinds of code blocks recognized by babycode: inline and block. Inline code blocks do not break a paragraph. They can be added with <code class="inline-code">[code]your code here[/code]</code>. As long as there are no line breaks inside the code block, it is considered inline. If there are any, it will produce this:</p>
|
<p>There are two kinds of code blocks recognized by babycode: inline and block. Inline code blocks do not break a paragraph. They can be added with <code class="inline-code">[code]your code here[/code]</code>. As long as there are no line breaks inside the code block, it is considered inline. If there are any, it will produce this:</p>
|
||||||
{{ ('[code]%s[/code]' % code) | babycode | safe }}
|
{{ ('[code]%s[/code]' % code) | babycode | safe }}
|
||||||
@@ -168,9 +167,18 @@
|
|||||||
<a class="mention display me" href="#mentions" title="@your-username">Your display name</a>
|
<a class="mention display me" href="#mentions" title="@your-username">Your display name</a>
|
||||||
<p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p>
|
<p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="guide-section">
|
||||||
|
{% set hr_example = "some section\n---\nanother section" %}
|
||||||
|
<h2 id="rule">Horizontal rules</h2>
|
||||||
|
<p>The special <code class="inline-code">---</code> markup inserts a horizontal separator, also known as a horizontal rule:</p>
|
||||||
|
{{ ("[code]%s[/code]" % hr_example) | babycode | safe }}
|
||||||
|
Will become
|
||||||
|
{{ hr_example | babycode(true) | safe}}
|
||||||
|
<p>Horizontal rules will always break the current paragraph.</p>
|
||||||
|
</section>
|
||||||
<section class="guide-section">
|
<section class="guide-section">
|
||||||
<h2 id="void-tags">Void tags</h2>
|
<h2 id="void-tags">Void tags</h2>
|
||||||
<p>The special void tags <code class="inline-code">[lb]</code>, <code class="inline-code">[rb]</code>, and <code class="inline-code">[@]</code> will appear as the literal characters <code class="inline-code">[</code>, <code class="inline-code">]</code>, and <code class="inline-code">@</code> respectively. Unlike other tags, they are self-contained and have no closing equivalent.</p>
|
<p>The special void tags <code class="inline-code">[lb]</code>, <code class="inline-code">[rb]</code>, <code class="inline-code">[d]</code> and <code class="inline-code">[at]</code> will appear as the literal characters <code class="inline-code">[</code>, <code class="inline-code">]</code>, <code class="inline-code">-</code>, and <code class="inline-code">@</code> respectively. Unlike other tags, they are self-contained and have no closing equivalent.</p>
|
||||||
<ul class="guide-list">
|
<ul class="guide-list">
|
||||||
{% set lbrb = "[color=red]This text will be red[/color]\n\n[lb]color=red[rb]This text won't be red[lb]/color[rb]" %}
|
{% set lbrb = "[color=red]This text will be red[/color]\n\n[lb]color=red[rb]This text won't be red[lb]/color[rb]" %}
|
||||||
<li><code class="inline-code">[lb]</code> and <code class="inline-code">[rb]</code> allow you to use square brackets without them being interpreted as Babycode:
|
<li><code class="inline-code">[lb]</code> and <code class="inline-code">[rb]</code> allow you to use square brackets without them being interpreted as Babycode:
|
||||||
@@ -178,7 +186,8 @@
|
|||||||
Will result in:<br>
|
Will result in:<br>
|
||||||
{{ lbrb | babycode | safe }}
|
{{ lbrb | babycode | safe }}
|
||||||
</li>
|
</li>
|
||||||
<li>The <code class="inline-code">[@]</code> tag allows you to use the @ symbol without it being turned into a mention.</li>
|
<li>The <code class="inline-code">[at]</code> tag allows you to use the @ symbol without it being turned into a mention.</li>
|
||||||
|
<li>The <code class="inline-code">[d]</code> tag allows you to use the - (dash) symbol without it being turned into a rule.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from 'common/macros.html' import sortable_list, sortable_list_item %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="darkbg">
|
<div class="darkbg">
|
||||||
<h1>Change topics order</h1>
|
<h1>Change topics order</h1>
|
||||||
<p>Drag topic titles to reoder them. Press submit when done. The topics will appear to users in the order set here.</p>
|
<p>Drag topic titles to reoder them. Press "Save order" when done. The topics will appear to users in the order set here.</p>
|
||||||
<form method="post" id=topics-container>
|
<form method="post">
|
||||||
{% for topic in topics %}
|
|
||||||
<div draggable="true" class="draggable-topic" ondragover="dragOver(event)" ondragstart="dragStart(event)" ondragend="dragEnd()">
|
|
||||||
<div class="thread-title">{{ topic['name'] }}</div>
|
|
||||||
<div>{{ topic.description }}</div>
|
|
||||||
<input type="hidden" name="{{ topic['id'] }}" value="{{ topic['sort-order'] }}" class="topic-input">
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<input type=submit value="Save order">
|
<input type=submit value="Save order">
|
||||||
|
{% call() sortable_list() %}
|
||||||
|
{% for topic in topics %}
|
||||||
|
{% call() sortable_list_item(key="topics") %}
|
||||||
|
<div class="thread-title">{{ topic.name }}</div>
|
||||||
|
<div>{{ topic.description }}</div>
|
||||||
|
<input type="hidden" name="topics[]" value="{{ topic.id }}" class="topic-input">
|
||||||
|
{% endcall %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endcall %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ "/static/js/sort-topics.js" | cachebust }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
19
app/templates/threads/thread.atom
Normal file
19
app/templates/threads/thread.atom
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'base.atom' %}
|
||||||
|
{% from 'common/macros.html' import rss_html_content %}
|
||||||
|
{% block title %}replies to {{thread.title}}{% endblock %}
|
||||||
|
{% block canonical_link %}{{url_for('threads.thread', slug=thread.slug, _external=true)}}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% for post in posts %}
|
||||||
|
{% set post_url = get_post_url(post.id, _anchor=true, external=true) %}
|
||||||
|
<entry>
|
||||||
|
<title>Re: {{ thread.title }}</title>
|
||||||
|
<link href="{{ post_url }}"/>
|
||||||
|
<id>{{ post_url }}</id>
|
||||||
|
<updated>{{ post.edited_at | iso8601 }}</updated>
|
||||||
|
{{rss_html_content(post.content_rss)}}
|
||||||
|
<author>
|
||||||
|
<name>{{ post.display_name }} @{{ post.username }}</name>
|
||||||
|
</author>
|
||||||
|
</entry>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% from 'common/macros.html' import pager, babycode_editor_form, full_post, bookmark_button %}
|
{% from 'common/macros.html' import pager, babycode_editor_form, full_post, bookmark_button, rss_button %}
|
||||||
{% from 'common/icons.html' import icn_bookmark %}
|
{% from 'common/icons.html' import icn_bookmark %}
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ thread.title }}{% endblock %}
|
{% block title %}{{ thread.title }}{% endblock %}
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
<input class="warn" type="submit" value="Move thread">
|
<input class="warn" type="submit" value="Move thread">
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ rss_button(url_for('threads.thread_atom', slug=thread.slug)) }}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
|
|||||||
20
app/templates/topics/topic.atom
Normal file
20
app/templates/topics/topic.atom
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'base.atom' %}
|
||||||
|
{% from 'common/macros.html' import rss_html_content %}
|
||||||
|
{% block title %}latest threads in {{target_topic.name}}{% endblock %}
|
||||||
|
{% block canonical_link %}{{url_for('topics.topic', slug=target_topic.slug, _external=true)}}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<subtitle>{{ target_topic.description }}</subtitle>
|
||||||
|
{% for thread in threads_list %}
|
||||||
|
<entry>
|
||||||
|
<title>[new thread] {{ thread.title | escape }}</title>
|
||||||
|
<link href="{{ url_for('threads.thread', slug=thread.slug, _external=true)}}" />
|
||||||
|
<link rel="replies" type="application/atom+xml" href="{{ url_for('threads.thread_atom', slug=thread.slug, _external=true)}}" />
|
||||||
|
<id>{{ url_for('threads.thread', slug=thread.slug, _external=true)}}</id>
|
||||||
|
<updated>{{ thread.created_at | iso8601 }}</updated>
|
||||||
|
{{rss_html_content(thread.original_post_content)}}
|
||||||
|
<author>
|
||||||
|
<name>{{thread.started_by_display_name}} @{{ thread.started_by }}</name>
|
||||||
|
</author>
|
||||||
|
</entry>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% from 'common/macros.html' import pager, timestamp, motd %}
|
{% from 'common/macros.html' import pager, timestamp, motd, rss_button %}
|
||||||
{% from 'common/icons.html' import icn_lock, icn_sticky %}
|
{% from 'common/icons.html' import icn_lock, icn_sticky %}
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}browsing topic {{ topic['name'] }}{% endblock %}
|
{% block title %}browsing topic {{ topic['name'] }}{% endblock %}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<nav class="darkbg">
|
<nav class="darkbg">
|
||||||
<h1 class="thread-title">All threads in "{{topic['name']}}"</h1>
|
<h1 class="thread-title">All threads in "{{topic['name']}}"</h1>
|
||||||
<span>{{topic['description']}}</span>
|
<span>{{topic['description']}}</span>
|
||||||
<div>
|
<div class="thread-actions">
|
||||||
{% if active_user %}
|
{% if active_user %}
|
||||||
{% if not (topic['is_locked']) | int or active_user.is_mod() %}
|
{% if not (topic['is_locked']) | int or active_user.is_mod() %}
|
||||||
<a class="linkbutton" href="{{ url_for("threads.create", topic_id=topic['id']) }}">New thread</a>
|
<a class="linkbutton" href="{{ url_for("threads.create", topic_id=topic['id']) }}">New thread</a>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<input class="warn" type="submit" id="lock" value="{{"Unlock topic" if topic['is_locked'] else "Lock topic"}}">
|
<input class="warn" type="submit" id="lock" value="{{"Unlock topic" if topic['is_locked'] else "Lock topic"}}">
|
||||||
</form>
|
</form>
|
||||||
<button type="button" class="critical" id="topic-delete-dialog-open">Delete</button>
|
<button type="button" class="critical" id="topic-delete-dialog-open">Delete</button>
|
||||||
|
{{ rss_button(url_for('topics.topic_atom', slug=topic.slug)) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,41 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from 'common/macros.html' import sortable_list, sortable_list_item %}
|
||||||
{% block title %}managing bookmark collections{% endblock %}
|
{% block title %}managing bookmark collections{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="darkbg">
|
<div class="darkbg">
|
||||||
<h1>Manage bookmark collections</h1>
|
<h1>Manage bookmark collections</h1>
|
||||||
<p>Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.</p>
|
<p>Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.</p>
|
||||||
<div>
|
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} CollectionsEditor">
|
||||||
<button type="button" id="add-collection-button">Add new collection</button>
|
<button type="button" data-send="addCollection">Add new collection</button>
|
||||||
<div id="collections-container">
|
{% set sorted_collections = collections | sort(attribute='sort_order') %}
|
||||||
{% for collection in collections | sort(attribute='sort_order') %}
|
{% macro collection_inner(collection) %}
|
||||||
<div class="draggable-collection {{ "default" if collection.is_default else ""}}"
|
<input type="text" class="collection-name" value="{{collection.name}}" placeholder="Collection name" required autocomplete="off" maxlength="60">
|
||||||
{% if not collection.is_default %}
|
|
||||||
draggable="true"
|
|
||||||
ondragover="dragOver(event)"
|
|
||||||
ondragstart="dragStart(event)"
|
|
||||||
ondragend="dragEnd()"
|
|
||||||
{% else %}
|
|
||||||
id="default-collection"
|
|
||||||
{% endif %}
|
|
||||||
data-collection-id="{{ collection.id }}">
|
|
||||||
<input type="text" class="collection-name" value="{{ collection.name }}" placeholder="Collection name" required autocomplete="off" maxlength="60"><br>
|
|
||||||
<div>{{ collection.get_threads_count() }} {{ "thread" | pluralize(num=collection.get_threads_count()) }}, {{ collection.get_posts_count() }} {{ "post" | pluralize(num=collection.get_posts_count()) }}</div>
|
<div>{{ collection.get_threads_count() }} {{ "thread" | pluralize(num=collection.get_threads_count()) }}, {{ collection.get_posts_count() }} {{ "post" | pluralize(num=collection.get_posts_count()) }}</div>
|
||||||
{% if collection.is_default %}
|
{% if collection.is_default %}
|
||||||
<i>Default collection</i>
|
<i>Default collection</i>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="delete-button critical">Delete</button>
|
<button type="button" class="delete-button critical" data-send="deleteCollection">Delete</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% endmacro %}
|
||||||
|
{% call() sortable_list(attr={'data-receive': 'addCollection' }) %}
|
||||||
|
{% call() sortable_list_item(key='collections', immovable=true, attr={'data-collection-id': sorted_collections[0].id, 'data-receive': 'deleteCollection getCollectionData testValidity'}) %}
|
||||||
|
{{ collection_inner(sorted_collections[0]) }}
|
||||||
|
{% endcall %}
|
||||||
|
{% for collection in sorted_collections[1:] %}
|
||||||
|
{% call() sortable_list_item(key='collections', attr={'data-collection-id': collection.id, 'data-receive': 'deleteCollection getCollectionData testValidity'}) %}
|
||||||
|
{{ collection_inner(collection) }}
|
||||||
|
{% endcall %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endcall %}
|
||||||
|
<button data-use="saveCollections" type="button" id="save-button" data-submit-href="{{ url_for('api.manage_bookmark_collections', user_id=active_user.id) }}">Save</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="save-button" data-submit-href="{{ url_for('api.manage_bookmark_collections', user_id=active_user.id) }}">Save</button>
|
<template id="new-collection-template">
|
||||||
</div>
|
{% call() sortable_list_item(key='collections', attr={'data-receive': 'deleteCollection getCollectionData testValidity'}) %}
|
||||||
</div>
|
<input type="text" class="collection-name" value="" placeholder="Collection name" required autocomplete="off" maxlength="60">
|
||||||
<script src="{{ "/static/js/manage-bookmark-collections.js" | cachebust }}"></script>
|
<div>0 threads, 0 posts</div>
|
||||||
|
<button type="button" class="delete-button critical" data-send="deleteCollection">Delete</button>
|
||||||
|
{% endcall %}
|
||||||
|
</template>
|
||||||
|
</bitty-7-0>
|
||||||
|
{#<script src="{{ "/static/js/manage-bookmark-collections.js" | cachebust }}"></script>#}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% from 'common/macros.html' import babycode_editor_component %}
|
{% from 'common/macros.html' import babycode_editor_component, badge_editor_single, sortable_list %}
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}settings{% endblock %}
|
{% block title %}settings{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -18,6 +18,16 @@
|
|||||||
<span>1MB maximum size. Avatar will be cropped to square.</span>
|
<span>1MB maximum size. Avatar will be cropped to square.</span>
|
||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset class="hfc">
|
||||||
|
<legend>Change password</legend>
|
||||||
|
<form method='post' action='{{ url_for('users.change_password', username=active_user.username) }}'>
|
||||||
|
<label for="new_password">New password</label><br>
|
||||||
|
<input type="password" id="new_password" name="new_password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
||||||
|
<label for="new_password2">Confirm new password</label><br>
|
||||||
|
<input type="password" id="new_password2" name="new_password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
||||||
|
<input class="warn" type="submit" value="Change password">
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
<fieldset class="hfc">
|
<fieldset class="hfc">
|
||||||
<legend>Personalization</legend>
|
<legend>Personalization</legend>
|
||||||
<form method='post'>
|
<form method='post'>
|
||||||
@@ -44,20 +54,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="hfc">
|
<fieldset class="hfc">
|
||||||
<legend>Change password</legend>
|
|
||||||
<form method='post' action='{{ url_for('users.change_password', username=active_user.username) }}'>
|
|
||||||
<label for="new_password">New password</label><br>
|
|
||||||
<input type="password" id="new_password" name="new_password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
|
||||||
<label for="new_password2">Confirm new password</label><br>
|
|
||||||
<input type="password" id="new_password2" name="new_password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
|
||||||
<input class="warn" type="submit" value="Change password">
|
|
||||||
</form>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Badges</legend>
|
<legend>Badges</legend>
|
||||||
<a href="{{ url_for('guides.guide_page', category='user-guides', slug='settings', _anchor='badges')}}">Badges help</a>
|
<a href="{{ url_for('guides.guide_page', category='user-guides', slug='settings', _anchor='badges')}}">Badges help</a>
|
||||||
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorForm" data-listeners="click input submit change">
|
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorForm" data-listeners="click input submit change">
|
||||||
<form data-use="badgeEditorPrepareSubmit" data-init='loadBadgeEditor' data-receive='addBadge' method='post' enctype='multipart/form-data' action='{{ url_for('users.save_badges', username=active_user.username) }}'>
|
<form data-use="badgeEditorPrepareSubmit" data-init='loadBadgeEditor' method='post' enctype='multipart/form-data' action='{{ url_for('users.save_badges', username=active_user.username) }}'>
|
||||||
<div>Loading badges…</div>
|
<div>Loading badges…</div>
|
||||||
<div>If badges fail to load, JS may be disabled.</div>
|
<div>If badges fail to load, JS may be disabled.</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -68,4 +68,12 @@
|
|||||||
<a class="linkbutton critical" href="{{ url_for('users.delete_page', username=active_user.username) }}">Delete account</a>
|
<a class="linkbutton critical" href="{{ url_for('users.delete_page', username=active_user.username) }}">Delete account</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template id='badge-editor-template'>
|
||||||
|
{{ badge_editor_single(options=uploads) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="badges-list-template">
|
||||||
|
{{ sortable_list(attr={'data-receive': 'addBadge'}) }}
|
||||||
|
</template>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
|
### REQUIRED CONFIGURATION
|
||||||
|
## the following settings are required.
|
||||||
|
## the app will not work if they are missing.
|
||||||
|
|
||||||
|
# the domain name you will be serving Pyrom from, without the scheme, including the subdomain(s).
|
||||||
|
# this is overridden by the app in development.
|
||||||
|
# used for generating URLs.
|
||||||
|
# the app will not start if this field is missing.
|
||||||
|
SERVER_NAME = "forum.your.domain"
|
||||||
|
|
||||||
|
### OPTIONAL CONFIGURATION
|
||||||
|
## the following settings are set to their default values.
|
||||||
|
## you can override any of them.
|
||||||
|
|
||||||
|
# your forum's name, shown on the header.
|
||||||
SITE_NAME = "Pyrom"
|
SITE_NAME = "Pyrom"
|
||||||
DISABLE_SIGNUP = false # if true, no one can sign up.
|
|
||||||
|
# if true, users can not sign up manually. see the following two settings.
|
||||||
|
DISABLE_SIGNUP = false
|
||||||
|
|
||||||
# if neither of the following two options is true,
|
# if neither of the following two options is true,
|
||||||
# no one can sign up. this may be useful later when/if LDAP is implemented.
|
# 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.
|
# if true, allows moderators to create invite links. useless unless DISABLE_SIGNUP is true.
|
||||||
USERS_CAN_INVITE = false # if true, allows users to create invite links. useless unless DISABLE_SIGNUP to be true.
|
MODS_CAN_INVITE = true
|
||||||
|
|
||||||
|
# if true, allows users to create invite links. useless unless DISABLE_SIGNUP is true.
|
||||||
|
USERS_CAN_INVITE = false
|
||||||
|
|
||||||
# contact information, will be shown in /guides/contact
|
# contact information, will be shown in /guides/contact
|
||||||
# some babycodes allowed
|
# some babycodes allowed
|
||||||
|
|||||||
BIN
data/static/badges/pride-bisexual.webp
Normal file
BIN
data/static/badges/pride-bisexual.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 366 B |
@@ -44,13 +44,13 @@
|
|||||||
|
|
||||||
.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 {
|
.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;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
font-family: "Cadman", sans-serif;
|
font-family: "Cadman", sans-serif;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
margin: 10px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -69,7 +69,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
@@ -114,7 +114,7 @@ body {
|
|||||||
|
|
||||||
.site-title {
|
.site-title {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
font-size: 3rem;
|
font-size: 3em;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -122,14 +122,15 @@ body {
|
|||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-actions {
|
.thread-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 0 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
@@ -222,7 +223,7 @@ code {
|
|||||||
pre code {
|
pre code {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
|
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
color: white;
|
color: white;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
@@ -606,7 +607,7 @@ pre code { /* Literal.Number.Integer.Long */ }
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +800,7 @@ input[type=file]::file-selector-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 15px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
@@ -859,13 +860,17 @@ input[type=text], input[type=password], textarea, select {
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: rgb(217.8, 225.6, 208.2);
|
background-color: rgb(217.8, 225.6, 208.2);
|
||||||
font-size: 100%;
|
font-size: 1em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
background-color: rgb(230.2, 235.4, 223.8);
|
background-color: rgb(230.2, 235.4, 223.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:not(form input):invalid {
|
||||||
|
border: 2px dashed red;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font-family: "Atkinson Hyperlegible Mono", monospace;
|
font-family: "Atkinson Hyperlegible Mono", monospace;
|
||||||
}
|
}
|
||||||
@@ -1068,35 +1073,6 @@ textarea {
|
|||||||
background-color: none;
|
background-color: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-topic {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #c1ceb1;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 15px 0;
|
|
||||||
border-top: 5px outset rgb(217.26, 220.38, 213.42);
|
|
||||||
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
|
||||||
}
|
|
||||||
.draggable-topic.dragged {
|
|
||||||
background-color: rgb(177, 206, 204.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable-collection {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #c1ceb1;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 15px 0;
|
|
||||||
border-top: 5px outset rgb(217.26, 220.38, 213.42);
|
|
||||||
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
|
||||||
}
|
|
||||||
.draggable-collection.dragged {
|
|
||||||
background-color: rgb(177, 206, 204.5);
|
|
||||||
}
|
|
||||||
.draggable-collection.default {
|
|
||||||
background-color: #beb1ce;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing {
|
.editing {
|
||||||
background-color: rgb(217.26, 220.38, 213.42);
|
background-color: rgb(217.26, 220.38, 213.42);
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1099,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.babycode-preview-errors-container {
|
.babycode-preview-errors-container {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@@ -1269,9 +1245,6 @@ ul.horizontal li, ol.horizontal li {
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
}
|
}
|
||||||
.babycode-button > * {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-popover {
|
.quote-popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1455,7 +1428,7 @@ a.mention:hover, a.mention:visited:hover {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
--grid-item-max-width: calc((100% - 10px) / 2);
|
--grid-item-max-width: calc((100% - 10px) / 2);
|
||||||
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
|
||||||
}
|
}
|
||||||
.settings-grid fieldset {
|
.settings-grid fieldset {
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
@@ -1523,3 +1496,83 @@ img.badge-button {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rss-button {
|
||||||
|
background-color: #fba668;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:hover {
|
||||||
|
background-color: rgb(251.8, 183.8, 134.2);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:active {
|
||||||
|
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
body {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.guide-container {
|
||||||
|
grid-template-areas: "guide-toc" "guide-topics";
|
||||||
|
grid-template-columns: unset;
|
||||||
|
}
|
||||||
|
.guide-toc {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
.guide-section {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ol.sortable-list {
|
||||||
|
list-style: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ol.sortable-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: #c1ceb1;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-top: 5px outset rgb(217.26, 220.38, 213.42);
|
||||||
|
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||||
|
}
|
||||||
|
ol.sortable-list li.dragged {
|
||||||
|
background-color: rgb(177, 206, 204.5);
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable {
|
||||||
|
background-color: #beb1ce;
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable .dragger {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-item-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sortable-item-inner > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.sortable-item-inner.row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.sortable-item-inner:not(.row) > * {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fg {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,13 +44,13 @@
|
|||||||
|
|
||||||
.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 {
|
.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;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
font-family: "Cadman", sans-serif;
|
font-family: "Cadman", sans-serif;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
margin: 10px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -69,7 +69,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
@@ -114,7 +114,7 @@ body {
|
|||||||
|
|
||||||
.site-title {
|
.site-title {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
font-size: 3rem;
|
font-size: 3em;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -122,14 +122,15 @@ body {
|
|||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-actions {
|
.thread-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 0 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
@@ -222,7 +223,7 @@ code {
|
|||||||
pre code {
|
pre code {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: #302731;
|
background-color: #302731;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
color: white;
|
color: white;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
@@ -606,7 +607,7 @@ pre code { /* Literal.Number.Integer.Long */ }
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +800,7 @@ input[type=file]::file-selector-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 15px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
@@ -859,13 +860,17 @@ input[type=text], input[type=password], textarea, select {
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
color: #e6e6e6;
|
color: #e6e6e6;
|
||||||
background-color: #371e37;
|
background-color: #371e37;
|
||||||
font-size: 100%;
|
font-size: 1em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
background-color: #514151;
|
background-color: #514151;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:not(form input):invalid {
|
||||||
|
border: 2px dashed #d53232;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font-family: "Atkinson Hyperlegible Mono", monospace;
|
font-family: "Atkinson Hyperlegible Mono", monospace;
|
||||||
}
|
}
|
||||||
@@ -1068,35 +1073,6 @@ textarea {
|
|||||||
background-color: #503250;
|
background-color: #503250;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-topic {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #9b649b;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 15px 0;
|
|
||||||
border-top: 5px outset #503250;
|
|
||||||
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
|
|
||||||
}
|
|
||||||
.draggable-topic.dragged {
|
|
||||||
background-color: #3c283c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable-collection {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #9b649b;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 15px 0;
|
|
||||||
border-top: 5px outset #503250;
|
|
||||||
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
|
|
||||||
}
|
|
||||||
.draggable-collection.dragged {
|
|
||||||
background-color: #3c283c;
|
|
||||||
}
|
|
||||||
.draggable-collection.default {
|
|
||||||
background-color: #8a5584;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing {
|
.editing {
|
||||||
background-color: #503250;
|
background-color: #503250;
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1099,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.babycode-preview-errors-container {
|
.babycode-preview-errors-container {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@@ -1269,9 +1245,6 @@ ul.horizontal li, ol.horizontal li {
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
}
|
}
|
||||||
.babycode-button > * {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-popover {
|
.quote-popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1455,12 +1428,12 @@ a.mention:hover, a.mention:visited:hover {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
--grid-item-max-width: calc((100% - 10px) / 2);
|
--grid-item-max-width: calc((100% - 10px) / 2);
|
||||||
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
|
||||||
}
|
}
|
||||||
.settings-grid fieldset {
|
.settings-grid fieldset {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: rgb(141.6, 79.65, 141.6);
|
background-color: #503250;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hfc {
|
.hfc {
|
||||||
@@ -1481,10 +1454,10 @@ h1 {
|
|||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
.settings-badge-container:has(input:invalid) {
|
.settings-badge-container:has(input:invalid) {
|
||||||
border: 2px dashed red;
|
border: 2px dashed #d53232;
|
||||||
}
|
}
|
||||||
.settings-badge-container input:invalid {
|
.settings-badge-container input:invalid {
|
||||||
border: 2px dashed red;
|
border: 2px dashed #d53232;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-badge-file-picker {
|
.settings-badge-file-picker {
|
||||||
@@ -1524,6 +1497,86 @@ img.badge-button {
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rss-button {
|
||||||
|
background-color: #fba668;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:hover {
|
||||||
|
background-color: rgb(251.8, 183.8, 134.2);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:active {
|
||||||
|
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
body {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.guide-container {
|
||||||
|
grid-template-areas: "guide-toc" "guide-topics";
|
||||||
|
grid-template-columns: unset;
|
||||||
|
}
|
||||||
|
.guide-toc {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
.guide-section {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ol.sortable-list {
|
||||||
|
list-style: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ol.sortable-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: #9b649b;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-top: 5px outset #503250;
|
||||||
|
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
|
||||||
|
}
|
||||||
|
ol.sortable-list li.dragged {
|
||||||
|
background-color: #3c283c;
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable {
|
||||||
|
background-color: #8a5584;
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable .dragger {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgb(96.95, 81.55, 96.95);
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-item-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sortable-item-inner > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.sortable-item-inner.row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.sortable-item-inner:not(.row) > * {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fg {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border: 10px solid rgb(40, 40, 40);
|
border: 10px solid rgb(40, 40, 40);
|
||||||
|
|||||||
@@ -44,13 +44,13 @@
|
|||||||
|
|
||||||
.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 {
|
.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;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
font-family: "Cadman", sans-serif;
|
font-family: "Cadman", sans-serif;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
margin: 6px 0;
|
margin: 3px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -69,7 +69,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
@@ -114,7 +114,7 @@ body {
|
|||||||
|
|
||||||
.site-title {
|
.site-title {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
font-size: 3rem;
|
font-size: 3em;
|
||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -122,14 +122,15 @@ body {
|
|||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-actions {
|
.thread-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 0 3px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
@@ -222,7 +223,7 @@ code {
|
|||||||
pre code {
|
pre code {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
|
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
color: white;
|
color: white;
|
||||||
border-bottom-right-radius: 16px;
|
border-bottom-right-radius: 16px;
|
||||||
border-bottom-left-radius: 16px;
|
border-bottom-left-radius: 16px;
|
||||||
@@ -606,7 +607,7 @@ pre code { /* Literal.Number.Integer.Long */ }
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +800,7 @@ input[type=file]::file-selector-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 8px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
@@ -859,13 +860,17 @@ input[type=text], input[type=password], textarea, select {
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: rgb(247.2, 175.2, 156);
|
background-color: rgb(247.2, 175.2, 156);
|
||||||
font-size: 100%;
|
font-size: 1em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
background-color: rgb(249.8, 201.8, 189);
|
background-color: rgb(249.8, 201.8, 189);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:not(form input):invalid {
|
||||||
|
border: 2px dashed #f73030;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font-family: "Atkinson Hyperlegible Mono", monospace;
|
font-family: "Atkinson Hyperlegible Mono", monospace;
|
||||||
}
|
}
|
||||||
@@ -1068,35 +1073,6 @@ textarea {
|
|||||||
background-color: #f27a5a;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable-collection {
|
|
||||||
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-collection.dragged {
|
|
||||||
background-color: #f27a5a;
|
|
||||||
}
|
|
||||||
.draggable-collection.default {
|
|
||||||
background-color: #b54444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing {
|
.editing {
|
||||||
background-color: rgb(219.84, 191.04, 183.36);
|
background-color: rgb(219.84, 191.04, 183.36);
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1099,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.babycode-preview-errors-container {
|
.babycode-preview-errors-container {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@@ -1269,9 +1245,6 @@ ul.horizontal li, ol.horizontal li {
|
|||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
}
|
}
|
||||||
.babycode-button > * {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-popover {
|
.quote-popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1455,7 +1428,7 @@ a.mention:hover, a.mention:visited:hover {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
--grid-item-max-width: calc((100% - 6px) / 2);
|
--grid-item-max-width: calc((100% - 6px) / 2);
|
||||||
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
|
||||||
}
|
}
|
||||||
.settings-grid fieldset {
|
.settings-grid fieldset {
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
@@ -1481,10 +1454,10 @@ h1 {
|
|||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
.settings-badge-container:has(input:invalid) {
|
.settings-badge-container:has(input:invalid) {
|
||||||
border: 2px dashed red;
|
border: 2px dashed #f73030;
|
||||||
}
|
}
|
||||||
.settings-badge-container input:invalid {
|
.settings-badge-container input:invalid {
|
||||||
border: 2px dashed red;
|
border: 2px dashed #f73030;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-badge-file-picker {
|
.settings-badge-file-picker {
|
||||||
@@ -1524,6 +1497,86 @@ img.badge-button {
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rss-button {
|
||||||
|
background-color: #fba668;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:hover {
|
||||||
|
background-color: rgb(251.8, 183.8, 134.2);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:active {
|
||||||
|
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
body {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.guide-container {
|
||||||
|
grid-template-areas: "guide-toc" "guide-topics";
|
||||||
|
grid-template-columns: unset;
|
||||||
|
}
|
||||||
|
.guide-toc {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
.guide-section {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ol.sortable-list {
|
||||||
|
list-style: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ol.sortable-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
ol.sortable-list li.dragged {
|
||||||
|
background-color: #f27a5a;
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable {
|
||||||
|
background-color: #b54444;
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable .dragger {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgb(155.8907865169, 93.2211235955, 76.5092134831);
|
||||||
|
padding: 3px 6px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-item-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sortable-item-inner > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.sortable-item-inner.row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.sortable-item-inner:not(.row) > * {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fg {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
border-top-left-radius: 16px;
|
border-top-left-radius: 16px;
|
||||||
border-top-right-radius: 16px;
|
border-top-right-radius: 16px;
|
||||||
|
|||||||
@@ -44,13 +44,13 @@
|
|||||||
|
|
||||||
.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 {
|
.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;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
font-family: "Cadman", sans-serif;
|
font-family: "Cadman", sans-serif;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
margin: 10px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -69,7 +69,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
@@ -114,7 +114,7 @@ body {
|
|||||||
|
|
||||||
.site-title {
|
.site-title {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
font-size: 3rem;
|
font-size: 3em;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -122,14 +122,15 @@ body {
|
|||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-actions {
|
.thread-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 0 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
@@ -222,7 +223,7 @@ code {
|
|||||||
pre code {
|
pre code {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: rgb(37.9418181818, 42.3818181818, 50.8581818182);
|
background-color: rgb(37.9418181818, 42.3818181818, 50.8581818182);
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
color: white;
|
color: white;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
@@ -606,7 +607,7 @@ pre code { /* Literal.Number.Integer.Long */ }
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +800,7 @@ input[type=file]::file-selector-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 15px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
@@ -859,13 +860,17 @@ input[type=text], input[type=password], textarea, select {
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: rgb(225.6, 232.2, 244.8);
|
background-color: rgb(225.6, 232.2, 244.8);
|
||||||
font-size: 100%;
|
font-size: 1em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
background-color: rgb(235.4, 239.8, 248.2);
|
background-color: rgb(235.4, 239.8, 248.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:not(form input):invalid {
|
||||||
|
border: 2px dashed red;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font-family: "Atkinson Hyperlegible Mono", monospace;
|
font-family: "Atkinson Hyperlegible Mono", monospace;
|
||||||
}
|
}
|
||||||
@@ -1068,35 +1073,6 @@ textarea {
|
|||||||
background-color: none;
|
background-color: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-topic {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #ced9ee;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 15px 0;
|
|
||||||
border-top: 5px outset rgb(231.36, 234, 239.04);
|
|
||||||
border-bottom: 5px outset rgb(136.0836363636, 149.3636363636, 174.7163636364);
|
|
||||||
}
|
|
||||||
.draggable-topic.dragged {
|
|
||||||
background-color: #eecee9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable-collection {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #ced9ee;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 15px 0;
|
|
||||||
border-top: 5px outset rgb(231.36, 234, 239.04);
|
|
||||||
border-bottom: 5px outset rgb(136.0836363636, 149.3636363636, 174.7163636364);
|
|
||||||
}
|
|
||||||
.draggable-collection.dragged {
|
|
||||||
background-color: #eecee9;
|
|
||||||
}
|
|
||||||
.draggable-collection.default {
|
|
||||||
background-color: #eee3ce;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing {
|
.editing {
|
||||||
background-color: rgb(231.36, 234, 239.04);
|
background-color: rgb(231.36, 234, 239.04);
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1099,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.babycode-preview-errors-container {
|
.babycode-preview-errors-container {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@@ -1269,9 +1245,6 @@ ul.horizontal li, ol.horizontal li {
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
}
|
}
|
||||||
.babycode-button > * {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-popover {
|
.quote-popover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1455,7 +1428,7 @@ a.mention:hover, a.mention:visited:hover {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
--grid-item-max-width: calc((100% - 10px) / 2);
|
--grid-item-max-width: calc((100% - 10px) / 2);
|
||||||
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
|
||||||
}
|
}
|
||||||
.settings-grid fieldset {
|
.settings-grid fieldset {
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
@@ -1523,3 +1496,83 @@ img.badge-button {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rss-button {
|
||||||
|
background-color: #fba668;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:hover {
|
||||||
|
background-color: rgb(251.8, 183.8, 134.2);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.rss-button:active {
|
||||||
|
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
body {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.guide-container {
|
||||||
|
grid-template-areas: "guide-toc" "guide-topics";
|
||||||
|
grid-template-columns: unset;
|
||||||
|
}
|
||||||
|
.guide-toc {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
.guide-section {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ol.sortable-list {
|
||||||
|
list-style: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ol.sortable-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: #ced9ee;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-top: 5px outset rgb(231.36, 234, 239.04);
|
||||||
|
border-bottom: 5px outset rgb(136.0836363636, 149.3636363636, 174.7163636364);
|
||||||
|
}
|
||||||
|
ol.sortable-list li.dragged {
|
||||||
|
background-color: #eecee9;
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable {
|
||||||
|
background-color: #eee3ce;
|
||||||
|
}
|
||||||
|
ol.sortable-list li.immovable .dragger {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgb(136.0836363636, 149.3636363636, 174.7163636364);
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-item-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sortable-item-inner > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.sortable-item-inner.row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.sortable-item-inner:not(.row) > * {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fg {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
if (inThread()) {
|
if (inThread()) {
|
||||||
const form = ta.closest('.post-edit-form');
|
const form = ta.closest('.post-edit-form');
|
||||||
console.log(ta.closest('.post-edit-form'));
|
|
||||||
if (form){
|
if (form){
|
||||||
form.addEventListener("submit", () => {
|
form.addEventListener("submit", () => {
|
||||||
localStorage.removeItem(window.location.pathname);
|
localStorage.removeItem(window.location.pathname);
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))}
|
|||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
async showBookmarkMenu(ev, el) {
|
async showBookmarkMenu(ev, el) {
|
||||||
if ((el.sender.dataset.bookmarkId === el.ds('bookmarkId')) && el.childElementCount === 0) {
|
if ((ev.sender.dataset.bookmarkId === el.prop('bookmarkId')) && el.childElementCount === 0) {
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
'id': el.sender.dataset.conceptId,
|
'id': ev.sender.dataset.conceptId,
|
||||||
'require_reload': el.dataset.requireReload,
|
'require_reload': el.dataset.requireReload,
|
||||||
});
|
});
|
||||||
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${el.sender.dataset.bookmarkType}?${searchParams}`;
|
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${ev.sender.dataset.bookmarkType}?${searchParams}`;
|
||||||
const res = await this.api.getHTML(bookmarkMenuHref);
|
const res = await this.api.getHTML(bookmarkMenuHref);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
return;
|
return;
|
||||||
@@ -50,9 +50,9 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectBookmarkCollection(ev, el) {
|
selectBookmarkCollection(ev, el) {
|
||||||
const clicked = el.sender;
|
const clicked = ev.sender;
|
||||||
|
|
||||||
if (el.sender === el) {
|
if (ev.sender === el) {
|
||||||
if (clicked.classList.contains('selected')) {
|
if (clicked.classList.contains('selected')) {
|
||||||
clicked.classList.remove('selected');
|
clicked.classList.remove('selected');
|
||||||
} else {
|
} else {
|
||||||
@@ -64,7 +64,7 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveBookmarks(ev, el) {
|
async saveBookmarks(ev, el) {
|
||||||
const bookmarkHref = el.ds('bookmarkEndpoint');
|
const bookmarkHref = el.prop('bookmarkEndpoint');
|
||||||
const collection = el.querySelector('.bookmark-dropdown-item.selected');
|
const collection = el.querySelector('.bookmark-dropdown-item.selected');
|
||||||
let data = {};
|
let data = {};
|
||||||
if (collection) {
|
if (collection) {
|
||||||
@@ -73,7 +73,7 @@ export default class {
|
|||||||
data['memo'] = el.querySelector('.bookmark-memo-input').value;
|
data['memo'] = el.querySelector('.bookmark-memo-input').value;
|
||||||
} else {
|
} else {
|
||||||
data['operation'] = 'remove';
|
data['operation'] = 'remove';
|
||||||
data['collection_id'] = el.ds('originallyContainedIn');
|
data['collection_id'] = el.prop('originallyContainedIn');
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -83,7 +83,7 @@ export default class {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const requireReload = el.dsInt('requireReload') !== 0;
|
const requireReload = el.propToInt('requireReload') !== 0;
|
||||||
el.remove();
|
el.remove();
|
||||||
await fetch(bookmarkHref, options);
|
await fetch(bookmarkHref, options);
|
||||||
if (requireReload) {
|
if (requireReload) {
|
||||||
@@ -104,10 +104,10 @@ export default class {
|
|||||||
toggleAccordion(ev, el) {
|
toggleAccordion(ev, el) {
|
||||||
const accordion = el;
|
const accordion = el;
|
||||||
const header = accordion.querySelector('.accordion-header');
|
const header = accordion.querySelector('.accordion-header');
|
||||||
if (!header.contains(el.sender)){
|
if (!header.contains(ev.sender)){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const btn = el.sender;
|
const btn = ev.sender;
|
||||||
const content = el.querySelector('.accordion-content');
|
const content = el.querySelector('.accordion-content');
|
||||||
// these are all meant to be in sync
|
// these are all meant to be in sync
|
||||||
accordion.classList.toggle('hidden');
|
accordion.classList.toggle('hidden');
|
||||||
@@ -117,15 +117,15 @@ export default class {
|
|||||||
|
|
||||||
toggleTab(ev, el) {
|
toggleTab(ev, el) {
|
||||||
const tabButtonsContainer = el.querySelector('.tab-buttons');
|
const tabButtonsContainer = el.querySelector('.tab-buttons');
|
||||||
if (!el.contains(el.sender)) {
|
if (!el.contains(ev.sender)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.sender.classList.contains('active')) {
|
if (ev.sender.classList.contains('active')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetId = el.senderDs('targetId');
|
const targetId = ev.sender.prop('targetId');
|
||||||
const contents = el.querySelectorAll('.tab-content');
|
const contents = el.querySelectorAll('.tab-content');
|
||||||
for (let content of contents) {
|
for (let content of contents) {
|
||||||
if (content.id === targetId) {
|
if (content.id === targetId) {
|
||||||
@@ -145,7 +145,7 @@ export default class {
|
|||||||
|
|
||||||
#previousMarkup = null;
|
#previousMarkup = null;
|
||||||
async babycodePreview(ev, el) {
|
async babycodePreview(ev, el) {
|
||||||
if (el.sender.classList.contains('active')) {
|
if (ev.sender.classList.contains('active')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +200,9 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
insertBabycodeTag(ev, el) {
|
insertBabycodeTag(ev, el) {
|
||||||
const tagStart = el.senderDs('tag');
|
const tagStart = ev.sender.prop('tag');
|
||||||
const breakLine = 'breakLine' in el.sender.dataset;
|
const breakLine = 'breakLine' in ev.sender.dataset;
|
||||||
const prefill = 'prefill' in el.sender.dataset ? el.sender.dataset.prefill : '';
|
const prefill = 'prefill' in ev.sender.dataset ? ev.sender.dataset.prefill : '';
|
||||||
|
|
||||||
const hasAttr = tagStart[tagStart.length - 1] === '=';
|
const hasAttr = tagStart[tagStart.length - 1] === '=';
|
||||||
let tagEnd = tagStart;
|
let tagEnd = tagStart;
|
||||||
@@ -250,13 +250,13 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addQuote(ev, el) {
|
addQuote(ev, el) {
|
||||||
el.value += el.sender.value;
|
el.value += ev.sender.value;
|
||||||
el.scrollIntoView();
|
el.scrollIntoView();
|
||||||
el.focus();
|
el.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
convertTimestamps(ev, el) {
|
convertTimestamps(ev, el) {
|
||||||
const timestamp = el.dsInt('utc');
|
const timestamp = el.propToInt('utc');
|
||||||
if (!isNaN(timestamp)) {
|
if (!isNaN(timestamp)) {
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(timestamp * 1000);
|
||||||
el.textContent = date.toLocaleString();
|
el.textContent = date.toLocaleString();
|
||||||
@@ -273,7 +273,7 @@ export default class {
|
|||||||
this.#currentUsername = userInfo.value.user.username;
|
this.#currentUsername = userInfo.value.user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.ds('username') === this.#currentUsername) {
|
if (el.prop('username') === this.#currentUsername) {
|
||||||
el.classList.add('me');
|
el.classList.add('me');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,11 +287,7 @@ export class BadgeEditorForm {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.#badgeTemplate === undefined){
|
if (this.#badgeTemplate === undefined){
|
||||||
const badge = await this.api.getHTML(`${badgeEditorEndpoint}/template`)
|
this.#badgeTemplate = document.getElementById('badge-editor-template').content.firstElementChild.outerHTML;
|
||||||
if (!badge.value){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#badgeTemplate= badge.value;
|
|
||||||
}
|
}
|
||||||
el.replaceChildren();
|
el.replaceChildren();
|
||||||
const addButton = `<button data-disable-if-max="1" data-receive="updateBadgeCount" DISABLE_IF_MAX type="button" data-send="addBadge">Add badge</button>`;
|
const addButton = `<button data-disable-if-max="1" data-receive="updateBadgeCount" DISABLE_IF_MAX type="button" data-send="addBadge">Add badge</button>`;
|
||||||
@@ -303,31 +299,35 @@ export class BadgeEditorForm {
|
|||||||
['DISABLE_IF_MAX', badgeCount === 10 ? 'disabled' : ''],
|
['DISABLE_IF_MAX', badgeCount === 10 ? 'disabled' : ''],
|
||||||
];
|
];
|
||||||
el.appendChild(this.api.makeHTML(controls, subs));
|
el.appendChild(this.api.makeHTML(controls, subs));
|
||||||
el.appendChild(badges.value);
|
|
||||||
|
const listTemplate = document.getElementById('badges-list-template').content.firstElementChild.outerHTML;
|
||||||
|
const list = this.api.makeHTML(listTemplate).firstElementChild;
|
||||||
|
list.appendChild(badges.value);
|
||||||
|
el.appendChild(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
addBadge(ev, el) {
|
addBadge(ev, el) {
|
||||||
if (this.#badgeTemplate === undefined) {
|
if (this.#badgeTemplate === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const badge = this.#badgeTemplate.cloneNode(true);
|
const badge = this.api.makeHTML(this.#badgeTemplate).firstElementChild;
|
||||||
el.appendChild(badge);
|
el.appendChild(badge);
|
||||||
this.api.localTrigger('updateBadgeCount');
|
this.api.localTrigger('updateBadgeCount');
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBadge(ev, el) {
|
deleteBadge(ev, el) {
|
||||||
if (!el.contains(el.sender)) {
|
if (!el.contains(ev.sender)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.remove();
|
el.remove();
|
||||||
this.api.localTrigger('updateBadgeCount');
|
this.api.localTrigger('updateBadgeCount');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBadgeCount(_ev, el) {
|
updateBadgeCount(ev, el) {
|
||||||
const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length;
|
const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length;
|
||||||
if (el.dsInt('disableIfMax') === 1) {
|
if (el.propToInt('disableIfMax') === 1) {
|
||||||
el.disabled = badgeCount === 10;
|
el.disabled = badgeCount === 10;
|
||||||
} else if (el.dsInt('count') === 1) {
|
} else if (el.propToInt('count') === 1) {
|
||||||
el.textContent = `${badgeCount}/10`;
|
el.textContent = `${badgeCount}/10`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,9 +344,7 @@ export class BadgeEditorForm {
|
|||||||
noUploads.forEach(e => {
|
noUploads.forEach(e => {
|
||||||
e.value = null;
|
e.value = null;
|
||||||
})
|
})
|
||||||
// console.log(noUploads);
|
|
||||||
el.submit();
|
el.submit();
|
||||||
// console.log('would submit now');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,13 +362,13 @@ export class BadgeEditorBadge {
|
|||||||
if (ev.type !== 'change') {
|
if (ev.type !== 'change') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: el.sender doesn't have a bittyParentBittyId
|
// TODO: ev.sender doesn't have a bittyParent
|
||||||
const selectBittyParent = el.sender.closest('bitty-7-0');
|
const selectBittyParent = ev.sender.closest('bitty-7-0');
|
||||||
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
|
if (el.bittyParent !== selectBittyParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.val === 'custom') {
|
if (ev.value === 'custom') {
|
||||||
if (this.#badgeCustomImageData) {
|
if (this.#badgeCustomImageData) {
|
||||||
el.src = this.#badgeCustomImageData;
|
el.src = this.#badgeCustomImageData;
|
||||||
} else {
|
} else {
|
||||||
@@ -378,7 +376,7 @@ export class BadgeEditorBadge {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const option = el.sender.selectedOptions[0];
|
const option = ev.sender.selectedOptions[0];
|
||||||
el.src = option.dataset.filePath;
|
el.src = option.dataset.filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,13 +384,13 @@ export class BadgeEditorBadge {
|
|||||||
if (ev.type !== 'change') {
|
if (ev.type !== 'change') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (el.bittyParentBittyId !== el.sender.bittyParentBittyId) {
|
if (el.bittyParent !== ev.sender.bittyParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = ev.target.files[0];
|
const file = ev.target.files[0];
|
||||||
if (file.size >= 1000 * 500) {
|
if (file.size >= 1000 * 500) {
|
||||||
this.api.trigger('badgeErrorSize');
|
this.api.localTrigger('badgeErrorSize');
|
||||||
this.#badgeCustomImageData = null;
|
this.#badgeCustomImageData = null;
|
||||||
el.removeAttribute('src');
|
el.removeAttribute('src');
|
||||||
return;
|
return;
|
||||||
@@ -403,14 +401,14 @@ export class BadgeEditorBadge {
|
|||||||
reader.onload = async e => {
|
reader.onload = async e => {
|
||||||
const dimsValid = await validateBase64Img(e.target.result);
|
const dimsValid = await validateBase64Img(e.target.result);
|
||||||
if (!dimsValid) {
|
if (!dimsValid) {
|
||||||
this.api.trigger('badgeErrorDim');
|
this.api.localTrigger('badgeErrorDim');
|
||||||
this.#badgeCustomImageData = null;
|
this.#badgeCustomImageData = null;
|
||||||
el.removeAttribute('src');
|
el.removeAttribute('src');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#badgeCustomImageData = e.target.result;
|
this.#badgeCustomImageData = e.target.result;
|
||||||
el.src = this.#badgeCustomImageData;
|
el.src = this.#badgeCustomImageData;
|
||||||
this.api.trigger('badgeHideErrors');
|
this.api.localTrigger('badgeHideErrors');
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@@ -420,13 +418,13 @@ export class BadgeEditorBadge {
|
|||||||
if (ev.type !== 'change') {
|
if (ev.type !== 'change') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: el.sender doesn't have a bittyParentBittyId
|
// TODO: ev.sender doesn't have a bittyParent
|
||||||
const selectBittyParent = el.sender.closest('bitty-7-0');
|
const selectBittyParent = ev.sender.closest('bitty-7-0');
|
||||||
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
|
if (el.bittyParent !== selectBittyParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filePicker = el.querySelector('input[type=file]');
|
const filePicker = el.querySelector('input[type=file]');
|
||||||
if (ev.val === 'custom') {
|
if (ev.value === 'custom') {
|
||||||
el.classList.remove('hidden');
|
el.classList.remove('hidden');
|
||||||
if (filePicker.dataset.validity) {
|
if (filePicker.dataset.validity) {
|
||||||
filePicker.setCustomValidity(filePicker.dataset.validity);
|
filePicker.setCustomValidity(filePicker.dataset.validity);
|
||||||
@@ -440,38 +438,106 @@ export class BadgeEditorBadge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openBadgeFilePicker(ev, el) {
|
openBadgeFilePicker(ev, el) {
|
||||||
// TODO: el.sender doesn't have a bittyParentBittyId
|
// TODO: ev.sender doesn't have a bittyParent
|
||||||
if (el.sender.parentNode !== el.parentNode) {
|
if (ev.sender.parentNode !== el.parentNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.click();
|
el.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
badgeErrorSize(_ev, el) {
|
badgeErrorSize(ev, el) {
|
||||||
if (el.sender !== el.bittyParent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const validity = "Image can't be over 500KB."
|
const validity = "Image can't be over 500KB."
|
||||||
el.dataset.validity = validity;
|
el.dataset.validity = validity;
|
||||||
el.setCustomValidity(validity);
|
el.setCustomValidity(validity);
|
||||||
el.reportValidity();
|
el.reportValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
badgeErrorDim(_ev, el) {
|
badgeErrorDim(ev, el) {
|
||||||
if (el.sender !== el.bittyParent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const validity = "Image must be exactly 88x31 pixels."
|
const validity = "Image must be exactly 88x31 pixels."
|
||||||
el.dataset.validity = validity;
|
el.dataset.validity = validity;
|
||||||
el.setCustomValidity(validity);
|
el.setCustomValidity(validity);
|
||||||
el.reportValidity();
|
el.reportValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
badgeHideErrors(_ev, el) {
|
badgeHideErrors(ev, el) {
|
||||||
if (el.sender !== el.bittyParent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete el.dataset.validity;
|
delete el.dataset.validity;
|
||||||
el.setCustomValidity('');
|
el.setCustomValidity('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCollectionDataForEl = el => {
|
||||||
|
const nameInput = el.querySelector(".collection-name");
|
||||||
|
const collectionId = el.dataset.collectionId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: collectionId,
|
||||||
|
name: nameInput.value,
|
||||||
|
is_new: !('collectionId' in el.dataset),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CollectionsEditor {
|
||||||
|
#collectionTemplate = undefined;
|
||||||
|
#collectionsData = [];
|
||||||
|
#removedCollections = [];
|
||||||
|
#valid = true;
|
||||||
|
|
||||||
|
addCollection(ev, el) {
|
||||||
|
if (this.#collectionTemplate === undefined) {
|
||||||
|
this.#collectionTemplate = document.getElementById('new-collection-template').content;
|
||||||
|
}
|
||||||
|
// interesting
|
||||||
|
const newCollection = this.api.makeHTML(this.#collectionTemplate.firstElementChild.outerHTML);
|
||||||
|
el.appendChild(newCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCollection(ev, el) {
|
||||||
|
if (!el.contains(ev.sender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ('collectionId' in el.dataset) {
|
||||||
|
this.#removedCollections.push(el.dataset.collectionId);
|
||||||
|
}
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveCollections(ev, el) {
|
||||||
|
this.#valid = true;
|
||||||
|
this.api.localTrigger('testValidity');
|
||||||
|
if (!this.#valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#collectionsData = [];
|
||||||
|
this.api.localTrigger('getCollectionData');
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
collections: this.#collectionsData,
|
||||||
|
removed_collections: this.#removedCollections,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await this.api.getJSON(el.prop('submitHref'), [], {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCollectionData(ev, el) {
|
||||||
|
this.#collectionsData.push(getCollectionDataForEl(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
testValidity(ev, el) {
|
||||||
|
const input = el.querySelector('input');
|
||||||
|
if (!input.validity.valid) {
|
||||||
|
input.reportValidity();
|
||||||
|
this.#valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
let removedCollections = [];
|
|
||||||
|
|
||||||
document.getElementById("add-collection-button").addEventListener("click", () => {
|
|
||||||
const container = document.getElementById("collections-container");
|
|
||||||
const currentCount = container.querySelectorAll(".draggable-collection").length;
|
|
||||||
|
|
||||||
const newId = `new-${Date.now()}`
|
|
||||||
const collectionHtml = `
|
|
||||||
<div class="draggable-collection"
|
|
||||||
data-collection-id="${newId}"
|
|
||||||
draggable="true"
|
|
||||||
ondragover="dragOver(event)"
|
|
||||||
ondragstart="dragStart(event)"
|
|
||||||
ondragend="dragEnd()">
|
|
||||||
<input type="text" class="collection-name" value="" required placeholder="Enter collection name" autocomplete="off" maxlength="60"><br>
|
|
||||||
<div>0 threads, 0 posts</div>
|
|
||||||
<button type="button" class="delete-button critical">Delete</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.insertAdjacentHTML('beforeend', collectionHtml);
|
|
||||||
})
|
|
||||||
|
|
||||||
document.addEventListener("click", e => {
|
|
||||||
if (!e.target.classList.contains("delete-button")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const collectionDiv = e.target.closest(".draggable-collection");
|
|
||||||
const collectionId = collectionDiv.dataset.collectionId;
|
|
||||||
|
|
||||||
if (!collectionId.startsWith("new-")) {
|
|
||||||
removedCollections.push(collectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionDiv.remove();
|
|
||||||
})
|
|
||||||
|
|
||||||
document.getElementById("save-button").addEventListener("click", async () => {
|
|
||||||
const collections = [];
|
|
||||||
const collectionDivs = document.querySelectorAll(".draggable-collection");
|
|
||||||
let isValid = true;
|
|
||||||
collectionDivs.forEach((collection, index) => {
|
|
||||||
const collectionId = collection.dataset.collectionId;
|
|
||||||
const nameInput = collection.querySelector(".collection-name");
|
|
||||||
|
|
||||||
if (!nameInput.reportValidity()) {
|
|
||||||
isValid = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
collections.push({
|
|
||||||
id: collectionId,
|
|
||||||
name: nameInput.value,
|
|
||||||
is_new: collectionId.startsWith("new-"),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
collections: collections,
|
|
||||||
removed_collections: removedCollections,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const saveHref = document.getElementById('save-button').dataset.submitHref;
|
|
||||||
const response = await fetch(saveHref, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
console.error("Error saving collections");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving collections: ", error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// drag logic
|
|
||||||
// https://codepen.io/crouchingtigerhiddenadam/pen/qKXgap
|
|
||||||
|
|
||||||
let selected = null;
|
|
||||||
const container = document.getElementById("collections-container");
|
|
||||||
function isBefore(el1, el2) {
|
|
||||||
let cur;
|
|
||||||
if (el2.parentNode === el1.parentNode) {
|
|
||||||
for (cur = el1.previousSibling; cur; cur = cur.previousSibling) {
|
|
||||||
if (cur === el2) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragOver(e) {
|
|
||||||
let target = e.target.closest(".draggable-collection")
|
|
||||||
|
|
||||||
if (!target || target === selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBefore(selected, target)) {
|
|
||||||
container.insertBefore(selected, target)
|
|
||||||
} else {
|
|
||||||
container.insertBefore(selected, target.nextSibling)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragEnd() {
|
|
||||||
if (!selected) return;
|
|
||||||
|
|
||||||
selected.classList.remove("dragged")
|
|
||||||
selected = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragStart(e) {
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
|
||||||
e.dataTransfer.setData('text/plain', "")
|
|
||||||
selected = e.target
|
|
||||||
selected.classList.add("dragged")
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// https://codepen.io/crouchingtigerhiddenadam/pen/qKXgap
|
|
||||||
let selected = null;
|
|
||||||
let container = document.getElementById("topics-container")
|
|
||||||
|
|
||||||
function isBefore(el1, el2) {
|
|
||||||
let cur
|
|
||||||
if (el2.parentNode === el1.parentNode) {
|
|
||||||
for (cur = el1.previousSibling; cur; cur = cur.previousSibling) {
|
|
||||||
if (cur === el2) return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragOver(e) {
|
|
||||||
let target = e.target.closest(".draggable-topic")
|
|
||||||
|
|
||||||
if (!target || target === selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBefore(selected, target)) {
|
|
||||||
container.insertBefore(selected, target)
|
|
||||||
} else {
|
|
||||||
container.insertBefore(selected, target.nextSibling)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragEnd() {
|
|
||||||
if (!selected) return;
|
|
||||||
|
|
||||||
selected.classList.remove("dragged")
|
|
||||||
selected = null;
|
|
||||||
for (let i = 0; i < container.childElementCount - 1; i++) {
|
|
||||||
let input = container.children[i].querySelector(".topic-input");
|
|
||||||
input.value = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragStart(e) {
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
|
||||||
e.dataTransfer.setData('text/plain', null)
|
|
||||||
selected = e.target
|
|
||||||
selected.classList.add("dragged")
|
|
||||||
}
|
|
||||||
@@ -90,21 +90,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
minWidth: origMinWidth,
|
minWidth: origMinWidth,
|
||||||
minHeight: origMinHeight,
|
minHeight: origMinHeight,
|
||||||
} = getComputedStyle(img);
|
} = getComputedStyle(img);
|
||||||
console.log(img, img.naturalWidth, img.naturalHeight, origMinWidth, origMinHeight, origMaxWidth, origMaxHeight)
|
|
||||||
if (img.naturalWidth < parseInt(origMinWidth)) {
|
if (img.naturalWidth < parseInt(origMinWidth)) {
|
||||||
console.log(1)
|
|
||||||
img.style.minWidth = img.naturalWidth + "px";
|
img.style.minWidth = img.naturalWidth + "px";
|
||||||
}
|
}
|
||||||
if (img.naturalHeight < parseInt(origMinHeight)) {
|
if (img.naturalHeight < parseInt(origMinHeight)) {
|
||||||
console.log(2)
|
|
||||||
img.style.minHeight = img.naturalHeight + "px";
|
img.style.minHeight = img.naturalHeight + "px";
|
||||||
}
|
}
|
||||||
if (img.naturalWidth < parseInt(origMaxWidth)) {
|
if (img.naturalWidth < parseInt(origMaxWidth)) {
|
||||||
console.log(3)
|
|
||||||
img.style.maxWidth = img.naturalWidth + "px";
|
img.style.maxWidth = img.naturalWidth + "px";
|
||||||
}
|
}
|
||||||
if (img.naturalHeight < parseInt(origMaxHeight)) {
|
if (img.naturalHeight < parseInt(origMaxHeight)) {
|
||||||
console.log(4)
|
|
||||||
img.style.maxHeight = img.naturalHeight + "px";
|
img.style.maxHeight = img.naturalHeight + "px";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,3 +128,108 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
function isBefore(el1, el2) {
|
||||||
|
if (el2.parentNode === el1.parentNode) {
|
||||||
|
for (let cur = el1.previousSibling; cur; cur = cur.previousSibling) {
|
||||||
|
if (cur === el2) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let draggedItem = null;
|
||||||
|
|
||||||
|
function sortableItemDragStart(e, item) {
|
||||||
|
const box = item.getBoundingClientRect();
|
||||||
|
const oX = e.clientX - box.left;
|
||||||
|
const oY = e.clientY - box.top;
|
||||||
|
draggedItem = item;
|
||||||
|
item.classList.add('dragged');
|
||||||
|
e.dataTransfer.setDragImage(item, oX, oY);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortableItemDragEnd(e, item) {
|
||||||
|
draggedItem = null;
|
||||||
|
item.classList.remove('dragged');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortableItemDragOver(e, item) {
|
||||||
|
const target = e.target.closest('.sortable-item');
|
||||||
|
if (!target || target === draggedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inSameList = draggedItem.dataset.sortableListKey === target.dataset.sortableListKey;
|
||||||
|
if (!inSameList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetList = draggedItem.closest('.sortable-list');
|
||||||
|
if (isBefore(draggedItem, target)) {
|
||||||
|
targetList.insertBefore(draggedItem, target);
|
||||||
|
} else {
|
||||||
|
targetList.insertBefore(draggedItem, target.nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItemsHandled = new Map();
|
||||||
|
|
||||||
|
const getListItemsHandled = (list) => {
|
||||||
|
return listItemsHandled.get(list) || new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerSortableList(list) {
|
||||||
|
list.querySelectorAll('li:not(.immovable)').forEach(item => {
|
||||||
|
const listItems = getListItemsHandled(list);
|
||||||
|
listItems.add(item);
|
||||||
|
listItemsHandled.set(list, listItems);
|
||||||
|
const dragger = item.querySelector('.dragger');
|
||||||
|
dragger.addEventListener('dragstart', e => {sortableItemDragStart(e, item)});
|
||||||
|
dragger.addEventListener('dragend', e => {sortableItemDragEnd(e, item)});
|
||||||
|
item.addEventListener('dragover', e => {sortableItemDragOver(e, item)});
|
||||||
|
});
|
||||||
|
|
||||||
|
const obs = new MutationObserver(records => {
|
||||||
|
for (const mutation of records) {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (!(node instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!node.classList.contains('sortable-item')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const listItems = getListItemsHandled(list)
|
||||||
|
if (listItems.has(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dragger = node.querySelector('.dragger');
|
||||||
|
dragger.addEventListener('dragstart', e => {sortableItemDragStart(e, node)});
|
||||||
|
dragger.addEventListener('dragend', e => {sortableItemDragEnd(e, node)});
|
||||||
|
node.addEventListener('dragover', e => {sortableItemDragOver(e, node)});
|
||||||
|
listItems.add(node);
|
||||||
|
listItemsHandled.set(list, listItems);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
obs.observe(list, {childList: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.sortable-list').forEach(registerSortableList);
|
||||||
|
|
||||||
|
listsObs = new MutationObserver(records => {
|
||||||
|
for (const mutation of records) {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (!(node instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!node.classList.contains('sortable-list')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registerSortableList(node);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
listsObs.observe(document.body, {childList: true, subtree: true});
|
||||||
|
}
|
||||||
|
|||||||
1
data/static/js/vnd/bitty-7.0.0-rc1.min.js
vendored
1
data/static/js/vnd/bitty-7.0.0-rc1.min.js
vendored
File diff suppressed because one or more lines are too long
1
data/static/js/vnd/bitty-7.0.0.js
Normal file
1
data/static/js/vnd/bitty-7.0.0.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,9 +1,11 @@
|
|||||||
argon2-cffi==25.1.0
|
argon2-cffi==25.1.0
|
||||||
argon2-cffi-bindings==21.2.0
|
argon2-cffi-bindings==21.2.0
|
||||||
blinker==1.9.0
|
blinker==1.9.0
|
||||||
|
cachelib==0.13.0
|
||||||
cffi==1.17.1
|
cffi==1.17.1
|
||||||
click==8.2.1
|
click==8.2.1
|
||||||
Flask==3.1.1
|
Flask==3.1.1
|
||||||
|
Flask-Caching==2.3.1
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
Jinja2==3.1.6
|
Jinja2==3.1.6
|
||||||
MarkupSafe==3.0.2
|
MarkupSafe==3.0.2
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ $PAGE_SIDE_MARGIN: 100px !default;
|
|||||||
// BORDERS
|
// BORDERS
|
||||||
// **************
|
// **************
|
||||||
$DEFAULT_BORDER: 1px solid black !default;
|
$DEFAULT_BORDER: 1px solid black !default;
|
||||||
|
$DEFAULT_BORDER_INVALID: 2px dashed $BUTTON_COLOR_CRITICAL !default;
|
||||||
$DEFAULT_BORDER_RADIUS: 4px !default;
|
$DEFAULT_BORDER_RADIUS: 4px !default;
|
||||||
|
|
||||||
// other variables can be found before the rule that uses them. they are usually constructed from these basic variables.
|
// other variables can be found before the rule that uses them. they are usually constructed from these basic variables.
|
||||||
@@ -115,10 +116,10 @@ $DEFAULT_BORDER_RADIUS: 4px !default;
|
|||||||
$button_border: $DEFAULT_BORDER !default;
|
$button_border: $DEFAULT_BORDER !default;
|
||||||
$button_padding: $SMALL_PADDING $BIG_PADDING !default;
|
$button_padding: $SMALL_PADDING $BIG_PADDING !default;
|
||||||
$button_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
$button_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||||
$button_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
|
$button_margin: $SMALL_PADDING $ZERO_PADDING !default;
|
||||||
%button-base {
|
%button-base {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
font-family: "Cadman", sans-serif;
|
font-family: "Cadman", sans-serif;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: $button_border;
|
border: $button_border;
|
||||||
@@ -188,7 +189,7 @@ $link_color_visited: #730c0c !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
$topnav_color: $ACCENT_COLOR !default;
|
$topnav_color: $ACCENT_COLOR !default;
|
||||||
@@ -224,7 +225,7 @@ $user_actions_gap: $MEDIUM_BIG_PADDING !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$site_title_margin: $ZERO_PADDING $BIG_PADDING !default;
|
$site_title_margin: $ZERO_PADDING $BIG_PADDING !default;
|
||||||
$site_title_size: 3rem !default;
|
$site_title_size: 3em !default;
|
||||||
$site_title_color: $DEFAULT_FONT_COLOR !default;
|
$site_title_color: $DEFAULT_FONT_COLOR !default;
|
||||||
.site-title {
|
.site-title {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
@@ -235,18 +236,19 @@ $site_title_color: $DEFAULT_FONT_COLOR !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$thread_title_margin: $ZERO_PADDING !default;
|
$thread_title_margin: $ZERO_PADDING !default;
|
||||||
$thread_title_size: 1.5rem !default;
|
$thread_title_size: 1.5em !default;
|
||||||
.thread-title {
|
.thread-title {
|
||||||
margin: $thread_title_margin;
|
margin: $thread_title_margin;
|
||||||
font-size: $thread_title_size;
|
font-size: $thread_title_size;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
$thread_actions_gap: $SMALL_PADDING !default;
|
$thread_actions_gap: $ZERO_PADDING $SMALL_PADDING !default;
|
||||||
.thread-actions {
|
.thread-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $thread_actions_gap;
|
gap: $thread_actions_gap;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
$post_usercard_width: 230px !default;
|
$post_usercard_width: 230px !default;
|
||||||
@@ -372,7 +374,7 @@ $code_border_left: $MEDIUM_PADDING solid $LIGHT_2 !default;
|
|||||||
pre code {
|
pre code {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: $code_background_color;
|
background-color: $code_background_color;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
color: $code_font_color;
|
color: $code_font_color;
|
||||||
border-bottom-right-radius: $code_border_radius;
|
border-bottom-right-radius: $code_border_radius;
|
||||||
border-bottom-left-radius: $code_border_radius;
|
border-bottom-left-radius: $code_border_radius;
|
||||||
@@ -496,7 +498,7 @@ $inline_code_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: $inline_code_margin;
|
margin: $inline_code_margin;
|
||||||
border-radius: $inline_code_border_radius;
|
border-radius: $inline_code_border_radius;
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +649,7 @@ input[type="file"]::file-selector-button {
|
|||||||
margin: $MEDIUM_PADDING;
|
margin: $MEDIUM_PADDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
$para_margin: $MEDIUM_BIG_PADDING $ZERO_PADDING !default;
|
$para_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
|
||||||
p {
|
p {
|
||||||
margin: $para_margin;
|
margin: $para_margin;
|
||||||
}
|
}
|
||||||
@@ -700,7 +702,7 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
color: $text_input_font_color;
|
color: $text_input_font_color;
|
||||||
background-color: $text_input_background;
|
background-color: $text_input_background;
|
||||||
font-size: 100%;
|
font-size: 1em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@@ -708,6 +710,11 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lone required inputs managed by js
|
||||||
|
input:not(form input):invalid {
|
||||||
|
border: $DEFAULT_BORDER_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font-family: "Atkinson Hyperlegible Mono", monospace;
|
font-family: "Atkinson Hyperlegible Mono", monospace;
|
||||||
}
|
}
|
||||||
@@ -965,54 +972,6 @@ $topic_locked_background: none !default;
|
|||||||
background-color: $topic_locked_background;
|
background-color: $topic_locked_background;
|
||||||
}
|
}
|
||||||
|
|
||||||
$draggable_topic_background: $ACCENT_COLOR !default;
|
|
||||||
$draggable_topic_dragged_color: $BUTTON_COLOR !default;
|
|
||||||
$draggable_topic_padding: $BIG_PADDING !default;
|
|
||||||
$draggable_topic_margin: $MEDIUM_BIG_PADDING 0 !default;
|
|
||||||
$draggable_topic_border: 5px outset !default;
|
|
||||||
$draggable_topic_border_top: $draggable_topic_border $LIGHT !default;
|
|
||||||
$draggable_topic_border_bottom: $draggable_topic_border $DARK_2 !default;
|
|
||||||
.draggable-topic {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: $draggable_topic_background;
|
|
||||||
padding: $draggable_topic_padding;
|
|
||||||
margin: $draggable_topic_margin;
|
|
||||||
border-top: $draggable_topic_border_top;
|
|
||||||
border-bottom: $draggable_topic_border_bottom;
|
|
||||||
|
|
||||||
&.dragged {
|
|
||||||
background-color: $draggable_topic_dragged_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$draggable_collection_background: $ACCENT_COLOR !default;
|
|
||||||
$draggable_collection_dragged_color: $BUTTON_COLOR !default;
|
|
||||||
$draggable_collection_default_color: $BUTTON_COLOR_2 !default;
|
|
||||||
$draggable_collection_padding: $BIG_PADDING !default;
|
|
||||||
$draggable_collection_margin: $MEDIUM_BIG_PADDING 0 !default;
|
|
||||||
$draggable_collection_border: 5px outset !default;
|
|
||||||
$draggable_collection_border_top: $draggable_collection_border $LIGHT !default;
|
|
||||||
$draggable_collection_border_bottom: $draggable_collection_border $DARK_2 !default;
|
|
||||||
.draggable-collection {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: $draggable_collection_background;
|
|
||||||
padding: $draggable_collection_padding;
|
|
||||||
margin: $draggable_collection_margin;
|
|
||||||
border-top: $draggable_collection_border_top;
|
|
||||||
border-bottom: $draggable_collection_border_bottom;
|
|
||||||
|
|
||||||
&.dragged {
|
|
||||||
background-color: $draggable_collection_dragged_color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.default {
|
|
||||||
background-color: $draggable_collection_default_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$post_editing_header_color: $LIGHT !default;
|
$post_editing_header_color: $LIGHT !default;
|
||||||
.editing {
|
.editing {
|
||||||
background-color: $post_editing_header_color;
|
background-color: $post_editing_header_color;
|
||||||
@@ -1041,7 +1000,7 @@ $post_editing_context_margin: $BIG_PADDING $ZERO_PADDING !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.babycode-preview-errors-container {
|
.babycode-preview-errors-container {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tab_button_color: $BUTTON_COLOR !default;
|
$tab_button_color: $BUTTON_COLOR !default;
|
||||||
@@ -1199,10 +1158,6 @@ $babycode_button_min_width: $accordion_button_size !default;
|
|||||||
.babycode-button {
|
.babycode-button {
|
||||||
padding: $babycode_button_padding;
|
padding: $babycode_button_padding;
|
||||||
min-width: $babycode_button_min_width;
|
min-width: $babycode_button_min_width;
|
||||||
|
|
||||||
&> * {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$quote_fragment_background_color: #00000080 !default;
|
$quote_fragment_background_color: #00000080 !default;
|
||||||
@@ -1415,9 +1370,10 @@ a.mention, a.mention:visited {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$settings_grid_gap: $MEDIUM_PADDING !default;
|
$settings_grid_gap: $MEDIUM_PADDING !default;
|
||||||
$settings_grid_item_min_width: 400px !default;
|
$settings_grid_item_min_width: 600px !default;
|
||||||
$settings_grid_fieldset_border: 1px solid $DEFAULT_FONT_COLOR_INVERSE !default;
|
$settings_grid_fieldset_border: 1px solid $DEFAULT_FONT_COLOR_INVERSE !default;
|
||||||
$settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
$settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||||
|
$settings_grid_fieldset_background_color: $DARK_1_LIGHTER !default;
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $settings_grid_gap;
|
gap: $settings_grid_gap;
|
||||||
@@ -1428,7 +1384,7 @@ $settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
|||||||
& fieldset {
|
& fieldset {
|
||||||
border: $settings_grid_fieldset_border;
|
border: $settings_grid_fieldset_border;
|
||||||
border-radius: $settings_grid_fieldset_border_radius;
|
border-radius: $settings_grid_fieldset_border_radius;
|
||||||
background-color: $DARK_1_LIGHTER;
|
background-color: $settings_grid_fieldset_background_color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1443,7 +1399,7 @@ h1 {
|
|||||||
|
|
||||||
$settings_badge_container_gap: $SMALL_PADDING !default;
|
$settings_badge_container_gap: $SMALL_PADDING !default;
|
||||||
$settings_badge_container_border: $DEFAULT_BORDER !default;
|
$settings_badge_container_border: $DEFAULT_BORDER !default;
|
||||||
$settings_badge_container_border_invalid: 2px dashed red !default;
|
$settings_badge_container_border_invalid: $DEFAULT_BORDER_INVALID !default;
|
||||||
$settings_badge_container_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
$settings_badge_container_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||||
$settings_badge_container_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
|
$settings_badge_container_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
|
||||||
$settings_badge_container_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
|
$settings_badge_container_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
|
||||||
@@ -1511,3 +1467,117 @@ $badges_container_gap: $SMALL_PADDING !default;
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: $badges_container_gap;
|
gap: $badges_container_gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$rss_button_color: #fba668 !default;
|
||||||
|
$rss_button_color_hover: color.scale($rss_button_color, $lightness: 20%) !default;
|
||||||
|
$rss_button_color_active: color.scale($rss_button_color, $lightness: -10%, $saturation: -70%) !default;
|
||||||
|
$rss_button_font_color: black !default;
|
||||||
|
$rss_button_font_color_hover: black !default;
|
||||||
|
$rss_button_font_color_active: black !default;
|
||||||
|
.rss-button {
|
||||||
|
background-color: $rss_button_color;
|
||||||
|
color: $rss_button_font_color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $rss_button_color_hover;
|
||||||
|
color: $rss_button_font_color_hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $rss_button_color_active;
|
||||||
|
color: $rss_button_font_color_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
$body_portrait_margin: $BIG_PADDING $ZERO_PADDING !default;
|
||||||
|
body {
|
||||||
|
margin: $body_portrait_margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-container {
|
||||||
|
grid-template-areas:
|
||||||
|
"guide-toc"
|
||||||
|
"guide-topics";
|
||||||
|
grid-template-columns: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
$guide_section_padding_right_portrait: $BIG_PADDING !default;
|
||||||
|
.guide-section {
|
||||||
|
padding-right: $guide_section_padding_right_portrait;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortable_item_background: $ACCENT_COLOR !default;
|
||||||
|
$sortable_item_dragged_color: $BUTTON_COLOR !default;
|
||||||
|
$sortable_item_immovable_color: $BUTTON_COLOR_2 !default;
|
||||||
|
$sortable_item_padding: $BIG_PADDING !default;
|
||||||
|
$sortable_item_margin: $MEDIUM_BIG_PADDING 0 !default;
|
||||||
|
$sortable_item_border: 5px outset !default;
|
||||||
|
$sortable_item_border_top: $sortable_item_border $LIGHT !default;
|
||||||
|
$sortable_item_border_bottom: $sortable_item_border $DARK_2 !default;
|
||||||
|
$sortable_item_gap: $MEDIUM_PADDING !default;
|
||||||
|
ol.sortable-list {
|
||||||
|
list-style: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
gap: $sortable_item_gap;
|
||||||
|
background-color: $sortable_item_background;
|
||||||
|
padding: $sortable_item_padding;
|
||||||
|
margin: $sortable_item_margin;
|
||||||
|
border-top: $sortable_item_border_top;
|
||||||
|
border-bottom: $sortable_item_border_bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.dragged {
|
||||||
|
background-color: $sortable_item_dragged_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.immovable {
|
||||||
|
background-color: $sortable_item_immovable_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.immovable .dragger {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortable_item_grabber_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
|
||||||
|
.dragger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: $DARK_2;
|
||||||
|
padding: $sortable_item_grabber_padding;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortable_item_inner_gap: $MEDIUM_PADDING !default;
|
||||||
|
.sortable-item-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: $sortable_item_inner_gap;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.row) > * {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fg {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ $br: 8px;
|
|||||||
|
|
||||||
$mention_font_color: $fc,
|
$mention_font_color: $fc,
|
||||||
|
|
||||||
|
$settings_grid_fieldset_background_color: $lightish_accent,
|
||||||
|
|
||||||
// $settings_badge_container_border_invalid: 2px dashed $crit,
|
// $settings_badge_container_border_invalid: 2px dashed $crit,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user