![Florian Müllner](/assets/img/avatar_default.png)
The Extensions app is also distributed as flatpak, so we cannot assume that its version matches the shell. In order to not show all extensions as disabled when running under a shell version that doesn't include the `enabled` property yet, add a fallback based on the current state. https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/7004 Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3073>
424 lines
12 KiB
JavaScript
424 lines
12 KiB
JavaScript
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
import GObject from 'gi://GObject';
|
|
|
|
import {
|
|
ExtensionState, ExtensionType, deserializeExtension
|
|
} from './misc/extensionUtils.js';
|
|
|
|
const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions');
|
|
const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
|
|
|
|
let shellVersion;
|
|
|
|
function loadInterfaceXML(iface) {
|
|
const uri = `resource:///org/gnome/Extensions/dbus-interfaces/${iface}.xml`;
|
|
const f = Gio.File.new_for_uri(uri);
|
|
|
|
try {
|
|
let [ok_, bytes] = f.load_contents(null);
|
|
return new TextDecoder().decode(bytes);
|
|
} catch (e) {
|
|
console.error(`Failed to load D-Bus interface ${iface}`);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const Extension = GObject.registerClass({
|
|
GTypeName: 'Extension',
|
|
Properties: {
|
|
'uuid': GObject.ParamSpec.string(
|
|
'uuid', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
''),
|
|
'name': GObject.ParamSpec.string(
|
|
'name', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
''),
|
|
'description': GObject.ParamSpec.string(
|
|
'description', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
''),
|
|
'state': GObject.ParamSpec.int(
|
|
'state', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
1, 99, ExtensionState.INITIALIZED),
|
|
'enabled': GObject.ParamSpec.boolean(
|
|
'enabled', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
'creator': GObject.ParamSpec.string(
|
|
'creator', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
''),
|
|
'url': GObject.ParamSpec.string(
|
|
'url', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
''),
|
|
'version': GObject.ParamSpec.string(
|
|
'version', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
''),
|
|
'error': GObject.ParamSpec.string(
|
|
'error', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
''),
|
|
'has-error': GObject.ParamSpec.boolean(
|
|
'has-error', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
'has-prefs': GObject.ParamSpec.boolean(
|
|
'has-prefs', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
'has-update': GObject.ParamSpec.boolean(
|
|
'has-update', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
'has-version': GObject.ParamSpec.boolean(
|
|
'has-version', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
'can-change': GObject.ParamSpec.boolean(
|
|
'can-change', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
'is-user': GObject.ParamSpec.boolean(
|
|
'is-user', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
},
|
|
}, class Extension extends GObject.Object {
|
|
constructor(variant) {
|
|
super();
|
|
this.update(variant);
|
|
}
|
|
|
|
update(variant) {
|
|
const deserialized = deserializeExtension(variant);
|
|
|
|
const {
|
|
uuid, type, state, enabled, error, hasPrefs, hasUpdate, canChange, metadata,
|
|
} = deserialized;
|
|
|
|
if (!this._uuid)
|
|
this._uuid = uuid;
|
|
|
|
if (this._uuid !== uuid)
|
|
throw new Error(`Invalid update of extension ${this._uuid} with data from ${uuid}`);
|
|
|
|
this.freeze_notify();
|
|
|
|
const {name} = metadata;
|
|
if (this._name !== name) {
|
|
this._name = name;
|
|
this.notify('name');
|
|
}
|
|
|
|
const [desc] = metadata.description.split('\n');
|
|
if (this._description !== desc) {
|
|
this._description = desc;
|
|
this.notify('description');
|
|
}
|
|
|
|
if (this._type !== type) {
|
|
this._type = type;
|
|
this.notify('is-user');
|
|
}
|
|
|
|
if (this._errorDetail !== error) {
|
|
this._errorDetail = error;
|
|
this.notify('error');
|
|
}
|
|
|
|
if (this._enabled !== enabled) {
|
|
this._enabled = enabled;
|
|
this.notify('enabled');
|
|
}
|
|
|
|
if (this._state !== state) {
|
|
const hadError = this.hasError;
|
|
this._state = state;
|
|
this.notify('state');
|
|
|
|
// Compat with older shell versions
|
|
if (this._enabled === undefined)
|
|
this.notify('enabled');
|
|
|
|
if (this.hasError !== hadError) {
|
|
this.notify('has-error');
|
|
this.notify('error');
|
|
}
|
|
}
|
|
|
|
const creator = metadata.creator ?? '';
|
|
if (this._creator !== creator) {
|
|
this._creator = creator;
|
|
this.notify('creator');
|
|
}
|
|
|
|
const url = metadata.url ?? '';
|
|
if (this._url !== url) {
|
|
this._url = url;
|
|
this.notify('url');
|
|
}
|
|
|
|
const version = String(
|
|
metadata['version-name'] || metadata['version'] || '');
|
|
if (this._version !== version) {
|
|
this._version = version;
|
|
this.notify('version');
|
|
this.notify('has-version');
|
|
}
|
|
|
|
if (this._hasPrefs !== hasPrefs) {
|
|
this._hasPrefs = hasPrefs;
|
|
this.notify('has-prefs');
|
|
}
|
|
|
|
if (this._hasUpdate !== hasUpdate) {
|
|
this._hasUpdate = hasUpdate;
|
|
this.notify('has-update');
|
|
}
|
|
|
|
if (this._canChange !== canChange) {
|
|
this._canChange = canChange;
|
|
this.notify('can-change');
|
|
}
|
|
|
|
this.thaw_notify();
|
|
}
|
|
|
|
get uuid() {
|
|
return this._uuid;
|
|
}
|
|
|
|
get name() {
|
|
return this._name;
|
|
}
|
|
|
|
get description() {
|
|
return this._description;
|
|
}
|
|
|
|
get state() {
|
|
return this._state;
|
|
}
|
|
|
|
get enabled() {
|
|
// Compat with older shell versions
|
|
if (this._enabled === undefined) {
|
|
return this.state === ExtensionState.ACTIVE ||
|
|
this.state === ExtensionState.ACTIVATING;
|
|
}
|
|
|
|
return this._enabled;
|
|
}
|
|
|
|
get creator() {
|
|
return this._creator;
|
|
}
|
|
|
|
get url() {
|
|
return this._url;
|
|
}
|
|
|
|
get version() {
|
|
return this._version;
|
|
}
|
|
|
|
get error() {
|
|
if (!this.hasError)
|
|
return '';
|
|
|
|
if (this.state === ExtensionState.OUT_OF_DATE) {
|
|
return this.version !== ''
|
|
? _('The installed version of this extension (%s) is incompatible with the current version of GNOME (%s). The extension has been disabled.').format(this.version, shellVersion)
|
|
: _('The installed version of this extension is incompatible with the current version of GNOME (%s). The extension has been disabled.').format(shellVersion);
|
|
}
|
|
|
|
const message = [
|
|
_('An error has occurred in this extension. This could cause issues elsewhere in the system. It is recommended to turn the extension off until the error is resolved.'),
|
|
];
|
|
|
|
if (this._errorDetail) {
|
|
message.push(
|
|
// translators: Details for an extension error
|
|
_('Error details:'), this._errorDetail);
|
|
}
|
|
|
|
return message.join('\n\n');
|
|
}
|
|
|
|
get hasError() {
|
|
return this.state === ExtensionState.OUT_OF_DATE ||
|
|
this.state === ExtensionState.ERROR;
|
|
}
|
|
|
|
get hasPrefs() {
|
|
return this._hasPrefs;
|
|
}
|
|
|
|
get hasUpdate() {
|
|
return this._hasUpdate;
|
|
}
|
|
|
|
get hasVersion() {
|
|
return this._version !== '';
|
|
}
|
|
|
|
get canChange() {
|
|
return this._canChange;
|
|
}
|
|
|
|
get isUser() {
|
|
return this._type === ExtensionType.PER_USER;
|
|
}
|
|
});
|
|
const {$gtype: TYPE_EXTENSION} = Extension;
|
|
export {TYPE_EXTENSION as Extension};
|
|
|
|
export const ExtensionManager = GObject.registerClass({
|
|
Properties: {
|
|
'user-extensions-enabled': GObject.ParamSpec.boolean(
|
|
'user-extensions-enabled', null, null,
|
|
GObject.ParamFlags.READWRITE,
|
|
true),
|
|
'extensions': GObject.ParamSpec.object(
|
|
'extensions', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
Gio.ListModel),
|
|
'n-updates': GObject.ParamSpec.int(
|
|
'n-updates', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
0, 999, 0),
|
|
'failed': GObject.ParamSpec.boolean(
|
|
'failed', null, null,
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
},
|
|
Signals: {
|
|
'extensions-loaded': {},
|
|
},
|
|
}, class ExtensionManager extends GObject.Object {
|
|
constructor() {
|
|
super();
|
|
|
|
this._extensions = new Gio.ListStore({itemType: Extension});
|
|
|
|
this._proxyReady = false;
|
|
this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
|
|
'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions',
|
|
() => {
|
|
this._proxyReady = true;
|
|
shellVersion = this._shellProxy.ShellVersion;
|
|
|
|
this._shellProxy.connect('notify::g-name-owner',
|
|
() => this.notify('failed'));
|
|
this.notify('failed');
|
|
});
|
|
|
|
this._shellProxy.connect('g-properties-changed', (proxy, properties) => {
|
|
const enabledChanged = !!properties.lookup_value('UserExtensionsEnabled', null);
|
|
if (enabledChanged)
|
|
this.notify('user-extensions-enabled');
|
|
});
|
|
this._shellProxy.connectSignal(
|
|
'ExtensionStateChanged', this._onExtensionStateChanged.bind(this));
|
|
|
|
this._loadExtensions().catch(console.error);
|
|
}
|
|
|
|
get extensions() {
|
|
return this._extensions;
|
|
}
|
|
|
|
get userExtensionsEnabled() {
|
|
return this._shellProxy.UserExtensionsEnabled ?? false;
|
|
}
|
|
|
|
set userExtensionsEnabled(enabled) {
|
|
this._shellProxy.UserExtensionsEnabled = enabled;
|
|
}
|
|
|
|
get nUpdates() {
|
|
let nUpdates = 0;
|
|
for (const ext of this._extensions) {
|
|
if (ext.isUser && ext.hasUpdate)
|
|
nUpdates++;
|
|
}
|
|
return nUpdates;
|
|
}
|
|
|
|
get failed() {
|
|
return this._proxyReady && this._shellProxy.gNameOwner === null;
|
|
}
|
|
|
|
enableExtension(uuid) {
|
|
this._shellProxy.EnableExtensionAsync(uuid).catch(console.error);
|
|
}
|
|
|
|
disableExtension(uuid) {
|
|
this._shellProxy.DisableExtensionAsync(uuid).catch(console.error);
|
|
}
|
|
|
|
uninstallExtension(uuid) {
|
|
this._shellProxy.UninstallExtensionAsync(uuid).catch(console.error);
|
|
}
|
|
|
|
openExtensionPrefs(uuid, parentHandle) {
|
|
this._shellProxy.OpenExtensionPrefsAsync(uuid,
|
|
parentHandle,
|
|
{modal: new GLib.Variant('b', true)}).catch(console.error);
|
|
}
|
|
|
|
checkForUpdates() {
|
|
this._shellProxy.CheckForUpdatesAsync().catch(console.error);
|
|
}
|
|
|
|
async _loadExtensions() {
|
|
const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
|
|
|
|
for (let uuid in extensionsMap) {
|
|
const extension = new Extension(extensionsMap[uuid]);
|
|
this._extensions.append(extension);
|
|
}
|
|
this.emit('extensions-loaded');
|
|
}
|
|
|
|
_findExtension(uuid) {
|
|
const len = this._extensions.get_n_items();
|
|
for (let pos = 0; pos < len; pos++) {
|
|
const extension = this._extensions.get_item(pos);
|
|
if (extension.uuid === uuid)
|
|
return [extension, pos];
|
|
}
|
|
|
|
return [null, -1];
|
|
}
|
|
|
|
_onExtensionStateChanged(p, sender, [uuid, newState]) {
|
|
const [extension, pos] = this._findExtension(uuid);
|
|
|
|
if (extension)
|
|
extension.update(newState);
|
|
|
|
if (!extension)
|
|
this._extensions.append(new Extension(newState));
|
|
else if (extension.state === ExtensionState.UNINSTALLED)
|
|
this._extensions.remove(pos);
|
|
|
|
if (this._updatesCheckId)
|
|
return;
|
|
|
|
this._updatesCheckId = GLib.timeout_add_seconds(
|
|
GLib.PRIORITY_DEFAULT, 1, () => {
|
|
this.notify('n-updates');
|
|
|
|
delete this._updatesCheckId;
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
}
|
|
});
|