coderkun
704fa3278a
Show the length of an album in the tooltip on the Playlist and the Library panel.
2114 lines
76 KiB
Python
2114 lines
76 KiB
Python
#!/usr/bin/env python3
|
||
|
||
|
||
import gi
|
||
gi.require_version('Gtk', '3.0')
|
||
try:
|
||
import keyring
|
||
use_keyring = True
|
||
except:
|
||
use_keyring = False
|
||
import gettext
|
||
import logging
|
||
import math
|
||
import sys
|
||
import threading
|
||
|
||
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib, Gio
|
||
|
||
from mcg import client
|
||
from mcg.utils import SortOrder
|
||
from mcg.utils import Utils
|
||
from mcg.zeroconf import ZeroconfProvider
|
||
|
||
|
||
|
||
|
||
class ShortcutsDialog():
|
||
|
||
|
||
def __init__(self, builder, window):
|
||
# Widgets
|
||
self._window = builder.get_object('shortcuts-dialog')
|
||
self._window.set_transient_for(window.get())
|
||
|
||
|
||
def get(self):
|
||
return self._window
|
||
|
||
|
||
def present(self):
|
||
self._window.present()
|
||
|
||
|
||
|
||
|
||
class InfoDialog():
|
||
|
||
|
||
def __init__(self, builder):
|
||
self._logger = logging.getLogger(__name__)
|
||
|
||
# Widgets
|
||
self._info_dialog = builder.get_object('info-dialog')
|
||
self._resize_logo()
|
||
|
||
|
||
def get(self):
|
||
return self._info_dialog
|
||
|
||
|
||
def run(self):
|
||
self._info_dialog.run()
|
||
self._info_dialog.hide()
|
||
|
||
|
||
def _resize_logo(self):
|
||
try:
|
||
logo_pixbuf = self._info_dialog.get_logo()
|
||
self._info_dialog.set_logo(
|
||
logo_pixbuf.scale_simple(256, 256, GdkPixbuf.InterpType.HYPER)
|
||
)
|
||
except:
|
||
self._logger.warn("Failed to resize logo")
|
||
|
||
|
||
|
||
|
||
class WindowState(GObject.Object):
|
||
WIDTH = 'width'
|
||
HEIGHT = 'height'
|
||
IS_MAXIMIZED = 'is_maximized'
|
||
IS_FULLSCREENED = 'is_fullscreened'
|
||
width = GObject.Property(type=int, default=800)
|
||
height = GObject.Property(type=int, default=600)
|
||
is_maximized = GObject.Property(type=bool, default=False)
|
||
is_fullscreened = GObject.Property(type=bool, default=False)
|
||
|
||
|
||
def __init__(self):
|
||
GObject.Object.__init__(self)
|
||
|
||
|
||
|
||
|
||
class Window():
|
||
SETTING_HOST = 'host'
|
||
SETTING_PORT = 'port'
|
||
SETTING_CONNECTED = 'connected'
|
||
SETTING_IMAGE_DIR = 'image-dir'
|
||
SETTING_WINDOW_WIDTH = 'width'
|
||
SETTING_WINDOW_HEIGHT = 'height'
|
||
SETTING_WINDOW_MAXIMIZED = 'is-maximized'
|
||
SETTING_PANEL = 'panel'
|
||
SETTING_ITEM_SIZE = 'item-size'
|
||
SETTING_SORT_ORDER = 'sort-order'
|
||
SETTING_SORT_TYPE = 'sort-type'
|
||
STOCK_ICON_DEFAULT = 'image-x-generic-symbolic'
|
||
_PANEL_INDEX_SERVER = 0
|
||
_PANEL_INDEX_COVER = 1
|
||
_PANEL_INDEX_PLAYLIST = 2
|
||
_PANEL_INDEX_LIBRARY = 3
|
||
_CSS_SELECTION = 'selection'
|
||
_CUSTOM_STARTUP_COMPLETE = 'startup-complete'
|
||
|
||
|
||
def __init__(self, app, builder, title, settings):
|
||
self._appwindow = builder.get_object('appwindow')
|
||
self._appwindow.set_application(app)
|
||
self._appwindow.set_title(title)
|
||
self._settings = settings
|
||
self._panels = []
|
||
self._mcg = client.Client()
|
||
self._logger = logging.getLogger(__name__)
|
||
self._state = WindowState()
|
||
|
||
# Login screen
|
||
self._connection_panel = ConnectionPanel(builder)
|
||
# Panels
|
||
self._panels.append(ServerPanel(builder))
|
||
self._panels.append(CoverPanel(builder))
|
||
self._panels.append(PlaylistPanel(builder))
|
||
self._panels.append(LibraryPanel(builder))
|
||
|
||
# Widgets
|
||
# InfoBar
|
||
self._infobar = InfoBar(builder)
|
||
# Stack
|
||
self._content_stack = builder.get_object('contentstack')
|
||
self._stack = builder.get_object('panelstack')
|
||
# Header
|
||
self._header_bar = HeaderBar(builder)
|
||
# Toolbar stack
|
||
self._toolbar_stack = builder.get_object('toolbarstack')
|
||
|
||
# Properties
|
||
self._header_bar.set_sensitive(False, False)
|
||
self._connection_panel.set_host(self._settings.get_string(Window.SETTING_HOST))
|
||
self._connection_panel.set_port(self._settings.get_int(Window.SETTING_PORT))
|
||
if use_keyring:
|
||
self._connection_panel.set_password(keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME))
|
||
self._connection_panel.set_image_dir(self._settings.get_string(Window.SETTING_IMAGE_DIR))
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(self._settings.get_enum(Window.SETTING_SORT_ORDER))
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(self._settings.get_boolean(Window.SETTING_SORT_TYPE))
|
||
|
||
# Signals
|
||
self._header_bar.connect('stack-switched', self.on_header_bar_stack_switched)
|
||
self._header_bar.connect('toolbar-connect', self.on_header_bar_connect)
|
||
self._header_bar.connect('toolbar-playpause', self.on_header_bar_playpause)
|
||
self._header_bar.connect('toolbar-set-volume', self.on_header_bar_set_volume)
|
||
self._connection_panel.connect('connection-changed', self.on_connection_panel_connection_changed)
|
||
self._panels[Window._PANEL_INDEX_SERVER].connect('change-output-device', self.on_server_panel_output_device_changed)
|
||
self._panels[Window._PANEL_INDEX_COVER].connect('toggle-fullscreen', self.on_cover_panel_toggle_fullscreen)
|
||
self._panels[Window._PANEL_INDEX_COVER].connect('set-song', self.on_cover_panel_set_song)
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('clear-playlist', self.on_playlist_panel_clear_playlist)
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove', self.on_playlist_panel_remove)
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove-multiple', self.on_playlist_panel_remove_multiple)
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('play', self.on_playlist_panel_play)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].connect('update', self.on_library_panel_update)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].connect('play', self.on_library_panel_play)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].connect('queue', self.on_library_panel_queue)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].connect('queue-multiple', self.on_library_panel_queue_multiple)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].connect('item-size-changed', self.on_library_panel_item_size_changed)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-order-changed', self.on_library_panel_sort_order_changed)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-type-changed', self.on_library_panel_sort_type_changed)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_CONNECTION, self.on_mcg_connect)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_STATUS, self.on_mcg_status)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_STATS, self.on_mcg_stats)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_OUTPUT_DEVICES, self.on_mcg_load_output_devices)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_PULSE_ALBUMS, self.on_mcg_pulse_albums)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_INIT_ALBUMS, self.on_mcg_init_albums)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_CUSTOM, self.on_mcg_custom)
|
||
self._mcg.connect_signal(client.Client.SIGNAL_ERROR, self.on_mcg_error)
|
||
self._settings.connect('changed::'+Window.SETTING_PANEL, self.on_settings_panel_changed)
|
||
self._settings.connect('changed::'+Window.SETTING_ITEM_SIZE, self.on_settings_item_size_changed)
|
||
self._settings.connect('changed::'+Window.SETTING_SORT_ORDER, self.on_settings_sort_order_changed)
|
||
self._settings.connect('changed::'+Window.SETTING_SORT_TYPE, self.on_settings_sort_type_changed)
|
||
self._settings.bind(Window.SETTING_WINDOW_WIDTH, self._state, WindowState.WIDTH, Gio.SettingsBindFlags.DEFAULT)
|
||
self._settings.bind(Window.SETTING_WINDOW_HEIGHT, self._state, WindowState.HEIGHT, Gio.SettingsBindFlags.DEFAULT)
|
||
self._settings.bind(Window.SETTING_WINDOW_MAXIMIZED, self._state, WindowState.IS_MAXIMIZED, Gio.SettingsBindFlags.DEFAULT)
|
||
|
||
handlers = {
|
||
'on_appwindow_size_allocate': self.on_resize,
|
||
'on_appwindow_window_state_event': self.on_state
|
||
}
|
||
handlers.update(self._header_bar.get_signal_handlers())
|
||
handlers.update(self._infobar.get_signal_handlers())
|
||
handlers.update(self._connection_panel.get_signal_handlers())
|
||
handlers.update(self._panels[Window._PANEL_INDEX_COVER].get_signal_handlers())
|
||
handlers.update(self._panels[Window._PANEL_INDEX_PLAYLIST].get_signal_handlers())
|
||
handlers.update(self._panels[Window._PANEL_INDEX_LIBRARY].get_signal_handlers())
|
||
builder.connect_signals(handlers)
|
||
|
||
# Actions
|
||
self._appwindow.set_default_size(self._state.width, self._state.height)
|
||
if self._state.get_property(WindowState.IS_MAXIMIZED):
|
||
self._appwindow.maximize()
|
||
self._appwindow.show_all()
|
||
self._content_stack.set_visible_child(self._connection_panel.get())
|
||
if self._settings.get_boolean(Window.SETTING_CONNECTED):
|
||
self._connect()
|
||
|
||
# Menu actions
|
||
self._connect_action = Gio.SimpleAction.new_stateful("connect", None, GLib.Variant.new_boolean(False))
|
||
self._connect_action.connect('change-state', self.on_menu_connect)
|
||
self._appwindow.add_action(self._connect_action)
|
||
self._play_action = Gio.SimpleAction.new_stateful("play", None, GLib.Variant.new_boolean(False))
|
||
self._play_action.set_enabled(False)
|
||
self._play_action.connect('change-state', self.on_menu_play)
|
||
self._appwindow.add_action(self._play_action)
|
||
self._clear_playlist_action = Gio.SimpleAction.new("clear-playlist", None)
|
||
self._clear_playlist_action.set_enabled(False)
|
||
self._clear_playlist_action.connect('activate', self.on_menu_clear_playlist)
|
||
self._appwindow.add_action(self._clear_playlist_action)
|
||
panel_variant = GLib.Variant.new_string("0")
|
||
self._panel_action = Gio.SimpleAction.new_stateful("panel", panel_variant.get_type(), panel_variant)
|
||
self._panel_action.set_enabled(False)
|
||
self._panel_action.connect('change-state', self.on_menu_panel)
|
||
self._appwindow.add_action(self._panel_action)
|
||
|
||
|
||
def get(self):
|
||
return self._appwindow
|
||
|
||
|
||
def present(self):
|
||
self._appwindow.present()
|
||
|
||
|
||
def on_resize(self, widget, event):
|
||
if not self._state.get_property(WindowState.IS_MAXIMIZED):
|
||
size = self._appwindow.get_size()
|
||
self._state.set_property(WindowState.WIDTH, size.width)
|
||
self._state.set_property(WindowState.HEIGHT, size.height)
|
||
|
||
|
||
def on_state(self, widget, state):
|
||
self._state.set_property(WindowState.IS_MAXIMIZED, (state.new_window_state & Gdk.WindowState.MAXIMIZED > 0))
|
||
self._fullscreen((state.new_window_state & Gdk.WindowState.FULLSCREEN > 0))
|
||
|
||
|
||
def on_menu_connect(self, action, value):
|
||
self._connect()
|
||
|
||
|
||
def on_menu_play(self, action, value):
|
||
self._mcg.playpause()
|
||
|
||
|
||
def on_menu_clear_playlist(self, action, value):
|
||
self._mcg.clear_playlist()
|
||
|
||
|
||
def on_menu_panel(self, action, value):
|
||
action.set_state(value)
|
||
self._stack.set_visible_child(self._panels[int(value.get_string())].get())
|
||
|
||
|
||
# HeaderBar callbacks
|
||
|
||
def on_header_bar_stack_switched(self, widget):
|
||
self._set_visible_toolbar()
|
||
self._save_visible_panel()
|
||
self._set_menu_visible_panel()
|
||
for panel in self._panels:
|
||
panel.set_selected(panel.get() == self._stack.get_visible_child())
|
||
GObject.idle_add(
|
||
self._stack.child_set_property,
|
||
self._stack.get_visible_child(),
|
||
'needs-attention',
|
||
False
|
||
)
|
||
|
||
|
||
def on_header_bar_connect(self, widget):
|
||
self._connect()
|
||
|
||
|
||
def on_header_bar_playpause(self, widget):
|
||
self._mcg.playpause()
|
||
self._mcg.get_status()
|
||
|
||
|
||
def on_header_bar_set_volume(self, widget, volume):
|
||
self._mcg.set_volume(volume)
|
||
|
||
|
||
# Panel callbacks
|
||
|
||
def on_connection_panel_connection_changed(self, widget, host, port, password, image_dir):
|
||
self._settings.set_string(Window.SETTING_HOST, host)
|
||
self._settings.set_int(Window.SETTING_PORT, port)
|
||
if use_keyring:
|
||
if password:
|
||
keyring.set_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME, password)
|
||
else:
|
||
if keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME):
|
||
keyring.delete_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME)
|
||
self._settings.set_string(Window.SETTING_IMAGE_DIR, image_dir)
|
||
|
||
|
||
def on_playlist_panel_clear_playlist(self, widget):
|
||
self._mcg.clear_playlist()
|
||
|
||
|
||
def on_playlist_panel_remove(self, widget, album):
|
||
self._mcg.remove_album_from_playlist(album)
|
||
|
||
|
||
def on_playlist_panel_remove_multiple(self, widget, albums):
|
||
self._mcg.remove_albums_from_playlist(albums)
|
||
|
||
|
||
def on_playlist_panel_play(self, widget, album):
|
||
self._mcg.play_album_from_playlist(album)
|
||
|
||
|
||
def on_server_panel_output_device_changed(self, widget, device, enabled):
|
||
self._mcg.enable_output_device(device, enabled)
|
||
|
||
|
||
def on_cover_panel_toggle_fullscreen(self, widget):
|
||
if not self._state.get_property(WindowState.IS_FULLSCREENED):
|
||
self._appwindow.fullscreen()
|
||
else:
|
||
self._appwindow.unfullscreen()
|
||
|
||
|
||
def on_cover_panel_set_song(self, widget, pos, time):
|
||
self._mcg.seek(pos, time)
|
||
|
||
|
||
def on_library_panel_update(self, widget):
|
||
self._mcg.update()
|
||
|
||
|
||
def on_library_panel_play(self, widget, album):
|
||
self._mcg.play_album(album)
|
||
|
||
|
||
def on_library_panel_queue(self, widget, album):
|
||
self._mcg.queue_album(album)
|
||
|
||
|
||
def on_library_panel_queue_multiple(self, widget, albums):
|
||
self._mcg.queue_albums(albums)
|
||
|
||
|
||
def on_library_panel_item_size_changed(self, widget, size):
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
|
||
self._settings.set_int(Window.SETTING_ITEM_SIZE, self._panels[Window._PANEL_INDEX_LIBRARY].get_item_size())
|
||
|
||
|
||
def on_library_panel_sort_order_changed(self, widget, sort_order):
|
||
self._settings.set_enum(Window.SETTING_SORT_ORDER, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_order())
|
||
|
||
|
||
def on_library_panel_sort_type_changed(self, widget, sort_type):
|
||
self._settings.set_boolean(Window.SETTING_SORT_TYPE, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_type())
|
||
|
||
|
||
# MCG callbacks
|
||
|
||
def on_mcg_connect(self, connected):
|
||
if connected:
|
||
GObject.idle_add(self._connect_connected)
|
||
self._mcg.load_playlist()
|
||
self._mcg.load_albums()
|
||
self._mcg.get_custom(Window._CUSTOM_STARTUP_COMPLETE)
|
||
self._mcg.get_status()
|
||
self._mcg.get_stats()
|
||
self._mcg.get_output_devices()
|
||
self._connect_action.set_state(GLib.Variant.new_boolean(True))
|
||
self._play_action.set_enabled(True)
|
||
self._clear_playlist_action.set_enabled(True)
|
||
self._panel_action.set_enabled(True)
|
||
else:
|
||
GObject.idle_add(self._connect_disconnected)
|
||
self._connect_action.set_state(GLib.Variant.new_boolean(False))
|
||
self._play_action.set_enabled(False)
|
||
self._clear_playlist_action.set_enabled(False)
|
||
self._panel_action.set_enabled(False)
|
||
|
||
|
||
def on_mcg_status(self, state, album, pos, time, volume, file, audio, bitrate, error):
|
||
# Album
|
||
GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_album, album)
|
||
if not album and self._state.get_property(WindowState.IS_FULLSCREENED):
|
||
self._fullscreen(False)
|
||
# State
|
||
if state == 'play':
|
||
GObject.idle_add(self._header_bar.set_play)
|
||
GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_play, pos, time)
|
||
self._play_action.set_state(GLib.Variant.new_boolean(True))
|
||
elif state == 'pause' or state == 'stop':
|
||
GObject.idle_add(self._header_bar.set_pause)
|
||
GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_pause)
|
||
self._play_action.set_state(GLib.Variant.new_boolean(False))
|
||
# Volume
|
||
GObject.idle_add(self._header_bar.set_volume, volume)
|
||
# Status
|
||
self._panels[Window._PANEL_INDEX_SERVER].set_status(file, audio, bitrate, error)
|
||
# Error
|
||
if error is None:
|
||
self._infobar.hide()
|
||
else:
|
||
self._show_error(error)
|
||
|
||
|
||
def on_mcg_stats(self, artists, albums, songs, dbplaytime, playtime, uptime):
|
||
self._panels[Window._PANEL_INDEX_SERVER].set_stats(artists, albums, songs, dbplaytime, playtime, uptime)
|
||
|
||
|
||
def on_mcg_load_output_devices(self, devices):
|
||
self._panels[Window._PANEL_INDEX_SERVER].set_output_devices(devices)
|
||
|
||
|
||
def on_mcg_load_playlist(self, playlist):
|
||
self._panels[self._PANEL_INDEX_PLAYLIST].set_playlist(self._connection_panel.get_host(), playlist)
|
||
|
||
|
||
def on_mcg_init_albums(self):
|
||
GObject.idle_add(self._panels[self._PANEL_INDEX_LIBRARY].init_albums)
|
||
|
||
|
||
def on_mcg_pulse_albums(self):
|
||
GObject.idle_add(self._panels[self._PANEL_INDEX_LIBRARY].load_albums)
|
||
|
||
|
||
def on_mcg_load_albums(self, albums):
|
||
self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._connection_panel.get_host(), albums)
|
||
|
||
|
||
def on_mcg_custom(self, name):
|
||
if name == Window._CUSTOM_STARTUP_COMPLETE:
|
||
for panel in self._panels:
|
||
GObject.idle_add(
|
||
self._stack.child_set_property,
|
||
panel.get(),
|
||
'needs-attention',
|
||
False
|
||
)
|
||
|
||
|
||
def on_mcg_error(self, error):
|
||
GObject.idle_add(self._show_error, str(error))
|
||
|
||
|
||
# Settings callbacks
|
||
|
||
def on_settings_panel_changed(self, settings, key):
|
||
panel_index = settings.get_int(key)
|
||
self._stack.set_visible_child(self._panels[panel_index].get())
|
||
|
||
|
||
def on_settings_item_size_changed(self, settings, key):
|
||
size = settings.get_int(key)
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(size)
|
||
|
||
|
||
def on_settings_sort_order_changed(self, settings, key):
|
||
sort_order = settings.get_enum(key)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(sort_order)
|
||
|
||
|
||
def on_settings_sort_type_changed(self, settings, key):
|
||
sort_type = settings.get_boolean(key)
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(sort_type)
|
||
|
||
|
||
# Private methods
|
||
|
||
def _connect(self):
|
||
self._connection_panel.get().set_sensitive(False)
|
||
self._header_bar.set_sensitive(False, True)
|
||
if self._mcg.is_connected():
|
||
self._mcg.disconnect()
|
||
self._settings.set_boolean(Window.SETTING_CONNECTED, False)
|
||
else:
|
||
host = self._connection_panel.get_host()
|
||
port = self._connection_panel.get_port()
|
||
password = self._connection_panel.get_password()
|
||
image_dir = self._connection_panel.get_image_dir()
|
||
self._mcg.connect(host, port, password, image_dir)
|
||
self._settings.set_boolean(Window.SETTING_CONNECTED, True)
|
||
|
||
|
||
def _connect_connected(self):
|
||
self._header_bar.connected()
|
||
self._header_bar.set_sensitive(True, False)
|
||
self._content_stack.set_visible_child(self._stack)
|
||
self._stack.set_visible_child(self._panels[self._settings.get_int(Window.SETTING_PANEL)].get())
|
||
|
||
|
||
def _connect_disconnected(self):
|
||
self._panels[Window._PANEL_INDEX_PLAYLIST].stop_threads();
|
||
self._panels[Window._PANEL_INDEX_LIBRARY].stop_threads();
|
||
self._header_bar.disconnected()
|
||
self._header_bar.set_sensitive(False, False)
|
||
self._save_visible_panel()
|
||
self._content_stack.set_visible_child(self._connection_panel.get())
|
||
self._connection_panel.get().set_sensitive(True)
|
||
|
||
|
||
def _fullscreen(self, fullscreened_new):
|
||
if fullscreened_new != self._state.get_property(WindowState.IS_FULLSCREENED):
|
||
self._state.set_property(WindowState.IS_FULLSCREENED, fullscreened_new)
|
||
if self._state.get_property(WindowState.IS_FULLSCREENED):
|
||
self._header_bar.get().hide()
|
||
self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(True)
|
||
else:
|
||
self._header_bar.get().show()
|
||
self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(False)
|
||
|
||
|
||
def _save_visible_panel(self):
|
||
panels = [panel.get() for panel in self._panels]
|
||
panel_index_selected = panels.index(self._stack.get_visible_child())
|
||
self._settings.set_int(Window.SETTING_PANEL, panel_index_selected)
|
||
|
||
|
||
def _set_menu_visible_panel(self):
|
||
panels = [panel.get() for panel in self._panels]
|
||
panel_index_selected = panels.index(self._stack.get_visible_child())
|
||
self._panel_action.set_state(GLib.Variant.new_string(str(panel_index_selected)))
|
||
|
||
|
||
def _set_visible_toolbar(self):
|
||
panels = [panel.get() for panel in self._panels]
|
||
panel_index_selected = panels.index(self._stack.get_visible_child())
|
||
toolbar = self._panels[panel_index_selected].get_toolbar()
|
||
self._toolbar_stack.set_visible_child(toolbar)
|
||
|
||
|
||
def _show_error(self, message):
|
||
self._infobar.show_error(message)
|
||
|
||
|
||
|
||
|
||
class HeaderBar(GObject.GObject):
|
||
__gsignals__ = {
|
||
'stack-switched': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||
'toolbar-connect': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||
'toolbar-playpause': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||
'toolbar-set-volume': (GObject.SIGNAL_RUN_FIRST, None, (int,))
|
||
}
|
||
|
||
|
||
def __init__(self, builder):
|
||
GObject.GObject.__init__(self)
|
||
self._changing_volume = False
|
||
self._setting_volume = False
|
||
|
||
# Widgets
|
||
self._header_bar = builder.get_object('headerbar')
|
||
self._title_stack = builder.get_object('headerbar-title-stack')
|
||
self._connection_label = builder.get_object('headerbar-connectionn-label')
|
||
self._stack_switcher = StackSwitcher(builder)
|
||
self._button_connect = builder.get_object('headerbar-connection')
|
||
self._button_playpause = builder.get_object('headerbar-playpause')
|
||
self._button_volume = builder.get_object('headerbar-volume')
|
||
|
||
# Signals
|
||
self._stack_switcher.connect('stack-switched', self.on_stack_switched)
|
||
self._button_handlers = {
|
||
'on_headerbar-connection_active_notify': self.on_connection_active_notify,
|
||
'on_headerbar-connection_state_set': self.on_connection_state_set,
|
||
'on_headerbar-playpause_toggled': self.on_playpause_toggled,
|
||
'on_headerbar-volume_value_changed': self.on_volume_changed,
|
||
'on_headerbar-volume_button_press_event': self.on_volume_press,
|
||
'on_headerbar-volume_button_release_event': self.on_volume_release
|
||
}
|
||
|
||
|
||
def get(self):
|
||
return self._header_bar
|
||
|
||
|
||
def get_signal_handlers(self):
|
||
return self._button_handlers
|
||
|
||
|
||
def set_sensitive(self, sensitive, connecting):
|
||
self._button_playpause.set_sensitive(sensitive)
|
||
self._button_volume.set_sensitive(sensitive)
|
||
self._stack_switcher.get().set_sensitive(sensitive)
|
||
self._button_connect.set_sensitive(not connecting)
|
||
|
||
|
||
def on_connection_active_notify(self, widget, status):
|
||
self.emit('toolbar-connect')
|
||
|
||
|
||
def on_connection_state_set(self, widget, state):
|
||
return True
|
||
|
||
|
||
def on_playpause_toggled(self, widget):
|
||
self.emit('toolbar-playpause')
|
||
|
||
|
||
def on_stack_switched(self, widget):
|
||
self.emit('stack-switched')
|
||
|
||
|
||
def on_volume_changed(self, widget, value):
|
||
if not self._setting_volume:
|
||
self.emit('toolbar-set-volume', int(value*100))
|
||
|
||
|
||
def on_volume_press(self, *args):
|
||
self.volume_set_active(None, None, True)
|
||
|
||
|
||
def on_volume_release(self, *args):
|
||
self.volume_set_active(None, None, False)
|
||
|
||
|
||
def volume_set_active(self, widget, event, active):
|
||
self._changing_volume = active
|
||
|
||
|
||
def connected(self):
|
||
self._button_connect.handler_block_by_func(
|
||
self.on_connection_active_notify
|
||
)
|
||
self._button_connect.set_active(True)
|
||
self._button_connect.set_state(True)
|
||
self._button_connect.handler_unblock_by_func(
|
||
self.on_connection_active_notify
|
||
)
|
||
self._title_stack.set_visible_child(self._stack_switcher.get())
|
||
|
||
|
||
def disconnected(self):
|
||
self._button_connect.handler_block_by_func(
|
||
self.on_connection_active_notify
|
||
)
|
||
self._button_connect.set_active(False)
|
||
self._button_connect.set_state(False)
|
||
self._button_connect.handler_unblock_by_func(
|
||
self.on_connection_active_notify
|
||
)
|
||
self._title_stack.set_visible_child(self._connection_label)
|
||
|
||
|
||
def set_play(self):
|
||
self._button_playpause.handler_block_by_func(
|
||
self.on_playpause_toggled
|
||
)
|
||
self._button_playpause.set_active(True)
|
||
self._button_playpause.handler_unblock_by_func(
|
||
self.on_playpause_toggled
|
||
)
|
||
|
||
|
||
def set_pause(self):
|
||
self._button_playpause.handler_block_by_func(
|
||
self.on_playpause_toggled
|
||
)
|
||
self._button_playpause.set_active(False)
|
||
self._button_playpause.handler_unblock_by_func(
|
||
self.on_playpause_toggled
|
||
)
|
||
|
||
|
||
def set_volume(self, volume):
|
||
if volume >= 0:
|
||
self._button_volume.set_visible(True)
|
||
if not self._changing_volume:
|
||
self._setting_volume = True
|
||
self._button_volume.set_value(volume / 100)
|
||
self._setting_volume = False
|
||
else:
|
||
self._button_volume.set_visible(False)
|
||
|
||
|
||
|
||
|
||
class InfoBar():
|
||
def __init__(self, builder):
|
||
# Widgets
|
||
self._revealer = builder.get_object('server-info-revealer')
|
||
self._bar = builder.get_object('server-info-bar')
|
||
self._message_label = builder.get_object('server-info-label')
|
||
|
||
|
||
def get_signal_handlers(self):
|
||
return {
|
||
'on_server-info-bar_close': self.on_close,
|
||
'on_server-info-bar_response': self.on_response
|
||
}
|
||
|
||
|
||
def on_close(self, *args):
|
||
self.hide()
|
||
|
||
|
||
def on_response(self, widget, response):
|
||
self.hide()
|
||
|
||
|
||
def hide(self):
|
||
self._revealer.set_reveal_child(False)
|
||
|
||
|
||
def show_error(self, message):
|
||
self._bar.set_message_type(Gtk.MessageType.ERROR)
|
||
self._message_label.set_text(message)
|
||
self._revealer.set_reveal_child(True)
|
||
|
||
|
||
|
||
|
||
class ConnectionPanel(GObject.GObject):
|
||
__gsignals__ = {
|
||
'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str, str))
|
||
}
|
||
|
||
|
||
def __init__(self, builder):
|
||
GObject.GObject.__init__(self)
|
||
self._services = Gtk.ListStore(str, str, int)
|
||
self._profile = None
|
||
|
||
# Widgets
|
||
self._panel = builder.get_object('connection-panel')
|
||
# Zeroconf
|
||
self._zeroconf_list = builder.get_object('server-zeroconf-list')
|
||
self._zeroconf_list.set_model(self._services)
|
||
renderer = Gtk.CellRendererText()
|
||
column = Gtk.TreeViewColumn("Zeroconf", renderer, text=0)
|
||
self._zeroconf_list.append_column(column)
|
||
# Host
|
||
self._host_entry = builder.get_object('server-host')
|
||
# Port
|
||
self._port_spinner = builder.get_object('server-port')
|
||
# Passwort
|
||
self._password_entry = builder.get_object('server-password')
|
||
# Image directory
|
||
self._image_dir_entry = builder.get_object('server-image-dir')
|
||
|
||
# Zeroconf provider
|
||
self._zeroconf_provider = ZeroconfProvider()
|
||
self._zeroconf_provider.connect_signal(ZeroconfProvider.SIGNAL_SERVICE_NEW, self.on_new_service)
|
||
|
||
|
||
def get(self):
|
||
return self._panel
|
||
|
||
|
||
def get_signal_handlers(self):
|
||
return {
|
||
'on_server-zeroconf-list-selection_changed': self.on_service_selected,
|
||
'on_server-zeroconf-list_focus_out_event': self.on_zeroconf_list_outfocused,
|
||
'on_server-host_focus_out_event': self.on_host_entry_outfocused,
|
||
'on_server-port_value_changed': self.on_port_spinner_value_changed,
|
||
'on_server-password_focus_out_event': self.on_password_entry_outfocused,
|
||
'on_server-image-dir_focus_out_event': self.on_image_dir_entry_outfocused
|
||
}
|
||
|
||
|
||
def on_new_service(self, service):
|
||
name, host, port = service
|
||
self._services.append([name, host, port])
|
||
|
||
|
||
def on_service_selected(self, selection):
|
||
model, treeiter = selection.get_selected()
|
||
if treeiter != None:
|
||
service = model[treeiter]
|
||
self.set_host(service[1])
|
||
self.set_port(service[2])
|
||
|
||
|
||
def on_zeroconf_list_outfocused(self, widget, event):
|
||
self._zeroconf_list.get_selection().unselect_all()
|
||
|
||
|
||
def on_host_entry_outfocused(self, widget, event):
|
||
self._call_back()
|
||
|
||
|
||
def on_port_spinner_value_changed(self, widget):
|
||
self._call_back()
|
||
|
||
|
||
def on_password_entry_outfocused(self, widget, event):
|
||
self._call_back()
|
||
|
||
|
||
def on_image_dir_entry_outfocused(self, widget, event):
|
||
self._call_back()
|
||
|
||
|
||
def set_host(self, host):
|
||
self._host_entry.set_text(host)
|
||
|
||
|
||
def get_host(self):
|
||
return self._host_entry.get_text()
|
||
|
||
|
||
def set_port(self, port):
|
||
self._port_spinner.set_value(port)
|
||
|
||
|
||
def get_port(self):
|
||
return self._port_spinner.get_value_as_int()
|
||
|
||
|
||
def set_password(self, password):
|
||
if password is None:
|
||
password = ""
|
||
self._password_entry.set_text(password)
|
||
|
||
|
||
def get_password(self):
|
||
if self._password_entry.get_text() == "":
|
||
return None
|
||
else:
|
||
return self._password_entry.get_text()
|
||
|
||
|
||
def set_image_dir(self, image_dir):
|
||
self._image_dir_entry.set_text(image_dir)
|
||
|
||
|
||
def get_image_dir(self):
|
||
return self._image_dir_entry.get_text()
|
||
|
||
|
||
def _call_back(self):
|
||
self.emit('connection-changed', self.get_host(), self.get_port(), self.get_password(), self.get_image_dir())
|
||
|
||
|
||
|
||
|
||
class ServerPanel(GObject.GObject):
|
||
__gsignals__ = {
|
||
'change-output-device': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,bool,)),
|
||
}
|
||
|
||
|
||
def __init__(self, builder):
|
||
GObject.GObject.__init__(self)
|
||
self._none_label = ""
|
||
self._output_buttons = {}
|
||
self._is_selected = False
|
||
|
||
# Widgets
|
||
self._panel = builder.get_object('server-panel')
|
||
self._toolbar = builder.get_object('server-toolbar')
|
||
self._stack = builder.get_object('server-stack')
|
||
|
||
# Status widgets
|
||
self._status_file = builder.get_object('server-status-file')
|
||
self._status_audio = builder.get_object('server-status-audio')
|
||
self._status_bitrate = builder.get_object('server-status-bitrate')
|
||
self._status_error = builder.get_object('server-status-error')
|
||
self._none_label = self._status_file.get_label()
|
||
|
||
# Stats widgets
|
||
self._stats_artists = builder.get_object('server-stats-artists')
|
||
self._stats_albums = builder.get_object('server-stats-albums')
|
||
self._stats_songs = builder.get_object('server-stats-songs')
|
||
self._stats_dbplaytime = builder.get_object('server-stats-dbplaytime')
|
||
self._stats_playtime = builder.get_object('server-stats-playtime')
|
||
self._stats_uptime = builder.get_object('server-stats-uptime')
|
||
|
||
# Audio ouptut devices widgets
|
||
self._output_devices = builder.get_object('server-output-devices')
|
||
|
||
|
||
def get(self):
|
||
return self._panel
|
||
|
||
|
||
def set_selected(self, selected):
|
||
self._is_selected = selected
|
||
|
||
|
||
def get_toolbar(self):
|
||
return self._toolbar
|
||
|
||
|
||
def on_output_device_toggled(self, widget, device):
|
||
self.emit('change-output-device', device, widget.get_active())
|
||
|
||
|
||
def set_status(self, file, audio, bitrate, error):
|
||
if file:
|
||
file = GObject.markup_escape_text(file)
|
||
else:
|
||
file = self._none_label
|
||
self._status_file.set_markup(file)
|
||
# Audio information
|
||
if audio:
|
||
parts = audio.split(":")
|
||
if len(parts) == 3:
|
||
audio = "{} Hz, {} bit, {} channels".format(parts[0], parts[1], parts[2])
|
||
else:
|
||
audio = self._none_label
|
||
self._status_audio.set_markup(audio)
|
||
# Bitrate
|
||
if bitrate:
|
||
bitrate = bitrate + " kb/s"
|
||
else:
|
||
bitrate = self._none_label
|
||
self._status_bitrate.set_markup(bitrate)
|
||
# Error
|
||
if error:
|
||
error = GObject.markup_escape_text(error)
|
||
else:
|
||
error = self._none_label
|
||
self._status_error.set_markup(error)
|
||
|
||
|
||
def set_stats(self, artists, albums, songs, dbplaytime, playtime, uptime):
|
||
self._stats_artists.set_text(str(artists))
|
||
self._stats_albums.set_text(str(albums))
|
||
self._stats_songs.set_text(str(songs))
|
||
self._stats_dbplaytime.set_text(str(dbplaytime))
|
||
self._stats_playtime.set_text(str(playtime))
|
||
self._stats_uptime.set_text(str(uptime))
|
||
|
||
|
||
def set_output_devices(self, devices):
|
||
device_ids = []
|
||
|
||
# Add devices
|
||
for device in devices:
|
||
device_ids.append(device.get_id())
|
||
if device.get_id() in self._output_buttons.keys():
|
||
self._output_buttons[device.get_id()].freeze_notify()
|
||
self._output_buttons[device.get_id()].set_active(device.is_enabled())
|
||
self._output_buttons[device.get_id()].thaw_notify()
|
||
else:
|
||
button = Gtk.CheckButton(device.get_name())
|
||
if device.is_enabled():
|
||
button.set_active(True)
|
||
handler = button.connect('toggled', self.on_output_device_toggled, device)
|
||
self._output_devices.insert(button, -1)
|
||
self._output_buttons[device.get_id()] = button
|
||
self._output_devices.show_all()
|
||
|
||
# Remove devices
|
||
for id in self._output_buttons.keys():
|
||
if id not in device_ids:
|
||
self._output_devices.remove(self._output_buttons[id].get_parent())
|
||
|
||
|
||
|
||
|
||
|
||
class CoverPanel(GObject.GObject):
|
||
__gsignals__ = {
|
||
'toggle-fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||
'set-song': (GObject.SIGNAL_RUN_FIRST, None, (int, int,))
|
||
}
|
||
|
||
|
||
def __init__(self, builder):
|
||
GObject.GObject.__init__(self)
|
||
|
||
self._current_album = None
|
||
self._cover_pixbuf = None
|
||
self._timer = None
|
||
self._properties = {}
|
||
self._icon_theme = Gtk.IconTheme.get_default()
|
||
self._fullscreened = False
|
||
self._is_selected = False
|
||
self._current_size = None
|
||
|
||
# Widgets
|
||
self._appwindow = builder.get_object('appwindow')
|
||
self._panel = builder.get_object('cover-panel')
|
||
self._toolbar = builder.get_object('cover-toolbar')
|
||
# Toolbar menu
|
||
self._toolbar_fullscreen_button = builder.get_object('cover-toolbar-fullscreen')
|
||
# Cover
|
||
self._cover_stack = builder.get_object('cover-stack')
|
||
self._cover_spinner = builder.get_object('cover-spinner')
|
||
self._cover_scroll = builder.get_object('cover-scroll')
|
||
self._cover_box = builder.get_object('cover-box')
|
||
self._cover_image = builder.get_object('cover-image')
|
||
self._cover_stack.set_visible_child(self._cover_scroll)
|
||
self._cover_pixbuf = self._get_default_image()
|
||
# Album Infos
|
||
self._cover_info_scroll = builder.get_object('cover-info-scroll')
|
||
self._info_revealer = builder.get_object('cover-info-revealer')
|
||
self._info_box = builder.get_object('cover-info-box')
|
||
self._album_title_label = builder.get_object('cover-album')
|
||
self._album_date_label = builder.get_object('cover-date')
|
||
self._album_artist_label = builder.get_object('cover-artist')
|
||
# Songs
|
||
self._songs_scale = builder.get_object('cover-songs')
|
||
|
||
# Initial actions
|
||
GObject.idle_add(self._enable_tracklist)
|
||
|
||
|
||
def get(self):
|
||
return self._panel
|
||
|
||
|
||
def set_selected(self, selected):
|
||
self._is_selected = selected
|
||
|
||
|
||
def get_toolbar(self):
|
||
return self._toolbar
|
||
|
||
|
||
def get_signal_handlers(self):
|
||
return {
|
||
'on_cover-toolbar-fullscreen_clicked': self.on_fullscreen_clicked,
|
||
'on_cover-box_button_press_event': self.on_cover_box_pressed,
|
||
'on_cover-scroll_size_allocate': self.on_cover_size_allocate,
|
||
'on_cover-songs_button_press_event': self.on_songs_start_change,
|
||
'on_cover-songs_button_release_event': self.on_songs_change
|
||
}
|
||
|
||
|
||
def on_fullscreen_clicked(self, widget):
|
||
self.emit('toggle-fullscreen')
|
||
|
||
|
||
def on_cover_box_pressed(self, widget, event):
|
||
if self._current_album and event.type == Gdk.EventType._2BUTTON_PRESS:
|
||
self.emit('toggle-fullscreen')
|
||
|
||
|
||
def on_cover_size_allocate(self, widget, allocation):
|
||
GObject.idle_add(self._resize_image)
|
||
self._cover_info_scroll.set_max_content_width(allocation.width // 2)
|
||
|
||
|
||
def on_songs_start_change(self, widget, event):
|
||
if self._timer:
|
||
GObject.source_remove(self._timer)
|
||
self._timer = None
|
||
|
||
|
||
def on_songs_change(self, widget, event):
|
||
value = int(self._songs_scale.get_value())
|
||
time = self._current_album.get_length()
|
||
tracks = self._current_album.get_tracks()
|
||
pos = 0
|
||
for index in range(len(tracks)-1, -1, -1):
|
||
time = time - tracks[index].get_length()
|
||
pos = tracks[index].get_pos()
|
||
if time < value:
|
||
break
|
||
time = max(value - time - 1, 0)
|
||
self.emit('set-song', pos, time)
|
||
|
||
|
||
def set_album(self, album):
|
||
if album:
|
||
# Set labels
|
||
self._album_title_label.set_label(album.get_title())
|
||
self._album_date_label.set_label(', '.join(album.get_dates()))
|
||
self._album_artist_label.set_label(', '.join(album.get_albumartists()))
|
||
|
||
# Set tracks
|
||
self._set_tracks(album)
|
||
|
||
# Set current album
|
||
old_album = self._current_album
|
||
self._current_album = album
|
||
self._enable_tracklist()
|
||
self._toolbar_fullscreen_button.set_sensitive(self._current_album is not None)
|
||
|
||
# Load cover
|
||
threading.Thread(target=self._set_cover, args=(old_album, album,)).start()
|
||
|
||
|
||
def set_play(self, pos, time):
|
||
if self._timer is not None:
|
||
GObject.source_remove(self._timer)
|
||
self._timer = None
|
||
tracks = self._current_album.get_tracks()
|
||
for index in range(0, pos):
|
||
time = time + tracks[index].get_length()
|
||
|
||
self._songs_scale.set_value(time+1)
|
||
self._timer = GObject.timeout_add(1000, self._playing)
|
||
|
||
|
||
def set_pause(self):
|
||
if self._timer is not None:
|
||
GObject.source_remove(self._timer)
|
||
self._timer = None
|
||
|
||
|
||
def set_fullscreen(self, active):
|
||
if active:
|
||
self._info_revealer.set_reveal_child(False)
|
||
self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
|
||
GObject.idle_add(self._resize_image)
|
||
# Hide curser
|
||
self._appwindow.get_window().set_cursor(
|
||
Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "none")
|
||
)
|
||
self._fullscreened = True
|
||
else:
|
||
self._fullscreened = False
|
||
self._info_revealer.set_reveal_child(True)
|
||
self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
|
||
GObject.idle_add(self._resize_image)
|
||
# Reset cursor
|
||
self._appwindow.get_window().set_cursor(
|
||
Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "default")
|
||
)
|
||
|
||
|
||
def _set_cover(self, current_album, new_album):
|
||
self._cover_stack.set_visible_child(self._cover_spinner)
|
||
self._cover_spinner.start()
|
||
if not current_album or not new_album or current_album != new_album:
|
||
url = new_album.get_cover() if new_album else None
|
||
if url:
|
||
# Load image and draw it
|
||
self._cover_pixbuf = Utils.load_cover(url)
|
||
else:
|
||
# Reset image
|
||
self._cover_pixbuf = self._get_default_image()
|
||
self._current_size = None
|
||
self._resize_image()
|
||
self._cover_stack.set_visible_child(self._cover_scroll)
|
||
self._cover_spinner.stop()
|
||
|
||
|
||
def _set_tracks(self, album):
|
||
self._songs_scale.clear_marks()
|
||
self._songs_scale.set_range(0, album.get_length())
|
||
length = 0
|
||
for track in album.get_tracks():
|
||
cur_length = length
|
||
if length > 0 and length < album.get_length():
|
||
cur_length = cur_length + 1
|
||
self._songs_scale.add_mark(
|
||
cur_length,
|
||
Gtk.PositionType.RIGHT,
|
||
GObject.markup_escape_text(
|
||
Utils.create_track_title(track)
|
||
)
|
||
)
|
||
length = length + track.get_length()
|
||
self._songs_scale.add_mark(
|
||
length,
|
||
Gtk.PositionType.RIGHT,
|
||
"{0[0]:02d}:{0[1]:02d} minutes".format(divmod(length, 60))
|
||
)
|
||
|
||
|
||
def _enable_tracklist(self):
|
||
if self._current_album:
|
||
# enable
|
||
self._info_revealer.set_reveal_child(True)
|
||
else:
|
||
# disable
|
||
self._info_revealer.set_reveal_child(False)
|
||
|
||
|
||
def _playing(self):
|
||
value = self._songs_scale.get_value() + 1
|
||
self._songs_scale.set_value(value)
|
||
|
||
return True
|
||
|
||
|
||
def _resize_image(self):
|
||
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
|
||
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
|
||
"""
|
||
# Get size
|
||
size = self._cover_scroll.get_allocation()
|
||
# Abort if size is the same
|
||
if self._current_size and size.width == self._current_size.width and size.height == self._current_size.height:
|
||
return
|
||
self._current_size = size
|
||
|
||
# Get pixelbuffer
|
||
pixbuf = self._cover_pixbuf
|
||
# Check pixelbuffer
|
||
if pixbuf is None:
|
||
return
|
||
|
||
# Skalierungswert für Breite und Höhe ermitteln
|
||
ratioW = float(size.width) / float(pixbuf.get_width())
|
||
ratioH = float(size.height) / float(pixbuf.get_height())
|
||
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
|
||
ratio = min(ratioW, ratioH)
|
||
ratio = min(ratio, 1)
|
||
# Neue Breite und Höhe berechnen
|
||
width = int(math.floor(pixbuf.get_width()*ratio))
|
||
height = int(math.floor(pixbuf.get_height()*ratio))
|
||
if width <= 0 or height <= 0:
|
||
return
|
||
# Pixelpuffer auf Oberfläche zeichnen
|
||
self._cover_image.set_allocation(self._cover_scroll.get_allocation())
|
||
self._cover_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
|
||
self._cover_image.show()
|
||
|
||
|
||
def _get_default_image(self):
|
||
return self._icon_theme.load_icon(
|
||
Window.STOCK_ICON_DEFAULT,
|
||
512,
|
||
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
|
||
)
|
||
|
||
|
||
|
||
|
||
class PlaylistPanel(GObject.GObject):
|
||
__gsignals__ = {
|
||
'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||
'remove': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||
'remove-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||
'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,))
|
||
}
|
||
|
||
|
||
def __init__(self, builder):
|
||
GObject.GObject.__init__(self)
|
||
self._host = None
|
||
self._item_size = 150
|
||
self._playlist = None
|
||
self._playlist_albums = None
|
||
self._playlist_lock = threading.Lock()
|
||
self._playlist_stop = threading.Event()
|
||
self._icon_theme = Gtk.IconTheme.get_default()
|
||
self._standalone_pixbuf = None
|
||
self._selected_albums = []
|
||
self._is_selected = False
|
||
|
||
# Widgets
|
||
self._appwindow = builder.get_object('appwindow')
|
||
self._panel = builder.get_object('playlist-panel')
|
||
self._toolbar = builder.get_object('playlist-toolbar')
|
||
self._headerbar = builder.get_object('headerbar')
|
||
self._headerbar_standalone = builder.get_object('headerbar-playlist-standalone')
|
||
self._panel_normal = builder.get_object('playlist-panel-normal')
|
||
self._panel_standalone = builder.get_object('playlist-panel-standalone')
|
||
self._actionbar_revealer = builder.get_object('playlist-actionbar-revealer')
|
||
|
||
# Select button
|
||
self._select_button = builder.get_object('playlist-toolbar-select')
|
||
# Clear button
|
||
self._playlist_clear_button = builder.get_object('playlist-toolbar-clear')
|
||
# Playlist Grid: Model
|
||
self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
|
||
# Playlist Grid
|
||
self._playlist_grid = builder.get_object('playlist-iconview')
|
||
self._playlist_grid.set_model(self._playlist_grid_model)
|
||
self._playlist_grid.set_pixbuf_column(0)
|
||
self._playlist_grid.set_text_column(-1)
|
||
self._playlist_grid.set_tooltip_column(1)
|
||
# Action bar (normal)
|
||
actionbar = builder.get_object('playlist-actionbar')
|
||
cancel_button = Gtk.Button(gettext.gettext("cancel"))
|
||
cancel_button.connect('clicked', self.on_selection_cancel_clicked)
|
||
actionbar.pack_start(cancel_button)
|
||
remove_button = Gtk.Button('remove')
|
||
remove_button.connect('clicked', self.on_selection_remove_clicked)
|
||
actionbar.pack_end(remove_button)
|
||
|
||
# Standalone labels
|
||
self._standalone_title = builder.get_object('headerbar-playlist-standalone-title')
|
||
self._standalone_artist = builder.get_object('headerbar-playlist-standalone-artist')
|
||
# Standalone Image
|
||
self._standalone_stack = builder.get_object('playlist-standalone-stack')
|
||
self._standalone_spinner = builder.get_object('playlist-standalone-spinner')
|
||
self._standalone_scroll = builder.get_object('playlist-standalone-scroll')
|
||
self._standalone_image = builder.get_object('playlist-standalone-image')
|
||
# Action bar (standalone)
|
||
actionbar_standalone = builder.get_object('playlist-standalone-actionbar')
|
||
play_button = Gtk.Button(gettext.gettext("play"))
|
||
play_button.connect('clicked', self.on_standalone_play_clicked)
|
||
actionbar_standalone.pack_end(play_button)
|
||
remove_button = Gtk.Button(gettext.gettext("remove"))
|
||
remove_button.connect('clicked', self.on_standalone_remove_clicked)
|
||
actionbar_standalone.pack_end(remove_button)
|
||
|
||
|
||
def get(self):
|
||
return self._panel
|
||
|
||
|
||
def set_selected(self, selected):
|
||
self._is_selected = selected
|
||
|
||
|
||
def get_toolbar(self):
|
||
return self._toolbar
|
||
|
||
|
||
def get_signal_handlers(self):
|
||
return {
|
||
'on_playlist-toolbar-select_toggled': self.on_select_toggled,
|
||
'on_playlist-toolbar-clear_clicked': self.clear_clicked,
|
||
'on_playlist-iconview_item_activated': self.on_playlist_grid_clicked,
|
||
'on_playlist-iconview_selection_changed': self.on_playlist_grid_selection_changed,
|
||
'on_playlist-standalone-scroll_size_allocate': self.on_standalone_scroll_size_allocate,
|
||
'on_headerbar-playlist-standalone-close_clicked': self.on_standalone_close_clicked
|
||
}
|
||
|
||
|
||
def on_select_toggled(self, widget):
|
||
if widget.get_active():
|
||
self._actionbar_revealer.set_reveal_child(True)
|
||
self._playlist_grid.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
||
self._playlist_grid.get_style_context().add_class(Window._CSS_SELECTION)
|
||
else:
|
||
self._actionbar_revealer.set_reveal_child(False)
|
||
self._playlist_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||
self._playlist_grid.get_style_context().remove_class(Window._CSS_SELECTION)
|
||
|
||
|
||
def clear_clicked(self, widget):
|
||
if widget is self._playlist_clear_button:
|
||
self.emit('clear-playlist')
|
||
|
||
|
||
def on_playlist_grid_clicked(self, widget, path):
|
||
# Get selected album
|
||
iter = self._playlist_grid_model.get_iter(path)
|
||
hash = self._playlist_grid_model.get_value(iter, 2)
|
||
album = self._playlist_albums[hash]
|
||
self._selected_albums = [album]
|
||
|
||
# Show standalone album
|
||
if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE:
|
||
# Set labels
|
||
self._standalone_title.set_text(album.get_title())
|
||
self._standalone_artist.set_text(", ".join(album.get_albumartists()))
|
||
|
||
# Show panel
|
||
self._open_standalone()
|
||
|
||
# Load cover
|
||
threading.Thread(target=self._show_standalone_image, args=(album,)).start()
|
||
|
||
|
||
def on_playlist_grid_selection_changed(self, widget):
|
||
self._selected_albums = []
|
||
for path in widget.get_selected_items():
|
||
iter = self._playlist_grid_model.get_iter(path)
|
||
hash = self._playlist_grid_model.get_value(iter, 2)
|
||
self._selected_albums.append(self._playlist_albums[hash])
|
||
|
||
|
||
def on_selection_cancel_clicked(self, widget):
|
||
self._select_button.set_active(False)
|
||
|
||
|
||
def on_selection_remove_clicked(self, widget):
|
||
self.emit('remove-multiple', self._selected_albums)
|
||
self._select_button.set_active(False)
|
||
|
||
|
||
def on_standalone_scroll_size_allocate(self, widget, allocation):
|
||
self._resize_standalone_image()
|
||
|
||
|
||
def on_standalone_close_clicked(self, widget):
|
||
self._close_standalone()
|
||
|
||
|
||
def on_standalone_remove_clicked(self, widget):
|
||
self.emit('remove', self._selected_albums[0])
|
||
self._close_standalone()
|
||
|
||
|
||
def on_standalone_play_clicked(self, widget):
|
||
self.emit('play', self._selected_albums[0])
|
||
self._close_standalone()
|
||
|
||
|
||
def set_item_size(self, item_size):
|
||
if self._item_size != item_size:
|
||
self._item_size = item_size
|
||
self._redraw()
|
||
|
||
|
||
def get_item_size(self):
|
||
return self._item_size
|
||
|
||
|
||
def set_playlist(self, host, playlist):
|
||
self._host = host
|
||
self._playlist_stop.set()
|
||
threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start()
|
||
|
||
|
||
def stop_threads(self):
|
||
self._playlist_stop.set()
|
||
|
||
|
||
def _set_playlist(self, host, playlist, size):
|
||
if not self._is_selected and self._playlist != playlist:
|
||
GObject.idle_add(
|
||
self.get().get_parent().child_set_property,
|
||
self.get(),
|
||
'needs-attention',
|
||
True
|
||
)
|
||
self._playlist_lock.acquire()
|
||
self._playlist_stop.clear()
|
||
self._playlist = playlist
|
||
self._playlist_albums = {}
|
||
for album in playlist:
|
||
self._playlist_albums[album.get_id()] = album
|
||
self._playlist_grid.set_model(None)
|
||
self._playlist_grid.freeze_child_notify()
|
||
self._playlist_grid_model.clear()
|
||
GObject.idle_add(self._playlist_grid.set_item_padding, size / 100)
|
||
|
||
cache = client.MCGCache(host, size)
|
||
for album in playlist:
|
||
pixbuf = None
|
||
if album.get_cover() is not None:
|
||
try:
|
||
pixbuf = Utils.load_thumbnail(cache, album, size)
|
||
except Exception as e:
|
||
print(e)
|
||
if pixbuf is None:
|
||
pixbuf = self._icon_theme.load_icon(
|
||
Window.STOCK_ICON_DEFAULT,
|
||
self._item_size,
|
||
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
|
||
)
|
||
if pixbuf is not None:
|
||
self._playlist_grid_model.append([
|
||
pixbuf,
|
||
GObject.markup_escape_text("\n".join([
|
||
album.get_title(),
|
||
', '.join(album.get_dates()),
|
||
Utils.create_artists_label(album),
|
||
Utils.create_length_label(album)
|
||
])),
|
||
album.get_id()
|
||
])
|
||
|
||
if self._playlist_stop.is_set():
|
||
self._playlist_lock.release()
|
||
return
|
||
|
||
self._playlist_grid.set_model(self._playlist_grid_model)
|
||
self._playlist_grid.thaw_child_notify()
|
||
# TODO why set_columns()?
|
||
#self._playlist_grid.set_columns(len(playlist))
|
||
self._playlist_lock.release()
|
||
|
||
|
||
def _redraw(self):
|
||
if self._playlist is not None:
|
||
self.set_playlist(self._host, self._playlist)
|
||
|
||
|
||
def _open_standalone(self):
|
||
self._panel.set_visible_child(self._panel_standalone)
|
||
self._appwindow.set_titlebar(self._headerbar_standalone)
|
||
|
||
|
||
def _close_standalone(self):
|
||
self._panel.set_visible_child(self._panel.get_children()[0])
|
||
self._appwindow.set_titlebar(self._headerbar)
|
||
|
||
|
||
def _show_standalone_image(self, album):
|
||
self._standalone_stack.set_visible_child(self._standalone_spinner)
|
||
self._standalone_spinner.start()
|
||
url = album.get_cover()
|
||
if url:
|
||
# Load image and draw it
|
||
self._standalone_pixbuf = Utils.load_cover(url)
|
||
else:
|
||
# Reset image
|
||
self._standalone_pixbuf = self._get_default_image()
|
||
self._resize_standalone_image()
|
||
self._standalone_stack.set_visible_child(self._standalone_scroll)
|
||
self._standalone_spinner.stop()
|
||
|
||
|
||
def _resize_standalone_image(self):
|
||
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
|
||
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
|
||
"""
|
||
pixbuf = self._standalone_pixbuf
|
||
size = self._standalone_scroll.get_allocation()
|
||
# Check pixelbuffer
|
||
if pixbuf is None:
|
||
return
|
||
|
||
# Skalierungswert für Breite und Höhe ermitteln
|
||
ratioW = float(size.width) / float(pixbuf.get_width())
|
||
ratioH = float(size.height) / float(pixbuf.get_height())
|
||
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
|
||
ratio = min(ratioW, ratioH)
|
||
ratio = min(ratio, 1)
|
||
# Neue Breite und Höhe berechnen
|
||
width = int(math.floor(pixbuf.get_width()*ratio))
|
||
height = int(math.floor(pixbuf.get_height()*ratio))
|
||
if width <= 0 or height <= 0:
|
||
return
|
||
# Pixelpuffer auf Oberfläche zeichnen
|
||
self._standalone_image.set_allocation(self._standalone_scroll.get_allocation())
|
||
self._standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
|
||
self._standalone_image.show()
|
||
|
||
|
||
def _get_default_image(self):
|
||
return self._icon_theme.load_icon(
|
||
Window.STOCK_ICON_DEFAULT,
|
||
512,
|
||
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
|
||
)
|
||
|
||
|
||
|
||
|
||
class LibraryPanel(GObject.GObject):
|
||
__gsignals__ = {
|
||
'update': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||
'play': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
|
||
'queue': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
|
||
'queue-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||
'item-size-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
|
||
'sort-order-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
|
||
'sort-type-changed': (GObject.SIGNAL_RUN_FIRST, None, (Gtk.SortType,))
|
||
}
|
||
|
||
|
||
def __init__(self, builder):
|
||
GObject.GObject.__init__(self)
|
||
self._buttons = {}
|
||
self._albums = None
|
||
self._host = "localhost"
|
||
self._filter_string = ""
|
||
self._item_size = 150
|
||
self._sort_order = SortOrder.YEAR
|
||
self._sort_type = Gtk.SortType.DESCENDING
|
||
self._grid_pixbufs = {}
|
||
self._old_ranges = {}
|
||
self._library_lock = threading.Lock()
|
||
self._library_stop = threading.Event()
|
||
self._icon_theme = Gtk.IconTheme.get_default()
|
||
self._standalone_pixbuf = None
|
||
self._selected_albums = []
|
||
self._allocation = (0, 0)
|
||
self._is_selected = False
|
||
|
||
# Widgets
|
||
self._appwindow = builder.get_object('appwindow')
|
||
self._panel = builder.get_object('library-panel')
|
||
self._toolbar = builder.get_object('library-toolbar')
|
||
self._headerbar = builder.get_object('headerbar')
|
||
self._headerbar_standalone = builder.get_object('headerbar-library-standalone')
|
||
self._panel_normal = builder.get_object('library-panel-normal')
|
||
self._panel_standalone = builder.get_object('library-panel-standalone')
|
||
self._actionbar_revealer = builder.get_object('library-actionbar-revealer')
|
||
|
||
# Select button
|
||
self._select_button = builder.get_object('library-toolbar-select')
|
||
# Filter/search bar
|
||
self._filter_bar = builder.get_object('library-filter-bar')
|
||
self._filter_entry = builder.get_object('library-filter')
|
||
# Progress Bar
|
||
self._stack = builder.get_object('library-stack')
|
||
self._progress_box = builder.get_object('library-progress-box')
|
||
self._pgross_image = builder.get_object('library-progress-image')
|
||
self._pgross_image.set_from_pixbuf(self._get_default_image())
|
||
self._progress_bar = builder.get_object('library-progress')
|
||
self._scroll = builder.get_object('library-scroll')
|
||
# Toolbar menu
|
||
self._toolbar_search_bar = builder.get_object('library-toolbar-search')
|
||
self._toolbar_popover = builder.get_object('library-toolbar-popover')
|
||
self._toolbar_sort_buttons = {
|
||
SortOrder.ARTIST: builder.get_object('library-toolbar-sort-artist'),
|
||
SortOrder.TITLE: builder.get_object('library-toolbar-sort-title'),
|
||
SortOrder.YEAR: builder.get_object('library-toolbar-sort-year')
|
||
}
|
||
self._toolbar_sort_order_button = builder.get_object('library-toolbar-sort-order')
|
||
self._grid_scale = builder.get_object('library-toolbar-scale')
|
||
self._grid_scale.set_value(self._item_size)
|
||
self._grid_adjustment = builder.get_object('library-scale-adjustment')
|
||
# Library Grid: Model
|
||
self._library_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
|
||
self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
|
||
self._library_grid_model.set_sort_column_id(2, self._sort_type)
|
||
self._library_grid_filter = self._library_grid_model.filter_new()
|
||
self._library_grid_filter.set_visible_func(self.on_filter_visible)
|
||
# Library Grid
|
||
self._library_grid = builder.get_object('library-iconview')
|
||
self._library_grid.set_model(self._library_grid_filter)
|
||
self._library_grid.set_pixbuf_column(0)
|
||
self._library_grid.set_text_column(-1)
|
||
self._library_grid.set_tooltip_column(1)
|
||
# Action bar (normal)
|
||
actionbar = builder.get_object('library-actionbar')
|
||
cancel_button = Gtk.Button(gettext.gettext("cancel"))
|
||
cancel_button.connect('clicked', self.on_selection_cancel_clicked)
|
||
actionbar.pack_start(cancel_button)
|
||
add_button = Gtk.Button(gettext.gettext("queue"))
|
||
add_button.connect('clicked', self.on_selection_add_clicked)
|
||
actionbar.pack_end(add_button)
|
||
|
||
# Standalone labels
|
||
self._standalone_title = builder.get_object('headerbar-library-standalone-title')
|
||
self._standalone_artist = builder.get_object('headerbar-library-standalone-artist')
|
||
# Standalone Image
|
||
self._standalone_stack = builder.get_object('library-standalone-stack')
|
||
self._standalone_spinner = builder.get_object('library-standalone-spinner')
|
||
self._standalone_scroll = builder.get_object('library-standalone-scroll')
|
||
self._standalone_image = builder.get_object('library-standalone-image')
|
||
# Action bar (standalone)
|
||
actionbar_standalone = builder.get_object('library-standalone-actionbar')
|
||
play_button = Gtk.Button(gettext.gettext("play"))
|
||
play_button.connect('clicked', self.on_standalone_play_clicked)
|
||
actionbar_standalone.pack_end(play_button)
|
||
queue_button = Gtk.Button(gettext.gettext("queue"))
|
||
queue_button.connect('clicked', self.on_standalone_queue_clicked)
|
||
actionbar_standalone.pack_end(queue_button)
|
||
|
||
|
||
def get(self):
|
||
return self._panel
|
||
|
||
|
||
def set_selected(self, selected):
|
||
self._is_selected = selected
|
||
|
||
|
||
def get_toolbar(self):
|
||
return self._toolbar
|
||
|
||
|
||
def get_signal_handlers(self):
|
||
return {
|
||
'on_library-toolbar-search_toggled': self.on_search_toggled,
|
||
'on_library-toolbar-select_toggled': self.on_select_toggled,
|
||
'on_library-toolbar-scale_change_value': self.on_grid_scale_change,
|
||
'on_library-toolbar-scale_button_release_event': self.on_grid_scale_changed,
|
||
'on_library-toolbar-update_clicked': self.on_update_clicked,
|
||
'on_library-toolbar-sort-toggled': self.on_sort_toggled,
|
||
'on_library-toolbar-sort-order_toggled': self.on_sort_order_toggled,
|
||
'on_library-filter-bar_notify': self.on_filter_bar_notify,
|
||
'on_library-filter_search_changed': self.on_filter_entry_changed,
|
||
'on_library-iconview_item_activated': self.on_library_grid_clicked,
|
||
'on_library-iconview_selection_changed': self.on_library_grid_selection_changed,
|
||
'on_library-standalone-scroll_size_allocate': self.on_standalone_scroll_size_allocate,
|
||
'on_headerbar-library-standalone-close_clicked': self.on_standalone_close_clicked,
|
||
'on_library-iconview_size_allocate': self.on_resize
|
||
}
|
||
|
||
|
||
def on_resize(self, widget, event):
|
||
new_allocation = (widget.get_allocation().width, widget.get_allocation().height)
|
||
if new_allocation == self._allocation:
|
||
return
|
||
self._allocation = new_allocation
|
||
self._grid_scale.clear_marks()
|
||
width = widget.get_allocation().width
|
||
|
||
lower = int(self._grid_adjustment.get_lower())
|
||
upper = int(self._grid_adjustment.get_upper())
|
||
countMin = max(int(width / upper), 1)
|
||
countMax = max(int(width / lower), 1)
|
||
for index in range(countMin, countMax):
|
||
pixel = int(width / index)
|
||
pixel = pixel - (2 * int(pixel / 100))
|
||
self._grid_scale.add_mark(
|
||
pixel,
|
||
Gtk.PositionType.BOTTOM,
|
||
None
|
||
)
|
||
|
||
|
||
def on_search_toggled(self, widget):
|
||
self._filter_bar.set_search_mode(widget.get_active())
|
||
|
||
|
||
def on_select_toggled(self, widget):
|
||
if widget.get_active():
|
||
self._actionbar_revealer.set_reveal_child(True)
|
||
self._library_grid.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
||
self._library_grid.get_style_context().add_class(Window._CSS_SELECTION)
|
||
else:
|
||
self._actionbar_revealer.set_reveal_child(False)
|
||
self._library_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||
self._library_grid.get_style_context().remove_class(Window._CSS_SELECTION)
|
||
|
||
|
||
def on_grid_scale_change(self, widget, scroll, value):
|
||
size = math.floor(value)
|
||
range = self._grid_scale.get_adjustment()
|
||
if size < range.get_lower() or size > range.get_upper():
|
||
return
|
||
self._item_size = size
|
||
GObject.idle_add(self._library_grid.set_item_padding, size / 100)
|
||
GObject.idle_add(self._set_widget_grid_size, self._library_grid, size, True)
|
||
|
||
|
||
def on_grid_scale_changed(self, widget, event):
|
||
size = round(self._grid_scale.get_value())
|
||
range = self._grid_scale.get_adjustment()
|
||
if size < range.get_lower() or size > range.get_upper():
|
||
return False
|
||
self.emit('item-size-changed', size)
|
||
self._toolbar_popover.popdown()
|
||
self._redraw()
|
||
return False
|
||
|
||
|
||
def on_update_clicked(self, widget):
|
||
self.emit('update')
|
||
|
||
|
||
def on_sort_toggled(self, widget):
|
||
if widget.get_active():
|
||
sort = [key for key, value in self._toolbar_sort_buttons.items() if value is widget][0]
|
||
self._change_sort(sort)
|
||
|
||
|
||
def on_sort_order_toggled(self, button):
|
||
if button.get_active():
|
||
sort_type = Gtk.SortType.DESCENDING
|
||
else:
|
||
sort_type = Gtk.SortType.ASCENDING
|
||
self._sort_type = sort_type
|
||
self._library_grid_model.set_sort_column_id(2, sort_type)
|
||
self.emit('sort-type-changed', sort_type)
|
||
|
||
|
||
def on_filter_bar_notify(self, widget, value):
|
||
if self._toolbar_search_bar.get_active() is not self._filter_bar.get_search_mode():
|
||
self._toolbar_search_bar.set_active(self._filter_bar.get_search_mode())
|
||
|
||
|
||
def on_filter_entry_changed(self, widget):
|
||
self._filter_string = self._filter_entry.get_text()
|
||
GObject.idle_add(self._library_grid_filter.refilter)
|
||
|
||
|
||
def on_library_grid_clicked(self, widget, path):
|
||
# Get selected album
|
||
path = self._library_grid_filter.convert_path_to_child_path(path)
|
||
iter = self._library_grid_model.get_iter(path)
|
||
id = self._library_grid_model.get_value(iter, 2)
|
||
album = self._albums[id]
|
||
self._selected_albums = [album]
|
||
|
||
# Show standalone album
|
||
if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE:
|
||
# Set labels
|
||
self._standalone_title.set_text(album.get_title())
|
||
self._standalone_artist.set_text(", ".join(album.get_albumartists()))
|
||
|
||
# Show panel
|
||
self._open_standalone()
|
||
|
||
# Load cover
|
||
threading.Thread(target=self._show_standalone_image, args=(album,)).start()
|
||
|
||
|
||
def on_library_grid_selection_changed(self, widget):
|
||
self._selected_albums = []
|
||
for path in widget.get_selected_items():
|
||
path = self._library_grid_filter.convert_path_to_child_path(path)
|
||
iter = self._library_grid_model.get_iter(path)
|
||
id = self._library_grid_model.get_value(iter, 2)
|
||
self._selected_albums.insert(0, self._albums[id])
|
||
|
||
|
||
def on_filter_visible(self, model, iter, data):
|
||
id = model.get_value(iter, 2)
|
||
if not id in self._albums.keys():
|
||
return
|
||
album = self._albums[id]
|
||
return album.filter(self._filter_string)
|
||
|
||
|
||
def on_selection_cancel_clicked(self, widget):
|
||
self._select_button.set_active(False)
|
||
|
||
|
||
def on_selection_add_clicked(self, widget):
|
||
ids = [album.get_id() for album in self._selected_albums]
|
||
self.emit('queue-multiple', ids)
|
||
self._select_button.set_active(False)
|
||
|
||
|
||
def on_standalone_scroll_size_allocate(self, widget, allocation):
|
||
self._resize_standalone_image()
|
||
|
||
|
||
def on_standalone_play_clicked(self, widget):
|
||
self.emit('play', self._selected_albums[0].get_id())
|
||
self._close_standalone()
|
||
|
||
|
||
def on_standalone_queue_clicked(self, widget):
|
||
self.emit('queue', self._selected_albums[0].get_id())
|
||
self._close_standalone()
|
||
|
||
|
||
def on_standalone_close_clicked(self, widget):
|
||
self._close_standalone()
|
||
|
||
|
||
def set_item_size(self, item_size):
|
||
if self._item_size != item_size:
|
||
self._item_size = item_size
|
||
self._grid_scale.set_value(item_size)
|
||
self._redraw()
|
||
|
||
|
||
def get_item_size(self):
|
||
return self._item_size
|
||
|
||
|
||
def set_sort_order(self, sort):
|
||
if self._sort_order != sort:
|
||
button = self._toolbar_sort_buttons[sort]
|
||
if button and not button.get_active():
|
||
button.set_active(True)
|
||
self._sort_order = sort
|
||
self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
|
||
|
||
|
||
def get_sort_order(self):
|
||
return self._sort_order
|
||
|
||
|
||
def set_sort_type(self, sort_type):
|
||
if self._sort_type != sort_type:
|
||
if sort_type:
|
||
sort_type_gtk = Gtk.SortType.DESCENDING
|
||
self._toolbar_sort_order_button.set_active(True)
|
||
else:
|
||
sort_type_gtk = Gtk.SortType.ASCENDING
|
||
self._toolbar_sort_order_button.set_active(False)
|
||
if self._sort_type != sort_type_gtk:
|
||
self._sort_type = sort_type_gtk
|
||
self._library_grid_model.set_sort_column_id(2, sort_type)
|
||
|
||
|
||
def get_sort_type(self):
|
||
return (self._sort_type != Gtk.SortType.ASCENDING)
|
||
|
||
|
||
def init_albums(self):
|
||
self._progress_bar.set_text(gettext.gettext("Loading albums"))
|
||
|
||
|
||
def load_albums(self):
|
||
self._progress_bar.pulse()
|
||
|
||
|
||
def set_albums(self, host, albums):
|
||
self._host = host
|
||
self._library_stop.set()
|
||
threading.Thread(target=self._set_albums, args=(host, albums, self._item_size,)).start()
|
||
|
||
|
||
def compare_albums(self, model, row1, row2, criterion):
|
||
id1 = model.get_value(row1, 2)
|
||
id2 = model.get_value(row2, 2)
|
||
|
||
if not id1 or not id2:
|
||
return
|
||
return client.MCGAlbum.compare(self._albums[id1], self._albums[id2], criterion)
|
||
|
||
|
||
def stop_threads(self):
|
||
self._library_stop.set()
|
||
|
||
|
||
def _change_sort(self, sort):
|
||
self._sort_order = sort
|
||
self._library_grid_model.set_sort_func(2, self.compare_albums, sort)
|
||
self.emit('sort-order-changed', sort)
|
||
|
||
|
||
def _set_albums(self, host, albums, size):
|
||
if not self._is_selected and albums != self._albums:
|
||
GObject.idle_add(
|
||
self.get().get_parent().child_set_property,
|
||
self.get(),
|
||
'needs-attention',
|
||
True
|
||
)
|
||
self._library_lock.acquire()
|
||
self._library_stop.clear()
|
||
self._albums = albums
|
||
GObject.idle_add(self._stack.set_visible_child, self._progress_box)
|
||
GObject.idle_add(self._progress_bar.set_fraction, 0.0)
|
||
GObject.idle_add(self._library_grid.set_item_padding, size / 100)
|
||
self._library_grid.set_model(None)
|
||
self._library_grid.freeze_child_notify()
|
||
self._library_grid_model.clear()
|
||
|
||
i = 0
|
||
n = len(albums)
|
||
cache = client.MCGCache(host, size)
|
||
self._grid_pixbufs.clear()
|
||
for album_id in albums.keys():
|
||
album = albums[album_id]
|
||
pixbuf = None
|
||
try:
|
||
pixbuf = Utils.load_thumbnail(cache, album, size)
|
||
except Exception as e:
|
||
print(e)
|
||
if pixbuf is None:
|
||
pixbuf = self._icon_theme.load_icon(
|
||
Window.STOCK_ICON_DEFAULT,
|
||
self._item_size,
|
||
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
|
||
)
|
||
if pixbuf is not None:
|
||
self._grid_pixbufs[album.get_id()] = pixbuf
|
||
self._library_grid_model.append([
|
||
pixbuf,
|
||
GObject.markup_escape_text("\n".join([
|
||
album.get_title(),
|
||
', '.join(album.get_dates()),
|
||
Utils.create_artists_label(album),
|
||
Utils.create_length_label(album)
|
||
])),
|
||
album_id
|
||
])
|
||
|
||
i += 1
|
||
GObject.idle_add(self._progress_bar.set_fraction, i/n)
|
||
GObject.idle_add(self._progress_bar.set_text, gettext.gettext("Loading images"))
|
||
if self._library_stop.is_set():
|
||
self._library_lock.release()
|
||
return
|
||
|
||
self._library_grid.set_model(self._library_grid_filter)
|
||
self._library_grid.thaw_child_notify()
|
||
self._library_grid.set_item_width(-1)
|
||
self._library_lock.release()
|
||
self._stack.set_visible_child(self._scroll)
|
||
|
||
|
||
def _set_widget_grid_size(self, grid_widget, size, vertical):
|
||
self._library_stop.set()
|
||
threading.Thread(target=self._set_widget_grid_size_thread, args=(grid_widget, size, vertical,)).start()
|
||
|
||
|
||
def _set_widget_grid_size_thread(self, grid_widget, size, vertical):
|
||
self._library_lock.acquire()
|
||
self._library_stop.clear()
|
||
grid_filter = grid_widget.get_model()
|
||
grid_model = grid_filter.get_model()
|
||
|
||
# get old_range
|
||
grid_widget_id = id(grid_widget)
|
||
if grid_widget_id not in self._old_ranges or self._old_ranges[grid_widget_id] is None:
|
||
self._old_ranges[grid_widget_id] = range(0, len(grid_filter))
|
||
old_range = self._old_ranges[grid_widget_id]
|
||
old_start = len(old_range) > 0 and old_range[0] or 0
|
||
old_end = len(old_range) > 0 and old_range[len(old_range)-1] + 1 or 0
|
||
|
||
# calculate visible range
|
||
w = (grid_widget.get_allocation().width // size) + (vertical and 0 or 1)
|
||
h = (grid_widget.get_allocation().height // size) + (vertical and 1 or 0)
|
||
c = w * h
|
||
vis_range = grid_widget.get_visible_range()
|
||
if vis_range is None:
|
||
self._library_lock.release()
|
||
return
|
||
(vis_start,), (vis_end,) = vis_range
|
||
vis_end = min(vis_start + c, len(grid_filter))
|
||
vis_range = range(vis_start, vis_end)
|
||
|
||
# set pixbuf
|
||
cur_start = min(old_start, vis_start)
|
||
cur_end = max(old_end, vis_end)
|
||
cur_range = range(cur_start, cur_end)
|
||
for index in cur_range:
|
||
iter = grid_filter.convert_iter_to_child_iter(grid_filter[index].iter)
|
||
if index in vis_range:
|
||
album_id = grid_model.get_value(iter, 2)
|
||
pixbuf = self._grid_pixbufs[album_id]
|
||
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST)
|
||
else:
|
||
pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 1, 1)
|
||
grid_model.set_value(iter, 0, pixbuf)
|
||
|
||
if self._library_stop.is_set():
|
||
self._library_lock.release()
|
||
return
|
||
|
||
self._old_ranges[grid_widget_id] = vis_range
|
||
grid_widget.set_item_width(size)
|
||
|
||
self._library_lock.release()
|
||
|
||
|
||
def _redraw(self):
|
||
if self._albums is not None:
|
||
self.set_albums(self._host, self._albums)
|
||
|
||
|
||
def _open_standalone(self):
|
||
self._panel.set_visible_child(self._panel_standalone)
|
||
self._appwindow.set_titlebar(self._headerbar_standalone)
|
||
|
||
|
||
def _close_standalone(self):
|
||
self._panel.set_visible_child(self._panel.get_children()[0])
|
||
self._appwindow.set_titlebar(self._headerbar)
|
||
|
||
|
||
def _show_standalone_image(self, album):
|
||
self._standalone_stack.set_visible_child(self._standalone_spinner)
|
||
self._standalone_spinner.start()
|
||
url = album.get_cover()
|
||
if url:
|
||
# Load image and draw it
|
||
self._standalone_pixbuf = Utils.load_cover(url)
|
||
else:
|
||
# Reset image
|
||
self._standalone_pixbuf = self._get_default_image()
|
||
self._resize_standalone_image()
|
||
self._standalone_stack.set_visible_child(self._standalone_scroll)
|
||
self._standalone_spinner.stop()
|
||
|
||
|
||
def _resize_standalone_image(self):
|
||
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
|
||
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
|
||
"""
|
||
pixbuf = self._standalone_pixbuf
|
||
size = self._standalone_scroll.get_allocation()
|
||
# Check pixelbuffer
|
||
if pixbuf is None:
|
||
return
|
||
|
||
# Skalierungswert für Breite und Höhe ermitteln
|
||
ratioW = float(size.width) / float(pixbuf.get_width())
|
||
ratioH = float(size.height) / float(pixbuf.get_height())
|
||
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
|
||
ratio = min(ratioW, ratioH)
|
||
ratio = min(ratio, 1)
|
||
# Neue Breite und Höhe berechnen
|
||
width = int(math.floor(pixbuf.get_width()*ratio))
|
||
height = int(math.floor(pixbuf.get_height()*ratio))
|
||
if width <= 0 or height <= 0:
|
||
return
|
||
# Pixelpuffer auf Oberfläche zeichnen
|
||
self._standalone_image.set_allocation(self._standalone_scroll.get_allocation())
|
||
self._standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
|
||
self._standalone_image.show()
|
||
|
||
|
||
def _get_default_image(self):
|
||
return self._icon_theme.load_icon(
|
||
Window.STOCK_ICON_DEFAULT,
|
||
512,
|
||
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
|
||
)
|
||
|
||
|
||
|
||
|
||
class StackSwitcher(GObject.GObject):
|
||
__gsignals__ = {
|
||
'stack-switched': (GObject.SIGNAL_RUN_FIRST, None, ())
|
||
}
|
||
|
||
|
||
def __init__(self, builder):
|
||
GObject.GObject.__init__(self)
|
||
|
||
self._temp_button = None
|
||
self._stack_switcher = builder.get_object('header-panelswitcher')
|
||
for child in self._stack_switcher.get_children():
|
||
if type(child) is Gtk.RadioButton:
|
||
child.connect('clicked', self.on_clicked)
|
||
|
||
|
||
def on_clicked(self, widget):
|
||
if not self._temp_button:
|
||
self._temp_button = widget
|
||
else:
|
||
self._temp_button = None
|
||
self.emit('stack-switched')
|
||
|
||
|
||
def get(self):
|
||
return self._stack_switcher
|