summaryrefslogtreecommitdiffstats
path: root/subprojects/extensions-app/js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--subprojects/extensions-app/js/gnome-extensions-app.in2
-rw-r--r--subprojects/extensions-app/js/main.js525
-rw-r--r--subprojects/extensions-app/js/meson.build32
-rw-r--r--subprojects/extensions-app/js/misc/config.js1
-rw-r--r--subprojects/extensions-app/js/org.gnome.Extensions.in6
-rw-r--r--subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml9
6 files changed, 575 insertions, 0 deletions
diff --git a/subprojects/extensions-app/js/gnome-extensions-app.in b/subprojects/extensions-app/js/gnome-extensions-app.in
new file mode 100644
index 0000000..dc6d627
--- /dev/null
+++ b/subprojects/extensions-app/js/gnome-extensions-app.in
@@ -0,0 +1,2 @@
+#!/bin/sh
+@gjs@ @pkgdatadir@/@app_id@ "$@"
diff --git a/subprojects/extensions-app/js/main.js b/subprojects/extensions-app/js/main.js
new file mode 100644
index 0000000..aae5b9b
--- /dev/null
+++ b/subprojects/extensions-app/js/main.js
@@ -0,0 +1,525 @@
+/* exported main */
+imports.gi.versions.Gdk = '3.0';
+imports.gi.versions.Gtk = '3.0';
+
+const Gettext = imports.gettext;
+const Package = imports.package;
+const { Gdk, GLib, Gio, GObject, Gtk, Shew } = imports.gi;
+
+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', 'call_finish');
+Gio._promisify(Shew.WindowExporter.prototype, 'export', 'export_finish');
+
+function loadInterfaceXML(iface) {
+ const uri = 'resource:///org/gnome/Extensions/dbus-interfaces/%s.xml'.format(iface);
+ const f = Gio.File.new_for_uri(uri);
+
+ try {
+ let [ok_, bytes] = f.load_contents(null);
+ return imports.byteArray.toString(bytes);
+ } catch (e) {
+ log('Failed to load D-Bus interface %s'.format(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 Gtk.Application {
+ _init() {
+ GLib.set_prgname('gnome-extensions-app');
+ super._init({ application_id: 'org.gnome.Extensions' });
+ }
+
+ get shellProxy() {
+ return this._shellProxy;
+ }
+
+ vfunc_activate() {
+ this._shellProxy.CheckForUpdatesRemote();
+ this._window.present();
+ }
+
+ vfunc_startup() {
+ super.vfunc_startup();
+
+ let provider = new Gtk.CssProvider();
+ let uri = 'resource:///org/gnome/Extensions/css/application.css';
+ try {
+ provider.load_from_file(Gio.File.new_for_uri(uri));
+ } catch (e) {
+ logError(e, 'Failed to add application style');
+ }
+ Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
+ provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ 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: [
+ 'userList',
+ 'systemList',
+ 'mainBox',
+ 'mainStack',
+ 'scrolledWindow',
+ 'updatesBar',
+ 'updatesLabel',
+ ],
+}, class ExtensionsWindow extends Gtk.ApplicationWindow {
+ _init(params) {
+ super._init(params);
+
+ this._updatesCheckId = 0;
+
+ this._exporter = new Shew.WindowExporter({ window: this });
+ this._exportedHandle = '';
+
+ this._mainBox.set_focus_vadjustment(this._scrolledWindow.vadjustment);
+
+ let action;
+ action = new Gio.SimpleAction({ name: 'show-about' });
+ action.connect('activate', this._showAbout.bind(this));
+ this.add_action(action);
+
+ action = new Gio.SimpleAction({ name: 'logout' });
+ action.connect('activate', this._logout.bind(this));
+ this.add_action(action);
+
+ action = new Gio.SimpleAction({
+ name: 'user-extensions-enabled',
+ state: new GLib.Variant('b', false),
+ });
+ action.connect('activate', toggleState);
+ action.connect('change-state', (a, state) => {
+ this._shellProxy.UserExtensionsEnabled = state.get_boolean();
+ });
+ this.add_action(action);
+
+ this._userList.set_sort_func(this._sortList.bind(this));
+ this._userList.set_header_func(this._updateHeader.bind(this));
+
+ this._systemList.set_sort_func(this._sortList.bind(this));
+ this._systemList.set_header_func(this._updateHeader.bind(this));
+
+ 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.UninstallExtensionRemote(uuid);
+ dialog.destroy();
+ });
+ dialog.present();
+ }
+
+ async openPrefs(uuid) {
+ if (!this._exportedHandle) {
+ try {
+ this._exportedHandle = await this._exporter.export();
+ } catch (e) {
+ log('Failed to export window: %s'.format(e.message));
+ }
+ }
+
+ this._shellProxy.OpenExtensionPrefsRemote(uuid,
+ this._exportedHandle,
+ { modal: new GLib.Variant('b', true) });
+ }
+
+ _showAbout() {
+ let aboutDialog = new Gtk.AboutDialog({
+ authors: [
+ 'Florian Müllner <fmuellner@gnome.org>',
+ 'Jasper St. Pierre <jstpierre@mecheye.net>',
+ 'Didier Roche <didrocks@ubuntu.com>',
+ ],
+ translator_credits: _('translator-credits'),
+ program_name: _('Extensions'),
+ comments: _('Manage your GNOME Extensions'),
+ license_type: Gtk.License.GPL_2_0,
+ logo_icon_name: 'org.gnome.Extensions',
+ version: imports.package.version,
+
+ transient_for: this,
+ modal: true,
+ });
+ aboutDialog.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);
+ }
+
+ _updateHeader(row, before) {
+ if (!before || row.get_header())
+ return;
+
+ let sep = new Gtk.Separator({ orientation: Gtk.Orientation.HORIZONTAL });
+ row.set_header(sep);
+ }
+
+ _findExtensionRow(uuid) {
+ return [
+ ...this._userList.get_children(),
+ ...this._systemList.get_children(),
+ ].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.destroy();
+ row = null;
+ }
+
+ if (row) {
+ if (extension.state === ExtensionState.UNINSTALLED)
+ row.destroy();
+ } else {
+ this._addExtensionRow(extension);
+ }
+
+ this._syncListVisibility();
+ }
+
+ _scanExtensions() {
+ this._shellProxy.ListExtensionsRemote(([extensionsMap], e) => {
+ if (e) {
+ if (e instanceof Gio.DBusError) {
+ log('Failed to connect to shell proxy: %s'.format(e.toString()));
+ this._mainStack.visible_child_name = 'noshell';
+ } else {
+ throw e;
+ }
+ return;
+ }
+
+ for (let uuid in extensionsMap) {
+ let extension = ExtensionUtils.deserializeExtension(extensionsMap[uuid]);
+ this._addExtensionRow(extension);
+ }
+ this._extensionsLoaded();
+ });
+ }
+
+ _addExtensionRow(extension) {
+ let row = new ExtensionRow(extension);
+ row.show_all();
+
+ if (row.type === ExtensionType.PER_USER)
+ this._userList.add(row);
+ else
+ this._systemList.add(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._userList.visible = this._userList.get_children().length > 0;
+ this._systemList.visible = this._systemList.get_children().length > 0;
+
+ if (this._userList.visible || this._systemList.visible)
+ this._mainStack.visible_child_name = 'main';
+ else
+ this._mainStack.visible_child_name = 'placeholder';
+ }
+
+ _checkUpdates() {
+ let nUpdates = this._userList.get_children().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.visible = 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',
+ 'authorLabel',
+ 'errorLabel',
+ 'errorIcon',
+ 'updatesIcon',
+ 'switch',
+ 'revealButton',
+ 'revealer',
+ ],
+}, class ExtensionRow extends Gtk.ListBoxRow {
+ _init(extension) {
+ super._init();
+
+ this._app = Gio.Application.get_default();
+ this._extension = extension;
+ this._prefsModule = 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_toplevel().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_toplevel().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.EnableExtensionRemote(this.uuid);
+ else
+ this._app.shellProxy.DisableExtensionRemote(this.uuid);
+ });
+ this._actionGroup.add_action(action);
+
+ this._nameLabel.label = this.name;
+
+ let desc = this._extension.metadata.description.split('\n')[0];
+ this._descriptionLabel.label = desc;
+
+ this._revealButton.connect('clicked', () => {
+ this._revealer.reveal_child = !this._revealer.reveal_child;
+ });
+ this._revealer.connect('notify::reveal-child', () => {
+ if (this._revealer.reveal_child)
+ this._revealButton.get_style_context().add_class('expanded');
+ else
+ this._revealButton.get_style_context().remove_class('expanded');
+ });
+
+ 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();
+ }
+
+ 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');
+ }
+
+ _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._errorLabel.label = this.error;
+ this._errorLabel.visible = this.error !== '';
+
+ this._versionLabel.label = this.version.toString();
+ this._versionLabel.visible = this.version !== '';
+
+ this._authorLabel.label = this.creator.toString();
+ this._authorLabel.visible = this.creator !== '';
+ }
+
+ _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'.format(s));
+ },
+
+ userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']),
+ };
+}
+
+function main(argv) {
+ initEnvironment();
+ Package.initGettext();
+
+ new Application().run(argv);
+}
diff --git a/subprojects/extensions-app/js/meson.build b/subprojects/extensions-app/js/meson.build
new file mode 100644
index 0000000..a378a7e
--- /dev/null
+++ b/subprojects/extensions-app/js/meson.build
@@ -0,0 +1,32 @@
+launcherconf = configuration_data()
+launcherconf.set('app_id', app_id)
+launcherconf.set('PACKAGE_NAME', package_name)
+launcherconf.set('PACKAGE_VERSION', meson.project_version())
+launcherconf.set('prefix', prefix)
+launcherconf.set('libdir', libdir)
+launcherconf.set('pkgdatadir', pkgdatadir)
+launcherconf.set('gjs', gjs.path())
+
+configure_file(
+ input: prgname + '.in',
+ output: prgname,
+ configuration: launcherconf,
+ install_dir: bindir,
+ install_mode: 'rwxr-xr-x',
+)
+
+configure_file(
+ input: app_id + '.in',
+ output: app_id,
+ configuration: launcherconf,
+ install_dir: pkgdatadir,
+)
+
+gnome.compile_resources(
+ app_id + '.src',
+ app_id + '.src.gresource.xml',
+ source_dir: ['.', '../../../js'],
+ gresource_bundle: true,
+ install: true,
+ install_dir: pkgdatadir
+)
diff --git a/subprojects/extensions-app/js/misc/config.js b/subprojects/extensions-app/js/misc/config.js
new file mode 100644
index 0000000..d213b78
--- /dev/null
+++ b/subprojects/extensions-app/js/misc/config.js
@@ -0,0 +1 @@
+/* Fake module to satify import in ExtensionUtils */
diff --git a/subprojects/extensions-app/js/org.gnome.Extensions.in b/subprojects/extensions-app/js/org.gnome.Extensions.in
new file mode 100644
index 0000000..da7ab25
--- /dev/null
+++ b/subprojects/extensions-app/js/org.gnome.Extensions.in
@@ -0,0 +1,6 @@
+imports.package.start({
+ name: '@PACKAGE_NAME@',
+ version: '@PACKAGE_VERSION@',
+ prefix: '@prefix@',
+ libdir: '@libdir@',
+});
diff --git a/subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml b/subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml
new file mode 100644
index 0000000..8cda0f9
--- /dev/null
+++ b/subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/Extensions/js">
+ <file>main.js</file>
+
+ <file>misc/config.js</file>
+ <file>misc/extensionUtils.js</file>
+ </gresource>
+</gresources>