7a9bfa2744
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;
|
|
});
|
|
}
|
|
});
|