d318bf539d
Most media players have a media player that shows the title of a song and artists in the song. In those media players the title is highlighted (bold text) and the list of artists is under the title. Shell does the exact opposite in the player in the notification area. Example media players: Spotify, Rhythmbox, GNOME Music https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1480
301 lines
9.4 KiB
JavaScript
301 lines
9.4 KiB
JavaScript
/* exported MediaSection */
|
|
const { Gio, GObject, Shell, St } = imports.gi;
|
|
const Signals = imports.signals;
|
|
|
|
const Main = imports.ui.main;
|
|
const MessageList = imports.ui.messageList;
|
|
|
|
const { loadInterfaceXML } = imports.misc.fileUtils;
|
|
|
|
const DBusIface = loadInterfaceXML('org.freedesktop.DBus');
|
|
const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface);
|
|
|
|
const MprisIface = loadInterfaceXML('org.mpris.MediaPlayer2');
|
|
const MprisProxy = Gio.DBusProxy.makeProxyWrapper(MprisIface);
|
|
|
|
const MprisPlayerIface = loadInterfaceXML('org.mpris.MediaPlayer2.Player');
|
|
const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface);
|
|
|
|
const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.';
|
|
|
|
var MediaMessage = GObject.registerClass(
|
|
class MediaMessage extends MessageList.Message {
|
|
_init(player) {
|
|
super._init('', '');
|
|
|
|
this._player = player;
|
|
|
|
this._icon = new St.Icon({ style_class: 'media-message-cover-icon' });
|
|
this.setIcon(this._icon);
|
|
|
|
this._prevButton = this.addMediaControl('media-skip-backward-symbolic',
|
|
() => {
|
|
this._player.previous();
|
|
});
|
|
|
|
this._playPauseButton = this.addMediaControl(null,
|
|
() => {
|
|
this._player.playPause();
|
|
});
|
|
|
|
this._nextButton = this.addMediaControl('media-skip-forward-symbolic',
|
|
() => {
|
|
this._player.next();
|
|
});
|
|
|
|
this._updateHandlerId =
|
|
this._player.connect('changed', this._update.bind(this));
|
|
this._closedHandlerId =
|
|
this._player.connect('closed', this.close.bind(this));
|
|
this._update();
|
|
}
|
|
|
|
_onDestroy() {
|
|
super._onDestroy();
|
|
this._player.disconnect(this._updateHandlerId);
|
|
this._player.disconnect(this._closedHandlerId);
|
|
}
|
|
|
|
vfunc_clicked() {
|
|
this._player.raise();
|
|
Main.panel.closeCalendar();
|
|
}
|
|
|
|
_updateNavButton(button, sensitive) {
|
|
button.reactive = sensitive;
|
|
}
|
|
|
|
_update() {
|
|
this.setTitle(this._player.trackTitle);
|
|
this.setBody(this._player.trackArtists.join(', '));
|
|
|
|
if (this._player.trackCoverUrl) {
|
|
let file = Gio.File.new_for_uri(this._player.trackCoverUrl);
|
|
this._icon.gicon = new Gio.FileIcon({ 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;
|
|
|
|
this._updateNavButton(this._prevButton, this._player.canGoPrevious);
|
|
this._updateNavButton(this._nextButton, this._player.canGoNext);
|
|
}
|
|
});
|
|
|
|
var MprisPlayer = class MprisPlayer {
|
|
constructor(busName) {
|
|
this._mprisProxy = new MprisProxy(Gio.DBus.session, busName,
|
|
'/org/mpris/MediaPlayer2',
|
|
this._onMprisProxyReady.bind(this));
|
|
this._playerProxy = new MprisPlayerProxy(Gio.DBus.session, busName,
|
|
'/org/mpris/MediaPlayer2',
|
|
this._onPlayerProxyReady.bind(this));
|
|
|
|
this._visible = false;
|
|
this._trackArtists = [];
|
|
this._trackTitle = '';
|
|
this._trackCoverUrl = '';
|
|
this._busName = busName;
|
|
}
|
|
|
|
get status() {
|
|
return this._playerProxy.PlaybackStatus;
|
|
}
|
|
|
|
get trackArtists() {
|
|
return this._trackArtists;
|
|
}
|
|
|
|
get trackTitle() {
|
|
return this._trackTitle;
|
|
}
|
|
|
|
get trackCoverUrl() {
|
|
return this._trackCoverUrl;
|
|
}
|
|
|
|
playPause() {
|
|
this._playerProxy.PlayPauseRemote();
|
|
}
|
|
|
|
get canGoNext() {
|
|
return this._playerProxy.CanGoNext;
|
|
}
|
|
|
|
next() {
|
|
this._playerProxy.NextRemote();
|
|
}
|
|
|
|
get canGoPrevious() {
|
|
return this._playerProxy.CanGoPrevious;
|
|
}
|
|
|
|
previous() {
|
|
this._playerProxy.PreviousRemote();
|
|
}
|
|
|
|
raise() {
|
|
// 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 = '%s.desktop'.format(this._mprisProxy.DesktopEntry);
|
|
app = Shell.AppSystem.get_default().lookup_app(desktopId);
|
|
}
|
|
|
|
if (app)
|
|
app.activate();
|
|
else if (this._mprisProxy.CanRaise)
|
|
this._mprisProxy.RaiseRemote();
|
|
}
|
|
|
|
_close() {
|
|
this._mprisProxy.disconnect(this._ownerNotifyId);
|
|
this._mprisProxy = null;
|
|
|
|
this._playerProxy.disconnect(this._propsChangedId);
|
|
this._playerProxy = null;
|
|
|
|
this.emit('closed');
|
|
}
|
|
|
|
_onMprisProxyReady() {
|
|
this._ownerNotifyId = this._mprisProxy.connect('notify::g-name-owner',
|
|
() => {
|
|
if (!this._mprisProxy.g_name_owner)
|
|
this._close();
|
|
});
|
|
// It is possible for the bus to disappear before the previous signal
|
|
// is connected, so we must ensure that the bus still exists at this
|
|
// point.
|
|
if (!this._mprisProxy.g_name_owner)
|
|
this._close();
|
|
}
|
|
|
|
_onPlayerProxyReady() {
|
|
this._propsChangedId = this._playerProxy.connect('g-properties-changed',
|
|
this._updateState.bind(this));
|
|
this._updateState();
|
|
}
|
|
|
|
_updateState() {
|
|
let metadata = {};
|
|
for (let prop in this._playerProxy.Metadata)
|
|
metadata[prop] = this._playerProxy.Metadata[prop].deep_unpack();
|
|
|
|
// Validate according to the spec; some clients send buggy metadata:
|
|
// https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata
|
|
this._trackArtists = metadata['xesam:artist'];
|
|
if (!Array.isArray(this._trackArtists) ||
|
|
!this._trackArtists.every(artist => typeof artist === 'string')) {
|
|
if (typeof this._trackArtists !== 'undefined') {
|
|
log(('Received faulty track artist metadata from %s; ' +
|
|
'expected an array of strings, got %s (%s)').format(
|
|
this._busName, this._trackArtists, typeof this._trackArtists));
|
|
}
|
|
this._trackArtists = [_("Unknown artist")];
|
|
}
|
|
|
|
this._trackTitle = metadata['xesam:title'];
|
|
if (typeof this._trackTitle !== 'string') {
|
|
if (typeof this._trackTitle !== 'undefined') {
|
|
log(('Received faulty track title metadata from %s; ' +
|
|
'expected a string, got %s (%s)').format(
|
|
this._busName, this._trackTitle, typeof this._trackTitle));
|
|
}
|
|
this._trackTitle = _("Unknown title");
|
|
}
|
|
|
|
this._trackCoverUrl = metadata['mpris:artUrl'];
|
|
if (typeof this._trackCoverUrl !== 'string') {
|
|
if (typeof this._trackCoverUrl !== 'undefined') {
|
|
log(('Received faulty track cover art metadata from %s; ' +
|
|
'expected a string, got %s (%s)').format(
|
|
this._busName, this._trackCoverUrl, typeof this._trackCoverUrl));
|
|
}
|
|
this._trackCoverUrl = '';
|
|
}
|
|
|
|
this.emit('changed');
|
|
|
|
let visible = this._playerProxy.CanPlay;
|
|
|
|
if (this._visible != visible) {
|
|
this._visible = visible;
|
|
if (visible)
|
|
this.emit('show');
|
|
else
|
|
this.emit('hide');
|
|
}
|
|
}
|
|
};
|
|
Signals.addSignalMethods(MprisPlayer.prototype);
|
|
|
|
var MediaSection = GObject.registerClass(
|
|
class MediaSection extends MessageList.MessageListSection {
|
|
_init() {
|
|
super._init();
|
|
|
|
this._players = new Map();
|
|
|
|
this._proxy = new DBusProxy(Gio.DBus.session,
|
|
'org.freedesktop.DBus',
|
|
'/org/freedesktop/DBus',
|
|
this._onProxyReady.bind(this));
|
|
}
|
|
|
|
get allowed() {
|
|
return !Main.sessionMode.isGreeter;
|
|
}
|
|
|
|
_addPlayer(busName) {
|
|
if (this._players.get(busName))
|
|
return;
|
|
|
|
let player = new MprisPlayer(busName);
|
|
let message = null;
|
|
player.connect('closed',
|
|
() => {
|
|
this._players.delete(busName);
|
|
});
|
|
player.connect('show', () => {
|
|
message = new MediaMessage(player);
|
|
this.addMessage(message, true);
|
|
});
|
|
player.connect('hide', () => {
|
|
this.removeMessage(message, true);
|
|
message = null;
|
|
});
|
|
|
|
this._players.set(busName, player);
|
|
}
|
|
|
|
_onProxyReady() {
|
|
this._proxy.ListNamesRemote(([names]) => {
|
|
names.forEach(name => {
|
|
if (!name.startsWith(MPRIS_PLAYER_PREFIX))
|
|
return;
|
|
|
|
this._addPlayer(name);
|
|
});
|
|
});
|
|
this._proxy.connectSignal('NameOwnerChanged',
|
|
this._onNameOwnerChanged.bind(this));
|
|
}
|
|
|
|
_onNameOwnerChanged(proxy, sender, [name, oldOwner, newOwner]) {
|
|
if (!name.startsWith(MPRIS_PLAYER_PREFIX))
|
|
return;
|
|
|
|
if (newOwner && !oldOwner)
|
|
this._addPlayer(name);
|
|
}
|
|
});
|