diff --git a/data/de.coderkun.mcg.gresource.xml b/data/de.coderkun.mcg.gresource.xml index 02969fc..bd04296 100644 --- a/data/de.coderkun.mcg.gresource.xml +++ b/data/de.coderkun.mcg.gresource.xml @@ -1,11 +1,22 @@ - gtk.glade gtk.css - gtk.shortcuts.ui - gtk.menu.ui - mcg.svg noise-texture.png + icons/mcg.svg + ui/window.ui + ui/gtk.menu.ui + ui/info-dialog.ui + ui/shortcuts-dialog.ui + ui/connection-panel.ui + ui/album-headerbar.ui + ui/server-toolbar.ui + ui/server-panel.ui + ui/cover-toolbar.ui + ui/cover-panel.ui + ui/playlist-toolbar.ui + ui/playlist-panel.ui + ui/library-toolbar.ui + ui/library-panel.ui diff --git a/data/gtk.glade b/data/gtk.glade deleted file mode 100644 index fd77059..0000000 --- a/data/gtk.glade +++ /dev/null @@ -1,1975 +0,0 @@ - - - - - - True - False - - - True - True - True - - - - True - False - go-previous-symbolic - - - - - - - True - False - vertical - - - True - False - Title - True - - - - - - - False - True - 0 - - - - - True - False - Artist - True - - - False - True - 1 - - - - - - - True - False - - - True - True - True - - - - True - False - go-previous-symbolic - - - - - - - True - False - vertical - - - True - False - Title - True - - - - - - - False - True - 0 - - - - - True - False - Artist - True - - - False - True - 1 - - - - - - - 100 - 1000 - 150 - 1 - 10 - - - False - - - True - False - vertical - - - 350 - True - True - library-scale-adjustment - False - -1 - 0 - 0 - False - - - - - True - True - 0 - - - - - gtk-refresh - True - True - True - none - True - - - - False - True - 1 - - - - - True - False - vertical - - - True - False - vertical - - - False - True - 0 - - - - - True - False - Sort - - - False - True - 1 - - - - - sort by artist - True - True - False - True - library-toolbar-sort-year - - - - False - True - 2 - - - - - sort by title - True - True - False - True - library-toolbar-sort-year - - - - False - True - 3 - - - - - sort by year - True - True - False - True - True - - - - False - True - 4 - - - - - gtk-sort-descending - True - True - False - True - True - True - - - - False - True - 5 - - - - - False - True - 2 - - - - - - - 1024 - 9999 - 6600 - 1 - 100 - - - False - mcg.svg - - - - - headerbar - True - False - True - - - True - True - Connect or disconnect - - - - - 1 - - - - - True - False - - - 2 - - - - - True - True - True - Switch between play and pause - - - - True - False - media-playback-start - - - - - 3 - - - - - True - True - False - True - Adjust the volume - none - vertical - audio-volume-muted - audio-volume-high - audio-volume-low - audio-volume-medium - - - - - - True - True - center - center - none - - - - - True - True - center - center - none - - - - - 4 - - - - - headerbar-connection - True - False - 100 - crossfade - - - True - False - Connect to MPD - - - - - - page1 - page1 - - - - - True - False - panelstack - - - page0 - page0 - 1 - - - - - 4 - - - - - True - False - - - True - False - 6 - end - - - True - True - True - Show the cover in fullscreen mode - win.toggle-fullscreen - - - True - False - view-fullscreen-symbolic - - - - - False - False - 0 - True - - - - - cover - - - - - True - False - 6 - end - - - - - - server - 1 - - - - - True - False - 6 - end - - - True - True - True - Select multiple albums - - - - True - False - object-select-symbolic - - - - - False - False - 0 - True - - - - - True - True - True - Clear the playlist - - - - True - False - edit-clear - - - - - False - False - 1 - True - - - - - playlist - 2 - - - - - True - False - 6 - end - - - True - True - True - Search the library - - - - True - False - system-search-symbolic - - - - - - False - False - 0 - True - - - - - True - True - True - Select multiple albums - - - - True - False - object-select-symbolic - - - - - False - False - 1 - True - - - - - True - True - True - Settings and actions - library-toolbar-popover - - - True - False - open-menu-symbolic - - - - - False - False - 2 - True - - - - - library - 3 - - - - - end - 4 - - - - - - - True - False - - - True - False - 100 - crossfade - - - True - False - vertical - - - True - False - - - 500 - True - False - 5 - True - True - - - True - False - - - True - True - - - - - - - - - True - True - 0 - - - - - True - False - 5 - vertical - - - False - True - end - 1 - - - - - 0 - 0 - 6 - - - - - True - True - localhost - Enter hostname or IP address - - - - 1 - 1 - - - - - True - True - False - Enter password or leave blank - password - - - - 1 - 5 - - - - - True - True - 6600 - server-port-adjustment - 6600 - - - - 1 - 3 - - - - - True - False - start - Host: - - - 1 - 0 - - - - - True - False - start - Port: - - - 1 - 2 - - - - - True - False - start - Password: - - - 1 - 4 - - - - - True - False - 0 - - - - - True - False - 0 - - - - - page1 - page1 - - - - - True - False - - - True - False - - - True - False - server-stack - - - False - True - 0 - - - - - True - False - - - True - False - - - - - - True - False - vertical - - - - - - True - False - 5 - 5 - 5 - 5 - vertical - 10 - - - True - False - dialog-information-symbolic - 6 - - - False - True - 0 - - - - - True - False - 2 - 5 - - - True - False - start - start - File: - - - 0 - 0 - - - - - True - False - start - start - Audio: - - - 0 - 1 - - - - - True - False - start - start - Bitrate: - - - 0 - 2 - - - - - True - False - start - start - Error: - - - 0 - 3 - - - - - True - False - start - <i>none</i> - True - True - True - 0 - - - 1 - 2 - - - - - True - False - start - <i>none</i> - True - True - True - 0 - - - 1 - 1 - - - - - True - False - start - <i>none</i> - True - True - True - 0 - - - 1 - 0 - - - - - True - False - start - <i>none</i> - True - True - True - 0 - - - 1 - 3 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - page0 - Status - - - - - True - False - - - - - - True - False - vertical - - - - - - True - False - vertical - 10 - - - True - False - starred-symbolic - 6 - - - False - True - 0 - - - - - True - False - 2 - 5 - - - True - False - end - right - True - - - 0 - 0 - - - - - True - False - end - right - - - 0 - 1 - - - - - True - False - end - right - - - 0 - 2 - - - - - True - False - start - Albums - - - 1 - 1 - - - - - True - False - start - Songs - - - 1 - 2 - - - - - True - False - start - Artists - - - 1 - 0 - - - - - True - False - end - right - - - 0 - 3 - - - - - True - False - start - Seconds - - - 1 - 3 - - - - - True - False - end - right - - - 0 - 6 - - - - - True - False - end - right - - - 0 - 5 - - - - - True - False - - - 0 - 4 - 2 - - - - - True - False - start - Seconds played - - - 1 - 5 - - - - - True - False - Seconds running - - - 1 - 6 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - page1 - Statistics - 1 - - - - - True - False - - - - - - True - False - vertical - - - - - - True - False - vertical - 10 - - - True - False - audio-speakers-symbolic - 6 - - - False - True - 0 - - - - - True - False - none - - - - False - True - 1 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - page2 - Audio Devices - 2 - - - - - True - True - 1 - - - - - server - Server - - - - - True - False - - - True - False - - - True - False - - - cover-spinner - - - - - True - True - False - False - - - - True - False - - - True - False - - - - True - False - 0 - - - - - - - - - cover-scroll - 1 - - - - - -1 - - - - - True - False - end - slide-right - - - True - True - start - never - 200 - True - - - True - False - none - - - True - False - start - 5 - 5 - 5 - 5 - vertical - - - True - False - 5 - 5 - 5 - True - - - True - False - start - Album - True - 0 - - - 0 - 0 - - - - - True - False - start - Date - True - 0 - - - 0 - 1 - - - - - True - False - start - Artist - True - 0 - - - 0 - 2 - - - - - - False - True - 10 - 0 - - - - - True - True - vertical - False - 0 - False - - - - - True - True - end - 1 - - - - - - - - - - - - - - cover - Cover - 1 - - - - - True - False - slide-left-right - - - True - False - vertical - - - True - True - - - True - True - 0 - horizontal - 0 - 0 - 1 - 0 - True - - - - - - - - - True - True - 0 - - - - - True - False - slide-up - - - True - False - - - - - False - True - 1 - - - - - page2 - page2 - - - - - True - False - vertical - - - True - False - - - True - False - - - standalone-spinne - - - - - True - True - False - False - - - True - False - - - True - False - gtk-missing-image - 6 - - - - - - - standalone-scroll - 1 - - - - - True - True - 0 - - - - - True - False - - - False - True - 1 - - - - - page1 - page1 - 1 - - - - - playlist - Playlist - 2 - - - - - True - False - slide-left-right - - - True - False - vertical - - - True - True - False - - - - True - True - edit-find-symbolic - False - False - search library - - - - - - False - True - 0 - - - - - - - - True - False - crossfade - - - True - False - vertical - - - - - - True - False - vertical - - - - - - True - False - center - vertical - 10 - - - True - False - 6 - - - False - True - 0 - - - - - 200 - True - False - 0.01 - True - - - False - True - 1 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - page1 - page1 - - - - - True - True - - - True - True - 0 - horizontal - 0 - 0 - 1 - 0 - True - - - - - - - - - - page0 - page0 - 1 - - - - - True - True - 2 - - - - - True - False - none - - - True - False - - - - - False - True - 3 - - - - - page0 - page0 - - - - - True - False - vertical - - - True - False - crossfade - - - True - False - - - standalone-spinne - - - - - True - True - False - False - - - - True - False - - - True - False - gtk-missing-image - 6 - - - - - - - standalone-scroll - 1 - - - - - True - True - 0 - - - - - True - False - - - False - True - 1 - - - - - page1 - page1 - 1 - - - - - library - Library - 3 - - - - - page0 - page0 - 1 - - - - - -1 - - - - - True - False - start - - - False - start - 10 - 10 - vertical - top - True - - - - - False - - - False - False - 0 - - - - - False - - - True - False - - - False - True - 0 - - - - - False - False - 0 - - - - - - - - - - - - False - False - True - center - dialog - center - appwindow - CoverGrid - 2.1.2 - CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks. - http://www.suruatoel.xyz/codes/mcg - mcg.svg - gpl-3-0 - - - - - - False - vertical - 2 - - - False - end - - - - - - - - - False - False - 0 - - - - - - - - - diff --git a/data/mcg.svg b/data/icons/mcg.svg similarity index 68% rename from data/mcg.svg rename to data/icons/mcg.svg index ae0a7c5..1cebef7 100644 --- a/data/mcg.svg +++ b/data/icons/mcg.svg @@ -1,6 +1,4 @@ - - + version="1.1" + viewBox="0 0 67.733333 67.733335" + height="256" + width="256"> CoverGrid (mcg) + style="color-interpolation-filters:sRGB"> + flood-color="rgb(255,255,255)" + flood-opacity="0.498039" /> + operator="in" + in2="SourceGraphic" + in="flood" /> + stdDeviation="7" + in="composite1" /> + dy="6" + dx="6" /> + operator="over" + in2="offset" + in="SourceGraphic" /> + inkscape:showpageshadow="false" + showborder="true" + inkscape:pagecheckerboard="true" + units="px" + inkscape:window-maximized="1" + inkscape:window-y="0" + inkscape:window-x="0" + inkscape:window-height="1030" + inkscape:window-width="1916" + inkscape:guide-bbox="true" + showguides="true" + showgrid="false" + inkscape:current-layer="layer1" + inkscape:document-units="mm" + inkscape:cy="490.36801" + inkscape:cx="311.30244" + inkscape:zoom="0.7" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base"> + orientation="0,1" + position="319.76785,-21.166667" /> @@ -120,94 +119,95 @@ + inkscape:groupmode="layer" + inkscape:label="Ebene 1"> + transform="matrix(0.253073,0,0,0.25346533,-0.4687123,18.990219)" + id="g4504"> - + - - - - - + + + + + - + - + + width="87.841667" + height="87.841667" + x="1.8520833" + y="117.61248" /> + inkscape:rounded="0" + inkscape:flatsided="true" + sodipodi:arg2="1.051523" + sodipodi:arg1="0.0043254268" + sodipodi:r2="97.404572" + sodipodi:r1="102.49749" + sodipodi:cy="136.27939" + sodipodi:cx="102.3238" + sodipodi:sides="3" + id="path4661" + style="opacity:0.58;fill:#000000;fill-opacity:1;stroke-width:0.980952;filter:url(#filter4516)" + sodipodi:type="star" /> diff --git a/data/mcg.png b/data/mcg.png new file mode 100644 index 0000000..e992960 Binary files /dev/null and b/data/mcg.png differ diff --git a/data/ui/album-headerbar.ui b/data/ui/album-headerbar.ui new file mode 100644 index 0000000..a384338 --- /dev/null +++ b/data/ui/album-headerbar.ui @@ -0,0 +1,61 @@ + + + + + + diff --git a/data/ui/connection-panel.ui b/data/ui/connection-panel.ui new file mode 100644 index 0000000..7f8000e --- /dev/null +++ b/data/ui/connection-panel.ui @@ -0,0 +1,163 @@ + + + + + + 1024 + 9999 + 6600 + 1 + 100 + + + diff --git a/data/ui/cover-panel.ui b/data/ui/cover-panel.ui new file mode 100644 index 0000000..aca3b61 --- /dev/null +++ b/data/ui/cover-panel.ui @@ -0,0 +1,180 @@ + + + + + + diff --git a/data/ui/cover-toolbar.ui b/data/ui/cover-toolbar.ui new file mode 100644 index 0000000..338cee3 --- /dev/null +++ b/data/ui/cover-toolbar.ui @@ -0,0 +1,33 @@ + + + + + + diff --git a/data/gtk.menu.ui b/data/ui/gtk.menu.ui similarity index 97% rename from data/gtk.menu.ui rename to data/ui/gtk.menu.ui index e12fb59..a71ccb6 100644 --- a/data/gtk.menu.ui +++ b/data/ui/gtk.menu.ui @@ -51,7 +51,7 @@
- app.shortcuts + win.show-help-overlay Keyboard Shortcuts <Primary>k diff --git a/data/ui/info-dialog.ui b/data/ui/info-dialog.ui new file mode 100644 index 0000000..be9a804 --- /dev/null +++ b/data/ui/info-dialog.ui @@ -0,0 +1,49 @@ + + + + + + diff --git a/data/ui/library-panel.ui b/data/ui/library-panel.ui new file mode 100644 index 0000000..de06df0 --- /dev/null +++ b/data/ui/library-panel.ui @@ -0,0 +1,303 @@ + + + + + + diff --git a/data/ui/library-toolbar.ui b/data/ui/library-toolbar.ui new file mode 100644 index 0000000..dc63149 --- /dev/null +++ b/data/ui/library-toolbar.ui @@ -0,0 +1,232 @@ + + + + + + 100 + 1000 + 150 + 1 + 10 + + + False + + + True + False + vertical + + + 350 + True + True + grid_adjustment + False + -1 + 0 + 0 + False + + + + + True + True + 0 + + + + + gtk-refresh + True + True + True + none + True + + + + False + True + 1 + + + + + True + False + vertical + + + True + False + vertical + + + False + True + 0 + + + + + True + False + Sort + + + False + True + 1 + + + + + sort by artist + True + True + False + True + sort_year + + + + False + True + 2 + + + + + sort by title + True + True + False + True + sort_year + + + + False + True + 3 + + + + + sort by year + True + True + False + True + True + + + + False + True + 4 + + + + + gtk-sort-descending + True + True + False + True + True + True + + + + False + True + 5 + + + + + False + True + 2 + + + + + + + diff --git a/data/ui/playlist-panel.ui b/data/ui/playlist-panel.ui new file mode 100644 index 0000000..a2a31ce --- /dev/null +++ b/data/ui/playlist-panel.ui @@ -0,0 +1,194 @@ + + + + + + diff --git a/data/ui/playlist-toolbar.ui b/data/ui/playlist-toolbar.ui new file mode 100644 index 0000000..c061395 --- /dev/null +++ b/data/ui/playlist-toolbar.ui @@ -0,0 +1,55 @@ + + + + + + diff --git a/data/ui/server-panel.ui b/data/ui/server-panel.ui new file mode 100644 index 0000000..568648d --- /dev/null +++ b/data/ui/server-panel.ui @@ -0,0 +1,512 @@ + + + + + + diff --git a/data/ui/server-toolbar.ui b/data/ui/server-toolbar.ui new file mode 100644 index 0000000..c7561c1 --- /dev/null +++ b/data/ui/server-toolbar.ui @@ -0,0 +1,14 @@ + + + + + + diff --git a/data/gtk.shortcuts.ui b/data/ui/shortcuts-dialog.ui similarity index 98% rename from data/gtk.shortcuts.ui rename to data/ui/shortcuts-dialog.ui index 048cc3b..97dbc66 100644 --- a/data/gtk.shortcuts.ui +++ b/data/ui/shortcuts-dialog.ui @@ -1,7 +1,7 @@ - + diff --git a/data/ui/window.ui b/data/ui/window.ui new file mode 100644 index 0000000..0acc759 --- /dev/null +++ b/data/ui/window.ui @@ -0,0 +1,233 @@ + + + + + + diff --git a/mcg/albumheaderbar.py b/mcg/albumheaderbar.py new file mode 100644 index 0000000..5927a3f --- /dev/null +++ b/mcg/albumheaderbar.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk, GObject + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/album-headerbar.ui') +class AlbumHeaderbar(Gtk.HeaderBar): + __gtype_name__ = 'McgAlbumHeaderbar' + __gsignals__ = { + 'close': (GObject.SIGNAL_RUN_FIRST, None, ()) + } + + # Widgets + standalone_title = Gtk.Template.Child() + standalone_artist = Gtk.Template.Child() + + + def __init__(self): + super().__init__() + + + @Gtk.Template.Callback() + def on_close_clicked(self, widget): + self.emit('close') + + + def set_album(self, album): + self.standalone_title.set_text(album.get_title()) + self.standalone_artist.set_text(", ".join(album.get_albumartists())) diff --git a/mcg/application.py b/mcg/application.py index 0c47831..f458fa1 100644 --- a/mcg/application.py +++ b/mcg/application.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 -import gi -gi.require_version('Gtk', '3.0') -import gettext import logging import urllib +import gi +gi.require_version('Gtk', '3.0') from gi.repository import Gio, Gtk, Gdk, GLib -from mcg import Environment -from mcg import widgets +from mcg.window import Window +from mcg.infodialog import InfoDialog @@ -32,7 +31,6 @@ class Application(Gtk.Application): def __init__(self): Gtk.Application.__init__(self, application_id=Application.ID, flags=Gio.ApplicationFlags.FLAGS_NONE) self._window = None - self._shortcuts_window = None self._info_dialog = None self._verbosity = logging.WARNING self.add_main_option_entries([ @@ -53,12 +51,9 @@ class Application(Gtk.Application): def do_startup(self): Gtk.Application.do_startup(self) self._setup_logging() - self._load_resource() self._load_settings() self._set_default_settings() self._load_css() - self._setup_locale() - self._load_ui() self._setup_actions() self._load_appmenu() @@ -66,22 +61,15 @@ class Application(Gtk.Application): def do_activate(self): Gtk.Application.do_activate(self) if not self._window: - self._window = widgets.Window(self, self._builder, Application.TITLE, self._settings) + self._window = Window(self, Application.TITLE, self._settings) self._window.present() - def on_menu_shortcuts(self, action, value): - builder = Gtk.Builder() - builder.set_translation_domain(Application.DOMAIN) - builder.add_from_resource(self._get_resource_path('gtk.shortcuts.ui')) - shortcuts_dialog = widgets.ShortcutsDialog(builder, self._window) - shortcuts_dialog.present() - - def on_menu_info(self, action, value): if not self._info_dialog: - self._info_dialog = widgets.InfoDialog(self._builder) + self._info_dialog = InfoDialog() self._info_dialog.run() + self._info_dialog.hide() def on_menu_quit(self, action, value): @@ -95,13 +83,6 @@ class Application(Gtk.Application): ) - def _load_resource(self): - self._resource = Gio.resource_load( - Environment.get_data(Application.ID + '.gresource') - ) - Gio.Resource._register(self._resource) - - def _load_settings(self): self._settings = Gio.Settings.new(Application.ID) @@ -121,23 +102,7 @@ class Application(Gtk.Application): ) - def _setup_locale(self): - relpath = Environment.get_locale() - gettext.bindtextdomain(Application.DOMAIN, relpath) - gettext.textdomain(Application.DOMAIN) - - - def _load_ui(self): - # Create builder to load UI - self._builder = Gtk.Builder() - self._builder.set_translation_domain(Application.DOMAIN) - self._builder.add_from_resource(self._get_resource_path('gtk.glade')) - - def _setup_actions(self): - action = Gio.SimpleAction.new("shortcuts", None) - action.connect('activate', self.on_menu_shortcuts) - self.add_action(action) action = Gio.SimpleAction.new("info", None) action.connect('activate', self.on_menu_info) self.add_action(action) @@ -149,7 +114,7 @@ class Application(Gtk.Application): def _load_appmenu(self): builder = Gtk.Builder() builder.set_translation_domain(Application.DOMAIN) - builder.add_from_resource(self._get_resource_path('gtk.menu.ui')) + builder.add_from_resource(self._get_resource_path('ui/gtk.menu.ui')) self.set_app_menu(builder.get_object('app-menu')) diff --git a/mcg/connectionpanel.py b/mcg/connectionpanel.py new file mode 100644 index 0000000..2139d01 --- /dev/null +++ b/mcg/connectionpanel.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk, GObject + +from mcg.zeroconf import ZeroconfProvider + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/connection-panel.ui') +class ConnectionPanel(Gtk.Box): + __gtype_name__ = 'McgConnectionPanel' + __gsignals__ = { + 'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str)) + } + + # Widgets + zeroconf_list = Gtk.Template.Child() + host_entry = Gtk.Template.Child() + port_spinner = Gtk.Template.Child() + password_entry = Gtk.Template.Child() + + + def __init__(self): + super().__init__() + self._services = Gtk.ListStore(str, str, int) + self._profile = None + + # Zeroconf + self.zeroconf_list.set_model(self._services) + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Zeroconf", renderer, text=0) + self.zeroconf_list.append_column(column) + + # Zeroconf provider + self._zeroconf_provider = ZeroconfProvider() + self._zeroconf_provider.connect_signal(ZeroconfProvider.SIGNAL_SERVICE_NEW, self.on_new_service) + + + def on_new_service(self, service): + name, host, port = service + self._services.append([name, host, port]) + + + @Gtk.Template.Callback() + 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]) + + + @Gtk.Template.Callback() + def on_zeroconf_list_outfocused(self, widget, event): + self.zeroconf_list.get_selection().unselect_all() + + + @Gtk.Template.Callback() + def on_host_entry_outfocused(self, widget, event): + self._call_back() + + + @Gtk.Template.Callback() + def on_port_spinner_value_changed(self, widget): + self._call_back() + + + @Gtk.Template.Callback() + def on_password_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 _call_back(self): + self.emit('connection-changed', self.get_host(), self.get_port(), self.get_password(),) diff --git a/mcg/coverpanel.py b/mcg/coverpanel.py new file mode 100644 index 0000000..14da478 --- /dev/null +++ b/mcg/coverpanel.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') +import logging +import math + +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf + +from mcg.utils import Utils + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/cover-toolbar.ui') +class CoverToolbar(Gtk.ButtonBox): + __gtype_name__ = 'McgCoverToolbar' + __gsignals__ = { + 'fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ()) + } + + # Widgets + fullscreen_button = Gtk.Template.Child() + + + def __init__(self): + super().__init__() + + + def set_fullscreen_sensitive(self, sensitive): + self.fullscreen_button.set_sensitive(sensitive) + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/cover-panel.ui') +class CoverPanel(Gtk.Overlay): + __gtype_name__ = 'McgCoverPanel' + __gsignals__ = { + 'toggle-fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'set-song': (GObject.SIGNAL_RUN_FIRST, None, (int, int,)), + 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) + } + + # Widgets + # Cover + cover_stack = Gtk.Template.Child() + cover_spinner = Gtk.Template.Child() + cover_scroll = Gtk.Template.Child() + cover_box = Gtk.Template.Child() + cover_image = Gtk.Template.Child() + # Album Infos + cover_info_scroll = Gtk.Template.Child() + info_revealer = Gtk.Template.Child() + album_title_label = Gtk.Template.Child() + album_date_label = Gtk.Template.Child() + album_artist_label = Gtk.Template.Child() + # Songs + songs_scale = Gtk.Template.Child() + + + + def __init__(self): + super().__init__() + + self._current_album = None + self._current_cover_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 + self._cover_pixbuf = self._get_default_image() + + # Widgets + self._toolbar = CoverToolbar() + self.cover_stack.set_visible_child(self.cover_scroll) + + # Initial actions + GObject.idle_add(self._enable_tracklist) + + + def get_toolbar(self): + return self._toolbar + + + def set_selected(self, selected): + self._is_selected = selected + + + @Gtk.Template.Callback() + def on_cover_box_pressed(self, widget, event): + if self._current_album and event.type == Gdk.EventType._2BUTTON_PRESS: + self.emit('toggle-fullscreen') + + + @Gtk.Template.Callback() + 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) + + + @Gtk.Template.Callback() + def on_songs_start_change(self, widget, event): + if self._timer: + GObject.source_remove(self._timer) + self._timer = None + + + @Gtk.Template.Callback() + 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) + + # Load cover + if album != self._current_cover_album: + self.cover_stack.set_visible_child(self.cover_spinner) + self.cover_spinner.start() + self.emit('albumart', album.get_id() if album else None) + + # Set current album + self._current_album = album + self._enable_tracklist() + self._toolbar.set_fullscreen_sensitive(self._current_album is not None) + + + 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) + 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) + + + def set_albumart(self, album, data): + if album == self._current_album: + if data: + # Load image and draw it + try: + self._cover_pixbuf = Utils.load_pixbuf(data) + except Exception as e: + self._logger.exception("Failed to set albumart") + self._cover_pixbuf = self._get_default_image() + else: + # Reset image + self._cover_pixbuf = self._get_default_image() + self._current_size = None + # Show image + self._resize_image() + self.cover_stack.set_visible_child(self.cover_scroll) + self.cover_spinner.stop() + self._current_cover_album = album + + + 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( + Utils.STOCK_ICON_DEFAULT, + 512, + Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE + ) diff --git a/mcg/infodialog.py b/mcg/infodialog.py new file mode 100644 index 0000000..5fa3e81 --- /dev/null +++ b/mcg/infodialog.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') +import logging + +from gi.repository import Gtk, GObject, GdkPixbuf + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/info-dialog.ui') +class InfoDialog(Gtk.AboutDialog): + __gtype_name__ = 'McgInfoDialog' + + + def __init__(self): + super().__init__() + + self._logger = logging.getLogger(__name__) diff --git a/mcg/librarypanel.py b/mcg/librarypanel.py new file mode 100644 index 0000000..eccb846 --- /dev/null +++ b/mcg/librarypanel.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') +import gettext +import logging +import math +import threading + +from gi.repository import Gtk, GObject, GdkPixbuf + +from mcg import client +from mcg.albumheaderbar import AlbumHeaderbar +from mcg.utils import SortOrder +from mcg.utils import Utils + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/library-toolbar.ui') +class LibraryToolbar(Gtk.ButtonBox): + __gtype_name__ = 'McgLibraryToolbar' + __gsignals__ = { + 'select': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), + 'toggle-search': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), + 'update': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'start-scale': (GObject.SIGNAL_RUN_FIRST, None, (int,)), + 'end-scale': (GObject.SIGNAL_RUN_FIRST, None, (int,)), + 'sort': (GObject.SIGNAL_RUN_FIRST, None, (int,)), + 'sort-type': (GObject.SIGNAL_RUN_FIRST, None, (Gtk.SortType,)) + } + + # Widgets + select_button = Gtk.Template.Child() + toolbar_search_bar = Gtk.Template.Child() + toolbar_popover = Gtk.Template.Child() + toolbar_sort_order_button = Gtk.Template.Child() + sort_artist = Gtk.Template.Child() + sort_title = Gtk.Template.Child() + sort_year = Gtk.Template.Child() + grid_scale = Gtk.Template.Child() + + + def __init__(self, item_size): + super().__init__() + + # Toolbar menu + self.grid_scale.set_value(item_size) + self._toolbar_sort_buttons = { + SortOrder.ARTIST: self.sort_artist, + SortOrder.TITLE: self.sort_title, + SortOrder.YEAR: self.sort_year + } + + + @Gtk.Template.Callback() + def on_select_toggled(self, widget): + self.emit('select', widget.get_active()) + + + @Gtk.Template.Callback() + def on_search_toggled(self, widget): + self.emit('toggle-search', widget.get_active()) + + + @Gtk.Template.Callback() + def on_update_clicked(self, widget): + self.emit('update') + + + @Gtk.Template.Callback() + def on_grid_scale_change(self, widget, scroll, value): + self.emit('start-scale', value) + + + @Gtk.Template.Callback() + def on_grid_scale_changed(self, widget, event): + self.emit('end-scale', self.grid_scale.get_value()) + self.toolbar_popover.popdown() + + + @Gtk.Template.Callback() + 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.emit('sort', sort) + + + @Gtk.Template.Callback() + def on_sort_order_toggled(self, button): + if button.get_active(): + sort_type = Gtk.SortType.DESCENDING + else: + sort_type = Gtk.SortType.ASCENDING + self.emit('sort-type', sort_type) + + + def get_grid_scale(self): + return self.grid_scale + + + def is_search_active(self): + return self.toolbar_search_bar.get_active() + + + def set_search_active(self, active): + self.toolbar_search_bar.set_active(active) + + + def exit_selection(self): + self.select_button.set_active(False) + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/library-panel.ui') +class LibraryPanel(Gtk.Stack): + __gtype_name__ = 'McgLibraryPanel' + __gsignals__ = { + 'open-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'close-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), + '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,)), + 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) + } + + + # Widgets + panel_standalone = Gtk.Template.Child() + actionbar_revealer = Gtk.Template.Child() + # Filter/search bar + filter_bar = Gtk.Template.Child() + filter_entry = Gtk.Template.Child() + # Progress Bar + stack = Gtk.Template.Child() + progress_box = Gtk.Template.Child() + progress_image = Gtk.Template.Child() + progress_bar = Gtk.Template.Child() + scroll = Gtk.Template.Child() + # Library Grid + library_grid = Gtk.Template.Child() + # Standalone Image + standalone_stack = Gtk.Template.Child() + standalone_spinner = Gtk.Template.Child() + standalone_scroll = Gtk.Template.Child() + standalone_image = Gtk.Template.Child() + + + def __init__(self, client): + super().__init__() + self._logger = logging.getLogger(__name__) + self._client = client + 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._toolbar = LibraryToolbar(self._item_size) + self._toolbar.connect('select', self.on_toolbar_select) + self._toolbar.connect('toggle-search', self.on_toolbar_toggle_search) + self._toolbar.connect('update', self.on_toolbar_update) + self._toolbar.connect('start-scale', self.on_toolbar_scale) + self._toolbar.connect('end-scale', self.on_toolbar_scaled) + self._toolbar.connect('sort', self.on_toolbar_sort) + self._toolbar.connect('sort-type', self.on_toolbar_sort_type) + # Header bar + self._headerbar_standalone = AlbumHeaderbar() + self._headerbar_standalone.connect('close', self.on_standalone_close_clicked) + # Progress Bar + self.progress_image.set_from_pixbuf(self._get_default_image()) + # 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.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) + + + def get_headerbar_standalone(self): + return self._headerbar_standalone + + + def get_toolbar(self): + return self._toolbar + + + def set_selected(self, selected): + self._is_selected = selected + + + @Gtk.Template.Callback() + 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._toolbar.get_grid_scale().clear_marks() + width = widget.get_allocation().width + + lower = int(self._toolbar.get_grid_scale().get_adjustment().get_lower()) + upper = int(self._toolbar.get_grid_scale().get_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._toolbar.get_grid_scale().add_mark( + pixel, + Gtk.PositionType.BOTTOM, + None + ) + + + def on_toolbar_toggle_search(self, widget, active): + self.filter_bar.set_search_mode(active) + + + def on_toolbar_select(self, widget, active): + if 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(Utils.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(Utils.CSS_SELECTION) + + + def on_toolbar_update(self, widget): + self.emit('update') + + + def on_toolbar_scale(self, widget, value): + size = math.floor(value) + range = self._toolbar.get_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_toolbar_scaled(self, widget, value): + size = round(value) + range = self._toolbar.get_grid_scale().get_adjustment() + if size < range.get_lower() or size > range.get_upper(): + return False + self.emit('item-size-changed', size) + self._redraw() + return False + + + def on_toolbar_sort(self, widget, sort): + self._change_sort(sort) + + + def on_toolbar_sort_type(self, widget, sort_type): + self._sort_type = sort_type + self._library_grid_model.set_sort_column_id(2, sort_type) + self.emit('sort-type-changed', sort_type) + + + @Gtk.Template.Callback() + def on_filter_bar_notify(self, widget, value): + if self._toolbar.is_search_active() is not self.filter_bar.get_search_mode(): + self._toolbar.set_search_active(self.filter_bar.get_search_mode()) + + + @Gtk.Template.Callback() + def on_filter_entry_changed(self, widget): + self._filter_string = self.filter_entry.get_text() + GObject.idle_add(self._library_grid_filter.refilter) + + + @Gtk.Template.Callback() + 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] + self.emit('albumart', id) + + # Show standalone album + if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE: + # Set labels + self._headerbar_standalone.set_album(album) + + # Show panel + self._open_standalone() + + # Set cover loading indicator + self.standalone_stack.set_visible_child(self.standalone_spinner) + self.standalone_spinner.start() + + + @Gtk.Template.Callback() + 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) + + + @Gtk.Template.Callback() + def on_selection_cancel_clicked(self, widget): + self._toolbar.exit_selection() + + + @Gtk.Template.Callback() + def on_selection_add_clicked(self, widget): + ids = [album.get_id() for album in self._selected_albums] + self.emit('queue-multiple', ids) + self._toolbar.exit_selection() + + + @Gtk.Template.Callback() + def on_standalone_scroll_size_allocate(self, widget, allocation): + self._resize_standalone_image() + + + @Gtk.Template.Callback() + def on_standalone_play_clicked(self, widget): + self.emit('play', self._selected_albums[0].get_id()) + self._close_standalone() + + + @Gtk.Template.Callback() + 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._toolbar.get_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 set_albumart(self, album, data): + if album in self._selected_albums: + if data: + # Load image and draw it + try: + self._standalone_pixbuf = Utils.load_pixbuf(data) + except Exception as e: + self._logger.exception("Failed to set albumart") + self._standalone_pixbuf = self._get_default_image() + else: + self._standalone_pixbuf = self._get_default_image() + # Show image + self._resize_standalone_image() + self.standalone_stack.set_visible_child(self.standalone_scroll) + self.standalone_spinner.stop() + + + 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_parent().child_set_property, + self, + '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, self._client, album, size) + except client.CommandException: + # Exception is handled by client + pass + except Exception as e: + self._logger.exception("Failed to load albumart") + if pixbuf is None: + pixbuf = self._icon_theme.load_icon( + Utils.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.set_visible_child(self.panel_standalone) + self.emit('open-standalone') + + + def _close_standalone(self): + self.set_visible_child(self.get_children()[0]) + self.emit('close-standalone') + + + 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( + Utils.STOCK_ICON_DEFAULT, + 512, + Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE + ) + + + + diff --git a/mcg/mcg.py b/mcg/mcg.py index 7e5913c..e449755 100755 --- a/mcg/mcg.py +++ b/mcg/mcg.py @@ -3,17 +3,42 @@ import sys -from mcg.application import Application +from mcg import Environment +def setup_resources(): + from gi.repository import Gio + + resource = Gio.resource_load( + Environment.get_data('de.coderkun.mcg.gresource') + ) + Gio.Resource._register(resource) + + +def setup_locale(): + import gettext + + gettext.bindtextdomain('mcg', Environment.get_locale()) + gettext.textdomain('mcg') + + +def run_application(): + from mcg.application import Application + + app = Application() + return app.run(sys.argv) + + def main(): - # Start application - app = Application() - exit_status = app.run(sys.argv) - sys.exit(exit_status) + setup_resources() + setup_locale() + return run_application() + + if __name__ == "__main__": main() + sys.exit(main()) diff --git a/mcg/playlistpanel.py b/mcg/playlistpanel.py new file mode 100644 index 0000000..4210efc --- /dev/null +++ b/mcg/playlistpanel.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') +import logging +import math +import threading + +from gi.repository import Gtk, GObject, GdkPixbuf + +from mcg import client +from mcg.albumheaderbar import AlbumHeaderbar +from mcg.utils import Utils + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/playlist-toolbar.ui') +class PlaylistToolbar(Gtk.ButtonBox): + __gtype_name__ = 'McgPlaylistToolbar' + __gsignals__ = { + 'select': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), + 'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()) + } + + # Widgets + playlist_clear_button = Gtk.Template.Child() + select_button = Gtk.Template.Child() + + + def __init__(self): + super().__init__() + + + @Gtk.Template.Callback() + def on_select_toggled(self, widget): + self.emit('select', widget.get_active()) + + + @Gtk.Template.Callback() + def on_clear_clicked(self, widget): + if widget is self.playlist_clear_button: + self.emit('clear-playlist') + + + def exit_selection(self): + self.select_button.set_active(False) + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/playlist-panel.ui') +class PlaylistPanel(Gtk.Stack): + __gtype_name__ = 'McgPlaylistPanel' + __gsignals__ = { + 'open-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'close-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'remove-album': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), + 'remove-multiple-albums': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), + 'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), + 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) + } + + + # Widgets + panel_standalone = Gtk.Template.Child() + actionbar_revealer = Gtk.Template.Child() + # Playlist Grid + playlist_grid = Gtk.Template.Child() + # Action bar (normal) + actionbar = Gtk.Template.Child() + actionbar_standalone = Gtk.Template.Child() + # Standalone Image + standalone_stack = Gtk.Template.Child() + standalone_spinner = Gtk.Template.Child() + standalone_scroll = Gtk.Template.Child() + standalone_image = Gtk.Template.Child() + + + def __init__(self, client): + GObject.GObject.__init__(self) + self._client = client + 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._toolbar = PlaylistToolbar() + self._toolbar.connect('select', self.on_toolbar_select) + self._toolbar.connect('clear-playlist', self.on_toolbar_clear) + # Header bar + self._headerbar_standalone = AlbumHeaderbar() + self._headerbar_standalone.connect('close', self.on_headerbar_close_clicked) + # Playlist Grid: Model + self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str) + # Playlist Grid + 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) + + + def get_headerbar_standalone(self): + return self._headerbar_standalone + + + def get_toolbar(self): + return self._toolbar + + + def set_selected(self, selected): + self._is_selected = selected + + + def on_toolbar_select(self, widget, active): + if 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(Utils.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(Utils.CSS_SELECTION) + + + def on_toolbar_clear(self, widget): + self.emit('clear-playlist') + + + @Gtk.Template.Callback() + 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] + self.emit('albumart', hash) + + # Show standalone album + if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE: + # Set labels + self._headerbar_standalone.set_album(album) + + # Show panel + self._open_standalone() + + # Set cover loading indicator + self.standalone_stack.set_visible_child(self.standalone_spinner) + self.standalone_spinner.start() + + + @Gtk.Template.Callback() + 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]) + + + @Gtk.Template.Callback() + def on_selection_cancel_clicked(self, widget): + self._toolbar.exit_selection() + + + @Gtk.Template.Callback() + def on_selection_remove_clicked(self, widget): + self.emit('remove-multiple-albums', self._selected_albums) + self._toolbar.exit_selection() + + + @Gtk.Template.Callback() + def on_standalone_scroll_size_allocate(self, widget, allocation): + self._resize_standalone_image() + + + def on_headerbar_close_clicked(self, widget): + self._close_standalone() + + + @Gtk.Template.Callback() + def on_standalone_remove_clicked(self, widget): + self.emit('remove-album', self._selected_albums[0]) + self._close_standalone() + + + @Gtk.Template.Callback() + 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 set_albumart(self, album, data): + if album in self._selected_albums: + if data: + # Load image and draw it + try: + self._standalone_pixbuf = Utils.load_pixbuf(data) + except Exception as e: + self._logger.exception("Failed to set albumart") + self._cover_pixbuf = self._get_default_image() + else: + self._cover_pixbuf = self._get_default_image() + # Show image + self._resize_standalone_image() + self.standalone_stack.set_visible_child(self.standalone_scroll) + self.standalone_spinner.stop() + + + 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_parent().child_set_property, + self, + '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 + # Load albumart thumbnail + try: + pixbuf = Utils.load_thumbnail(cache, self._client, album, size) + except client.CommandException: + # Exception is handled by client + pass + except Exception: + self._logger.exception("Failed to load albumart") + if pixbuf is None: + pixbuf = self._icon_theme.load_icon( + Utils.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.set_visible_child(self.panel_standalone) + self.emit('open-standalone') + + + def _close_standalone(self): + self.set_visible_child(self.get_children()[0]) + self.emit('close-standalone') + + + 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( + Utils.STOCK_ICON_DEFAULT, + 512, + Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE + ) + + + + diff --git a/mcg/serverpanel.py b/mcg/serverpanel.py new file mode 100644 index 0000000..0876dc3 --- /dev/null +++ b/mcg/serverpanel.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk, GObject + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/server-toolbar.ui') +class ServerToolbar(Gtk.ButtonBox): + __gtype_name__ = 'McgServerToolbar' + + + def __init__(self): + super().__init__() + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/server-panel.ui') +class ServerPanel(Gtk.Box): + __gtype_name__ = 'McgServerPanel' + __gsignals__ = { + 'change-output-device': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,bool,)), + } + + # Widgets + # Status widgets + status_file = Gtk.Template.Child() + status_audio = Gtk.Template.Child() + status_bitrate = Gtk.Template.Child() + status_error = Gtk.Template.Child() + # Stats widgets + stats_artists = Gtk.Template.Child() + stats_albums = Gtk.Template.Child() + stats_songs = Gtk.Template.Child() + stats_dbplaytime = Gtk.Template.Child() + stats_playtime = Gtk.Template.Child() + stats_uptime = Gtk.Template.Child() + # Audio ouptut devices widgets + output_devices = Gtk.Template.Child() + + + def __init__(self): + super().__init__() + self._none_label = "" + self._output_buttons = {} + self._is_selected = False + + # Widgets + self._toolbar = ServerToolbar() + self._none_label = self.status_file.get_label() + + + 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()) diff --git a/mcg/shortcutsdialog.py b/mcg/shortcutsdialog.py new file mode 100644 index 0000000..197400c --- /dev/null +++ b/mcg/shortcutsdialog.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/shortcuts-dialog.ui') +class ShortcutsDialog(Gtk.ShortcutsWindow): + __gtype_name__ = 'McgShortcutsDialog' + + + def __init__(self): + super().__init__() diff --git a/mcg/utils.py b/mcg/utils.py index 0cc53ec..12a3ce4 100644 --- a/mcg/utils.py +++ b/mcg/utils.py @@ -14,6 +14,8 @@ from gi.repository import GdkPixbuf class Utils: + CSS_SELECTION = 'selection' + STOCK_ICON_DEFAULT = 'image-x-generic-symbolic' def load_pixbuf(data): diff --git a/mcg/widgets.py b/mcg/widgets.py deleted file mode 100644 index a0197b5..0000000 --- a/mcg/widgets.py +++ /dev/null @@ -1,2150 +0,0 @@ -#!/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_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._mcg)) - self._panels.append(LibraryPanel(builder, self._mcg)) - - # 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._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_COVER].connect('albumart', self.on_cover_panel_albumart) - 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_PLAYLIST].connect('albumart', self.on_playlist_panel_albumart) - 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._panels[Window._PANEL_INDEX_LIBRARY].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_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) - 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._appwindow.add_action(self._toggle_fullscreen_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()) - - - def on_menu_toggle_fullscreen(self, action, value): - self._stack.set_visible_child(self._panels[Window._PANEL_INDEX_COVER].get()) - if not self._state.get_property(WindowState.IS_FULLSCREENED): - self._appwindow.fullscreen() - else: - self._appwindow.unfullscreen() - - - # 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): - 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.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_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._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()) - - - 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_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_load_albumart(self, album, data): - GObject.idle_add(self._panels[self._PANEL_INDEX_COVER].set_albumart, album, data) - GObject.idle_add(self._panels[self._PANEL_INDEX_PLAYLIST].set_albumart, album, data) - GObject.idle_add(self._panels[self._PANEL_INDEX_LIBRARY].set_albumart, album, data) - - - 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() - self._mcg.connect(host, port, password) - 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)) - } - - - 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') - - # 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 - } - - - 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 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 _call_back(self): - self.emit('connection-changed', self.get_host(), self.get_port(), self.get_password(),) - - - - -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,)), - 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) - } - - - def __init__(self, builder): - GObject.GObject.__init__(self) - - self._current_album = None - self._current_cover_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-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_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) - - # Load cover - if album != self._current_cover_album: - self._cover_stack.set_visible_child(self._cover_spinner) - self._cover_spinner.start() - self.emit('albumart', album.get_id() if album else None) - - # Set current album - self._current_album = album - self._enable_tracklist() - self._toolbar_fullscreen_button.set_sensitive(self._current_album is not None) - - - 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_albumart(self, album, data): - if album == self._current_album: - if data: - # Load image and draw it - try: - self._cover_pixbuf = Utils.load_pixbuf(data) - except Exception as e: - self._logger.exception("Failed to set albumart") - self._cover_pixbuf = self._get_default_image() - else: - # Reset image - self._cover_pixbuf = self._get_default_image() - self._current_size = None - # Show image - self._resize_image() - self._cover_stack.set_visible_child(self._cover_scroll) - self._cover_spinner.stop() - self._current_cover_album = album - - - 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,)), - 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) - } - - - def __init__(self, builder, client): - GObject.GObject.__init__(self) - self._logger = logging.getLogger(__name__) - self._client = client - 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] - self.emit('albumart', hash) - - # 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() - - # Set cover loading indicator - self._standalone_stack.set_visible_child(self._standalone_spinner) - self._standalone_spinner.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 set_albumart(self, album, data): - if album in self._selected_albums: - if data: - # Load image and draw it - try: - self._standalone_pixbuf = Utils.load_pixbuf(data) - except Exception as e: - self._logger.exception("Failed to set albumart") - self._cover_pixbuf = self._get_default_image() - else: - self._cover_pixbuf = self._get_default_image() - # Show image - self._resize_standalone_image() - self._standalone_stack.set_visible_child(self._standalone_scroll) - self._standalone_spinner.stop() - - - 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 - # Load albumart thumbnail - try: - pixbuf = Utils.load_thumbnail(cache, self._client, album, size) - except client.CommandException: - # Exception is handled by client - pass - except Exception: - self._logger.exception("Failed to load albumart") - 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 _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,)), - 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) - } - - - def __init__(self, builder, client): - GObject.GObject.__init__(self) - self._logger = logging.getLogger(__name__) - self._client = client - 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] - self.emit('albumart', id) - - # 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() - - # Set cover loading indicator - self._standalone_stack.set_visible_child(self._standalone_spinner) - self._standalone_spinner.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 set_albumart(self, album, data): - if album in self._selected_albums: - if data: - # Load image and draw it - try: - self._standalone_pixbuf = Utils.load_pixbuf(data) - except Exception as e: - self._logger.exception("Failed to set albumart") - self._standalone_pixbuf = self._get_default_image() - else: - self._standalone_pixbuf = self._get_default_image() - # Show image - self._resize_standalone_image() - self._standalone_stack.set_visible_child(self._standalone_scroll) - self._standalone_spinner.stop() - - - 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, self._client, album, size) - except client.CommandException: - # Exception is handled by client - pass - except Exception as e: - self._logger.exception("Failed to load albumart") - 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 _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 diff --git a/mcg/window.py b/mcg/window.py new file mode 100644 index 0000000..e7a487d --- /dev/null +++ b/mcg/window.py @@ -0,0 +1,650 @@ +#!/usr/bin/env python3 + + +import gi +gi.require_version('Gtk', '3.0') +try: + import keyring + use_keyring = True +except: + use_keyring = False +import logging + +from gi.repository import Gtk, Gdk, GObject, GLib, Gio + +from mcg import client +from mcg.shortcutsdialog import ShortcutsDialog +from mcg.connectionpanel import ConnectionPanel +from mcg.serverpanel import ServerPanel +from mcg.coverpanel import CoverPanel +from mcg.playlistpanel import PlaylistPanel +from mcg.librarypanel import LibraryPanel +from mcg.zeroconf import ZeroconfProvider + + + + +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): + super().__init__() + + + + +@Gtk.Template(resource_path='/de/coderkun/mcg/ui/window.ui') +class Window(Gtk.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' + _CUSTOM_STARTUP_COMPLETE = 'startup-complete' + + # Widgets + content_stack = Gtk.Template.Child() + panel_stack = Gtk.Template.Child() + toolbar_stack = Gtk.Template.Child() + # Headerbar + headerbar = Gtk.Template.Child() + headerbar_title_stack = Gtk.Template.Child() + headerbar_panel_switcher = Gtk.Template.Child() + headerbar_connection_label = Gtk.Template.Child() + headerbar_button_connect = Gtk.Template.Child() + headerbar_button_playpause = Gtk.Template.Child() + headerbar_button_volume = Gtk.Template.Child() + # Infobar + info_revealer = Gtk.Template.Child() + info_bar = Gtk.Template.Child() + info_label = Gtk.Template.Child() + + + def __init__(self, app, title, settings): + super().__init__() + self.set_application(app) + self.set_title(title) + self._settings = settings + self._panels = [] + self._mcg = client.Client() + self._state = WindowState() + self._changing_volume = False + self._setting_volume = False + + # 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(self._connection_panel) + self.panel_stack.add_titled(self._server_panel, 'server-panel', "Server") + self.panel_stack.add_titled(self._cover_panel, 'cover-panel', "Cover") + self.panel_stack.add_titled(self._playlist_panel, 'playlist-panel', "Playlist") + self.panel_stack.add_titled(self._library_panel, 'library-panel', "Library") + # Header + self._playlist_panel.get_headerbar_standalone().connect('close', self.on_panel_close_standalone) + self._library_panel.get_headerbar_standalone().connect('close', self.on_panel_close_standalone) + # Toolbar stack + self.toolbar_stack.add(self._server_panel.get_toolbar()) + self.toolbar_stack.add(self._cover_panel.get_toolbar()) + self.toolbar_stack.add(self._playlist_panel.get_toolbar()) + self.toolbar_stack.add(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._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_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) + + # Actions + self.set_default_size(self._state.width, self._state.height) + if self._state.get_property(WindowState.IS_MAXIMIZED): + self.maximize() + self.show_all() + 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) + + + # 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.IS_FULLSCREENED): + self.fullscreen() + else: + self.unfullscreen() + + + # Window callbacks + + @Gtk.Template.Callback() + def on_resize(self, widget, event): + if not self._state.get_property(WindowState.IS_MAXIMIZED): + size = self.get_size() + self._state.set_property(WindowState.WIDTH, size.width) + self._state.set_property(WindowState.HEIGHT, size.height) + + + @Gtk.Template.Callback() + 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)) + + + # HeaderBar callbacks + + @Gtk.Template.Callback() + def on_headerbar_connection_active_notify(self, widget, status): + self._connect() + + + @Gtk.Template.Callback() + def on_headerbar_connection_state_set(self, widget, state): + return True + + + @Gtk.Template.Callback() + def on_headerbar_volume_press(self, widget, active): + self._changing_volume = active + + + @Gtk.Template.Callback() + def on_headerbar_volume_release(self, widget, active): + self._changing_volume = active + + + @Gtk.Template.Callback() + def on_headerbar_playpause_toggled(self, widget): + self._mcg.playpause() + self._mcg.get_status() + + + @Gtk.Template.Callback() + def on_headerbar_volume_changed(self, widget, value): + if not self._setting_volume: + self._mcg.set_volume(int(value*100)) + + + # Infobar callback + + @Gtk.Template.Callback() + def on_info_bar_close(self, *args): + self.info_revealer.set_reveal_child(False) + + + @Gtk.Template.Callback() + def on_info_bar_response(self, widget, response): + self.info_revealer.set_reveal_child(False) + + + + # 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()) + GObject.idle_add( + self.panel_stack.child_set_property, + self.panel_stack.get_visible_child(), + 'needs-attention', + False + ) + + def on_panel_open_standalone(self, panel): + self.set_titlebar(panel.get_headerbar_standalone()) + + + def on_panel_close_standalone(self, headerbar): + self.set_titlebar(self.headerbar) + + 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.IS_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, self._library_panel.get_sort_order()) + + + def on_library_panel_sort_type_changed(self, widget, sort_type): + self._settings.set_boolean(Window.SETTING_SORT_TYPE, self._library_panel.get_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_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._cover_panel.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._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 is None: + self.info_revealer.set_reveal_child(False) + else: + 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): + GObject.idle_add(self._cover_panel.set_albumart, album, data) + GObject.idle_add(self._playlist_panel.set_albumart, album, data) + GObject.idle_add(self._library_panel.set_albumart, album, data) + + + def on_mcg_custom(self, name): + if name == Window._CUSTOM_STARTUP_COMPLETE: + for panel in self._panels: + GObject.idle_add( + self.panel_stack.child_set_property, + panel, + '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.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.IS_FULLSCREENED): + self._state.set_property(WindowState.IS_FULLSCREENED, fullscreened_new) + if self._state.get_property(WindowState.IS_FULLSCREENED): + self.headerbar.hide() + self._cover_panel.set_fullscreen(True) + # Hide cursor + self.get_window().set_cursor( + Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "none") + ) + else: + self.headerbar.show() + self._cover_panel.set_fullscreen(False) + # Reset cursor + self.get_window().set_cursor( + Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "default") + ) + + + 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_button_playpause.handler_block_by_func( + self.on_headerbar_playpause_toggled + ) + self.headerbar_button_playpause.set_active(True) + self.headerbar_button_playpause.handler_unblock_by_func( + self.on_headerbar_playpause_toggled + ) + + + def _set_pause(self): + self.headerbar_button_playpause.handler_block_by_func( + self.on_headerbar_playpause_toggled + ) + self.headerbar_button_playpause.set_active(False) + self.headerbar_button_playpause.handler_unblock_by_func( + self.on_headerbar_playpause_toggled + ) + + + def _set_volume(self, volume): + if volume >= 0: + self.headerbar_button_volume.set_visible(True) + if not self._changing_volume: + 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_button_connect.handler_block_by_func( + self.on_headerbar_connection_active_notify + ) + self.headerbar_button_connect.set_active(True) + self.headerbar_button_connect.set_state(True) + self.headerbar_button_connect.handler_unblock_by_func( + self.on_headerbar_connection_active_notify + ) + self.headerbar_title_stack.set_visible_child(self.headerbar_panel_switcher) + + + def _headerbar_disconnected(self): + self.headerbar_button_connect.handler_block_by_func( + self.on_headerbar_connection_active_notify + ) + self.headerbar_button_connect.set_active(False) + self.headerbar_button_connect.set_state(False) + self.headerbar_button_connect.handler_unblock_by_func( + self.on_headerbar_connection_active_notify + ) + self.headerbar_title_stack.set_visible_child(self.headerbar_connection_label) + + + 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_bar.set_message_type(Gtk.MessageType.ERROR) + self.info_label.set_text(message) + self.info_revealer.set_reveal_child(True)