add ability to bookmark posts and threads, courtesy of bitty
This commit is contained in:
@@ -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
|
||||
@@ -131,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)
|
||||
@@ -138,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
|
||||
|
||||
@@ -200,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
|
||||
|
||||
@@ -371,6 +371,16 @@ class BookmarkCollections(Model):
|
||||
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'
|
||||
|
||||
@@ -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, Users, BookmarkCollections
|
||||
from ..models import APIRateLimits, Threads, Reactions, Users, BookmarkCollections, BookmarkedThreads, BookmarkedPosts
|
||||
from ..db import db
|
||||
|
||||
bp = Blueprint("api", __name__, url_prefix="/api/")
|
||||
@@ -139,3 +139,76 @@ def manage_bookmark_collections(user_id):
|
||||
|
||||
|
||||
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
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)
|
||||
@@ -10,8 +10,10 @@
|
||||
{% 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>
|
||||
<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 %}
|
||||
@@ -24,6 +26,7 @@
|
||||
<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>
|
||||
|
||||
@@ -28,6 +28,14 @@
|
||||
</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>
|
||||
@@ -104,7 +112,8 @@
|
||||
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…"
|
||||
show_bookmark = false, memo = None, bookmark_message = "Bookmark…",
|
||||
reload_after_bookmark = false
|
||||
) %}
|
||||
{% set postclass = "post" %}
|
||||
{% if editing %}
|
||||
@@ -182,7 +191,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% 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 %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
23
app/templates/components/bookmarks_dropdown.html
Normal file
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,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 %}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ thread.title }}{% endblock %}
|
||||
@@ -30,7 +30,7 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if can_bookmark %}
|
||||
<button type="button" class="contain-svg inline icon">{{ icn_bookmark(20) }}Bookmark…</button>
|
||||
{{ bookmark_button(type="thread", id=thread.id) }}
|
||||
{% endif %}
|
||||
{% if can_lock %}
|
||||
<form class="modform" action="{{ url_for("threads.lock", slug=thread.slug) }}" method="post">
|
||||
|
||||
@@ -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 %}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}browsing topic {{ topic['name'] }}{% endblock %}
|
||||
@@ -53,7 +53,7 @@
|
||||
</span>
|
||||
<span>
|
||||
{% if active_user and not active_user.is_guest() -%}
|
||||
<button class="thread-info-bookmark-button contain-svg icon" type="button">{{ icn_bookmark(20) }}Bookmark…</button>
|
||||
{{ bookmark_button(type="thread", id=thread.id) }}
|
||||
{%- endif %}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -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 %}
|
||||
{% 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' %}
|
||||
@@ -28,7 +29,7 @@
|
||||
<i>{{ thread.note }}</i>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="contain-svg inline icon">{{ icn_bookmark(20) }}Manage…</button>
|
||||
{{ bookmark_button(type='thread', id=thread.thread_id, message='Manage…', require_reload=true) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -40,7 +41,7 @@
|
||||
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…") }}
|
||||
{{ 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 %}
|
||||
|
||||
@@ -1005,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 {
|
||||
@@ -1183,7 +1182,6 @@ ul, ol {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1303,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;
|
||||
}
|
||||
|
||||
@@ -1005,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 {
|
||||
@@ -1183,7 +1182,6 @@ ul, ol {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 10px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1304,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);
|
||||
|
||||
@@ -1005,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 {
|
||||
@@ -1183,7 +1182,6 @@ ul, ol {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid black;
|
||||
margin: 6px 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion.hidden {
|
||||
@@ -1304,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
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -885,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;
|
||||
@@ -1095,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 {
|
||||
@@ -1221,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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user