Refactored to use a TreeView and allowed for reordering of the queue
This commit is contained in:
1
setup.py
1
setup.py
@@ -46,6 +46,7 @@ setup(
|
|||||||
package_data={
|
package_data={
|
||||||
'sublime': [
|
'sublime': [
|
||||||
'ui/app_styles.css',
|
'ui/app_styles.css',
|
||||||
|
'ui/images/play-queue-play.png',
|
||||||
'ui/mpris_specs/org.mpris.MediaPlayer2.xml',
|
'ui/mpris_specs/org.mpris.MediaPlayer2.xml',
|
||||||
'ui/mpris_specs/org.mpris.MediaPlayer2.Player.xml',
|
'ui/mpris_specs/org.mpris.MediaPlayer2.Player.xml',
|
||||||
'ui/mpris_specs/org.mpris.MediaPlayer2.Playlists.xml',
|
'ui/mpris_specs/org.mpris.MediaPlayer2.Playlists.xml',
|
||||||
|
@@ -9,6 +9,8 @@ gi.require_version('Gtk', '3.0')
|
|||||||
gi.require_version('Notify', '0.7')
|
gi.require_version('Notify', '0.7')
|
||||||
from gi.repository import Gdk, Gio, GLib, Gtk, Notify, GdkPixbuf
|
from gi.repository import Gdk, Gio, GLib, Gtk, Notify, GdkPixbuf
|
||||||
|
|
||||||
|
import Levenshtein
|
||||||
|
|
||||||
from .ui.main import MainWindow
|
from .ui.main import MainWindow
|
||||||
from .ui.configure_servers import ConfigureServersDialog
|
from .ui.configure_servers import ConfigureServersDialog
|
||||||
from .ui.settings import SettingsDialog
|
from .ui.settings import SettingsDialog
|
||||||
@@ -90,8 +92,6 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
add_action('prev-track', self.on_prev_track)
|
add_action('prev-track', self.on_prev_track)
|
||||||
add_action('repeat-press', self.on_repeat_press)
|
add_action('repeat-press', self.on_repeat_press)
|
||||||
add_action('shuffle-press', self.on_shuffle_press)
|
add_action('shuffle-press', self.on_shuffle_press)
|
||||||
add_action(
|
|
||||||
'play-queue-click', self.on_play_queue_click, parameter_type='i')
|
|
||||||
|
|
||||||
# Navigation actions.
|
# Navigation actions.
|
||||||
add_action('play-next', self.on_play_next, parameter_type='as')
|
add_action('play-next', self.on_play_next, parameter_type='as')
|
||||||
@@ -139,6 +139,8 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
'device-update', self.on_device_update)
|
'device-update', self.on_device_update)
|
||||||
self.window.player_controls.connect(
|
self.window.player_controls.connect(
|
||||||
'volume-change', self.on_volume_change)
|
'volume-change', self.on_volume_change)
|
||||||
|
self.window.player_controls.connect(
|
||||||
|
'play-queue-reorder', self.on_play_queue_reorder)
|
||||||
self.window.connect('key-press-event', self.on_window_key_press)
|
self.window.connect('key-press-event', self.on_window_key_press)
|
||||||
|
|
||||||
self.window.show_all()
|
self.window.show_all()
|
||||||
@@ -236,6 +238,11 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
# Send out to the bus that we exist.
|
# Send out to the bus that we exist.
|
||||||
self.dbus_manager.property_diff()
|
self.dbus_manager.property_diff()
|
||||||
|
|
||||||
|
def on_play_queue_reorder(self, _, new_queue):
|
||||||
|
self.state.play_queue = new_queue
|
||||||
|
self.save_play_queue()
|
||||||
|
self.update_window()
|
||||||
|
|
||||||
# ########## DBUS MANAGMENT ########## #
|
# ########## DBUS MANAGMENT ########## #
|
||||||
def do_dbus_register(self, connection, path):
|
def do_dbus_register(self, connection, path):
|
||||||
def get_state_and_player():
|
def get_state_and_player():
|
||||||
@@ -516,9 +523,6 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
self.state.shuffle_on = not self.state.shuffle_on
|
self.state.shuffle_on = not self.state.shuffle_on
|
||||||
self.update_window()
|
self.update_window()
|
||||||
|
|
||||||
def on_play_queue_click(self, action, song_index):
|
|
||||||
self.play_song(song_index.get_int32(), reset=True)
|
|
||||||
|
|
||||||
@dbus_propagate()
|
@dbus_propagate()
|
||||||
def on_play_next(self, action, song_ids):
|
def on_play_next(self, action, song_ids):
|
||||||
if self.state.current_song is None:
|
if self.state.current_song is None:
|
||||||
@@ -593,7 +597,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
self.state.active_playlist_id = None
|
self.state.active_playlist_id = None
|
||||||
|
|
||||||
# If shuffle is enabled, then shuffle the playlist.
|
# If shuffle is enabled, then shuffle the playlist.
|
||||||
if self.state.shuffle_on:
|
if self.state.shuffle_on and not metadata.get('no_reshuffle'):
|
||||||
song_id = song_queue[song_index]
|
song_id = song_queue[song_index]
|
||||||
|
|
||||||
del song_queue[song_index]
|
del song_queue[song_index]
|
||||||
|
BIN
sublime/ui/images/play-queue-play.png
Normal file
BIN
sublime/ui/images/play-queue-play.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 903 B |
97
sublime/ui/images/play-queue-play.svg
Normal file
97
sublime/ui/images/play-queue-play.svg
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 3.1 KiB |
@@ -1,11 +1,12 @@
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gtk, Gdk, Pango, GObject, Gio, GLib
|
from gi.repository import Gtk, GdkPixbuf, Pango, GObject, Gio, GLib
|
||||||
|
|
||||||
from sublime.cache_manager import CacheManager
|
from sublime.cache_manager import CacheManager
|
||||||
from sublime.state_manager import ApplicationState, RepeatType
|
from sublime.state_manager import ApplicationState, RepeatType
|
||||||
@@ -49,23 +50,19 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
GObject.TYPE_NONE,
|
GObject.TYPE_NONE,
|
||||||
(object, bool),
|
(object, bool),
|
||||||
),
|
),
|
||||||
|
'play-queue-reorder': (
|
||||||
|
GObject.SignalFlags.RUN_FIRST,
|
||||||
|
GObject.TYPE_NONE,
|
||||||
|
(object, ),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
editing: bool = False
|
editing: bool = False
|
||||||
|
editing_play_queue_song_list: bool = False
|
||||||
|
reordering_play_queue_song_list: bool = False
|
||||||
current_song = None
|
current_song = None
|
||||||
current_device = None
|
current_device = None
|
||||||
chromecasts: List[ChromecastPlayer] = []
|
chromecasts: List[ChromecastPlayer] = []
|
||||||
|
|
||||||
class PlayQueueSong(GObject.GObject):
|
|
||||||
song_index = GObject.Property(type=int)
|
|
||||||
song_id = GObject.Property(type=str)
|
|
||||||
playing = GObject.Property(type=str)
|
|
||||||
|
|
||||||
def __init__(self, song_index: int, song_id: str, playing: bool):
|
|
||||||
GObject.GObject.__init__(self)
|
|
||||||
self.song_index = song_index
|
|
||||||
self.song_id = song_id
|
|
||||||
self.playing = str(playing)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Gtk.ActionBar.__init__(self)
|
Gtk.ActionBar.__init__(self)
|
||||||
self.set_name('player-controls-bar')
|
self.set_name('player-controls-bar')
|
||||||
@@ -164,12 +161,52 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
self.popover_label.set_markup(
|
self.popover_label.set_markup(
|
||||||
f'<b>Play Queue:</b> {play_queue_len} {song_label}')
|
f'<b>Play Queue:</b> {play_queue_len} {song_label}')
|
||||||
|
|
||||||
new_model = [
|
self.editing_play_queue_song_list = True
|
||||||
PlayerControls.PlayQueueSong(
|
|
||||||
i, s, has_current_song and i == state.current_song_index)
|
new_store = []
|
||||||
for i, s in enumerate(state.play_queue)
|
|
||||||
]
|
# Use this so that we can have a closure around the index
|
||||||
util.diff_model_store(self.play_queue_store, new_model)
|
# variables.
|
||||||
|
def make_play_queue_updater(idx):
|
||||||
|
def on_cover_art_future_done(cover_art_filename):
|
||||||
|
self.play_queue_store[idx][0] = cover_art_filename
|
||||||
|
|
||||||
|
def on_song_details_future_done(song_details):
|
||||||
|
title = util.esc(song_details.title)
|
||||||
|
album = util.esc(song_details.album)
|
||||||
|
artist = util.esc(song_details.artist)
|
||||||
|
label = f'<b>{title}</b>\n{util.dot_join(album, artist)}'
|
||||||
|
self.play_queue_store[idx][1] = label
|
||||||
|
|
||||||
|
# Cover Art
|
||||||
|
cover_art_future = CacheManager.get_cover_art_filename(
|
||||||
|
song_details.coverArt,
|
||||||
|
size=50,
|
||||||
|
)
|
||||||
|
cover_art_future.add_done_callback(
|
||||||
|
lambda f: GLib.idle_add(
|
||||||
|
on_cover_art_future_done, f.result()))
|
||||||
|
|
||||||
|
return lambda f: GLib.idle_add(
|
||||||
|
on_song_details_future_done, f.result())
|
||||||
|
|
||||||
|
for i, song_id in enumerate(state.play_queue):
|
||||||
|
new_store.append(
|
||||||
|
[
|
||||||
|
'',
|
||||||
|
'\n',
|
||||||
|
i == state.current_song_index,
|
||||||
|
song_id,
|
||||||
|
])
|
||||||
|
|
||||||
|
# Get the song details.
|
||||||
|
song_details_future = CacheManager.get_song_details(song_id)
|
||||||
|
song_details_future.add_done_callback(
|
||||||
|
make_play_queue_updater(i))
|
||||||
|
|
||||||
|
util.diff_song_store(self.play_queue_store, new_store)
|
||||||
|
|
||||||
|
self.editing_play_queue_song_list = False
|
||||||
|
|
||||||
@util.async_callback(
|
@util.async_callback(
|
||||||
lambda *k, **v: CacheManager.get_cover_art_filename(*k, **v),
|
lambda *k, **v: CacheManager.get_cover_art_filename(*k, **v),
|
||||||
@@ -212,6 +249,15 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
self.play_queue_popover.popup()
|
self.play_queue_popover.popup()
|
||||||
self.play_queue_popover.show_all()
|
self.play_queue_popover.show_all()
|
||||||
|
|
||||||
|
def on_song_activated(self, treeview, idx, column):
|
||||||
|
# The song ID is in the last column of the model.
|
||||||
|
self.emit(
|
||||||
|
'song-clicked',
|
||||||
|
idx.get_indices()[0],
|
||||||
|
[m[-1] for m in self.play_queue_store],
|
||||||
|
{'no_reshuffle': True},
|
||||||
|
)
|
||||||
|
|
||||||
def update_device_list(self, force=False):
|
def update_device_list(self, force=False):
|
||||||
self.device_list_loading.show()
|
self.device_list_loading.show()
|
||||||
|
|
||||||
@@ -263,46 +309,65 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
def on_device_refresh_click(self, button):
|
def on_device_refresh_click(self, button):
|
||||||
self.update_device_list(force=True)
|
self.update_device_list(force=True)
|
||||||
|
|
||||||
def on_play_queue_button_press(self, listbox, event):
|
def on_play_queue_button_press(self, tree, event):
|
||||||
if event.button == 3: # Right click
|
if event.button == 3: # Right click
|
||||||
clicked_row = listbox.get_row_at_y(event.y)
|
clicked_path = tree.get_path_at_pos(event.x, event.y)
|
||||||
clicked_row_index = clicked_row.get_index()
|
|
||||||
selected_indexes = [
|
|
||||||
r.get_index() for r in listbox.get_selected_rows()
|
|
||||||
]
|
|
||||||
|
|
||||||
if clicked_row_index not in selected_indexes:
|
store, paths = tree.get_selection().get_selected_rows()
|
||||||
listbox.unselect_all()
|
allow_deselect = False
|
||||||
listbox.select_row(clicked_row)
|
|
||||||
selected_indexes = [clicked_row_index]
|
|
||||||
|
|
||||||
def on_download_state_change(song_id=None):
|
def on_download_state_change(song_id=None):
|
||||||
# Refresh the entire window (no force) because the song could
|
# Refresh the entire window (no force) because the song could
|
||||||
# be in a list anywhere in the window.
|
# be in a list anywhere in the window.
|
||||||
self.emit('refresh-window', {}, False)
|
self.emit('refresh-window', {}, False)
|
||||||
|
|
||||||
song_ids = [
|
# Use the new selection instead of the old one for calculating what
|
||||||
self.play_queue_store[idx].song_id for idx in selected_indexes
|
# to do the right click on.
|
||||||
]
|
if clicked_path[0] not in paths:
|
||||||
|
paths = [clicked_path[0]]
|
||||||
|
allow_deselect = True
|
||||||
|
|
||||||
|
song_ids = [self.play_queue_store[p][-1] for p in paths]
|
||||||
|
|
||||||
remove_text = (
|
remove_text = (
|
||||||
'Remove ' + util.pluralize('song', len(song_ids))
|
'Remove ' + util.pluralize('song', len(song_ids))
|
||||||
+ ' from queue')
|
+ ' from queue')
|
||||||
|
|
||||||
def on_remove_songs_click(_):
|
def on_remove_songs_click(_):
|
||||||
self.emit('songs-removed', selected_indexes)
|
self.emit('songs-removed', [p.get_indices()[0] for p in paths])
|
||||||
|
|
||||||
util.show_song_popover(
|
util.show_song_popover(
|
||||||
song_ids,
|
song_ids,
|
||||||
event.x,
|
event.x,
|
||||||
event.y,
|
event.y,
|
||||||
listbox,
|
tree,
|
||||||
on_download_state_change=on_download_state_change,
|
on_download_state_change=on_download_state_change,
|
||||||
extra_menu_items=[
|
extra_menu_items=[
|
||||||
(Gtk.ModelButton(text=remove_text), on_remove_songs_click),
|
(Gtk.ModelButton(text=remove_text), on_remove_songs_click),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If the click was on a selected row, don't deselect anything.
|
||||||
|
if not allow_deselect:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def on_play_queue_model_row_move(self, *args):
|
||||||
|
# If we are programatically editing the song list, don't do anything.
|
||||||
|
if self.editing_play_queue_song_list:
|
||||||
|
return
|
||||||
|
|
||||||
|
# We get both a delete and insert event, I think it's deterministic
|
||||||
|
# which one comes first, but just in case, we have this
|
||||||
|
# reordering_play_queue_song_list flag.
|
||||||
|
if self.reordering_play_queue_song_list:
|
||||||
|
self.emit(
|
||||||
|
'play-queue-reorder',
|
||||||
|
[s[-1] for s in self.play_queue_store],
|
||||||
|
)
|
||||||
|
self.reordering_play_queue_song_list = False
|
||||||
|
else:
|
||||||
|
self.reordering_play_queue_song_list = True
|
||||||
|
|
||||||
def create_song_display(self):
|
def create_song_display(self):
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
|
||||||
@@ -490,18 +555,65 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
min_content_width=400,
|
min_content_width=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.play_queue_store = Gio.ListStore()
|
self.play_queue_store = Gtk.ListStore(
|
||||||
self.play_queue_list = Gtk.ListBox(activate_on_single_click=False)
|
str, # image filename
|
||||||
|
str, # title, album, artist
|
||||||
|
bool, # playing
|
||||||
|
str, # song ID
|
||||||
|
)
|
||||||
|
self.play_queue_list = Gtk.TreeView(
|
||||||
|
model=self.play_queue_store,
|
||||||
|
reorderable=True,
|
||||||
|
headers_visible=False,
|
||||||
|
)
|
||||||
|
self.play_queue_list.get_selection().set_mode(
|
||||||
|
Gtk.SelectionMode.MULTIPLE)
|
||||||
|
|
||||||
|
# Album Art column.
|
||||||
|
def filename_to_pixbuf(column, cell, model, iter, flags):
|
||||||
|
filename = model.get_value(iter, 0)
|
||||||
|
if not filename:
|
||||||
|
cell.set_property('icon_name', '')
|
||||||
|
return
|
||||||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
|
||||||
|
|
||||||
|
# If this is the playing song, then overlay the play icon.
|
||||||
|
if model.get_value(iter, 2):
|
||||||
|
play_overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
|
||||||
|
str(
|
||||||
|
Path(__file__).parent.joinpath(
|
||||||
|
'images/play-queue-play.png')))
|
||||||
|
|
||||||
|
play_overlay_pixbuf.composite(
|
||||||
|
pixbuf, 0, 0, 50, 50, 0, 0, 1, 1,
|
||||||
|
GdkPixbuf.InterpType.NEAREST, 255)
|
||||||
|
|
||||||
|
cell.set_property('pixbuf', pixbuf)
|
||||||
|
|
||||||
|
renderer = Gtk.CellRendererPixbuf()
|
||||||
|
renderer.set_fixed_size(55, 60)
|
||||||
|
column = Gtk.TreeViewColumn('', renderer)
|
||||||
|
column.set_cell_data_func(renderer, filename_to_pixbuf)
|
||||||
|
column.set_resizable(True)
|
||||||
|
self.play_queue_list.append_column(column)
|
||||||
|
|
||||||
|
renderer = Gtk.CellRendererText(
|
||||||
|
markup=True,
|
||||||
|
ellipsize=Pango.EllipsizeMode.END,
|
||||||
|
)
|
||||||
|
column = Gtk.TreeViewColumn('', renderer, markup=1)
|
||||||
|
self.play_queue_list.append_column(column)
|
||||||
|
|
||||||
|
self.play_queue_list.connect('row-activated', self.on_song_activated)
|
||||||
self.play_queue_list.connect(
|
self.play_queue_list.connect(
|
||||||
'button-press-event', self.on_play_queue_button_press)
|
'button-press-event', self.on_play_queue_button_press)
|
||||||
self.play_queue_list.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
|
||||||
self.play_queue_list.bind_model(
|
# Set up drag-and-drop on the song list for editing the order of the
|
||||||
self.play_queue_store,
|
# playlist.
|
||||||
self.create_play_queue_row,
|
self.play_queue_store.connect(
|
||||||
)
|
'row-inserted', self.on_play_queue_model_row_move)
|
||||||
self.play_queue_list.drag_dest_set(
|
self.play_queue_store.connect(
|
||||||
Gtk.DestDefaults.ALL, None, Gdk.DragAction.MOVE)
|
'row-deleted', self.on_play_queue_model_row_move)
|
||||||
self.play_queue_list.connect('drag-data-received', lambda *a: print(a))
|
|
||||||
|
|
||||||
play_queue_scrollbox.add(self.play_queue_list)
|
play_queue_scrollbox.add(self.play_queue_list)
|
||||||
play_queue_popover_box.pack_end(play_queue_scrollbox, True, True, 0)
|
play_queue_popover_box.pack_end(play_queue_scrollbox, True, True, 0)
|
||||||
@@ -524,74 +636,3 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
vbox.pack_start(box, False, True, 0)
|
vbox.pack_start(box, False, True, 0)
|
||||||
vbox.pack_start(Gtk.Box(), True, True, 0)
|
vbox.pack_start(Gtk.Box(), True, True, 0)
|
||||||
return vbox
|
return vbox
|
||||||
|
|
||||||
def create_play_queue_row(self, model: PlayQueueSong):
|
|
||||||
draggable_container = Gtk.EventBox()
|
|
||||||
row = Gtk.ListBoxRow(
|
|
||||||
action_name='app.play-queue-click',
|
|
||||||
action_target=GLib.Variant('i', model.song_index),
|
|
||||||
)
|
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, margin=3)
|
|
||||||
|
|
||||||
overlay = Gtk.Overlay()
|
|
||||||
image = SpinnerImage(image_name='play-queue-row-image')
|
|
||||||
overlay.add(image)
|
|
||||||
box.add(overlay)
|
|
||||||
|
|
||||||
# Add a play icon overlay if this is the currently playing song.
|
|
||||||
if model.playing == 'True':
|
|
||||||
# TODO style this with a a border so that it is visible when the
|
|
||||||
# cover art looks similar.
|
|
||||||
play_icon = Gtk.Image(
|
|
||||||
name='play-queue-playing-icon',
|
|
||||||
halign=Gtk.Align.CENTER,
|
|
||||||
valign=Gtk.Align.CENTER,
|
|
||||||
)
|
|
||||||
play_icon.set_from_icon_name(
|
|
||||||
'media-playback-start-symbolic',
|
|
||||||
Gtk.IconSize.DIALOG,
|
|
||||||
)
|
|
||||||
overlay.add_overlay(play_icon)
|
|
||||||
|
|
||||||
label = Gtk.Label(
|
|
||||||
label='\n',
|
|
||||||
use_markup=True,
|
|
||||||
margin=10,
|
|
||||||
halign=Gtk.Align.START,
|
|
||||||
ellipsize=Pango.EllipsizeMode.END,
|
|
||||||
max_width_chars=35,
|
|
||||||
)
|
|
||||||
box.add(label)
|
|
||||||
row.add(box)
|
|
||||||
|
|
||||||
def update_image(image_filename):
|
|
||||||
image.set_from_file(image_filename)
|
|
||||||
image.set_loading(False)
|
|
||||||
row.show_all()
|
|
||||||
|
|
||||||
def update_row(song_details):
|
|
||||||
title = util.esc(song_details.title)
|
|
||||||
album = util.esc(song_details.album)
|
|
||||||
artist = util.esc(song_details.artist)
|
|
||||||
label.set_markup(f'<b>{title}</b>\n{util.dot_join(album, artist)}')
|
|
||||||
row.show_all()
|
|
||||||
|
|
||||||
cover_art_future = CacheManager.get_cover_art_filename(
|
|
||||||
song_details.coverArt, size=50)
|
|
||||||
cover_art_future.add_done_callback(
|
|
||||||
lambda f: GLib.idle_add(update_image, f.result()))
|
|
||||||
|
|
||||||
song_details_future = CacheManager.get_song_details(model.song_id)
|
|
||||||
song_details_future.add_done_callback(
|
|
||||||
lambda f: GLib.idle_add(update_row, f.result()))
|
|
||||||
|
|
||||||
draggable_container.add(row)
|
|
||||||
|
|
||||||
def on_drag_data_get(widget, drag_context, data, info, time):
|
|
||||||
data.set_text(str(model.song_index))
|
|
||||||
|
|
||||||
draggable_container.drag_source_set(
|
|
||||||
Gdk.ModifierType.BUTTON1_MASK, None, Gdk.DragAction.MOVE)
|
|
||||||
draggable_container.connect('drag-data-get', on_drag_data_get)
|
|
||||||
|
|
||||||
return draggable_container
|
|
||||||
|
@@ -409,9 +409,9 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|||||||
# Set up drag-and-drop on the song list for editing the order of the
|
# Set up drag-and-drop on the song list for editing the order of the
|
||||||
# playlist.
|
# playlist.
|
||||||
self.playlist_song_store.connect(
|
self.playlist_song_store.connect(
|
||||||
'row-inserted', self.playlist_model_row_move)
|
'row-inserted', self.on_playlist_model_row_move)
|
||||||
self.playlist_song_store.connect(
|
self.playlist_song_store.connect(
|
||||||
'row-deleted', self.playlist_model_row_move)
|
'row-deleted', self.on_playlist_model_row_move)
|
||||||
|
|
||||||
playlist_view_scroll_window.add(self.playlist_songs)
|
playlist_view_scroll_window.add(self.playlist_songs)
|
||||||
|
|
||||||
@@ -547,8 +547,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|||||||
'refresh-window',
|
'refresh-window',
|
||||||
{
|
{
|
||||||
'selected_playlist_id':
|
'selected_playlist_id':
|
||||||
None if result == Gtk.ResponseType.NO else
|
None if result == Gtk.ResponseType.NO else self.playlist_id
|
||||||
self.playlist_id
|
|
||||||
},
|
},
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
@@ -580,7 +579,8 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|||||||
def on_shuffle_all_button(self, btn):
|
def on_shuffle_all_button(self, btn):
|
||||||
self.emit(
|
self.emit(
|
||||||
'song-clicked',
|
'song-clicked',
|
||||||
randint(0, len(self.playlist_song_store) - 1),
|
randint(0,
|
||||||
|
len(self.playlist_song_store) - 1),
|
||||||
[m[-1] for m in self.playlist_song_store],
|
[m[-1] for m in self.playlist_song_store],
|
||||||
{
|
{
|
||||||
'force_shuffle_state': True,
|
'force_shuffle_state': True,
|
||||||
@@ -650,15 +650,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|||||||
if not allow_deselect:
|
if not allow_deselect:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def make_label(self, text=None, name=None, **params):
|
def on_playlist_model_row_move(self, *args):
|
||||||
return Gtk.Label(
|
|
||||||
label=text,
|
|
||||||
name=name,
|
|
||||||
halign=Gtk.Align.START,
|
|
||||||
**params,
|
|
||||||
)
|
|
||||||
|
|
||||||
def playlist_model_row_move(self, *args):
|
|
||||||
# If we are programatically editing the song list, don't do anything.
|
# If we are programatically editing the song list, don't do anything.
|
||||||
if self.editing_playlist_song_list:
|
if self.editing_playlist_song_list:
|
||||||
return
|
return
|
||||||
@@ -672,6 +664,14 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|||||||
else:
|
else:
|
||||||
self.reordering_playlist_song_list = True
|
self.reordering_playlist_song_list = True
|
||||||
|
|
||||||
|
def make_label(self, text=None, name=None, **params):
|
||||||
|
return Gtk.Label(
|
||||||
|
label=text,
|
||||||
|
name=name,
|
||||||
|
halign=Gtk.Align.START,
|
||||||
|
**params,
|
||||||
|
)
|
||||||
|
|
||||||
@util.async_callback(lambda *a, **k: CacheManager.get_playlist(*a, **k))
|
@util.async_callback(lambda *a, **k: CacheManager.get_playlist(*a, **k))
|
||||||
def update_playlist_order(self, playlist, state: ApplicationState):
|
def update_playlist_order(self, playlist, state: ApplicationState):
|
||||||
self.playlist_view_loading_box.show_all()
|
self.playlist_view_loading_box.show_all()
|
||||||
|
Reference in New Issue
Block a user