80ac32a268
The extensions app is another part of the code base that is largely separate from the main code base, and can therefore be ported to ESM before the big switch. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2786>
555 lines
16 KiB
JavaScript
555 lines
16 KiB
JavaScript
import Adw from 'gi://Adw?version=1';
|
|
import GLib from 'gi://GLib';
|
|
import Gio from 'gi://Gio';
|
|
import GObject from 'gi://GObject';
|
|
import Gtk from 'gi://Gtk?version=4.0';
|
|
import Shew from 'gi://Shew';
|
|
|
|
import * as Gettext from 'gettext';
|
|
const Package = imports.package;
|
|
|
|
Package.initFormat();
|
|
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
|
|
|
const { ExtensionState, ExtensionType } = ExtensionUtils;
|
|
|
|
const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions');
|
|
const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
|
|
|
|
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
|
Gio._promisify(Shew.WindowExporter.prototype, 'export');
|
|
|
|
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) {
|
|
log(`Failed to load D-Bus interface ${iface}`);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function toggleState(action) {
|
|
let state = action.get_state();
|
|
action.change_state(new GLib.Variant('b', !state.get_boolean()));
|
|
}
|
|
|
|
var Application = GObject.registerClass(
|
|
class Application extends Adw.Application {
|
|
_init() {
|
|
GLib.set_prgname('gnome-extensions-app');
|
|
super._init({ application_id: Package.name });
|
|
|
|
this.connect('window-removed', (a, window) => window.run_dispose());
|
|
}
|
|
|
|
get shellProxy() {
|
|
return this._shellProxy;
|
|
}
|
|
|
|
vfunc_activate() {
|
|
this._shellProxy.CheckForUpdatesAsync().catch(logError);
|
|
this._window.present();
|
|
}
|
|
|
|
vfunc_startup() {
|
|
super.vfunc_startup();
|
|
|
|
this.add_action_entries(
|
|
[{
|
|
name: 'quit',
|
|
activate: () => this._window.close(),
|
|
}]);
|
|
|
|
this.set_accels_for_action('app.quit', ['<Primary>q']);
|
|
|
|
this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
|
|
'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions');
|
|
|
|
this._window = new ExtensionsWindow({ application: this });
|
|
}
|
|
});
|
|
|
|
var ExtensionsWindow = GObject.registerClass({
|
|
GTypeName: 'ExtensionsWindow',
|
|
Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui',
|
|
InternalChildren: [
|
|
'userGroup',
|
|
'userList',
|
|
'systemGroup',
|
|
'systemList',
|
|
'mainStack',
|
|
'searchBar',
|
|
'searchButton',
|
|
'searchEntry',
|
|
'updatesBar',
|
|
'updatesLabel',
|
|
],
|
|
}, class ExtensionsWindow extends Adw.ApplicationWindow {
|
|
_init(params) {
|
|
super._init(params);
|
|
|
|
this._updatesCheckId = 0;
|
|
|
|
this._exporter = new Shew.WindowExporter({ window: this });
|
|
this._exportedHandle = '';
|
|
|
|
this.add_action_entries(
|
|
[{
|
|
name: 'show-about',
|
|
activate: () => this._showAbout(),
|
|
}, {
|
|
name: 'logout',
|
|
activate: () => this._logout(),
|
|
}, {
|
|
name: 'user-extensions-enabled',
|
|
state: 'false',
|
|
change_state: (a, state) => {
|
|
this._shellProxy.UserExtensionsEnabled = state.get_boolean();
|
|
},
|
|
}]);
|
|
|
|
this._searchTerms = [];
|
|
this._searchEntry.connect('search-changed', () => {
|
|
const { text } = this._searchEntry;
|
|
if (text === '')
|
|
this._searchTerms = [];
|
|
else
|
|
[this._searchTerms] = GLib.str_tokenize_and_fold(text, null);
|
|
|
|
this._userList.invalidate_filter();
|
|
this._systemList.invalidate_filter();
|
|
});
|
|
|
|
this._userList.set_sort_func(this._sortList.bind(this));
|
|
this._userList.set_filter_func(this._filterList.bind(this));
|
|
this._userList.set_placeholder(new Gtk.Label({
|
|
label: _('No Matches'),
|
|
margin_start: 12,
|
|
margin_end: 12,
|
|
margin_top: 12,
|
|
margin_bottom: 12,
|
|
}));
|
|
this._userList.connect('row-activated', (_list, row) => row.activate());
|
|
|
|
this._systemList.set_sort_func(this._sortList.bind(this));
|
|
this._systemList.set_filter_func(this._filterList.bind(this));
|
|
this._systemList.set_placeholder(new Gtk.Label({
|
|
label: _('No Matches'),
|
|
margin_start: 12,
|
|
margin_end: 12,
|
|
margin_top: 12,
|
|
margin_bottom: 12,
|
|
}));
|
|
this._systemList.connect('row-activated', (_list, row) => row.activate());
|
|
|
|
this._shellProxy.connectSignal('ExtensionStateChanged',
|
|
this._onExtensionStateChanged.bind(this));
|
|
|
|
this._shellProxy.connect('g-properties-changed',
|
|
this._onUserExtensionsEnabledChanged.bind(this));
|
|
this._onUserExtensionsEnabledChanged();
|
|
|
|
this._scanExtensions();
|
|
}
|
|
|
|
get _shellProxy() {
|
|
return this.application.shellProxy;
|
|
}
|
|
|
|
uninstall(uuid) {
|
|
let row = this._findExtensionRow(uuid);
|
|
|
|
let dialog = new Gtk.MessageDialog({
|
|
transient_for: this,
|
|
modal: true,
|
|
text: _('Remove “%s”?').format(row.name),
|
|
secondary_text: _('If you remove the extension, you need to return to download it if you want to enable it again'),
|
|
});
|
|
|
|
dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
|
|
dialog.add_button(_('Remove'), Gtk.ResponseType.ACCEPT)
|
|
.get_style_context().add_class('destructive-action');
|
|
|
|
dialog.connect('response', (dlg, response) => {
|
|
if (response === Gtk.ResponseType.ACCEPT)
|
|
this._shellProxy.UninstallExtensionAsync(uuid).catch(logError);
|
|
dialog.destroy();
|
|
});
|
|
dialog.present();
|
|
}
|
|
|
|
async openPrefs(uuid) {
|
|
if (!this._exportedHandle) {
|
|
try {
|
|
this._exportedHandle = await this._exporter.export();
|
|
} catch (e) {
|
|
log(`Failed to export window: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
this._shellProxy.OpenExtensionPrefsAsync(uuid,
|
|
this._exportedHandle,
|
|
{modal: new GLib.Variant('b', true)}).catch(logError);
|
|
}
|
|
|
|
_showAbout() {
|
|
let aboutWindow = new Adw.AboutWindow({
|
|
developers: [
|
|
'Florian Müllner <fmuellner@gnome.org>',
|
|
'Jasper St. Pierre <jstpierre@mecheye.net>',
|
|
'Didier Roche <didrocks@ubuntu.com>',
|
|
'Romain Vigier <contact@romainvigier.fr>',
|
|
],
|
|
designers: [
|
|
'Allan Day <allanpday@gmail.com>',
|
|
'Tobias Bernard <tbernard@gnome.org>',
|
|
],
|
|
translator_credits: _('translator-credits'),
|
|
application_name: _('Extensions'),
|
|
license_type: Gtk.License.GPL_2_0,
|
|
application_icon: Package.name,
|
|
version: Package.version,
|
|
developer_name: _('The GNOME Project'),
|
|
website: 'https://apps.gnome.org/app/org.gnome.Extensions/',
|
|
issue_url: 'https://gitlab.gnome.org/GNOME/gnome-shell/issues/new',
|
|
|
|
transient_for: this,
|
|
});
|
|
aboutWindow.present();
|
|
}
|
|
|
|
_logout() {
|
|
this.application.get_dbus_connection().call(
|
|
'org.gnome.SessionManager',
|
|
'/org/gnome/SessionManager',
|
|
'org.gnome.SessionManager',
|
|
'Logout',
|
|
new GLib.Variant('(u)', [0]),
|
|
null,
|
|
Gio.DBusCallFlags.NONE,
|
|
-1,
|
|
null);
|
|
}
|
|
|
|
_sortList(row1, row2) {
|
|
return row1.name.localeCompare(row2.name);
|
|
}
|
|
|
|
_filterList(row) {
|
|
return this._searchTerms.every(
|
|
t => row.keywords.some(k => k.startsWith(t)));
|
|
}
|
|
|
|
_findExtensionRow(uuid) {
|
|
return [
|
|
...this._userList,
|
|
...this._systemList,
|
|
].find(c => c.uuid === uuid);
|
|
}
|
|
|
|
_onUserExtensionsEnabledChanged() {
|
|
let action = this.lookup_action('user-extensions-enabled');
|
|
action.set_state(
|
|
new GLib.Variant('b', this._shellProxy.UserExtensionsEnabled));
|
|
}
|
|
|
|
_onExtensionStateChanged(proxy, senderName, [uuid, newState]) {
|
|
let extension = ExtensionUtils.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) {
|
|
let extension = ExtensionUtils.deserializeExtension(extensionsMap[uuid]);
|
|
this._addExtensionRow(extension);
|
|
}
|
|
this._extensionsLoaded();
|
|
} catch (e) {
|
|
if (e instanceof Gio.DBusError) {
|
|
log(`Failed to connect to shell proxy: ${e}`);
|
|
this._mainStack.visible_child_name = 'noshell';
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
_addExtensionRow(extension) {
|
|
let row = new ExtensionRow(extension);
|
|
|
|
if (row.type === ExtensionType.PER_USER)
|
|
this._userList.append(row);
|
|
else
|
|
this._systemList.append(row);
|
|
}
|
|
|
|
_queueUpdatesCheck() {
|
|
if (this._updatesCheckId)
|
|
return;
|
|
|
|
this._updatesCheckId = GLib.timeout_add_seconds(
|
|
GLib.PRIORITY_DEFAULT, 1, () => {
|
|
this._checkUpdates();
|
|
|
|
this._updatesCheckId = 0;
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
}
|
|
|
|
_syncListVisibility() {
|
|
this._userGroup.visible = [...this._userList].length > 1;
|
|
this._systemGroup.visible = [...this._systemList].length > 1;
|
|
|
|
if (this._userGroup.visible || this._systemGroup.visible)
|
|
this._mainStack.visible_child_name = 'main';
|
|
else
|
|
this._mainStack.visible_child_name = 'placeholder';
|
|
}
|
|
|
|
_checkUpdates() {
|
|
let nUpdates = [...this._userList].filter(c => c.hasUpdate).length;
|
|
|
|
this._updatesLabel.label = Gettext.ngettext(
|
|
'%d extension will be updated on next login.',
|
|
'%d extensions will be updated on next login.',
|
|
nUpdates).format(nUpdates);
|
|
this._updatesBar.revealed = nUpdates > 0;
|
|
}
|
|
|
|
_extensionsLoaded() {
|
|
this._syncListVisibility();
|
|
this._checkUpdates();
|
|
}
|
|
});
|
|
|
|
var ExtensionRow = GObject.registerClass({
|
|
GTypeName: 'ExtensionRow',
|
|
Template: 'resource:///org/gnome/Extensions/ui/extension-row.ui',
|
|
InternalChildren: [
|
|
'nameLabel',
|
|
'descriptionLabel',
|
|
'versionLabel',
|
|
'errorLabel',
|
|
'errorIcon',
|
|
'updatesIcon',
|
|
'switch',
|
|
'actionsBox',
|
|
],
|
|
}, class ExtensionRow extends Gtk.ListBoxRow {
|
|
_init(extension) {
|
|
super._init();
|
|
|
|
this._app = Gio.Application.get_default();
|
|
this._extension = extension;
|
|
this._prefsModule = null;
|
|
|
|
[this._keywords] = GLib.str_tokenize_and_fold(this.name, null);
|
|
|
|
this._actionGroup = new Gio.SimpleActionGroup();
|
|
this.insert_action_group('row', this._actionGroup);
|
|
|
|
let action;
|
|
action = new Gio.SimpleAction({
|
|
name: 'show-prefs',
|
|
enabled: this.hasPrefs,
|
|
});
|
|
action.connect('activate', () => this.get_root().openPrefs(this.uuid));
|
|
this._actionGroup.add_action(action);
|
|
|
|
action = new Gio.SimpleAction({
|
|
name: 'show-url',
|
|
enabled: this.url !== '',
|
|
});
|
|
action.connect('activate', () => {
|
|
Gio.AppInfo.launch_default_for_uri(
|
|
this.url, this.get_display().get_app_launch_context());
|
|
});
|
|
this._actionGroup.add_action(action);
|
|
|
|
action = new Gio.SimpleAction({
|
|
name: 'uninstall',
|
|
enabled: this.type === ExtensionType.PER_USER,
|
|
});
|
|
action.connect('activate', () => this.get_root().uninstall(this.uuid));
|
|
this._actionGroup.add_action(action);
|
|
|
|
action = new Gio.SimpleAction({
|
|
name: 'enabled',
|
|
state: new GLib.Variant('b', false),
|
|
});
|
|
action.connect('activate', toggleState);
|
|
action.connect('change-state', (a, state) => {
|
|
if (state.get_boolean())
|
|
this._app.shellProxy.EnableExtensionAsync(this.uuid).catch(logError);
|
|
else
|
|
this._app.shellProxy.DisableExtensionAsync(this.uuid).catch(logError);
|
|
});
|
|
this._actionGroup.add_action(action);
|
|
|
|
this._nameLabel.label = this.name;
|
|
|
|
const desc = this._extension.metadata.description.split('\n')[0];
|
|
this._descriptionLabel.label = desc;
|
|
this._descriptionLabel.tooltip_text = desc;
|
|
|
|
this.connect('destroy', this._onDestroy.bind(this));
|
|
|
|
this._extensionStateChangedId = this._app.shellProxy.connectSignal(
|
|
'ExtensionStateChanged', (p, sender, [uuid, newState]) => {
|
|
if (this.uuid !== uuid)
|
|
return;
|
|
|
|
this._extension = ExtensionUtils.deserializeExtension(newState);
|
|
this._updateState();
|
|
});
|
|
this._updateState();
|
|
}
|
|
|
|
vfunc_activate() {
|
|
this._switch.mnemonic_activate(false);
|
|
}
|
|
|
|
get uuid() {
|
|
return this._extension.uuid;
|
|
}
|
|
|
|
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 || '';
|
|
}
|
|
|
|
get error() {
|
|
if (!this.hasError)
|
|
return '';
|
|
|
|
if (this._extension.state === ExtensionState.OUT_OF_DATE)
|
|
return _('The extension is incompatible with the current GNOME version');
|
|
|
|
return this._extension.error
|
|
? this._extension.error : _('The extension had an error');
|
|
}
|
|
|
|
get keywords() {
|
|
return this._keywords;
|
|
}
|
|
|
|
_updateState() {
|
|
let state = this._extension.state === ExtensionState.ENABLED;
|
|
|
|
let action = this._actionGroup.lookup('enabled');
|
|
action.set_state(new GLib.Variant('b', state));
|
|
action.enabled = this._canToggle();
|
|
|
|
if (!action.enabled)
|
|
this._switch.active = state;
|
|
|
|
this._updatesIcon.visible = this.hasUpdate;
|
|
this._errorIcon.visible = this.hasError;
|
|
|
|
this._descriptionLabel.visible = !this.hasError;
|
|
|
|
this._errorLabel.label = this.error;
|
|
this._errorLabel.visible = this.error !== '';
|
|
|
|
this._versionLabel.label = this.version.toString();
|
|
this._versionLabel.visible = this.version !== '';
|
|
}
|
|
|
|
_onDestroy() {
|
|
if (!this._app.shellProxy)
|
|
return;
|
|
|
|
if (this._extensionStateChangedId)
|
|
this._app.shellProxy.disconnectSignal(this._extensionStateChangedId);
|
|
this._extensionStateChangedId = 0;
|
|
}
|
|
|
|
_canToggle() {
|
|
return this._extension.canChange;
|
|
}
|
|
});
|
|
|
|
function initEnvironment() {
|
|
// Monkey-patch in a "global" object that fakes some Shell utilities
|
|
// that ExtensionUtils depends on.
|
|
globalThis.global = {
|
|
log(...args) {
|
|
print(args.join(', '));
|
|
},
|
|
|
|
logError(s) {
|
|
log(`ERROR: ${s}`);
|
|
},
|
|
|
|
userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} argv - command line arguments
|
|
* @returns {void}
|
|
*/
|
|
export async function main(argv) {
|
|
initEnvironment();
|
|
Package.initGettext();
|
|
|
|
await new Application().runAsync(argv);
|
|
}
|