diff --git a/app/models.py b/app/models.py index 1983a43..391a1f3 100644 --- a/app/models.py +++ b/app/models.py @@ -511,6 +511,12 @@ class BookmarkCollections(Model): """ res = db.fetch_one(q, user_id) + @classmethod + def get_for_user(cls, user_id): + q = """SELECT * FROM bookmark_collections WHERE user_id = ? ORDER BY sort_order ASC""" + res = db.query(q, user_id) + return [cls.from_data(row) for row in res] + def has_posts(self): q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ?) as e' res = db.fetch_one(q, self.id)['e'] diff --git a/app/routes/hyperapi.py b/app/routes/hyperapi.py index 9a57beb..29dab65 100644 --- a/app/routes/hyperapi.py +++ b/app/routes/hyperapi.py @@ -24,7 +24,7 @@ def get_bookmark_dropdown(): except ValueError: return 'error', 400 is_thread = concept_kind == 'thread' - collections = BookmarkCollections.findall({'user_id': user.id}) + collections = BookmarkCollections.get_for_user(user.id) in_collection = None note = '' for collection in collections: diff --git a/app/routes/users.py b/app/routes/users.py index aaad3bd..e4ef761 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -448,6 +448,69 @@ def bookmarks(username): username = username.lower() return 'stub' +@bp.get('//bookmarks/collections/') +@login_required +@user_required +@redirect_to_own +def bookmark_collections(username): + user = get_active_user() + collections = BookmarkCollections.get_for_user(user.id) + return render_template('users/manage_collections.html', collections=collections) + +@bp.post('//bookmarks/collections/') +@login_required +@user_required +@redirect_to_own +def edit_bookmark_collections(username): + user = get_active_user() + ids = request.form.getlist('id[]') + names = request.form.getlist('name[]') + if len(ids) == 0 or len(ids) != len(names): + abort(400) + deleted_ids = filter(lambda x: x.strip(), request.form.get('deleted_ids', '').split(';')) + try: + deleted_ids = map(lambda x: int(x), deleted_ids) + except ValueError: + abort(400) + + with db.transaction(): + for new_order, id in enumerate(ids): + new_name = names[new_order] + if id == 'new': + bc = BookmarkCollections.create({ + 'user_id': user.id, + 'is_default': False, + 'name': new_name, + 'sort_order': new_order, + }) + continue + id = int(id) + bc = BookmarkCollections.find({'id': id}) + if not bc: + continue + if bc.user_id != user.id: + continue + if bc.is_default: + new_order = 0 + elif new_order == 0: + new_order = 1 + bc.update({ + 'name': new_name, + 'sort_order': new_order, + }) + + for deleted_id in deleted_ids: + bc = BookmarkCollections.find({'id': deleted_id}) + if not bc: + continue + if bc.user_id != user.id: + continue + if bc.is_default: + continue + bc.delete() + + return redirect(url_for('.bookmark_collections', username=username)) + @bp.get('//delete-confirm/') @login_required @redirect_to_own diff --git a/app/templates/common/macros.html b/app/templates/common/macros.html index f39884e..1993e06 100644 --- a/app/templates/common/macros.html +++ b/app/templates/common/macros.html @@ -270,7 +270,7 @@ {%- endmacro %} {% macro sortable_list_item(key, immovable=false, attr=none) -%} -
  • +
  • {{ icn_dragger() }}
    {{ caller() }}
  • diff --git a/app/templates/users/manage_collections.html b/app/templates/users/manage_collections.html new file mode 100644 index 0000000..7cd36a1 --- /dev/null +++ b/app/templates/users/manage_collections.html @@ -0,0 +1,44 @@ +{%- from 'common/macros.html' import subheader, sortable_list, sortable_list_item -%} +{%- macro collection_item(name='', can_delete=true, id=-1, thread_count=0, post_count=0) -%} + + +{{thread_count}} {{'thread' | pluralize(num=thread_count)}}, {{post_count}} {{'post' | pluralize(num=post_count)}} +{%- if not can_delete -%} +Default collection +{%- else -%} + +{%- endif -%} +{%- endmacro -%} +{%- extends 'base.html' -%} +{%- block title -%}managing bookmark collections{%- endblock -%} +{%- block content -%} + +{%- set sh -%} + +Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it. + +
    This page requires JS enabled to work correctly.
    +{%- endset -%} +{%- call() subheader('Manage bookmark collections', sh) -%} +
    + Actions + + +
    +{%- endcall -%} +
    + + {%- call() sortable_list(attr={'data-r': 'addCollection'}) -%} + {%- for collection in collections -%} + {%- call() sortable_list_item(key='bc', immovable=collection.is_default == 1, attr={'data-r': 'deleteCollection', 'data-id': collection.id}) -%} + {{ collection_item(name=collection.name, can_delete=collection.is_default != 1, thread_count=collection.get_threads_count(), post_count=collection.get_posts_count(), id=collection.id) }} + {%- endcall -%} + {%- endfor -%} + {%- endcall -%} +
    + +{%- endblock -%} diff --git a/data/static/css/style.css b/data/static/css/style.css index f2c0ca9..dfbf2bc 100644 --- a/data/static/css/style.css +++ b/data/static/css/style.css @@ -297,6 +297,25 @@ a.site-title { margin-top: var(--medium-padding); } } + + &.primary-bg { + --main-color: var(--bg-color-primary); + background-color: var(--bg-color-primary); + } + + &.secondary-bg { + --main-color: var(--bg-color-secondary); + --rotation: 0deg; + } + + &.tertiary-bg { + --main-color: var(--bg-color-tertiary); + --rotation: 0deg; + } + + &.contrast-bg { + --main-color: var(--bg-color-contrast); + } } .info { @@ -368,25 +387,6 @@ ul.horizontal, ol.horizontal { } } -.primary-bg { - --main-color: var(--bg-color-primary); - background-color: var(--bg-color-primary); -} - -.secondary-bg { - --main-color: var(--bg-color-secondary); - --rotation: 0deg; -} - -.tertiary-bg { - --main-color: var(--bg-color-tertiary); - --rotation: 0deg; -} - -.contrast-bg { - --main-color: var(--bg-color-contrast); -} - .motd { display: flex; gap: var(--big-padding); diff --git a/data/static/js/bits/collections-editor.js b/data/static/js/bits/collections-editor.js new file mode 100644 index 0000000..ce354cd --- /dev/null +++ b/data/static/js/bits/collections-editor.js @@ -0,0 +1,18 @@ +export const b = {} + +export function addCollection(ev, sender, el) { + el.innerHTML += b.templates.collectionItem; +} + +export function deleteCollection(ev, sender, el) { + if (!el.contains(sender)) return; + b.send({ 'id': el.prop('id') }, 'countDeletedCollection'); + el.remove(); +} + +export function countDeletedCollection(payload, _, el) { + if (payload.id === 'new') { + return; + } + el.value += `${payload.id};` +} diff --git a/data/static/js/bits/progressive-enhancement.js b/data/static/js/bits/progressive-enhancement.js index dc2716e..f186a06 100644 --- a/data/static/js/bits/progressive-enhancement.js +++ b/data/static/js/bits/progressive-enhancement.js @@ -1,5 +1,5 @@ export const b = { - init: 'enhance', + init: 'enhance enhanceHide', } export function enhance(_, __, el) { @@ -18,3 +18,11 @@ export function enhance(_, __, el) { } } } + +export function enhanceHide(_, __, el) { + if (el === undefined) { + return; + } + + el.style.display = 'none'; +} diff --git a/data/static/js/ui.js b/data/static/js/ui.js index 30f8f11..d9c3791 100644 --- a/data/static/js/ui.js +++ b/data/static/js/ui.js @@ -70,8 +70,8 @@ if (listItems.has(node)) return; const dragger = node.querySelector('.dragger'); - dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, item) }); - dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, item) }); + dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, node) }); + dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, node) }); node.addEventListener('dragover', e => { sortableItemDragOver(e, node) }); listItems.add(node); listItemsHandled.set(list, listItems);