|
|
|
@@ -4,7 +4,7 @@ from random import randint
|
|
|
|
|
from typing import Any, cast, Dict, List, Tuple
|
|
|
|
|
|
|
|
|
|
from fuzzywuzzy import fuzz
|
|
|
|
|
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
|
|
|
|
|
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango, Handy
|
|
|
|
|
|
|
|
|
|
from ..adapters import AdapterManager, api_objects as API
|
|
|
|
|
from ..config import AppConfiguration
|
|
|
|
@@ -14,6 +14,7 @@ from ..ui.common import (
|
|
|
|
|
LoadError,
|
|
|
|
|
SongListColumn,
|
|
|
|
|
SpinnerImage,
|
|
|
|
|
Sizer,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -80,7 +81,7 @@ class EditPlaylistDialog(Gtk.Dialog):
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PlaylistsPanel(Gtk.Paned):
|
|
|
|
|
class PlaylistsPanel(Handy.Leaflet):
|
|
|
|
|
"""Defines the playlists panel."""
|
|
|
|
|
|
|
|
|
|
__gsignals__ = {
|
|
|
|
@@ -99,9 +100,12 @@ class PlaylistsPanel(Gtk.Paned):
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
|
|
|
|
|
|
|
|
|
list_sizer = Sizer(natural_width=400)
|
|
|
|
|
self.playlist_list = PlaylistList()
|
|
|
|
|
self.pack1(self.playlist_list, False, False)
|
|
|
|
|
list_sizer.add(self.playlist_list)
|
|
|
|
|
self.add(list_sizer)
|
|
|
|
|
|
|
|
|
|
details_sizer = Sizer(hexpand=True, natural_width=800)
|
|
|
|
|
self.playlist_detail_panel = PlaylistDetailPanel()
|
|
|
|
|
self.playlist_detail_panel.connect(
|
|
|
|
|
"song-clicked",
|
|
|
|
@@ -111,7 +115,24 @@ class PlaylistsPanel(Gtk.Paned):
|
|
|
|
|
"refresh-window",
|
|
|
|
|
lambda _, *args: self.emit("refresh-window", *args),
|
|
|
|
|
)
|
|
|
|
|
self.pack2(self.playlist_detail_panel, True, False)
|
|
|
|
|
details_sizer.add(self.playlist_detail_panel)
|
|
|
|
|
self.add(details_sizer)
|
|
|
|
|
|
|
|
|
|
def playlist_clicked(_):
|
|
|
|
|
if self.get_folded():
|
|
|
|
|
self.set_visible_child(details_sizer)
|
|
|
|
|
self.playlist_list.connect("playlist-clicked", playlist_clicked)
|
|
|
|
|
|
|
|
|
|
def back_clicked(_):
|
|
|
|
|
self.set_visible_child(list_sizer)
|
|
|
|
|
self.playlist_detail_panel.connect("back-clicked", back_clicked)
|
|
|
|
|
|
|
|
|
|
def folded_changed(*_):
|
|
|
|
|
if not self.get_folded():
|
|
|
|
|
self.set_visible_child(list_sizer)
|
|
|
|
|
|
|
|
|
|
self.playlist_detail_panel.show_mobile = self.get_folded()
|
|
|
|
|
self.connect("notify::folded", folded_changed)
|
|
|
|
|
|
|
|
|
|
def update(self, app_config: AppConfiguration = None, force: bool = False):
|
|
|
|
|
self.playlist_list.update(app_config=app_config, force=force)
|
|
|
|
@@ -125,6 +146,7 @@ class PlaylistList(Gtk.Box):
|
|
|
|
|
GObject.TYPE_NONE,
|
|
|
|
|
(object, bool),
|
|
|
|
|
),
|
|
|
|
|
"playlist-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offline_mode = False
|
|
|
|
@@ -220,6 +242,7 @@ class PlaylistList(Gtk.Box):
|
|
|
|
|
self.playlists_store = Gio.ListStore()
|
|
|
|
|
self.list = Gtk.ListBox(name="playlist-list-listbox")
|
|
|
|
|
self.list.bind_model(self.playlists_store, create_playlist_row)
|
|
|
|
|
self.list.connect("row-selected", lambda *_: self.emit("playlist-clicked"))
|
|
|
|
|
list_scroll_window.add(self.list)
|
|
|
|
|
self.pack_start(list_scroll_window, True, True, 0)
|
|
|
|
|
|
|
|
|
@@ -311,6 +334,7 @@ class PlaylistList(Gtk.Box):
|
|
|
|
|
|
|
|
|
|
class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
__gsignals__ = {
|
|
|
|
|
"back-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
|
|
|
|
|
"song-clicked": (
|
|
|
|
|
GObject.SignalFlags.RUN_FIRST,
|
|
|
|
|
GObject.TYPE_NONE,
|
|
|
|
@@ -323,8 +347,9 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
show_mobile = GObject.Property(type=bool, default=False)
|
|
|
|
|
|
|
|
|
|
playlist_id = None
|
|
|
|
|
playlist_details_expanded = False
|
|
|
|
|
offline_mode = False
|
|
|
|
|
|
|
|
|
|
editing_playlist_song_list: bool = False
|
|
|
|
@@ -332,8 +357,54 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
Gtk.Overlay.__init__(self, name="playlist-view-overlay")
|
|
|
|
|
|
|
|
|
|
self.connect("notify::show-mobile", self.on_show_mobile_changed)
|
|
|
|
|
|
|
|
|
|
self.playlist_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
|
|
|
|
|
|
|
|
action_bar = Gtk.ActionBar()
|
|
|
|
|
|
|
|
|
|
back_button_revealer = Gtk.Revealer(transition_type=Gtk.RevealerTransitionType.CROSSFADE)
|
|
|
|
|
self.bind_property("show-mobile", back_button_revealer, "reveal-child", GObject.BindingFlags.SYNC_CREATE)
|
|
|
|
|
|
|
|
|
|
back_button = IconButton("go-previous-symbolic")
|
|
|
|
|
back_button.connect("clicked", lambda *_: self.emit("back-clicked"))
|
|
|
|
|
back_button_revealer.add(back_button)
|
|
|
|
|
|
|
|
|
|
action_bar.pack_start(back_button_revealer)
|
|
|
|
|
|
|
|
|
|
self.view_refresh_button = IconButton(
|
|
|
|
|
"view-refresh-symbolic", "Refresh playlist info"
|
|
|
|
|
)
|
|
|
|
|
self.view_refresh_button.connect("clicked", self.on_view_refresh_click)
|
|
|
|
|
action_bar.pack_end(self.view_refresh_button)
|
|
|
|
|
|
|
|
|
|
self.playlist_edit_button = IconButton("document-edit-symbolic", "Edit paylist")
|
|
|
|
|
self.playlist_edit_button.connect("clicked", self.on_playlist_edit_button_click)
|
|
|
|
|
action_bar.pack_end(self.playlist_edit_button)
|
|
|
|
|
|
|
|
|
|
self.download_all_button = IconButton(
|
|
|
|
|
"folder-download-symbolic", "Download all songs in the playlist"
|
|
|
|
|
)
|
|
|
|
|
self.download_all_button.connect(
|
|
|
|
|
"clicked", self.on_playlist_list_download_all_button_click
|
|
|
|
|
)
|
|
|
|
|
action_bar.pack_end(self.download_all_button)
|
|
|
|
|
|
|
|
|
|
self.shuffle_all_button = IconButton(
|
|
|
|
|
"media-playlist-shuffle-symbolic",
|
|
|
|
|
)
|
|
|
|
|
self.shuffle_all_button.connect("clicked", self.on_shuffle_all_button)
|
|
|
|
|
action_bar.pack_end(self.shuffle_all_button)
|
|
|
|
|
|
|
|
|
|
self.play_all_button = IconButton(
|
|
|
|
|
"media-playback-start-symbolic",
|
|
|
|
|
)
|
|
|
|
|
self.play_all_button.connect("clicked", self.on_play_all_clicked)
|
|
|
|
|
action_bar.pack_end(self.play_all_button)
|
|
|
|
|
|
|
|
|
|
self.playlist_box.add(action_bar)
|
|
|
|
|
|
|
|
|
|
playlist_info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
|
|
|
|
|
|
|
|
self.playlist_artwork = SpinnerImage(
|
|
|
|
@@ -360,71 +431,8 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
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",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.play_all_button = IconButton(
|
|
|
|
|
"media-playback-start-symbolic",
|
|
|
|
|
label="Play All",
|
|
|
|
|
relief=True,
|
|
|
|
|
)
|
|
|
|
|
self.play_all_button.connect("clicked", self.on_play_all_clicked)
|
|
|
|
|
self.play_shuffle_buttons.pack_start(self.play_all_button, False, False, 0)
|
|
|
|
|
|
|
|
|
|
self.shuffle_all_button = IconButton(
|
|
|
|
|
"media-playlist-shuffle-symbolic",
|
|
|
|
|
label="Shuffle All",
|
|
|
|
|
relief=True,
|
|
|
|
|
)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
playlist_info_box.pack_start(playlist_details_box, True, True, 0)
|
|
|
|
|
|
|
|
|
|
# Action buttons & expand/collapse button
|
|
|
|
|
action_buttons_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
|
|
|
self.playlist_action_buttons = Gtk.Box(
|
|
|
|
|
orientation=Gtk.Orientation.HORIZONTAL, spacing=10
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.download_all_button = IconButton(
|
|
|
|
|
"folder-download-symbolic", "Download all songs in the playlist"
|
|
|
|
|
)
|
|
|
|
|
self.download_all_button.connect(
|
|
|
|
|
"clicked", self.on_playlist_list_download_all_button_click
|
|
|
|
|
)
|
|
|
|
|
self.playlist_action_buttons.add(self.download_all_button)
|
|
|
|
|
|
|
|
|
|
self.playlist_edit_button = IconButton("document-edit-symbolic", "Edit paylist")
|
|
|
|
|
self.playlist_edit_button.connect("clicked", self.on_playlist_edit_button_click)
|
|
|
|
|
self.playlist_action_buttons.add(self.playlist_edit_button)
|
|
|
|
|
|
|
|
|
|
self.view_refresh_button = IconButton(
|
|
|
|
|
"view-refresh-symbolic", "Refresh playlist info"
|
|
|
|
|
)
|
|
|
|
|
self.view_refresh_button.connect("clicked", self.on_view_refresh_click)
|
|
|
|
|
self.playlist_action_buttons.add(self.view_refresh_button)
|
|
|
|
|
|
|
|
|
|
action_buttons_container.pack_start(
|
|
|
|
|
self.playlist_action_buttons, False, False, 10
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
action_buttons_container.pack_start(Gtk.Box(), True, True, 0)
|
|
|
|
|
|
|
|
|
|
expand_button_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
|
|
|
self.expand_collapse_button = IconButton(
|
|
|
|
|
"pan-up-symbolic", "Expand playlist details"
|
|
|
|
|
)
|
|
|
|
|
self.expand_collapse_button.connect("clicked", self.on_expand_collapse_click)
|
|
|
|
|
expand_button_container.pack_end(self.expand_collapse_button, False, False, 0)
|
|
|
|
|
action_buttons_container.add(expand_button_container)
|
|
|
|
|
|
|
|
|
|
playlist_info_box.pack_end(action_buttons_container, False, False, 5)
|
|
|
|
|
|
|
|
|
|
self.playlist_box.add(playlist_info_box)
|
|
|
|
|
|
|
|
|
|
self.error_container = Gtk.Box()
|
|
|
|
@@ -506,6 +514,9 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
self.playlist_view_loading_box.add(playlist_view_spinner)
|
|
|
|
|
self.add_overlay(self.playlist_view_loading_box)
|
|
|
|
|
|
|
|
|
|
def on_show_mobile_changed(self, *_):
|
|
|
|
|
self.playlist_artwork.set_image_size( 120 if self.show_mobile else 200)
|
|
|
|
|
|
|
|
|
|
update_playlist_view_order_token = 0
|
|
|
|
|
|
|
|
|
|
def update(self, app_config: AppConfiguration, force: bool = False):
|
|
|
|
@@ -555,40 +566,18 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
|
|
|
|
|
self.playlist_id = playlist.id
|
|
|
|
|
|
|
|
|
|
if app_config:
|
|
|
|
|
self.playlist_details_expanded = app_config.state.playlist_details_expanded
|
|
|
|
|
|
|
|
|
|
up_down = "up" if self.playlist_details_expanded else "down"
|
|
|
|
|
self.expand_collapse_button.set_icon(f"pan-{up_down}-symbolic")
|
|
|
|
|
self.expand_collapse_button.set_tooltip_text(
|
|
|
|
|
"Collapse" if self.playlist_details_expanded else "Expand"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Update the info display.
|
|
|
|
|
self.playlist_name.set_markup(f"<b>{playlist.name}</b>")
|
|
|
|
|
self.playlist_name.set_tooltip_text(playlist.name)
|
|
|
|
|
|
|
|
|
|
if self.playlist_details_expanded:
|
|
|
|
|
self.playlist_artwork.get_style_context().remove_class("collapsed")
|
|
|
|
|
self.playlist_name.get_style_context().remove_class("collapsed")
|
|
|
|
|
self.playlist_box.show_all()
|
|
|
|
|
self.playlist_indicator.set_markup("PLAYLIST")
|
|
|
|
|
|
|
|
|
|
if playlist.comment:
|
|
|
|
|
self.playlist_comment.set_text(playlist.comment)
|
|
|
|
|
self.playlist_comment.set_tooltip_text(playlist.comment)
|
|
|
|
|
self.playlist_comment.show()
|
|
|
|
|
else:
|
|
|
|
|
self.playlist_comment.hide()
|
|
|
|
|
|
|
|
|
|
self.playlist_stats.set_markup(self._format_stats(playlist))
|
|
|
|
|
if playlist.comment:
|
|
|
|
|
self.playlist_comment.set_text(playlist.comment)
|
|
|
|
|
self.playlist_comment.set_tooltip_text(playlist.comment)
|
|
|
|
|
self.playlist_comment.show()
|
|
|
|
|
else:
|
|
|
|
|
self.playlist_artwork.get_style_context().add_class("collapsed")
|
|
|
|
|
self.playlist_name.get_style_context().add_class("collapsed")
|
|
|
|
|
self.playlist_box.show_all()
|
|
|
|
|
self.playlist_indicator.hide()
|
|
|
|
|
self.playlist_comment.hide()
|
|
|
|
|
self.playlist_stats.hide()
|
|
|
|
|
|
|
|
|
|
self.playlist_stats.set_markup(self._format_stats(playlist))
|
|
|
|
|
|
|
|
|
|
# Update the artwork.
|
|
|
|
|
self.update_playlist_artwork(playlist.cover_art, order_token=order_token)
|
|
|
|
@@ -677,7 +666,6 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
self.editing_playlist_song_list = False
|
|
|
|
|
|
|
|
|
|
self.playlist_view_loading_box.hide()
|
|
|
|
|
self.playlist_action_buttons.show_all()
|
|
|
|
|
|
|
|
|
|
@util.async_callback(
|
|
|
|
|
partial(AdapterManager.get_cover_art_uri, scheme="file"),
|
|
|
|
@@ -698,11 +686,6 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|
|
|
|
self.playlist_artwork.set_from_file(cover_art_filename)
|
|
|
|
|
self.playlist_artwork.set_loading(False)
|
|
|
|
|
|
|
|
|
|
if self.playlist_details_expanded:
|
|
|
|
|
self.playlist_artwork.set_image_size(200)
|
|
|
|
|
else:
|
|
|
|
|
self.playlist_artwork.set_image_size(70)
|
|
|
|
|
|
|
|
|
|
# Event Handlers
|
|
|
|
|
# =========================================================================
|
|
|
|
|
def on_view_refresh_click(self, _):
|
|
|
|
|