const Gio = imports.gi.Gio; const Lang = imports.lang; const Signals = imports.signals; const Shell = imports.gi.Shell; const St = imports.gi.St; const Calendar = imports.ui.calendar; const Main = imports.ui.main; const MessageList = imports.ui.messageList; const DBusIface = '<node> \ <interface name="org.freedesktop.DBus"> \ <method name="ListNames"> \ <arg type="as" direction="out" name="names" /> \ </method> \ <signal name="NameOwnerChanged"> \ <arg type="s" direction="out" name="name" /> \ <arg type="s" direction="out" name="oldOwner" /> \ <arg type="s" direction="out" name="newOwner" /> \ </signal> \ </interface> \ </node>'; const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface); const MprisIface = '<node> \ <interface name="org.mpris.MediaPlayer2"> \ <method name="Raise" /> \ <property name="CanRaise" type="b" access="read" /> \ <property name="DesktopEntry" type="s" access="read" /> \ </interface> \ </node>'; const MprisProxy = Gio.DBusProxy.makeProxyWrapper(MprisIface); const MprisPlayerIface = '<node> \ <interface name="org.mpris.MediaPlayer2.Player"> \ <method name="PlayPause" /> \ <method name="Next" /> \ <method name="Previous" /> \ <property name="CanPlay" type="b" access="read" /> \ <property name="Metadata" type="a{sv}" access="read" /> \ <property name="PlaybackStatus" type="s" access="read" /> \ </interface> \ </node>'; const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface); const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.'; const MediaMessage = new Lang.Class({ Name: 'MediaMessage', Extends: MessageList.Message, _init: function(player) { this._player = player; this.parent('', ''); this._icon = new St.Icon({ style_class: 'media-message-cover-icon' }); this.setIcon(this._icon); this.addMediaControl('media-skip-backward-symbolic', Lang.bind(this, function() { this._player.previous(); })); this._playPauseButton = this.addMediaControl(null, Lang.bind(this, function() { this._player.playPause(); })); this.addMediaControl('media-skip-forward-symbolic', Lang.bind(this, function() { this._player.next(); })); this._player.connect('changed', Lang.bind(this, this._update)); this._player.connect('closed', Lang.bind(this, this.close)); this._update(); }, _onClicked: function() { this._player.raise(); Main.panel.closeCalendar(); }, _update: function() { this.setTitle(this._player.trackArtists.join(', ')); this.setBody(this._player.trackTitle); if (this._player.trackCoverUrl) { let file = Gio.File.new_for_uri(this._player.trackCoverUrl); this._icon.gicon = new Gio.FileIcon({ file: file }); this._icon.remove_style_class_name('fallback'); } else { this._icon.icon_name = 'audio-x-generic-symbolic'; this._icon.add_style_class_name('fallback'); } let isPlaying = this._player.status == 'Playing'; let iconName = isPlaying ? 'media-playback-pause-symbolic' : 'media-playback-start-symbolic'; this._playPauseButton.child.icon_name = iconName; } }); const MprisPlayer = new Lang.Class({ Name: 'MprisPlayer', _init: function(busName) { this._mprisProxy = new MprisProxy(Gio.DBus.session, busName, '/org/mpris/MediaPlayer2', Lang.bind(this, this._onMprisProxyReady)); this._playerProxy = new MprisPlayerProxy(Gio.DBus.session, busName, '/org/mpris/MediaPlayer2', Lang.bind(this, this._onPlayerProxyReady)); this._visible = false; this._trackArtists = []; this._trackTitle = ''; this._trackCoverUrl = ''; }, get status() { return this._playerProxy.PlaybackStatus; }, get trackArtists() { return this._trackArtists; }, get trackTitle() { return this._trackTitle; }, get trackCoverUrl() { return this._trackCoverUrl; }, playPause: function() { this._playerProxy.PlayPauseRemote(); }, next: function() { this._playerProxy.NextRemote(); }, previous: function() { this._playerProxy.PreviousRemote(); }, raise: function() { // The remote Raise() method may run into focus stealing prevention, // so prefer activating the app via .desktop file if possible let app = null; if (this._mprisProxy.DesktopEntry) { let desktopId = this._mprisProxy.DesktopEntry + '.desktop'; app = Shell.AppSystem.get_default().lookup_app(desktopId); } if (app) app.activate(); else if (this._mprisProxy.CanRaise) this._mprisProxy.RaiseRemote(); }, _close: function() { this._mprisProxy.disconnect(this._ownerNotifyId); this._mprisProxy = null; this._playerProxy.disconnect(this._propsChangedId); this._playerProxy = null; this.emit('closed'); }, _onMprisProxyReady: function() { this._ownerNotifyId = this._mprisProxy.connect('notify::g-name-owner', Lang.bind(this, function() { if (!this._mprisProxy.g_name_owner) this._close(); })); }, _onPlayerProxyReady: function() { this._propsChangedId = this._playerProxy.connect('g-properties-changed', Lang.bind(this, this._updateState)); this._updateState(); }, _updateState: function() { let metadata = {}; for (let prop in this._playerProxy.Metadata) metadata[prop] = this._playerProxy.Metadata[prop].deep_unpack(); this._trackArtists = metadata['xesam:artist'] || [_("Unknown artist")]; this._trackTitle = metadata['xesam:title'] || _("Unknown title"); this._trackCoverUrl = metadata['mpris:artUrl'] || ''; this.emit('changed'); let visible = this._playerProxy.CanPlay; if (this._visible != visible) { this._visible = visible; if (visible) this.emit('show'); else this._close(); } } }); Signals.addSignalMethods(MprisPlayer.prototype); const MediaSection = new Lang.Class({ Name: 'MediaSection', Extends: MessageList.MessageListSection, _init: function() { this.parent(_("Media")); this._players = new Map(); this._proxy = new DBusProxy(Gio.DBus.session, 'org.freedesktop.DBus', '/org/freedesktop/DBus', Lang.bind(this, this._onProxyReady)); }, _shouldShow: function() { return !this.empty && Calendar.isToday(this._date); }, _addPlayer: function(busName) { if (this._players.get(busName)) return; let player = new MprisPlayer(busName); player.connect('closed', Lang.bind(this, function() { this._players.delete(busName); })); player.connect('show', Lang.bind(this, function() { let message = new MediaMessage(player); this.addMessage(message, true); })); this._players.set(busName, player); }, _onProxyReady: function() { this._proxy.ListNamesRemote(Lang.bind(this, function([names]) { names.forEach(Lang.bind(this, function(name) { if (!name.startsWith(MPRIS_PLAYER_PREFIX)) return; this._addPlayer(name); })); })); this._proxy.connectSignal('NameOwnerChanged', Lang.bind(this, this._onNameOwnerChanged)); }, _onNameOwnerChanged: function(proxy, sender, [name, oldOwner, newOwner]) { if (!name.startsWith(MPRIS_PLAYER_PREFIX)) return; if (newOwner && !oldOwner) this._addPlayer(name); } });