123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- import Gio from 'gi://Gio';
- import GObject from 'gi://GObject';
- import * as Main from 'resource:///org/gnome/shell/ui/main.js';
- import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
- import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js';
- // Bootstrap
- import {
- Extension,
- gettext as _,
- ngettext
- } from 'resource:///org/gnome/shell/extensions/extension.js';
- import Config from './config.mjs';
- import * as Clipboard from './shell/clipboard.js';
- import * as Device from './shell/device.js';
- import * as Keybindings from './shell/keybindings.js';
- import * as Notification from './shell/notification.js';
- import * as Input from './shell/input.js';
- import * as Utils from './shell/utils.js';
- import Remote from './utils/remote.mjs';
- import setup from './utils/setup.mjs';
- const QuickSettingsMenu = Main.panel.statusArea.quickSettings;
- /**
- * A System Indicator used as the hub for spawning device indicators and
- * indicating that the extension is active when there are none.
- */
- const ServiceToggle = GObject.registerClass({
- GTypeName: 'GSConnectServiceIndicator',
- }, class ServiceToggle extends QuickSettings.QuickMenuToggle {
- _init() {
- super._init({
- title: 'GSConnect',
- toggleMode: true,
- });
- this.set({iconName: 'org.gnome.Shell.Extensions.GSConnect-symbolic'});
- // Set QuickMenuToggle header.
- this.menu.setHeader('org.gnome.Shell.Extensions.GSConnect-symbolic', 'GSConnect',
- _('Sync between your devices'));
- this._menus = {};
- this._keybindings = new Keybindings.Manager();
- // GSettings
- this.settings = new Gio.Settings({
- settings_schema: Config.GSCHEMA.lookup(
- 'org.gnome.Shell.Extensions.GSConnect',
- null
- ),
- path: '/org/gnome/shell/extensions/gsconnect/',
- });
- // Bind the toggle to enabled key
- this.settings.bind('enabled',
- this, 'checked',
- Gio.SettingsBindFlags.DEFAULT);
- this._enabledId = this.settings.connect(
- 'changed::enabled',
- this._onEnabledChanged.bind(this)
- );
- this._panelModeId = this.settings.connect(
- 'changed::show-indicators',
- this._sync.bind(this)
- );
- // Service Proxy
- this.service = new Remote.Service();
- this._deviceAddedId = this.service.connect(
- 'device-added',
- this._onDeviceAdded.bind(this)
- );
- this._deviceRemovedId = this.service.connect(
- 'device-removed',
- this._onDeviceRemoved.bind(this)
- );
- this._serviceChangedId = this.service.connect(
- 'notify::active',
- this._onServiceChanged.bind(this)
- );
- // Service Menu -> Devices Section
- this.deviceSection = new PopupMenu.PopupMenuSection();
- this.deviceSection.actor.add_style_class_name('gsconnect-device-section');
- this.settings.bind(
- 'show-indicators',
- this.deviceSection.actor,
- 'visible',
- Gio.SettingsBindFlags.INVERT_BOOLEAN
- );
- this.menu.addMenuItem(this.deviceSection);
- // Service Menu -> Separator
- this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
- // Service Menu -> "Mobile Settings"
- this.menu.addSettingsAction(
- _('Mobile Settings'),
- 'org.gnome.Shell.Extensions.GSConnect.Preferences.desktop');
- // Prime the service
- this._initService();
- }
- async _initService() {
- try {
- if (this.settings.get_boolean('enabled'))
- await this.service.start();
- else
- await this.service.reload();
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- _sync() {
- const available = this.service.devices.filter(device => {
- return (device.connected && device.paired);
- });
- const panelMode = this.settings.get_boolean('show-indicators');
- // Hide status indicator if in Panel mode or no devices are available
- serviceIndicator._indicator.visible = (!panelMode && available.length);
- // Show device indicators in Panel mode if available
- for (const device of this.service.devices) {
- const isAvailable = available.includes(device);
- const indicator = Main.panel.statusArea[device.g_object_path];
- indicator.visible = panelMode && isAvailable;
- const menu = this._menus[device.g_object_path];
- menu.actor.visible = !panelMode && isAvailable;
- menu._title.actor.visible = !panelMode && isAvailable;
- }
- // Set subtitle on Quick Settings tile
- if (available.length === 1) {
- this.subtitle = available[0].name;
- } else if (available.length > 1) {
- // TRANSLATORS: %d is the number of devices connected
- this.subtitle = ngettext(
- '%d Connected',
- '%d Connected',
- available.length
- ).format(available.length);
- } else {
- this.subtitle = null;
- }
- }
- _onDeviceChanged(device, changed, invalidated) {
- try {
- const properties = changed.deepUnpack();
- if (properties.hasOwnProperty('Connected') ||
- properties.hasOwnProperty('Paired'))
- this._sync();
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- _onDeviceAdded(service, device) {
- try {
- // Device Indicator
- const indicator = new Device.Indicator({device: device});
- Main.panel.addToStatusArea(device.g_object_path, indicator);
- // Device Menu
- const menu = new Device.Menu({
- device: device,
- menu_type: 'list',
- });
- this._menus[device.g_object_path] = menu;
- this.deviceSection.addMenuItem(menu);
- // Device Settings
- device.settings = new Gio.Settings({
- settings_schema: Config.GSCHEMA.lookup(
- 'org.gnome.Shell.Extensions.GSConnect.Device',
- true
- ),
- path: `/org/gnome/shell/extensions/gsconnect/device/${device.id}/`,
- });
- // Keyboard Shortcuts
- device.__keybindingsChangedId = device.settings.connect(
- 'changed::keybindings',
- this._onDeviceKeybindingsChanged.bind(this, device)
- );
- this._onDeviceKeybindingsChanged(device);
- // Watch the for status changes
- device.__deviceChangedId = device.connect(
- 'g-properties-changed',
- this._onDeviceChanged.bind(this)
- );
- this._sync();
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- _onDeviceRemoved(service, device, sync = true) {
- try {
- // Stop watching for status changes
- if (device.__deviceChangedId)
- device.disconnect(device.__deviceChangedId);
- // Release keybindings
- if (device.__keybindingsChangedId) {
- device.settings.disconnect(device.__keybindingsChangedId);
- device._keybindings.map(id => this._keybindings.remove(id));
- }
- // Destroy the indicator
- Main.panel.statusArea[device.g_object_path].destroy();
- // Destroy the menu
- this._menus[device.g_object_path].destroy();
- delete this._menus[device.g_object_path];
- if (sync)
- this._sync();
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- _onDeviceKeybindingsChanged(device) {
- try {
- // Reset any existing keybindings
- if (device.hasOwnProperty('_keybindings'))
- device._keybindings.map(id => this._keybindings.remove(id));
- device._keybindings = [];
- // Get the keybindings
- const keybindings = device.settings.get_value('keybindings').deepUnpack();
- // Apply the keybindings
- for (const [action, accelerator] of Object.entries(keybindings)) {
- const [, name, parameter] = Gio.Action.parse_detailed_name(action);
- const actionId = this._keybindings.add(
- accelerator,
- () => device.action_group.activate_action(name, parameter)
- );
- if (actionId !== 0)
- device._keybindings.push(actionId);
- }
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- async _onEnabledChanged(settings, key) {
- try {
- if (this.settings.get_boolean('enabled'))
- await this.service.start();
- else
- await this.service.stop();
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- async _onServiceChanged(service, pspec) {
- try {
- // If it's enabled, we should try to restart now
- if (this.settings.get_boolean('enabled'))
- await this.service.start();
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- destroy() {
- // Unhook from Remote.Service
- if (this.service) {
- this.service.disconnect(this._serviceChangedId);
- this.service.disconnect(this._deviceAddedId);
- this.service.disconnect(this._deviceRemovedId);
- for (const device of this.service.devices)
- this._onDeviceRemoved(this.service, device, false);
- if (!this.settings.get_boolean('keep-alive-when-locked'))
- this.service.stop();
- this.service.destroy();
- }
- // Disconnect any keybindings
- this._keybindings.destroy();
- // Disconnect from any GSettings changes
- this.settings.disconnect(this._enabledId);
- this.settings.disconnect(this._panelModeId);
- this.settings.run_dispose();
- // Destroy the PanelMenu.SystemIndicator actors
- this.menu.destroy();
- super.destroy();
- }
- });
- const ServiceIndicator = GObject.registerClass(
- class ServiceIndicator extends QuickSettings.SystemIndicator {
- _init() {
- super._init();
- // Create the icon for the indicator
- this._indicator = this._addIndicator();
- this._indicator.icon_name = 'org.gnome.Shell.Extensions.GSConnect-symbolic';
- // Hide the indicator by default
- this._indicator.visible = false;
- // Create the toggle menu and associate it with the indicator
- this.quickSettingsItems.push(new ServiceToggle());
- // Add the indicator to the panel and the toggle to the menu
- QuickSettingsMenu.addExternalIndicator(this);
- }
- destroy() {
- // Set enabled state to false to kill the service on destroy
- this.quickSettingsItems.forEach(item => item.destroy());
- // Destroy the indicator
- this._indicator.destroy();
- super.destroy();
- }
- });
- let serviceIndicator = null;
- export default class GSConnectExtension extends Extension {
- lockscreenInput = null;
- constructor(metadata) {
- super(metadata);
- setup(this.path);
- // If installed as a user extension, this checks the permissions
- // on certain critical files in the extension directory
- // to ensure that they have the executable bit set,
- // and makes them executable if not. Some packaging methods
- // (particularly GitHub Actions artifacts) automatically remove
- // executable bits from all contents, presumably for security.
- Utils.ensurePermissions();
- // If installed as a user extension, this will install the Desktop entry,
- // DBus and systemd service files necessary for DBus activation and
- // GNotifications. Since there's no uninit()/uninstall() hook for extensions
- // and they're only used *by* GSConnect, they should be okay to leave.
- Utils.installService();
- // These modify the notification source for GSConnect's GNotifications and
- // need to be active even when the extension is disabled (eg. lock screen).
- // Since they *only* affect notifications from GSConnect, it should be okay
- // to leave them applied.
- Notification.patchGSConnectNotificationSource();
- Notification.patchGtkNotificationDaemon();
- // This watches for the service to start and exports a custom clipboard
- // portal for use on Wayland
- Clipboard.watchService();
- }
- enable() {
- serviceIndicator = new ServiceIndicator();
- Notification.patchGtkNotificationSources();
- this.lockscreenInput = new Input.LockscreenRemoteAccess();
- this.lockscreenInput.patchInhibitor();
- }
- disable() {
- serviceIndicator.destroy();
- serviceIndicator = null;
- Notification.unpatchGtkNotificationSources();
- if (this.lockscreenInput) {
- this.lockscreenInput.unpatchInhibitor();
- this.lockscreenInput = null;
- }
- }
- }
|