add ability to bookmark posts and threads, courtesy of bitty

This commit is contained in:
2025-11-23 22:07:50 +03:00
parent 962b833a80
commit 075a9bd498
15 changed files with 521 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
from flask import Flask, session from flask import Flask, session, request
from dotenv import load_dotenv from dotenv import load_dotenv
from .models import Avatars, Users, PostHistory, Posts from .models import Avatars, Users, PostHistory, Posts
from .auth import digest from .auth import digest
@@ -131,6 +131,7 @@ def create_app():
from app.routes.mod import bp as mod_bp from app.routes.mod import bp as mod_bp
from app.routes.api import bp as api_bp from app.routes.api import bp as api_bp
from app.routes.posts import bp as posts_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(app_bp)
app.register_blueprint(topics_bp) app.register_blueprint(topics_bp)
app.register_blueprint(threads_bp) app.register_blueprint(threads_bp)
@@ -138,6 +139,7 @@ def create_app():
app.register_blueprint(mod_bp) app.register_blueprint(mod_bp)
app.register_blueprint(api_bp) app.register_blueprint(api_bp)
app.register_blueprint(posts_bp) app.register_blueprint(posts_bp)
app.register_blueprint(hyperapi_bp)
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_SECURE'] = True
@@ -200,6 +202,15 @@ def create_app():
for id_, text in matches 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 # this only happens at build time but
# build time is when updates are done anyway # build time is when updates are done anyway
# sooo... /shrug # sooo... /shrug

View File

@@ -371,6 +371,16 @@ class BookmarkCollections(Model):
res = db.fetch_one(q, self.id) res = db.fetch_one(q, self.id)
return int(res['pc']) 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): class BookmarkedPosts(Model):
table = 'bookmarked_posts' table = 'bookmarked_posts'

View File

@@ -2,7 +2,7 @@ from flask import Blueprint, request, url_for
from ..lib.babycode import babycode_to_html from ..lib.babycode import babycode_to_html
from ..constants import REACTION_EMOJI from ..constants import REACTION_EMOJI
from .users import is_logged_in, get_active_user from .users import is_logged_in, get_active_user
from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections, BookmarkedThreads, BookmarkedPosts
from ..db import db from ..db import db
bp = Blueprint("api", __name__, url_prefix="/api/") bp = Blueprint("api", __name__, url_prefix="/api/")
@@ -139,3 +139,76 @@ def manage_bookmark_collections(user_id):
return {'status': 'ok'}, 200 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
View 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)

View File

@@ -10,8 +10,10 @@
{% endif %} {% endif %}
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}"> <link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<script src="/static/js/vnd/bitty-5.1.0-rc6.min.js" type="module"></script>
</head> </head>
<body> <body>
<bitty-5-1 data-connect="/static/js/bitties/pyrom-bitty.js">
{% 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 %}
@@ -24,6 +26,7 @@
<footer class="darkbg"> <footer class="darkbg">
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span> <span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
</footer> </footer>
</bitty-5-1>
<script src="{{ "/static/js/ui.js" | cachebust }}"></script> <script src="{{ "/static/js/ui.js" | cachebust }}"></script>
<script src="{{ "/static/js/date-fmt.js" | cachebust }}"></script> <script src="{{ "/static/js/date-fmt.js" | cachebust }}"></script>
</body> </body>

View File

@@ -28,6 +28,14 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro bookmark_button(type, id, message = "Bookmark&hellip;", 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) %} {% macro infobox(message, kind=InfoboxKind.INFO) %}
<div class="{{ "infobox " + InfoboxHTMLClass[kind] }}"> <div class="{{ "infobox " + InfoboxHTMLClass[kind] }}">
<span> <span>
@@ -104,7 +112,8 @@
post, render_sig = True, is_latest = False, post, render_sig = True, is_latest = False,
editing = False, active_user = None, no_reply = false, editing = False, active_user = None, no_reply = false,
Reactions = none, show_thread_title = false, Reactions = none, show_thread_title = false,
show_bookmark = false, memo = None, bookmark_message = "Bookmark&hellip;" show_bookmark = false, memo = None, bookmark_message = "Bookmark&hellip;",
reload_after_bookmark = false
) %} ) %}
{% set postclass = "post" %} {% set postclass = "post" %}
{% if editing %} {% if editing %}
@@ -182,7 +191,7 @@
{% endif %} {% endif %}
{% if show_bookmark %} {% if show_bookmark %}
<button type="button" class="contain-svg inline icon">{{ icn_bookmark(20) }}{{ bookmark_message | safe }}</button> {{ bookmark_button(type="post", id=post.id, message=bookmark_message, require_reload=reload_after_bookmark)}}
{% endif %} {% endif %}
</span> </span>
</div> </div>

View 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>

View File

@@ -1,4 +1,4 @@
{% 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 %} {% from 'common/icons.html' import icn_bookmark %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ thread.title }}{% endblock %} {% block title %}{{ thread.title }}{% endblock %}
@@ -30,7 +30,7 @@
</form> </form>
{% endif %} {% endif %}
{% if can_bookmark %} {% if can_bookmark %}
<button type="button" class="contain-svg inline icon">{{ icn_bookmark(20) }}Bookmark&hellip;</button> {{ bookmark_button(type="thread", id=thread.id) }}
{% endif %} {% endif %}
{% if can_lock %} {% if can_lock %}
<form class="modform" action="{{ url_for("threads.lock", slug=thread.slug) }}" method="post"> <form class="modform" action="{{ url_for("threads.lock", slug=thread.slug) }}" method="post">

View File

