More UI fixes for offline mode
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
v0.9.3
|
||||
======
|
||||
v0.10.0
|
||||
=======
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -9,40 +9,47 @@ v0.9.3
|
||||
``~/.config/sublime-music``) and re-run Sublime Music to restart the
|
||||
configuration process.
|
||||
|
||||
**Note:** this release does not have Flatpak support due to the fact that
|
||||
Flatpak does not support Python 3.8 yet.
|
||||
Features
|
||||
--------
|
||||
|
||||
* **UI Features**
|
||||
**Albums Tab Improvements**
|
||||
|
||||
* **Albums Tab**
|
||||
* The Albums tab is now paginated with configurable page sizes.
|
||||
* You can sort the Albums tab ascending or descending.
|
||||
* Opening an closing an album on the Albums tab now has a nice animation.
|
||||
|
||||
* The Albums tab is now paginated with configurable page sizes.
|
||||
* You can sort the Albums tab ascending or descending.
|
||||
* Opening an closing an album on the Albums tab now has a nice animation.
|
||||
**Player Controls**
|
||||
|
||||
* **Player Controls**
|
||||
* The amount of the song that is cached is now shown while streaming a song.
|
||||
* The notification for resuming a play queue is now a non-modal notification
|
||||
that pops up right above the player controls.
|
||||
|
||||
* The amount of the song that is cached is now shown while streaming a song.
|
||||
* The notification for resuming a play queue is now a non-modal
|
||||
notification that pops up right above the player controls.
|
||||
**New Icons**
|
||||
|
||||
* **New Icons**
|
||||
* The Devices button now uses the Chromecast logo.
|
||||
* Custom icons for "Add to play queue", and "Play next" buttons. Thanks to
|
||||
@samsartor for contributing the SVGs!
|
||||
* A new icon for indicating the connection state to the Subsonic server.
|
||||
Contributed by @samsartor.
|
||||
|
||||
* The Devices button now uses the Chromecast logo.
|
||||
* Custom icons for "Add to play queue", and "Play next" buttons. Thanks to
|
||||
@samsartor for contributing the SVGs!
|
||||
* A new icon for indicating the connection state to the Subsonic server.
|
||||
Contributed by @samsartor.
|
||||
**Settings**
|
||||
|
||||
* **Settings**
|
||||
* Settings are now in the popup under the gear icon rather than in a separate
|
||||
popup window.
|
||||
* The music provider configuration has gotten a major revamp.
|
||||
* You can now clear the cache via an option in the Downloads popup. There are
|
||||
options for removing the entire cache and removing just the song file cache.
|
||||
|
||||
* Settings are now in the popup under the gear icon rather than in a
|
||||
separate popup window.
|
||||
* You can now clear the cache via an option in the Downloads popup. There
|
||||
are options for removing the entire cache and removing just the song file
|
||||
cache.
|
||||
**Offline Mode**
|
||||
|
||||
* **Backend**
|
||||
* You can enable *Offline Mode* from the server menu.
|
||||
* Features that require network access are disabled in offline mode.
|
||||
* You can still browse anything that is already cached offline.
|
||||
|
||||
.. MENTION man page
|
||||
|
||||
Under The Hood
|
||||
--------------
|
||||
|
||||
This release has a ton of under-the-hood changes to make things more robust
|
||||
and performant.
|
||||
|
@@ -77,7 +77,6 @@
|
||||
<update_contact>me_AT_sumnerevans.com</update_contact>
|
||||
|
||||
<releases>
|
||||
<release version="0.9.2" date="2020-05-07">
|
||||
</release>
|
||||
<release version="0.10.0" date="2020-05-07"></release>
|
||||
</releases>
|
||||
</component>
|
||||
|
@@ -1 +1 @@
|
||||
__version__ = "0.9.2"
|
||||
__version__ = "0.10.0"
|
||||
|
@@ -251,9 +251,11 @@ class FilesystemAdapter(CachingAdapter):
|
||||
)
|
||||
if cover_art:
|
||||
filename = self.cover_art_dir.joinpath(str(cover_art.file_hash))
|
||||
if cover_art.valid and filename.exists():
|
||||
return str(filename)
|
||||
raise CacheMissError(partial_data=str(filename))
|
||||
if filename.exists():
|
||||
if cover_art.valid:
|
||||
return str(filename)
|
||||
else:
|
||||
raise CacheMissError(partial_data=str(filename))
|
||||
|
||||
raise CacheMissError()
|
||||
|
||||
@@ -269,9 +271,11 @@ class FilesystemAdapter(CachingAdapter):
|
||||
if (song_file := song.file) and (
|
||||
filename := self._compute_song_filename(song_file)
|
||||
):
|
||||
if song_file.valid and filename.exists():
|
||||
return str(filename)
|
||||
raise CacheMissError(partial_data=str(filename))
|
||||
if filename.exists():
|
||||
if song_file.valid:
|
||||
return str(filename)
|
||||
else:
|
||||
raise CacheMissError(partial_data=str(filename))
|
||||
except models.CacheInfo.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
@@ -152,6 +152,7 @@ class SubsonicAdapter(Adapter):
|
||||
sleep(15)
|
||||
|
||||
def _set_ping_status(self):
|
||||
# TODO don't ping in offline mode
|
||||
try:
|
||||
# Try to ping the server with a timeout of 2 seconds.
|
||||
self._get_json(self._make_url("ping"), timeout=2)
|
||||
|
@@ -824,9 +824,10 @@ class SublimeMusicApp(Gtk.Application):
|
||||
return False
|
||||
|
||||
# Allow spaces to work in the text entry boxes.
|
||||
if window.search_entry.has_focus():
|
||||
return False
|
||||
if window.playlists_panel.playlist_list.new_playlist_entry.has_focus():
|
||||
if (
|
||||
window.search_entry.has_focus()
|
||||
or window.playlists_panel.playlist_list.new_playlist_entry.has_focus()
|
||||
):
|
||||
return False
|
||||
|
||||
# Spacebar, home/prev
|
||||
|
@@ -555,7 +555,9 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
grid_detail_grid_box.add(self.grid_top)
|
||||
|
||||
self.detail_box_revealer = Gtk.Revealer(valign=Gtk.Align.END)
|
||||
self.detail_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.detail_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, name="artist-detail-box"
|
||||
)
|
||||
self.detail_box.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
self.detail_box_inner = Gtk.Box()
|
||||
|
@@ -261,3 +261,12 @@
|
||||
#artist-info-panel {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@define-color detail_color rgba(0, 0, 0, 0.2);
|
||||
#artist-detail-box {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
box-shadow: inset 0 5px 5px @detail_color,
|
||||
inset 0 -5px 5px @detail_color;
|
||||
background-color: @detail_color;
|
||||
}
|
||||
|
@@ -232,6 +232,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
name="playlist-play-shuffle-buttons",
|
||||
)
|
||||
|
||||
# TODO: make these disabled if there are no songs that can be played.
|
||||
play_button = IconButton(
|
||||
"media-playback-start-symbolic", label="Play All", relief=True,
|
||||
)
|
||||
@@ -341,7 +342,11 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self.artist_indicator.set_text("ARTIST")
|
||||
self.artist_stats.set_markup(self.format_stats(artist))
|
||||
|
||||
self.artist_bio.set_markup(util.esc(artist.biography))
|
||||
if artist.biography:
|
||||
self.artist_bio.set_markup(util.esc(artist.biography))
|
||||
self.artist_bio.show()
|
||||
else:
|
||||
self.artist_bio.hide()
|
||||
|
||||
if len(artist.similar_artists or []) > 0:
|
||||
self.similar_artists_label.set_markup("<b>Similar Artists:</b> ")
|
||||
@@ -450,7 +455,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self.albums_list.spinner.hide()
|
||||
self.artist_artwork.set_loading(False)
|
||||
|
||||
def make_label(self, text: str = None, name: str = None, **params,) -> Gtk.Label:
|
||||
def make_label(self, text: str = None, name: str = None, **params) -> Gtk.Label:
|
||||
return Gtk.Label(
|
||||
label=text, name=name, halign=Gtk.Align.START, xalign=0, **params,
|
||||
)
|
||||
@@ -538,7 +543,11 @@ class AlbumsListWithSongs(Gtk.Overlay):
|
||||
album_with_songs.show_all()
|
||||
self.box.add(album_with_songs)
|
||||
|
||||
self.spinner.stop()
|
||||
# Update everything (no force to ensure that if we are online, then everything
|
||||
# is clickable)
|
||||
for c in self.box.get_children():
|
||||
c.update(app_config=app_config)
|
||||
|
||||
self.spinner.hide()
|
||||
|
||||
def on_song_selected(self, album_component: AlbumWithSongs):
|
||||
|
@@ -207,27 +207,28 @@ class MusicDirectoryList(Gtk.Box):
|
||||
self.list.bind_model(self.drilldown_directories_store, self.create_row)
|
||||
scrollbox.add(self.list)
|
||||
|
||||
self.directory_song_store = Gtk.ListStore(
|
||||
str, str, str, str, # cache status, title, duration, song ID
|
||||
)
|
||||
# clickable, cache status, title, duration, song ID
|
||||
self.directory_song_store = Gtk.ListStore(bool, str, str, str, str)
|
||||
|
||||
self.directory_song_list = Gtk.TreeView(
|
||||
model=self.directory_song_store,
|
||||
name="directory-songs-list",
|
||||
headers_visible=False,
|
||||
)
|
||||
self.directory_song_list.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
selection = self.directory_song_list.get_selection()
|
||||
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
selection.set_select_function(lambda _, model, path, current: model[path[0]][0])
|
||||
|
||||
# Song status column.
|
||||
renderer = Gtk.CellRendererPixbuf()
|
||||
renderer.set_fixed_size(30, 35)
|
||||
column = Gtk.TreeViewColumn("", renderer, icon_name=0)
|
||||
column = Gtk.TreeViewColumn("", renderer, icon_name=1)
|
||||
column.set_resizable(True)
|
||||
self.directory_song_list.append_column(column)
|
||||
|
||||
self.directory_song_list.append_column(SongListColumn("TITLE", 1, bold=True))
|
||||
self.directory_song_list.append_column(SongListColumn("TITLE", 2, bold=True))
|
||||
self.directory_song_list.append_column(
|
||||
SongListColumn("DURATION", 2, align=1, width=40)
|
||||
SongListColumn("DURATION", 3, align=1, width=40)
|
||||
)
|
||||
|
||||
self.directory_song_list.connect("row-activated", self.on_song_activated)
|
||||
@@ -253,6 +254,10 @@ class MusicDirectoryList(Gtk.Box):
|
||||
)
|
||||
|
||||
if app_config:
|
||||
# Deselect everything if switching online to offline.
|
||||
if self.offline_mode != app_config.offline_mode:
|
||||
self.directory_song_list.get_selection().unselect_all()
|
||||
|
||||
self.offline_mode = app_config.offline_mode
|
||||
|
||||
self.refresh_button.set_sensitive(not self.offline_mode)
|
||||
@@ -316,6 +321,11 @@ class MusicDirectoryList(Gtk.Box):
|
||||
|
||||
new_songs_store = [
|
||||
[
|
||||
(
|
||||
not self.offline_mode
|
||||
or status_icon
|
||||
in ("folder-download-symbolic", "view-pin-symbolic")
|
||||
),
|
||||
status_icon,
|
||||
util.esc(song.title),
|
||||
util.format_song_duration(song.duration),
|
||||
@@ -327,7 +337,15 @@ class MusicDirectoryList(Gtk.Box):
|
||||
]
|
||||
else:
|
||||
new_songs_store = [
|
||||
[status_icon] + song_model[1:]
|
||||
[
|
||||
(
|
||||
not self.offline_mode
|
||||
or status_icon
|
||||
in ("folder-download-symbolic", "view-pin-symbolic")
|
||||
),
|
||||
status_icon,
|
||||
*song_model[2:],
|
||||
]
|
||||
for status_icon, song_model in zip(
|
||||
util.get_cached_status_icons(song_ids), self.directory_song_store
|
||||
)
|
||||
@@ -384,6 +402,8 @@ class MusicDirectoryList(Gtk.Box):
|
||||
# Event Handlers
|
||||
# ==================================================================================
|
||||
def on_song_activated(self, treeview: Any, idx: Gtk.TreePath, column: Any):
|
||||
if not self.directory_song_store[idx[0]][0]:
|
||||
return
|
||||
# The song ID is in the last column of the model.
|
||||
self.emit(
|
||||
"song-clicked",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from random import randint
|
||||
from typing import Any, List
|
||||
from typing import Any, cast, List
|
||||
|
||||
from gi.repository import Gdk, GLib, GObject, Gtk, Pango
|
||||
|
||||
@@ -125,8 +125,8 @@ class AlbumWithSongs(Gtk.Box):
|
||||
self.loading_indicator_container = Gtk.Box()
|
||||
album_details.add(self.loading_indicator_container)
|
||||
|
||||
# cache status, title, duration, song ID
|
||||
self.album_song_store = Gtk.ListStore(str, str, str, str)
|
||||
# clickable, cache status, title, duration, song ID
|
||||
self.album_song_store = Gtk.ListStore(bool, str, str, str, str)
|
||||
|
||||
self.album_songs = Gtk.TreeView(
|
||||
model=self.album_song_store,
|
||||
@@ -137,17 +137,19 @@ class AlbumWithSongs(Gtk.Box):
|
||||
margin_right=10,
|
||||
margin_bottom=10,
|
||||
)
|
||||
self.album_songs.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
selection = self.album_songs.get_selection()
|
||||
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
selection.set_select_function(lambda _, model, path, current: model[path[0]][0])
|
||||
|
||||
# Song status column.
|
||||
renderer = Gtk.CellRendererPixbuf()
|
||||
renderer.set_fixed_size(30, 35)
|
||||
column = Gtk.TreeViewColumn("", renderer, icon_name=0)
|
||||
column = Gtk.TreeViewColumn("", renderer, icon_name=1)
|
||||
column.set_resizable(True)
|
||||
self.album_songs.append_column(column)
|
||||
|
||||
self.album_songs.append_column(SongListColumn("TITLE", 1, bold=True))
|
||||
self.album_songs.append_column(SongListColumn("DURATION", 2, align=1, width=40))
|
||||
self.album_songs.append_column(SongListColumn("TITLE", 2, bold=True))
|
||||
self.album_songs.append_column(SongListColumn("DURATION", 3, align=1, width=40))
|
||||
|
||||
self.album_songs.connect("row-activated", self.on_song_activated)
|
||||
self.album_songs.connect("button-press-event", self.on_song_button_press)
|
||||
@@ -167,6 +169,8 @@ class AlbumWithSongs(Gtk.Box):
|
||||
self.emit("song-selected")
|
||||
|
||||
def on_song_activated(self, treeview: Any, idx: Gtk.TreePath, column: Any):
|
||||
if not self.album_song_store[idx[0]][0]:
|
||||
return
|
||||
# The song ID is in the last column of the model.
|
||||
self.emit(
|
||||
"song-clicked",
|
||||
@@ -243,6 +247,9 @@ class AlbumWithSongs(Gtk.Box):
|
||||
|
||||
def update(self, app_config: AppConfiguration = None, force: bool = False):
|
||||
if app_config:
|
||||
# Deselect everything if switching online to offline.
|
||||
if self.offline_mode != app_config.offline_mode:
|
||||
self.album_songs.get_selection().unselect_all()
|
||||
self.offline_mode = app_config.offline_mode
|
||||
|
||||
self.update_album_songs(self.album.id, app_config=app_config, force=force)
|
||||
@@ -273,30 +280,42 @@ class AlbumWithSongs(Gtk.Box):
|
||||
order_token: int = None,
|
||||
):
|
||||
song_ids = [s.id for s in album.songs or []]
|
||||
new_store = [
|
||||
[
|
||||
cached_status,
|
||||
util.esc(song.title),
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
]
|
||||
for cached_status, song in zip(
|
||||
util.get_cached_status_icons(song_ids), album.songs or []
|
||||
new_store = []
|
||||
any_song_playable = False
|
||||
for cached_status, song in zip(
|
||||
util.get_cached_status_icons(song_ids), album.songs or []
|
||||
):
|
||||
playable = not self.offline_mode or cached_status in (
|
||||
"folder-download-symbolic",
|
||||
"view-pin-symbolic",
|
||||
)
|
||||
]
|
||||
new_store.append(
|
||||
[
|
||||
playable,
|
||||
cached_status,
|
||||
util.esc(song.title),
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
]
|
||||
)
|
||||
any_song_playable |= playable
|
||||
|
||||
song_ids = [song[-1] for song in new_store]
|
||||
song_ids = [cast(str, song[-1]) for song in new_store]
|
||||
|
||||
self.play_btn.set_sensitive(True)
|
||||
self.shuffle_btn.set_sensitive(True)
|
||||
self.play_btn.set_sensitive(any_song_playable)
|
||||
self.shuffle_btn.set_sensitive(any_song_playable)
|
||||
self.download_all_btn.set_sensitive(
|
||||
not self.offline_mode and AdapterManager.can_batch_download_songs()
|
||||
)
|
||||
|
||||
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.play_next_btn.set_action_name("app.add-to-queue")
|
||||
self.add_to_queue_btn.set_action_name("app.play-next")
|
||||
if any_song_playable:
|
||||
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.play_next_btn.set_action_name("app.add-to-queue")
|
||||
self.add_to_queue_btn.set_action_name("app.play-next")
|
||||
else:
|
||||
self.play_next_btn.set_action_name("")
|
||||
self.add_to_queue_btn.set_action_name("")
|
||||
|
||||
util.diff_song_store(self.album_song_store, new_store)
|
||||
|
||||
|
@@ -17,6 +17,6 @@ class SongListColumn(Gtk.TreeViewColumn):
|
||||
)
|
||||
renderer.set_fixed_size(width or -1, 35)
|
||||
|
||||
super().__init__(header, renderer, text=text_idx)
|
||||
super().__init__(header, renderer, text=text_idx, sensitive=0)
|
||||
self.set_resizable(True)
|
||||
self.set_expand(not width)
|
||||
|
@@ -1,97 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 13.229166 13.229167"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="play-queue-play.svg"
|
||||
inkscape:export-filename="/home/sumner/projects/sublime-music/sublime/ui/images/play-queue-play.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6"
|
||||
inkscape:cx="15.843479"
|
||||
inkscape:cy="55.759456"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1271"
|
||||
inkscape:window-height="1404"
|
||||
inkscape:window-x="1283"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-283.77082)">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.39981332"
|
||||
id="path4520"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="6.6130843"
|
||||
sodipodi:cy="291.79547"
|
||||
sodipodi:r1="5.6454182"
|
||||
sodipodi:r2="1.3420769"
|
||||
sodipodi:arg1="0.52306766"
|
||||
sodipodi:arg2="1.6763933"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 11.503658,294.61558 -9.7781494,0.005 4.8845773,-8.47073 z"
|
||||
inkscape:transform-center-x="-1.6314957"
|
||||
inkscape:transform-center-y="0.0017367273"
|
||||
transform="matrix(0,1.1570367,-1.1570367,0,342.71928,282.73209)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.39981332"
|
||||
id="path4520-9"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="6.6130843"
|
||||
sodipodi:cy="291.79547"
|
||||
sodipodi:r1="5.6454182"
|
||||
sodipodi:r2="1.3420769"
|
||||
sodipodi:arg1="0.52306766"
|
||||
sodipodi:arg2="1.6763933"
|
||||
inkscape:flatsided="true"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 11.503658,294.61558 -9.7781494,0.005 4.8845773,-8.47073 z"
|
||||
inkscape:transform-center-x="-1.1634043"
|
||||
inkscape:transform-center-y="0.0012332389"
|
||||
transform="matrix(0,0.8250756,-0.8250756,0,245.84428,284.92787)" />
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 13.229 13.229">
|
||||
<path d="M1.838 12.271L1.832.958l9.801 5.651z"/>
|
||||
<path d="M2.764 10.648L2.76 2.581l6.989 4.03z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 213 B |
@@ -406,6 +406,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
lambda _: print("switch"),
|
||||
menu_name="switch-provider",
|
||||
)
|
||||
# TODO
|
||||
music_provider_button.set_action_name("app.configure-servers")
|
||||
vbox.add(music_provider_button)
|
||||
|
||||
|
@@ -290,17 +290,17 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
name="playlist-play-shuffle-buttons",
|
||||
)
|
||||
|
||||
play_button = IconButton(
|
||||
self.play_all_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)
|
||||
self.play_all_button.connect("clicked", self.on_play_all_clicked)
|
||||
self.play_shuffle_buttons.pack_start(self.play_all_button, False, False, 0)
|
||||
|
||||
shuffle_button = IconButton(
|
||||
self.shuffle_all_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)
|
||||
self.shuffle_all_button.connect("clicked", self.on_shuffle_all_button)
|
||||
self.play_shuffle_buttons.pack_start(self.shuffle_all_button, False, False, 5)
|
||||
|
||||
playlist_details_box.add(self.play_shuffle_buttons)
|
||||
|
||||
@@ -352,6 +352,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
playlist_view_scroll_window = Gtk.ScrolledWindow()
|
||||
|
||||
self.playlist_song_store = Gtk.ListStore(
|
||||
bool, # clickable
|
||||
str, # cache status
|
||||
str, # title
|
||||
str, # album
|
||||
@@ -391,20 +392,22 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
enable_search=True,
|
||||
)
|
||||
self.playlist_songs.set_search_equal_func(playlist_song_list_search_fn)
|
||||
self.playlist_songs.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
selection = self.playlist_songs.get_selection()
|
||||
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
selection.set_select_function(lambda _, model, path, current: model[path[0]][0])
|
||||
|
||||
# Song status column.
|
||||
renderer = Gtk.CellRendererPixbuf()
|
||||
renderer.set_fixed_size(30, 35)
|
||||
column = Gtk.TreeViewColumn("", renderer, icon_name=0)
|
||||
column = Gtk.TreeViewColumn("", renderer, icon_name=1)
|
||||
column.set_resizable(True)
|
||||
self.playlist_songs.append_column(column)
|
||||
|
||||
self.playlist_songs.append_column(SongListColumn("TITLE", 1, bold=True))
|
||||
self.playlist_songs.append_column(SongListColumn("ALBUM", 2))
|
||||
self.playlist_songs.append_column(SongListColumn("ARTIST", 3))
|
||||
self.playlist_songs.append_column(SongListColumn("TITLE", 2, bold=True))
|
||||
self.playlist_songs.append_column(SongListColumn("ALBUM", 3))
|
||||
self.playlist_songs.append_column(SongListColumn("ARTIST", 4))
|
||||
self.playlist_songs.append_column(
|
||||
SongListColumn("DURATION", 4, align=1, width=40)
|
||||
SongListColumn("DURATION", 5, align=1, width=40)
|
||||
)
|
||||
|
||||
self.playlist_songs.connect("row-activated", self.on_song_activated)
|
||||
@@ -434,6 +437,10 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
update_playlist_view_order_token = 0
|
||||
|
||||
def update(self, app_config: AppConfiguration, force: bool = False):
|
||||
# Deselect everything if switching online to offline.
|
||||
if self.offline_mode != app_config.offline_mode:
|
||||
self.playlist_songs.get_selection().unselect_all()
|
||||
|
||||
self.offline_mode = app_config.offline_mode
|
||||
if app_config.state.selected_playlist_id is None:
|
||||
self.playlist_box.hide()
|
||||
@@ -532,33 +539,47 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
song_ids.append(c.id)
|
||||
songs.append(c)
|
||||
|
||||
new_songs_store = []
|
||||
can_play_any_song = False
|
||||
cached_status_icons = ("folder-download-symbolic", "view-pin-symbolic")
|
||||
|
||||
if force:
|
||||
self._current_song_ids = song_ids
|
||||
|
||||
new_songs_store = [
|
||||
[
|
||||
status_icon,
|
||||
song.title,
|
||||
album.name if (album := song.album) else None,
|
||||
artist.name if (artist := song.artist) else None,
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
]
|
||||
for status_icon, song in zip(
|
||||
util.get_cached_status_icons(song_ids),
|
||||
[cast(API.Song, s) for s in songs],
|
||||
# Regenerate the store from the actual song data (this is more expensive
|
||||
# because when coming from the cache, we are doing 2N fk requests to
|
||||
# albums).
|
||||
for status_icon, song in zip(
|
||||
util.get_cached_status_icons(song_ids),
|
||||
[cast(API.Song, s) for s in songs],
|
||||
):
|
||||
playable = not self.offline_mode or status_icon in cached_status_icons
|
||||
can_play_any_song |= playable
|
||||
new_songs_store.append(
|
||||
[
|
||||
playable,
|
||||
status_icon,
|
||||
song.title,
|
||||
album.name if (album := song.album) else None,
|
||||
artist.name if (artist := song.artist) else None,
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
]
|
||||
)
|
||||
]
|
||||
else:
|
||||
new_songs_store = [
|
||||
[status_icon] + song_model[1:]
|
||||
for status_icon, song_model in zip(
|
||||
util.get_cached_status_icons(song_ids), self.playlist_song_store
|
||||
)
|
||||
]
|
||||
# Just update the clickable state and download state.
|
||||
for status_icon, song_model in zip(
|
||||
util.get_cached_status_icons(song_ids), self.playlist_song_store
|
||||
):
|
||||
playable = not self.offline_mode or status_icon in cached_status_icons
|
||||
can_play_any_song |= playable
|
||||
new_songs_store.append([playable, status_icon, *song_model[2:]])
|
||||
|
||||
util.diff_song_store(self.playlist_song_store, new_songs_store)
|
||||
|
||||
self.play_all_button.set_sensitive(can_play_any_song)
|
||||
self.shuffle_all_button.set_sensitive(can_play_any_song)
|
||||
|
||||
self.editing_playlist_song_list = False
|
||||
|
||||
self.playlist_view_loading_box.hide()
|
||||
@@ -657,7 +678,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
def download_state_change(song_id: str):
|
||||
GLib.idle_add(
|
||||
lambda: self.update_playlist_view(
|
||||
self.playlist_id, order_token=self.update_playlist_view_order_token,
|
||||
self.playlist_id, order_token=self.update_playlist_view_order_token
|
||||
)
|
||||
)
|
||||
|
||||
@@ -757,7 +778,12 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
self.offline_mode,
|
||||
on_download_state_change=on_download_state_change,
|
||||
extra_menu_items=[
|
||||
(Gtk.ModelButton(text=remove_text), on_remove_songs_click),
|
||||
(
|
||||
Gtk.ModelButton(
|
||||
text=remove_text, sensitive=not self.offline_mode
|
||||
),
|
||||
on_remove_songs_click,
|
||||
)
|
||||
],
|
||||
on_playlist_state_change=lambda: self.emit("refresh-window", {}, True),
|
||||
)
|
||||
|
@@ -218,14 +218,18 @@ def show_song_popover(
|
||||
# Add all of the menu items to the popover.
|
||||
song_count = len(song_ids)
|
||||
|
||||
go_to_album_button = Gtk.ModelButton(
|
||||
text="Go to album", action_name="app.go-to-album"
|
||||
)
|
||||
go_to_artist_button = Gtk.ModelButton(
|
||||
text="Go to artist", action_name="app.go-to-artist"
|
||||
)
|
||||
play_next_button = Gtk.ModelButton(text="Play next", sensitive=False)
|
||||
add_to_queue_button = Gtk.ModelButton(text="Add to queue", sensitive=False)
|
||||
if not offline_mode:
|
||||
play_next_button.set_action_name("app.play-next")
|
||||
play_next_button.set_action_target_value(GLib.Variant("as", song_ids))
|
||||
add_to_queue_button.set_action_name("app.add-to-queue")
|
||||
add_to_queue_button.set_action_target_value(GLib.Variant("as", song_ids))
|
||||
|
||||
go_to_album_button = Gtk.ModelButton(text="Go to album", sensitive=False)
|
||||
go_to_artist_button = Gtk.ModelButton(text="Go to artist", sensitive=False)
|
||||
browse_to_song = Gtk.ModelButton(
|
||||
text=f"Browse to {pluralize('song', song_count)}", action_name="app.browse-to",
|
||||
text=f"Browse to {pluralize('song', song_count)}", sensitive=False
|
||||
)
|
||||
download_song_button = Gtk.ModelButton(
|
||||
text=f"Download {pluralize('song', song_count)}", sensitive=False
|
||||
@@ -246,6 +250,10 @@ def show_song_popover(
|
||||
for status in song_cache_statuses
|
||||
):
|
||||
remove_download_button.set_sensitive(True)
|
||||
play_next_button.set_action_target_value(GLib.Variant("as", song_ids))
|
||||
play_next_button.set_action_name("app.play-next")
|
||||
add_to_queue_button.set_action_target_value(GLib.Variant("as", song_ids))
|
||||
add_to_queue_button.set_action_name("app.add-to-queue")
|
||||
|
||||
albums, artists, parents = set(), set(), set()
|
||||
for song in songs:
|
||||
@@ -258,14 +266,18 @@ def show_song_popover(
|
||||
artists.add(id_)
|
||||
|
||||
if len(albums) == 1 and list(albums)[0] is not None:
|
||||
album_value = GLib.Variant("s", list(albums)[0])
|
||||
go_to_album_button.set_action_target_value(album_value)
|
||||
go_to_album_button.set_action_target_value(
|
||||
GLib.Variant("s", list(albums)[0])
|
||||
)
|
||||
go_to_album_button.set_action_name("app.go-to-album")
|
||||
if len(artists) == 1 and list(artists)[0] is not None:
|
||||
artist_value = GLib.Variant("s", list(artists)[0])
|
||||
go_to_artist_button.set_action_target_value(artist_value)
|
||||
go_to_artist_button.set_action_target_value(
|
||||
GLib.Variant("s", list(artists)[0])
|
||||
)
|
||||
go_to_artist_button.set_action_name("app.go-to-artist")
|
||||
if len(parents) == 1 and list(parents)[0] is not None:
|
||||
parent_value = GLib.Variant("s", list(parents)[0])
|
||||
browse_to_song.set_action_target_value(parent_value)
|
||||
browse_to_song.set_action_target_value(GLib.Variant("s", list(parents)[0]))
|
||||
browse_to_song.set_action_name("app.browse-to")
|
||||
|
||||
def batch_get_song_details() -> List[Song]:
|
||||
return [
|
||||
@@ -278,16 +290,8 @@ def show_song_popover(
|
||||
)
|
||||
|
||||
menu_items = [
|
||||
Gtk.ModelButton(
|
||||
text="Play next",
|
||||
action_name="app.play-next",
|
||||
action_target=GLib.Variant("as", song_ids),
|
||||
),
|
||||
Gtk.ModelButton(
|
||||
text="Add to queue",
|
||||
action_name="app.add-to-queue",
|
||||
action_target=GLib.Variant("as", song_ids),
|
||||
),
|
||||
play_next_button,
|
||||
add_to_queue_button,
|
||||
Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL),
|
||||
go_to_album_button,
|
||||
go_to_artist_button,
|
||||
@@ -300,6 +304,7 @@ def show_song_popover(
|
||||
text=f"Add {pluralize('song', song_count)} to playlist",
|
||||
menu_name="add-to-playlist",
|
||||
name="menu-item-add-to-playlist",
|
||||
sensitive=not offline_mode,
|
||||
),
|
||||
*(extra_menu_items or []),
|
||||
]
|
||||
@@ -319,27 +324,30 @@ def show_song_popover(
|
||||
# Create the "Add song(s) to playlist" sub-menu.
|
||||
playlists_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Back button
|
||||
playlists_vbox.add(Gtk.ModelButton(inverted=True, centered=True, menu_name="main"))
|
||||
if not offline_mode:
|
||||
# Back button
|
||||
playlists_vbox.add(
|
||||
Gtk.ModelButton(inverted=True, centered=True, menu_name="main")
|
||||
)
|
||||
|
||||
# Loading indicator
|
||||
loading_indicator = Gtk.Spinner(name="menu-item-spinner")
|
||||
loading_indicator.start()
|
||||
playlists_vbox.add(loading_indicator)
|
||||
# Loading indicator
|
||||
loading_indicator = Gtk.Spinner(name="menu-item-spinner")
|
||||
loading_indicator.start()
|
||||
playlists_vbox.add(loading_indicator)
|
||||
|
||||
# Create a future to make the actual playlist buttons
|
||||
def on_get_playlists_done(f: Result[List[Playlist]]):
|
||||
playlists_vbox.remove(loading_indicator)
|
||||
# Create a future to make the actual playlist buttons
|
||||
def on_get_playlists_done(f: Result[List[Playlist]]):
|
||||
playlists_vbox.remove(loading_indicator)
|
||||
|
||||
for playlist in f.result():
|
||||
button = Gtk.ModelButton(text=playlist.name)
|
||||
button.get_style_context().add_class("menu-button")
|
||||
button.connect("clicked", on_add_to_playlist_click, playlist)
|
||||
button.show()
|
||||
playlists_vbox.pack_start(button, False, True, 0)
|
||||
for playlist in f.result():
|
||||
button = Gtk.ModelButton(text=playlist.name)
|
||||
button.get_style_context().add_class("menu-button")
|
||||
button.connect("clicked", on_add_to_playlist_click, playlist)
|
||||
button.show()
|
||||
playlists_vbox.pack_start(button, False, True, 0)
|
||||
|
||||
playlists_result = AdapterManager.get_playlists()
|
||||
playlists_result.add_done_callback(on_get_playlists_done)
|
||||
playlists_result = AdapterManager.get_playlists()
|
||||
playlists_result.add_done_callback(on_get_playlists_done)
|
||||
|
||||
popover.add(playlists_vbox)
|
||||
popover.child_set_property(playlists_vbox, "submenu", "add-to-playlist")
|
||||
|
Reference in New Issue
Block a user