Merge branch 'master' of gitlab.com:robozman/libremsonic

This commit is contained in:
Sumner Evans
2019-09-03 08:35:39 -06:00
16 changed files with 453 additions and 209 deletions

View File

@@ -67,9 +67,11 @@ class LibremsonicApp(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
def add_action(name: str, fn):
def add_action(name: str, fn, parameter_type=None):
"""Registers an action with the application."""
action = Gio.SimpleAction.new(name, None)
if type(parameter_type) == str:
parameter_type = GLib.VariantType(parameter_type)
action = Gio.SimpleAction.new(name, parameter_type)
action.connect('activate', fn)
self.add_action(action)
@@ -84,6 +86,12 @@ class LibremsonicApp(Gtk.Application):
add_action('repeat-press', self.on_repeat_press)
add_action('shuffle-press', self.on_shuffle_press)
# Navigation actions.
add_action('play-next', self.on_play_next, parameter_type='as')
add_action('add-to-queue', self.on_add_to_queue, parameter_type='as')
add_action('go-to-album', self.on_go_to_album, parameter_type='s')
add_action('go-to-artist', self.on_go_to_artist, parameter_type='s')
add_action('mute-toggle', self.on_mute_toggle)
add_action(
'update-play-queue-from-server',
@@ -135,6 +143,7 @@ class LibremsonicApp(Gtk.Application):
self.update_window()
# Configure the players
self.last_play_queue_update = 0
def time_observer(value):
@@ -207,6 +216,9 @@ class LibremsonicApp(Gtk.Application):
dialog.destroy()
def on_play_pause(self, *args):
if self.state.current_song is None:
return
if self.player.song_loaded:
self.player.toggle_play()
self.save_play_queue()
@@ -269,6 +281,34 @@ class LibremsonicApp(Gtk.Application):
self.state.shuffle_on = not self.state.shuffle_on
self.update_window()
def on_play_next(self, action, song_ids):
if self.state.current_song is None:
insert_at = 0
else:
insert_at = (
self.state.play_queue.index(self.state.current_song.id) + 1)
self.state.play_queue = (self.state.play_queue[:insert_at]
+ list(song_ids)
+ self.state.play_queue[insert_at:])
self.state.old_play_queue.extend(song_ids)
self.update_window()
def on_add_to_queue(self, action, song_ids):
self.state.play_queue.extend(song_ids)
self.state.old_play_queue.extend(song_ids)
self.update_window()
def on_go_to_album(self, action, album_id):
# TODO
self.state.current_tab = 'albums'
self.update_window()
def on_go_to_artist(self, action, artist_id):
self.state.current_tab = 'artists'
self.state.selected_artist_id = artist_id.get_string()
self.update_window()
def on_server_list_changed(self, action, servers):
self.state.config.servers = servers
self.state.save()
@@ -278,11 +318,12 @@ class LibremsonicApp(Gtk.Application):
self.state.save()
self.reset_cache_manager()
self.update_window()
def reset_cache_manager(self):
CacheManager.reset(
self.state.config,
self.state.config.servers[self.state.config.current_server]
self.current_server
if self.state.config.current_server >= 0 else None,
)
@@ -290,13 +331,17 @@ class LibremsonicApp(Gtk.Application):
self.update_window()
def on_stack_change(self, stack, child):
self.state.current_tab = stack.get_visible_child_name()
self.update_window()
def on_song_clicked(self, win, song_id, song_queue):
def on_song_clicked(self, win, song_id, song_queue, metadata):
# Reset the play queue so that we don't ever revert back to the
# previous one.
old_play_queue = song_queue.copy()
if metadata.get('force_shuffle_state') is not None:
self.state.shuffle_on = metadata['force_shuffle_state']
# If shuffle is enabled, then shuffle the playlist.
if self.state.shuffle_on:
song_queue.remove(song_id)
@@ -383,6 +428,11 @@ class LibremsonicApp(Gtk.Application):
self.save_play_queue()
CacheManager.shutdown()
# ########## PROPERTIES ########## #
@property
def current_server(self):
return self.state.config.servers[self.state.config.current_server]
# ########## HELPER METHODS ########## #
def show_configure_servers_dialog(self):
"""Show the Connect to Server dialog."""
@@ -435,7 +485,6 @@ class LibremsonicApp(Gtk.Application):
uri, stream = CacheManager.get_song_filename_or_stream(
song,
force_stream=self.state.config.always_stream,
format='mp3',
)
self.state.current_song = song
@@ -448,7 +497,10 @@ class LibremsonicApp(Gtk.Application):
self.player.reset()
self.state.song_progress = 0
def on_song_download_complete(_):
def on_song_download_complete(song_id):
if self.state.current_song != song.id:
return
# Switch to the local media if the player can hotswap (MPV can,
# Chromecast cannot hotswap without lag).
if self.player.can_hotswap_source:
@@ -494,18 +546,21 @@ class LibremsonicApp(Gtk.Application):
self.update_window),
)
if self.current_server.sync_enabled:
CacheManager.scrobble(song.id)
song_details_future = CacheManager.get_song_details(song)
song_details_future.add_done_callback(
lambda f: GLib.idle_add(do_play_song, f.result()), )
def save_play_queue(self):
if len(self.state.play_queue) == 0:
return
position = self.state.song_progress
self.last_play_queue_update = position
current_server = self.state.config.current_server
current_server = self.state.config.servers[current_server]
if current_server.sync_enabled:
if self.current_server.sync_enabled:
CacheManager.executor.submit(
CacheManager.save_play_queue,
id=self.state.play_queue,

View File

@@ -570,6 +570,12 @@ class CacheManager(metaclass=Singleton):
def get_play_queue(self) -> Future:
return CacheManager.executor.submit(self.server.get_play_queue)
def scrobble(self, song_id: int) -> Future:
def do_scrobble():
self.server.scrobble(song_id)
return CacheManager.executor.submit(do_scrobble)
def get_song_filename_or_stream(
self,
song: Child,

View File

@@ -39,17 +39,19 @@ class ApplicationState:
loads.
"""
config: AppConfiguration = AppConfiguration()
current_song: Child
config_file: str
current_song: Child = None
config_file: str = None
playing: bool = False
play_queue: List[str]
old_play_queue: List[str]
play_queue: List[str] = []
old_play_queue: List[str] = []
volume: int = 100
old_volume: int = 100
repeat_type: RepeatType = RepeatType.NO_REPEAT
shuffle_on: bool = False
song_progress: float = 0
current_device: str = 'this device'
current_tab: str = 'albums'
selected_artist_id: str = None
def to_json(self):
current_song = (self.current_song.id if
@@ -65,6 +67,8 @@ class ApplicationState:
'shuffle_on': getattr(self, 'shuffle_on', None),
'song_progress': getattr(self, 'song_progress', None),
'current_device': getattr(self, 'current_device', 'this device'),
'current_tab': getattr(self, 'current_tab', 'albums'),
'selected_artist_id': getattr(self, 'selected_artist_id', None),
}
def load_from_json(self, json_object):
@@ -83,6 +87,8 @@ class ApplicationState:
self.shuffle_on = json_object.get('shuffle_on', False)
self.song_progress = json_object.get('song_progress', 0.0)
self.current_device = json_object.get('current_device', 'this device')
self.current_tab = json_object.get('current_tab', 'albums')
self.selected_artist_id = json_object.get('selected_artist_id', None)
def load(self):
self.config = self.get_config(self.config_file)

View File

@@ -1,5 +1,4 @@
import gi
import threading
from typing import Optional, Union
gi.require_version('Gtk', '3.0')
@@ -8,7 +7,7 @@ from gi.repository import Gtk, GObject, GLib
from libremsonic.state_manager import ApplicationState
from libremsonic.cache_manager import CacheManager
from libremsonic.ui import util
from libremsonic.ui.common import AlbumWithSongs, CoverArtGrid
from libremsonic.ui.common import AlbumWithSongs, IconButton, CoverArtGrid
from libremsonic.server.api_objects import Child, AlbumWithSongsID3
@@ -20,7 +19,7 @@ class AlbumsPanel(Gtk.Box):
'song-clicked': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(str, object),
(str, object, object),
),
}
@@ -84,7 +83,7 @@ class AlbumsPanel(Gtk.Box):
self.to_year_entry.connect('changed', self.on_year_changed)
actionbar.pack_start(self.to_year_entry)
refresh = util.button_with_icon('view-refresh')
refresh = IconButton('view-refresh')
refresh.connect('clicked', lambda *a: self.update(force=True))
actionbar.pack_end(refresh)
@@ -94,7 +93,8 @@ class AlbumsPanel(Gtk.Box):
self.grid = AlbumsGrid()
self.grid.connect(
'song-clicked',
lambda _, song, queue: self.emit('song-clicked', song, queue),
lambda _, song, queue, metadata: self.emit('song-clicked', song,
queue, metadata),
)
scrolled_window.add(self.grid)
self.add(scrolled_window)
@@ -124,6 +124,7 @@ class AlbumsPanel(Gtk.Box):
model.append((genre.value, genre.value))
self.populating_genre_combo = False
# TODO Get this from state
self.genre_combo.set_active_id(self.currently_active_genre)
genres_future = CacheManager.get_genres()

View File

@@ -4,6 +4,15 @@
margin-bottom: 10px;
}
#icon-button-box image {
margin: 5px 2px;
}
#icon-button-box label {
margin-left: 5px;
margin-right: 3px;
}
/* ********** Playlist ********** */
#playlist-list-listbox row {
margin: 0;
@@ -58,6 +67,10 @@
margin: -10px 0 0 10px;
}
#playlist-play-shuffle-buttons {
margin-bottom: 10px;
}
/* ********** Playback Controls ********** */
#player-controls-album-artwork {
min-height: 70px;
@@ -73,8 +86,10 @@
/* Make the play icon look centered. */
#player-controls-bar #play-button image {
margin-left: 1px;
margin-left: 5px;
margin-right: 5px;
margin-top: 1px;
margin-bottom: 1px;
}
#player-controls-bar #song-scrubber {

View File

@@ -3,12 +3,12 @@ from typing import List, Union, Optional
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, Pango
from gi.repository import Gtk, GObject, Pango, GLib
from libremsonic.state_manager import ApplicationState
from libremsonic.cache_manager import CacheManager
from libremsonic.ui import util
from libremsonic.ui.common import AlbumWithSongs, SpinnerImage
from libremsonic.ui.common import AlbumWithSongs, IconButton, SpinnerImage
from libremsonic.server.api_objects import (
AlbumID3,
@@ -25,7 +25,7 @@ class ArtistsPanel(Gtk.Paned):
'song-clicked': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(str, object),
(str, object, object),
),
}
artist_id: Optional[str] = None
@@ -33,49 +33,30 @@ class ArtistsPanel(Gtk.Paned):
def __init__(self, *args, **kwargs):
Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
self.selected_artist = None
self.artist_list = ArtistList()
self.artist_list.connect(
'selection-changed',
self.on_list_selection_changed,
)
self.pack1(self.artist_list, False, False)
self.artist_detail_panel = ArtistDetailPanel()
self.artist_detail_panel.connect(
'song-clicked',
lambda _, song, queue: self.emit('song-clicked', song, queue),
lambda _, song, queue, metadata: self.emit('song-clicked', song,
queue, metadata),
)
self.pack2(self.artist_detail_panel, True, False)
def update(self, state: ApplicationState):
self.artist_list.update(state)
if self.artist_id:
self.artist_detail_panel.update(self.artist_id)
def on_list_selection_changed(self, artist_list, artist):
self.artist_id = artist.id
self.artist_detail_panel.update(self.artist_id)
if state.selected_artist_id:
self.artist_detail_panel.update(state.selected_artist_id)
class ArtistList(Gtk.Box):
__gsignals__ = {
'selection-changed': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(object, ),
),
}
def __init__(self):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.artist_map = {}
list_actions = Gtk.ActionBar()
refresh = util.button_with_icon('view-refresh')
refresh = IconButton('view-refresh')
refresh.connect('clicked', lambda *a: self.update(force=True))
list_actions.pack_end(refresh)
@@ -92,24 +73,19 @@ class ArtistList(Gtk.Box):
self.loading_indicator.add(loading_spinner)
self.list.add(self.loading_indicator)
self.list.connect('row-activated', self.on_row_activated)
list_scroll_window.add(self.list)
self.pack_start(list_scroll_window, True, True, 0)
def update(self, state=None, force=False):
self.update_list(force=force)
self.update_list(force=force, state=state)
@util.async_callback(
lambda *a, **k: CacheManager.get_artists(*a, **k),
before_download=lambda self: self.loading_indicator.show(),
on_failure=lambda self, e: self.loading_indicator.hide(),
)
def update_list(self, artists):
selected_row = self.list.get_selected_row()
selected_artist = None
if selected_row:
selected_artist = self.artist_map.get(selected_row.get_index())
def update_list(self, artists, state: ApplicationState):
# TODO use a diff here
# Remove everything
for row in self.list.get_children()[1:]:
self.list.remove(row)
@@ -118,9 +94,9 @@ class ArtistList(Gtk.Box):
for i, artist in enumerate(artists):
# Use i + 1 because of the loading indicator in index 0.
if selected_artist and artist.id == selected_artist.id:
if (state.selected_artist_id
and artist.id == (state.selected_artist_id or -1)):
selected_idx = i + 1
self.artist_map[i + 1] = artist
label_text = [f'<b>{util.esc(artist.name)}</b>']
@@ -129,7 +105,11 @@ class ArtistList(Gtk.Box):
label_text.append('{} {}'.format(
album_count, util.pluralize('album', album_count)))
self.list.add(
row = Gtk.ListBoxRow(
action_name='app.go-to-artist',
action_target=GLib.Variant('s', artist.id),
)
row.add(
Gtk.Label(
label='\n'.join(label_text),
use_markup=True,
@@ -138,16 +118,16 @@ class ArtistList(Gtk.Box):
ellipsize=Pango.EllipsizeMode.END,
max_width_chars=30,
))
self.list.add(row)
if selected_idx:
row = self.list.get_row_at_index(selected_idx)
# TODO scroll to the row
self.list.select_row(row)
self.list.show_all()
self.loading_indicator.hide()
def on_row_activated(self, listbox, row):
self.emit('selection-changed', self.artist_map[row.get_index()])
class ArtistDetailPanel(Gtk.Box):
"""Defines the artists list."""
@@ -156,7 +136,7 @@ class ArtistDetailPanel(Gtk.Box):
'song-clicked': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(str, object),
(str, object, object),
),
}
@@ -190,12 +170,12 @@ class ArtistDetailPanel(Gtk.Box):
self.artist_action_buttons = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL)
view_refresh_button = util.button_with_icon('view-refresh-symbolic')
view_refresh_button = IconButton('view-refresh-symbolic')
view_refresh_button.connect('clicked', self.on_view_refresh_click)
self.artist_action_buttons.pack_end(view_refresh_button, False, False,
5)
download_all_btn = util.button_with_icon('folder-download-symbolic')
download_all_btn = IconButton('folder-download-symbolic')
download_all_btn.connect('clicked', self.on_download_all_click)
self.artist_action_buttons.pack_end(download_all_btn, False, False, 5)
@@ -237,14 +217,15 @@ class ArtistDetailPanel(Gtk.Box):
self.albums_list = AlbumsListWithSongs()
self.albums_list.connect(
'song-clicked',
lambda _, song, queue: self.emit('song-clicked', song, queue),
lambda _, song, queue, metadata: self.emit('song-clicked', song,
queue, metadata),
)
artist_info_box.pack_start(self.albums_list, True, True, 0)
self.add(artist_info_box)
def update(self, album_id):
self.update_artist_view(album_id)
def update(self, artist_id):
self.update_artist_view(artist_id)
def get_model_list_future(self, before_download):
def do_get_model_list() -> List[Child]:
@@ -259,7 +240,11 @@ class ArtistDetailPanel(Gtk.Box):
before_download=lambda self: self.artist_artwork.set_loading(True),
on_failure=lambda self, e: print('fail a', e),
)
def update_artist_view(self, artist: ArtistWithAlbumsID3):
def update_artist_view(
self,
artist: ArtistWithAlbumsID3,
state: ApplicationState,
):
self.artist_id = artist.id
self.artist_indicator.set_text('ARTIST')
self.artist_name.set_markup(util.esc(f'<b>{artist.name}</b>'))
@@ -274,7 +259,11 @@ class ArtistDetailPanel(Gtk.Box):
@util.async_callback(
lambda *a, **k: CacheManager.get_artist_info(*a, **k),
)
def update_artist_info(self, artist_info: ArtistInfo2):
def update_artist_info(
self,
artist_info: ArtistInfo2,
state: ApplicationState,
):
self.artist_bio.set_markup(util.esc(''.join(artist_info.biography)))
if len(artist_info.similarArtist or []) > 0:
@@ -285,9 +274,10 @@ class ArtistDetailPanel(Gtk.Box):
for artist in artist_info.similarArtist[:5]:
self.similar_artists_button_box.add(
Gtk.LinkButton(
uri=f'artist://{artist.id}',
label=artist.name,
name='similar-artist-button',
action_name='app.go-to-artist',
action_target=GLib.Variant('s', artist.id),
))
self.similar_artists_box.show_all()
else:
@@ -298,7 +288,11 @@ class ArtistDetailPanel(Gtk.Box):
before_download=lambda self: self.artist_artwork.set_loading(True),
on_failure=lambda self, e: self.artist_artwork.set_loading(False),
)
def update_artist_artwork(self, cover_art_filename):
def update_artist_artwork(
self,
cover_art_filename,
state: ApplicationState,
):
self.artist_artwork.set_from_file(cover_art_filename)
self.artist_artwork.set_loading(False)
@@ -308,10 +302,20 @@ class ArtistDetailPanel(Gtk.Box):
self.update_artist_view(self.artist_id, force=True)
def on_download_all_click(self, btn):
print('download all')
songs_for_download = []
artist = CacheManager.get_artist(self.artist_id).result()
for album in artist.album:
print(album)
for album in (artist.get('album', artist.get('child', []))):
album_songs = CacheManager.get_album(album.id).result()
album_songs = album_songs.get('child', album_songs.get('song', []))
for song in album_songs:
songs_for_download.append(song.id)
CacheManager.batch_download_songs(
songs_for_download,
before_download=lambda: self.update_artist_view(self.artist_id),
on_song_download_complete=lambda i: self.update_artist_view(
self.artist_id),
)
# Helper Methods
# =========================================================================
@@ -351,7 +355,7 @@ class AlbumsListWithSongs(Gtk.Overlay):
'song-clicked': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(str, object),
(str, object, object),
),
}
@@ -381,7 +385,8 @@ class AlbumsListWithSongs(Gtk.Overlay):
album_with_songs = AlbumWithSongs(album, show_artist_name=False)
album_with_songs.connect(
'song-clicked',
lambda _, song, queue: self.emit('song-clicked', song, queue),
lambda _, song, queue, metadata: self.emit(
'song-clicked', song, queue, metadata),
)
album_with_songs.connect('song-selected', self.on_song_selected)
album_with_songs.show_all()

View File

@@ -1,6 +1,13 @@
from .album_with_songs import AlbumWithSongs
from .cover_art_grid import CoverArtGrid
from .edit_form_dialog import EditFormDialog
from .icon_button import IconButton
from .spinner_image import SpinnerImage
__all__ = ('AlbumWithSongs', 'CoverArtGrid', 'EditFormDialog', 'SpinnerImage')
__all__ = (
'AlbumWithSongs',
'CoverArtGrid',
'EditFormDialog',
'IconButton',
'SpinnerImage',
)

View File

@@ -1,12 +1,15 @@
from typing import Union
from random import randint
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, Pango, GLib
from libremsonic.state_manager import ApplicationState
from libremsonic.cache_manager import CacheManager
from libremsonic.ui import util
from .icon_button import IconButton
from .spinner_image import SpinnerImage
from libremsonic.server.api_objects import (
@@ -26,7 +29,7 @@ class AlbumWithSongs(Gtk.Box):
'song-clicked': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(str, object),
(str, object, object),
),
}
@@ -60,13 +63,44 @@ class AlbumWithSongs(Gtk.Box):
lambda f: GLib.idle_add(cover_art_future_done, f))
album_details = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
album_details.add(
album_title_and_buttons = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL)
# TODO: deal with super long-ass titles
album_title_and_buttons.add(
Gtk.Label(
label=album.get('name', album.get('title')),
name='artist-album-list-album-name',
halign=Gtk.Align.START,
))
self.play_btn = IconButton('media-playback-start-symbolic',
sensitive=False)
self.play_btn.connect('clicked', self.play_btn_clicked)
album_title_and_buttons.pack_start(self.play_btn, False, False, 5)
self.shuffle_btn = IconButton('media-playlist-shuffle-symbolic',
sensitive=False)
self.shuffle_btn.connect('clicked', self.shuffle_btn_clicked)
album_title_and_buttons.pack_start(self.shuffle_btn, False, False, 5)
self.play_next_btn = IconButton('go-top-symbolic',
action_name='app.play-next')
album_title_and_buttons.pack_start(self.play_next_btn, False, False, 5)
self.add_to_queue_btn = IconButton('go-jump-symbolic',
action_name='app.add-to-queue')
album_title_and_buttons.pack_start(self.add_to_queue_btn, False, False,
5)
self.download_all_btn = IconButton('folder-download-symbolic',
sensitive=False)
self.download_all_btn.connect('clicked', self.on_download_all_click)
album_title_and_buttons.pack_end(self.download_all_btn, False, False,
5)
album_details.add(album_title_and_buttons)
stats = [
album.artist if show_artist_name else None,
album.year,
@@ -144,6 +178,8 @@ class AlbumWithSongs(Gtk.Box):
self.update_album_songs(album.id)
# Event Handlers
# =========================================================================
def on_song_selection_change(self, event):
if not self.album_songs.has_focus():
self.emit('song-selected')
@@ -152,7 +188,7 @@ class AlbumWithSongs(Gtk.Box):
# The song ID is in the last column of the model.
song_id = self.album_song_store[idx][-1]
self.emit('song-clicked', song_id,
[m[-1] for m in self.album_song_store])
[m[-1] for m in self.album_song_store], {})
def on_song_button_press(self, tree, event):
if event.button == 3: # Right click
@@ -192,6 +228,34 @@ class AlbumWithSongs(Gtk.Box):
if not allow_deselect:
return True
def on_download_all_click(self, btn):
CacheManager.batch_download_songs(
[x[-1] for x in self.album_song_store],
before_download=self.update,
on_song_download_complete=lambda x: self.update(),
)
def play_btn_clicked(self, btn):
song_ids = [x[-1] for x in self.album_song_store]
self.emit(
'song-clicked',
song_ids[0],
song_ids,
{'force_shuffle_state': False},
)
def shuffle_btn_clicked(self, btn):
rand_idx = randint(0, len(self.album_song_store) - 1)
song_ids = [x[-1] for x in self.album_song_store]
self.emit(
'song-clicked',
song_ids[rand_idx],
song_ids,
{'force_shuffle_state': True},
)
# Helper Methods
# =========================================================================
def deselect_all(self):
self.album_songs.get_selection().unselect_all()
@@ -206,13 +270,24 @@ class AlbumWithSongs(Gtk.Box):
def update_album_songs(
self,
album: Union[AlbumWithSongsID3, Child, Directory],
state: ApplicationState,
):
new_store = [[
util.get_cached_status_icon(CacheManager.get_cached_status(song)),
util.esc(song.title),
util.format_song_duration(song.duration),
song.id,
] for song in album.get('child', album.get('song', []))]
] for song in (album.get('child') or album.get('song') or [])]
song_ids = [song[-1] for song in new_store]
self.play_btn.set_sensitive(True)
self.shuffle_btn.set_sensitive(True)
self.play_next_btn.set_action_target_value(GLib.Variant(
'as', song_ids))
self.add_to_queue_btn.set_action_target_value(
GLib.Variant('as', song_ids))
self.download_all_btn.set_sensitive(True)
util.diff_store(self.album_song_store, new_store)
self.loading_indicator.hide()

View File

@@ -15,7 +15,7 @@ class CoverArtGrid(Gtk.ScrolledWindow):
'song-clicked': (
GObject.SignalFlags.RUN_FIRST,
GObject.TYPE_NONE,
(str, object),
(str, object, object),
)
}
@@ -131,7 +131,6 @@ class CoverArtGrid(Gtk.ScrolledWindow):
force_reload_from_master=(old_len != new_len or force))
stop_loading()
print('update grid')
future = self.get_model_list_future(
before_download=start_loading,
force=force,
@@ -284,7 +283,8 @@ class CoverArtGrid(Gtk.ScrolledWindow):
detail_element = self.create_detail_element_from_model(model)
detail_element.connect(
'song-clicked',
lambda _, song, queue: self.emit('song-clicked', song, queue),
lambda _, song, queue, metadata: self.emit(
'song-clicked', song, queue, metadata),
)
detail_element.connect('song-selected', lambda *a: None)

View File

@@ -0,0 +1,33 @@
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk
class IconButton(Gtk.Button):
def __init__(
self,
icon_name,
relief=False,
icon_size=Gtk.IconSize.BUTTON,
label=None,
**kwargs
):
Gtk.Button.__init__(self, **kwargs)
self.icon_size = icon_size
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
name='icon-button-box')
self.image = Gtk.Image()
self.image.set_from_icon_name(icon_name, self.icon_size)
box.add(self.image)
if label is not None:
box.add(Gtk.Label(label=label))
if not relief:
self.props.relief = Gtk.ReliefStyle.NONE
self.add(box)
def set_icon(self, icon_name):
self.image.set_from_icon_name(icon_name, self.icon_size)

View File

@@ -253,6 +253,8 @@ class ChromecastPlayer(Player):
# Set host_ip
# TODO should have a mechanism to update this. Maybe it should be
# determined every time we try and play a song.
# TODO does not work properyfly when on VPNs when the DNS is piped over
# the VPN tunnel.
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
@@ -337,6 +339,7 @@ class ChromecastPlayer(Player):
cover_art_url = CacheManager.get_cover_art_url(song.id, 1000)
self.chromecast.media_controller.play_media(
file_or_url,
# Just pretend that whatever we send it is mp3, even if it isn't.
'audio/mp3',
current_time=progress,
title=song.title,

View File

@@ -6,7 +6,7 @@ from gi.repository import Gtk, GObject
from libremsonic.server import Server
from libremsonic.config import ServerConfiguration
from libremsonic.ui.common import EditFormDialog
from libremsonic.ui.common import EditFormDialog, IconButton
class EditServerDialog(EditFormDialog):
@@ -66,7 +66,7 @@ class EditServerDialog(EditFormDialog):
)
dialog.format_secondary_markup(
f'Connection to {server_address} resulted in the following '
'error:\n\n{err}')
f'error:\n\n{err}')
dialog.run()
dialog.destroy()
@@ -94,7 +94,7 @@ class ConfigureServersDialog(Gtk.Dialog):
self.server_configs = config.servers
self.selected_server_index = config.current_server
self.set_default_size(450, 300)
self.set_default_size(500, 300)
# Flow box to hold the server list and the buttons.
flowbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
@@ -110,15 +110,18 @@ class ConfigureServersDialog(Gtk.Dialog):
# Add all of the buttons to the button box.
self.buttons = [
# TODO get good icons for these
(Gtk.Button(label='Edit...'),
lambda e: self.on_edit_clicked(e, False), 'start', True),
(Gtk.Button(label='Add...'),
(IconButton('document-edit-symbolic', label='Edit...',
relief=True), lambda e: self.on_edit_clicked(e, False),
'start', True),
(IconButton('list-add', label='Add...', relief=True),
lambda e: self.on_edit_clicked(e, True), 'start', False),
(Gtk.Button(label='Remove'), self.on_remove_clicked, 'start',
True),
(Gtk.Button(label='Close'), lambda _: self.close(), 'end', False),
(Gtk.Button(label='Connect'), self.on_connect_clicked, 'end',
True),
(IconButton('list-remove', label='Remove',
relief=True), self.on_remove_clicked, 'start', True),
(IconButton('window-close', label='Close',
relief=True), lambda _: self.close(), 'end', False),
(IconButton('network-transmit-receive',
label='Connect',
relief=True), self.on_connect_clicked, 'end', True),
]
for button_cfg in self.buttons:
btn, action, pack_end, requires_selection = button_cfg

View File

@@ -12,8 +12,8 @@ from libremsonic.state_manager import ApplicationState
class MainWindow(Gtk.ApplicationWindow):
"""Defines the main window for LibremSonic."""
__gsignals__ = {
'song-clicked':
(GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, object)),
'song-clicked': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
(str, object, object)),
}
def __init__(self, *args, **kwargs):
@@ -50,14 +50,16 @@ class MainWindow(Gtk.ApplicationWindow):
self.connected_to_label.set_markup(
f'<span style="italic">Not Connected to a Server</span>')
self.stack.set_visible_child_name(state.current_tab)
active_panel = self.stack.get_visible_child()
if hasattr(active_panel, 'update'):
active_panel.update(state)
self.player_controls.update(state)
def on_song_clicked(self, panel, song, queue):
self.emit('song-clicked', song, queue)
def on_song_clicked(self, panel, song, queue, metadata):
self.emit('song-clicked', song, queue, metadata)
def create_stack(self, **kwargs):
stack = Gtk.Stack()

View File

@@ -7,7 +7,7 @@ from gi.repository import Gtk, Pango, GObject, Gio, GLib
from libremsonic.cache_manager import CacheManager
from libremsonic.state_manager import ApplicationState, RepeatType
from libremsonic.ui import util
from libremsonic.ui.common import SpinnerImage
from libremsonic.ui.common import IconButton, SpinnerImage
from libremsonic.ui.common.players import ChromecastPlayer
@@ -41,8 +41,7 @@ class PlayerControls(Gtk.ActionBar):
state.current_song.duration)
icon = 'pause' if state.playing else 'start'
self.play_button.get_child().set_from_icon_name(
f"media-playback-{icon}-symbolic", Gtk.IconSize.LARGE_TOOLBAR)
self.play_button.set_icon(f"media-playback-{icon}-symbolic")
has_current_song = (hasattr(state, 'current_song')
and state.current_song is not None)
@@ -58,22 +57,14 @@ class PlayerControls(Gtk.ActionBar):
# TODO: it's not correct to use symboloc vs. not symbolic icons for
# lighter/darker versions of the icon. Fix this by using FG color I
# think? But then we have to deal with styling, which sucks.
icon = Gio.ThemedIcon(name=state.repeat_type.icon)
image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON)
self.repeat_button.remove(self.repeat_button.get_child())
self.repeat_button.add(image)
self.repeat_button.show_all()
self.repeat_button.set_icon(state.repeat_type.icon)
# Shuffle button state
# TODO: it's not correct to use symboloc vs. not symbolic icons for
# lighter/darker versions of the icon. Fix this by using FG color I
# think? But then we have to deal with styling, which sucks.
icon = Gio.ThemedIcon(name='media-playlist-shuffle'
+ ('-symbolic' if state.shuffle_on else ''))
image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON)
self.shuffle_button.remove(self.shuffle_button.get_child())
self.shuffle_button.add(image)
self.shuffle_button.show_all()
self.shuffle_button.set_icon('media-playlist-shuffle' +
('-symbolic' if state.shuffle_on else ''))
self.song_scrubber.set_sensitive(has_current_song)
self.prev_button.set_sensitive(has_current_song)
@@ -118,7 +109,8 @@ class PlayerControls(Gtk.ActionBar):
else:
song_label = str(play_queue_len) + ' ' + util.pluralize(
'song', play_queue_len)
self.popover_label.set_markup(f'<b>Play Queue:</b> {song_label}')
self.popover_label.set_markup(
f'<b>Play Queue:</b> {song_label}')
# Remove everything from the play queue.
for c in self.play_queue_list.get_children():
@@ -159,7 +151,7 @@ class PlayerControls(Gtk.ActionBar):
before_download=lambda self: self.album_art.set_loading(True),
on_failure=lambda self, e: self.album_art.set_loading(False),
)
def update_cover_art(self, cover_art_filename):
def update_cover_art(self, cover_art_filename, state):
self.album_art.set_from_file(cover_art_filename)
self.album_art.set_loading(False)
@@ -285,35 +277,32 @@ class PlayerControls(Gtk.ActionBar):
buttons.pack_start(Gtk.Box(), True, True, 0)
# Repeat button
self.repeat_button = util.button_with_icon('media-playlist-repeat')
self.repeat_button = IconButton('media-playlist-repeat')
self.repeat_button.set_action_name('app.repeat-press')
buttons.pack_start(self.repeat_button, False, False, 5)
# Previous button
self.prev_button = util.button_with_icon(
'media-skip-backward-symbolic',
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
self.prev_button = IconButton('media-skip-backward-symbolic',
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
self.prev_button.set_action_name('app.prev-track')
buttons.pack_start(self.prev_button, False, False, 5)
# Play button
self.play_button = util.button_with_icon(
'media-playback-start-symbolic',
relief=True,
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
self.play_button = IconButton('media-playback-start-symbolic',
relief=True,
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
self.play_button.set_name('play-button')
self.play_button.set_action_name('app.play-pause')
buttons.pack_start(self.play_button, False, False, 0)
# Next button
self.next_button = util.button_with_icon(
'media-skip-forward-symbolic',
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
self.next_button = IconButton('media-skip-forward-symbolic',
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
self.next_button.set_action_name('app.next-track')
buttons.pack_start(self.next_button, False, False, 5)
# Shuffle button
self.shuffle_button = util.button_with_icon('media-playlist-shuffle')
self.shuffle_button = IconButton('media-playlist-shuffle')
self.shuffle_button.set_action_name('app.shuffle-press')
buttons.pack_start(self.shuffle_button, False, False, 5)
@@ -329,8 +318,8 @@ class PlayerControls(Gtk.ActionBar):
# Device button (for chromecast)
# TODO need icon
device_button = util.button_with_icon(
'view-list-symbolic', icon_size=Gtk.IconSize.LARGE_TOOLBAR)
device_button = IconButton('view-list-symbolic',
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
device_button.connect('clicked', self.on_device_click)
box.pack_start(device_button, False, True, 5)
@@ -347,7 +336,7 @@ class PlayerControls(Gtk.ActionBar):
)
device_popover_header.add(self.popover_label)
refresh_devices = util.button_with_icon('view-refresh')
refresh_devices = IconButton('view-refresh')
refresh_devices.connect('clicked', self.on_device_refresh_click)
device_popover_header.pack_end(refresh_devices, False, False, 0)
@@ -376,8 +365,8 @@ class PlayerControls(Gtk.ActionBar):
self.device_popover.add(device_popover_box)
# Play Queue button
play_queue_button = util.button_with_icon(
'view-list-symbolic', icon_size=Gtk.IconSize.LARGE_TOOLBAR)
play_queue_button = IconButton('view-list-symbolic',
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
play_queue_button.connect('clicked', self.on_play_queue_click)
box.pack_start(play_queue_button, False, True, 5)
@@ -412,7 +401,7 @@ class PlayerControls(Gtk.ActionBar):
self.play_queue_popover.add(play_queue_popover_box)
# Volume mute toggle
self.volume_mute_toggle = util.button_with_icon('audio-volume-high')
self.volume_mute_toggle = IconButton('audio-volume-high')
self.volume_mute_toggle.set_action_name('app.mute-toggle')
box.pack_start(self.volume_mute_toggle, False, True, 0)

View File

@@ -1,4 +1,5 @@
from functools import lru_cache
from random import randint
from typing import List, OrderedDict
from fuzzywuzzy import process
@@ -11,7 +12,7 @@ from libremsonic.server.api_objects import PlaylistWithSongs
from libremsonic.state_manager import ApplicationState
from libremsonic.cache_manager import CacheManager
from libremsonic.ui import util
from libremsonic.ui.common import EditFormDialog, SpinnerImage
from libremsonic.ui.common import EditFormDialog, IconButton, SpinnerImage
class EditPlaylistDialog(EditFormDialog):
@@ -38,8 +39,8 @@ class EditPlaylistDialog(EditFormDialog):
class PlaylistsPanel(Gtk.Paned):
"""Defines the playlists panel."""
__gsignals__ = {
'song-clicked':
(GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, object)),
'song-clicked': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
(str, object, object)),
}
playlist_map: OrderedDict[int, PlaylistWithSongs] = {}
@@ -57,17 +58,14 @@ class PlaylistsPanel(Gtk.Paned):
playlist_list_actions = Gtk.ActionBar()
self.new_playlist = Gtk.Button(relief=Gtk.ReliefStyle.NONE)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
add_icon = Gio.ThemedIcon(name='list-add')
image = Gtk.Image.new_from_gicon(add_icon, Gtk.IconSize.LARGE_TOOLBAR)
box.add(image)
box.add(Gtk.Label(label='New Playlist', margin=5))
self.new_playlist.add(box)
self.new_playlist = IconButton(
icon_name='list-add',
label='New Playlist',
)
self.new_playlist.connect('clicked', self.on_new_playlist_clicked)
playlist_list_actions.pack_start(self.new_playlist)
list_refresh_button = util.button_with_icon('view-refresh')
list_refresh_button = IconButton('view-refresh')
list_refresh_button.connect('clicked', self.on_list_refresh_click)
playlist_list_actions.pack_end(list_refresh_button)
@@ -150,18 +148,18 @@ class PlaylistsPanel(Gtk.Paned):
self.playlist_action_buttons = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL)
view_refresh_button = util.button_with_icon('view-refresh-symbolic')
view_refresh_button = IconButton('view-refresh-symbolic')
view_refresh_button.connect('clicked', self.on_view_refresh_click)
self.playlist_action_buttons.pack_end(view_refresh_button, False,
False, 5)
playlist_edit_button = util.button_with_icon('document-edit-symbolic')
playlist_edit_button = IconButton('document-edit-symbolic')
playlist_edit_button.connect('clicked',
self.on_playlist_edit_button_click)
self.playlist_action_buttons.pack_end(playlist_edit_button, False,
False, 5)
download_all_button = util.button_with_icon('folder-download-symbolic')
download_all_button = IconButton('folder-download-symbolic')
download_all_button.connect(
'clicked', self.on_playlist_list_download_all_button_click)
self.playlist_action_buttons.pack_end(download_all_button, False,
@@ -184,6 +182,29 @@ class PlaylistsPanel(Gtk.Paned):
self.playlist_stats = self.make_label(name='playlist-stats')
playlist_details_box.add(self.playlist_stats)
self.play_shuffle_buttons = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
name='playlist-play-shuffle-buttons',
)
play_button = IconButton(
'media-playback-start-symbolic',
label='Play All',
relief=True,
)
play_button.connect('clicked', self.on_play_all_clicked)
self.play_shuffle_buttons.pack_start(play_button, False, False, 0)
shuffle_button = IconButton(
'media-playlist-shuffle-symbolic',
label='Shuffle All',
relief=True,
)
shuffle_button.connect('clicked', self.on_shuffle_all_button)
self.play_shuffle_buttons.pack_start(shuffle_button, False, False, 5)
playlist_details_box.add(self.play_shuffle_buttons)
self.big_info_panel.pack_start(playlist_details_box, True, True, 10)
playlist_box.pack_start(self.big_info_panel, False, True, 0)
@@ -351,11 +372,29 @@ class PlaylistsPanel(Gtk.Paned):
self.playlist_list.get_selected_row().get_index()]
self.update_playlist_view(playlist.id, force=True)
def on_play_all_clicked(self, btn):
song_id = self.playlist_song_store[0][-1]
self.emit('song-clicked', song_id,
[m[-1] for m in self.playlist_song_store],
{'force_shuffle_state': False})
def on_shuffle_all_button(self, btn):
rand_idx = randint(0, len(self.playlist_song_store) - 1)
self.emit(
'song-clicked',
self.playlist_song_store[rand_idx][-1],
[m[-1] for m in self.playlist_song_store],
{'force_shuffle_state': True},
)
def on_song_activated(self, treeview, idx, column):
# The song ID is in the last column of the model.
song_id = self.playlist_song_store[idx][-1]
self.emit('song-clicked', song_id,
[m[-1] for m in self.playlist_song_store])
self.emit(
'song-clicked',
self.playlist_song_store[idx][-1],
[m[-1] for m in self.playlist_song_store],
{},
)
def on_song_button_press(self, tree, event):
if event.button == 3: # Right click
@@ -456,8 +495,10 @@ class PlaylistsPanel(Gtk.Paned):
playlist_id = self.playlist_map[selected.get_index()].id
self.update_playlist_view(playlist_id)
self.playlist_action_buttons.show()
self.play_shuffle_buttons.show()
else:
self.playlist_action_buttons.hide()
self.play_shuffle_buttons.hide()
self.playlist_songs.set_headers_visible(state.config.show_headers)
@@ -489,7 +530,11 @@ class PlaylistsPanel(Gtk.Paned):
before_download=lambda self: self.set_playlist_list_loading(True),
on_failure=lambda self, e: self.set_playlist_list_loading(False),
)
def update_playlist_list(self, playlists: List[PlaylistWithSongs]):
def update_playlist_list(
self,
playlists: List[PlaylistWithSongs],
state: ApplicationState,
):
selected_row = self.playlist_list.get_selected_row()
selected_playlist = None
if selected_row:
@@ -522,7 +567,7 @@ class PlaylistsPanel(Gtk.Paned):
on_failure=lambda self, e: (self.set_playlist_view_loading(False) or
self.playlist_artwork.set_loading(False)),
)
def update_playlist_view(self, playlist):
def update_playlist_view(self, playlist, state: ApplicationState):
# Update the Playlist Info panel
self.update_playlist_artwork(playlist.coverArt)
self.playlist_indicator.set_markup('PLAYLIST')
@@ -536,11 +581,12 @@ class PlaylistsPanel(Gtk.Paned):
self.update_playlist_song_list(playlist.id)
self.playlist_action_buttons.show()
self.play_shuffle_buttons.show()
@util.async_callback(
lambda *a, **k: CacheManager.get_playlist(*a, **k),
)
def update_playlist_song_list(self, playlist):
def update_playlist_song_list(self, playlist, state: ApplicationState):
# Update the song list model. This requires some fancy diffing to
# update the list.
self.editing_playlist_song_list = True
@@ -564,7 +610,11 @@ class PlaylistsPanel(Gtk.Paned):
before_download=lambda self: self.playlist_artwork.set_loading(True),
on_failure=lambda self, e: self.playlist_artwork.set_loading(False),
)
def update_playlist_artwork(self, cover_art_filename):
def update_playlist_artwork(
self,
cover_art_filename,
state: ApplicationState,
):
self.playlist_artwork.set_from_file(cover_art_filename)
self.playlist_artwork.set_loading(False)
@@ -572,7 +622,7 @@ class PlaylistsPanel(Gtk.Paned):
lambda *a, **k: CacheManager.get_playlist(*a, **k),
# TODO make loading here
)
def update_playlist_order(self, playlist):
def update_playlist_order(self, playlist, state: ApplicationState):
CacheManager.update_playlist(
playlist_id=playlist.id,
song_index_to_remove=list(range(playlist.songCount)),

View File

@@ -8,27 +8,11 @@ from deepdiff import DeepDiff
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk, GLib, Gdk
from gi.repository import Gtk, GLib, Gdk
from libremsonic.cache_manager import CacheManager, SongCacheStatus
def button_with_icon(
icon_name,
relief=False,
icon_size=Gtk.IconSize.BUTTON,
) -> Gtk.Button:
button = Gtk.Button()
icon = Gio.ThemedIcon(name=icon_name)
image = Gtk.Image.new_from_gicon(icon, icon_size)
button.add(image)
if not relief:
button.props.relief = Gtk.ReliefStyle.NONE
return button
def format_song_duration(duration_secs) -> str:
return f'{duration_secs // 60}:{duration_secs % 60:02}'
@@ -119,18 +103,6 @@ def show_song_popover(
show_remove_from_playlist_button: bool = False,
extra_menu_items: List[Tuple[Gtk.ModelButton, Any]] = [],
):
def on_play_next_click(button):
print('play next click')
def on_add_to_queue_click(button):
print('add to queue click')
def on_go_to_album_click(button):
print('go to album click')
def on_go_to_artist_click(button):
print('go to artist click')
def on_download_songs_click(button):
CacheManager.batch_download_songs(
song_ids,
@@ -159,9 +131,13 @@ def show_song_popover(
# Determine if we should enable the download button.
download_sensitive, remove_download_sensitive = False, False
albums, artists = set(), set()
for song_id in song_ids:
details = CacheManager.get_song_details(song_id).result()
status = CacheManager.get_cached_status(details)
albums.add(details.albumId)
artists.add(details.artistId)
if download_sensitive or status == SongCacheStatus.NOT_CACHED:
download_sensitive = True
@@ -171,18 +147,32 @@ def show_song_popover(
remove_download_sensitive = True
menu_items = [
(Gtk.ModelButton(text='Play next'), on_play_next_click),
(Gtk.ModelButton(text='Add to queue'), on_add_to_queue_click),
(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), None),
(
Gtk.ModelButton(text='Go to album', sensitive=len(song_ids) == 1),
on_go_to_album_click,
Gtk.ModelButton(
text='Play next',
action_name='app.play-next',
action_target=GLib.Variant('as', song_ids),
),
(
Gtk.ModelButton(text='Go to artist', sensitive=len(song_ids) == 1),
on_go_to_artist_click,
Gtk.ModelButton(
text='Add to queue',
action_name='app.add-to-queue',
action_target=GLib.Variant('as', song_ids),
),
(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), None),
Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
Gtk.ModelButton(
text='Go to album',
action_name='app.go-to-album',
action_target=GLib.Variant('s',
list(albums)[0]),
sensitive=len(albums) == 1,
),
Gtk.ModelButton(
text='Go to artist',
action_name='app.go-to-artist',
action_target=GLib.Variant('s',
list(artists)[0]),
sensitive=len(artists) == 1,
),
Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
(
Gtk.ModelButton(
text=f"Download {pluralize('song', song_count)}",
@@ -197,23 +187,23 @@ def show_song_popover(
),
on_remove_downloads_click,
),
(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), None),
(
Gtk.ModelButton(
text=f"Add {pluralize('song', song_count)} to playlist",
menu_name='add-to-playlist',
),
None,
Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
Gtk.ModelButton(
text=f"Add {pluralize('song', song_count)} to playlist",
menu_name='add-to-playlist',
),
*extra_menu_items,
]
for item, action in menu_items:
if action:
item.connect('clicked', action)
if type(item) == Gtk.ModelButton:
for item in menu_items:
if type(item) == tuple:
el, fn = item
el.connect('clicked', fn)
el.get_style_context().add_class('menu-button')
vbox.pack_start(item[0], False, True, 0)
else:
item.get_style_context().add_class('menu-button')
vbox.pack_start(item, False, True, 0)
vbox.pack_start(item, False, True, 0)
popover.add(vbox)
@@ -249,7 +239,11 @@ def show_song_popover(
popover.show_all()
def async_callback(future_fn, before_download=None, on_failure=None):
def async_callback(
future_fn,
before_download=None,
on_failure=None,
):
"""
Defines the ``async_callback`` decorator.
@@ -263,7 +257,7 @@ def async_callback(future_fn, before_download=None, on_failure=None):
"""
def decorator(callback_fn):
@functools.wraps(callback_fn)
def wrapper(self, *args, **kwargs):
def wrapper(self, *args, state=None, **kwargs):
if before_download:
on_before_download = (
lambda: GLib.idle_add(before_download, self))
@@ -278,7 +272,7 @@ def async_callback(future_fn, before_download=None, on_failure=None):
on_failure(self, e)
return
return GLib.idle_add(callback_fn, self, result)
return GLib.idle_add(callback_fn, self, result, state)
future: Future = future_fn(
*args,