#!/usr/bin/env python3 import gi import locale import logging import math import threading gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, Gio, Adw from mcg import client from mcg.albumheaderbar import AlbumHeaderbar from mcg.utils import SortOrder from mcg.utils import Utils from mcg.utils import GridItem from mcg.utils import SearchFilter @Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/library-panel.ui') class LibraryPanel(Adw.Bin): __gtype_name__ = 'McgLibraryPanel' __gsignals__ = { 'open-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), 'close-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), 'update': (GObject.SIGNAL_RUN_FIRST, None, ()), 'play': (GObject.SIGNAL_RUN_FIRST, None, (str, )), 'queue': (GObject.SIGNAL_RUN_FIRST, None, (str, )), 'queue-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT, )), 'item-size-changed': (GObject.SIGNAL_RUN_FIRST, None, (int, )), 'sort-order-changed': (GObject.SIGNAL_RUN_FIRST, None, (int, )), 'sort-type-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool, )), 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str, )), } # Widgets library_stack = Gtk.Template.Child() panel_normal = Gtk.Template.Child() panel_standalone = Gtk.Template.Child() actionbar_revealer = Gtk.Template.Child() # Toolbar toolbar = Gtk.Template.Child() select_button = Gtk.Template.Child() toolbar_search_bar = Gtk.Template.Child() toolbar_popover = Gtk.Template.Child() toolbar_sort_order_button = Gtk.Template.Child() sort_artist = Gtk.Template.Child() sort_title = Gtk.Template.Child() sort_year = Gtk.Template.Child() sort_modified = Gtk.Template.Child() grid_scale = Gtk.Template.Child() # Filter/search bar filter_bar = Gtk.Template.Child() filter_entry = Gtk.Template.Child() # Progress Bar stack = Gtk.Template.Child() progress_box = Gtk.Template.Child() progress_image = Gtk.Template.Child() progress_bar = Gtk.Template.Child() scroll = Gtk.Template.Child() # Library Grid library_grid = Gtk.Template.Child() # Action bar (normal) actionbar = Gtk.Template.Child() actionbar_standalone = Gtk.Template.Child() # Standalone Image standalone_stack = Gtk.Template.Child() standalone_spinner = Gtk.Template.Child() standalone_scroll = Gtk.Template.Child() standalone_image = Gtk.Template.Child() def __init__(self, client, **kwargs): super().__init__(**kwargs) self._logger = logging.getLogger(__name__) self._client = client self._buttons = {} self._albums = None self._host = "localhost" self._item_size = 150 self._sort_order = SortOrder.YEAR self._sort_type = Gtk.SortType.DESCENDING self._grid_pixbufs = {} self._grid_width = 0 self._old_ranges = {} self._library_lock = threading.Lock() self._library_stop = threading.Event() self._icon_theme = Gtk.IconTheme.get_for_display( Gdk.Display.get_default()) self._standalone_pixbuf = None self._selected_albums = [] self._is_selected = False # Widgets # Header bar self._headerbar_standalone = AlbumHeaderbar() self._headerbar_standalone.connect('close', self.on_standalone_close_clicked) # Library Grid: Model self._library_grid_model = Gio.ListStore() self._library_grid_filter = Gtk.FilterListModel() self._library_grid_filter.set_model(self._library_grid_model) self._library_grid_selection_multi = Gtk.MultiSelection.new( self._library_grid_filter) self._library_grid_selection_single = Gtk.SingleSelection.new( self._library_grid_filter) # Library Grid self.library_grid.set_model(self._library_grid_selection_single) # Toolbar menu self.grid_scale.set_value(self._item_size) self._toolbar_sort_buttons = { SortOrder.ARTIST: self.sort_artist, SortOrder.TITLE: self.sort_title, SortOrder.YEAR: self.sort_year, SortOrder.MODIFIED: self.sort_modified } # Button controller for grid scale button_controller = Gtk.GestureClick() button_controller.connect('unpaired-release', self.on_grid_scale_released) self.grid_scale.add_controller(button_controller) def get_headerbar_standalone(self): return self._headerbar_standalone def get_toolbar(self): return self.toolbar def set_selected(self, selected): self._is_selected = selected @Gtk.Template.Callback() def on_select_toggled(self, widget): if self.select_button.get_active(): self.actionbar_revealer.set_reveal_child(True) self.library_grid.set_model(self._library_grid_selection_multi) self.library_grid.set_single_click_activate(False) self.library_grid.get_style_context().add_class( Utils.CSS_SELECTION) else: self.actionbar_revealer.set_reveal_child(False) self.library_grid.set_model(self._library_grid_selection_single) self.library_grid.set_single_click_activate(True) self.library_grid.get_style_context().remove_class( Utils.CSS_SELECTION) @Gtk.Template.Callback() def on_update_clicked(self, widget): self.emit('update') def on_grid_scale_released(self, widget, x, y, npress, sequence): size = math.floor(self.grid_scale.get_value()) grid_range = self.grid_scale.get_adjustment() if size < grid_range.get_lower() or size > grid_range.get_upper(): return self._item_size = size self.emit('item-size-changed', size) self._redraw() GObject.idle_add(self.toolbar_popover.popdown) @Gtk.Template.Callback() def on_grid_scale_changed(self, widget): size = math.floor(self.grid_scale.get_value()) grid_range = widget.get_adjustment() if size < grid_range.get_lower() or size > grid_range.get_upper(): return self._set_widget_grid_size(self.library_grid, size, True) @Gtk.Template.Callback() def on_sort_toggled(self, widget): if widget.get_active(): self._sort_order = [ key for key, value in self._toolbar_sort_buttons.items() if value is widget ][0] self._sort_grid_model() self.emit('sort-order-changed', self._sort_order) @Gtk.Template.Callback() def on_sort_order_toggled(self, button): if button.get_active(): self._sort_type = Gtk.SortType.DESCENDING else: self._sort_type = Gtk.SortType.ASCENDING self._sort_grid_model() self.emit('sort-type-changed', button.get_active()) def set_size(self, width, height): self._set_marks() self._resize_standalone_image() @Gtk.Template.Callback() def on_filter_entry_changed(self, widget): self._library_grid_filter.set_filter( SearchFilter(self.filter_entry.get_text())) @Gtk.Template.Callback() def on_library_grid_clicked(self, widget, position): # Get selected album item = self._library_grid_filter.get_item(position) album = item.get_album() album_id = album.get_id() self._selected_albums = [album] self.emit('albumart', album_id) # Show standalone album if widget.get_model() == self._library_grid_selection_single: # Set labels self._headerbar_standalone.set_album(album) # Show panel self._open_standalone() # Set cover loading indicator self.standalone_stack.set_visible_child(self.standalone_spinner) self.standalone_spinner.start() @Gtk.Template.Callback() def on_selection_cancel_clicked(self, widget): self.select_button.set_active(False) @Gtk.Template.Callback() def on_selection_add_clicked(self, widget): self.emit('queue-multiple', self._get_selected_albums()) self.select_button.set_active(False) @Gtk.Template.Callback() def on_standalone_play_clicked(self, widget): self.emit('play', self._selected_albums[0].get_id()) self._close_standalone() @Gtk.Template.Callback() def on_standalone_queue_clicked(self, widget): self.emit('queue', self._selected_albums[0].get_id()) self._close_standalone() def on_standalone_close_clicked(self, widget): self._close_standalone() def show_search(self): self.filter_bar.set_search_mode(True) def set_item_size(self, item_size): if self._item_size != item_size: self._item_size = item_size self.grid_scale.set_value(item_size) self._redraw() def get_item_size(self): return self._item_size def set_sort_order(self, sort): button = self._toolbar_sort_buttons[sort] if button: self._sort_order = [ key for key, value in self._toolbar_sort_buttons.items() if value is button ][0] if not button.get_active(): button.set_active(True) self._sort_grid_model() def set_sort_type(self, sort_type): if sort_type: sort_type_gtk = Gtk.SortType.DESCENDING else: sort_type_gtk = Gtk.SortType.ASCENDING if sort_type_gtk != self._sort_type: self._sort_type = sort_type_gtk self.toolbar_sort_order_button.set_active(sort_type) self._sort_grid_model() def get_sort_type(self): return (self._sort_type != Gtk.SortType.ASCENDING) def init_albums(self): self.progress_bar.set_text(locale.gettext("Loading albums")) def load_albums(self): self.progress_bar.pulse() def set_albums(self, host, albums): self._host = host self._library_stop.set() threading.Thread(target=self._set_albums, args=( host, albums, self._item_size, )).start() def set_albumart(self, album, data): if album in self._selected_albums: if data: # Load image and draw it try: self._standalone_pixbuf = Utils.load_pixbuf(data) except Exception: self._logger.exception("Failed to set albumart") self._standalone_pixbuf = self._get_default_image() else: self._standalone_pixbuf = self._get_default_image() # Show image GObject.idle_add(self._show_image) def _sort_grid_model(self): GObject.idle_add(self._library_grid_model.sort, self._grid_model_compare_func, self._sort_order, self._sort_type) def _grid_model_compare_func(self, item1, item2, criterion, order): return client.MCGAlbum.compare(item1.get_album(), item2.get_album(), criterion, (order == Gtk.SortType.DESCENDING)) def stop_threads(self): self._library_stop.set() def _set_albums(self, host, albums, size): self._library_lock.acquire() self._albums = albums stack_transition_type = self.stack.get_transition_type() GObject.idle_add(self.stack.set_transition_type, Gtk.StackTransitionType.NONE) GObject.idle_add(self.stack.set_visible_child, self.progress_box) GObject.idle_add(self.progress_bar.set_fraction, 0.0) GObject.idle_add(self.stack.set_transition_type, stack_transition_type) GObject.idle_add(self._library_grid_model.remove_all) i = 0 n = len(albums) cache = client.MCGCache(host, size) self._grid_pixbufs.clear() for album_id in albums.keys(): album = albums[album_id] pixbuf = None try: pixbuf = Utils.load_thumbnail(cache, self._client, album, size) except client.CommandException: # Exception is handled by client pass except Exception as e: self._logger.exception("Failed to load albumart", e) if pixbuf is None: pixbuf = self._icon_theme.lookup_icon( Utils.STOCK_ICON_DEFAULT, None, self._item_size, self._item_size, Gtk.TextDirection.LTR, Gtk.IconLookupFlags.FORCE_SYMBOLIC) if pixbuf is not None: self._grid_pixbufs[album.get_id()] = pixbuf GObject.idle_add(self._library_grid_model.append, GridItem(album, pixbuf)) i += 1 GObject.idle_add(self.progress_bar.set_fraction, i / n) GObject.idle_add(self.progress_bar.set_text, locale.gettext("Loading images")) self._library_lock.release() GObject.idle_add(self.stack.set_visible_child, self.scroll) self._sort_grid_model() def _set_widget_grid_size(self, grid_widget, size, vertical): self._library_stop.set() threading.Thread(target=self._set_widget_grid_size_thread, args=( grid_widget, size, vertical, )).start() def _set_widget_grid_size_thread(self, grid_widget, size, vertical): self._library_lock.acquire() self._library_stop.clear() if size == self._item_size: self._library_lock.release() return for i in range(self._library_grid_model.get_n_items()): grid_item = self._library_grid_model.get_item(i) album_id = grid_item.get_album().get_id() pixbuf = self._grid_pixbufs[album_id] if pixbuf is not None: pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST) else: pixbuf = self._icon_theme.lookup_icon( Utils.STOCK_ICON_DEFAULT, None, size, size, Gtk.TextDirection.LTR, Gtk.IconLookupFlags.FORCE_SYMBOLIC) GObject.idle_add(grid_item.set_cover, pixbuf) if self._library_stop.is_set(): self._library_lock.release() return self._library_lock.release() def _show_image(self): self._resize_standalone_image() self.standalone_stack.set_visible_child(self.standalone_scroll) self.standalone_spinner.stop() def _redraw(self): if self._albums is not None: self.set_albums(self._host, self._albums) def _set_marks(self): width = self.scroll.get_width() if width == self._grid_width: return self._grid_width = width self.grid_scale.clear_marks() lower = int(self.grid_scale.get_adjustment().get_lower()) upper = int(self.grid_scale.get_adjustment().get_upper()) count_min = max(int(width / upper), 1) count_max = max(int(width / lower), 1) for index in range(count_min, count_max): pixel = int(width / index) pixel = pixel - (2 * int(pixel / 100)) self.grid_scale.add_mark(pixel, Gtk.PositionType.BOTTOM, None) def _open_standalone(self): self.library_stack.set_visible_child(self.panel_standalone) self.emit('open-standalone') def _close_standalone(self): self.library_stack.set_visible_child(self.panel_normal) self.emit('close-standalone') def _resize_standalone_image(self): # Get size size_width = self.standalone_stack.get_width() size_height = self.standalone_stack.get_height() # Get pixelbuffer pixbuf = self._standalone_pixbuf # Check pixelbuffer if pixbuf is None: return # Skalierungswert für Breite und Höhe ermitteln ratio_w = float(size_width) / float(pixbuf.get_width()) ratio_h = float(size_height) / float(pixbuf.get_height()) # Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren ratio = min(ratio_w, ratio_h) ratio = min(ratio, 1) # Neue Breite und Höhe berechnen width = int(math.floor(pixbuf.get_width() * ratio)) height = int(math.floor(pixbuf.get_height() * ratio)) if width <= 0 or height <= 0: return # Pixelpuffer auf Oberfläche zeichnen self.standalone_image.set_from_pixbuf( pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER)) self.standalone_image.show() def _get_default_image(self): return self._icon_theme.lookup_icon(Utils.STOCK_ICON_DEFAULT, None, 512, 512, Gtk.TextDirection.LTR, Gtk.IconLookupFlags.FORCE_SYMBOLIC) def _get_selected_albums(self): albums = [] for i in range(self.library_grid.get_model().get_n_items()): if self.library_grid.get_model().is_selected(i): albums.append(self.library_grid.get_model().get_item( i).get_album().get_id()) return albums