extensions-app: Move classes to their own files
Using a single source file is getting unwieldy, so start splitting it up. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3067>
This commit is contained in:
parent
ee5b428232
commit
3d070fdc6b
@ -92,7 +92,8 @@ src/shell-util.c
|
||||
src/st/st-icon-theme.c
|
||||
subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in
|
||||
subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in
|
||||
subprojects/extensions-app/js/main.js
|
||||
subprojects/extensions-app/js/extensionRow.js
|
||||
subprojects/extensions-app/js/extensionsWindow.js
|
||||
subprojects/extensions-app/data/ui/extension-row.ui
|
||||
subprojects/extensions-app/data/ui/extensions-window.ui
|
||||
subprojects/extensions-app/data/ui/help-overlay.ui
|
||||
|
193
subprojects/extensions-app/js/extensionRow.js
Normal file
193
subprojects/extensions-app/js/extensionRow.js
Normal file
@ -0,0 +1,193 @@
|
||||
import Adw from 'gi://Adw?version=1';
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import {ExtensionState, ExtensionType, deserializeExtension} from './misc/extensionUtils.js';
|
||||
|
||||
export const ExtensionRow = GObject.registerClass({
|
||||
GTypeName: 'ExtensionRow',
|
||||
Template: 'resource:///org/gnome/Extensions/ui/extension-row.ui',
|
||||
InternalChildren: [
|
||||
'detailsPopover',
|
||||
'descriptionLabel',
|
||||
'versionLabel',
|
||||
'errorLabel',
|
||||
'errorButton',
|
||||
'updatesButton',
|
||||
'switch',
|
||||
'actionsBox',
|
||||
],
|
||||
}, class ExtensionRow extends Adw.ActionRow {
|
||||
_init(extension) {
|
||||
super._init();
|
||||
|
||||
this._app = Gio.Application.get_default();
|
||||
this._extension = extension;
|
||||
|
||||
[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._detailsPopover.popdown();
|
||||
this.get_root().openPrefs(this.uuid);
|
||||
});
|
||||
this._actionGroup.add_action(action);
|
||||
|
||||
action = new Gio.SimpleAction({
|
||||
name: 'show-url',
|
||||
enabled: this.url !== '',
|
||||
});
|
||||
action.connect('activate', () => {
|
||||
this._detailsPopover.popdown();
|
||||
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._detailsPopover.popdown();
|
||||
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', () => {
|
||||
const state = action.get_state();
|
||||
action.change_state(new GLib.Variant('b', !state.get_boolean()));
|
||||
});
|
||||
|
||||
action.connect('change-state', (a, state) => {
|
||||
if (state.get_boolean())
|
||||
this._app.shellProxy.EnableExtensionAsync(this.uuid).catch(console.error);
|
||||
else
|
||||
this._app.shellProxy.DisableExtensionAsync(this.uuid).catch(console.error);
|
||||
});
|
||||
this._actionGroup.add_action(action);
|
||||
|
||||
this.title = this.name;
|
||||
|
||||
const desc = this._extension.metadata.description.split('\n')[0];
|
||||
this._descriptionLabel.label = 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 = 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-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() {
|
||||
const state = this._extension.state === ExtensionState.ENABLED;
|
||||
|
||||
const action = this._actionGroup.lookup_action('enabled');
|
||||
action.set_state(new GLib.Variant('b', state));
|
||||
action.enabled = this._extension.canChange;
|
||||
|
||||
if (!action.enabled)
|
||||
this._switch.active = state;
|
||||
|
||||
this._updatesButton.visible = this.hasUpdate;
|
||||
this._errorButton.visible = this.hasError;
|
||||
this._errorLabel.label = this.error;
|
||||
|
||||
this._versionLabel.label = _('Version %s').format(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;
|
||||
}
|
||||
});
|
||||
|
293
subprojects/extensions-app/js/extensionsWindow.js
Normal file
293
subprojects/extensions-app/js/extensionsWindow.js
Normal file
@ -0,0 +1,293 @@
|
||||
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';
|
||||
|
||||
const Package = imports.package;
|
||||
import * as Gettext from 'gettext';
|
||||
|
||||
import * as Config from './misc/config.js';
|
||||
import {ExtensionRow} from './extensionRow.js';
|
||||
import {ExtensionState, ExtensionType, deserializeExtension} from './misc/extensionUtils.js';
|
||||
|
||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||
Gio._promisify(Shew.WindowExporter.prototype, 'export');
|
||||
|
||||
export const ExtensionsWindow = GObject.registerClass({
|
||||
GTypeName: 'ExtensionsWindow',
|
||||
Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui',
|
||||
InternalChildren: [
|
||||
'userGroup',
|
||||
'userList',
|
||||
'systemGroup',
|
||||
'systemList',
|
||||
'mainStack',
|
||||
'searchBar',
|
||||
'searchButton',
|
||||
'searchEntry',
|
||||
'updatesBanner',
|
||||
],
|
||||
}, class ExtensionsWindow extends Adw.ApplicationWindow {
|
||||
_init(params) {
|
||||
super._init(params);
|
||||
|
||||
if (Config.PROFILE === 'development')
|
||||
this.add_css_class('devel');
|
||||
|
||||
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) {
|
||||
const row = this._findExtensionRow(uuid);
|
||||
|
||||
const 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(console.error);
|
||||
dialog.destroy();
|
||||
});
|
||||
dialog.present();
|
||||
}
|
||||
|
||||
async openPrefs(uuid) {
|
||||
if (!this._exportedHandle) {
|
||||
try {
|
||||
this._exportedHandle = await this._exporter.export();
|
||||
} catch (e) {
|
||||
console.warn(`Failed to export window: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this._shellProxy.OpenExtensionPrefsAsync(uuid,
|
||||
this._exportedHandle,
|
||||
{modal: new GLib.Variant('b', true)}).catch(console.error);
|
||||
}
|
||||
|
||||
_showAbout() {
|
||||
const 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() {
|
||||
const action = this.lookup_action('user-extensions-enabled');
|
||||
action.set_state(
|
||||
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) {
|
||||
const 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() {
|
||||
const nUpdates = [...this._userList].filter(c => c.hasUpdate).length;
|
||||
|
||||
this._updatesBanner.title = Gettext.ngettext(
|
||||
'%d extension will be updated on next login.',
|
||||
'%d extensions will be updated on next login.',
|
||||
nUpdates).format(nUpdates);
|
||||
this._updatesBanner.revealed = nUpdates > 0;
|
||||
}
|
||||
|
||||
_extensionsLoaded() {
|
||||
this._syncListVisibility();
|
||||
this._checkUpdates();
|
||||
}
|
||||
});
|
@ -2,26 +2,17 @@ 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 {setConsoleLogDomain} from 'console';
|
||||
import * as Gettext from 'gettext';
|
||||
const Package = imports.package;
|
||||
|
||||
Package.initFormat();
|
||||
|
||||
import * as Config from './misc/config.js';
|
||||
import * as ExtensionUtils from './misc/extensionUtils.js';
|
||||
|
||||
import {ExtensionState, ExtensionType} from './misc/extensionUtils.js';
|
||||
import {ExtensionsWindow} from './extensionsWindow.js';
|
||||
|
||||
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);
|
||||
@ -36,11 +27,6 @@ function loadInterfaceXML(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() {
|
||||
@ -77,470 +63,6 @@ class Application extends Adw.Application {
|
||||
}
|
||||
});
|
||||
|
||||
var ExtensionsWindow = GObject.registerClass({
|
||||
GTypeName: 'ExtensionsWindow',
|
||||
Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui',
|
||||
InternalChildren: [
|
||||
'userGroup',
|
||||
'userList',
|
||||
'systemGroup',
|
||||
'systemList',
|
||||
'mainStack',
|
||||
'searchBar',
|
||||
'searchButton',
|
||||
'searchEntry',
|
||||
'updatesBanner',
|
||||
],
|
||||
}, class ExtensionsWindow extends Adw.ApplicationWindow {
|
||||
_init(params) {
|
||||
super._init(params);
|
||||
|
||||
if (Config.PROFILE === 'development')
|
||||
this.add_css_class('devel');
|
||||
|
||||
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(console.error);
|
||||
dialog.destroy();
|
||||
});
|
||||
dialog.present();
|
||||
}
|
||||
|
||||
async openPrefs(uuid) {
|
||||
if (!this._exportedHandle) {
|
||||
try {
|
||||
this._exportedHandle = await this._exporter.export();
|
||||
} catch (e) {
|
||||
console.warn(`Failed to export window: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this._shellProxy.OpenExtensionPrefsAsync(uuid,
|
||||
this._exportedHandle,
|
||||
{modal: new GLib.Variant('b', true)}).catch(console.error);
|
||||
}
|
||||
|
||||
_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) {
|
||||
console.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._updatesBanner.title = Gettext.ngettext(
|
||||
'%d extension will be updated on next login.',
|
||||
'%d extensions will be updated on next login.',
|
||||
nUpdates).format(nUpdates);
|
||||
this._updatesBanner.revealed = nUpdates > 0;
|
||||
}
|
||||
|
||||
_extensionsLoaded() {
|
||||
this._syncListVisibility();
|
||||
this._checkUpdates();
|
||||
}
|
||||
});
|
||||
|
||||
var ExtensionRow = GObject.registerClass({
|
||||
GTypeName: 'ExtensionRow',
|
||||
Template: 'resource:///org/gnome/Extensions/ui/extension-row.ui',
|
||||
InternalChildren: [
|
||||
'detailsPopover',
|
||||
'descriptionLabel',
|
||||
'versionLabel',
|
||||
'errorLabel',
|
||||
'errorButton',
|
||||
'updatesButton',
|
||||
'switch',
|
||||
'actionsBox',
|
||||
],
|
||||
}, class ExtensionRow extends Adw.ActionRow {
|
||||
_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._detailsPopover.popdown();
|
||||
this.get_root().openPrefs(this.uuid);
|
||||
});
|
||||
this._actionGroup.add_action(action);
|
||||
|
||||
action = new Gio.SimpleAction({
|
||||
name: 'show-url',
|
||||
enabled: this.url !== '',
|
||||
});
|
||||
action.connect('activate', () => {
|
||||
this._detailsPopover.popdown();
|
||||
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._detailsPopover.popdown();
|
||||
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(console.error);
|
||||
else
|
||||
this._app.shellProxy.DisableExtensionAsync(this.uuid).catch(console.error);
|
||||
});
|
||||
this._actionGroup.add_action(action);
|
||||
|
||||
this.title = this.name;
|
||||
|
||||
const desc = this._extension.metadata.description.split('\n')[0];
|
||||
this._descriptionLabel.label = 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();
|
||||
}
|
||||
|
||||
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-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() {
|
||||
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._updatesButton.visible = this.hasUpdate;
|
||||
this._errorButton.visible = this.hasError;
|
||||
this._errorLabel.label = this.error;
|
||||
|
||||
this._versionLabel.label = _('Version %s').format(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;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Main entrypoint for the app
|
||||
*
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/gnome/Extensions@profile@/js">
|
||||
<file>extensionRow.js</file>
|
||||
<file>extensionsWindow.js</file>
|
||||
<file>main.js</file>
|
||||
|
||||
<file>misc/config.js</file>
|
||||
|
Loading…
x
Reference in New Issue
Block a user