Play queue enabled/disabled depending on cache state
This commit is contained in:
6
pyproject.toml
Normal file
6
pyproject.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[tool.black]
|
||||||
|
exclude = '''
|
||||||
|
(
|
||||||
|
/flatpak/
|
||||||
|
)
|
||||||
|
'''
|
@@ -181,6 +181,10 @@
|
|||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#play-queue-image-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* ********** General ********** */
|
/* ********** General ********** */
|
||||||
.menu-button {
|
.menu-button {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@@ -258,15 +262,18 @@
|
|||||||
margin: 15px;
|
margin: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@define-color box_shadow_color rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
#artist-info-panel {
|
#artist-info-panel {
|
||||||
|
box-shadow: 0 5px 5px @box_shadow_color;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@define-color detail_color rgba(0, 0, 0, 0.2);
|
|
||||||
#artist-detail-box {
|
#artist-detail-box {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
box-shadow: inset 0 5px 5px @detail_color,
|
box-shadow: inset 0 5px 5px @box_shadow_color,
|
||||||
inset 0 -5px 5px @detail_color;
|
inset 0 -5px 5px @box_shadow_color;
|
||||||
background-color: @detail_color;
|
background-color: @box_shadow_color;
|
||||||
}
|
}
|
||||||
|
@@ -234,13 +234,13 @@ class ArtistDetailPanel(Gtk.Box):
|
|||||||
|
|
||||||
# TODO: make these disabled if there are no songs that can be played.
|
# TODO: make these disabled if there are no songs that can be played.
|
||||||
play_button = IconButton(
|
play_button = IconButton(
|
||||||
"media-playback-start-symbolic", label="Play All", relief=True,
|
"media-playback-start-symbolic", label="Play All", relief=True
|
||||||
)
|
)
|
||||||
play_button.connect("clicked", self.on_play_all_clicked)
|
play_button.connect("clicked", self.on_play_all_clicked)
|
||||||
self.play_shuffle_buttons.pack_start(play_button, False, False, 0)
|
self.play_shuffle_buttons.pack_start(play_button, False, False, 0)
|
||||||
|
|
||||||
shuffle_button = IconButton(
|
shuffle_button = IconButton(
|
||||||
"media-playlist-shuffle-symbolic", label="Shuffle All", relief=True,
|
"media-playlist-shuffle-symbolic", label="Shuffle All", relief=True
|
||||||
)
|
)
|
||||||
shuffle_button.connect("clicked", self.on_shuffle_all_button)
|
shuffle_button.connect("clicked", self.on_shuffle_all_button)
|
||||||
self.play_shuffle_buttons.pack_start(shuffle_button, False, False, 5)
|
self.play_shuffle_buttons.pack_start(shuffle_button, False, False, 5)
|
||||||
|
@@ -7,7 +7,7 @@ from typing import Any, Callable, List, Optional, Tuple
|
|||||||
from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango
|
from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango
|
||||||
from pychromecast import Chromecast
|
from pychromecast import Chromecast
|
||||||
|
|
||||||
from sublime.adapters import AdapterManager, Result
|
from sublime.adapters import AdapterManager, Result, SongCacheStatus
|
||||||
from sublime.adapters.api_objects import Song
|
from sublime.adapters.api_objects import Song
|
||||||
from sublime.config import AppConfiguration
|
from sublime.config import AppConfiguration
|
||||||
from sublime.players import ChromecastPlayer
|
from sublime.players import ChromecastPlayer
|
||||||
@@ -177,6 +177,7 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
self.update_device_list()
|
self.update_device_list()
|
||||||
|
|
||||||
# Short circuit if no changes to the play queue
|
# Short circuit if no changes to the play queue
|
||||||
|
force |= self.offline_mode != app_config.offline_mode
|
||||||
self.offline_mode = app_config.offline_mode
|
self.offline_mode = app_config.offline_mode
|
||||||
self.load_play_queue_button.set_sensitive(not self.offline_mode)
|
self.load_play_queue_button.set_sensitive(not self.offline_mode)
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
if order_token != self.play_queue_update_order_token:
|
if order_token != self.play_queue_update_order_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.play_queue_store[idx][0] = cover_art_filename
|
self.play_queue_store[idx][1] = cover_art_filename
|
||||||
|
|
||||||
def get_cover_art_filename_or_create_future(
|
def get_cover_art_filename_or_create_future(
|
||||||
cover_art_id: Optional[str], idx: int, order_token: int
|
cover_art_id: Optional[str], idx: int, order_token: int
|
||||||
@@ -247,21 +248,26 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
if order_token != self.play_queue_update_order_token:
|
if order_token != self.play_queue_update_order_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.play_queue_store[idx][1] = calculate_label(song_details)
|
self.play_queue_store[idx][2] = calculate_label(song_details)
|
||||||
|
|
||||||
# Cover Art
|
# Cover Art
|
||||||
filename = get_cover_art_filename_or_create_future(
|
filename = get_cover_art_filename_or_create_future(
|
||||||
song_details.cover_art, idx, order_token
|
song_details.cover_art, idx, order_token
|
||||||
)
|
)
|
||||||
if filename:
|
if filename:
|
||||||
self.play_queue_store[idx][0] = filename
|
self.play_queue_store[idx][1] = filename
|
||||||
|
|
||||||
current_play_queue = [x[-1] for x in self.play_queue_store]
|
current_play_queue = [x[-1] for x in self.play_queue_store]
|
||||||
if app_config.state.play_queue != current_play_queue:
|
if app_config.state.play_queue != current_play_queue:
|
||||||
self.play_queue_update_order_token += 1
|
self.play_queue_update_order_token += 1
|
||||||
|
|
||||||
song_details_results = []
|
song_details_results = []
|
||||||
for i, song_id in enumerate(app_config.state.play_queue):
|
for i, (song_id, cached_status) in enumerate(
|
||||||
|
zip(
|
||||||
|
app_config.state.play_queue,
|
||||||
|
AdapterManager.get_cached_statuses(app_config.state.play_queue),
|
||||||
|
)
|
||||||
|
):
|
||||||
song_details_result = AdapterManager.get_song_details(song_id)
|
song_details_result = AdapterManager.get_song_details(song_id)
|
||||||
|
|
||||||
cover_art_filename = ""
|
cover_art_filename = ""
|
||||||
@@ -282,6 +288,11 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
|
|
||||||
new_store.append(
|
new_store.append(
|
||||||
[
|
[
|
||||||
|
(
|
||||||
|
not self.offline_mode
|
||||||
|
or cached_status
|
||||||
|
in (SongCacheStatus.CACHED, SongCacheStatus.PERMANENTLY_CACHED)
|
||||||
|
),
|
||||||
cover_art_filename,
|
cover_art_filename,
|
||||||
label,
|
label,
|
||||||
i == app_config.state.current_song_index,
|
i == app_config.state.current_song_index,
|
||||||
@@ -361,6 +372,8 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
self.play_queue_popover.show_all()
|
self.play_queue_popover.show_all()
|
||||||
|
|
||||||
def on_song_activated(self, t: Any, idx: Gtk.TreePath, c: Any):
|
def on_song_activated(self, t: Any, idx: Gtk.TreePath, c: Any):
|
||||||
|
if not self.play_queue_store[idx[0]][0]:
|
||||||
|
return
|
||||||
# The song ID is in the last column of the model.
|
# The song ID is in the last column of the model.
|
||||||
self.emit(
|
self.emit(
|
||||||
"song-clicked",
|
"song-clicked",
|
||||||
@@ -481,7 +494,7 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
# reordering_play_queue_song_list flag.
|
# reordering_play_queue_song_list flag.
|
||||||
if self.reordering_play_queue_song_list:
|
if self.reordering_play_queue_song_list:
|
||||||
currently_playing_index = [
|
currently_playing_index = [
|
||||||
i for i, s in enumerate(self.play_queue_store) if s[2]
|
i for i, s in enumerate(self.play_queue_store) if s[3] # playing
|
||||||
][0]
|
][0]
|
||||||
self.emit(
|
self.emit(
|
||||||
"refresh-window",
|
"refresh-window",
|
||||||
@@ -706,6 +719,7 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.play_queue_store = Gtk.ListStore(
|
self.play_queue_store = Gtk.ListStore(
|
||||||
|
bool, # playable
|
||||||
str, # image filename
|
str, # image filename
|
||||||
str, # title, album, artist
|
str, # title, album, artist
|
||||||
bool, # playing
|
bool, # playing
|
||||||
@@ -714,30 +728,35 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
self.play_queue_list = Gtk.TreeView(
|
self.play_queue_list = Gtk.TreeView(
|
||||||
model=self.play_queue_store, reorderable=True, headers_visible=False,
|
model=self.play_queue_store, reorderable=True, headers_visible=False,
|
||||||
)
|
)
|
||||||
self.play_queue_list.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
|
selection = self.play_queue_list.get_selection()
|
||||||
|
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||||
|
selection.set_select_function(lambda _, model, path, current: model[path[0]][0])
|
||||||
|
|
||||||
# Album Art column.
|
# Album Art column. This function defines what image to use for the play queue
|
||||||
|
# song icon.
|
||||||
def filename_to_pixbuf(
|
def filename_to_pixbuf(
|
||||||
column: Any,
|
column: Any,
|
||||||
cell: Gtk.CellRendererPixbuf,
|
cell: Gtk.CellRendererPixbuf,
|
||||||
model: Gtk.ListStore,
|
model: Gtk.ListStore,
|
||||||
iter: Gtk.TreeIter,
|
tree_iter: Gtk.TreeIter,
|
||||||
flags: Any,
|
flags: Any,
|
||||||
):
|
):
|
||||||
filename = model.get_value(iter, 0)
|
cell.set_property("sensitive", model.get_value(tree_iter, 0))
|
||||||
|
filename = model.get_value(tree_iter, 1)
|
||||||
if not filename:
|
if not filename:
|
||||||
cell.set_property("icon_name", "")
|
cell.set_property("icon_name", "")
|
||||||
return
|
return
|
||||||
|
|
||||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filename, 50, 50, True)
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filename, 50, 50, True)
|
||||||
|
|
||||||
# If this is the playing song, then overlay the play icon.
|
# If this is the playing song, then overlay the play icon.
|
||||||
if model.get_value(iter, 2):
|
if model.get_value(tree_iter, 3):
|
||||||
play_overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
|
play_overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
|
||||||
str(Path(__file__).parent.joinpath("images/play-queue-play.png"))
|
str(Path(__file__).parent.joinpath("images/play-queue-play.png"))
|
||||||
)
|
)
|
||||||
|
|
||||||
play_overlay_pixbuf.composite(
|
play_overlay_pixbuf.composite(
|
||||||
pixbuf, 0, 0, 50, 50, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 255
|
pixbuf, 0, 0, 50, 50, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 200
|
||||||
)
|
)
|
||||||
|
|
||||||
cell.set_property("pixbuf", pixbuf)
|
cell.set_property("pixbuf", pixbuf)
|
||||||
@@ -749,8 +768,8 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
column.set_resizable(True)
|
column.set_resizable(True)
|
||||||
self.play_queue_list.append_column(column)
|
self.play_queue_list.append_column(column)
|
||||||
|
|
||||||
renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END,)
|
renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END)
|
||||||
column = Gtk.TreeViewColumn("", renderer, markup=1)
|
column = Gtk.TreeViewColumn("", renderer, markup=2, sensitive=0)
|
||||||
self.play_queue_list.append_column(column)
|
self.play_queue_list.append_column(column)
|
||||||
|
|
||||||
self.play_queue_list.connect("row-activated", self.on_song_activated)
|
self.play_queue_list.connect("row-activated", self.on_song_activated)
|
||||||
|
@@ -713,6 +713,8 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_song_activated(self, _, idx: Gtk.TreePath, col: Any):
|
def on_song_activated(self, _, idx: Gtk.TreePath, col: Any):
|
||||||
|
if not self.playlist_song_store[idx[0]][0]:
|
||||||
|
return
|
||||||
# The song ID is in the last column of the model.
|
# The song ID is in the last column of the model.
|
||||||
self.emit(
|
self.emit(
|
||||||
"song-clicked",
|
"song-clicked",
|
||||||
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Generator, List, Tuple
|
from typing import Any, Generator, List, Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from dateutil.tz import tzutc
|
||||||
|
|
||||||
from sublime.adapters.subsonic import (
|
from sublime.adapters.subsonic import (
|
||||||
api_objects as SubsonicAPI,
|
api_objects as SubsonicAPI,
|
||||||
@@ -111,8 +112,8 @@ def test_get_playlists(adapter: SubsonicAdapter):
|
|||||||
name="Test",
|
name="Test",
|
||||||
song_count=132,
|
song_count=132,
|
||||||
duration=timedelta(seconds=33072),
|
duration=timedelta(seconds=33072),
|
||||||
created=datetime(2020, 3, 27, 5, 38, 45, 0, tzinfo=timezone.utc),
|
created=datetime(2020, 3, 27, 5, 38, 45, 0, tzinfo=tzutc()),
|
||||||
changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=timezone.utc),
|
changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=tzutc()),
|
||||||
comment="Foo",
|
comment="Foo",
|
||||||
owner="foo",
|
owner="foo",
|
||||||
public=True,
|
public=True,
|
||||||
@@ -123,8 +124,8 @@ def test_get_playlists(adapter: SubsonicAdapter):
|
|||||||
name="Bar",
|
name="Bar",
|
||||||
song_count=23,
|
song_count=23,
|
||||||
duration=timedelta(seconds=847),
|
duration=timedelta(seconds=847),
|
||||||
created=datetime(2020, 3, 27, 5, 39, 4, 0, tzinfo=timezone.utc),
|
created=datetime(2020, 3, 27, 5, 39, 4, 0, tzinfo=tzutc()),
|
||||||
changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=timezone.utc),
|
changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=tzutc()),
|
||||||
comment="",
|
comment="",
|
||||||
owner="foo",
|
owner="foo",
|
||||||
public=False,
|
public=False,
|
||||||
@@ -136,7 +137,7 @@ def test_get_playlists(adapter: SubsonicAdapter):
|
|||||||
logging.info(filename)
|
logging.info(filename)
|
||||||
logging.debug(data)
|
logging.debug(data)
|
||||||
adapter._set_mock_data(data)
|
adapter._set_mock_data(data)
|
||||||
assert adapter.get_playlists() == expected
|
assert adapter.get_playlists() == sorted(expected, key=lambda e: e.name)
|
||||||
|
|
||||||
# When playlists is null, expect an empty list.
|
# When playlists is null, expect an empty list.
|
||||||
adapter._set_mock_data(mock_json())
|
adapter._set_mock_data(mock_json())
|
||||||
|
@@ -46,14 +46,15 @@ def test_yaml_load_unload():
|
|||||||
|
|
||||||
|
|
||||||
def test_config_migrate():
|
def test_config_migrate():
|
||||||
config = AppConfiguration()
|
config = AppConfiguration(always_stream=True)
|
||||||
server = ServerConfiguration(
|
server = ServerConfiguration(
|
||||||
name="Test", server_address="https://test.host", username="test"
|
name="Test", server_address="https://test.host", username="test"
|
||||||
)
|
)
|
||||||
config.servers.append(server)
|
config.servers.append(server)
|
||||||
config.migrate()
|
config.migrate()
|
||||||
|
|
||||||
assert config.version == 3
|
assert config.version == 4
|
||||||
|
assert config.allow_song_downloads is False
|
||||||
for server in config.servers:
|
for server in config.servers:
|
||||||
server.version == 0
|
server.version == 0
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user