@@ -1,4 +1,4 @@
{% 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 %} {% from 'common/icons.html' import icn_bookmark, icn_lock, icn_sticky %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}browsing topic {{ topic['name'] }}{% endblock %} {% block title %}browsing topic {{ topic['name'] }}{% endblock %}
@@ -53,7 +53,7 @@
</span> </span>
<span> <span>
{% if active_user and not active_user.is_guest() -%} {% if active_user and not active_user.is_guest() -%}
<button class="thread-info-bookmark-button contain-svg icon" type="button">{{ icn_bookmark(20) }}Bookmark&hellip;</button> {{ bookmark_button(type="thread", id=thread.id) }}
{%- endif %} {%- endif %}
</span> </span>
</span> </span>

View File

@@ -1,9 +1,10 @@
{% from "common/macros.html" import accordion, full_post %} {% from "common/macros.html" import accordion, full_post, bookmark_button %}
{% from "common/icons.html" import icn_bookmark %} {% from "common/icons.html" import icn_bookmark %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}bookmarks{% endblock %} {% block title %}bookmarks{% endblock %}
{% block content %} {% block content %}
<div class="darkbg inbox-container"> <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') %} {% for collection in collections | sort(attribute='sort_order') %}
{% call(section) accordion(disabled=collection.is_empty()) %} {% call(section) accordion(disabled=collection.is_empty()) %}
{% if section == 'header' %} {% if section == 'header' %}
@@ -28,7 +29,7 @@
<i>{{ thread.note }}</i> <i>{{ thread.note }}</i>
</td> </td>
<td> <td>
<button type="button" class="contain-svg inline icon">{{ icn_bookmark(20) }}Manage&hellip;</button> {{ bookmark_button(type='thread', id=thread.thread_id, message='Manage&hellip;', require_reload=true) }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -40,7 +41,7 @@
Posts{{" (no bookmarks)" if not collection.has_posts() else ""}} Posts{{" (no bookmarks)" if not collection.has_posts() else ""}}
{% else %} {% else %}
{% for post in collection.get_posts() %} {% 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&hellip;") }} {{ 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&hellip;", reload_after_bookmark=true) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endcall %} {% endcall %}

View File

@@ -1005,7 +1005,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
margin: 10px 0; margin: 10px 0;
overflow: hidden;
} }
.colorful-table tr th { .colorful-table tr th {
@@ -1183,7 +1182,6 @@ ul, ol {
box-sizing: border-box; box-sizing: border-box;
border: 1px solid black; border: 1px solid black;
margin: 10px 5px; margin: 10px 5px;
overflow: hidden;
} }
.accordion.hidden { .accordion.hidden {
@@ -1303,3 +1301,58 @@ footer {
.babycode-guide-list { .babycode-guide-list {
border-bottom: 1px dashed; 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;
}

View File

@@ -1005,7 +1005,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
margin: 10px 0; margin: 10px 0;
overflow: hidden;
} }
.colorful-table tr th { .colorful-table tr th {
@@ -1183,7 +1182,6 @@ ul, ol {
box-sizing: border-box; box-sizing: border-box;
border: 1px solid black; border: 1px solid black;
margin: 10px 5px; margin: 10px 5px;
overflow: hidden;
} }
.accordion.hidden { .accordion.hidden {
@@ -1304,6 +1302,61 @@ footer {
border-bottom: 1px dashed; 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 { #topnav {
margin-bottom: 10px; margin-bottom: 10px;
border: 10px solid rgb(40, 40, 40); border: 10px solid rgb(40, 40, 40);

View File

@@ -1005,7 +1005,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
margin: 6px 0; margin: 6px 0;
overflow: hidden;
} }
.colorful-table tr th { .colorful-table tr th {
@@ -1183,7 +1182,6 @@ ul, ol {
box-sizing: border-box; box-sizing: border-box;
border: 1px solid black; border: 1px solid black;
margin: 6px 3px; margin: 6px 3px;
overflow: hidden;
} }
.accordion.hidden { .accordion.hidden {
@@ -1304,6 +1302,61 @@ footer {
border-bottom: 1px dashed; 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 { #topnav {
border-top-left-radius: 16px; border-top-left-radius: 16px;
border-top-right-radius: 16px; border-top-right-radius: 16px;

View 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();
}
}
}

View File

@@ -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: color.adjust($ACCENT_COLOR, $hue: 90) !default;
$BUTTON_COLOR_2: color.adjust($ACCENT_COLOR, $hue: 180) !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; $ACCORDION_COLOR: color.adjust($ACCENT_COLOR, $hue: 140, $lightness: -10%, $saturation: -15%) !default;
$DEFAULT_FONT_COLOR: black !default; $DEFAULT_FONT_COLOR: black !default;
@@ -885,7 +893,7 @@ $colorful_table_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
margin: $colorful_table_margin; margin: $colorful_table_margin;
overflow: hidden; // overflow: hidden;
} }
$colorful_table_th_color: $BUTTON_COLOR_2 !default; $colorful_table_th_color: $BUTTON_COLOR_2 !default;
@@ -1095,7 +1103,7 @@ $accordion_margin: $MEDIUM_PADDING $SMALL_PADDING !default;
box-sizing: border-box; box-sizing: border-box;
border: $accordion_border; border: $accordion_border;
margin: $accordion_margin; margin: $accordion_margin;
overflow: hidden; // overflow: hidden;
} }
.accordion.hidden { .accordion.hidden {
@@ -1221,3 +1229,79 @@ $babycode_guide_list_border: 1px dashed !default;
.babycode-guide-list { .babycode-guide-list {
border-bottom: $babycode_guide_list_border; 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;
}