mcg/src/playlistpanel.py

279 lines
10 KiB
Python

#!/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