Files
sublime-music/sublime_music/ui/search.py
Benjamin Schaaf 82a881ebfd WIP
2022-01-12 16:44:55 +11:00

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))