#!/usr/bin/env python3 import gi import math import threading gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Gdk, Gio, GObject, GdkPixbuf, Adw from mcg import client from mcg.albumheaderbar import AlbumHeaderbar from mcg.utils import Utils from mcg.utils import GridItem @Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/playlist-panel.ui') class PlaylistPanel(Adw.Bin): __gtype_name__ = 'McgPlaylistPanel' __gsignals__ = { 'open-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), 'close-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), 'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()), 'remove-album': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT, )), 'remove-multiple-albums': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT, )), 'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT, )), 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str, )), } # Widgets playlist_stack = Gtk.Template.Child() panel_normal = Gtk.Template.Child() panel_standalone = Gtk.Template.Child() actionbar_revealer = Gtk.Template.Child() # Toolbar toolbar = Gtk.Template.Child() playlist_clear_button = Gtk.Template.Child() select_button = Gtk.Template.Child() # Playlist Grid playlist_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._client = client self._host = None self._item_size = 150 self._playlist = None self._playlist_albums = None self._playlist_lock = threading.Lock() self._playlist_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_headerbar_close_clicked) # Playlist Grid: Model self._playlist_grid_model = Gio.ListStore() self._playlist_grid_selection_multi = Gtk.MultiSelection.new( self._playlist_grid_model) self._playlist_grid_selection_single = Gtk.SingleSelection.new( self._playlist_grid_model) # Playlist Grid self.playlist_grid.set_model(self._playlist_grid_selection_single) 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.playlist_grid.set_model(self._playlist_grid_selection_multi) self.playlist_grid.set_single_click_activate(False) self.playlist_grid.get_style_context().add_class( Utils.CSS_SELECTION) else: self.actionbar_revealer.set_reveal_child(False) self.playlist_grid.set_model(self._playlist_grid_selection_single) self.playlist_grid.set_single_click_activate(True) self.playlist_grid.get_style_context().remove_class( Utils.CSS_SELECTION) @Gtk.Template.Callback() def on_clear_clicked(self, widget): self.emit('clear-playlist') @Gtk.Template.Callback() def on_playlist_grid_clicked(self, widget, position): # Get selected album item = self._playlist_grid_model.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._playlist_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_remove_clicked(self, widget): self.emit('remove-multiple-albums', self._get_selected_albums()) self.select_button.set_active(False) def on_headerbar_close_clicked(self, widget): self._close_standalone() @Gtk.Template.Callback() def on_standalone_remove_clicked(self, widget): self.emit('remove-album', self._get_selected_albums()[0]) self._close_standalone() @Gtk.Template.Callback() def on_standalone_play_clicked(self, widget): self.emit('play', self._get_selected_albums()[0]) self._close_standalone() def set_size(self, width, height): self._resize_standalone_image() def set_item_size(self, item_size): if self._item_size != item_size: self._item_size = item_size self._redraw() def get_item_size(self): return self._item_size def set_playlist(self, host, playlist): self._host = host self._playlist_stop.set() threading.Thread(target=self._set_playlist, args=( host, playlist, 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._cover_pixbuf = self._get_default_image() else: self._cover_pixbuf = self._get_default_image() # Show image GObject.idle_add(self._show_image) def stop_threads(self): self._playlist_stop.set() def _set_playlist(self, host, playlist, size): self._playlist_lock.acquire() self._playlist_stop.clear() self._playlist = playlist self._playlist_albums = {} for album in playlist: self._playlist_albums[album.get_id()] = album self._playlist_grid_model.remove_all() cache = client.MCGCache(host, size) for album in playlist: pixbuf = None # Load albumart thumbnail try: pixbuf = Utils.load_thumbnail(cache, self._client, album, size) except client.CommandException: # Exception is handled by client pass except Exception: self._logger.exception("Failed to load albumart") 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._playlist_grid_model.append(GridItem(album, pixbuf)) if self._playlist_stop.is_set(): self._playlist_lock.release() return self.playlist_grid.set_model(self._playlist_grid_selection_single) self._playlist_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._playlist is not None: self.set_playlist(self._host, self._playlist) def _open_standalone(self): self.playlist_stack.set_visible_child(self.panel_standalone) self.emit('open-standalone') def _close_standalone(self): self.playlist_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.playlist_grid.get_model().get_n_items()): if self.playlist_grid.get_model().is_selected(i): albums.append( self.playlist_grid.get_model().get_item(i).get_album()) return albums