mcg/src/librarypanel.py

472 lines
18 KiB
Python
Raw Normal View History

#!/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')
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
from mcg.utils import GridItem
from mcg.utils import SearchFilter
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/library-panel.ui')
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
library_stack = Gtk.Template.Child()
panel_normal = Gtk.Template.Child()
panel_standalone = Gtk.Template.Child()
actionbar_revealer = Gtk.Template.Child()
2023-01-08 18:20:30 +01:00
# 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()
2023-01-08 18:20:30 +01:00
# 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()
2023-01-08 18:20:30 +01:00
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 = {}
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
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):
2023-01-08 18:20:30 +01:00
return self.toolbar
def set_selected(self, selected):
self._is_selected = selected
@Gtk.Template.Callback()
2023-01-08 18:20:30 +01:00
def on_select_toggled(self, widget):
if self.select_button.get_active():
self.actionbar_revealer.set_reveal_child(True)
2023-01-08 18:20:30 +01:00
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)
2023-01-08 18:20:30 +01:00
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)
2023-01-08 18:20:30 +01:00
@Gtk.Template.Callback()
def on_update_clicked(self, widget):
self.emit('update')
2023-01-08 18:20:30 +01:00
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()
2023-01-08 18:20:30 +01:00
GObject.idle_add(self.toolbar_popover.popdown)
2023-01-08 18:20:30 +01:00
@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():
2023-01-08 18:20:30 +01:00
return
self._set_widget_grid_size(self.library_grid, size, True)
2023-01-08 18:20:30 +01:00
@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]
2023-01-08 18:20:30 +01:00
self._sort_grid_model()
self.emit('sort-order-changed', self._sort_order)
@Gtk.Template.Callback()
2023-01-08 18:20:30 +01:00
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()
2023-01-08 18:20:30 +01:00
def on_library_grid_clicked(self, widget, position):
# Get selected album
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
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):
2023-01-08 18:20:30 +01:00
self.select_button.set_active(False)
@Gtk.Template.Callback()
def on_selection_add_clicked(self, widget):
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
self.grid_scale.set_value(item_size)
self._redraw()
def get_item_size(self):
return self._item_size
def set_sort_order(self, sort):
2023-01-08 18:20:30 +01:00
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]
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
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)
2024-11-03 10:44:11 +01:00
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
2020-10-24 14:58:28 +02:00
GObject.idle_add(self._show_image)
2023-01-08 18:20:30 +01:00
def _sort_grid_model(self):
GObject.idle_add(self._library_grid_model.sort,
self._grid_model_compare_func, self._sort_order,
self._sort_type)
2023-01-08 18:20:30 +01:00
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
2023-01-08 18:20:30 +01:00
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)
2023-01-08 18:20:30 +01:00
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:
2023-01-08 18:20:30 +01:00
self._logger.exception("Failed to load albumart", e)
if pixbuf is None:
2023-01-08 18:20:30 +01:00
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()
2023-01-08 18:20:30 +01:00
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()
2023-01-08 18:20:30 +01:00
if size == self._item_size:
self._library_lock.release()
return
2023-01-08 18:20:30 +01:00
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:
2023-01-08 18:20:30 +01:00
pixbuf = self._icon_theme.lookup_icon(
Utils.STOCK_ICON_DEFAULT, None, size, size,
Gtk.TextDirection.LTR, Gtk.IconLookupFlags.FORCE_SYMBOLIC)
2023-01-08 18:20:30 +01:00
GObject.idle_add(grid_item.set_cover, pixbuf)
if self._library_stop.is_set():
self._library_lock.release()
return
self._library_lock.release()
2020-10-24 14:58:28 +02:00
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)
2023-01-08 18:20:30 +01:00
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):
2023-01-08 18:20:30 +01:00
pixel = int(width / index)
pixel = pixel - (2 * int(pixel / 100))
self.grid_scale.add_mark(pixel, Gtk.PositionType.BOTTOM, None)
2023-01-08 18:20:30 +01:00
def _open_standalone(self):
2023-01-08 18:20:30 +01:00
self.library_stack.set_visible_child(self.panel_standalone)
self.emit('open-standalone')
def _close_standalone(self):
2023-01-08 18:20:30 +01:00
self.library_stack.set_visible_child(self.panel_normal)
self.emit('close-standalone')
def _resize_standalone_image(self):
2023-01-08 18:20:30 +01:00
# 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)
2023-01-08 18:20:30 +01:00
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())
2023-01-08 18:20:30 +01:00
return albums