263 lines
9.0 KiB
Python
263 lines
9.0 KiB
Python
from functools import partial
|
|
from typing import Optional, List, Any, Union, Tuple
|
|
|
|
import bleach
|
|
|
|
from gi.repository import Gio, GLib, GObject, Gtk, Pango, Handy, Gdk, GdkPixbuf
|
|
|
|
from ..adapters import (
|
|
AdapterManager,
|
|
api_objects as API,
|
|
Result,
|
|
)
|
|
from . import util
|
|
from ..config import AppConfiguration, ProviderConfiguration
|
|
from .actions import run_action
|
|
|
|
class SearchPanel(Gtk.ScrolledWindow):
|
|
_ratchet = 0
|
|
_query: str = ''
|
|
_search: Optional[Result] = None
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self._art_cache = {}
|
|
|
|
clamp = Handy.Clamp(margin=12)
|
|
|
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
|
|
self.entry = Gtk.Entry(hexpand=True, placeholder_text="Search")
|
|
self.entry.connect("notify::text", lambda *_: run_action(self, 'search.set-query', self.entry.get_text()))
|
|
box.pack_start(self.entry, False, False, 10)
|
|
|
|
scrolled_window = ()
|
|
|
|
self.stack = Gtk.Stack(transition_type=Gtk.StackTransitionType.NONE, homogeneous=True)
|
|
|
|
self.spinner = Gtk.Spinner(active=False, hexpand=True, vexpand=True)
|
|
self.stack.add(self.spinner)
|
|
|
|
self.store = Gtk.ListStore(
|
|
int, # type
|
|
str, # art
|
|
str, # title, subtitle
|
|
str, # id
|
|
)
|
|
self.art_cache = {}
|
|
|
|
self.list = Gtk.TreeView(
|
|
model=self.store,
|
|
reorderable=False,
|
|
headers_visible=False)
|
|
|
|
renderer = Gtk.CellRendererPixbuf(stock_size=Gtk.IconSize.LARGE_TOOLBAR)
|
|
renderer.set_fixed_size(40, 60)
|
|
column = Gtk.TreeViewColumn("", renderer)
|
|
column.set_cell_data_func(renderer, self._get_type_pixbuf)
|
|
column.set_resizable(True)
|
|
self.list.append_column(column)
|
|
|
|
renderer = Gtk.CellRendererPixbuf()
|
|
renderer.set_fixed_size(45, 60)
|
|
column = Gtk.TreeViewColumn("", renderer)
|
|
column.set_cell_data_func(renderer, self._get_result_pixbuf)
|
|
column.set_resizable(True)
|
|
self.list.append_column(column)
|
|
|
|
renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END)
|
|
column = Gtk.TreeViewColumn("", renderer, markup=2)
|
|
column.set_expand(True)
|
|
self.list.append_column(column)
|
|
|
|
renderer = Gtk.CellRendererPixbuf(icon_name="view-more-symbolic")
|
|
renderer.set_fixed_size(45, 60)
|
|
self.options_column = Gtk.TreeViewColumn("", renderer)
|
|
self.options_column.set_resizable(True)
|
|
self.list.append_column(self.options_column)
|
|
|
|
self.list.connect("button-press-event", self._on_list_button_press)
|
|
|
|
self.stack.add(self.list)
|
|
|
|
box.pack_start(self.stack, True, True, 10)
|
|
|
|
clamp.add(box)
|
|
|
|
self.add(clamp)
|
|
|
|
def _get_type_pixbuf(self,
|
|
column: Any,
|
|
cell: Gtk.CellRendererPixbuf,
|
|
model: Gtk.ListStore,
|
|
tree_iter: Gtk.TreeIter,
|
|
flags: Any):
|
|
kind = model.get_value(tree_iter, 0)
|
|
|
|
if kind == API.SearchResult.Kind.ARTIST.value:
|
|
cell.set_property("icon-name", "avatar-default-symbolic")
|
|
elif kind == API.SearchResult.Kind.ALBUM.value:
|
|
cell.set_property("icon-name", "media-optical-symbolic")
|
|
elif kind == API.SearchResult.Kind.SONG.value:
|
|
cell.set_property("icon-name", "folder-music-symbolic")
|
|
elif kind == API.SearchResult.Kind.PLAYLIST.value:
|
|
cell.set_property("icon-name", "open-menu-symbolic")
|
|
else:
|
|
assert False
|
|
|
|
def _get_pixbuf_from_path(self, path: Optional[str]):
|
|
if not path:
|
|
return None
|
|
|
|
if path in self._art_cache:
|
|
return self._art_cache[path]
|
|
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, 50, 50, True)
|
|
|
|
self._art_cache[path] = pixbuf
|
|
return pixbuf
|
|
|
|
def _get_result_pixbuf(self,
|
|
column: Any,
|
|
cell: Gtk.CellRendererPixbuf,
|
|
model: Gtk.ListStore,
|
|
tree_iter: Gtk.TreeIter,
|
|
flags: Any):
|
|
filename = model.get_value(tree_iter, 1)
|
|
pixbuf = self._get_pixbuf_from_path(filename)
|
|
if not pixbuf:
|
|
cell.set_property("icon-name", "")
|
|
else:
|
|
cell.set_property("pixbuf", pixbuf)
|
|
|
|
def _on_list_button_press(self, tree: Gtk.ListStore, event: Gdk.EventButton) -> bool:
|
|
if event.button != 1:
|
|
return False
|
|
|
|
path, column, cell_x, cell_y = tree.get_path_at_pos(event.x, event.y)
|
|
index = path.get_indices()[0]
|
|
row = self.store[index]
|
|
|
|
if column == self.options_column:
|
|
area = tree.get_cell_area(path, self.options_column)
|
|
x = area.x + area.width / 2
|
|
y = area.y + area.height / 2
|
|
|
|
# TODO: Show popup
|
|
return True
|
|
else:
|
|
if row[0] == API.SearchResult.Kind.ARTIST.value:
|
|
run_action(self, 'app.go-to-artist', row[3])
|
|
elif row[0] == API.SearchResult.Kind.ALBUM.value:
|
|
run_action(self, 'app.go-to-album', row[3])
|
|
elif row[0] == API.SearchResult.Kind.SONG.value:
|
|
run_action(self, 'app.play-song', 0, [row[3]], {"force_shuffle_state": GLib.Variant('b', False)})
|
|
elif row[0] == API.SearchResult.Kind.PLAYLIST.value:
|
|
run_action(self, 'app.go-to-playlist', row[3])
|
|
else:
|
|
assert False
|
|
|
|
return True
|
|
|
|
def update(self, app_config: AppConfiguration, force: bool = False):
|
|
query = app_config.state.search_query
|
|
|
|
if query != self._query:
|
|
self._query = query
|
|
|
|
self.entry.set_text(self._query)
|
|
|
|
if self._search:
|
|
self._search.cancel()
|
|
|
|
self._ratchet += 1
|
|
|
|
if self._query:
|
|
def search_callback(ratchet, result: API.SearchResult):
|
|
if ratchet != self._ratchet:
|
|
return
|
|
|
|
GLib.idle_add(self._update_search_results, result.get_results(), ratchet)
|
|
|
|
self._search = AdapterManager.search(
|
|
self._query,
|
|
search_callback=partial(search_callback, self._ratchet))
|
|
self._set_loading(True)
|
|
else:
|
|
self._update_search_results([], self._ratchet)
|
|
|
|
def _set_loading(self, loading: bool):
|
|
if loading:
|
|
self.spinner.start()
|
|
self.stack.set_visible_child(self.spinner)
|
|
else:
|
|
self.spinner.stop()
|
|
self.stack.set_visible_child(self.list)
|
|
|
|
def _set_art(self, index: int, path: str, ratchet: int):
|
|
if ratchet != self._ratchet:
|
|
return
|
|
|
|
self.store[index][1] = path
|
|
|
|
def _get_art_path(self, index: int, art_id: Optional[str], ratchet: int):
|
|
cover_art_result = AdapterManager.get_cover_art_uri(art_id, "file")
|
|
if not cover_art_result.data_is_available:
|
|
def on_done(result: Result):
|
|
if ratchet != self._ratchet:
|
|
return
|
|
|
|
GLib.idle_add(self._set_art, index, result.result(), ratchet)
|
|
|
|
cover_art_result.add_done_callback(on_done)
|
|
return None
|
|
|
|
# The cover art is already cached.
|
|
return cover_art_result.result()
|
|
|
|
def _update_search_results(
|
|
self,
|
|
results: List[Tuple[API.SearchResult.Kind, API.SearchResult.ValueType]],
|
|
ratchet: int,
|
|
):
|
|
if ratchet != self._ratchet:
|
|
return
|
|
|
|
self._set_loading(False)
|
|
|
|
self.store.clear()
|
|
self._art_cache = {}
|
|
|
|
for index, (kind, result) in enumerate(results):
|
|
id = result.id
|
|
|
|
if kind is API.SearchResult.Kind.ARTIST:
|
|
art_path = None
|
|
title = f"<b>{bleach.clean(result.name)}</b>"
|
|
|
|
elif kind is API.SearchResult.Kind.ALBUM:
|
|
art_path = self._get_art_path(index, result.cover_art, ratchet)
|
|
|
|
artist = bleach.clean(result.artist.name if result.artist else None)
|
|
song_count = f"{result.song_count} {util.pluralize('song', result.song_count)}"
|
|
title = f"<b>{bleach.clean(result.name)}</b>\n{util.dot_join(artist, song_count)}"
|
|
|
|
elif kind is API.SearchResult.Kind.SONG:
|
|
art_path = self._get_art_path(index, result.cover_art, ratchet)
|
|
|
|
name = bleach.clean(result.title)
|
|
album = bleach.clean(result.album.name if result.album else None)
|
|
artist = bleach.clean(result.artist.name if result.artist else None)
|
|
title = f"<b>{name}</b>\n{util.dot_join(album, artist)}"
|
|
|
|
elif kind is API.SearchResult.Kind.PLAYLIST:
|
|
art_path = None
|
|
|
|
title = f"<b>{bleach.clean(result.name)}</b>\n{result.song_count} {util.pluralize('song', result.song_count)}"
|
|
|
|
else:
|
|
assert False
|
|
|
|
self.store.append((kind.value, art_path, title, id))
|