extensions-app: Split out ExtensionManager
The extension handling is currently intertwined with the UI. Splitting it out provides a clearer separation, and will allow us to switch to a model-based UI later. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3067>
This commit is contained in:
parent
3d070fdc6b
commit
f7ded3e509
@ -92,6 +92,7 @@ src/shell-util.c
|
|||||||
src/st/st-icon-theme.c
|
src/st/st-icon-theme.c
|
||||||
subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in
|
subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in
|
||||||
subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in
|
subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in
|
||||||
|
subprojects/extensions-app/js/extensionManager.js
|
||||||
subprojects/extensions-app/js/extensionRow.js
|
subprojects/extensions-app/js/extensionRow.js
|
||||||
subprojects/extensions-app/js/extensionsWindow.js
|
subprojects/extensions-app/js/extensionsWindow.js
|
||||||
subprojects/extensions-app/data/ui/extension-row.ui
|
subprojects/extensions-app/data/ui/extension-row.ui
|
||||||
|
295
subprojects/extensions-app/js/extensionManager.js
Normal file
295
subprojects/extensions-app/js/extensionManager.js
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -3,7 +3,7 @@ import GLib from 'gi://GLib';
|
|||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
import GObject from 'gi://GObject';
|
import GObject from 'gi://GObject';
|
||||||
|
|
||||||
import {ExtensionState, ExtensionType, deserializeExtension} from './misc/extensionUtils.js';
|
import {ExtensionState} from './misc/extensionUtils.js';
|
||||||
|
|
||||||
export const ExtensionRow = GObject.registerClass({
|
export const ExtensionRow = GObject.registerClass({
|
||||||
GTypeName: 'ExtensionRow',
|
GTypeName: 'ExtensionRow',
|
||||||
@ -25,40 +25,38 @@ export const ExtensionRow = GObject.registerClass({
|
|||||||
this._app = Gio.Application.get_default();
|
this._app = Gio.Application.get_default();
|
||||||
this._extension = extension;
|
this._extension = extension;
|
||||||
|
|
||||||
[this._keywords] = GLib.str_tokenize_and_fold(this.name, null);
|
|
||||||
|
|
||||||
this._actionGroup = new Gio.SimpleActionGroup();
|
this._actionGroup = new Gio.SimpleActionGroup();
|
||||||
this.insert_action_group('row', this._actionGroup);
|
this.insert_action_group('row', this._actionGroup);
|
||||||
|
|
||||||
let action;
|
let action;
|
||||||
action = new Gio.SimpleAction({
|
action = new Gio.SimpleAction({
|
||||||
name: 'show-prefs',
|
name: 'show-prefs',
|
||||||
enabled: this.hasPrefs,
|
enabled: extension.hasPrefs,
|
||||||
});
|
});
|
||||||
action.connect('activate', () => {
|
action.connect('activate', () => {
|
||||||
this._detailsPopover.popdown();
|
this._detailsPopover.popdown();
|
||||||
this.get_root().openPrefs(this.uuid);
|
this.get_root().openPrefs(extension);
|
||||||
});
|
});
|
||||||
this._actionGroup.add_action(action);
|
this._actionGroup.add_action(action);
|
||||||
|
|
||||||
action = new Gio.SimpleAction({
|
action = new Gio.SimpleAction({
|
||||||
name: 'show-url',
|
name: 'show-url',
|
||||||
enabled: this.url !== '',
|
enabled: extension.url !== '',
|
||||||
});
|
});
|
||||||
action.connect('activate', () => {
|
action.connect('activate', () => {
|
||||||
this._detailsPopover.popdown();
|
this._detailsPopover.popdown();
|
||||||
Gio.AppInfo.launch_default_for_uri(
|
Gio.AppInfo.launch_default_for_uri(
|
||||||
this.url, this.get_display().get_app_launch_context());
|
extension.url, this.get_display().get_app_launch_context());
|
||||||
});
|
});
|
||||||
this._actionGroup.add_action(action);
|
this._actionGroup.add_action(action);
|
||||||
|
|
||||||
action = new Gio.SimpleAction({
|
action = new Gio.SimpleAction({
|
||||||
name: 'uninstall',
|
name: 'uninstall',
|
||||||
enabled: this.type === ExtensionType.PER_USER,
|
enabled: extension.isUser,
|
||||||
});
|
});
|
||||||
action.connect('activate', () => {
|
action.connect('activate', () => {
|
||||||
this._detailsPopover.popdown();
|
this._detailsPopover.popdown();
|
||||||
this.get_root().uninstall(this.uuid);
|
this.get_root().uninstall(extension);
|
||||||
});
|
});
|
||||||
this._actionGroup.add_action(action);
|
this._actionGroup.add_action(action);
|
||||||
|
|
||||||
@ -70,97 +68,28 @@ export const ExtensionRow = GObject.registerClass({
|
|||||||
const state = action.get_state();
|
const state = action.get_state();
|
||||||
action.change_state(new GLib.Variant('b', !state.get_boolean()));
|
action.change_state(new GLib.Variant('b', !state.get_boolean()));
|
||||||
});
|
});
|
||||||
|
|
||||||
action.connect('change-state', (a, state) => {
|
action.connect('change-state', (a, state) => {
|
||||||
|
const {uuid} = this._extension;
|
||||||
if (state.get_boolean())
|
if (state.get_boolean())
|
||||||
this._app.shellProxy.EnableExtensionAsync(this.uuid).catch(console.error);
|
this._app.extensionManager.enableExtension(uuid);
|
||||||
else
|
else
|
||||||
this._app.shellProxy.DisableExtensionAsync(this.uuid).catch(console.error);
|
this._app.extensionManager.disableExtension(uuid);
|
||||||
});
|
});
|
||||||
this._actionGroup.add_action(action);
|
this._actionGroup.add_action(action);
|
||||||
|
|
||||||
this.title = this.name;
|
this.title = extension.name;
|
||||||
|
|
||||||
const desc = this._extension.metadata.description.split('\n')[0];
|
this._descriptionLabel.label = extension.description;
|
||||||
this._descriptionLabel.label = desc;
|
|
||||||
|
|
||||||
this.connect('destroy', this._onDestroy.bind(this));
|
this.connect('destroy', this._onDestroy.bind(this));
|
||||||
|
|
||||||
this._extensionStateChangedId = this._app.shellProxy.connectSignal(
|
this._extensionStateChangedId = this._app.extensionManager.connect(
|
||||||
'ExtensionStateChanged', (p, sender, [uuid, newState]) => {
|
`extension-changed::${extension.uuid}`, () => this._updateState());
|
||||||
if (this.uuid !== uuid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._extension = deserializeExtension(newState);
|
|
||||||
this._updateState();
|
|
||||||
});
|
|
||||||
this._updateState();
|
this._updateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
get uuid() {
|
get extension() {
|
||||||
return this._extension.uuid;
|
return this._extension;
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this._extension.metadata.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasPrefs() {
|
|
||||||
return this._extension.hasPrefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasUpdate() {
|
|
||||||
return this._extension.hasUpdate || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasError() {
|
|
||||||
const {state} = this._extension;
|
|
||||||
return state === ExtensionState.OUT_OF_DATE ||
|
|
||||||
state === ExtensionState.ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
get type() {
|
|
||||||
return this._extension.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
get creator() {
|
|
||||||
return this._extension.metadata.creator || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
get url() {
|
|
||||||
return this._extension.metadata.url || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
get version() {
|
|
||||||
return this._extension.metadata['version-name'] || this._extension.metadata.version || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
get error() {
|
|
||||||
if (!this.hasError)
|
|
||||||
return '';
|
|
||||||
|
|
||||||
if (this._extension.state === ExtensionState.OUT_OF_DATE) {
|
|
||||||
const {ShellVersion: shellVersion} = this._app.shellProxy;
|
|
||||||
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._extension.error) {
|
|
||||||
message.push(
|
|
||||||
// translators: Details for an extension error
|
|
||||||
_('Error details:'), this._extension.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return message.join('\n\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
get keywords() {
|
|
||||||
return this._keywords;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateState() {
|
_updateState() {
|
||||||
@ -173,21 +102,17 @@ export const ExtensionRow = GObject.registerClass({
|
|||||||
if (!action.enabled)
|
if (!action.enabled)
|
||||||
this._switch.active = state;
|
this._switch.active = state;
|
||||||
|
|
||||||
this._updatesButton.visible = this.hasUpdate;
|
this._updatesButton.visible = this._extension.hasUpdate;
|
||||||
this._errorButton.visible = this.hasError;
|
this._errorButton.visible = this._extension.hasError;
|
||||||
this._errorLabel.label = this.error;
|
this._errorLabel.label = this._extension.error;
|
||||||
|
|
||||||
this._versionLabel.label = _('Version %s').format(this.version.toString());
|
this._versionLabel.label = _('Version %s').format(this._extension.version);
|
||||||
this._versionLabel.visible = this.version !== '';
|
this._versionLabel.visible = this._extension.version !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDestroy() {
|
_onDestroy() {
|
||||||
if (!this._app.shellProxy)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (this._extensionStateChangedId)
|
if (this._extensionStateChangedId)
|
||||||
this._app.shellProxy.disconnectSignal(this._extensionStateChangedId);
|
this._app.extensionManager.disconnect(this._extensionStateChangedId);
|
||||||
this._extensionStateChangedId = 0;
|
delete this._extensionStateChangedId;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import * as Gettext from 'gettext';
|
|||||||
|
|
||||||
import * as Config from './misc/config.js';
|
import * as Config from './misc/config.js';
|
||||||
import {ExtensionRow} from './extensionRow.js';
|
import {ExtensionRow} from './extensionRow.js';
|
||||||
import {ExtensionState, ExtensionType, deserializeExtension} from './misc/extensionUtils.js';
|
|
||||||
|
|
||||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||||
Gio._promisify(Shew.WindowExporter.prototype, 'export');
|
Gio._promisify(Shew.WindowExporter.prototype, 'export');
|
||||||
@ -36,8 +35,6 @@ export const ExtensionsWindow = GObject.registerClass({
|
|||||||
if (Config.PROFILE === 'development')
|
if (Config.PROFILE === 'development')
|
||||||
this.add_css_class('devel');
|
this.add_css_class('devel');
|
||||||
|
|
||||||
this._updatesCheckId = 0;
|
|
||||||
|
|
||||||
this._exporter = new Shew.WindowExporter({window: this});
|
this._exporter = new Shew.WindowExporter({window: this});
|
||||||
this._exportedHandle = '';
|
this._exportedHandle = '';
|
||||||
|
|
||||||
@ -52,7 +49,8 @@ export const ExtensionsWindow = GObject.registerClass({
|
|||||||
name: 'user-extensions-enabled',
|
name: 'user-extensions-enabled',
|
||||||
state: 'false',
|
state: 'false',
|
||||||
change_state: (a, state) => {
|
change_state: (a, state) => {
|
||||||
this._shellProxy.UserExtensionsEnabled = state.get_boolean();
|
const {extensionManager} = this.application;
|
||||||
|
extensionManager.userExtensionsEnabled = state.get_boolean();
|
||||||
},
|
},
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
@ -90,27 +88,38 @@ export const ExtensionsWindow = GObject.registerClass({
|
|||||||
}));
|
}));
|
||||||
this._systemList.connect('row-activated', (_list, row) => row.activate());
|
this._systemList.connect('row-activated', (_list, row) => row.activate());
|
||||||
|
|
||||||
this._shellProxy.connectSignal('ExtensionStateChanged',
|
const {extensionManager} = this.application;
|
||||||
this._onExtensionStateChanged.bind(this));
|
extensionManager.connect('notify::failed',
|
||||||
|
() => this._syncVisiblePage());
|
||||||
this._shellProxy.connect('g-properties-changed',
|
extensionManager.connect('notify::n-updates',
|
||||||
|
() => this._checkUpdates());
|
||||||
|
extensionManager.connect('notify::user-extensions-enabled',
|
||||||
this._onUserExtensionsEnabledChanged.bind(this));
|
this._onUserExtensionsEnabledChanged.bind(this));
|
||||||
this._onUserExtensionsEnabledChanged();
|
this._onUserExtensionsEnabledChanged();
|
||||||
|
|
||||||
this._scanExtensions();
|
extensionManager.connect('extension-added',
|
||||||
|
(mgr, extension) => this._addExtensionRow(extension));
|
||||||
|
extensionManager.connect('extension-removed',
|
||||||
|
(mgr, extension) => this._removeExtensionRow(extension));
|
||||||
|
extensionManager.connect('extension-changed',
|
||||||
|
(mgr, extension) => {
|
||||||
|
const row = this._findExtensionRow(extension);
|
||||||
|
const isUser = row?.get_parent() === this._userList;
|
||||||
|
if (extension.isUser !== isUser) {
|
||||||
|
this._removeExtensionRow(extension);
|
||||||
|
this._addExtensionRow(extension);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionManager.connect('extensions-loaded',
|
||||||
|
() => this._extensionsLoaded());
|
||||||
}
|
}
|
||||||
|
|
||||||
get _shellProxy() {
|
uninstall(extension) {
|
||||||
return this.application.shellProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
uninstall(uuid) {
|
|
||||||
const row = this._findExtensionRow(uuid);
|
|
||||||
|
|
||||||
const dialog = new Gtk.MessageDialog({
|
const dialog = new Gtk.MessageDialog({
|
||||||
transient_for: this,
|
transient_for: this,
|
||||||
modal: true,
|
modal: true,
|
||||||
text: _('Remove “%s”?').format(row.name),
|
text: _('Remove “%s”?').format(extension.name),
|
||||||
secondary_text: _('If you remove the extension, you need to return to download it if you want to enable it again'),
|
secondary_text: _('If you remove the extension, you need to return to download it if you want to enable it again'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,14 +128,16 @@ export const ExtensionsWindow = GObject.registerClass({
|
|||||||
.get_style_context().add_class('destructive-action');
|
.get_style_context().add_class('destructive-action');
|
||||||
|
|
||||||
dialog.connect('response', (dlg, response) => {
|
dialog.connect('response', (dlg, response) => {
|
||||||
|
const {extensionManager} = this.application;
|
||||||
|
|
||||||
if (response === Gtk.ResponseType.ACCEPT)
|
if (response === Gtk.ResponseType.ACCEPT)
|
||||||
this._shellProxy.UninstallExtensionAsync(uuid).catch(console.error);
|
extensionManager.uninstallExtension(extension.uuid);
|
||||||
dialog.destroy();
|
dialog.destroy();
|
||||||
});
|
});
|
||||||
dialog.present();
|
dialog.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
async openPrefs(uuid) {
|
async openPrefs(extension) {
|
||||||
if (!this._exportedHandle) {
|
if (!this._exportedHandle) {
|
||||||
try {
|
try {
|
||||||
this._exportedHandle = await this._exporter.export();
|
this._exportedHandle = await this._exporter.export();
|
||||||
@ -135,9 +146,8 @@ export const ExtensionsWindow = GObject.registerClass({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._shellProxy.OpenExtensionPrefsAsync(uuid,
|
const {extensionManager} = this.application;
|
||||||
this._exportedHandle,
|
extensionManager.openExtensionPrefs(extension.uuid, this._exportedHandle);
|
||||||
{modal: new GLib.Variant('b', true)}).catch(console.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_showAbout() {
|
_showAbout() {
|
||||||
@ -180,104 +190,68 @@ export const ExtensionsWindow = GObject.registerClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
_sortList(row1, row2) {
|
_sortList(row1, row2) {
|
||||||
return row1.name.localeCompare(row2.name);
|
const {name: name1} = row1.extension;
|
||||||
|
const {name: name2} = row2.extension;
|
||||||
|
return name1.localeCompare(name2);
|
||||||
}
|
}
|
||||||
|
|
||||||
_filterList(row) {
|
_filterList(row) {
|
||||||
|
const {keywords} = row.extension;
|
||||||
return this._searchTerms.every(
|
return this._searchTerms.every(
|
||||||
t => row.keywords.some(k => k.startsWith(t)));
|
t => keywords.some(k => k.startsWith(t)));
|
||||||
}
|
}
|
||||||
|
|
||||||
_findExtensionRow(uuid) {
|
_findExtensionRow(extension) {
|
||||||
return [
|
return [
|
||||||
...this._userList,
|
...this._userList,
|
||||||
...this._systemList,
|
...this._systemList,
|
||||||
].find(c => c.uuid === uuid);
|
].find(c => c.extension === extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserExtensionsEnabledChanged() {
|
_onUserExtensionsEnabledChanged() {
|
||||||
|
const {userExtensionsEnabled} = this.application.extensionManager;
|
||||||
const action = this.lookup_action('user-extensions-enabled');
|
const action = this.lookup_action('user-extensions-enabled');
|
||||||
action.set_state(
|
action.set_state(new GLib.Variant('b', userExtensionsEnabled));
|
||||||
new GLib.Variant('b', this._shellProxy.UserExtensionsEnabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
_onExtensionStateChanged(proxy, senderName, [uuid, newState]) {
|
|
||||||
const extension = deserializeExtension(newState);
|
|
||||||
let row = this._findExtensionRow(uuid);
|
|
||||||
|
|
||||||
this._queueUpdatesCheck();
|
|
||||||
|
|
||||||
// the extension's type changed; remove the corresponding row
|
|
||||||
// and reset the variable to null so that we create a new row
|
|
||||||
// below and add it to the appropriate list
|
|
||||||
if (row && row.type !== extension.type) {
|
|
||||||
row.get_parent().remove(row);
|
|
||||||
row = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row) {
|
|
||||||
if (extension.state === ExtensionState.UNINSTALLED)
|
|
||||||
row.get_parent().remove(row);
|
|
||||||
} else {
|
|
||||||
this._addExtensionRow(extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._syncListVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _scanExtensions() {
|
|
||||||
try {
|
|
||||||
const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
|
|
||||||
|
|
||||||
for (let uuid in extensionsMap) {
|
|
||||||
const extension = deserializeExtension(extensionsMap[uuid]);
|
|
||||||
this._addExtensionRow(extension);
|
|
||||||
}
|
|
||||||
this._extensionsLoaded();
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Gio.DBusError) {
|
|
||||||
console.log(`Failed to connect to shell proxy: ${e}`);
|
|
||||||
this._mainStack.visible_child_name = 'noshell';
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_addExtensionRow(extension) {
|
_addExtensionRow(extension) {
|
||||||
const row = new ExtensionRow(extension);
|
const row = new ExtensionRow(extension);
|
||||||
|
|
||||||
if (row.type === ExtensionType.PER_USER)
|
if (extension.isUser)
|
||||||
this._userList.append(row);
|
this._userList.append(row);
|
||||||
else
|
else
|
||||||
this._systemList.append(row);
|
this._systemList.append(row);
|
||||||
|
|
||||||
|
this._syncListVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
_queueUpdatesCheck() {
|
_removeExtensionRow(extension) {
|
||||||
if (this._updatesCheckId)
|
const row = this._findExtensionRow(extension);
|
||||||
return;
|
if (row)
|
||||||
|
row.get_parent().remove(row);
|
||||||
this._updatesCheckId = GLib.timeout_add_seconds(
|
this._syncListVisibility();
|
||||||
GLib.PRIORITY_DEFAULT, 1, () => {
|
|
||||||
this._checkUpdates();
|
|
||||||
|
|
||||||
this._updatesCheckId = 0;
|
|
||||||
return GLib.SOURCE_REMOVE;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncListVisibility() {
|
_syncListVisibility() {
|
||||||
this._userGroup.visible = [...this._userList].length > 1;
|
this._userGroup.visible = [...this._userList].length > 1;
|
||||||
this._systemGroup.visible = [...this._systemList].length > 1;
|
this._systemGroup.visible = [...this._systemList].length > 1;
|
||||||
|
|
||||||
if (this._userGroup.visible || this._systemGroup.visible)
|
this._syncVisiblePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncVisiblePage() {
|
||||||
|
const {extensionManager} = this.application;
|
||||||
|
|
||||||
|
if (extensionManager.failed)
|
||||||
|
this._mainStack.visible_child_name = 'noshell';
|
||||||
|
else if (this._userGroup.visible || this._systemGroup.visible)
|
||||||
this._mainStack.visible_child_name = 'main';
|
this._mainStack.visible_child_name = 'main';
|
||||||
else
|
else
|
||||||
this._mainStack.visible_child_name = 'placeholder';
|
this._mainStack.visible_child_name = 'placeholder';
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkUpdates() {
|
_checkUpdates() {
|
||||||
const nUpdates = [...this._userList].filter(c => c.hasUpdate).length;
|
const {nUpdates} = this.application.extensionManager;
|
||||||
|
|
||||||
this._updatesBanner.title = Gettext.ngettext(
|
this._updatesBanner.title = Gettext.ngettext(
|
||||||
'%d extension will be updated on next login.',
|
'%d extension will be updated on next login.',
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Adw from 'gi://Adw?version=1';
|
import Adw from 'gi://Adw?version=1';
|
||||||
import GLib from 'gi://GLib';
|
import GLib from 'gi://GLib';
|
||||||
import Gio from 'gi://Gio';
|
|
||||||
import GObject from 'gi://GObject';
|
import GObject from 'gi://GObject';
|
||||||
|
|
||||||
import {setConsoleLogDomain} from 'console';
|
import {setConsoleLogDomain} from 'console';
|
||||||
@ -8,25 +7,9 @@ const Package = imports.package;
|
|||||||
|
|
||||||
Package.initFormat();
|
Package.initFormat();
|
||||||
|
|
||||||
|
import {ExtensionManager} from './extensionManager.js';
|
||||||
import {ExtensionsWindow} from './extensionsWindow.js';
|
import {ExtensionsWindow} from './extensionsWindow.js';
|
||||||
|
|
||||||
const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions');
|
|
||||||
const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
var Application = GObject.registerClass(
|
var Application = GObject.registerClass(
|
||||||
class Application extends Adw.Application {
|
class Application extends Adw.Application {
|
||||||
_init() {
|
_init() {
|
||||||
@ -36,12 +19,12 @@ class Application extends Adw.Application {
|
|||||||
this.connect('window-removed', (a, window) => window.run_dispose());
|
this.connect('window-removed', (a, window) => window.run_dispose());
|
||||||
}
|
}
|
||||||
|
|
||||||
get shellProxy() {
|
get extensionManager() {
|
||||||
return this._shellProxy;
|
return this._extensionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
vfunc_activate() {
|
vfunc_activate() {
|
||||||
this._shellProxy.CheckForUpdatesAsync().catch(console.error);
|
this._extensionManager.checkForUpdates();
|
||||||
this._window.present();
|
this._window.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,8 +39,7 @@ class Application extends Adw.Application {
|
|||||||
|
|
||||||
this.set_accels_for_action('app.quit', ['<Primary>q']);
|
this.set_accels_for_action('app.quit', ['<Primary>q']);
|
||||||
|
|
||||||
this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
|
this._extensionManager = new ExtensionManager();
|
||||||
'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions');
|
|
||||||
|
|
||||||
this._window = new ExtensionsWindow({application: this});
|
this._window = new ExtensionsWindow({application: this});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/org/gnome/Extensions@profile@/js">
|
<gresource prefix="/org/gnome/Extensions@profile@/js">
|
||||||
|
<file>extensionManager.js</file>
|
||||||
<file>extensionRow.js</file>
|
<file>extensionRow.js</file>
|
||||||
<file>extensionsWindow.js</file>
|
<file>extensionsWindow.js</file>
|
||||||
<file>main.js</file>
|
<file>main.js</file>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user