#!/usr/bin/env python3 import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') import logging import math import threading 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() id = album.get_id() self._selected_albums = [album] self.emit('albumart', 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 as e: 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): """Diese Methode skaliert das geladene Bild aus dem Pixelpuffer auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse """ # 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 ratioW = float(size_width) / float(pixbuf.get_width()) ratioH = float(size_height) / float(pixbuf.get_height()) # Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren ratio = min(ratioW, ratioH) 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