This commit is contained in:
Benjamin Schaaf
2022-01-12 16:44:55 +11:00
parent 1f31e62340
commit 82a881ebfd
8 changed files with 365 additions and 344 deletions

View File

@@ -5,6 +5,7 @@ import abc
import logging
from datetime import datetime, timedelta
from functools import lru_cache, partial
from enum import Enum
from typing import (
Any,
Callable,
@@ -145,18 +146,23 @@ class SearchResult:
both server and local results.
"""
class Kind(Enum):
ARTIST = 0
ALBUM = 1
SONG = 2
PLAYLIST = 2
ValueType = Union[Artist, Album, Song, Playlist]
def __init__(self, query: str = None):
self.query = query
self.similiarity_partial = partial(
similarity_ratio, self.query.lower() if self.query else ""
)
self._artists: Dict[str, Artist] = {}
self._albums: Dict[str, Album] = {}
self._songs: Dict[str, Song] = {}
self._playlists: Dict[str, Playlist] = {}
self._results: Dict[Tuple[Kind, str], ValueType] = {}
def __repr__(self) -> str:
fields = ("query", "_artists", "_albums", "_songs", "_playlists")
fields = ("query", "_results")
formatted_fields = map(lambda f: f"{f}={getattr(self, f)}", fields)
return f"<SearchResult {' '.join(formatted_fields)}>"
@@ -165,76 +171,57 @@ class SearchResult:
if results is None:
return
member = f"_{result_type}"
cast(Dict[str, Any], getattr(self, member)).update({r.id: r for r in results})
for result in results:
if result_type == 'artists':
kind = self.Kind.ARTIST
elif result_type == 'albums':
kind = self.Kind.ALBUM
elif result_type == 'songs':
kind = self.Kind.SONG
elif result_type == 'playlists':
kind = self.Kind.PLAYLIST
else:
assert False
self._results[(kind, result.id)] = result
def update(self, other: "SearchResult"):
assert self.query == other.query
self._artists.update(other._artists)
self._albums.update(other._albums)
self._songs.update(other._songs)
self._playlists.update(other._playlists)
self._results.update(other._results)
_S = TypeVar("_S")
def _transform(self, kind: Kind, value: ValueType) -> Tuple[str, ...]:
if kind is self.Kind.ARTIST:
return (value.name,)
elif kind is self.Kind.ALBUM:
return (value.name, value.artist and value.artist.name)
elif kind is self.Kind.SONG:
return (value.title, value.artist and value.artist.name)
elif kind is self.Kind.PLAYLIST:
return (value.name,)
else:
assert False
def _to_result(
self,
it: Dict[str, _S],
transform: Callable[[_S], Tuple[Optional[str], ...]],
) -> List[_S]:
def get_results(self) -> List[Tuple[Kind, ValueType]]:
assert self.query
all_results = []
for value in it.values():
transformed = transform(value)
if any(t is None for t in transformed):
for (kind, _), value in self._results.items():
try:
transformed = self._transform(kind, value)
except Exception:
continue
max_similarity = max(
self.similiarity_partial(t.lower())
for t in transformed
if t is not None
)
(self.similiarity_partial(t.lower()) for t in transformed
if t is not None),
default=0)
if max_similarity < 60:
continue
all_results.append((max_similarity, value))
all_results.append((max_similarity, (kind, value)))
all_results.sort(key=lambda rx: rx[0], reverse=True)
result: List[SearchResult._S] = []
for ratio, x in all_results:
if ratio >= 60 and len(result) < 20:
result.append(x)
else:
# No use going on, all the rest are less.
break
logging.debug(similarity_ratio.cache_info())
return result
@property
def artists(self) -> List[Artist]:
return self._to_result(self._artists, lambda a: (a.name,))
def _try_get_artist_name(self, obj: Union[Album, Song]) -> Optional[str]:
try:
assert obj.artist
return obj.artist.name
except Exception:
return None
@property
def albums(self) -> List[Album]:
return self._to_result(
self._albums, lambda a: (a.name, self._try_get_artist_name(a))
)
@property
def songs(self) -> List[Song]:
return self._to_result(
self._songs, lambda s: (s.title, self._try_get_artist_name(s))
)
@property
def playlists(self) -> List[Playlist]:
return self._to_result(self._playlists, lambda p: (p.name,))
return [r for _, r in all_results]

View File

@@ -775,17 +775,15 @@ class FilesystemAdapter(CachingAdapter):
elif data_key == KEYS.SEARCH_RESULTS:
data = cast(API.SearchResult, data)
for a in data._artists.values():
self._do_ingest_new_data(KEYS.ARTIST, a.id, a, partial=True)
for a in data._albums.values():
self._do_ingest_new_data(KEYS.ALBUM, a.id, a, partial=True)
for s in data._songs.values():
self._do_ingest_new_data(KEYS.SONG, s.id, s, partial=True)
for p in data._playlists.values():
self._do_ingest_new_data(KEYS.PLAYLIST_DETAILS, p.id, p, partial=True)
for (kind, id), v in data._results.items():
if kind is API.SearchResult.Kind.ARTIST:
self._do_ingest_new_data(KEYS.ARTIST, id, v, partial=True)
elif kind is API.SearchResult.Kind.ALBUM:
self._do_ingest_new_data(KEYS.ALBUM, id, v, partial=True)
elif kind is API.SearchResult.Kind.SONG:
self._do_ingest_new_data(KEYS.SONG, id, v, partial=True)
elif kind is API.SearchResult.Kind.PLAYLIST:
self._do_ingest_new_data(KEYS.PLAYLIST_DETAILS, id, v, partial=True)
elif data_key == KEYS.SONG:
api_song = cast(API.Song, data)

View File

@@ -140,11 +140,11 @@ class SublimeMusicApp(Gtk.Application):
self.tap.start()
# self.set_accels_for_action('app.play-pause', ["<Ctrl>F"])
self.set_accels_for_action('app.play-pause', ["space"])
self.set_accels_for_action('app.prev-track', ["Home"])
self.set_accels_for_action('app.next-track', ["End"])
self.set_accels_for_action('app.quit', ["<Ctrl>q"])
self.set_accels_for_action('app.quit', ["<Ctrl>w"])
# self.set_accels_for_action('app.play-pause', ["space"])
# self.set_accels_for_action('app.prev-track', ["Home"])
# self.set_accels_for_action('app.next-track', ["End"])
# self.set_accels_for_action('app.quit', ["<Ctrl>q"])
# self.set_accels_for_action('app.quit', ["<Ctrl>w"])
def do_activate(self):
@@ -177,6 +177,10 @@ class SublimeMusicApp(Gtk.Application):
register_action(playlists, self.playlists_set_details_expanded, 'set-details-expanded')
self.window.insert_action_group('playlists', playlists)
search = Gio.SimpleActionGroup()
register_action(search, self.search_set_query, 'set-query')
self.window.insert_action_group('search', search)
settings = Gio.SimpleActionGroup()
register_dataclass_actions(settings, self.app_config, after=self._save_and_refresh)
self.window.insert_action_group('settings', settings)
@@ -1000,6 +1004,10 @@ class SublimeMusicApp(Gtk.Application):
self.app_config.state.playlist_details_expanded = expanded
self.update_window()
def search_set_query(self, query: str):
self.app_config.state.search_query = query
self.update_window()
def players_set_option(self, player: str, option: str, value: Any):
self.app_config.player_config[player][option] = value

View File

@@ -22,8 +22,11 @@ from .actions import run_action
class ArtistsPanel(Handy.Leaflet):
"""Defines the arist panel."""
def __init__(self, *args, **kwargs):
super().__init__(transition_type=Handy.LeafletTransitionType.SLIDE, can_swipe_forward=False, interpolate_size=False)
def __init__(self):
super().__init__(
transition_type=Handy.LeafletTransitionType.SLIDE,
can_swipe_forward=False,
interpolate_size=False)
list_sizer = Sizer(natural_width=400)
self.artist_list = ArtistList()
@@ -185,6 +188,10 @@ class ArtistList(Gtk.Box):
self.loading_indicator.hide()
ARTIST_ARTWORK_SIZE_DESKTOP=200
ARTIST_ARTWORK_SIZE_MOBILE=80
class ArtistDetailPanel(Gtk.Box):
"""Defines the artists list."""
@@ -251,7 +258,7 @@ class ArtistDetailPanel(Gtk.Box):
self.artist_artwork = SpinnerImage(
loading=False,
image_size=200,
image_size=ARTIST_ARTWORK_SIZE_DESKTOP,
valign=Gtk.Align.START,
)
info_panel.pack_start(self.artist_artwork, False, False, 10)
@@ -260,7 +267,7 @@ class ArtistDetailPanel(Gtk.Box):
details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.artist_name = self.make_label(
name="artist-name", ellipsize=Pango.EllipsizeMode.END
name="artist-name", wrap=True,
)
details_box.add(self.artist_name)
@@ -316,7 +323,7 @@ class ArtistDetailPanel(Gtk.Box):
self.expand_button.set_active(not self.show_mobile)
self.artist_bio_revealer.set_reveal_child(not self.show_mobile)
self.expand_button_revealer.set_reveal_child(self.show_mobile)
self.artist_artwork.set_image_size( 120 if self.show_mobile else 200)
self.artist_artwork.set_image_size(ARTIST_ARTWORK_SIZE_MOBILE if self.show_mobile else ARTIST_ARTWORK_SIZE_DESKTOP)
def on_expand_button_clicked(self, *_):
up_down = "up" if self.expand_button.get_active() else "down"

View File

@@ -106,11 +106,10 @@ class AlbumWithSongs(Gtk.Box):
album_details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
# TODO (#43): deal with super long-ass titles
self.title = Gtk.Label(
name="artist-album-list-album-name",
halign=Gtk.Align.START,
ellipsize=Pango.EllipsizeMode.END,
wrap=True,
)
album_details.pack_start(self.title, False, False, 0)

View File

@@ -14,7 +14,7 @@ from ..adapters import (
)
from ..config import AppConfiguration, ProviderConfiguration
from ..players import PlayerManager
from . import albums, artists, browse, player_controls, playlists, util
from . import albums, artists, browse, search, player_controls, playlists, util
from .common import IconButton, IconToggleButton, IconMenuButton, SpinnerImage
from .actions import run_action
from .providers import ProvidersWindow
@@ -36,12 +36,14 @@ class MainWindow(Handy.ApplicationWindow):
self.artists_panel = artists.ArtistsPanel()
self.browse_panel = browse.BrowsePanel()
self.playlists_panel = playlists.PlaylistsPanel()
self.search_panel = search.SearchPanel()
self.stack = self._create_stack(
Albums=self.albums_panel,
Artists=self.artists_panel,
# Browse=self.browse_panel,
Playlists=self.playlists_panel,
)
self.stack.add_named(self.search_panel, "Search")
self.stack.set_transition_type(Gtk.StackTransitionType.NONE)
self.sidebar_flap = Handy.Flap(
@@ -53,6 +55,9 @@ class MainWindow(Handy.ApplicationWindow):
def stack_changed(*_):
self.sidebar_flap.set_reveal_flap(False)
if self.stack.get_visible_child() == self.search_panel:
self.search_panel.entry.grab_focus()
run_action(self, 'app.change-tab', self.stack.get_visible_child_name())
self.stack.connect("notify::visible-child", stack_changed)
@@ -159,8 +164,6 @@ class MainWindow(Handy.ApplicationWindow):
self.add(box)
self.connect("button-release-event", self._on_button_release)
self._settings_window = SettingsWindow(self)
self._downloads_window = DownloadsWindow(self)
self._providers_window = ProvidersWindow(self)
@@ -261,19 +264,12 @@ class MainWindow(Handy.ApplicationWindow):
desktop_header.set_show_close_button(True)
desktop_header.props.title = "Sublime Music"
# Search
self.search_entry = Gtk.SearchEntry(placeholder_text="Search everything...")
self.search_entry.connect("focus-in-event", self._on_search_entry_focus)
self.search_entry.connect(
"button-press-event", self._on_search_entry_button_press
)
self.search_entry.connect("focus-out-event", self._on_search_entry_loose_focus)
self.search_entry.connect("changed", self._on_search_entry_changed)
self.search_entry.connect("stop-search", self._on_search_entry_stop_search)
# desktop_header.pack_start(self.search_entry)
# Search popup
self._create_search_popup()
search_button = IconButton(
icon_name='system-search-symbolic',
tooltip_text="Search Everything",
relief=True)
search_button.connect('clicked', lambda *_: self.stack.set_visible_child(self.search_panel))
desktop_header.pack_start(search_button)
# Stack switcher
switcher = Gtk.StackSwitcher(stack=stack)
@@ -322,6 +318,13 @@ class MainWindow(Handy.ApplicationWindow):
self.sidebar_flap.bind_property("reveal-flap", button, "active", GObject.BindingFlags.BIDIRECTIONAL)
mobile_header.pack_start(button)
search_button = IconButton(
icon_name='system-search-symbolic',
tooltip_text="Search Everything",
relief=True)
search_button.connect('clicked', lambda *_: self.stack.set_visible_child(self.search_panel))
mobile_header.pack_end(search_button)
squeezer.add(mobile_header)
def squeezer_changed(squeezer, _):
@@ -363,20 +366,6 @@ class MainWindow(Handy.ApplicationWindow):
return box
def _create_label(
self, text: str, *args, halign: Gtk.Align = Gtk.Align.START, **kwargs
) -> Gtk.Label:
label = Gtk.Label(
use_markup=True,
halign=halign,
ellipsize=Pango.EllipsizeMode.END,
*args,
**kwargs,
)
label.set_markup(text)
label.get_style_context().add_class("search-result-row")
return label
def _create_toggle_menu_button(
self, label: str, settings_name: str
) -> Tuple[Gtk.Box, Gtk.Switch]:
@@ -443,76 +432,6 @@ class MainWindow(Handy.ApplicationWindow):
self._emit_settings_change({setting: self.get_property(prop.name)})
def _create_search_popup(self) -> Gtk.PopoverMenu:
self.search_popup = Gtk.PopoverMenu(modal=False)
results_scrollbox = Gtk.ScrolledWindow(
min_content_width=500,
min_content_height=700,
)
def make_search_result_header(text: str) -> Gtk.Label:
label = self._create_label(text)
label.get_style_context().add_class("search-result-header")
return label
search_results_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
name="search-results",
)
self.search_results_loading = Gtk.Spinner(active=False, name="search-spinner")
search_results_box.add(self.search_results_loading)
search_results_box.add(make_search_result_header("Songs"))
self.song_results = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
search_results_box.add(self.song_results)
search_results_box.add(make_search_result_header("Albums"))
self.album_results = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
search_results_box.add(self.album_results)
search_results_box.add(make_search_result_header("Artists"))
self.artist_results = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
search_results_box.add(self.artist_results)
search_results_box.add(make_search_result_header("Playlists"))
self.playlist_results = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
search_results_box.add(self.playlist_results)
results_scrollbox.add(search_results_box)
self.search_popup.add(results_scrollbox)
self.search_popup.set_relative_to(self.search_entry)
rect = Gdk.Rectangle()
rect.x = 22
rect.y = 28
rect.width = 1
rect.height = 1
self.search_popup.set_pointing_to(rect)
self.search_popup.set_position(Gtk.PositionType.BOTTOM)
# Event Listeners
# =========================================================================
def _on_button_release(self, win: Any, event: Gdk.EventButton) -> bool:
if not self._event_in_widgets(event, self.search_entry, self.search_popup):
self._hide_search()
# if not self._event_in_widgets(
# event,
# self.player_controls.device_button,
# self.player_controls.device_popover,
# ):
# self.player_controls.device_popover.popdown()
# if not self._event_in_widgets(
# event,
# self.player_controls.play_queue_button,
# self.player_controls.play_queue_popover,
# ):
# self.player_controls.play_queue_popover.popdown()
return False
def show_providers_window(self):
if self.is_initialized:
self._providers_window.open_status_page()
@@ -521,8 +440,6 @@ class MainWindow(Handy.ApplicationWindow):
self._show_transient_window(self._providers_window)
_transient_window = None
def _show_transient_window(self, window):
@@ -542,55 +459,6 @@ class MainWindow(Handy.ApplicationWindow):
run_action(self, 'app.refresh')
def _on_search_entry_focus(self, *args):
self._show_search()
def _on_search_entry_button_press(self, *args):
self._show_search()
def _on_search_entry_loose_focus(self, *args):
self._hide_search()
search_idx = 0
searches: Set[Result] = set()
def _on_search_entry_changed(self, entry: Gtk.Entry):
while len(self.searches) > 0:
search = self.searches.pop()
if search:
search.cancel()
if not self.search_popup.is_visible():
self.search_popup.show_all()
self.search_popup.popup()
def search_result_calback(idx: int, result: API.SearchResult):
# Ignore slow returned searches.
if idx < self.search_idx:
return
GLib.idle_add(self._update_search_results, result)
def search_result_done(r: Result):
if r.result() is True:
# The search was cancelled
return
# If all results are back, the stop the loading indicator.
GLib.idle_add(self._set_search_loading, False)
self.search_idx += 1
search_result = AdapterManager.search(
entry.get_text(),
search_callback=partial(search_result_calback, self.search_idx),
before_download=lambda: self._set_search_loading(True),
)
search_result.add_done_callback(search_result_done)
self.searches.add(search_result)
def _on_search_entry_stop_search(self, entry: Any):
self.search_popup.popdown()
# Helper Functions
# =========================================================================
def _emit_settings_change(self, changed_settings: Dict[str, Any]):
@@ -598,119 +466,10 @@ class MainWindow(Handy.ApplicationWindow):
return
self.emit("refresh-window", {"__settings__": changed_settings}, False)
def _show_search(self):
self.search_entry.set_size_request(300, -1)
self.search_popup.show_all()
self.search_results_loading.hide()
self.search_popup.popup()
def _hide_search(self):
self.search_popup.popdown()
self.search_entry.set_size_request(-1, -1)
def _set_search_loading(self, loading_state: bool):
if loading_state:
self.search_results_loading.start()
self.search_results_loading.show_all()
else:
self.search_results_loading.stop()
self.search_results_loading.hide()
def _remove_all_from_widget(self, widget: Gtk.Widget):
for c in widget.get_children():
widget.remove(c)
def _create_search_result_row(
self, text: str, action_name: str, id: str, cover_art_id: Optional[str]
) -> Gtk.Button:
def on_search_row_button_press(*args):
self.emit("go-to", action_name, id)
self._hide_search()
row = Gtk.Button(relief=Gtk.ReliefStyle.NONE)
row.connect("button-press-event", on_search_row_button_press)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
image = SpinnerImage(image_name="search-artwork", image_size=30)
box.add(image)
box.add(self._create_label(text))
row.add(box)
def image_callback(f: Result):
image.set_loading(False)
image.set_from_file(f.result())
artwork_future = AdapterManager.get_cover_art_uri(cover_art_id, "file")
artwork_future.add_done_callback(lambda f: GLib.idle_add(image_callback, f))
return row
def _update_search_results(self, search_results: API.SearchResult):
# Songs
if search_results.songs is not None:
self._remove_all_from_widget(self.song_results)
for song in search_results.songs:
label_text = util.dot_join(
f"<b>{song.title}</b>",
song.artist.name if song.artist else None,
)
assert song.album and song.album.id
self.song_results.add(
self._create_search_result_row(
bleach.clean(label_text), "album", song.album.id, song.cover_art
)
)
self.song_results.show_all()
# Albums
if search_results.albums is not None:
self._remove_all_from_widget(self.album_results)
for album in search_results.albums:
label_text = util.dot_join(
f"<b>{album.name}</b>",
album.artist.name if album.artist else None,
)
assert album.id
self.album_results.add(
self._create_search_result_row(
bleach.clean(label_text), "album", album.id, album.cover_art
)
)
self.album_results.show_all()
# Artists
if search_results.artists is not None:
self._remove_all_from_widget(self.artist_results)
for artist in search_results.artists:
assert artist.id
self.artist_results.add(
self._create_search_result_row(
bleach.clean(artist.name),
"artist",
artist.id,
artist.artist_image_url,
)
)
self.artist_results.show_all()
# Playlists
if search_results.playlists:
self._remove_all_from_widget(self.playlist_results)
for playlist in search_results.playlists:
self.playlist_results.add(
self._create_search_result_row(
bleach.clean(playlist.name),
"playlist",
playlist.id,
playlist.cover_art,
)
)
self.playlist_results.show_all()
def _event_in_widgets(self, event: Gdk.EventButton, *widgets) -> bool:
for widget in widgets:
if not widget.is_visible():

262
sublime_music/ui/search.py Normal file
View File

@@ -0,0 +1,262 @@
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))

View File

@@ -81,6 +81,7 @@ class UIState:
playlist_details_expanded: bool = True
artist_details_expanded: bool = True
loading_play_queue: bool = False
search_query: str = ''
current_album_search_query: AlbumSearchQuery = AlbumSearchQuery(
AlbumSearchQuery.Type.RANDOM,