Compare commits
12 Commits
86cd55c25b
...
075a9bd498
| Author | SHA1 | Date | |
|---|---|---|---|
|
075a9bd498
|
|||
|
962b833a80
|
|||
|
71b04ca4bd
|
|||
|
831eb32b8a
|
|||
|
10934c557d
|
|||
|
4b70ae1b43
|
|||
|
729b7300e6
|
|||
|
f8101e57c1
|
|||
|
95decd9a56
|
|||
|
b86e049263
|
|||
|
5233f2ef4c
|
|||
|
81183f2c02
|
@@ -60,4 +60,4 @@ $ python -m app.run
|
||||
```
|
||||
|
||||
# icons
|
||||
the icons in the `data/static/misc/` folder are by [Gabriele Malaspina](https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license)
|
||||
the icons in the `app/templates/common/icons.html` file are by [Gabriele Malaspina](https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license)
|
||||
|
||||
@@ -25,12 +25,11 @@ Designers: Paul James Miller
|
||||
|
||||
## ICONCINO
|
||||
|
||||
Affected files: [`data/static/misc/error.svg`](./data/static/misc/error.svg) [`data/static/misc/image.svg`](./data/static/misc/image.svg) [`data/static/misc/info.svg`](./data/static/misc/info.svg) [`data/static/misc/lock.svg`](./data/static/misc/lock.svg) [`data/static/misc/spoiler.svg`](./data/static/misc/spoiler.svg) [`data/static/misc/sticky.svg`](./data/static/misc/sticky.svg) [`data/static/misc/warn.svg`](./data/static/misc/warn.svg)
|
||||
Affected files: [`app/templates/common/icons.html`](./app/templates/common/icons.html)
|
||||
URL: https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license
|
||||
Copyright: Gabriele Malaspina
|
||||
Designers: Gabriele Malaspina
|
||||
License: CC0 1.0/CC BY 4.0
|
||||
CC BY 4.0 compliance: Modified to indicate the URL. Modified size.
|
||||
License: CC0 1.0
|
||||
|
||||
## Forumoji
|
||||
|
||||
@@ -72,3 +71,11 @@ Modified work Copyright (C) 2019-2025 by E. McConville <https://emcconville.com>
|
||||
|
||||
License: MIT
|
||||
Repo: https://github.com/emcconville/wand
|
||||
|
||||
## Bitty
|
||||
|
||||
Affected files: [`data/static/js/vnd/bitty-5.1.0-rc6.min.js`](./data/static/js/vnd/bitty-5.1.0-rc6.min.js)
|
||||
URL: https://bitty.alanwsmith.com/
|
||||
Copyright: `Copyright (c) 2025 Alan Smith - https://www.alanwsmith.com/`
|
||||
License: MIT
|
||||
Repo: https://github.com/alanwsmith/bitty
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import Flask, session
|
||||
from flask import Flask, session, request
|
||||
from dotenv import load_dotenv
|
||||
from .models import Avatars, Users, PostHistory, Posts
|
||||
from .auth import digest
|
||||
@@ -6,7 +6,7 @@ from .routes.users import is_logged_in, get_active_user, get_prefers_theme
|
||||
from .routes.threads import get_post_url
|
||||
from .constants import (
|
||||
PermissionLevel, permission_level_string,
|
||||
InfoboxKind, InfoboxIcons, InfoboxHTMLClass,
|
||||
InfoboxKind, InfoboxHTMLClass,
|
||||
REACTION_EMOJI,
|
||||
)
|
||||
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION
|
||||
@@ -48,21 +48,38 @@ def create_deleted_user():
|
||||
"permission": PermissionLevel.SYSTEM.value,
|
||||
})
|
||||
|
||||
def reparse_posts():
|
||||
def reparse_babycode():
|
||||
print('Re-parsing babycode, this may take a while...')
|
||||
from .db import db
|
||||
post_histories = PostHistory.findall([
|
||||
('markup_language', '=', 'babycode'),
|
||||
('format_version', 'IS NOT', BABYCODE_VERSION)
|
||||
])
|
||||
if len(post_histories) == 0:
|
||||
return
|
||||
print('Re-parsing babycode, this may take a while...')
|
||||
with db.transaction():
|
||||
for ph in post_histories:
|
||||
ph.update({
|
||||
'content': babycode_to_html(ph['original_markup']),
|
||||
'format_version': BABYCODE_VERSION,
|
||||
if len(post_histories) > 0:
|
||||
print('Re-parsing user posts...')
|
||||
with db.transaction():
|
||||
for ph in post_histories:
|
||||
ph.update({
|
||||
'content': babycode_to_html(ph['original_markup']),
|
||||
'format_version': BABYCODE_VERSION,
|
||||
})
|
||||
print('Re-parsing posts done.')
|
||||
|
||||
users_with_sigs = Users.findall([
|
||||
('signature_markup_language', '=', 'babycode'),
|
||||
('signature_format_version', 'IS NOT', BABYCODE_VERSION),
|
||||
('signature_original_markup', 'IS NOT', '')
|
||||
])
|
||||
if len(users_with_sigs) > 0:
|
||||
print('Re-parsing user sigs...')
|
||||
with db.transaction():
|
||||
for user in users_with_sigs:
|
||||
user.update({
|
||||
'signature_rendered': babycode_to_html(user['signature_original_markup']),
|
||||
'signature_format_version': BABYCODE_VERSION,
|
||||
})
|
||||
print(f'Re-parsed {len(users_with_sigs)} user sigs.')
|
||||
|
||||
print('Re-parsing done.')
|
||||
|
||||
def create_app():
|
||||
@@ -105,7 +122,7 @@ def create_app():
|
||||
create_admin()
|
||||
create_deleted_user()
|
||||
|
||||
reparse_posts()
|
||||
reparse_babycode()
|
||||
|
||||
from app.routes.app import bp as app_bp
|
||||
from app.routes.topics import bp as topics_bp
|
||||
@@ -114,6 +131,7 @@ def create_app():
|
||||
from app.routes.mod import bp as mod_bp
|
||||
from app.routes.api import bp as api_bp
|
||||
from app.routes.posts import bp as posts_bp
|
||||
from app.routes.hyperapi import bp as hyperapi_bp
|
||||
app.register_blueprint(app_bp)
|
||||
app.register_blueprint(topics_bp)
|
||||
app.register_blueprint(threads_bp)
|
||||
@@ -121,6 +139,7 @@ def create_app():
|
||||
app.register_blueprint(mod_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
app.register_blueprint(posts_bp)
|
||||
app.register_blueprint(hyperapi_bp)
|
||||
|
||||
app.config['SESSION_COOKIE_SECURE'] = True
|
||||
|
||||
@@ -135,7 +154,6 @@ def create_app():
|
||||
@app.context_processor
|
||||
def inject_constants():
|
||||
return {
|
||||
"InfoboxIcons": InfoboxIcons,
|
||||
"InfoboxHTMLClass": InfoboxHTMLClass,
|
||||
"InfoboxKind": InfoboxKind,
|
||||
"PermissionLevel": PermissionLevel,
|
||||
@@ -184,6 +202,15 @@ def create_app():
|
||||
for id_, text in matches
|
||||
]
|
||||
|
||||
@app.errorhandler(404)
|
||||
def _handle_404(e):
|
||||
if request.path.startswith('/hyperapi/'):
|
||||
return '<h1>not found</h1>', e.code
|
||||
elif request.path.startswith('/api/'):
|
||||
return {'error': 'not found'}, e.code
|
||||
else:
|
||||
return e
|
||||
|
||||
# this only happens at build time but
|
||||
# build time is when updates are done anyway
|
||||
# sooo... /shrug
|
||||
|
||||
@@ -56,13 +56,6 @@ class InfoboxKind(IntEnum):
|
||||
WARN = 2
|
||||
ERROR = 3
|
||||
|
||||
InfoboxIcons = {
|
||||
InfoboxKind.INFO: "/static/misc/info.svg",
|
||||
InfoboxKind.LOCK: "/static/misc/lock.svg",
|
||||
InfoboxKind.WARN: "/static/misc/warn.svg",
|
||||
InfoboxKind.ERROR: "/static/misc/error.svg",
|
||||
}
|
||||
|
||||
InfoboxHTMLClass = {
|
||||
InfoboxKind.INFO: "",
|
||||
InfoboxKind.LOCK: "warn",
|
||||
|
||||
@@ -5,12 +5,31 @@ def migrate_old_avatars():
|
||||
new_path = f"/static{avatar['file_path']}"
|
||||
db.execute('UPDATE avatars SET file_path = ? WHERE id = ?', new_path, avatar['id'])
|
||||
|
||||
def add_signature_format():
|
||||
db.execute('ALTER TABLE "users" ADD COLUMN "signature_markup_language" TEXT NOT NULL DEFAULT "babycode"')
|
||||
db.execute('ALTER TABLE "users" ADD COLUMN "signature_format_version" INTEGER DEFAULT NULL')
|
||||
|
||||
def create_default_bookmark_collections():
|
||||
from .constants import PermissionLevel
|
||||
q = """SELECT users.id FROM users
|
||||
LEFT JOIN bookmark_collections bc ON (users.id = bc.user_id AND bc.is_default = TRUE)
|
||||
WHERE bc.id IS NULL and users.permission IS NOT ?"""
|
||||
user_ids_without_default_collection = db.query(q, PermissionLevel.SYSTEM.value)
|
||||
if len(user_ids_without_default_collection) == 0:
|
||||
return
|
||||
|
||||
from .models import BookmarkCollections
|
||||
for user in user_ids_without_default_collection:
|
||||
BookmarkCollections.create_default(user['id'])
|
||||
|
||||
# format: [str|tuple(str, any...)|callable]
|
||||
MIGRATIONS = [
|
||||
migrate_old_avatars,
|
||||
'DELETE FROM sessions', # delete old lua porom sessions
|
||||
'ALTER TABLE "users" ADD COLUMN "invited_by" INTEGER REFERENCES users(id)', # invitation system
|
||||
'ALTER TABLE "post_history" ADD COLUMN "format_version" INTEGER DEFAULT NULL',
|
||||
add_signature_format,
|
||||
create_default_bookmark_collections,
|
||||
]
|
||||
|
||||
def run_migrations():
|
||||
|
||||
@@ -91,7 +91,7 @@ class Users(Model):
|
||||
|
||||
def can_invite(self):
|
||||
if not current_app.config['DISABLE_SIGNUP']:
|
||||
return True
|
||||
return False
|
||||
|
||||
if current_app.config['MODS_CAN_INVITE'] and self.is_mod():
|
||||
return True
|
||||
@@ -101,6 +101,11 @@ class Users(Model):
|
||||
|
||||
return False
|
||||
|
||||
def get_bookmark_collections(self):
|
||||
q = 'SELECT id FROM bookmark_collections WHERE user_id = ? ORDER BY sort_order ASC'
|
||||
res = db.query(q, self.id)
|
||||
return [BookmarkCollections.find({'id': bc['id']}) for bc in res]
|
||||
|
||||
|
||||
class Topics(Model):
|
||||
table = "topics"
|
||||
@@ -225,7 +230,7 @@ class Threads(Model):
|
||||
class Posts(Model):
|
||||
FULL_POSTS_QUERY = """
|
||||
SELECT
|
||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered, threads.slug AS thread_slug, threads.is_locked AS thread_is_locked
|
||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered, threads.slug AS thread_slug, threads.is_locked AS thread_is_locked, threads.title AS thread_title
|
||||
FROM
|
||||
posts
|
||||
JOIN
|
||||
@@ -239,6 +244,10 @@ class Posts(Model):
|
||||
|
||||
table = "posts"
|
||||
|
||||
def get_full_post_view(self):
|
||||
q = f'{self.FULL_POSTS_QUERY} WHERE posts.id = ?'
|
||||
return db.fetch_one(q, self.id)
|
||||
|
||||
class PostHistory(Model):
|
||||
table = "post_history"
|
||||
|
||||
@@ -317,3 +326,71 @@ class PasswordResetLinks(Model):
|
||||
|
||||
class InviteKeys(Model):
|
||||
table = 'invite_keys'
|
||||
|
||||
|
||||
class BookmarkCollections(Model):
|
||||
table = 'bookmark_collections'
|
||||
|
||||
@classmethod
|
||||
def create_default(cls, user_id):
|
||||
q = """INSERT INTO bookmark_collections (user_id, name, is_default, sort_order)
|
||||
VALUES (?, "Bookmarks", 1, 0) RETURNING id
|
||||
"""
|
||||
res = db.fetch_one(q, user_id)
|
||||
|
||||
def has_posts(self):
|
||||
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ?) as e'
|
||||
res = db.fetch_one(q, self.id)['e']
|
||||
return int(res) == 1
|
||||
|
||||
def has_threads(self):
|
||||
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_threads WHERE collection_id = ?) as e'
|
||||
res = db.fetch_one(q, self.id)['e']
|
||||
return int(res) == 1
|
||||
|
||||
def is_empty(self):
|
||||
return not (self.has_posts() or self.has_threads())
|
||||
|
||||
def get_threads(self):
|
||||
q = 'SELECT id FROM bookmarked_threads WHERE collection_id = ?'
|
||||
res = db.query(q, self.id)
|
||||
return [BookmarkedThreads.find({'id': bt['id']}) for bt in res]
|
||||
|
||||
def get_posts(self):
|
||||
q = 'SELECT id FROM bookmarked_posts WHERE collection_id = ?'
|
||||
res = db.query(q, self.id)
|
||||
return [BookmarkedPosts.find({'id': bt['id']}) for bt in res]
|
||||
|
||||
def get_threads_count(self):
|
||||
q = 'SELECT COUNT(*) as tc FROM bookmarked_threads WHERE collection_id = ?'
|
||||
res = db.fetch_one(q, self.id)
|
||||
return int(res['tc'])
|
||||
|
||||
def get_posts_count(self):
|
||||
q = 'SELECT COUNT(*) as pc FROM bookmarked_posts WHERE collection_id = ?'
|
||||
res = db.fetch_one(q, self.id)
|
||||
return int(res['pc'])
|
||||
|
||||
def has_thread(self, thread_id):
|
||||
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_threads WHERE collection_id = ? AND thread_id = ?) as e'
|
||||
res = db.fetch_one(q, self.id, int(thread_id))['e']
|
||||
return int(res) == 1
|
||||
|
||||
def has_post(self, post_id):
|
||||
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ? AND post_id = ?) as e'
|
||||
res = db.fetch_one(q, self.id, int(post_id))['e']
|
||||
return int(res) == 1
|
||||
|
||||
|
||||
class BookmarkedPosts(Model):
|
||||
table = 'bookmarked_posts'
|
||||
|
||||
def get_post(self):
|
||||
return Posts.find({'id': self.post_id})
|
||||
|
||||
|
||||
class BookmarkedThreads(Model):
|
||||
table = 'bookmarked_threads'
|
||||
|
||||
def get_thread(self):
|
||||
return Threads.find({'id': self.thread_id})
|
||||
|
||||
@@ -2,7 +2,7 @@ from flask import Blueprint, request, url_for
|
||||
from ..lib.babycode import babycode_to_html
|
||||
from ..constants import REACTION_EMOJI
|
||||
from .users import is_logged_in, get_active_user
|
||||
from ..models import APIRateLimits, Threads, Reactions
|
||||
from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections, BookmarkedThreads, BookmarkedPosts
|
||||
from ..db import db
|
||||
|
||||
bp = Blueprint("api", __name__, url_prefix="/api/")
|
||||
@@ -96,3 +96,119 @@ def remove_reaction(post_id):
|
||||
reaction.delete()
|
||||
|
||||
return {'status': 'removed'}
|
||||
|
||||
@bp.post('/manage-bookmark-collections/<user_id>')
|
||||
def manage_bookmark_collections(user_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||
|
||||
target_user = Users.find({'id': user_id})
|
||||
if target_user.id != get_active_user().id:
|
||||
return {'error': 'forbidden', 'error_code': 403}, 403
|
||||
|
||||
if target_user.is_guest():
|
||||
return {'error': 'forbidden', 'error_code': 403}, 403
|
||||
|
||||
collections_data = request.json
|
||||
for idx, coll_data in enumerate(collections_data.get('collections')):
|
||||
if coll_data['is_new']:
|
||||
collection = BookmarkCollections.create({
|
||||
'name': coll_data['name'],
|
||||
'user_id': target_user.id,
|
||||
'sort_order': idx,
|
||||
})
|
||||
else:
|
||||
collection = BookmarkCollections.find({'id': coll_data['id']})
|
||||
if not collection:
|
||||
continue
|
||||
|
||||
update = {'name': coll_data['name']}
|
||||
if not collection.is_default:
|
||||
update['sort_order'] = idx
|
||||
collection.update(update)
|
||||
|
||||
for removed_id in collections_data.get('removed_collections'):
|
||||
collection = BookmarkCollections.find({'id': removed_id})
|
||||
if not collection:
|
||||
continue
|
||||
|
||||
if collection.is_default:
|
||||
continue
|
||||
|
||||
collection.delete()
|
||||
|
||||
|
||||
return {'status': 'ok'}, 200
|
||||
|
||||
|
||||
@bp.post('/bookmark-post/<post_id>')
|
||||
def bookmark_post(post_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||
|
||||
operation = request.json.get('operation')
|
||||
if operation == 'remove' and request.json.get('collection_id', '') == '':
|
||||
return {'status': 'not modified'}, 304
|
||||
collection_id = int(request.json.get('collection_id'))
|
||||
post_id = int(post_id)
|
||||
memo = request.json.get('memo', '')
|
||||
|
||||
if operation == 'move':
|
||||
bm = BookmarkedPosts.find({'post_id': post_id})
|
||||
if not bm:
|
||||
BookmarkedPosts.create({
|
||||
'post_id': post_id,
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
else:
|
||||
bm.update({
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
elif operation == 'remove':
|
||||
bm = BookmarkedPosts.find({'post_id': post_id})
|
||||
if bm:
|
||||
bm.delete()
|
||||
else:
|
||||
return {'error': 'bad request'}, 400
|
||||
|
||||
return {'status': 'ok'}, 200
|
||||
|
||||
|
||||
@bp.post('/bookmark-thread/<thread_id>')
|
||||
def bookmark_thread(thread_id):
|
||||
if not is_logged_in():
|
||||
return {'error': 'not authorized', 'error_code': 401}, 401
|
||||
|
||||
operation = request.json.get('operation')
|
||||
if operation == 'remove' and request.json.get('collection_id', '') == '':
|
||||
return {'status': 'not modified'}, 304
|
||||
collection_id = int(request.json.get('collection_id'))
|
||||
thread_id = int(thread_id)
|
||||
memo = request.json.get('memo', '')
|
||||
|
||||
if operation == 'move':
|
||||
bm = BookmarkedThreads.find({'thread_id': thread_id})
|
||||
if not bm:
|
||||
BookmarkedThreads.create({
|
||||
'thread_id': thread_id,
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
else:
|
||||
bm.update({
|
||||
'collection_id': collection_id,
|
||||
'note': memo,
|
||||
})
|
||||
elif operation == 'remove':
|
||||
bm = BookmarkedThreads.find({
|
||||
'thread_id': thread_id,
|
||||
'note': memo,
|
||||
})
|
||||
if bm:
|
||||
bm.delete()
|
||||
else:
|
||||
return {'error': 'bad request'}, 400
|
||||
|
||||
return {'status': 'ok'}, 200
|
||||
|
||||
53
app/routes/hyperapi.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from flask import Blueprint, render_template, abort, request
|
||||
from .users import get_active_user, is_logged_in
|
||||
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads
|
||||
from functools import wraps
|
||||
|
||||
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
|
||||
|
||||
def login_required(view_func):
|
||||
@wraps(view_func)
|
||||
def dec(*args, **kwargs):
|
||||
if not is_logged_in():
|
||||
abort(403)
|
||||
return view_func(*args, **kwargs)
|
||||
return dec
|
||||
|
||||
def account_required(view_func):
|
||||
@wraps(view_func)
|
||||
def dec(*args, **kwargs):
|
||||
if get_active_user().is_guest():
|
||||
abort(403)
|
||||
return view_func(*args, **kwargs)
|
||||
return dec
|
||||
|
||||
@bp.errorhandler(403)
|
||||
def handle_403(e):
|
||||
return "<h1>forbidden</h1>", 403
|
||||
|
||||
|
||||
@bp.get('bookmarks-dropdown/<bookmark_type>')
|
||||
@login_required
|
||||
@account_required
|
||||
def bookmarks_dropdown(bookmark_type):
|
||||
collections = BookmarkCollections.findall({'user_id': get_active_user().id})
|
||||
concept_id = request.args.get('id')
|
||||
require_reload = bool(int(request.args.get('require_reload', default=0)))
|
||||
if bookmark_type.lower() == 'thread':
|
||||
selected = next(filter(lambda bc: bc.has_thread(concept_id), collections), None)
|
||||
elif bookmark_type.lower() == 'post':
|
||||
selected = next(filter(lambda bc: bc.has_post(concept_id), collections), None)
|
||||
else:
|
||||
abort(400)
|
||||
return
|
||||
|
||||
if selected:
|
||||
if bookmark_type.lower() == 'thread':
|
||||
memo = BookmarkedThreads.find({'collection_id': selected.id, 'thread_id': int(concept_id)}).note
|
||||
else:
|
||||
memo = BookmarkedPosts.find({'collection_id': selected.id, 'post_id': int(concept_id)}).note
|
||||
else:
|
||||
memo = ''
|
||||
|
||||
|
||||
return render_template('components/bookmarks_dropdown.html', collections=collections, id=concept_id, selected=selected, type=bookmark_type, memo=memo, require_reload=require_reload)
|
||||
@@ -3,8 +3,8 @@ from flask import (
|
||||
)
|
||||
from functools import wraps
|
||||
from ..db import db
|
||||
from ..lib.babycode import babycode_to_html
|
||||
from ..models import Users, Sessions, Subscriptions, Avatars, PasswordResetLinks, InviteKeys
|
||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||
from ..models import Users, Sessions, Subscriptions, Avatars, PasswordResetLinks, InviteKeys, BookmarkCollections, BookmarkedThreads
|
||||
from ..constants import InfoboxKind, PermissionLevel
|
||||
from ..auth import digest, verify
|
||||
from wand.image import Image
|
||||
@@ -260,6 +260,8 @@ def sign_up_post():
|
||||
"permission": PermissionLevel.GUEST.value,
|
||||
})
|
||||
|
||||
BookmarkCollections.create_default(new_user.id)
|
||||
|
||||
if current_app.config['DISABLE_SIGNUP']:
|
||||
invite_key = InviteKeys.find({'key': key})
|
||||
new_user.update({
|
||||
@@ -307,14 +309,19 @@ def settings_form(username):
|
||||
if topic_sort_by == 'activity' or topic_sort_by == 'thread':
|
||||
sort_by = session['sort_by'] = topic_sort_by
|
||||
status = request.form.get('status', default="")[:100]
|
||||
original_sig = request.form.get('signature', default='')
|
||||
rendered_sig = babycode_to_html(original_sig)
|
||||
original_sig = request.form.get('signature', default='').strip()
|
||||
if original_sig:
|
||||
rendered_sig = babycode_to_html(original_sig)
|
||||
else:
|
||||
rendered_sig = ''
|
||||
session['subscribe_by_default'] = request.form.get('subscribe_by_default', default='off') == 'on'
|
||||
|
||||
user.update({
|
||||
'status': status,
|
||||
'signature_original_markup': original_sig,
|
||||
'signature_rendered': rendered_sig,
|
||||
'signature_format_version': BABYCODE_VERSION,
|
||||
'signature_markup_language': 'babycode',
|
||||
})
|
||||
flash('Settings updated.', InfoboxKind.INFO)
|
||||
return redirect(url_for('.settings', username=user.username))
|
||||
@@ -683,3 +690,26 @@ def revoke_invite_link(username):
|
||||
invite.delete()
|
||||
|
||||
return redirect(url_for('.invite_links', username=target_user.username))
|
||||
|
||||
|
||||
@bp.get('/<username>/bookmarks')
|
||||
@login_required
|
||||
def bookmarks(username):
|
||||
target_user = Users.find({'username': username})
|
||||
if not target_user or target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.bookmarks', username=get_active_user().username))
|
||||
|
||||
collections = target_user.get_bookmark_collections()
|
||||
|
||||
return render_template('users/bookmarks.html', collections=collections)
|
||||
|
||||
|
||||
@bp.get('/<username>/bookmarks/collections')
|
||||
@login_required
|
||||
def bookmark_collections(username):
|
||||
target_user = Users.find({'username': username})
|
||||
if not target_user or target_user.username != get_active_user().username:
|
||||
return redirect(url_for('.bookmark_collections', username=get_active_user().username))
|
||||
|
||||
collections = target_user.get_bookmark_collections()
|
||||
return render_template('users/bookmark_collections.html', collections=collections)
|
||||
|
||||
@@ -96,6 +96,30 @@ SCHEMA = [
|
||||
"key" TEXT NOT NULL UNIQUE
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "bookmark_collections" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"user_id" REFERENCES users(id) ON DELETE CASCADE,
|
||||
"name" TEXT NOT NULL,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "bookmarked_posts" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"collection_id" REFERENCES bookmark_collections(id) ON DELETE CASCADE,
|
||||
"post_id" REFERENCES posts(id) ON DELETE CASCADE,
|
||||
"note" TEXT,
|
||||
UNIQUE(collection_id, post_id)
|
||||
)""",
|
||||
|
||||
"""CREATE TABLE IF NOT EXISTS "bookmarked_threads" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||
"collection_id" REFERENCES bookmark_collections(id) ON DELETE CASCADE,
|
||||
"thread_id" REFERENCES threads(id) ON DELETE CASCADE,
|
||||
"note" TEXT,
|
||||
UNIQUE(collection_id, thread_id)
|
||||
)""",
|
||||
|
||||
# INDEXES
|
||||
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",
|
||||
@@ -110,6 +134,15 @@ SCHEMA = [
|
||||
|
||||
"CREATE INDEX IF NOT EXISTS reaction_post_text ON reactions(post_id, reaction_text)",
|
||||
"CREATE INDEX IF NOT EXISTS reaction_user_post_text ON reactions(user_id, post_id, reaction_text)",
|
||||
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmark_collections_user_id ON bookmark_collections(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmark_collections_user_default ON bookmark_collections(user_id, is_default) WHERE is_default = 1",
|
||||
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_posts_collection ON bookmarked_posts(collection_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_posts_post ON bookmarked_posts(post_id)",
|
||||
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_collection ON bookmarked_threads(collection_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_thread ON bookmarked_threads(thread_id)",
|
||||
]
|
||||
|
||||
def create():
|
||||
|
||||
@@ -10,20 +10,23 @@
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<script src="/static/js/vnd/bitty-5.1.0-rc6.min.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
{% include 'common/topnav.html' %}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
{{ infobox(message, category) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
<footer class="darkbg">
|
||||
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
|
||||
</footer>
|
||||
<bitty-5-1 data-connect="/static/js/bitties/pyrom-bitty.js">
|
||||
{% include 'common/topnav.html' %}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
{{ infobox(message, category) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
<footer class="darkbg">
|
||||
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
|
||||
</footer>
|
||||
</bitty-5-1>
|
||||
<script src="{{ "/static/js/ui.js" | cachebust }}"></script>
|
||||
<script src="{{ "/static/js/date-fmt.js" | cachebust }}"></script>
|
||||
</body>
|
||||
|
||||
49
app/templates/common/icons.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{# https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license #}
|
||||
|
||||
{% macro icn_bookmark(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 6C6 4.89543 6.89543 4 8 4H16C17.1046 4 18 4.89543 18 6V18.7268C18 19.5969 16.9657 20.0519 16.3243 19.4639L12 15.5L7.67573 19.4639C7.03432 20.0519 6 19.5969 6 18.7268V6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_error(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.364 5.63604C19.9926 7.26472 21 9.51472 21 12C21 16.9706 16.9706 21 12 21C9.51472 21 7.26472 19.9926 5.63604 18.364M18.364 5.63604C16.7353 4.00736 14.4853 3 12 3C7.02944 3 3 7.02944 3 12C3 14.4853 4.00736 16.7353 5.63604 18.364M18.364 5.63604L5.63604 18.364" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_info(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8V8.5M12 12V16M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_lock(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 14V16M8 9V6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6V9M7 21H17C18.1046 21 19 20.1046 19 19V11C19 9.89543 18.1046 9 17 9H7C5.89543 9 5 9.89543 5 11V19C5 20.1046 5.89543 21 7 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_warn(width=60) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15H12.01M12 12V9M4.98207 19H19.0179C20.5615 19 21.5233 17.3256 20.7455 15.9923L13.7276 3.96153C12.9558 2.63852 11.0442 2.63852 10.2724 3.96153L3.25452 15.9923C2.47675 17.3256 3.43849 19 4.98207 19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_image(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 17L7.58959 13.7694C8.38025 13.0578 9.58958 13.0896 10.3417 13.8417L11.5 15L15.0858 11.4142C15.8668 10.6332 17.1332 10.6332 17.9142 11.4142L20 13.5M11 9C11 9.55228 10.5523 10 10 10C9.44772 10 9 9.55228 9 9C9 8.44772 9.44772 8 10 8C10.5523 8 11 8.44772 11 9ZM6 20H18C19.1046 20 20 19.1046 20 18V6C20 4.89543 19.1046 4 18 4H6C4.89543 4 4 4.89543 4 6V18C4 19.1046 4.89543 20 6 20Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_spoiler(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4L9.87868 9.87868M20 20L14.1213 14.1213M9.87868 9.87868C9.33579 10.4216 9 11.1716 9 12C9 13.6569 10.3431 15 12 15C12.8284 15 13.5784 14.6642 14.1213 14.1213M9.87868 9.87868L14.1213 14.1213M6.76821 6.76821C4.72843 8.09899 2.96378 10.026 2 11.9998C3.74646 15.5764 8.12201 19 11.9998 19C13.7376 19 15.5753 18.3124 17.2317 17.2317M9.76138 5.34717C10.5114 5.12316 11.2649 5 12.0005 5C15.8782 5 20.2531 8.42398 22 12.0002C21.448 13.1302 20.6336 14.2449 19.6554 15.2412" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icn_sticky(width=24) -%}
|
||||
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
@@ -1,3 +1,4 @@
|
||||
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark %}
|
||||
{% macro pager(current_page, page_count) %}
|
||||
{% set left_start = [1, current_page - 5] | max %}
|
||||
{% set right_end = [page_count, current_page + 5] | min %}
|
||||
@@ -27,20 +28,36 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro bookmark_button(type, id, message = "Bookmark…", require_reload=false) %}
|
||||
{% set bid = type[0] + id | string %}
|
||||
<div class="bookmark-dropdown">
|
||||
<button type="button" class="contain-svg inline icon" data-bookmark-type="{{type}}" data-send="showBookmarkMenu" data-concept-id="{{id}}" data-bookmark-id="{{bid}}">{{ icn_bookmark(20) }}{{ message | safe }}</button>
|
||||
<div class="bookmark-dropdown-inner" data-receive="showBookmarkMenu" data-bookmark-id="{{bid}}" data-require-reload={{require_reload | int}}></div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro infobox(message, kind=InfoboxKind.INFO) %}
|
||||
<div class="{{ "infobox " + InfoboxHTMLClass[kind] }}">
|
||||
<span>
|
||||
<div class="infobox-icon-container">
|
||||
<img src="{{ InfoboxIcons[kind] }}">
|
||||
{%- if kind == InfoboxKind.INFO -%}
|
||||
{{- icn_info() -}}
|
||||
{%- elif kind == InfoboxKind.LOCK -%}
|
||||
{{- icn_lock() -}}
|
||||
{%- elif kind == InfoboxKind.WARN -%}
|
||||
{{- icn_warn() -}}
|
||||
{%- elif kind == InfoboxKind.ERROR -%}
|
||||
{{- icn_error() -}}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{{ message }}
|
||||
</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timestamp(unix_ts) %}
|
||||
{% macro timestamp(unix_ts) -%}
|
||||
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
|
||||
{% endmacro %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="") %}
|
||||
<div class="babycode-editor-container">
|
||||
@@ -56,10 +73,10 @@
|
||||
<button class="babycode-button" type=button id="post-editor-underline" title="Insert Underline"><u>U</u></button>
|
||||
<button class="babycode-button" type=button id="post-editor-url" title="Insert Link"><code>://</code></button>
|
||||
<button class="babycode-button" type=button id="post-editor-code" title="Insert Code block"><code></></code></button>
|
||||
<button class="babycode-button contain-svg full" type=button id="post-editor-img" title="Insert Image"><img src="/static/misc/image.svg"></button>
|
||||
<button class="babycode-button contain-svg" type=button id="post-editor-img" title="Insert Image">{{ icn_image() }}</button>
|
||||
<button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list">1.</button>
|
||||
<button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list">•</button>
|
||||
<button class="babycode-button contain-svg full" type=button id="post-editor-spoiler" title="Insert spoiler"><img src="/static/misc/spoiler.svg"></button>
|
||||
<button class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler">{{ icn_spoiler() }}</button>
|
||||
</span>
|
||||
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }}>{{ prefill }}</textarea>
|
||||
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
|
||||
@@ -91,7 +108,13 @@
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None, no_reply = false, Reactions = none) %}
|
||||
{% macro full_post(
|
||||
post, render_sig = True, is_latest = False,
|
||||
editing = False, active_user = None, no_reply = false,
|
||||
Reactions = none, show_thread_title = false,
|
||||
show_bookmark = false, memo = None, bookmark_message = "Bookmark…",
|
||||
reload_after_bookmark = false
|
||||
) %}
|
||||
{% set postclass = "post" %}
|
||||
{% if editing %}
|
||||
{% set postclass = postclass + " editing" %}
|
||||
@@ -112,6 +135,14 @@
|
||||
|
||||
<div class="post-content-container" {{ "id=latest-post" if is_latest else "" }}>
|
||||
<div class="post-info">
|
||||
<span>
|
||||
{% if memo -%}
|
||||
Memo: <i>{{ memo }}</i> •
|
||||
{%- endif %}
|
||||
{% if show_thread_title %}
|
||||
<a href="{{ url_for('threads.thread', slug=post.thread_slug) }}">Thread: {{ post.thread_title }}</a>
|
||||
•
|
||||
{% endif %}
|
||||
<a href="{{ post_permalink }}" title="Permalink"><i>
|
||||
{% if (post['edited_at'] | int) > (post['created_at'] | int) %}
|
||||
Edited on {{ timestamp(post['edited_at']) }}
|
||||
@@ -119,7 +150,8 @@
|
||||
Posted on {{ timestamp(post['edited_at']) }}
|
||||
{% endif %}
|
||||
</i></a>
|
||||
<span>
|
||||
</span>
|
||||
<span class="thread-actions">
|
||||
{% set show_edit = false %}
|
||||
{% if active_user %}
|
||||
{% set show_edit = (active_user.id | string) == (post['user_id'] | string) and (not post['thread_is_locked'] or active_user.is_mod()) and not no_reply %}
|
||||
@@ -157,6 +189,10 @@
|
||||
{% if show_delete %}
|
||||
<button class="critical post-delete-button" value="{{ post['id'] }}">Delete</button>
|
||||
{% endif %}
|
||||
|
||||
{% if show_bookmark %}
|
||||
{{ bookmark_button(type="post", id=post.id, message=bookmark_message, require_reload=reload_after_bookmark)}}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
•
|
||||
<a href="{{ url_for('users.invite_links', username=user.username )}}">Invite to {{ config.SITE_NAME }}</a>
|
||||
{% endif %}
|
||||
{% if not user.is_guest() %}
|
||||
•
|
||||
<a href="{{ url_for('users.bookmarks', username=user.username) }}">Bookmarks</a>
|
||||
{% endif %}
|
||||
{% if user.is_mod() %}
|
||||
•
|
||||
<a href="{{ url_for("mod.user_list") }}">User list</a>
|
||||
|
||||
23
app/templates/components/bookmarks_dropdown.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% set bookmark_url = None %}
|
||||
{% if type == 'post' %}
|
||||
{% set bookmark_url = url_for('api.bookmark_post', post_id=id) %}
|
||||
{% else %}
|
||||
{% set bookmark_url = url_for('api.bookmark_thread', thread_id=id) %}
|
||||
{% endif %}
|
||||
<div class="bookmarks-dropdown" data-bookmark-type="{{type}}" data-receive="saveBookmarks" data-bookmark-endpoint="{{bookmark_url}}" data-originally-contained-in="{{ selected.id if selected else ""}}" data-require-reload={{require_reload | int}}>
|
||||
<div class="bookmarks-dropdown-header">
|
||||
<span>Bookmark collections</span>
|
||||
{% if not require_reload %}
|
||||
<a href="{{ url_for('users.bookmarks', username=get_active_user().username) }}">View bookmarks</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="bookmark-dropdown-items-container">
|
||||
{% for collection in collections %}
|
||||
<div class="bookmark-dropdown-item {{ "selected" if selected and (selected.id | int) == (collection.id | int) else ""}}" data-send="selectBookmarkCollection" data-receive="selectBookmarkCollection" data-collection-id="{{collection.id}}">{{collection.name}} ({{ collection.get_posts_count() }}p, {{ collection.get_threads_count() }}t)</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span>
|
||||
<input type="text" placeholder="Memo" class="bookmark-memo-input" value="{{memo}}"></input>
|
||||
<button type="button" data-send="saveBookmarks">Save</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1,12 +1,15 @@
|
||||
{% from 'common/macros.html' import pager, babycode_editor_form, full_post %}
|
||||
{% from 'common/macros.html' import pager, babycode_editor_form, full_post, bookmark_button %}
|
||||
{% from 'common/icons.html' import icn_bookmark %}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ thread.title }}{% endblock %}
|
||||
{% block content %}
|
||||
{% set can_post = false %}
|
||||
{% set can_lock = false %}
|
||||
{% set can_subscribe = false %}
|
||||
{% set can_bookmark = false %}
|
||||
{% if active_user %}
|
||||
{% set can_subscribe = true %}
|
||||
{% set can_bookmark = not active_user.is_guest() %}
|
||||
{% set can_post = (not thread.is_locked and not active_user.is_guest()) or active_user.is_mod() %}
|
||||
{% set can_lock = ((active_user.id | int) == (thread.user_id | int)) or active_user.is_mod() %}
|
||||
{% endif %}
|
||||
@@ -18,7 +21,7 @@
|
||||
• <i>stickied, so it's probably important</i>
|
||||
{% endif %}
|
||||
</span>
|
||||
<div>
|
||||
<div class="thread-actions">
|
||||
{% if can_subscribe %}
|
||||
<form class="modform" action="{{ url_for('threads.subscribe', slug=thread.slug) }}" method="post">
|
||||
<input type='hidden' name='last_visible_post' value='{{posts[-1].id}}'>
|
||||
@@ -26,6 +29,9 @@
|
||||
<input type='submit' value='{{ 'Unsubscribe' if is_subscribed else 'Subscribe' }}'>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if can_bookmark %}
|
||||
{{ bookmark_button(type="thread", id=thread.id) }}
|
||||
{% endif %}
|
||||
{% if can_lock %}
|
||||
<form class="modform" action="{{ url_for("threads.lock", slug=thread.slug) }}" method="post">
|
||||
<input type=hidden name='target_op' value="{{ (not thread.is_locked) | int }}">
|
||||
@@ -50,7 +56,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
{% for post in posts %}
|
||||
{{ full_post(post = post, active_user = active_user, is_latest = loop.index == (posts | length), Reactions = Reactions) }}
|
||||
{{ full_post(post = post, active_user = active_user, is_latest = loop.index == (posts | length), Reactions = Reactions, show_bookmark = can_bookmark) }}
|
||||
{% endfor %}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% from 'common/macros.html' import pager, timestamp %}
|
||||
{% from 'common/macros.html' import pager, timestamp, bookmark_button %}
|
||||
{% from 'common/icons.html' import icn_bookmark, icn_lock, icn_sticky %}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}browsing topic {{ topic['name'] }}{% endblock %}
|
||||
{% block content %}
|
||||
@@ -33,18 +34,28 @@
|
||||
<div class="thread">
|
||||
<div class="thread-sticky-container contain-svg">
|
||||
{% if thread['is_stickied'] %}
|
||||
<img src="/static/misc/sticky.svg">
|
||||
{{ icn_sticky(48) }}
|
||||
<i>Stickied</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="thread-info-container">
|
||||
<span>
|
||||
<span class="thread-title"><a href="{{ url_for("threads.thread", slug=thread['slug']) }}">{{thread['title']}}</a></span>
|
||||
{% if thread['id'] in subscriptions %}
|
||||
({{ subscriptions[thread['id']] }} unread)
|
||||
{% endif %}
|
||||
•
|
||||
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by'] }}</a> on {{ timestamp(thread['created_at'])}}
|
||||
<span class="thread-info-header">
|
||||
<span>
|
||||
<span class="thread-title"><a href="{{ url_for("threads.thread", slug=thread['slug']) }}">{{thread['title']}}</a>
|
||||
{% if thread['id'] in subscriptions %}
|
||||
({{ subscriptions[thread['id']] }} unread)
|
||||
{% endif %}
|
||||
</span>
|
||||
•
|
||||
<span>
|
||||
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by'] }}</a> on {{ timestamp(thread['created_at']) }}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
{% if active_user and not active_user.is_guest() -%}
|
||||
{{ bookmark_button(type="thread", id=thread.id) }}
|
||||
{%- endif %}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
Latest post by <a href="{{ url_for("users.page", username=thread['latest_post_username']) }}">{{ thread['latest_post_username'] }}</a>
|
||||
@@ -56,7 +67,7 @@
|
||||
</div>
|
||||
<div class="thread-locked-container contain-svg">
|
||||
{% if thread['is_locked'] %}
|
||||
<img src="/static/misc/lock.svg">
|
||||
{{ icn_lock(48) }}
|
||||
<i>Locked</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% from 'common/icons.html' import icn_lock %}
|
||||
{% from 'common/macros.html' import timestamp %}
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
@@ -33,7 +34,7 @@
|
||||
</div>
|
||||
<div class="topic-locked-container contain-svg">
|
||||
{% if topic['is_locked'] %}
|
||||
<img src="/static/misc/lock.svg"></img>
|
||||
{{ icn_lock(48) }}
|
||||
<i>Locked</i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
35
app/templates/users/bookmark_collections.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}managing bookmark collections{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg settings-container">
|
||||
<h1>Manage bookmark collections</h1>
|
||||
<p>Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.</p>
|
||||
<div>
|
||||
<button type="button" id="add-collection-button">Add new collection</button>
|
||||
<div id="collections-container">
|
||||
{% for collection in collections | sort(attribute='sort_order') %}
|
||||
<div class="draggable-collection {{ "default" if collection.is_default else ""}}"
|
||||
{% 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>
|
||||
{% if collection.is_default %}
|
||||
<i>Default collection</i>
|
||||
{% else %}
|
||||
<button type="button" class="delete-button critical">Delete</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" id="save-button" data-submit-href="{{ url_for('api.manage_bookmark_collections', user_id=active_user.id) }}">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ "/static/js/manage-bookmark-collections.js" | cachebust }}"></script>
|
||||
{% endblock %}
|
||||
52
app/templates/users/bookmarks.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% from "common/macros.html" import accordion, full_post, bookmark_button %}
|
||||
{% from "common/icons.html" import icn_bookmark %}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}bookmarks{% endblock %}
|
||||
{% block content %}
|
||||
<div class="darkbg inbox-container">
|
||||
<a class="linkbutton" href="{{ url_for('users.bookmark_collections', username=get_active_user().username) }}">Manage collections</a>
|
||||
{% for collection in collections | sort(attribute='sort_order') %}
|
||||
{% call(section) accordion(disabled=collection.is_empty()) %}
|
||||
{% if section == 'header' %}
|
||||
<h1 class="thread-title">{{ collection.name }}</h1>{{" (no bookmarks)" if collection.is_empty() else ""}}
|
||||
{% else %}
|
||||
{% call(inner_section) accordion(disabled=not collection.has_threads()) %}
|
||||
{% if inner_section == 'header' %}
|
||||
Threads{{" (no bookmarks)" if not collection.has_threads() else ""}}
|
||||
{% else %}
|
||||
<table class="colorful-table">
|
||||
<thead>
|
||||
<th>Title</th>
|
||||
<th>Memo</th>
|
||||
<th class="small">Manage</th>
|
||||
</thead>
|
||||
{% for thread in collection.get_threads() %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('threads.thread', slug=thread.get_thread().slug) }}">{{ thread.get_thread().title }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<i>{{ thread.note }}</i>
|
||||
</td>
|
||||
<td>
|
||||
{{ bookmark_button(type='thread', id=thread.thread_id, message='Manage…', require_reload=true) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% call(inner_section) accordion(disabled=not collection.has_posts()) %}
|
||||
{% if inner_section == 'header' %}
|
||||
Posts{{" (no bookmarks)" if not collection.has_posts() else ""}}
|
||||
{% else %}
|
||||
{% for post in collection.get_posts() %}
|
||||
{{ full_post(post.get_post().get_full_post_view(), no_reply=false, render_sig=false, show_thread_title=true, show_bookmark=true, memo=post.note, bookmark_message="Manage…", reload_after_bookmark=true) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -99,6 +99,12 @@ a:visited {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.thread-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
@@ -689,6 +695,10 @@ button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.icon, input[type=submit].icon, .linkbutton.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: red;
|
||||
color: white !important;
|
||||
@@ -706,6 +716,10 @@ button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critic
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.critical.icon, input[type=submit].critical.icon, .linkbutton.critical.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black !important;
|
||||
@@ -723,6 +737,10 @@ button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.warn.icon, input[type=submit].warn.icon, .linkbutton.warn.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
@@ -741,6 +759,10 @@ input[type=file]::file-selector-button.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
input[type=file]::file-selector-button.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
input[type=file]::file-selector-button {
|
||||
margin: 10px;
|
||||
}
|
||||
@@ -766,6 +788,10 @@ p {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.pagebutton.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
.pagebutton {
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
@@ -871,13 +897,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
.contain-svg.full > svg, .contain-svg.full > img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.contain-svg.inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.post-img-container {
|
||||
@@ -910,6 +931,17 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
}
|
||||
|
||||
.thread-info-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -973,7 +1005,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.colorful-table tr th {
|
||||
@@ -1028,6 +1059,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
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 {
|
||||
background-color: rgb(217.26, 220.38, 213.42);
|
||||
}
|
||||
@@ -1075,6 +1122,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.tab-button.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
.tab-button {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
@@ -1131,7 +1182,6 @@ ul, ol {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1225,6 +1275,10 @@ footer {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.reaction-button.active.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.reaction-popover {
|
||||
position: relative;
|
||||
@@ -1247,3 +1301,58 @@ footer {
|
||||
.babycode-guide-list {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bookmarks-dropdown {
|
||||
background-color: #c1ceb1;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 400px;
|
||||
padding: 10px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item {
|
||||
display: flex;
|
||||
padding: 10px 0;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
}
|
||||
.bookmark-dropdown-item:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
}
|
||||
.bookmark-dropdown-item::before {
|
||||
content: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E");
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.bookmark-dropdown-item.selected {
|
||||
background-color: #beb1ce;
|
||||
}
|
||||
.bookmark-dropdown-item.selected:hover {
|
||||
background-color: rgb(203, 192.6, 215.8);
|
||||
}
|
||||
.bookmark-dropdown-item.selected::before {
|
||||
content: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.bookmarks-dropdown-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
@@ -99,6 +99,12 @@ a:visited {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.thread-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
@@ -689,6 +695,10 @@ button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.icon, input[type=submit].icon, .linkbutton.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: #d53232;
|
||||
color: #e6e6e6 !important;
|
||||
@@ -706,6 +716,10 @@ button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critic
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.critical.icon, input[type=submit].critical.icon, .linkbutton.critical.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #eaea6a;
|
||||
color: black !important;
|
||||
@@ -723,6 +737,10 @@ button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
button.warn.icon, input[type=submit].warn.icon, .linkbutton.warn.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: #3c283c;
|
||||
@@ -741,6 +759,10 @@ input[type=file]::file-selector-button.reduced {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
input[type=file]::file-selector-button.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
input[type=file]::file-selector-button {
|
||||
margin: 10px;
|
||||
}
|
||||
@@ -766,6 +788,10 @@ p {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.pagebutton.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
.pagebutton {
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
@@ -871,13 +897,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
.contain-svg.full > svg, .contain-svg.full > img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.contain-svg.inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.post-img-container {
|
||||
@@ -910,6 +931,17 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
}
|
||||
|
||||
.thread-info-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -973,7 +1005,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.colorful-table tr th {
|
||||
@@ -1028,6 +1059,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
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 {
|
||||
background-color: #503250;
|
||||
}
|
||||
@@ -1075,6 +1122,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.tab-button.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
.tab-button {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
@@ -1131,7 +1182,6 @@ ul, ol {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1225,6 +1275,10 @@ footer {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.reaction-button.active.icon {
|
||||
padding-left: 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.reaction-popover {
|
||||
position: relative;
|
||||
@@ -1248,6 +1302,61 @@ footer {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bookmarks-dropdown {
|
||||
background-color: #9b649b;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 400px;
|
||||
padding: 10px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item {
|
||||
display: flex;
|
||||
padding: 10px 0;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px;
|
||||
color: #e6e6e6;
|
||||
background-color: #3c283c;
|
||||
}
|
||||
.bookmark-dropdown-item:hover {
|
||||
background-color: rgb(109.2, 72.8, 109.2);
|
||||
}
|
||||
.bookmark-dropdown-item::before {
|
||||
content: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E");
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.bookmark-dropdown-item.selected {
|
||||
background-color: #8a5584;
|
||||
}
|
||||
.bookmark-dropdown-item.selected:hover {
|
||||
background-color: rgb(167.4843049327, 112.9156950673, 161.3067264574);
|
||||
}
|
||||
.bookmark-dropdown-item.selected::before {
|
||||
content: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.bookmarks-dropdown-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
margin-bottom: 10px;
|
||||
border: 10px solid rgb(40, 40, 40);
|
||||
|
||||
@@ -99,6 +99,12 @@ a:visited {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.thread-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
@@ -689,6 +695,10 @@ button.reduced, input[type=submit].reduced, .linkbutton.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
button.icon, input[type=submit].icon, .linkbutton.icon {
|
||||
padding-left: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
background-color: #f73030;
|
||||
color: white !important;
|
||||
@@ -706,6 +716,10 @@ button.critical.reduced, input[type=submit].critical.reduced, .linkbutton.critic
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
button.critical.icon, input[type=submit].critical.icon, .linkbutton.critical.icon {
|
||||
padding-left: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #fbfb8d;
|
||||
color: black !important;
|
||||
@@ -723,6 +737,10 @@ button.warn.reduced, input[type=submit].warn.reduced, .linkbutton.warn.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
button.warn.icon, input[type=submit].warn.icon, .linkbutton.warn.icon {
|
||||
padding-left: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: #f27a5a;
|
||||
@@ -741,6 +759,10 @@ input[type=file]::file-selector-button.reduced {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
input[type=file]::file-selector-button.icon {
|
||||
padding-left: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
input[type=file]::file-selector-button {
|
||||
margin: 6px;
|
||||
}
|
||||
@@ -766,6 +788,10 @@ p {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
.pagebutton.icon {
|
||||
padding-left: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
.pagebutton {
|
||||
padding: 3px 3px;
|
||||
margin: 0;
|
||||
@@ -871,13 +897,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
.contain-svg.full > svg, .contain-svg.full > img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.contain-svg.inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.post-img-container {
|
||||
@@ -910,6 +931,17 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
}
|
||||
|
||||
.thread-info-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -973,7 +1005,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.colorful-table tr th {
|
||||
@@ -1028,6 +1059,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
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 {
|
||||
background-color: rgb(219.84, 191.04, 183.36);
|
||||
}
|
||||
@@ -1075,6 +1122,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
.tab-button.icon {
|
||||
padding-left: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
.tab-button {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
@@ -1131,7 +1182,6 @@ ul, ol {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 6px 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1225,6 +1275,10 @@ footer {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
.reaction-button.active.icon {
|
||||
padding-left: 8px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.reaction-popover {
|
||||
position: relative;
|
||||
@@ -1248,6 +1302,61 @@ footer {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bookmarks-dropdown {
|
||||
background-color: #f27a5a;
|
||||
border: 1px solid black;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 400px;
|
||||
padding: 6px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-item {
|
||||
display: flex;
|
||||
padding: 6px 0;
|
||||
margin: 6px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid black;
|
||||
border-radius: 16px;
|
||||
color: black;
|
||||
background-color: #f27a5a;
|
||||
}
|
||||
.bookmark-dropdown-item:hover {
|
||||
background-color: rgb(244.6, 148.6, 123);
|
||||
}
|
||||
.bookmark-dropdown-item::before {
|
||||
content: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E");
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.bookmark-dropdown-item.selected {
|
||||
background-color: #b54444;
|
||||
}
|
||||
.bookmark-dropdown-item.selected:hover {
|
||||
background-color: rgb(197.978313253, 103.221686747, 103.221686747);
|
||||
}
|
||||
.bookmark-dropdown-item.selected::before {
|
||||
content: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.bookmarks-dropdown-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
|
||||
64
data/static/js/bitties/pyrom-bitty.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown'
|
||||
|
||||
export default class {
|
||||
async showBookmarkMenu(ev, el) {
|
||||
if ((ev.target.dataset.bookmarkId === el.dataset.bookmarkId) && el.childElementCount === 0) {
|
||||
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${ev.target.dataset.bookmarkType}?id=${ev.target.dataset.conceptId}&require_reload=${el.dataset.requireReload}`;
|
||||
const res = await this.api.getHTML(bookmarkMenuHref);
|
||||
if (res.error) {
|
||||
return;
|
||||
}
|
||||
const frag = res.value;
|
||||
el.appendChild(frag);
|
||||
const menu = el.childNodes[0];
|
||||
const bRect = el.getBoundingClientRect()
|
||||
if (bRect.left < window.innerWidth - bRect.right) {
|
||||
menu.style.right = 'unset';
|
||||
}
|
||||
} else if (el.childElementCount > 0) {
|
||||
el.removeChild(el.childNodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
selectBookmarkCollection(ev, el) {
|
||||
const clicked = ev.target;
|
||||
|
||||
if (clicked === el) {
|
||||
if (clicked.classList.contains('selected')) {
|
||||
clicked.classList.remove('selected');
|
||||
} else {
|
||||
clicked.classList.add('selected');
|
||||
}
|
||||
} else {
|
||||
el.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
async saveBookmarks(ev, el) {
|
||||
const bookmarkHref = el.dataset.bookmarkEndpoint;
|
||||
const collection = el.querySelector('.bookmark-dropdown-item.selected');
|
||||
let data = {};
|
||||
if (collection) {
|
||||
data['operation'] = 'move';
|
||||
data['collection_id'] = collection.dataset.collectionId;
|
||||
data['memo'] = el.querySelector('.bookmark-memo-input').value;
|
||||
} else {
|
||||
data['operation'] = 'remove';
|
||||
data['collection_id'] = el.dataset.originallyContainedIn;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
const requireReload = parseInt(el.dataset.requireReload) !== 0;
|
||||
el.remove();
|
||||
await fetch(bookmarkHref, options);
|
||||
if (requireReload) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
128
data/static/js/manage-bookmark-collections.js
Normal file
@@ -0,0 +1,128 @@
|
||||
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
data/static/js/vnd/bitty-5.1.0-rc6.min.js
vendored
Normal file
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="60px" height="60px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.364 5.63604C19.9926 7.26472 21 9.51472 21 12C21 16.9706 16.9706 21 12 21C9.51472 21 7.26472 19.9926 5.63604 18.364M18.364 5.63604C16.7353 4.00736 14.4853 3 12 3C7.02944 3 3 7.02944 3 12C3 14.4853 4.00736 16.7353 5.63604 18.364M18.364 5.63604L5.63604 18.364" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
|
Before Width: | Height: | Size: 609 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 17L7.58959 13.7694C8.38025 13.0578 9.58958 13.0896 10.3417 13.8417L11.5 15L15.0858 11.4142C15.8668 10.6332 17.1332 10.6332 17.9142 11.4142L20 13.5M11 9C11 9.55228 10.5523 10 10 10C9.44772 10 9 9.55228 9 9C9 8.44772 9.44772 8 10 8C10.5523 8 11 8.44772 11 9ZM6 20H18C19.1046 20 20 19.1046 20 18V6C20 4.89543 19.1046 4 18 4H6C4.89543 4 4 4.89543 4 6V18C4 19.1046 4.89543 20 6 20Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
|
Before Width: | Height: | Size: 728 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="60px" height="60px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8V8.5M12 12V16M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
|
Before Width: | Height: | Size: 480 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="60px" height="60px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 14V16M8 9V6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6V9M7 21H17C18.1046 21 19 20.1046 19 19V11C19 9.89543 18.1046 9 17 9H7C5.89543 9 5 9.89543 5 11V19C5 20.1046 5.89543 21 7 21Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
|
Before Width: | Height: | Size: 539 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4L9.87868 9.87868M20 20L14.1213 14.1213M9.87868 9.87868C9.33579 10.4216 9 11.1716 9 12C9 13.6569 10.3431 15 12 15C12.8284 15 13.5784 14.6642 14.1213 14.1213M9.87868 9.87868L14.1213 14.1213M6.76821 6.76821C4.72843 8.09899 2.96378 10.026 2 11.9998C3.74646 15.5764 8.12201 19 11.9998 19C13.7376 19 15.5753 18.3124 17.2317 17.2317M9.76138 5.34717C10.5114 5.12316 11.2649 5 12.0005 5C15.8782 5 20.2531 8.42398 22 12.0002C21.448 13.1302 20.6336 14.2449 19.6554 15.2412" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
|
Before Width: | Height: | Size: 814 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
|
Before Width: | Height: | Size: 498 B |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="60px" height="60px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15H12.01M12 12V9M4.98207 19H19.0179C20.5615 19 21.5233 17.3256 20.7455 15.9923L13.7276 3.96153C12.9558 2.63852 11.0442 2.63852 10.2724 3.96153L3.25452 15.9923C2.47675 17.3256 3.43849 19 4.98207 19Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
|
Before Width: | Height: | Size: 550 B |
@@ -18,6 +18,14 @@ $MAIN_BG: color.scale($ACCENT_COLOR, $lightness: -10%, $saturation: -40%) !defau
|
||||
$BUTTON_COLOR: color.adjust($ACCENT_COLOR, $hue: 90) !default;
|
||||
$BUTTON_COLOR_2: color.adjust($ACCENT_COLOR, $hue: 180) !default;
|
||||
|
||||
$BUTTON_COLOR_HOVER: color.scale($BUTTON_COLOR, $lightness: 20%) !default;
|
||||
$BUTTON_COLOR_ACTIVE: color.scale($BUTTON_COLOR, $lightness: -10%, $saturation: -70%) !default;
|
||||
$BUTTON_COLOR_DISABLED: color.scale($BUTTON_COLOR, $lightness: 30%, $saturation: -90%) !default;
|
||||
|
||||
$BUTTON_COLOR_2_HOVER: color.scale($BUTTON_COLOR_2, $lightness: 20%) !default;
|
||||
$BUTTON_COLOR_2_ACTIVE: color.scale($BUTTON_COLOR_2, $lightness: -10%, $saturation: -70%) !default;
|
||||
$BUTTON_COLOR_2_DISABLED: color.scale($BUTTON_COLOR_2, $lightness: 30%, $saturation: -90%) !default;
|
||||
|
||||
$ACCORDION_COLOR: color.adjust($ACCENT_COLOR, $hue: 140, $lightness: -10%, $saturation: -15%) !default;
|
||||
|
||||
$DEFAULT_FONT_COLOR: black !default;
|
||||
@@ -104,6 +112,7 @@ $button_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
|
||||
|
||||
$reduced_button_margin: $ZERO_PADDING !default;
|
||||
$reduced_button_padding: $SMALL_PADDING !default;
|
||||
$icon_button_padding_left: $BIG_PADDING - 4px !default;
|
||||
@mixin button($color, $font_color) {
|
||||
@extend %button-base;
|
||||
background-color: $color;
|
||||
@@ -125,6 +134,12 @@ $reduced_button_padding: $SMALL_PADDING !default;
|
||||
margin: $reduced_button_margin;
|
||||
padding: $reduced_button_padding;
|
||||
}
|
||||
|
||||
// this is meant to be used with the contain-svg class, hence the flex-direction here
|
||||
&.icon {
|
||||
padding-left: $icon_button_padding_left;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
$navbar_padding: $MEDIUM_PADDING !default;
|
||||
@@ -206,6 +221,13 @@ $thread_title_size: 1.5rem !default;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
$thread_actions_gap: $SMALL_PADDING !default;
|
||||
.thread-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $thread_actions_gap;
|
||||
}
|
||||
|
||||
$post_usercard_width: 200px !default;
|
||||
$post_border: 2px outset $DARK_2 !default;
|
||||
.post {
|
||||
@@ -740,13 +762,9 @@ $thread_locked_background: none !default;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
&:not(.full) > svg, &:not(.full) > img {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
&.full > svg, &.full > img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,6 +804,18 @@ $thread_info_mask_image: $user_page_post_preview_mask_image !default;
|
||||
mask-image: $thread_info_mask_image;
|
||||
}
|
||||
|
||||
$thread_info_header_gap: $SMALL_PADDING !default;
|
||||
.thread-info-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: $thread_info_header_gap;
|
||||
}
|
||||
|
||||
.thread-info-bookmark-button {
|
||||
margin-left: auto !important; // :(
|
||||
}
|
||||
|
||||
$thread_info_post_preview_margin_right: $post_inner_padding_right !default;
|
||||
.thread-info-post-preview {
|
||||
overflow: hidden;
|
||||
@@ -863,7 +893,7 @@ $colorful_table_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: $colorful_table_margin;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
}
|
||||
|
||||
$colorful_table_th_color: $BUTTON_COLOR_2 !default;
|
||||
@@ -932,12 +962,39 @@ $draggable_topic_border_bottom: $draggable_topic_border $DARK_2 !default;
|
||||
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;
|
||||
.editing {
|
||||
background-color: $post_editing_header_color;
|
||||
@@ -1046,7 +1103,7 @@ $accordion_margin: $MEDIUM_PADDING $SMALL_PADDING !default;
|
||||
box-sizing: border-box;
|
||||
border: $accordion_border;
|
||||
margin: $accordion_margin;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1172,3 +1229,79 @@ $babycode_guide_list_border: 1px dashed !default;
|
||||
.babycode-guide-list {
|
||||
border-bottom: $babycode_guide_list_border;
|
||||
}
|
||||
|
||||
.bookmark-dropdown-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
$bookmarks_dropdown_background_color: $ACCENT_COLOR !default;
|
||||
$bookmarks_dropdown_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||
$bookmarks_dropdown_border: $button_border !default;
|
||||
$bookmarks_dropdown_shadow: 0 0 30px rgba(0, 0, 0, 0.25) !default;
|
||||
$bookmarks_dropdown_min_width: 400px !default;
|
||||
$bookmarks_dropdown_padding: $MEDIUM_PADDING !default;
|
||||
.bookmarks-dropdown {
|
||||
background-color: $bookmarks_dropdown_background_color;
|
||||
border: $bookmarks_dropdown_border;
|
||||
border-radius: $bookmarks_dropdown_border_radius;
|
||||
box-shadow: $bookmarks_dropdown_shadow;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: $bookmarks_dropdown_min_width;
|
||||
padding: $bookmarks_dropdown_padding;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
$bookmark_dropdown_item_padding: $MEDIUM_PADDING 0 !default;
|
||||
$bookmark_dropdown_item_margin: $MEDIUM_PADDING 0 !default;
|
||||
$bookmark_dropdown_item_font_color: $BUTTON_FONT_COLOR !default;
|
||||
$bookmark_dropdown_item_background: $BUTTON_COLOR !default;
|
||||
$bookmark_dropdown_item_background_hover: $BUTTON_COLOR_HOVER !default;
|
||||
$bookmark_dropdown_item_background_selected: $BUTTON_COLOR_2 !default;
|
||||
$bookmark_dropdown_item_background_selected_hover: $BUTTON_COLOR_2_HOVER !default;
|
||||
$bookmark_dropdown_item_icon_size: 24px !default;
|
||||
$bookmark_dropdown_item_icon_padding: 0 $MEDIUM_PADDING !default;
|
||||
.bookmark-dropdown-item {
|
||||
display: flex;
|
||||
padding: $bookmark_dropdown_item_padding;
|
||||
margin: $bookmark_dropdown_item_margin;
|
||||
cursor: pointer;
|
||||
border: $button_border;
|
||||
border-radius: $button_border_radius;
|
||||
color: $bookmark_dropdown_item_font_color;
|
||||
|
||||
background-color: $bookmark_dropdown_item_background;
|
||||
&:hover {
|
||||
background-color: $bookmark_dropdown_item_background_hover;
|
||||
}
|
||||
|
||||
&::before {
|
||||
// TODO: un-inline this once the bitty bug is fixed
|
||||
content: url('data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E');
|
||||
width: $bookmark_dropdown_item_icon_size;
|
||||
height: $bookmark_dropdown_item_icon_size;
|
||||
padding: $bookmark_dropdown_item_icon_padding;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $bookmark_dropdown_item_background_selected;
|
||||
&:hover {
|
||||
background-color: $bookmark_dropdown_item_background_selected_hover;
|
||||
}
|
||||
|
||||
&::before{
|
||||
content: url('data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%221.5%22%20y%3D%221.5%22%20width%3D%2221%22%20height%3D%2221%22%20rx%3D%223%22%20stroke%3D%22currentColor%22%20stroke-width%3D%223%22%20fill%3D%22none%22%2F%3E%3Crect%20x%3D%225%22%20y%3D%225%22%20width%3D%2214%22%20height%3D%2214%22%20rx%3D%222%22%20stroke%3D%22none%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bookmarks-dropdown-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
$bookmark_dropdown_items_container_max_height: 300px !default;
|
||||
.bookmark-dropdown-items-container {
|
||||
max-height: $bookmark_dropdown_items_container_max_height;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||