WIP
This commit is contained in:
@@ -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"
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
262
sublime_music/ui/search.py
Normal 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))
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user