471 lines
18 KiB
Python
471 lines
18 KiB
Python
#!/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
|