296 lines
8.2 KiB
JavaScript
Raw Normal View History

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;
}
class Extension {
constructor(variant) {
this.update(variant);
}
update(variant) {
const deserialized = deserializeExtension(variant);
const {
uuid, type, state, 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}`);
const {name} = metadata;
this._name = name;
[this._keywords] = GLib.str_tokenize_and_fold(name, null);
const [desc] = metadata.description.split('\n');
this._description = desc;
this._type = type;
this._errorDetail = error;
this._state = state;
const creator = metadata.creator ?? '';
this._creator = creator;
const url = metadata.url ?? '';
this._url = url;
const version = String(
metadata['version-name'] || metadata['version'] || '');
this._version = version;
this._hasPrefs = hasPrefs;
this._hasUpdate = hasUpdate;
this._canChange = canChange;
}
get uuid() {
return this._uuid;
}
get name() {
return this._name;
}
get description() {
return this._description;
}
get state() {
return this._state;
}
get creator() {
return this._creator;
}
get url() {
return this._url;
}
get version() {
return this._version;
}
get keywords() {
return this._keywords;
}
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;
}
}
export const ExtensionManager = GObject.registerClass({
Properties: {
'user-extensions-enabled': GObject.ParamSpec.boolean(
'user-extensions-enabled', null, null,
GObject.ParamFlags.READWRITE,
true),
'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: {
'extension-added': {param_types: [GObject.TYPE_JSOBJECT]},
'extension-removed': {param_types: [GObject.TYPE_JSOBJECT]},
'extension-changed': {param_types: [GObject.TYPE_JSOBJECT], flags: GObject.SignalFlags.DETAILED},
'extensions-loaded': {},
},
}, class ExtensionManager extends GObject.Object {
constructor() {
super();
this._extensions = new Map();
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 userExtensionsEnabled() {
return this._shellProxy.UserExtensionsEnabled ?? false;
}
set userExtensionsEnabled(enabled) {
this._shellProxy.UserExtensionsEnabled = enabled;
}
get nUpdates() {
let nUpdates = 0;
for (const ext of this._extensions.values()) {
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);
}
_addExtension(extension) {
const {uuid} = extension;
if (this._extensions.has(uuid))
return;
this._extensions.set(uuid, extension);
this.emit('extension-added', extension);
}
_removeExtension(extension) {
const {uuid} = extension;
if (this._extensions.delete(uuid))
this.emit('extension-removed', extension);
}
async _loadExtensions() {
const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
for (let uuid in extensionsMap) {
const extension = new Extension(extensionsMap[uuid]);
this._addExtension(extension);
}
this.emit('extensions-loaded');
}
_onExtensionStateChanged(p, sender, [uuid, newState]) {
const extension = this._extensions.get(uuid);
if (extension)
extension.update(newState);
if (!extension)
this._addExtension(new Extension(newState));
else if (extension.state === ExtensionState.UNINSTALLED)
this._removeExtension(extension);
else
this.emit(`extension-changed::${uuid}`, extension);
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;
});
}
});