Compare commits
11 Commits
ae9d33473c
...
c311fba500
| Author | SHA1 | Date | |
|---|---|---|---|
|
c311fba500
|
|||
|
4083c950c5
|
|||
|
5853c8b7a8
|
|||
|
93ee829405
|
|||
|
7247ac4cf8
|
|||
|
2c8bc6dca8
|
|||
|
edfa2e232f
|
|||
|
5676ced836
|
|||
|
7defd249b5
|
|||
|
74a95075f7
|
|||
|
c0eb867b2d
|
@@ -139,6 +139,11 @@ def bind_default_badges(path):
|
|||||||
'uploaded_at': int(os.path.getmtime(real_path)),
|
'uploaded_at': int(os.path.getmtime(real_path)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def clear_stale_invites():
|
||||||
|
from .db import db
|
||||||
|
from .util import time_now
|
||||||
|
db.execute('DELETE FROM "invite_keys" WHERE expires_at < ?', time_now())
|
||||||
|
|
||||||
def clear_stale_sessions():
|
def clear_stale_sessions():
|
||||||
from .db import db
|
from .db import db
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
@@ -162,6 +167,7 @@ cache = Cache()
|
|||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SITE_NAME'] = 'Pyrom'
|
app.config['SITE_NAME'] = 'Pyrom'
|
||||||
|
app.config['SITE_TAGLINE'] = 'anti-social media'
|
||||||
app.config['DISABLE_SIGNUP'] = False
|
app.config['DISABLE_SIGNUP'] = False
|
||||||
app.config['MODS_CAN_INVITE'] = True
|
app.config['MODS_CAN_INVITE'] = True
|
||||||
app.config['USERS_CAN_INVITE'] = False
|
app.config['USERS_CAN_INVITE'] = False
|
||||||
@@ -233,6 +239,7 @@ def create_app():
|
|||||||
|
|
||||||
clear_stale_sessions()
|
clear_stale_sessions()
|
||||||
clear_api_limits()
|
clear_api_limits()
|
||||||
|
clear_stale_invites()
|
||||||
|
|
||||||
reparse_babycode()
|
reparse_babycode()
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ 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'
|
'ALTER TABLE "post_history" ADD COLUMN "content_rss" STRING DEFAULT NULL',
|
||||||
|
'ALTER TABLE "invite_keys" ADD COLUMN "expires_at" INTEGER NOT NULL DEFAULT 0',
|
||||||
]
|
]
|
||||||
|
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
|
|||||||
@@ -511,6 +511,12 @@ class BookmarkCollections(Model):
|
|||||||
"""
|
"""
|
||||||
res = db.fetch_one(q, user_id)
|
res = db.fetch_one(q, user_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_user(cls, user_id):
|
||||||
|
q = """SELECT * FROM bookmark_collections WHERE user_id = ? ORDER BY sort_order ASC"""
|
||||||
|
res = db.query(q, user_id)
|
||||||
|
return [cls.from_data(row) for row in res]
|
||||||
|
|
||||||
def has_posts(self):
|
def has_posts(self):
|
||||||
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ?) as e'
|
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ?) as e'
|
||||||
res = db.fetch_one(q, self.id)['e']
|
res = db.fetch_one(q, self.id)['e']
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from flask import Blueprint, render_template, request, url_for
|
from flask import Blueprint, render_template, request, url_for
|
||||||
from ..auth import get_active_user, is_logged_in, hard_login_required
|
from ..auth import get_active_user, is_logged_in, hard_login_required
|
||||||
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads
|
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, Threads, Posts
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
|
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
|
||||||
@@ -24,7 +24,15 @@ def get_bookmark_dropdown():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return 'error', 400
|
return 'error', 400
|
||||||
is_thread = concept_kind == 'thread'
|
is_thread = concept_kind == 'thread'
|
||||||
collections = BookmarkCollections.findall({'user_id': user.id})
|
if is_thread:
|
||||||
|
target_thread = Threads.find({'id': concept_id})
|
||||||
|
if not target_thread:
|
||||||
|
return 'This thread no longer exists. Please refresh the page.', 404
|
||||||
|
else:
|
||||||
|
target_post = Posts.find({'id': concept_id})
|
||||||
|
if not target_post:
|
||||||
|
return 'This post no longer exists. Please refresh the page.', 404
|
||||||
|
collections = BookmarkCollections.get_for_user(user.id)
|
||||||
in_collection = None
|
in_collection = None
|
||||||
note = ''
|
note = ''
|
||||||
for collection in collections:
|
for collection in collections:
|
||||||
@@ -54,6 +62,9 @@ def bookmark_thread():
|
|||||||
bt.delete()
|
bt.delete()
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
|
if not Threads.find({'id': thread_id}):
|
||||||
|
return 'error', 404
|
||||||
|
|
||||||
target_collection = BookmarkCollections.find({'id': target_collection_id})
|
target_collection = BookmarkCollections.find({'id': target_collection_id})
|
||||||
note = request.form.get('note', '')
|
note = request.form.get('note', '')
|
||||||
if not target_collection:
|
if not target_collection:
|
||||||
@@ -91,6 +102,9 @@ def bookmark_post():
|
|||||||
bp.delete()
|
bp.delete()
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
|
if not Posts.find({'id': post_id}):
|
||||||
|
return 'error', 404
|
||||||
|
|
||||||
target_collection = BookmarkCollections.find({'id': target_collection_id})
|
target_collection = BookmarkCollections.find({'id': target_collection_id})
|
||||||
note = request.form.get('note', '')
|
note = request.form.get('note', '')
|
||||||
if not target_collection:
|
if not target_collection:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from flask import (
|
|||||||
abort, flash, current_app
|
abort, flash, current_app
|
||||||
)
|
)
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from secrets import compare_digest as compare_timesafe
|
from secrets import compare_digest as compare_timesafe, token_urlsafe
|
||||||
from wand.image import Image
|
from wand.image import Image
|
||||||
from wand.color import Color
|
from wand.color import Color
|
||||||
from wand.exceptions import WandException
|
from wand.exceptions import WandException
|
||||||
@@ -14,9 +14,9 @@ from ..auth import (
|
|||||||
login_required, revoke_session, get_active_user,
|
login_required, revoke_session, get_active_user,
|
||||||
parse_display_name, revoke_all_sessions, csrf_verified
|
parse_display_name, revoke_all_sessions, csrf_verified
|
||||||
)
|
)
|
||||||
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections
|
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections, InviteKeys
|
||||||
from ..constants import PermissionLevel, InfoboxKind
|
from ..constants import PermissionLevel, InfoboxKind
|
||||||
from ..util import get_form_checkbox
|
from ..util import get_form_checkbox, time_now
|
||||||
from ..lib.babycode import babycode_to_html
|
from ..lib.babycode import babycode_to_html
|
||||||
from ..db import db
|
from ..db import db
|
||||||
import math
|
import math
|
||||||
@@ -169,15 +169,34 @@ def log_out():
|
|||||||
@bp.get('/sign-up/')
|
@bp.get('/sign-up/')
|
||||||
@redirect_if_logged_in()
|
@redirect_if_logged_in()
|
||||||
def sign_up():
|
def sign_up():
|
||||||
return render_template('users/sign_up.html')
|
key = request.args.get('key', '')
|
||||||
|
if not key and current_app.config['DISABLE_SIGNUP']:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
elif key and current_app.config['DISABLE_SIGNUP']:
|
||||||
|
invite = InviteKeys.find({'key': key})
|
||||||
|
if not invite:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
inviter = Users.find({'id': invite.created_by})
|
||||||
|
return render_template('users/sign_up.html', invite=invite, inviter=inviter)
|
||||||
|
|
||||||
@bp.post('/sign-up/')
|
@bp.post('/sign-up/')
|
||||||
@redirect_if_logged_in()
|
@redirect_if_logged_in()
|
||||||
def sign_up_post():
|
def sign_up_post():
|
||||||
generic_error_page = redirect(url_for('.sign_up', error='The username or password you entered is invalid.'))
|
args_sans_error = dict(request.args)
|
||||||
invalid_username_error_page = redirect(url_for('.sign_up', error='This username cannot be used. Please pick another.'))
|
args_sans_error.pop('error', '')
|
||||||
passwords_error_page = redirect(url_for('.sign_up', error='The passwords do not match.'))
|
generic_error_page = redirect(url_for('.sign_up', error='The username or password you entered is invalid.', **args_sans_error))
|
||||||
|
invalid_username_error_page = redirect(url_for('.sign_up', error='This username cannot be used. Please pick another.', **args_sans_error))
|
||||||
|
passwords_error_page = redirect(url_for('.sign_up', error='The passwords do not match.', **args_sans_error))
|
||||||
username = request.form.get('username', default='')
|
username = request.form.get('username', default='')
|
||||||
|
if current_app.config['DISABLE_SIGNUP']:
|
||||||
|
key = request.form.get('key', '')
|
||||||
|
if not key:
|
||||||
|
return generic_error_page
|
||||||
|
invite = InviteKeys.find({'key': key})
|
||||||
|
if not invite:
|
||||||
|
return generic_error_page
|
||||||
|
if invite.expires_at < time_now():
|
||||||
|
return generic_error_page
|
||||||
if not username:
|
if not username:
|
||||||
return generic_error_page
|
return generic_error_page
|
||||||
if request.form.get('password') is None:
|
if request.form.get('password') is None:
|
||||||
@@ -197,12 +216,18 @@ def sign_up_post():
|
|||||||
|
|
||||||
password_hash = digest(request.form.get('password'))
|
password_hash = digest(request.form.get('password'))
|
||||||
|
|
||||||
user = Users.create({
|
user_data = {
|
||||||
'username': username_pair[0],
|
'username': username_pair[0],
|
||||||
'password_hash': password_hash,
|
'password_hash': password_hash,
|
||||||
'permission': PermissionLevel.GUEST.value,
|
'permission': PermissionLevel.GUEST.value,
|
||||||
'created_at': int(time.time()),
|
'created_at': int(time.time()),
|
||||||
})
|
}
|
||||||
|
if invite:
|
||||||
|
user_data['invited_by'] = invite.created_by
|
||||||
|
user_data['permission'] = PermissionLevel.USER.value
|
||||||
|
invite.delete()
|
||||||
|
|
||||||
|
user = Users.create(user_data)
|
||||||
|
|
||||||
BookmarkCollections.create_default(user.id)
|
BookmarkCollections.create_default(user.id)
|
||||||
|
|
||||||
@@ -217,6 +242,7 @@ def sign_up_post():
|
|||||||
if session['remember']:
|
if session['remember']:
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
|
|
||||||
|
flash(f'Welcome to {current_app.config['SITE_NAME']}!', InfoboxKind.INFO)
|
||||||
return redirect(url_for('topics.all_topics'))
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
@bp.get('/<username>/')
|
@bp.get('/<username>/')
|
||||||
@@ -286,9 +312,11 @@ def comments(username):
|
|||||||
def settings(username):
|
def settings(username):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
sort_by = session.get('sort_by', 'activity')
|
sort_by = session.get('sort_by', 'activity')
|
||||||
|
invites = InviteKeys.findall({'created_by': user.id})
|
||||||
return render_template(
|
return render_template(
|
||||||
'users/settings.html', user=user,
|
'users/settings.html', user=user,
|
||||||
sort_by=sort_by
|
sort_by=sort_by,
|
||||||
|
invites=invites,
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.post('/<username>/settings/set-avatar')
|
@bp.post('/<username>/settings/set-avatar')
|
||||||
@@ -445,8 +473,72 @@ def inbox(username):
|
|||||||
@login_required
|
@login_required
|
||||||
@redirect_to_own
|
@redirect_to_own
|
||||||
def bookmarks(username):
|
def bookmarks(username):
|
||||||
username = username.lower()
|
user = get_active_user()
|
||||||
return 'stub'
|
collections = BookmarkCollections.get_for_user(user.id)
|
||||||
|
return render_template('users/bookmarks.html', collections=collections)
|
||||||
|
|
||||||
|
@bp.get('/<username>/bookmarks/collections/')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def bookmark_collections(username):
|
||||||
|
user = get_active_user()
|
||||||
|
collections = BookmarkCollections.get_for_user(user.id)
|
||||||
|
return render_template('users/manage_collections.html', collections=collections)
|
||||||
|
|
||||||
|
@bp.post('/<username>/bookmarks/collections/')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def edit_bookmark_collections(username):
|
||||||
|
user = get_active_user()
|
||||||
|
ids = request.form.getlist('id[]')
|
||||||
|
names = request.form.getlist('name[]')
|
||||||
|
if len(ids) == 0 or len(ids) != len(names):
|
||||||
|
abort(400)
|
||||||
|
deleted_ids = filter(lambda x: x.strip(), request.form.get('deleted_ids', '').split(';'))
|
||||||
|
try:
|
||||||
|
deleted_ids = map(lambda x: int(x), deleted_ids)
|
||||||
|
except ValueError:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
with db.transaction():
|
||||||
|
for new_order, id in enumerate(ids):
|
||||||
|
new_name = names[new_order]
|
||||||
|
if id == 'new':
|
||||||
|
bc = BookmarkCollections.create({
|
||||||
|
'user_id': user.id,
|
||||||
|
'is_default': False,
|
||||||
|
'name': new_name,
|
||||||
|
'sort_order': new_order,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
id = int(id)
|
||||||
|
bc = BookmarkCollections.find({'id': id})
|
||||||
|
if not bc:
|
||||||
|
continue
|
||||||
|
if bc.user_id != user.id:
|
||||||
|
continue
|
||||||
|
if bc.is_default:
|
||||||
|
new_order = 0
|
||||||
|
elif new_order == 0:
|
||||||
|
new_order = 1
|
||||||
|
bc.update({
|
||||||
|
'name': new_name,
|
||||||
|
'sort_order': new_order,
|
||||||
|
})
|
||||||
|
|
||||||
|
for deleted_id in deleted_ids:
|
||||||
|
bc = BookmarkCollections.find({'id': deleted_id})
|
||||||
|
if not bc:
|
||||||
|
continue
|
||||||
|
if bc.user_id != user.id:
|
||||||
|
continue
|
||||||
|
if bc.is_default:
|
||||||
|
continue
|
||||||
|
bc.delete()
|
||||||
|
|
||||||
|
return redirect(url_for('.bookmark_collections', username=username))
|
||||||
|
|
||||||
@bp.get('/<username>/delete-confirm/')
|
@bp.get('/<username>/delete-confirm/')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -475,3 +567,40 @@ def delete_confirm_post(username):
|
|||||||
user.delete()
|
user.delete()
|
||||||
|
|
||||||
return redirect(url_for('topics.all_topics'))
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
@bp.post('/<username>/invite-keys/create/')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
|
@csrf_verified
|
||||||
|
def create_invite_key(username):
|
||||||
|
user = get_active_user()
|
||||||
|
if not user.can_invite():
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
key = token_urlsafe(16)
|
||||||
|
expires_at = time_now() + 48 * 60 * 60
|
||||||
|
|
||||||
|
invite = InviteKeys.create({
|
||||||
|
'created_by': user.id,
|
||||||
|
'expires_at': expires_at,
|
||||||
|
'key': key,
|
||||||
|
})
|
||||||
|
|
||||||
|
return redirect(url_for('.settings', username=username, _anchor='invite'))
|
||||||
|
|
||||||
|
@bp.post('/<username>/invite-keys/revoke/')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
|
@csrf_verified
|
||||||
|
def revoke_invite_key(username):
|
||||||
|
user = get_active_user()
|
||||||
|
if not user.can_invite():
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
key = request.form.get('key', '')
|
||||||
|
invite = InviteKeys.find({'created_by': user.id, 'key': key})
|
||||||
|
if not invite:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
invite.delete()
|
||||||
|
return redirect(url_for('.settings', username=username, _anchor='invite'))
|
||||||
|
|||||||
@@ -187,6 +187,8 @@ SCHEMA = [
|
|||||||
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_badge_upload_user ON badge_uploads(user_id)',
|
'CREATE INDEX IF NOT EXISTS idx_badge_upload_user ON badge_uploads(user_id)',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_badge_user ON badges(user_id)',
|
'CREATE INDEX IF NOT EXISTS idx_badge_user ON badges(user_id)',
|
||||||
|
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_invite_key_user ON invite_keys(created_by, key)'
|
||||||
]
|
]
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<bitty-8 data-connect="/static/js/bits/progressive-enhancement.js"></bitty-8>
|
<bitty-8 data-connect="/static/js/bits/progressive-enhancement.js"></bitty-8>
|
||||||
<bitty-8 data-connect="/static/js/bits/ui.js"></bitty-8>
|
<bitty-8 data-connect="/static/js/bits/ui.js"></bitty-8>
|
||||||
<bitty-8 data-connect="/static/js/bits/bookmark-menu.js"></bitty-8>
|
|
||||||
{%- include 'common/topnav.html' -%}
|
{%- include 'common/topnav.html' -%}
|
||||||
{%- with messages = get_flashed_messages(with_categories=true) -%}
|
{%- with messages = get_flashed_messages(with_categories=true) -%}
|
||||||
{%- if messages -%}
|
{%- if messages -%}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
<button type="button" title="insert spoiler" class="minimal" data-babycode-tag="spoiler=" data-break-line data-prefill="spoiler content" data-s="insertBabycode">s</button>
|
<button type="button" title="insert spoiler" class="minimal" data-babycode-tag="spoiler=" data-break-line data-prefill="spoiler content" data-s="insertBabycode">s</button>
|
||||||
<button type="button" title="insert emoji…" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
|
<button type="button" title="insert emoji…" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-last js-only" data-r="enhance babycodeEditorCharCount">stub: char count</span>
|
<span class="flex-last js-only" data-r="enhance babycodeEditorCharCount">0/</span>
|
||||||
</span>
|
</span>
|
||||||
<input type="hidden" name="babycode_banned_tags" id="{{id}}-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
|
<input type="hidden" name="babycode_banned_tags" id="{{id}}-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
|
||||||
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}} autocomplete="off" maxlength="5000" data-r="insertBabycode babycodePreviewInit babycodeEditorCharCountInit babycodeEditorQuote" data-listen="input" data-s="babycodeEditorCharCount" data-banned-tags="{{banned_tags | unique | list | tojson | forceescape}}">{{ prefill }}</textarea>
|
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}} autocomplete="off" maxlength="5000" data-r="insertBabycode babycodePreviewInit babycodeEditorCharCountInit babycodeEditorQuote" data-listen="input" data-s="babycodeEditorCharCount" data-banned-tags="{{banned_tags | unique | list | tojson | forceescape}}">{{ prefill }}</textarea>
|
||||||
@@ -136,10 +136,16 @@
|
|||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro bookmark_button(kind, id, text='Bookmark') -%}
|
||||||
|
<button autocomplete='off' data-r="enhance" data-s="showBookmarkMenu" disabled title="This feature requires JavaScript to be enabled." data-concept-kind="{{kind}}" data-concept-id="{{id}}">{{icn_bookmark(24)}}{{text}}…</button>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro full_post(
|
{% macro full_post(
|
||||||
post, render_sig=true, is_latest=false,
|
post, render_sig=true, is_latest=false,
|
||||||
show_toolbar=true, is_editing=false, thread=none,
|
show_toolbar=true, is_editing=false, thread=none,
|
||||||
show_reactions=true, show_thread=false, allow_reacting=true
|
show_reactions=true, show_thread=false, allow_reacting=true,
|
||||||
|
tb_edit=true, tb_quote=true, tb_delete=true, tb_bookmark=true,
|
||||||
|
bookmark_btn='Bookmark', tb_pretext=''
|
||||||
) -%}
|
) -%}
|
||||||
{%- if is_logged_in() -%}
|
{%- if is_logged_in() -%}
|
||||||
{%- set can_delete = post.user_id == get_active_user().id or is_mod() -%}
|
{%- set can_delete = post.user_id == get_active_user().id or is_mod() -%}
|
||||||
@@ -169,6 +175,9 @@
|
|||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<div class="plank even minimal secondary-bg no-shadow post-info">
|
<div class="plank even minimal secondary-bg no-shadow post-info">
|
||||||
<span>
|
<span>
|
||||||
|
{%- if tb_pretext -%}
|
||||||
|
<span>{{tb_pretext}} • </span>
|
||||||
|
{%- endif -%}
|
||||||
<a href="{{get_post_url(post.id, _anchor=true)}}">
|
<a href="{{get_post_url(post.id, _anchor=true)}}">
|
||||||
{%- if post.edited_at <= post.created_at -%}
|
{%- if post.edited_at <= post.created_at -%}
|
||||||
<i>Posted on {{timestamp(post.created_at)}}</i>
|
<i>Posted on {{timestamp(post.created_at)}}</i>
|
||||||
@@ -181,17 +190,19 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</span>
|
</span>
|
||||||
{%- if show_toolbar -%}
|
{%- if show_toolbar -%}
|
||||||
<span class="thread-actions">
|
<span class="subheader-actions">
|
||||||
{%- if owns -%}
|
{%- if owns and tb_edit -%}
|
||||||
<a class="linkbutton" href="{{url_for('posts.edit', post_id=post.id, _anchor='babycode-content')}}">Edit</a>
|
<a class="linkbutton" href="{{url_for('posts.edit', post_id=post.id, _anchor='babycode-content')}}">Edit</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if can_reply -%}
|
{%- if can_reply and tb_quote -%}
|
||||||
<button autocomplete='off' data-r="enhance" data-s="babycodeEditorQuote" disabled title="This feature requires JavaScript to be enabled." data-quote="{{post.original_markup}}" data-poster-name="{{ post.display_name if post.display_name else post.username }}">Quote</button>
|
<button autocomplete='off' data-r="enhance" data-s="babycodeEditorQuote" disabled title="This feature requires JavaScript to be enabled." data-quote="{{post.original_markup}}" data-poster-name="{{ post.display_name if post.display_name else post.username }}">Quote</button>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if can_delete -%}
|
{%- if can_delete and tb_delete -%}
|
||||||
<a class="linkbutton critical" href="{{url_for('posts.delete', post_id=post.id)}}">Delete</a>
|
<a class="linkbutton critical" href="{{url_for('posts.delete', post_id=post.id)}}">Delete</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<button autocomplete='off' data-r="enhance" data-s="showBookmarkMenu" disabled title="This feature requires JavaScript to be enabled." data-concept-kind="post" data-concept-id="{{post.id}}">{{icn_bookmark(24)}}Bookmark…</button>
|
{%- if tb_bookmark -%}
|
||||||
|
{{ bookmark_button('post', post.id, bookmark_btn) }}
|
||||||
|
{%- endif -%}
|
||||||
</span>
|
</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
@@ -229,6 +240,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro bookmark_menu() -%}
|
||||||
|
{%- if is_logged_in() -%}
|
||||||
|
<div id="bookmark-popover" data-r="showBookmarkMenu" class="plank even" popover>
|
||||||
|
<div class="bookmark-menu-header">
|
||||||
|
<span>Bookmark collections</span>
|
||||||
|
<a href="{{url_for('users.bookmarks', username=get_active_user().username)}}">View bookmarks</a>
|
||||||
|
</div>
|
||||||
|
<div class="bookmark-menu-inner" data-r="fillBookmarkMenu">Loading…</div>
|
||||||
|
</div>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro infobox(message, kind=InfoboxKind.INFO) -%}
|
{% macro infobox(message, kind=InfoboxKind.INFO) -%}
|
||||||
<div class="infobox plank top contain-svg horizontal {{InfoboxHTMLClass[kind]}}">
|
<div class="infobox plank top contain-svg horizontal {{InfoboxHTMLClass[kind]}}">
|
||||||
{%- if kind == InfoboxKind.INFO -%}
|
{%- if kind == InfoboxKind.INFO -%}
|
||||||
@@ -270,7 +293,7 @@
|
|||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro sortable_list_item(key, immovable=false, attr=none) -%}
|
{% macro sortable_list_item(key, immovable=false, attr=none) -%}
|
||||||
<li class="sortable-item{{ ' immovable' if immovable else '' }} plank even no-shadow {{'tertiary-bg' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
<li class="sortable-item{{ ' immovable' if immovable else '' }} plank even no-shadow {{'secondary-bg' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||||
<span class="dragger plank minimal even no-shadow tertiary-bg" draggable="{{ 'true' if not immovable else 'false' }}">{{ icn_dragger() }}</span>
|
<span class="dragger plank minimal even no-shadow tertiary-bg" draggable="{{ 'true' if not immovable else 'false' }}">{{ icn_dragger() }}</span>
|
||||||
<div class="sortable-item-inner">{{ caller() }}</div>
|
<div class="sortable-item-inner">{{ caller() }}</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<nav id="header" class="plank top">
|
<nav id="header" class="plank top">
|
||||||
<a class="site-title" href="/">Porom</a>
|
<a class="site-title" href="/">{{config.SITE_NAME}}</a>
|
||||||
<span>anti-social media</span>
|
<span>{{config.SITE_TAGLINE or ' ' | safe}}</span>
|
||||||
{%- if is_logged_in() -%}
|
{%- if is_logged_in() -%}
|
||||||
{%- with user = get_active_user() -%}
|
{%- with user = get_active_user() -%}
|
||||||
{%- set uc = user.get_unread_count() -%}
|
{%- set uc = user.get_unread_count() -%}
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
<input type="password" placeholder="Password" name="password" autocomplete="current-password" required>
|
<input type="password" placeholder="Password" name="password" autocomplete="current-password" required>
|
||||||
<span><input type="checkbox" name="remember" id="remember"> <label for="remember">Remember me</label></span>
|
<span><input type="checkbox" name="remember" id="remember"> <label for="remember">Remember me</label></span>
|
||||||
<input type="submit" value="Log in">
|
<input type="submit" value="Log in">
|
||||||
|
{%- if not config.DISABLE_SIGNUP -%}
|
||||||
<a href="{{url_for('users.sign_up')}}" class="linkbutton alt">Sign up</a>
|
<a href="{{url_for('users.sign_up')}}" class="linkbutton alt">Sign up</a>
|
||||||
|
{%- endif -%}
|
||||||
</form>
|
</form>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{%- from 'common/macros.html' import subheader, babycode_editor_component, sortable_list, sortable_list_item -%}
|
{%- from 'common/macros.html' import subheader, babycode_editor_component, sortable_list, sortable_list_item -%}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}settings{%- endblock -%}
|
{%- block title -%}moderation panel{%- endblock -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{{- subheader('Moderation panel') -}}
|
{{- subheader('Moderation panel') -}}
|
||||||
<fieldset class="plank">
|
<fieldset class="plank">
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
<input type="submit" value="Clear MOTD" class="warn">
|
<input type="submit" value="Clear MOTD" class="warn">
|
||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{{babycode_editor_component(placeholder='test', id='test-content')}}
|
|
||||||
<fieldset class="plank" id="sort-topics">
|
<fieldset class="plank" id="sort-topics">
|
||||||
<legend>Sort topics</legend>
|
<legend>Sort topics</legend>
|
||||||
<p>Drag topics around to reorder them. Press "Save order" when done.</p>
|
<p>Drag topics around to reorder them. Press "Save order" when done.</p>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{%- call() subheader("Delete post", "Are you sure you want to delete this post? This action can not be undone.") -%}
|
{%- call() subheader("Delete post", "Are you sure you want to delete this post? This action can not be undone.") -%}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<fieldset class="plank minimal even no-shadow thread-actions">
|
<fieldset class="plank minimal even no-shadow subheader-actions">
|
||||||
<legend>Please confirm</legend>
|
<legend>Please confirm</legend>
|
||||||
<a href="{{get_post_url(post.id, _anchor=true)}}" class="linkbutton">Cancel</a>
|
<a href="{{get_post_url(post.id, _anchor=true)}}" class="linkbutton">Cancel</a>
|
||||||
<input type="submit" value="Delete" class="critical">
|
<input type="submit" value="Delete" class="critical">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<form class="plank primary-bg full-width" method="POST">
|
<form class="plank primary-bg full-width" method="POST">
|
||||||
<label for="topic">Topic</label>
|
<label for="topic">Topic</label>
|
||||||
<select name="topic_id" id="topic" autocomplete="off">
|
<select name="topic_id" id="topic" autocomplete="off">
|
||||||
{%- for topic in topics -%}
|
{%- for topic in topics | sort(attribute='sort_order') -%}
|
||||||
<option value="{{topic.id}}" {{'selected' if selected_topic == topic.id else ''}} {{'disabled' if not get_active_user().can_post_to_thread_or_topic(topic) else ''}}>{{topic.name}}{{ ' (locked)' if topic.locked() else ''}}</option>
|
<option value="{{topic.id}}" {{'selected' if selected_topic == topic.id else ''}} {{'disabled' if not get_active_user().can_post_to_thread_or_topic(topic) else ''}}>{{topic.name}}{{ ' (locked)' if topic.locked() else ''}}</option>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
{%- from 'common/macros.html' import subheader, timestamp, pager, babycode_editor_component -%}
|
{%- from 'common/macros.html' import subheader, timestamp, pager, babycode_editor_component -%}
|
||||||
{%- from 'common/icons.html' import icn_bookmark -%}
|
{%- from 'common/icons.html' import icn_bookmark -%}
|
||||||
{%- from 'common/macros.html' import full_post with context -%}
|
{%- from 'common/macros.html' import full_post, bookmark_menu with context -%}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}{{thread.title}}{%- endblock -%}
|
{%- block title -%}{{thread.title}}{%- endblock -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
|
<bitty-8 data-connect="/static/js/bits/bookmark-menu.js"></bitty-8>
|
||||||
{%- set td -%}
|
{%- set td -%}
|
||||||
<ul class="horizontal">
|
<ul class="horizontal">
|
||||||
<li>Started by <a href="{{url_for('users.user_page', username=started_by.username)}}">{{started_by.get_readable_name()}}</a> in topic <a href="{{url_for('topics.topic_by_id', topic_id=topic.id)}}">{{topic.name}}</a></li>
|
<li>Started by <a href="{{url_for('users.user_page', username=started_by.username)}}">{{started_by.get_readable_name()}}</a> in topic <a href="{{url_for('topics.topic_by_id', topic_id=topic.id)}}">{{topic.name}}</a></li>
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{%- endset -%}
|
{%- endset -%}
|
||||||
{%- call() subheader(thread.title, td) -%}
|
{%- call() subheader(thread.title, td) -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Actions</legend>
|
<legend>Actions</legend>
|
||||||
{%- if is_logged_in() -%}
|
{%- if is_logged_in() -%}
|
||||||
{%- if thread.user_id == get_active_user().id -%}
|
{%- if thread.user_id == get_active_user().id -%}
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
<a href="{{url_for('threads.feed', thread_id=thread.id)}}" class="linkbutton rss">Subscribe via RSS</a>
|
<a href="{{url_for('threads.feed', thread_id=thread.id)}}" class="linkbutton rss">Subscribe via RSS</a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{%- if is_mod() -%}
|
{%- if is_mod() -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Moderation actions</legend>
|
<legend>Moderation actions</legend>
|
||||||
{%- if thread.user_id != get_active_user().id -%}
|
{%- if thread.user_id != get_active_user().id -%}
|
||||||
<a class="linkbutton warn" href="{{url_for('threads.edit', thread_id=thread.id)}}">Rename</a>
|
<a class="linkbutton warn" href="{{url_for('threads.edit', thread_id=thread.id)}}">Rename</a>
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
<input type="submit" value="Move" class="warn">
|
<input type="submit" value="Move" class="warn">
|
||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count) -}}
|
{{- pager(page, page_count) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</main>
|
</main>
|
||||||
<div class="plank secondary-bg">
|
<div class="plank secondary-bg">
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count) -}}
|
{{- pager(page, page_count) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -82,15 +83,7 @@
|
|||||||
<button>Stop updates</button>
|
<button>Stop updates</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{%- if is_logged_in() -%}
|
{{ bookmark_menu() }}
|
||||||
<div id="bookmark-popover" data-r="showBookmarkMenu" class="plank even" popover>
|
|
||||||
<div class="bookmark-menu-header">
|
|
||||||
<span>Bookmark collections</span>
|
|
||||||
<a href="{{url_for('users.bookmarks', username=get_active_user().username)}}">View bookmarks</a>
|
|
||||||
</div>
|
|
||||||
<div class="bookmark-menu-inner" data-r="fillBookmarkMenu">Loading…</div>
|
|
||||||
</div>
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(thread) -%}
|
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(thread) -%}
|
||||||
<form action="{{url_for('threads.reply', thread_id=thread.id)}}" method="POST" class="plank post-edit-form" data-listen="submit" data-r="clearThreadDraft" data-s="clearThreadDraft">
|
<form action="{{url_for('threads.reply', thread_id=thread.id)}}" method="POST" class="plank post-edit-form" data-listen="submit" data-r="clearThreadDraft" data-s="clearThreadDraft">
|
||||||
<h2 class="info">Reply to "{{thread.title}}"</h2>
|
<h2 class="info">Reply to "{{thread.title}}"</h2>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{%- endset -%}
|
{%- endset -%}
|
||||||
{%- call() subheader(('Threads in "%s"' % topic.name), td) -%}
|
{%- call() subheader(('Threads in "%s"' % topic.name), td) -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Actions</legend>
|
<legend>Actions</legend>
|
||||||
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(topic) -%}
|
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(topic) -%}
|
||||||
<a href="{{url_for('threads.new', topic_id=topic.id)}}" class="linkbutton">New thread</a>
|
<a href="{{url_for('threads.new', topic_id=topic.id)}}" class="linkbutton">New thread</a>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{%- if is_mod() -%}
|
{%- if is_mod() -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Moderation actions</legend>
|
<legend>Moderation actions</legend>
|
||||||
<a href="{{url_for('mod.edit_topic', topic_id=topic.id)}}" class="linkbutton">Edit</a>
|
<a href="{{url_for('mod.edit_topic', topic_id=topic.id)}}" class="linkbutton">Edit</a>
|
||||||
<form action="{{url_for('mod.lock_topic', topic_id=topic.id)}}" method="POST">
|
<form action="{{url_for('mod.lock_topic', topic_id=topic.id)}}" method="POST">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if threads | length > 0 -%}
|
{%- if threads | length > 0 -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count, args=request.args) -}}
|
{{- pager(page, page_count, args=request.args) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- if threads | length > 0 -%}
|
{%- if threads | length > 0 -%}
|
||||||
<div class="plank secondary-bg">
|
<div class="plank secondary-bg">
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count, args=request.args) -}}
|
{{- pager(page, page_count, args=request.args) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{%- call() subheader('All topics') -%}
|
{%- call() subheader('All topics') -%}
|
||||||
{%- if is_mod() -%}
|
{%- if is_mod() -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Moderation actions</legend>
|
<legend>Moderation actions</legend>
|
||||||
<a href="{{url_for('mod.new_topic')}}" class="linkbutton">New topic</a>
|
<a href="{{url_for('mod.new_topic')}}" class="linkbutton">New topic</a>
|
||||||
<a href="{{url_for('mod.index', _anchor='sort-topics')}}" class="linkbutton">Sort topics</a>
|
<a href="{{url_for('mod.index', _anchor='sort-topics')}}" class="linkbutton">Sort topics</a>
|
||||||
|
|||||||
56
app/templates/users/bookmarks.html
Normal file
56
app/templates/users/bookmarks.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{%- from 'common/macros.html' import full_post, bookmark_menu with context -%}
|
||||||
|
{%- from 'common/macros.html' import subheader, bookmark_button -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}bookmarks"{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
<bitty-8 data-connect="/static/js/bits/bookmark-menu.js"></bitty-8>
|
||||||
|
<bitty-8 data-connect="/static/js/bits/bookmarks.js"></bitty-8>
|
||||||
|
{%- call() subheader('Your bookmarks') -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal subheader-actions js-only" data-r="enhance">
|
||||||
|
<legend>Actions</legend>
|
||||||
|
<a href="{{url_for('users.bookmark_collections', username=get_active_user().username)}}" class="linkbutton">Manage collections</a>
|
||||||
|
</fieldset>
|
||||||
|
{%- endcall -%}
|
||||||
|
<div class="plank">
|
||||||
|
{%- for collection in collections -%}
|
||||||
|
{%- set thread_count = collection.get_threads_count() -%}
|
||||||
|
{%- set post_count = collection.get_posts_count() -%}
|
||||||
|
<details class="separated" data-id="{{collection.id}}" data-r="restoreCollectionDetails setCollectionDetails" data-s="setCollectionDetails">
|
||||||
|
<summary class="plank secondary-bg no-shadow even">{{collection.name}} ({{thread_count}} {{'thread' | pluralize(num=thread_count)}}, {{post_count}} {{'post' | pluralize(num=post_count)}})</summary>
|
||||||
|
{%- if thread_count > 0 -%}
|
||||||
|
<details class="inner" data-id="{{collection.id}}" data-r="restoreThreadDetails setThreadDetails" data-s="setThreadDetails">
|
||||||
|
<summary class="plank no-shadow even">Threads</summary>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="plank even no-shadow contrast-bg" style="--w:65%">Title</th>
|
||||||
|
<th class="plank even no-shadow contrast-bg" style="--w:25%">Memo</th>
|
||||||
|
<th class="plank even no-shadow contrast-bg" style="--w:10%">Manage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- for bt in collection.get_threads() -%}
|
||||||
|
{%- set thread = bt.get_thread() -%}
|
||||||
|
<tr>
|
||||||
|
<td class="plank even no-shadow minimal secondary-bg"><a href="{{url_for('threads.thread_by_id', thread_id=thread.id)}}">{{thread.title}}</a></td>
|
||||||
|
<td class="plank even no-shadow minimal secondary-bg">{{bt.note}}</td>
|
||||||
|
<td class="plank even no-shadow minimal secondary-bg">{{bookmark_button('thread', id=thread.id, text='Manage')}}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if post_count > 0 -%}
|
||||||
|
<details class="inner" data-id="{{collection.id}}" data-r="restorePostDetails setPostDetails" data-s="setPostDetails">
|
||||||
|
<summary class="plank no-shadow even">Posts</summary>
|
||||||
|
{%- for bp in collection.get_posts() -%}
|
||||||
|
<div class="post plank no-shadow even">{{ full_post(bp.get_post().get_full_post_view(), render_sig=false, show_thread=true, show_reactions=false, tb_edit=false, tb_quote=false, tb_delete=false, bookmark_btn='Manage', tb_pretext=('memo: ' + bp.note) if bp.note else '') }}</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
</details>
|
||||||
|
{%- endif -%}
|
||||||
|
</details>
|
||||||
|
{%- endfor -%}
|
||||||
|
</div>
|
||||||
|
{{ bookmark_menu() }}
|
||||||
|
{%- endblock -%}
|
||||||
44
app/templates/users/manage_collections.html
Normal file
44
app/templates/users/manage_collections.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{%- from 'common/macros.html' import subheader, sortable_list, sortable_list_item -%}
|
||||||
|
{%- macro collection_item(name='', can_delete=true, id=-1, thread_count=0, post_count=0) -%}
|
||||||
|
<input name="name[]" type="text" autocomplete="off" value="{{name}}" required maxlength=60 placeholder="Collection name">
|
||||||
|
<input type="hidden" name="id[]" value="{{ 'new' if id == -1 else id}}" autocomplete="off">
|
||||||
|
<span>{{thread_count}} {{'thread' | pluralize(num=thread_count)}}, {{post_count}} {{'post' | pluralize(num=post_count)}}</span>
|
||||||
|
{%- if not can_delete -%}
|
||||||
|
<i>Default collection</i>
|
||||||
|
{%- else -%}
|
||||||
|
<button type="button" class="critical" data-s="deleteCollection">Delete</button>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endmacro -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}managing bookmark collections{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
<bitty-8 data-connect="/static/js/bits/collections-editor.js"></bitty-8>
|
||||||
|
{%- set sh -%}
|
||||||
|
<span class="js-only" data-r="enhance">
|
||||||
|
Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.
|
||||||
|
</span>
|
||||||
|
<div data-r="enhanceHide">This page requires JS enabled to work correctly.</div>
|
||||||
|
{%- endset -%}
|
||||||
|
{%- call() subheader('Manage bookmark collections', sh) -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal subheader-actions js-only" data-r="enhance">
|
||||||
|
<legend>Actions</legend>
|
||||||
|
<button data-s="addCollection">Add new collection</button>
|
||||||
|
<input type="submit" class="alt" value="Save collections" form="collections-form">
|
||||||
|
</fieldset>
|
||||||
|
{%- endcall -%}
|
||||||
|
<form class="plank" method="POST" id="collections-form">
|
||||||
|
<input type="hidden" autocomplete="off" name="deleted_ids" value="" data-r="countDeletedCollection">
|
||||||
|
{%- call() sortable_list(attr={'data-r': 'addCollection'}) -%}
|
||||||
|
{%- for collection in collections -%}
|
||||||
|
{%- call() sortable_list_item(key='bc', immovable=collection.is_default == 1, attr={'data-r': 'deleteCollection', 'data-id': collection.id}) -%}
|
||||||
|
{{ collection_item(name=collection.name, can_delete=collection.is_default != 1, thread_count=collection.get_threads_count(), post_count=collection.get_posts_count(), id=collection.id) }}
|
||||||
|
{%- endcall -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endcall -%}
|
||||||
|
</form>
|
||||||
|
<script type="text/html" data-template="collectionItem">
|
||||||
|
{%- call() sortable_list_item(key='bc', attr={'data-r': 'deleteCollection', 'data-id': 'new'}) -%}
|
||||||
|
{{- collection_item() -}}
|
||||||
|
{%- endcall -%}
|
||||||
|
</script>
|
||||||
|
{%- endblock -%}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
{%- endset -%}
|
{%- endset -%}
|
||||||
{%- call() subheader("%s's posts" % target_user.get_readable_name(), td) -%}
|
{%- call() subheader("%s's posts" % target_user.get_readable_name(), td) -%}
|
||||||
{%- if posts -%}
|
{%- if posts -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count) -}}
|
{{- pager(page, page_count) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="post plank">{{full_post(post, show_toolbar=false, show_thread=true, allow_reacting=false)}}</div>
|
<div class="post plank">{{full_post(post, show_toolbar=false, show_thread=true, allow_reacting=false)}}</div>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
<div class="plank">
|
<div class="plank">
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count) -}}
|
{{- pager(page, page_count) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{%- from 'common/macros.html' import babycode_editor_component -%}
|
{%- from 'common/macros.html' import babycode_editor_component -%}
|
||||||
{%- from 'common/macros.html' import subheader, avatar -%}
|
{%- from 'common/macros.html' import subheader, avatar, timestamp -%}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}settings{%- endblock -%}
|
{%- block title -%}settings{%- endblock -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<span class="avatar-form-controls">
|
<span class="avatar-form-controls">
|
||||||
<label for="avatar" class="linkbutton alt">Upload…</label>
|
<label for="avatar" class="linkbutton alt">Upload…</label>
|
||||||
<span class="avatar-form-size-label">1MB max. Will be cropped to square.</span>
|
<span class="avatar-form-size-label">1MB max. Will be cropped to square.</span>
|
||||||
<input type="file" style="display: none;" id="avatar" name="avatar" accept="image/*">
|
<input type="file" style="display: none;" id="avatar" name="avatar" accept="image/*" required>
|
||||||
<input type="submit" value="Save">
|
<input type="submit" value="Save">
|
||||||
<input type="submit" class="warn" value="Clear" formaction="{{url_for('users.clear_avatar', username=user.username)}}">
|
<input type="submit" class="warn" value="Clear" formaction="{{url_for('users.clear_avatar', username=user.username)}}">
|
||||||
</span>
|
</span>
|
||||||
@@ -76,6 +76,45 @@
|
|||||||
<div>Loading badges…</div>
|
<div>Loading badges…</div>
|
||||||
<div>If badges fail to load, make sure JS is enabled.</div>
|
<div>If badges fail to load, make sure JS is enabled.</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
{%- if user.can_invite() -%}
|
||||||
|
<fieldset class="plank" id="invite">
|
||||||
|
<legend>Invite keys</legend>
|
||||||
|
<p>To manage growth, {{ config.SITE_NAME }} disallows direct sign ups. Instead, users already with an account may invite people they know. You can create invite links here.</p>
|
||||||
|
<p>Invite links are valid for 48 hours. Once an invite link is used to sign up, it can no longer be used.</p>
|
||||||
|
<form method="POST" action="{{url_for('users.create_invite_key', username=user.username)}}">
|
||||||
|
{{ csrf_input() | safe }}
|
||||||
|
<input type="submit" value="Create new invite">
|
||||||
|
</form>
|
||||||
|
{%- if invites -%}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="plank even no-shadow contrast-bg" style="--w: 50%;">Link</th>
|
||||||
|
<th class="plank even no-shadow contrast-bg" style="--w: 30%;">Expires</th>
|
||||||
|
<th class="plank even no-shadow contrast-bg" style="--w: 20%;">Revoke</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- for invite in invites -%}
|
||||||
|
<tr>
|
||||||
|
<td class="plank even no-shadow minimal"><a href="{{url_for('users.sign_up', key=invite.key)}}">Copy this</a></td>
|
||||||
|
<td class="plank even no-shadow minimal">{{timestamp(invite.expires_at)}}</td>
|
||||||
|
<td class="plank even no-shadow minimal center">
|
||||||
|
<form method="POST" action="{{url_for('users.revoke_invite_key', username=user.username)}}">
|
||||||
|
{{ csrf_input() | safe }}
|
||||||
|
<input type="hidden" name="key" value="{{invite.key}}">
|
||||||
|
<input type="submit" class="warn" value="Revoke">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{%- else -%}
|
||||||
|
<p>You do not have any invites pending activation.</p>
|
||||||
|
{%- endif -%}
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<fieldset class="plank">
|
<fieldset class="plank">
|
||||||
<legend>Disown & Delete account</legend>
|
<legend>Disown & Delete account</legend>
|
||||||
|
|||||||
@@ -4,13 +4,22 @@
|
|||||||
{%- block title -%}sign up{%- endblock -%}
|
{%- block title -%}sign up{%- endblock -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{%- set welcome -%}
|
{%- set welcome -%}
|
||||||
Please read the rules etc. stub
|
<p>Please read the rules etc. stub</p>
|
||||||
|
{%- if not inviter -%}
|
||||||
|
<p>After you sign up, a moderator will need to confirm your account before you will be allowed to post.
|
||||||
|
{%- else -%}
|
||||||
|
You have been invited by <a href="{{url_for('users.user_page', username=inviter.username)}}">{{inviter.get_readable_name()}}</a> to join {{config.SITE_NAME}}. Create an identity below.
|
||||||
|
{%- endif -%}
|
||||||
|
</p>
|
||||||
{%- endset -%}
|
{%- endset -%}
|
||||||
{{ subheader('Sign up', welcome)}}
|
{{ subheader('Sign up', welcome)}}
|
||||||
{%- if request.args.get('error') -%}
|
{%- if request.args.get('error') -%}
|
||||||
{{infobox(request.args.error, InfoboxKind.ERROR)}}
|
{{infobox(request.args.error, InfoboxKind.ERROR)}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<form class="plank primary-bg full-width" method="POST">
|
<form class="plank primary-bg full-width" method="POST">
|
||||||
|
{%- if invite -%}
|
||||||
|
<input type="hidden" name="key" value="{{invite.key}}">
|
||||||
|
{%- endif -%}
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" id="username" name="username" pattern="[a-zA-Z0-9_\-]{3,24}" title="3-24 characters. Only upper and lowercase letters, digits, hyphens, and underscores" autocomplete="username" required>
|
<input type="text" id="username" name="username" pattern="[a-zA-Z0-9_\-]{3,24}" title="3-24 characters. Only upper and lowercase letters, digits, hyphens, and underscores" autocomplete="username" required>
|
||||||
<label for="password">Create password</label>
|
<label for="password">Create password</label>
|
||||||
@@ -18,6 +27,6 @@ Please read the rules etc. stub
|
|||||||
<label for="password2">Confirm password</label>
|
<label for="password2">Confirm password</label>
|
||||||
<input type="password" id="password2" name="password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}" title="10+ chars with at least: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" autocomplete="new-password" required>
|
<input type="password" id="password2" name="password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}" title="10+ chars with at least: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" autocomplete="new-password" required>
|
||||||
<span><input type="checkbox" name="remember" id="remember"> <label for="remember">Remember me</label></span>
|
<span><input type="checkbox" name="remember" id="remember"> <label for="remember">Remember me</label></span>
|
||||||
<input type="submit" value="Sign up">
|
<input type="submit" value="Sign up" class="alt">
|
||||||
</form>
|
</form>
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{%- endset -%}
|
{%- endset -%}
|
||||||
{%- call() subheader("%s's started threads" % target_user.get_readable_name(), td) -%}
|
{%- call() subheader("%s's started threads" % target_user.get_readable_name(), td) -%}
|
||||||
{%- if threads -%}
|
{%- if threads -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count) -}}
|
{{- pager(page, page_count) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
<div class="plank">
|
<div class="plank">
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Page</legend>
|
<legend>Page</legend>
|
||||||
{{- pager(page, page_count) -}}
|
{{- pager(page, page_count) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{%- from 'common/macros.html' import subheader, timestamp, pager, avatar -%}
|
{%- from 'common/macros.html' import subheader, timestamp, pager, avatar -%}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}{{ target_user.get_readable_name() }}'s profile{%- endblock -%}
|
{%- block title -%}@{{ target_user.username }}{%- endblock -%}
|
||||||
{%- set stats = target_user.get_post_stats() -%}
|
{%- set stats = target_user.get_post_stats() -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{%- call() subheader("%s's profile" % target_user.get_readable_name()) -%}
|
{%- call() subheader("%s's profile" % target_user.get_readable_name()) -%}
|
||||||
{%- if is_logged_in() -%}
|
{%- if is_logged_in() -%}
|
||||||
|
|
||||||
{%- if target_user.id == get_active_user().id -%}
|
{%- if target_user.id == get_active_user().id -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Actions</legend>
|
<legend>Actions</legend>
|
||||||
<form action="{{url_for('users.log_out')}}" method="POST">
|
<form action="{{url_for('users.log_out')}}" method="POST">
|
||||||
<input type="submit" class="warn" value="Log out">
|
<input type="submit" class="warn" value="Log out">
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if get_active_user().is_mod() and target_user.id != get_active_user().id and target_user.permission < get_active_user().permission -%}
|
{%- if get_active_user().is_mod() and target_user.id != get_active_user().id and target_user.permission < get_active_user().permission -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal subheader-actions">
|
||||||
<legend>Moderation actions</legend>
|
<legend>Moderation actions</legend>
|
||||||
<form class="thread-actions" method="POST">
|
<form class="subheader-actions" method="POST">
|
||||||
{{csrf_input() | safe}}
|
{{csrf_input() | safe}}
|
||||||
{%- if target_user.is_guest() -%}
|
{%- if target_user.is_guest() -%}
|
||||||
<input class="warn" type="submit" value="Approve user" formaction="{{url_for('mod.make_user_regular', user_id=target_user.id)}}">
|
<input class="warn" type="submit" value="Approve user" formaction="{{url_for('mod.make_user_regular', user_id=target_user.id)}}">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="plank even minimal no-shadow user-stats">
|
<div class="plank even minimal no-shadow user-stats">
|
||||||
<h3 class="info">{{target_user.get_readable_name()}}</h3>
|
<h3 class="info">{{target_user.get_readable_name()}} (@{{target_user.username}})</h3>
|
||||||
<span>Display name: {{target_user.get_readable_name()}}</span>
|
<span>Display name: {{target_user.get_readable_name()}}</span>
|
||||||
<span>Mention: @{{target_user.username}}</span>
|
<span>Mention: @{{target_user.username}}</span>
|
||||||
<span>Status: <em>{{target_user.status}}</em></span>
|
<span>Status: <em>{{target_user.status}}</em></span>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ SERVER_NAME = "forum.your.domain"
|
|||||||
# your forum's name, shown on the header.
|
# your forum's name, shown on the header.
|
||||||
SITE_NAME = "Pyrom"
|
SITE_NAME = "Pyrom"
|
||||||
|
|
||||||
|
# the forum's tagline, shown below the title.
|
||||||
|
SITE_TAGLINE = "anti-social media"
|
||||||
|
|
||||||
# if true, users can not sign up manually. see the following two settings.
|
# if true, users can not sign up manually. see the following two settings.
|
||||||
DISABLE_SIGNUP = false
|
DISABLE_SIGNUP = false
|
||||||
|
|
||||||
|
|||||||
@@ -92,17 +92,18 @@ button, .linkbutton, input[type="submit"], input[type="file"]::file-selector-but
|
|||||||
--hover-color: hsl(from var(--main-color) h s calc(l * 1.05));
|
--hover-color: hsl(from var(--main-color) h s calc(l * 1.05));
|
||||||
--active-color: hsl(from var(--main-color) h s calc(l * 0.8));
|
--active-color: hsl(from var(--main-color) h s calc(l * 0.8));
|
||||||
--disabled-color: hsl(from var(--main-color) h calc(s * 0.5) l);
|
--disabled-color: hsl(from var(--main-color) h calc(s * 0.5) l);
|
||||||
--bottom-color: hsl(from var(--main-color) h s calc(l * 0.7));
|
--bottom-color: hsl(from var(--main-color) h s calc(l * 0.8));
|
||||||
--top-color: hsl(from var(--main-color) h s 95);
|
--top-color: hsl(from var(--main-color) h s 90);
|
||||||
--top-color2: hsl(from var(--main-color) h s calc(l * 1.1));
|
--top-color2: hsl(from var(--main-color) h s calc(l * 1.1));
|
||||||
--inset-color: #fff7;
|
--inset-color: #fff7;
|
||||||
|
--current-color: var(--main-color);
|
||||||
/* position: relative; */
|
/* position: relative; */
|
||||||
/* display: inline-block; */
|
/* display: inline-block; */
|
||||||
padding: var(--small-padding) var(--medium-padding);
|
padding: var(--small-padding) var(--medium-padding);
|
||||||
margin: var(--base-padding) 0px;
|
margin: var(--base-padding) 0px;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
border: solid var(--border-thickness) var(--border-color);
|
border: solid var(--border-thickness) var(--border-color);
|
||||||
background: linear-gradient(var(--top-color) 0%, var(--top-color2) 10%, var(--main-color) 12%, var(--main-color) 66%, var(--bottom-color) 100%);
|
background: linear-gradient(to bottom, var(--top-color), var(--main-color) 50%, var(--bottom-color) 75%);
|
||||||
/* box-shadow: inset 0px 2px 5px 3px var(--inset-color); */
|
/* box-shadow: inset 0px 2px 5px 3px var(--inset-color); */
|
||||||
/* color: var(--font-color); */
|
/* color: var(--font-color); */
|
||||||
/* HACK: better than contrast-color on critical */
|
/* HACK: better than contrast-color on critical */
|
||||||
@@ -141,7 +142,8 @@ button, .linkbutton, input[type="submit"], input[type="file"]::file-selector-but
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: linear-gradient(var(--top-color) 0%, var(--top-color2) 10%, var(--hover-color) 12%, var(--hover-color) 80%, var(--bottom-color) 100%);
|
/*background: linear-gradient(var(--top-color) 0%, var(--top-color2) 10%, var(--hover-color) 12%, var(--hover-color) 80%, var(--bottom-color) 100%);*/
|
||||||
|
background: linear-gradient(to bottom, var(--top-color), var(--hover-color) 50%, var(--hover-color) 80%, var(--bottom-color) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:is(:active, .active, [aria-selected='true']) {
|
&:is(:active, .active, [aria-selected='true']) {
|
||||||
@@ -297,6 +299,25 @@ a.site-title {
|
|||||||
margin-top: var(--medium-padding);
|
margin-top: var(--medium-padding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.primary-bg {
|
||||||
|
--main-color: var(--bg-color-primary);
|
||||||
|
background-color: var(--bg-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary-bg {
|
||||||
|
--main-color: var(--bg-color-secondary);
|
||||||
|
--rotation: 0deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tertiary-bg {
|
||||||
|
--main-color: var(--bg-color-tertiary);
|
||||||
|
--rotation: 0deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.contrast-bg {
|
||||||
|
--main-color: var(--bg-color-contrast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
@@ -368,25 +389,6 @@ ul.horizontal, ol.horizontal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-bg {
|
|
||||||
--main-color: var(--bg-color-primary);
|
|
||||||
background-color: var(--bg-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-bg {
|
|
||||||
--main-color: var(--bg-color-secondary);
|
|
||||||
--rotation: 0deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tertiary-bg {
|
|
||||||
--main-color: var(--bg-color-tertiary);
|
|
||||||
--rotation: 0deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contrast-bg {
|
|
||||||
--main-color: var(--bg-color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.motd {
|
.motd {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--big-padding);
|
gap: var(--big-padding);
|
||||||
@@ -465,7 +467,7 @@ footer {
|
|||||||
gap: var(--base-padding);
|
gap: var(--base-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-actions {
|
.subheader-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--base-padding);
|
gap: var(--base-padding);
|
||||||
@@ -662,12 +664,12 @@ details {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not([open]) summary::before {
|
&:not([open]) > summary::before {
|
||||||
content: '▶';
|
content: '▶';
|
||||||
padding-inline: var(--base-padding);
|
padding-inline: var(--base-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[open] summary::before {
|
&[open] > summary::before {
|
||||||
content: '▼';
|
content: '▼';
|
||||||
padding-inline: var(--base-padding);
|
padding-inline: var(--base-padding);
|
||||||
}
|
}
|
||||||
@@ -677,6 +679,10 @@ details.separated {
|
|||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details.inner {
|
||||||
|
margin-inline: var(--base-padding);
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-form {
|
.avatar-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--huge-padding);
|
gap: var(--huge-padding);
|
||||||
@@ -724,6 +730,17 @@ details.separated {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
th {
|
||||||
|
width: var(--w, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
td.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* babycode tags */
|
/* babycode tags */
|
||||||
.inline-code {
|
.inline-code {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ async function getHTML(endpoint, options = {}) {
|
|||||||
|
|
||||||
const params = new URLSearchParams(query);
|
const params = new URLSearchParams(query);
|
||||||
const res = await fetch(`${endpoint}?${params}`, options);
|
const res = await fetch(`${endpoint}?${params}`, options);
|
||||||
if (!res.ok) {
|
// if (!res.ok) {
|
||||||
console.error(res);
|
// console.error(res);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return { body: await res.text(), status: res.status };
|
return { body: await res.text(), status: res.status };
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ export async function bookmarkMenuSubmit(ev, _, el) {
|
|||||||
const status = (await getHTML(url, options)).status;
|
const status = (await getHTML(url, options)).status;
|
||||||
|
|
||||||
if (status !== 204) {
|
if (status !== 204) {
|
||||||
b.trigger('bookmarkMenuShowError');
|
b.send({ status: status }, 'bookmarkMenuShowError');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,11 +99,17 @@ export function bookmarkMenuResetSavedButton(_, __, el) {
|
|||||||
el.value = 'Save';
|
el.value = 'Save';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bookmarkMenuShowError(_, __, el) {
|
export function bookmarkMenuShowError(payload, _, el) {
|
||||||
if (el === undefined) {
|
if (el === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload.status === 404) {
|
||||||
|
el.innerText = 'This thread or post no longer exists. Please refresh the page.';
|
||||||
|
} else {
|
||||||
|
el.innerText = 'Something went wrong. Try again later.';
|
||||||
|
}
|
||||||
|
|
||||||
if (el.classList.contains('hidden')) {
|
if (el.classList.contains('hidden')) {
|
||||||
el.classList.remove('hidden');
|
el.classList.remove('hidden');
|
||||||
setTimeout(() => { b.trigger('bookmarkMenuHideError') }, 4000);
|
setTimeout(() => { b.trigger('bookmarkMenuHideError') }, 4000);
|
||||||
|
|||||||
62
data/static/js/bits/bookmarks.js
Normal file
62
data/static/js/bits/bookmarks.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
export const b = {
|
||||||
|
init: 'restoreCollectionDetails restoreThreadDetails restorePostDetails',
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLLECTION_DETAILS_KEY = 'collectionsOpen';
|
||||||
|
const THREAD_DETAILS_KEY = 'threadsOpen';
|
||||||
|
const POST_DETAILS_KEY = 'postsOpen';
|
||||||
|
|
||||||
|
let collectionDetailsData = {};
|
||||||
|
let collectionThreadDetailsData = {};
|
||||||
|
let collectionPostDetailsData = {};
|
||||||
|
|
||||||
|
async function setDetailsData(obj, key, id, isOpen) {
|
||||||
|
obj[id] = isOpen;
|
||||||
|
await b.savePageData(obj, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreCollectionDetails(_, __, el) {
|
||||||
|
collectionDetailsData = await b.loadPageData(COLLECTION_DETAILS_KEY, {});
|
||||||
|
el.open = collectionDetailsData[el.dataset.id] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setCollectionDetails(ev, sender, el) {
|
||||||
|
if (el !== sender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.target !== el.querySelector('summary')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(!el.open);
|
||||||
|
await setDetailsData(collectionDetailsData, COLLECTION_DETAILS_KEY, el.dataset.id, !el.open);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreThreadDetails(_, __, el) {
|
||||||
|
collectionThreadDetailsData = await b.loadPageData(THREAD_DETAILS_KEY, {});
|
||||||
|
el.open = collectionThreadDetailsData[el.dataset.id] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setThreadDetails(ev, sender, el) {
|
||||||
|
if (el !== sender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.target !== el.querySelector('summary')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await setDetailsData(collectionThreadDetailsData, THREAD_DETAILS_KEY, el.dataset.id, !el.open);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restorePostDetails(_, __, el) {
|
||||||
|
collectionPostDetailsData = await b.loadPageData(POST_DETAILS_KEY, {});
|
||||||
|
el.open = collectionPostDetailsData[el.dataset.id] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setPostDetails(ev, sender, el) {
|
||||||
|
if (el !== sender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.target !== el.querySelector('summary')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await setDetailsData(collectionPostDetailsData, POST_DETAILS_KEY, el.dataset.id, !el.open);
|
||||||
|
}
|
||||||
18
data/static/js/bits/collections-editor.js
Normal file
18
data/static/js/bits/collections-editor.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const b = {}
|
||||||
|
|
||||||
|
export function addCollection(ev, sender, el) {
|
||||||
|
el.innerHTML += b.templates.collectionItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCollection(ev, sender, el) {
|
||||||
|
if (!el.contains(sender)) return;
|
||||||
|
b.send({ 'id': el.prop('id') }, 'countDeletedCollection');
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countDeletedCollection(payload, _, el) {
|
||||||
|
if (payload.id === 'new') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.value += `${payload.id};`
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export const b = {
|
export const b = {
|
||||||
init: 'enhance',
|
init: 'enhance enhanceHide',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enhance(_, __, el) {
|
export function enhance(_, __, el) {
|
||||||
@@ -18,3 +18,11 @@ export function enhance(_, __, el) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function enhanceHide(_, __, el) {
|
||||||
|
if (el === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@
|
|||||||
if (listItems.has(node)) return;
|
if (listItems.has(node)) return;
|
||||||
|
|
||||||
const dragger = node.querySelector('.dragger');
|
const dragger = node.querySelector('.dragger');
|
||||||
dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, item) });
|
dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, node) });
|
||||||
dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, item) });
|
dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, node) });
|
||||||
node.addEventListener('dragover', e => { sortableItemDragOver(e, node) });
|
node.addEventListener('dragover', e => { sortableItemDragOver(e, node) });
|
||||||
listItems.add(node);
|
listItems.add(node);
|
||||||
listItemsHandled.set(list, listItems);
|
listItemsHandled.set(list, listItems);
|
||||||
@@ -95,3 +95,18 @@
|
|||||||
});
|
});
|
||||||
listsObs.observe(document.body, { childList: true, subtree: true })
|
listsObs.observe(document.body, { childList: true, subtree: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// babycode editor: press ctrl+enter to submit
|
||||||
|
document.querySelectorAll('.babycode-editor').forEach(ta => {
|
||||||
|
if (ta.form instanceof HTMLFormElement) {
|
||||||
|
ta.addEventListener('keydown', e => {
|
||||||
|
if (e.ctrlKey && e.key === 'Enter') {
|
||||||
|
if (ta.form.reportValidity()) {
|
||||||
|
ta.form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user