#!/usr/bin/env python3 import gi try: import keyring use_keyring = True except ImportError: use_keyring = False use_keyring = False import locale gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gdk, GObject, GLib, Gio from . import client from .shortcutsdialog import ShortcutsDialog from .connectionpanel import ConnectionPanel from .serverpanel import ServerPanel from .coverpanel import CoverPanel from .playlistpanel import PlaylistPanel from .librarypanel import LibraryPanel from .zeroconf import ZeroconfProvider class WindowState(GObject.Object): PROP_WIDTH = 'width' PROP_HEIGHT = 'height' PROP_MAXIMIZED = 'is_maximized' PROP_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): super().__init__() @Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/window.ui') class Window(Adw.ApplicationWindow): __gtype_name__ = 'McgAppWindow' SETTING_HOST = 'host' SETTING_PORT = 'port' SETTING_CONNECTED = 'connected' 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' # Widgets toolbar_view = Gtk.Template.Child() content_stack = Gtk.Template.Child() panel_stack = Gtk.Template.Child() toolbar_stack = Gtk.Template.Child() # Headerbar headerbar = Gtk.Template.Child() headerbar_panel_switcher = Gtk.Template.Child() headerbar_button_connect = Gtk.Template.Child() headerbar_button_playpause = Gtk.Template.Child() headerbar_button_volume = Gtk.Template.Child() # Infobar info_toast = Gtk.Template.Child() def __init__(self, app, title, settings, **kwargs): super().__init__(**kwargs) self.set_application(app) self.set_title(title) self._settings = settings self._panels = [] self._mcg = client.Client() self._state = WindowState() self._setting_volume = False self._headerbar_connection_button_active = True self._headerbar_playpause_button_active = True # Help/Shortcuts dialog self.set_help_overlay(ShortcutsDialog()) # Login screen self._connection_panel = ConnectionPanel() # Server panel self._server_panel = ServerPanel() self._panels.append(self._server_panel) # Cover panel self._cover_panel = CoverPanel() self._panels.append(self._cover_panel) # Playlist panel self._playlist_panel = PlaylistPanel(self._mcg) self._playlist_panel.connect('open-standalone', self.on_panel_open_standalone) self._playlist_panel.connect('close-standalone', self.on_panel_close_standalone) self._panels.append(self._playlist_panel) # Library panel self._library_panel = LibraryPanel(self._mcg) self._library_panel.connect('open-standalone', self.on_panel_open_standalone) self._library_panel.connect('close-standalone', self.on_panel_close_standalone) self._panels.append(self._library_panel) # Stack self.content_stack.add_child(self._connection_panel) self.panel_stack.add_titled_with_icon(self._server_panel, 'server-panel', locale.gettext("Server"), "network-wired-symbolic") self.panel_stack.add_titled_with_icon(self._cover_panel, 'cover-panel', locale.gettext("Cover"), "image-x-generic-symbolic") self.panel_stack.add_titled_with_icon(self._playlist_panel, 'playlist-panel', locale.gettext("Playlist"), "view-list-symbolic") self.panel_stack.add_titled_with_icon(self._library_panel, 'library-panel', locale.gettext("Library"), "emblem-music-symbolic") # Toolbar stack self.toolbar_stack.add_child(self._server_panel.get_toolbar()) self.toolbar_stack.add_child(self._cover_panel.get_toolbar()) self.toolbar_stack.add_child(self._playlist_panel.get_toolbar()) self.toolbar_stack.add_child(self._library_panel.get_toolbar()) # Properties self._set_headerbar_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._playlist_panel.set_item_size( self._settings.get_int(Window.SETTING_ITEM_SIZE)) self._library_panel.set_item_size( self._settings.get_int(Window.SETTING_ITEM_SIZE)) self._library_panel.set_sort_order( self._settings.get_enum(Window.SETTING_SORT_ORDER)) self._library_panel.set_sort_type( self._settings.get_boolean(Window.SETTING_SORT_TYPE)) # Signals self.connect("notify::default-width", self.on_resize) self.connect("notify::default-height", self.on_resize) self.connect("notify::maximized", self.on_maximized) self.connect("notify::fullscreened", self.on_fullscreened) self._connection_panel.connect( 'connection-changed', self.on_connection_panel_connection_changed) self.panel_stack.connect('notify::visible-child', self.on_stack_switched) self._server_panel.connect('change-output-device', self.on_server_panel_output_device_changed) self._cover_panel.connect('toggle-fullscreen', self.on_cover_panel_toggle_fullscreen) self._cover_panel.connect('set-song', self.on_cover_panel_set_song) self._cover_panel.connect('albumart', self.on_cover_panel_albumart) self._playlist_panel.connect('clear-playlist', self.on_playlist_panel_clear_playlist) self._playlist_panel.connect('remove-album', self.on_playlist_panel_remove) self._playlist_panel.connect('remove-multiple-albums', self.on_playlist_panel_remove_multiple) self._playlist_panel.connect('play', self.on_playlist_panel_play) self._playlist_panel.connect('albumart', self.on_playlist_panel_albumart) self._library_panel.connect('update', self.on_library_panel_update) self._library_panel.connect('play', self.on_library_panel_play) self._library_panel.connect('queue', self.on_library_panel_queue) self._library_panel.connect('queue-multiple', self.on_library_panel_queue_multiple) self._library_panel.connect('item-size-changed', self.on_library_panel_item_size_changed) self._library_panel.connect('sort-order-changed', self.on_library_panel_sort_order_changed) self._library_panel.connect('sort-type-changed', self.on_library_panel_sort_type_changed) self._library_panel.connect('albumart', self.on_library_panel_albumart) 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_LOAD_ALBUMART, self.on_mcg_load_albumart) 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.PROP_WIDTH, Gio.SettingsBindFlags.DEFAULT) self._settings.bind(Window.SETTING_WINDOW_HEIGHT, self._state, WindowState.PROP_HEIGHT, Gio.SettingsBindFlags.DEFAULT) self._settings.bind(Window.SETTING_WINDOW_MAXIMIZED, self._state, WindowState.PROP_MAXIMIZED, Gio.SettingsBindFlags.DEFAULT) # Actions self.set_default_size(self._state.width, self._state.height) if self._state.get_property(WindowState.PROP_MAXIMIZED): self.maximize() self.content_stack.set_visible_child(self._connection_panel) 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.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.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.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.add_action(self._panel_action) self._toggle_fullscreen_action = Gio.SimpleAction.new( "toggle-fullscreen", None) self._toggle_fullscreen_action.set_enabled(True) self._toggle_fullscreen_action.connect('activate', self.on_menu_toggle_fullscreen) self.add_action(self._toggle_fullscreen_action) self._search_library_action = Gio.SimpleAction.new( "search-library", None) self._search_library_action.set_enabled(True) self._search_library_action.connect('activate', self.on_menu_search_library) self.add_action(self._search_library_action) # Menu callbacks 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.panel_stack.set_visible_child(self._panels[int( value.get_string())]) def on_menu_toggle_fullscreen(self, action, value): self.panel_stack.set_visible_child(self._cover_panel) if not self._state.get_property(WindowState.PROP_FULLSCREENED): self.fullscreen() else: self.unfullscreen() def on_menu_search_library(self, action, value): self.panel_stack.set_visible_child(self.library_panel_page) self._library_panel.show_search() # Window callbacks def on_resize(self, widget, event): width = self.get_size(Gtk.Orientation.HORIZONTAL) height = self.get_size(Gtk.Orientation.VERTICAL) if width > 0: self._cover_panel.set_width(width) if not self._state.get_property(WindowState.PROP_MAXIMIZED): self._state.set_property(WindowState.PROP_WIDTH, width) self._state.set_property(WindowState.PROP_HEIGHT, height) GObject.idle_add(self._playlist_panel.set_size, width, height) GObject.idle_add(self._library_panel.set_size, width, height) def on_maximized(self, widget, maximized): self._state.set_property(WindowState.PROP_MAXIMIZED, maximized is True) def on_fullscreened(self, widget, fullscreened): self._fullscreen(self.is_fullscreen()) # HeaderBar callbacks @Gtk.Template.Callback() def on_headerbar_connection_state_set(self, widget, state): if self._headerbar_connection_button_active: self._connect() @Gtk.Template.Callback() def on_headerbar_volume_changed(self, widget, value): if not self._setting_volume: self._mcg.set_volume(int(value * 100)) @Gtk.Template.Callback() def on_headerbar_playpause_toggled(self, widget): if self._headerbar_playpause_button_active: self._mcg.playpause() self._mcg.get_status() # Panel callbacks def on_stack_switched(self, widget, prop): self._set_visible_toolbar() self._save_visible_panel() self._set_menu_visible_panel() for panel in self._panels: panel.set_selected(panel == self.panel_stack.get_visible_child()) def on_panel_open_standalone(self, panel): self.toolbar_view.add_top_bar(panel.get_headerbar_standalone()) self.toolbar_view.remove(self.headerbar) def on_panel_close_standalone(self, panel): self.toolbar_view.add_top_bar(self.headerbar) self.toolbar_view.remove(panel.get_headerbar_standalone()) def on_connection_panel_connection_changed(self, widget, host, port, password): 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) 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_playlist_panel_albumart(self, widget, album): self._mcg.get_albumart(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.PROP_FULLSCREENED): self.fullscreen() else: self.unfullscreen() def on_cover_panel_set_song(self, widget, pos, time): self._mcg.seek(pos, time) def on_cover_panel_albumart(self, widget, album): self._mcg.get_albumart(album) 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._playlist_panel.set_item_size(size) self._settings.set_int(Window.SETTING_ITEM_SIZE, self._library_panel.get_item_size()) def on_library_panel_sort_order_changed(self, widget, sort_order): self._settings.set_enum(Window.SETTING_SORT_ORDER, sort_order) def on_library_panel_sort_type_changed(self, widget, sort_type): self._settings.set_boolean(Window.SETTING_SORT_TYPE, sort_type) def on_library_panel_albumart(self, widget, album): self._mcg.get_albumart(album) # 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_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._cover_panel.set_album, album) if ( not album and self._state.get_property(WindowState.PROP_FULLSCREENED) ): self._fullscreen(False) # State if state == 'play': GObject.idle_add(self._set_play) GObject.idle_add(self._cover_panel.set_play, pos, time) self._play_action.set_state(GLib.Variant.new_boolean(True)) elif state == 'pause' or state == 'stop': GObject.idle_add(self._set_pause) GObject.idle_add(self._cover_panel.set_pause) self._play_action.set_state(GLib.Variant.new_boolean(False)) # Volume GObject.idle_add(self._set_volume, volume) # Status self._server_panel.set_status(file, audio, bitrate, error) # Error if error: self._show_error(error) def on_mcg_stats(self, artists, albums, songs, dbplaytime, playtime, uptime): self._server_panel.set_stats(artists, albums, songs, dbplaytime, playtime, uptime) def on_mcg_load_output_devices(self, devices): self._server_panel.set_output_devices(devices) def on_mcg_load_playlist(self, playlist): self._playlist_panel.set_playlist(self._connection_panel.get_host(), playlist) def on_mcg_init_albums(self): GObject.idle_add(self._library_panel.init_albums) def on_mcg_pulse_albums(self): GObject.idle_add(self._library_panel.load_albums) def on_mcg_load_albums(self, albums): self._library_panel.set_albums(self._connection_panel.get_host(), albums) def on_mcg_load_albumart(self, album, data): self._cover_panel.set_albumart(album, data) self._playlist_panel.set_albumart(album, data) self._library_panel.set_albumart(album, data) 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.panel_stack.set_visible_child(self._panels[panel_index]) def on_settings_item_size_changed(self, settings, key): size = settings.get_int(key) self._playlist_panel.set_item_size(size) self._library_panel.set_item_size(size) def on_settings_sort_order_changed(self, settings, key): sort_order = settings.get_enum(key) self._library_panel.set_sort_order(sort_order) def on_settings_sort_type_changed(self, settings, key): sort_type = settings.get_boolean(key) self._library_panel.set_sort_type(sort_type) # Private methods def _connect(self): self._connection_panel.set_sensitive(False) self._set_headerbar_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() self._mcg.connect(host, port, password) self._settings.set_boolean(Window.SETTING_CONNECTED, True) def _connect_connected(self): self._headerbar_connected() self._set_headerbar_sensitive(True, False) self.content_stack.set_visible_child(self.panel_stack) self.panel_stack.set_visible_child(self._panels[self._settings.get_int( Window.SETTING_PANEL)]) def _connect_disconnected(self): self._playlist_panel.stop_threads() self._library_panel.stop_threads() self._headerbar_disconnected() self._set_headerbar_sensitive(False, False) self._save_visible_panel() self.content_stack.set_visible_child(self._connection_panel) self._connection_panel.set_sensitive(True) def _fullscreen(self, fullscreened_new): if fullscreened_new != self._state.get_property( WindowState.PROP_FULLSCREENED ): self._state.set_property(WindowState.PROP_FULLSCREENED, fullscreened_new) if self._state.get_property(WindowState.PROP_FULLSCREENED): self.headerbar.hide() self._cover_panel.set_fullscreen(True) self.set_cursor(Gdk.Cursor.new_from_name("none", None)) else: self.headerbar.show() self._cover_panel.set_fullscreen(False) self.set_cursor(Gdk.Cursor.new_from_name("default", None)) def _save_visible_panel(self): panel_index_selected = self._panels.index( self.panel_stack.get_visible_child()) self._settings.set_int(Window.SETTING_PANEL, panel_index_selected) def _set_menu_visible_panel(self): panel_index_selected = self._panels.index( self.panel_stack.get_visible_child()) self._panel_action.set_state( GLib.Variant.new_string(str(panel_index_selected))) def _set_visible_toolbar(self): panel_index_selected = self._panels.index( self.panel_stack.get_visible_child()) toolbar = self._panels[panel_index_selected].get_toolbar() self.toolbar_stack.set_visible_child(toolbar) def _set_play(self): self._headerbar_playpause_button_active = False self.headerbar_button_playpause.set_active(True) self._headerbar_playpause_button_active = True def _set_pause(self): self._headerbar_playpause_button_active = False self.headerbar_button_playpause.set_active(False) self._headerbar_playpause_button_active = True def _set_volume(self, volume): if volume >= 0: self.headerbar_button_volume.set_visible(True) self._setting_volume = True self.headerbar_button_volume.set_value(volume / 100) self._setting_volume = False else: self.headerbar_button_volume.set_visible(False) def _headerbar_connected(self): self._headerbar_connection_button_active = False self.headerbar_button_connect.set_active(True) self.headerbar_button_connect.set_state(True) self._headerbar_connection_button_active = True def _headerbar_disconnected(self): self._headerbar_connection_button_active = False self.headerbar_button_connect.set_active(False) self.headerbar_button_connect.set_state(False) self._headerbar_connection_button_active = True def _set_headerbar_sensitive(self, sensitive, connecting): self.headerbar_button_playpause.set_sensitive(sensitive) self.headerbar_button_volume.set_sensitive(sensitive) self.headerbar_panel_switcher.set_sensitive(sensitive) self.headerbar_button_connect.set_sensitive(not connecting) def _show_error(self, message): self.info_toast.add_toast(Adw.Toast.new(message))