From c57b103e3aa9f47a80cc5744586e65eddc578f40 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 14 Aug 2024 08:24:58 +0200 Subject: [PATCH] Reorder playlist content using drag&drop Long press on a feed item to start dragging it. Hovering over other feed items, outlines them in blue (this happens automatically and appears to be implemented in gtk). Additionally, hovering on the top half of feed items adds a margin to the top of the widget, to indicate that the dragged item would be inserted before. Hovering on the bottom half shows the margin on the bottom, to show that the element will be inserted below. NOTE: d&d behaves weirdly on touch screens, but by using 2 fingers or a combination of dragging and clicking it seems to work BUG: gtk has problems with rendering the hover preview (at least on wayland/sway) --- melon/models/__init__.py | 21 ++++++++- melon/playlist/__init__.py | 87 +++++++++++++++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/melon/models/__init__.py b/melon/models/__init__.py index b45b162..1699073 100644 --- a/melon/models/__init__.py +++ b/melon/models/__init__.py @@ -865,7 +865,7 @@ def get_playlists() -> list[PlaylistWrapper]: return results -def add_to_local_playlist(playlist_id: int, vid, pos=None): +def add_to_local_playlist(playlist_id: int, vid, pos=None, notify=True): ensure_video(vid) conn = connect_to_db() if pos is None: @@ -887,6 +887,25 @@ def add_to_local_playlist(playlist_id: int, vid, pos=None): (playlist_id, vid.id, vid.server, pos), ) conn.close() + if notify: + notify("playlists_changed") + + +def set_local_playlist_content(playlist_id: int, videos: list[Resource]): + conn = connect_to_db() + # remove old playlist content + execute_sql( + conn, + """ + DELETE FROM local_playlist_content + WHERE id = ? + """, + (playlist_id,), + ) + # manually add new items + for index, entry in enumerate(videos): + add_to_local_playlist(playlist_id, entry, pos=index, notify=False) + conn.close() notify("playlists_changed") diff --git a/melon/playlist/__init__.py b/melon/playlist/__init__.py index 2967672..c0a1b4a 100644 --- a/melon/playlist/__init__.py +++ b/melon/playlist/__init__.py @@ -3,12 +3,12 @@ import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -from gi.repository import Gtk, Adw, GLib +from gi.repository import Gtk, Gdk, Adw, GLib, GObject from unidecode import unidecode from gettext import gettext as _ from melon.servers.utils import get_servers_list, get_server_instance -from melon.servers import Preference, PreferenceType +from melon.servers import Preference, PreferenceType, Resource from melon.widgets.iconbutton import IconButton from melon.widgets.feeditem import AdaptiveFeedItem from melon.widgets.simpledialog import SimpleDialog @@ -20,8 +20,7 @@ from melon.models import ( ensure_delete_local_playlist, set_local_playlist_thumbnail, delete_from_local_playlist, -) -from melon.models import ( + set_local_playlist_content, is_server_enabled, ensure_server_disabled, ensure_server_enabled, @@ -99,7 +98,9 @@ class LocalPlaylistScreen(Adw.NavigationPage): app_conf = get_app_settings() self.box.add(group) # add playlist content to group as well - for entry, index in sorted(playlist.content, key=lambda x: x[1]): + for raw_index, (entry, index) in enumerate( + sorted(playlist.content, key=lambda x: x[1]) + ): item = AdaptiveFeedItem(entry) item.menuitems = [ ( @@ -119,6 +120,26 @@ class LocalPlaylistScreen(Adw.NavigationPage): ), ), ] + drag_source = Gtk.DragSource() + drag_source.set_content( + Gdk.ContentProvider.new_union( + [ + Gdk.ContentProvider.new_for_value(raw_index), + ] + ) + ) + drag_source.set_actions(Gdk.DragAction.MOVE) + drag_source.connect("drag-begin", pass_me(self.entry_drag_begin, item)) + drag_source.connect("drag-end", self.entry_drag_end) + drag_source.connect("drag-cancel", self.entry_drag_cancel) + item.add_controller(drag_source) + drop_target = Gtk.DropTarget.new(GObject.TYPE_NONE, Gdk.DragAction.MOVE) + drop_target.set_gtypes([int]) + drop_target.connect("drop", pass_me(self.entry_drop, item, raw_index)) + drop_target.connect("motion", pass_me(self.entry_drop_motion, item)) + drop_target.connect("leave", pass_me(self.entry_drop_leave, item)) + item.add_controller(drop_target) + group.add(item) self.scrollview.set_child(self.box) @@ -126,6 +147,62 @@ class LocalPlaylistScreen(Adw.NavigationPage): self.toolbar_view.set_content(self.wrapper) self.set_child(self.toolbar_view) + def entry_drag_begin(self, source, drag, widget): + preview = Gtk.WidgetPaintable.new(widget) + source.set_icon(preview, 0, 0) + + def entry_drag_end(self, source, drag, delete_data): + source.set_icon(None, 0, 0) + + def entry_drag_cancel(self, source, drag, reason): + source.set_icon(None, 0, 0) + + def entry_drop(self, target, value, x, y, widget, index): + height = widget.get_height() + if value == index: + # do nothing + # because the item does not need to be moved + self.do_update() + return + if y > height / 2: + # insert below + target_index = index * 2 + 2 + else: + # insert above + target_index = index * 2 + # create database containing all entries, but spaced two apart + # this way, we can easily fit in new entries + db = dict( + [ + [x[1] * 2 + 1, x[0]] + for x in sorted(self.playlist.content, key=lambda x: x[1]) + ] + ) + # remove dropped entry + entry = db.pop(value * 2 + 1) + # insert entry at new position + db[target_index] = entry + # build updated video list + plist = [x[1] for x in sorted(db.items(), key=lambda x: x[0])] + # update playlist database using the list indices from the list + set_local_playlist_content(self.playlist_id, plist) + self.do_update() + + def entry_drop_motion(self, target, x, y, widget): + height = widget.get_height() + indicator_distance = 10 + if y > height / 2: + widget.set_margin_top(0) + widget.set_margin_bottom(indicator_distance) + else: + widget.set_margin_top(indicator_distance) + widget.set_margin_bottom(0) + return Gdk.DragAction.MOVE + + def entry_drop_leave(self, target, widget): + widget.set_margin_top(0) + widget.set_margin_bottom(0) + def open_edit(self): self.edit_diag = SimpleDialog() self.edit_diag.set_title(_("Edit Playlist")) -- 2.38.5