[gnome] Update extensions for version 48
This commit is contained in:
@ -3,13 +3,13 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
export default {
|
||||
PACKAGE_VERSION: 58,
|
||||
PACKAGE_VERSION: 62,
|
||||
PACKAGE_URL: 'https://github.com/GSConnect/gnome-shell-extension-gsconnect',
|
||||
PACKAGE_BUGREPORT: 'https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/new',
|
||||
PACKAGE_DATADIR: '/usr/local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io',
|
||||
PACKAGE_LOCALEDIR: '/usr/local/share/locale',
|
||||
GSETTINGS_SCHEMA_DIR: '/usr/local/share/glib-2.0/schemas',
|
||||
GNOME_SHELL_LIBDIR: '/usr/local/lib64',
|
||||
GNOME_SHELL_LIBDIR: '/usr/local/lib/x86_64-linux-gnu',
|
||||
|
||||
APP_ID: 'org.gnome.Shell.Extensions.GSConnect',
|
||||
APP_PATH: '/org/gnome/Shell/Extensions/GSConnect',
|
||||
|
||||
@ -22,9 +22,8 @@ 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 * as Remote from './utils/remote.js';
|
||||
import setup from './utils/setup.js';
|
||||
import * as Setup from './utils/setup.js';
|
||||
|
||||
const QuickSettingsMenu = Main.panel.statusArea.quickSettings;
|
||||
|
||||
@ -358,7 +357,7 @@ export default class GSConnectExtension extends Extension {
|
||||
|
||||
constructor(metadata) {
|
||||
super(metadata);
|
||||
setup(this.path);
|
||||
Setup.setup(this.path);
|
||||
|
||||
// If installed as a user extension, this checks the permissions
|
||||
// on certain critical files in the extension directory
|
||||
@ -366,13 +365,13 @@ export default class GSConnectExtension extends Extension {
|
||||
// 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();
|
||||
Setup.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();
|
||||
Setup.installService();
|
||||
|
||||
// These modify the notification source for GSConnect's GNotifications and
|
||||
// need to be active even when the extension is disabled (eg. lock screen).
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -4,9 +4,10 @@
|
||||
"name": "GSConnect",
|
||||
"shell-version": [
|
||||
"46",
|
||||
"47"
|
||||
"47",
|
||||
"48"
|
||||
],
|
||||
"url": "https://github.com/GSConnect/gnome-shell-extension-gsconnect/wiki",
|
||||
"uuid": "gsconnect@andyholmes.github.io",
|
||||
"version": 58
|
||||
"version": 62
|
||||
}
|
||||
Binary file not shown.
@ -60,7 +60,7 @@ export function rowSeparators(row, before) {
|
||||
*
|
||||
* @param {Gtk.ListBoxRow} row1 - The first row
|
||||
* @param {Gtk.ListBoxRow} row2 - The second row
|
||||
* @return {number} -1, 0 or 1
|
||||
* @returns {number} -1, 0 or 1
|
||||
*/
|
||||
export function titleSortFunc(row1, row2) {
|
||||
if (!row1.title || !row2.title)
|
||||
@ -605,7 +605,7 @@ export const Panel = GObject.registerClass({
|
||||
const isPresent = value.get_boolean();
|
||||
|
||||
resolve(isPresent);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
@ -614,7 +614,7 @@ export const Panel = GObject.registerClass({
|
||||
|
||||
this.battery_system_label.visible = hasBattery;
|
||||
this.battery_system.visible = hasBattery;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.battery_system_label.visible = false;
|
||||
this.battery_system.visible = false;
|
||||
}
|
||||
@ -814,7 +814,7 @@ export const Panel = GObject.registerClass({
|
||||
|
||||
try {
|
||||
applications = JSON.parse(settings.get_string('applications'));
|
||||
} catch (e) {
|
||||
} catch {
|
||||
applications = {};
|
||||
}
|
||||
|
||||
@ -858,7 +858,7 @@ export const Panel = GObject.registerClass({
|
||||
|
||||
try {
|
||||
applications = JSON.parse(settings.get_string('applications'));
|
||||
} catch (e) {
|
||||
} catch {
|
||||
applications = {};
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
import setup, {setupGettext} from '../utils/setup.js';
|
||||
import {setup, setupGettext} from '../utils/setup.js';
|
||||
|
||||
|
||||
// Bootstrap
|
||||
|
||||
@ -205,7 +205,7 @@ export const ShortcutChooserDialog = GObject.registerClass({
|
||||
* @param {string} accelerator - An accelerator
|
||||
* @param {number} [modeFlags] - Mode Flags
|
||||
* @param {number} [grabFlags] - Grab Flags
|
||||
* @param {boolean} %true if available, %false on error or unavailable
|
||||
* @returns {boolean} %true if available, %false on error or unavailable
|
||||
*/
|
||||
export async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0) {
|
||||
try {
|
||||
@ -272,7 +272,7 @@ export async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0
|
||||
*
|
||||
* @param {string} summary - A description of the keybinding's function
|
||||
* @param {string} accelerator - An accelerator as taken by Gtk.ShortcutLabel
|
||||
* @return {string} An accelerator or %null if it should be unset.
|
||||
* @returns {string} An accelerator or %null if it should be unset.
|
||||
*/
|
||||
export async function getAccelerator(summary, accelerator = null) {
|
||||
try {
|
||||
@ -311,4 +311,3 @@ export async function getAccelerator(summary, accelerator = null) {
|
||||
return accelerator;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -422,6 +422,28 @@ export const Window = GObject.registerClass({
|
||||
dialog.show_all();
|
||||
}
|
||||
|
||||
_validateName(name) {
|
||||
// None of the forbidden characters and at least one non-whitespace
|
||||
if (name.trim() && /^[^"',;:.!?()[\]<>]{1,32}$/.test(name))
|
||||
return true;
|
||||
|
||||
const dialog = new Gtk.MessageDialog({
|
||||
text: _('Invalid Device Name'),
|
||||
// TRANSLATOR: %s is a list of forbidden characters
|
||||
secondary_text: _('Device name must not contain any of %s ' +
|
||||
'and have a length of 1-32 characters')
|
||||
.format('<b><tt>^"\',;:.!?()[]<></tt></b>'),
|
||||
secondary_use_markup: true,
|
||||
buttons: Gtk.ButtonsType.OK,
|
||||
modal: true,
|
||||
transient_for: this,
|
||||
});
|
||||
dialog.connect('response', (dialog) => dialog.destroy());
|
||||
dialog.show_all();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* "Help" GAction
|
||||
*/
|
||||
@ -456,7 +478,7 @@ export const Window = GObject.registerClass({
|
||||
}
|
||||
|
||||
_onSetServiceName(widget) {
|
||||
if (this.rename_entry.text.length) {
|
||||
if (this._validateName(this.rename_entry.text)) {
|
||||
this.headerbar.title = this.rename_entry.text;
|
||||
this.settings.set_string('name', this.rename_entry.text);
|
||||
}
|
||||
|
||||
@ -7,15 +7,15 @@ import GLib from 'gi://GLib';
|
||||
import Adw from 'gi://Adw';
|
||||
|
||||
// Bootstrap
|
||||
import * as Utils from './shell/utils.js';
|
||||
import setup from './utils/setup.js';
|
||||
import * as Setup from './utils/setup.js';
|
||||
import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
export default class GSConnectExtensionPreferences extends ExtensionPreferences {
|
||||
constructor(metadata) {
|
||||
super(metadata);
|
||||
setup(this.path);
|
||||
Utils.installService();
|
||||
Setup.setup(this.path);
|
||||
Setup.ensurePermissions();
|
||||
Setup.installService();
|
||||
}
|
||||
|
||||
fillPreferencesWindow(window) {
|
||||
|
||||
@ -8,12 +8,13 @@ import GObject from 'gi://GObject';
|
||||
|
||||
import Config from '../../config.js';
|
||||
import * as Core from '../core.js';
|
||||
import Device from '../device.js';
|
||||
|
||||
// Retain compatibility with GLib < 2.80, which lacks GioUnix
|
||||
let GioUnix;
|
||||
try {
|
||||
GioUnix = (await import('gi://GioUnix')).default;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
GioUnix = {
|
||||
InputStream: Gio.UnixInputStream,
|
||||
OutputStream: Gio.UnixOutputStream,
|
||||
@ -42,7 +43,7 @@ try {
|
||||
Gio.SocketType.STREAM,
|
||||
Gio.SocketProtocol.TCP
|
||||
).get_option(6, 5);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
_LINUX_SOCKETS = false;
|
||||
}
|
||||
|
||||
@ -148,6 +149,10 @@ export const ChannelService = GObject.registerClass({
|
||||
return this._channels;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.certificate.common_name;
|
||||
}
|
||||
|
||||
get port() {
|
||||
if (this._port === undefined)
|
||||
this._port = PROTOCOL_PORT_DEFAULT;
|
||||
@ -172,13 +177,6 @@ export const ChannelService = GObject.registerClass({
|
||||
}
|
||||
|
||||
_initCertificate() {
|
||||
if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) {
|
||||
const error = new Error();
|
||||
error.name = _('OpenSSL not found');
|
||||
error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const certPath = GLib.build_filenamev([
|
||||
Config.CONFIGDIR,
|
||||
'certificate.pem',
|
||||
@ -190,12 +188,7 @@ export const ChannelService = GObject.registerClass({
|
||||
|
||||
// Ensure a certificate exists with our id as the common name
|
||||
this._certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath,
|
||||
this.id);
|
||||
|
||||
// If the service ID doesn't match the common name, this is probably a
|
||||
// certificate from an older version and we should amend ours to match
|
||||
if (this.id !== this._certificate.common_name)
|
||||
this._id = this._certificate.common_name;
|
||||
null);
|
||||
}
|
||||
|
||||
_initTcpListener() {
|
||||
@ -282,7 +275,7 @@ export const ChannelService = GObject.registerClass({
|
||||
this._udp6_source = this._udp6.create_source(GLib.IOCondition.IN, null);
|
||||
this._udp6_source.set_callback(this._onIncomingIdentity.bind(this, this._udp6));
|
||||
this._udp6_source.attach(null);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this._udp6 = null;
|
||||
}
|
||||
|
||||
@ -360,13 +353,24 @@ export const ChannelService = GObject.registerClass({
|
||||
async _onIdentity(packet) {
|
||||
try {
|
||||
// Bail if the deviceId is missing
|
||||
if (!packet.body.hasOwnProperty('deviceId'))
|
||||
return;
|
||||
if (!this.identity.body.deviceId)
|
||||
throw new Error('missing deviceId');
|
||||
|
||||
// Silently ignore our own broadcasts
|
||||
if (packet.body.deviceId === this.identity.body.deviceId)
|
||||
return;
|
||||
|
||||
// Reject invalid device IDs
|
||||
if (!Device.validateId(packet.body.deviceId))
|
||||
throw new Error(`invalid deviceId "${packet.body.deviceId}"`);
|
||||
|
||||
if (!packet.body.deviceName)
|
||||
throw new Error('missing deviceName');
|
||||
|
||||
// Reject invalid device names
|
||||
if (!Device.validateName(packet.body.deviceName))
|
||||
throw new Error(`invalid deviceName "${packet.body.deviceName}"`);
|
||||
|
||||
debug(packet);
|
||||
|
||||
// Create a new channel
|
||||
@ -596,7 +600,7 @@ export const Channel = GObject.registerClass({
|
||||
* Authenticate a TLS connection.
|
||||
*
|
||||
* @param {Gio.TlsConnection} connection - A TLS connection
|
||||
* @return {Promise} A promise for the operation
|
||||
* @returns {Promise} A promise for the operation
|
||||
*/
|
||||
async _authenticate(connection) {
|
||||
// Standard TLS Handshake
|
||||
@ -668,7 +672,7 @@ export const Channel = GObject.registerClass({
|
||||
* Wrap the connection in Gio.TlsClientConnection and initiate handshake
|
||||
*
|
||||
* @param {Gio.TcpConnection} connection - The unauthenticated connection
|
||||
* @return {Gio.TlsClientConnection} The authenticated connection
|
||||
* @returns {Gio.TlsClientConnection} The authenticated connection
|
||||
*/
|
||||
_encryptClient(connection) {
|
||||
_configureSocket(connection);
|
||||
@ -684,7 +688,7 @@ export const Channel = GObject.registerClass({
|
||||
* Wrap the connection in Gio.TlsServerConnection and initiate handshake
|
||||
*
|
||||
* @param {Gio.TcpConnection} connection - The unauthenticated connection
|
||||
* @return {Gio.TlsServerConnection} The authenticated connection
|
||||
* @returns {Gio.TlsServerConnection} The authenticated connection
|
||||
*/
|
||||
_encryptServer(connection) {
|
||||
_configureSocket(connection);
|
||||
@ -729,7 +733,25 @@ export const Channel = GObject.registerClass({
|
||||
if (!this.identity.body.deviceId)
|
||||
throw new Error('missing deviceId');
|
||||
|
||||
// Reject invalid device IDs
|
||||
if (!Device.validateId(this.identity.body.deviceId))
|
||||
throw new Error(`invalid deviceId "${this.identity.body.deviceId}"`);
|
||||
|
||||
if (!this.identity.body.deviceName)
|
||||
throw new Error('missing deviceName');
|
||||
|
||||
// Reject invalid device names
|
||||
if (!Device.validateName(this.identity.body.deviceName))
|
||||
throw new Error(`invalid deviceName "${this.identity.body.deviceName}"`);
|
||||
|
||||
this._connection = await this._encryptClient(connection);
|
||||
|
||||
// Starting with protocol version 8, the devices are expected to
|
||||
// exchange identity packets again after TLS negotiation
|
||||
if (this.identity.body.protocolVersion >= 8) {
|
||||
await this.sendPacket(this.backend.identity);
|
||||
this.identity = await this.readPacket();
|
||||
}
|
||||
} catch (e) {
|
||||
this.close();
|
||||
throw e;
|
||||
@ -754,6 +776,13 @@ export const Channel = GObject.registerClass({
|
||||
this.cancellable);
|
||||
|
||||
this._connection = await this._encryptServer(connection);
|
||||
|
||||
// Starting with protocol version 8, the devices are expected to
|
||||
// exchange identity packets again after TLS negotiation
|
||||
if (this.identity.body.protocolVersion >= 8) {
|
||||
await this.sendPacket(this.backend.identity);
|
||||
this.identity = await this.readPacket();
|
||||
}
|
||||
} catch (e) {
|
||||
this.close();
|
||||
throw e;
|
||||
@ -886,4 +915,3 @@ export const Channel = GObject.registerClass({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ export default class Controller {
|
||||
|
||||
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0];
|
||||
XKeycode.Super_L = modifier.keycode;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
debug('using default modifier keycodes');
|
||||
}
|
||||
}
|
||||
@ -304,9 +304,8 @@ export default class Controller {
|
||||
destroy() {
|
||||
try {
|
||||
Atspi.exit();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Silence errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ try {
|
||||
EBook = (await import('gi://EBook')).default;
|
||||
EBookContacts = (await import('gi://EBookContacts')).default;
|
||||
EDataServer = (await import('gi://EDataServer')).default;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
HAVE_EDS = false;
|
||||
}
|
||||
|
||||
@ -267,7 +267,7 @@ const Store = GObject.registerClass({
|
||||
this._ebooks = new Map();
|
||||
|
||||
// Get the current EBooks
|
||||
const registry = await this._getESourceRegistry();
|
||||
const registry = await EDataServer.SourceRegistry.new(null);
|
||||
|
||||
for (const source of registry.list_sources('Address Book'))
|
||||
await this._onAppeared(null, source);
|
||||
@ -329,7 +329,7 @@ const Store = GObject.registerClass({
|
||||
* Save a Uint8Array to file and return the path
|
||||
*
|
||||
* @param {Uint8Array} contents - An image byte array
|
||||
* @return {string|undefined} File path or %undefined on failure
|
||||
* @returns {string|undefined} File path or %undefined on failure
|
||||
*/
|
||||
async storeAvatar(contents) {
|
||||
const md5 = GLib.compute_checksum_for_data(GLib.ChecksumType.MD5,
|
||||
@ -353,10 +353,10 @@ const Store = GObject.registerClass({
|
||||
/**
|
||||
* Query the Store for a contact by name and/or number.
|
||||
*
|
||||
* @param {Object} query - A query object
|
||||
* @param {object} query - A query object
|
||||
* @param {string} [query.name] - The contact's name
|
||||
* @param {string} query.number - The contact's number
|
||||
* @return {Object} A contact object
|
||||
* @returns {object} A contact object
|
||||
*/
|
||||
query(query) {
|
||||
// First look for an existing contact by number
|
||||
@ -410,7 +410,7 @@ const Store = GObject.registerClass({
|
||||
/**
|
||||
* Add a contact, checking for validity
|
||||
*
|
||||
* @param {Object} contact - A contact object
|
||||
* @param {object} contact - A contact object
|
||||
* @param {boolean} write - Write to disk
|
||||
*/
|
||||
add(contact, write = true) {
|
||||
@ -468,8 +468,8 @@ const Store = GObject.registerClass({
|
||||
*
|
||||
* { "555-5555": { "name": "...", "numbers": [], ... } }
|
||||
*
|
||||
* @param {Object[]} addresses - A list of address objects
|
||||
* @return {Object} A dictionary of phone numbers and contacts
|
||||
* @param {object[]} addresses - A list of address objects
|
||||
* @returns {object} A dictionary of phone numbers and contacts
|
||||
*/
|
||||
lookupAddresses(addresses) {
|
||||
const contacts = {};
|
||||
@ -502,7 +502,7 @@ const Store = GObject.registerClass({
|
||||
/**
|
||||
* Update the contact store from a dictionary of our custom contact objects.
|
||||
*
|
||||
* @param {Object} json - an Object of contact Objects
|
||||
* @param {object} json - an Object of contact Objects
|
||||
*/
|
||||
async update(json = {}) {
|
||||
try {
|
||||
@ -610,4 +610,3 @@ const Store = GObject.registerClass({
|
||||
});
|
||||
|
||||
export default Store;
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ const Default = new Map();
|
||||
* followed by a call to `release()`.
|
||||
*
|
||||
* @param {string} name - The module name
|
||||
* @return {*} The default instance of a component
|
||||
* @returns {*} The default instance of a component
|
||||
*/
|
||||
export function acquire(name) {
|
||||
if (functionOverrides.acquire)
|
||||
@ -78,7 +78,7 @@ export function acquire(name) {
|
||||
* holder, the component will be freed.
|
||||
*
|
||||
* @param {string} name - The module name
|
||||
* @return {null} A %null value, useful for overriding a traced variable
|
||||
* @returns {null} A %null value, useful for overriding a traced variable
|
||||
*/
|
||||
export function release(name) {
|
||||
if (functionOverrides.release)
|
||||
@ -99,4 +99,3 @@ export function release(name) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ const RemoteSession = GObject.registerClass({
|
||||
get session_id() {
|
||||
try {
|
||||
return this.get_cached_property('SessionId').unpack();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -327,22 +327,24 @@ export default class Controller {
|
||||
}
|
||||
|
||||
async _ensureAdapter() {
|
||||
// Update the timestamp of the last event
|
||||
this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
|
||||
|
||||
// Session is active
|
||||
if (this._session !== null)
|
||||
return this._session;
|
||||
|
||||
// Mutter's RemoteDesktop is not available, fall back to Atspi
|
||||
if (this.connection === null) {
|
||||
debug('Falling back to Atspi');
|
||||
|
||||
this._session = new AtspiController();
|
||||
return this._session;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update the timestamp of the last event
|
||||
this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
|
||||
|
||||
// Session is active
|
||||
if (this._session !== null)
|
||||
return;
|
||||
|
||||
// Mutter's RemoteDesktop is not available, fall back to Atspi
|
||||
if (this.connection === null) {
|
||||
debug('Falling back to Atspi');
|
||||
|
||||
this._session = new AtspiController();
|
||||
|
||||
// Mutter is available and there isn't another session starting
|
||||
} else if (this._sessionStarting === false) {
|
||||
if (this._sessionStarting === false) {
|
||||
this._sessionStarting = true;
|
||||
|
||||
debug('Creating Mutter RemoteDesktop session');
|
||||
@ -368,18 +370,17 @@ export default class Controller {
|
||||
this._onSessionExpired.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
this._sessionStarting = false;
|
||||
}
|
||||
return this._session;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
|
||||
if (this._session !== null) {
|
||||
this._session.destroy();
|
||||
this._session = null;
|
||||
}
|
||||
|
||||
this._sessionStarting = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -387,111 +388,81 @@ export default class Controller {
|
||||
* Pointer Events
|
||||
*/
|
||||
movePointer(dx, dy) {
|
||||
try {
|
||||
if (dx === 0 && dy === 0)
|
||||
return;
|
||||
if (dx === 0 && dy === 0)
|
||||
return;
|
||||
|
||||
this._ensureAdapter();
|
||||
this._session.movePointer(dx, dy);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.movePointer(dx, dy))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
pressPointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.pressPointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.pressPointer(button))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
releasePointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.releasePointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.releasePointer(button))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
clickPointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.clickPointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.clickPointer(button))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
doubleclickPointer(button) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.doubleclickPointer(button);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.doubleclickPointer(button))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
scrollPointer(dx, dy) {
|
||||
if (dx === 0 && dy === 0)
|
||||
return;
|
||||
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.scrollPointer(dx, dy);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.scrollPointer(dx, dy))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
/*
|
||||
* Keyboard Events
|
||||
*/
|
||||
pressKeysym(keysym) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.pressKeysym(keysym);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.pressKeysym(keysym))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
releaseKeysym(keysym) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.releaseKeysym(keysym);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.releaseKeysym(keysym))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
pressreleaseKeysym(keysym) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
this._session.pressreleaseKeysym(keysym);
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
this._ensureAdapter()
|
||||
.then(session => session?.pressreleaseKeysym(keysym))
|
||||
.catch(e => debug(e));
|
||||
}
|
||||
|
||||
/*
|
||||
* High-level keyboard input
|
||||
*/
|
||||
pressKeys(input, modifiers) {
|
||||
try {
|
||||
this._ensureAdapter();
|
||||
|
||||
this._ensureAdapter()
|
||||
.then(session => {
|
||||
if (typeof input === 'string') {
|
||||
for (let i = 0; i < input.length; i++)
|
||||
this._session.pressKey(input[i], modifiers);
|
||||
session?.pressKey(input[i], modifiers);
|
||||
} else {
|
||||
this._session.pressKey(input, modifiers);
|
||||
session?.pressKey(input, modifiers);
|
||||
}
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
}).catch(e => debug(e));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@ -472,6 +472,10 @@ const PlayerProxy = GObject.registerClass({
|
||||
GTypeName: 'GSConnectMPRISPlayer',
|
||||
}, class PlayerProxy extends Player {
|
||||
|
||||
/**
|
||||
* @constructs GSConnectMPRISPlayer
|
||||
* @param {string} name - The name of the player
|
||||
*/
|
||||
_init(name) {
|
||||
super._init();
|
||||
|
||||
@ -539,7 +543,7 @@ const PlayerProxy = GObject.registerClass({
|
||||
_get(proxy, name, fallback = null) {
|
||||
try {
|
||||
return proxy.get_cached_property(name).recursiveUnpack();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@ -707,7 +711,7 @@ const PlayerProxy = GObject.registerClass({
|
||||
);
|
||||
|
||||
return reply.recursiveUnpack()[0];
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@ -914,7 +918,7 @@ const Manager = GObject.registerClass({
|
||||
* Check for a player by its Identity.
|
||||
*
|
||||
* @param {string} identity - A player name
|
||||
* @return {boolean} %true if the player was found
|
||||
* @returns {boolean} %true if the player was found
|
||||
*/
|
||||
hasPlayer(identity) {
|
||||
for (const player of this._players.values()) {
|
||||
@ -929,7 +933,7 @@ const Manager = GObject.registerClass({
|
||||
* Get a player by its Identity.
|
||||
*
|
||||
* @param {string} identity - A player name
|
||||
* @return {GSConnectMPRISPlayer|null} A player or %null
|
||||
* @returns {GSConnectMPRISPlayer|null} A player or %null
|
||||
*/
|
||||
getPlayer(identity) {
|
||||
for (const player of this._players.values()) {
|
||||
@ -943,7 +947,7 @@ const Manager = GObject.registerClass({
|
||||
/**
|
||||
* Get a list of player identities.
|
||||
*
|
||||
* @return {string[]} A list of player identities
|
||||
* @returns {string[]} A list of player identities
|
||||
*/
|
||||
getIdentities() {
|
||||
const identities = [];
|
||||
@ -1000,4 +1004,3 @@ const Manager = GObject.registerClass({
|
||||
* The service class for this component
|
||||
*/
|
||||
export default Manager;
|
||||
|
||||
|
||||
@ -144,7 +144,7 @@ const Listener = GObject.registerClass({
|
||||
*
|
||||
* @param {string} sender - A DBus unique name (eg. :1.2282)
|
||||
* @param {string} appName - @appName passed to Notify() (Optional)
|
||||
* @return {string} A well-known name or %null
|
||||
* @returns {string} A well-known name or %null
|
||||
*/
|
||||
async _getAppId(sender, appName) {
|
||||
try {
|
||||
@ -189,7 +189,7 @@ const Listener = GObject.registerClass({
|
||||
*
|
||||
* @param {string} sender - A DBus unique name
|
||||
* @param {string} [appName] - `appName` supplied by Notify()
|
||||
* @return {string} A well-known name or %null
|
||||
* @returns {string} A well-known name or %null
|
||||
*/
|
||||
async _getAppName(sender, appName = null) {
|
||||
// Check the cache first
|
||||
@ -201,7 +201,7 @@ const Listener = GObject.registerClass({
|
||||
const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
|
||||
this._names[appName] = appInfo.get_name();
|
||||
appName = appInfo.get_name();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Silence errors
|
||||
}
|
||||
|
||||
@ -261,7 +261,7 @@ const Listener = GObject.registerClass({
|
||||
/**
|
||||
* Export interfaces for proxying notifications and become a monitor
|
||||
*
|
||||
* @return {Promise} A promise for the operation
|
||||
* @returns {Promise} A promise for the operation
|
||||
*/
|
||||
_monitorConnection() {
|
||||
// libnotify Interface
|
||||
|
||||
@ -19,7 +19,7 @@ try {
|
||||
GIRepository.Repository.prepend_library_path(typelibDir);
|
||||
|
||||
Gvc = (await import('gi://Gvc')).default;
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
|
||||
|
||||
/**
|
||||
@ -33,7 +33,7 @@ if (Gvc) {
|
||||
return this.description;
|
||||
|
||||
return `${this.get_port().human_port} (${this.description})`;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return this.description;
|
||||
}
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@ import GLib from 'gi://GLib';
|
||||
let GSound = null;
|
||||
try {
|
||||
GSound = (await import('gi://GSound')).default;
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
|
||||
|
||||
const Player = class Player {
|
||||
@ -169,4 +169,3 @@ const Player = class Player {
|
||||
* The service class for this component
|
||||
*/
|
||||
export default Player;
|
||||
|
||||
|
||||
@ -6,13 +6,14 @@ import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import Device from './device.js';
|
||||
import plugins from './plugins/index.js';
|
||||
|
||||
|
||||
/**
|
||||
* Get the local device type.
|
||||
*
|
||||
* @return {string} A device type string
|
||||
* @returns {string} A device type string
|
||||
*/
|
||||
export function _getDeviceType() {
|
||||
try {
|
||||
@ -27,7 +28,7 @@ export function _getDeviceType() {
|
||||
return 'laptop';
|
||||
|
||||
return 'desktop';
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return 'desktop';
|
||||
}
|
||||
}
|
||||
@ -69,8 +70,8 @@ export class Packet {
|
||||
/**
|
||||
* Deserialize and return a new Packet from an Object or string.
|
||||
*
|
||||
* @param {Object|string} data - A string or dictionary to deserialize
|
||||
* @return {Core.Packet} A new packet object
|
||||
* @param {object|string} data - A string or dictionary to deserialize
|
||||
* @returns {Packet} A new packet object
|
||||
*/
|
||||
static deserialize(data) {
|
||||
return new Packet(data);
|
||||
@ -80,7 +81,7 @@ export class Packet {
|
||||
* Serialize the packet as a single line with a terminating new-line (`\n`)
|
||||
* character, ready to be written to a channel.
|
||||
*
|
||||
* @return {string} A serialized packet
|
||||
* @returns {string} A serialized packet
|
||||
*/
|
||||
serialize() {
|
||||
this.id = Date.now();
|
||||
@ -90,7 +91,7 @@ export class Packet {
|
||||
/**
|
||||
* Update the packet from a dictionary or string of JSON
|
||||
*
|
||||
* @param {Object|string} data - Source data
|
||||
* @param {object|string} data - Source data
|
||||
*/
|
||||
update(data) {
|
||||
try {
|
||||
@ -106,7 +107,7 @@ export class Packet {
|
||||
/**
|
||||
* Check if the packet has a payload.
|
||||
*
|
||||
* @return {boolean} %true if @packet has a payload
|
||||
* @returns {boolean} %true if @packet has a payload
|
||||
*/
|
||||
hasPayload() {
|
||||
if (!this.hasOwnProperty('payloadSize'))
|
||||
@ -219,7 +220,7 @@ export const Channel = GObject.registerClass({
|
||||
* Read a packet.
|
||||
*
|
||||
* @param {Gio.Cancellable} [cancellable] - A cancellable
|
||||
* @return {Promise<Core.Packet>} The packet
|
||||
* @returns {Promise<Packet>} The packet
|
||||
*/
|
||||
async readPacket(cancellable = null) {
|
||||
if (cancellable === null)
|
||||
@ -247,9 +248,9 @@ export const Channel = GObject.registerClass({
|
||||
/**
|
||||
* Send a packet.
|
||||
*
|
||||
* @param {Core.Packet} packet - The packet to send
|
||||
* @param {Packet} packet - The packet to send
|
||||
* @param {Gio.Cancellable} [cancellable] - A cancellable
|
||||
* @return {Promise<boolean>} %true if successful
|
||||
* @returns {Promise<boolean>} %true if successful
|
||||
*/
|
||||
sendPacket(packet, cancellable = null) {
|
||||
if (cancellable === null)
|
||||
@ -262,7 +263,7 @@ export const Channel = GObject.registerClass({
|
||||
/**
|
||||
* Reject a transfer.
|
||||
*
|
||||
* @param {Core.Packet} packet - A packet with payload info
|
||||
* @param {Packet} packet - A packet with payload info
|
||||
*/
|
||||
rejectTransfer(packet) {
|
||||
throw new GObject.NotImplementedError();
|
||||
@ -272,7 +273,7 @@ export const Channel = GObject.registerClass({
|
||||
* Download a payload from a device. Typically implementations will override
|
||||
* this with an async function.
|
||||
*
|
||||
* @param {Core.Packet} packet - A packet
|
||||
* @param {Packet} packet - A packet
|
||||
* @param {Gio.OutputStream} target - The target stream
|
||||
* @param {Gio.Cancellable} [cancellable] - A cancellable for the upload
|
||||
*/
|
||||
@ -285,7 +286,7 @@ export const Channel = GObject.registerClass({
|
||||
* Upload a payload to a device. Typically implementations will override
|
||||
* this with an async function.
|
||||
*
|
||||
* @param {Core.Packet} packet - The packet describing the transfer
|
||||
* @param {Packet} packet - The packet describing the transfer
|
||||
* @param {Gio.InputStream} source - The source stream
|
||||
* @param {number} size - The payload size
|
||||
* @param {Gio.Cancellable} [cancellable] - A cancellable for the upload
|
||||
@ -350,7 +351,7 @@ export const ChannelService = GObject.registerClass({
|
||||
|
||||
get name() {
|
||||
if (this._name === undefined)
|
||||
this._name = GLib.get_host_name();
|
||||
this._name = GLib.get_host_name().slice(0, 32);
|
||||
|
||||
return this._name;
|
||||
}
|
||||
@ -365,7 +366,7 @@ export const ChannelService = GObject.registerClass({
|
||||
|
||||
get id() {
|
||||
if (this._id === undefined)
|
||||
this._id = GLib.uuid_string_random();
|
||||
this._id = Device.generateId();
|
||||
|
||||
return this._id;
|
||||
}
|
||||
@ -406,7 +407,7 @@ export const ChannelService = GObject.registerClass({
|
||||
deviceId: this.id,
|
||||
deviceName: this.name,
|
||||
deviceType: _getDeviceType(),
|
||||
protocolVersion: 7,
|
||||
protocolVersion: 8,
|
||||
incomingCapabilities: [],
|
||||
outgoingCapabilities: [],
|
||||
},
|
||||
@ -429,7 +430,7 @@ export const ChannelService = GObject.registerClass({
|
||||
/**
|
||||
* Emit Core.ChannelService::channel
|
||||
*
|
||||
* @param {Core.Channel} channel - The new channel
|
||||
* @param {Channel} channel - The new channel
|
||||
*/
|
||||
channel(channel) {
|
||||
if (!this.emit('channel', channel))
|
||||
@ -542,7 +543,7 @@ export const Transfer = GObject.registerClass({
|
||||
/**
|
||||
* Ensure there is a stream for the transfer item.
|
||||
*
|
||||
* @param {Object} item - A transfer item
|
||||
* @param {object} item - A transfer item
|
||||
* @param {Gio.Cancellable} [cancellable] - A cancellable
|
||||
*/
|
||||
async _ensureStream(item, cancellable = null) {
|
||||
@ -583,7 +584,7 @@ export const Transfer = GObject.registerClass({
|
||||
/**
|
||||
* Add a file to the transfer.
|
||||
*
|
||||
* @param {Core.Packet} packet - A packet
|
||||
* @param {Packet} packet - A packet
|
||||
* @param {Gio.File} file - A file to transfer
|
||||
*/
|
||||
addFile(packet, file) {
|
||||
@ -600,7 +601,7 @@ export const Transfer = GObject.registerClass({
|
||||
/**
|
||||
* Add a filepath to the transfer.
|
||||
*
|
||||
* @param {Core.Packet} packet - A packet
|
||||
* @param {Packet} packet - A packet
|
||||
* @param {string} path - A filepath to transfer
|
||||
*/
|
||||
addPath(packet, path) {
|
||||
@ -617,7 +618,7 @@ export const Transfer = GObject.registerClass({
|
||||
/**
|
||||
* Add a stream to the transfer.
|
||||
*
|
||||
* @param {Core.Packet} packet - A packet
|
||||
* @param {Packet} packet - A packet
|
||||
* @param {Gio.InputStream|Gio.OutputStream} stream - A stream to transfer
|
||||
* @param {number} [size] - Payload size
|
||||
*/
|
||||
|
||||
@ -18,6 +18,7 @@ import system from 'system';
|
||||
import './init.js';
|
||||
|
||||
import Config from '../config.js';
|
||||
import Device from './device.js';
|
||||
import Manager from './manager.js';
|
||||
import * as ServiceUI from './ui/service.js';
|
||||
|
||||
@ -45,6 +46,61 @@ const Service = GObject.registerClass({
|
||||
this._initOptions();
|
||||
}
|
||||
|
||||
_migrateConfiguration() {
|
||||
if (!Device.validateName(this.settings.get_string('name')))
|
||||
this.settings.set('name', GLib.get_host_name().slice(0, 32));
|
||||
|
||||
const [certPath, keyPath] = [
|
||||
GLib.build_filenamev([Config.CONFIGDIR, 'certificate.pem']),
|
||||
GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']),
|
||||
];
|
||||
const certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath,
|
||||
null);
|
||||
|
||||
if (Device.validateId(certificate.common_name))
|
||||
return;
|
||||
|
||||
// Remove the old certificate, serving as the single source of truth
|
||||
// for the device ID
|
||||
try {
|
||||
Gio.File.new_for_path(certPath).delete(null);
|
||||
Gio.File.new_for_path(keyPath).delete(null);
|
||||
} catch {
|
||||
// Silence errors
|
||||
}
|
||||
|
||||
// For each device, remove it entirely if it violates the device ID
|
||||
// constraints, otherwise mark it unpaired to preserve the settings.
|
||||
const deviceList = this.settings.get_strv('devices').filter(id => {
|
||||
const settingsPath = `/org/gnome/shell/extensions/gsconnect/device/${id}/`;
|
||||
if (!Device.validateId(id)) {
|
||||
GLib.spawn_command_line_async(`dconf reset -f ${settingsPath}`);
|
||||
Gio.File.rm_rf(GLib.build_filenamev([Config.CACHEDIR, id]));
|
||||
debug(`Invalid device ID ${id} removed.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const settings = new Gio.Settings({
|
||||
settings_schema: Config.GSCHEMA.lookup(
|
||||
'org.gnome.Shell.Extensions.GSConnect.Device', true),
|
||||
path: settingsPath,
|
||||
});
|
||||
settings.set_boolean('paired', false);
|
||||
return true;
|
||||
});
|
||||
this.settings.set_strv('devices', deviceList);
|
||||
|
||||
// Notify the user
|
||||
const notification = Gio.Notification.new(_('Settings Migrated'));
|
||||
notification.set_body(_('GSConnect has updated to support changes to the KDE Connect protocol. Some devices may need to be repaired.'));
|
||||
notification.set_icon(new Gio.ThemedIcon({name: 'dialog-warning'}));
|
||||
notification.set_priority(Gio.NotificationPriority.HIGH);
|
||||
this.send_notification('settings-migrated', notification);
|
||||
|
||||
// Finally, reset the service ID to trigger re-generation.
|
||||
this.settings.reset('id');
|
||||
}
|
||||
|
||||
get settings() {
|
||||
if (this._settings === undefined) {
|
||||
this._settings = new Gio.Settings({
|
||||
@ -154,7 +210,7 @@ const Service = GObject.registerClass({
|
||||
/**
|
||||
* Report a service-level error
|
||||
*
|
||||
* @param {Object} error - An Error or object with name, message and stack
|
||||
* @param {object} error - An Error or object with name, message and stack
|
||||
*/
|
||||
notify_error(error) {
|
||||
try {
|
||||
@ -241,6 +297,9 @@ const Service = GObject.registerClass({
|
||||
// GActions & GSettings
|
||||
this._initActions();
|
||||
|
||||
// TODO: remove after a reasonable period of time
|
||||
this._migrateConfiguration();
|
||||
|
||||
this.manager.start();
|
||||
}
|
||||
|
||||
@ -699,4 +758,3 @@ const Service = GObject.registerClass({
|
||||
});
|
||||
|
||||
await (new Service()).runAsync([system.programInvocationName].concat(ARGV));
|
||||
|
||||
|
||||
@ -11,6 +11,8 @@ import * as Components from './components/index.js';
|
||||
import * as Core from './core.js';
|
||||
import plugins from './plugins/index.js';
|
||||
|
||||
const ALLOWED_TIMESTAMP_TIME_DIFFERENCE_SECONDS = 1800; // 30 min
|
||||
|
||||
/**
|
||||
* An object representing a remote device.
|
||||
*
|
||||
@ -88,6 +90,7 @@ const Device = GObject.registerClass({
|
||||
// GLib.Source timeout id's for pairing requests
|
||||
this._incomingPairRequest = 0;
|
||||
this._outgoingPairRequest = 0;
|
||||
this._pairingTimestamp = 0;
|
||||
|
||||
// Maps of name->Plugin, packet->Plugin, uuid->Transfer
|
||||
this._plugins = new Map();
|
||||
@ -128,6 +131,19 @@ const Device = GObject.registerClass({
|
||||
this._loadPlugins();
|
||||
}
|
||||
|
||||
static generateId() {
|
||||
return GLib.uuid_string_random().replaceAll('-', '');
|
||||
}
|
||||
|
||||
static validateId(id) {
|
||||
return /^[a-zA-Z0-9_-]{32,38}$/.test(id);
|
||||
}
|
||||
|
||||
static validateName(name) {
|
||||
// None of the forbidden characters and at least one non-whitespace
|
||||
return name.trim() && /^[^"',;:.!?()[\]<>]{1,32}$/.test(name);
|
||||
}
|
||||
|
||||
get channel() {
|
||||
if (this._channel === undefined)
|
||||
this._channel = null;
|
||||
@ -162,47 +178,34 @@ const Device = GObject.registerClass({
|
||||
|
||||
// FIXME: backend should do this stuff
|
||||
get encryption_info() {
|
||||
let localCert = null;
|
||||
let remoteCert = null;
|
||||
if (!this.channel)
|
||||
return '';
|
||||
|
||||
// Bluetooth connections have no certificate so we use the host address
|
||||
if (this.connection_type === 'bluetooth') {
|
||||
// TRANSLATORS: Bluetooth address for remote device
|
||||
return _('Bluetooth device at %s').format('???');
|
||||
|
||||
// If the device is connected use the certificate from the connection
|
||||
} else if (this.connected) {
|
||||
remoteCert = this.channel.peer_certificate;
|
||||
|
||||
// Otherwise pull it out of the settings
|
||||
} else if (this.paired) {
|
||||
remoteCert = Gio.TlsCertificate.new_from_pem(
|
||||
this.settings.get_string('certificate-pem'),
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
// FIXME: another ugly reach-around
|
||||
let lanBackend;
|
||||
const localCert = this.service.manager.backends.get('lan')?.certificate;
|
||||
const remoteCert = this.channel?.peer_certificate;
|
||||
if (!localCert || !remoteCert)
|
||||
return '';
|
||||
|
||||
if (this.service !== null)
|
||||
lanBackend = this.service.manager.backends.get('lan');
|
||||
const checksum = new GLib.Checksum(GLib.ChecksumType.SHA256);
|
||||
let [a, b] = [localCert.pubkey_der(), remoteCert.pubkey_der()];
|
||||
if (a.compare(b) < 0)
|
||||
[a, b] = [b, a]; // swap
|
||||
checksum.update(a.toArray());
|
||||
checksum.update(b.toArray());
|
||||
|
||||
if (lanBackend && lanBackend.certificate)
|
||||
localCert = lanBackend.certificate;
|
||||
if (this.channel?.identity.body.protocolVersion >= 8)
|
||||
checksum.update(String(this._pairingTimestamp));
|
||||
|
||||
|
||||
let verificationKey = '';
|
||||
if (localCert && remoteCert) {
|
||||
let a = localCert.pubkey_der();
|
||||
let b = remoteCert.pubkey_der();
|
||||
if (a.compare(b) < 0)
|
||||
[a, b] = [b, a]; // swap
|
||||
const checksum = new GLib.Checksum(GLib.ChecksumType.SHA256);
|
||||
checksum.update(a.toArray());
|
||||
checksum.update(b.toArray());
|
||||
verificationKey = checksum.get_string();
|
||||
}
|
||||
const verificationKey = checksum.get_string()
|
||||
.substring(0, 8)
|
||||
.toUpperCase();
|
||||
|
||||
// TRANSLATORS: Label for TLS connection verification key
|
||||
//
|
||||
@ -384,7 +387,7 @@ const Device = GObject.registerClass({
|
||||
*
|
||||
* @param {string[]} args - process arguments
|
||||
* @param {Gio.Cancellable} [cancellable] - optional cancellable
|
||||
* @return {Gio.Subprocess} The subprocess
|
||||
* @returns {Gio.Subprocess} The subprocess
|
||||
*/
|
||||
launchProcess(args, cancellable = null) {
|
||||
if (this._launcher === undefined) {
|
||||
@ -418,7 +421,7 @@ const Device = GObject.registerClass({
|
||||
* Handle a packet and pass it to the appropriate plugin.
|
||||
*
|
||||
* @param {Core.Packet} packet - The incoming packet object
|
||||
* @return {undefined} no return value
|
||||
* @returns {undefined} no return value
|
||||
*/
|
||||
handlePacket(packet) {
|
||||
try {
|
||||
@ -443,7 +446,7 @@ const Device = GObject.registerClass({
|
||||
/**
|
||||
* Send a packet to the device.
|
||||
*
|
||||
* @param {Object} packet - An object of packet data...
|
||||
* @param {object} packet - An object of packet data...
|
||||
*/
|
||||
async sendPacket(packet) {
|
||||
try {
|
||||
@ -524,7 +527,7 @@ const Device = GObject.registerClass({
|
||||
* device menu.
|
||||
*
|
||||
* @param {string} actionName - An action name with scope (eg. device.foo)
|
||||
* @return {number} An 0-based index or -1 if not found
|
||||
* @returns {number} An 0-based index or -1 if not found
|
||||
*/
|
||||
getMenuAction(actionName) {
|
||||
for (let i = 0, len = this.menu.get_n_items(); i < len; i++) {
|
||||
@ -533,7 +536,7 @@ const Device = GObject.registerClass({
|
||||
|
||||
if (val.unpack() === actionName)
|
||||
return i;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -546,7 +549,7 @@ const Device = GObject.registerClass({
|
||||
*
|
||||
* @param {Gio.MenuItem} menuItem - A GMenuItem
|
||||
* @param {number} [index] - The position to place the item
|
||||
* @return {number} The position the item was placed
|
||||
* @returns {number} The position the item was placed
|
||||
*/
|
||||
addMenuItem(menuItem, index = -1) {
|
||||
try {
|
||||
@ -570,7 +573,7 @@ const Device = GObject.registerClass({
|
||||
* @param {number} [index] - The position to place the item
|
||||
* @param {string} label - A label for the item
|
||||
* @param {string} icon_name - A themed icon name for the item
|
||||
* @return {number} The position the item was placed
|
||||
* @returns {number} The position the item was placed
|
||||
*/
|
||||
addMenuAction(action, index = -1, label, icon_name) {
|
||||
try {
|
||||
@ -600,7 +603,7 @@ const Device = GObject.registerClass({
|
||||
* Remove a GAction from the top level of the device menu by action name
|
||||
*
|
||||
* @param {string} actionName - A GAction name, including scope
|
||||
* @return {number} The position the item was removed from or -1
|
||||
* @returns {number} The position the item was removed from or -1
|
||||
*/
|
||||
removeMenuAction(actionName) {
|
||||
try {
|
||||
@ -631,7 +634,7 @@ const Device = GObject.registerClass({
|
||||
/**
|
||||
* Show a device notification.
|
||||
*
|
||||
* @param {Object} params - A dictionary of notification parameters
|
||||
* @param {object} params - A dictionary of notification parameters
|
||||
* @param {number} [params.id] - A UNIX epoch timestamp (ms)
|
||||
* @param {string} [params.title] - A title
|
||||
* @param {string} [params.body] - A body
|
||||
@ -728,7 +731,7 @@ const Device = GObject.registerClass({
|
||||
/**
|
||||
* Create a transfer object.
|
||||
*
|
||||
* @return {Core.Transfer} A new transfer
|
||||
* @returns {Core.Transfer} A new transfer
|
||||
*/
|
||||
createTransfer() {
|
||||
const transfer = new Core.Transfer({device: this});
|
||||
@ -747,13 +750,11 @@ const Device = GObject.registerClass({
|
||||
* Reject the transfer payload described by @packet.
|
||||
*
|
||||
* @param {Core.Packet} packet - A packet
|
||||
* @return {Promise} A promise for the operation
|
||||
* @returns {void}
|
||||
*/
|
||||
rejectTransfer(packet) {
|
||||
if (!packet || !packet.hasPayload())
|
||||
return;
|
||||
|
||||
return this.channel.rejectTransfer(packet);
|
||||
if (packet?.hasPayload())
|
||||
return this.channel.rejectTransfer(packet);
|
||||
}
|
||||
|
||||
openPath(action, parameter) {
|
||||
@ -816,15 +817,19 @@ const Device = GObject.registerClass({
|
||||
// The device thinks we're unpaired
|
||||
} else if (this.paired) {
|
||||
this._setPaired(true);
|
||||
this._pairingTimestamp = Math.floor(Date.now() / 1000);
|
||||
this.sendPacket({
|
||||
type: 'kdeconnect.pair',
|
||||
body: {pair: true},
|
||||
body: {
|
||||
pair: true,
|
||||
timestamp: this._pairingTimestamp,
|
||||
},
|
||||
});
|
||||
this._triggerPlugins();
|
||||
|
||||
// The device is requesting pairing
|
||||
} else {
|
||||
this._notifyPairRequest();
|
||||
this._notifyPairRequest(packet.body?.timestamp);
|
||||
}
|
||||
// Device is requesting unpairing/rejecting our request
|
||||
} else {
|
||||
@ -835,11 +840,14 @@ const Device = GObject.registerClass({
|
||||
|
||||
/**
|
||||
* Notify the user of an incoming pair request and set a 30s timeout
|
||||
*
|
||||
* @param {number} [timestamp] - Timestamp for the pair request
|
||||
*/
|
||||
_notifyPairRequest() {
|
||||
_notifyPairRequest(timestamp = 0) {
|
||||
// Reset any active request
|
||||
this._resetPairRequest();
|
||||
|
||||
this._pairingTimestamp = timestamp;
|
||||
this.showNotification({
|
||||
id: 'pair-request',
|
||||
// TRANSLATORS: eg. Pair Request from Google Pixel
|
||||
@ -884,6 +892,8 @@ const Device = GObject.registerClass({
|
||||
GLib.source_remove(this._outgoingPairRequest);
|
||||
this._outgoingPairRequest = 0;
|
||||
}
|
||||
|
||||
this._pairingTimestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -931,8 +941,24 @@ const Device = GObject.registerClass({
|
||||
// If we're accepting an incoming pair request, set the internal
|
||||
// paired state and send the confirmation before loading plugins.
|
||||
if (this._incomingPairRequest) {
|
||||
this._setPaired(true);
|
||||
if (this.identity?.body.protocolVersion >= 8) {
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
const diffTimestamp = Number.abs(this._pairingTimestamp - currentTimestamp);
|
||||
if (diffTimestamp > ALLOWED_TIMESTAMP_TIME_DIFFERENCE_SECONDS) {
|
||||
this._setPaired(false);
|
||||
this.showNotification({
|
||||
id: 'pair-request',
|
||||
// TRANSLATORS: eg. Failed to pair with Google Pixel
|
||||
title: _('Failed to pair with %s').format(this.name),
|
||||
body: _('Device clocks are out of sync'),
|
||||
icon: new Gio.ThemedIcon({name: 'dialog-warning-symbolic'}),
|
||||
priority: Gio.NotificationPriority.URGENT,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._setPaired(true);
|
||||
this.sendPacket({
|
||||
type: 'kdeconnect.pair',
|
||||
body: {pair: true},
|
||||
@ -945,9 +971,13 @@ const Device = GObject.registerClass({
|
||||
} else if (!this.paired) {
|
||||
this._resetPairRequest();
|
||||
|
||||
this._pairingTimestamp = Math.floor(Date.now() / 1000);
|
||||
this.sendPacket({
|
||||
type: 'kdeconnect.pair',
|
||||
body: {pair: true},
|
||||
body: {
|
||||
pair: true,
|
||||
timestamp: this._pairingTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
this._outgoingPairRequest = GLib.timeout_add_seconds(
|
||||
|
||||
@ -9,7 +9,7 @@ import GIRepository from 'gi://GIRepository';
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
import Config from '../config.js';
|
||||
import setup, {setupGettext} from '../utils/setup.js';
|
||||
import {setup, setupGettext} from '../utils/setup.js';
|
||||
|
||||
|
||||
// Promise Wrappers
|
||||
@ -163,7 +163,7 @@ if (!globalThis.HAVE_GNOME) {
|
||||
* A simple (for now) pre-comparison sanitizer for phone numbers
|
||||
* See: https://github.com/KDE/kdeconnect-kde/blob/master/smsapp/conversationlistmodel.cpp#L200-L210
|
||||
*
|
||||
* @return {string} Return the string stripped of leading 0, and ' ()-+'
|
||||
* @returns {string} Return the string stripped of leading 0, and ' ()-+'
|
||||
*/
|
||||
String.prototype.toPhoneNumber = function () {
|
||||
const strippedNumber = this.replace(/^0*|[ ()+-]/g, '');
|
||||
@ -179,7 +179,7 @@ String.prototype.toPhoneNumber = function () {
|
||||
* A simple equality check for phone numbers based on `toPhoneNumber()`
|
||||
*
|
||||
* @param {string} number - A phone number string to compare
|
||||
* @return {boolean} If `this` and @number are equivalent phone numbers
|
||||
* @returns {boolean} If `this` and @number are equivalent phone numbers
|
||||
*/
|
||||
String.prototype.equalsPhoneNumber = function (number) {
|
||||
const a = this.toPhoneNumber();
|
||||
@ -212,12 +212,12 @@ Gio.File.rm_rf = function (file) {
|
||||
Gio.File.rm_rf(iter.get_child(info));
|
||||
|
||||
iter.close(null);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Silence errors
|
||||
}
|
||||
|
||||
file.delete(null);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Silence errors
|
||||
}
|
||||
};
|
||||
@ -227,7 +227,7 @@ Gio.File.rm_rf = function (file) {
|
||||
* Extend GLib.Variant with a static method to recursively pack a variant
|
||||
*
|
||||
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
|
||||
* @return {GLib.Variant} The resulting GVariant
|
||||
* @returns {GLib.Variant} The resulting GVariant
|
||||
*/
|
||||
function _full_pack(obj) {
|
||||
let packed;
|
||||
@ -283,7 +283,7 @@ GLib.Variant.full_pack = _full_pack;
|
||||
* Extend GLib.Variant with a method to recursively deepUnpack() a variant
|
||||
*
|
||||
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
|
||||
* @return {*} The resulting object
|
||||
* @returns {*} The resulting object
|
||||
*/
|
||||
function _full_unpack(obj) {
|
||||
obj = (obj === undefined) ? this : obj;
|
||||
@ -310,7 +310,7 @@ function _full_unpack(obj) {
|
||||
unpacked[key] = Gio.Icon.deserialize(value);
|
||||
else
|
||||
unpacked[key] = _full_unpack(value);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
unpacked[key] = _full_unpack(value);
|
||||
}
|
||||
}
|
||||
@ -326,8 +326,8 @@ GLib.Variant.prototype.full_unpack = _full_unpack;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a GTlsCertificate from the PEM-encoded data in @cert_path and
|
||||
* @key_path. If either are missing a new pair will be generated.
|
||||
* Creates a GTlsCertificate from the PEM-encoded data in %cert_path and
|
||||
* %key_path. If either are missing a new pair will be generated.
|
||||
*
|
||||
* Additionally, the private key will be added using ssh-add to allow sftp
|
||||
* connections using Gio.
|
||||
@ -337,9 +337,16 @@ GLib.Variant.prototype.full_unpack = _full_unpack;
|
||||
* @param {string} certPath - Absolute path to a x509 certificate in PEM format
|
||||
* @param {string} keyPath - Absolute path to a private key in PEM format
|
||||
* @param {string} commonName - A unique common name for the certificate
|
||||
* @return {Gio.TlsCertificate} A TLS certificate
|
||||
* @returns {Gio.TlsCertificate} A TLS certificate
|
||||
*/
|
||||
Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) {
|
||||
if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) {
|
||||
const error = new Error();
|
||||
error.name = _('OpenSSL not found');
|
||||
error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check if the certificate/key pair already exists
|
||||
const certExists = GLib.file_test(certPath, GLib.FileTest.EXISTS);
|
||||
const keyExists = GLib.file_test(keyPath, GLib.FileTest.EXISTS);
|
||||
@ -348,17 +355,18 @@ Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = nul
|
||||
if (!certExists || !keyExists) {
|
||||
// If we weren't passed a common name, generate a random one
|
||||
if (!commonName)
|
||||
commonName = GLib.uuid_string_random();
|
||||
commonName = GLib.uuid_string_random().replaceAll('-', '');
|
||||
|
||||
const proc = new Gio.Subprocess({
|
||||
argv: [
|
||||
Config.OPENSSL_PATH, 'req',
|
||||
'-new', '-x509', '-sha256',
|
||||
'-out', certPath,
|
||||
'-newkey', 'rsa:4096', '-nodes',
|
||||
'-newkey', 'ec',
|
||||
'-pkeyopt', 'ec_paramgen_curve:prime256v1',
|
||||
'-keyout', keyPath,
|
||||
'-new', '-x509', '-nodes',
|
||||
'-days', '3650',
|
||||
'-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`,
|
||||
'-out', certPath,
|
||||
],
|
||||
flags: (Gio.SubprocessFlags.STDOUT_SILENCE |
|
||||
Gio.SubprocessFlags.STDERR_SILENCE),
|
||||
@ -396,7 +404,7 @@ Object.defineProperties(Gio.TlsCertificate.prototype, {
|
||||
/**
|
||||
* Get just the pubkey as a DER ByteArray of a certificate.
|
||||
*
|
||||
* @return {GLib.Bytes} The pubkey as DER of the certificate.
|
||||
* @returns {GLib.Bytes} The pubkey as DER of the certificate.
|
||||
*/
|
||||
'pubkey_der': {
|
||||
value: function () {
|
||||
|
||||
@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import Config from '../config.js';
|
||||
import * as Core from './core.js';
|
||||
import * as DBus from './utils/dbus.js';
|
||||
import Device from './device.js';
|
||||
|
||||
@ -42,6 +43,13 @@ const Manager = GObject.registerClass({
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'certificate': GObject.ParamSpec.object(
|
||||
'certificate',
|
||||
'Certificate',
|
||||
'The local TLS certificate',
|
||||
GObject.ParamFlags.READABLE,
|
||||
Gio.TlsCertificate
|
||||
),
|
||||
'discoverable': GObject.ParamSpec.boolean(
|
||||
'discoverable',
|
||||
'Discoverable',
|
||||
@ -53,7 +61,7 @@ const Manager = GObject.registerClass({
|
||||
'id',
|
||||
'Id',
|
||||
'The hostname or other network unique id',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.ParamFlags.READABLE,
|
||||
null
|
||||
),
|
||||
'name': GObject.ParamSpec.string(
|
||||
@ -92,6 +100,17 @@ const Manager = GObject.registerClass({
|
||||
return this._backends;
|
||||
}
|
||||
|
||||
get certificate() {
|
||||
if (this._certificate === undefined) {
|
||||
this._certificate = Gio.TlsCertificate.new_for_paths(
|
||||
GLib.build_filenamev([Config.CONFIGDIR, 'certificate.pem']),
|
||||
GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']),
|
||||
null);
|
||||
}
|
||||
|
||||
return this._certificate;
|
||||
}
|
||||
|
||||
get debug() {
|
||||
if (this._debug === undefined)
|
||||
this._debug = this.settings.get_boolean('debug');
|
||||
@ -156,18 +175,7 @@ const Manager = GObject.registerClass({
|
||||
}
|
||||
|
||||
get id() {
|
||||
if (this._id === undefined)
|
||||
this._id = this.settings.get_string('id');
|
||||
|
||||
return this._id;
|
||||
}
|
||||
|
||||
set id(value) {
|
||||
if (this.id === value)
|
||||
return;
|
||||
|
||||
this._id = value;
|
||||
this.notify('id');
|
||||
return this.certificate.common_name;
|
||||
}
|
||||
|
||||
get name() {
|
||||
@ -217,17 +225,12 @@ const Manager = GObject.registerClass({
|
||||
* GSettings
|
||||
*/
|
||||
_initSettings() {
|
||||
// Initialize the ID and name of the service
|
||||
if (this.settings.get_string('id').length === 0)
|
||||
this.settings.set_string('id', GLib.uuid_string_random());
|
||||
|
||||
if (this.settings.get_string('name').length === 0)
|
||||
this.settings.set_string('name', GLib.get_host_name());
|
||||
|
||||
// Bound Properties
|
||||
this.settings.bind('debug', this, 'debug', 0);
|
||||
this.settings.bind('discoverable', this, 'discoverable', 0);
|
||||
this.settings.bind('id', this, 'id', 0);
|
||||
this.settings.bind('name', this, 'name', 0);
|
||||
}
|
||||
|
||||
@ -382,7 +385,7 @@ const Manager = GObject.registerClass({
|
||||
* of known devices if it doesn't exist.
|
||||
*
|
||||
* @param {Core.Packet} packet - An identity packet for the device
|
||||
* @return {Device} A device object
|
||||
* @returns {Device} A device object
|
||||
*/
|
||||
_ensureDevice(packet) {
|
||||
let device = this.devices.get(packet.body.deviceId);
|
||||
@ -422,7 +425,7 @@ const Manager = GObject.registerClass({
|
||||
*/
|
||||
_removeDevice(id) {
|
||||
// Delete all GSettings
|
||||
const settings_path = `/org/gnome/shell/extensions/gsconnect/${id}/`;
|
||||
const settings_path = `/org/gnome/shell/extensions/gsconnect/device/${id}/`;
|
||||
GLib.spawn_command_line_async(`dconf reset -f ${settings_path}`);
|
||||
|
||||
// Delete the cache
|
||||
@ -438,7 +441,7 @@ const Manager = GObject.registerClass({
|
||||
* A GSourceFunc that tries to reconnect to each paired device, while
|
||||
* pruning unpaired devices that have disconnected.
|
||||
*
|
||||
* @return {boolean} Always %true
|
||||
* @returns {boolean} Always %true
|
||||
*/
|
||||
_reconnect() {
|
||||
for (const [id, device] of this.devices) {
|
||||
|
||||
@ -14,7 +14,7 @@ import system from 'system';
|
||||
let GioUnix;
|
||||
try {
|
||||
GioUnix = (await import('gi://GioUnix?version=2.0')).default;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
GioUnix = {
|
||||
InputStream: Gio.UnixInputStream,
|
||||
OutputStream: Gio.UnixOutputStream,
|
||||
@ -142,7 +142,7 @@ const NativeMessagingHost = GObject.registerClass({
|
||||
}
|
||||
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.quit();
|
||||
}
|
||||
}
|
||||
@ -152,7 +152,7 @@ const NativeMessagingHost = GObject.registerClass({
|
||||
const data = JSON.stringify(message);
|
||||
this._stdout.put_int32(data.length, null);
|
||||
this._stdout.put_string(data, null);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.quit();
|
||||
}
|
||||
}
|
||||
@ -186,7 +186,7 @@ const NativeMessagingHost = GObject.registerClass({
|
||||
_proxyGetter(name) {
|
||||
try {
|
||||
return this.get_cached_property(name).unpack();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -222,4 +222,3 @@ const NativeMessagingHost = GObject.registerClass({
|
||||
|
||||
// NOTE: must not pass ARGV
|
||||
await (new NativeMessagingHost()).runAsync([system.programInvocationName]);
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import Config from '../config.js';
|
||||
import * as Core from './core.js';
|
||||
import plugins from './plugins/index.js';
|
||||
|
||||
|
||||
@ -248,4 +249,3 @@ const Plugin = GObject.registerClass({
|
||||
});
|
||||
|
||||
export default Plugin;
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import * as Components from '../components/index.js';
|
||||
import * as Core from '../core.js';
|
||||
import Plugin from '../plugin.js';
|
||||
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import * as Core from '../core.js';
|
||||
import Plugin from '../plugin.js';
|
||||
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import GObject from 'gi://GObject';
|
||||
|
||||
import Plugin from '../plugin.js';
|
||||
import Contacts from '../components/contacts.js';
|
||||
import * as Core from '../core.js';
|
||||
|
||||
/*
|
||||
* We prefer libebook's vCard parser if it's available
|
||||
@ -18,7 +19,7 @@ export const setEBookContacts = (ebook) => { // This function is only for tests
|
||||
|
||||
try {
|
||||
EBookContacts = (await import('gi://EBookContacts')).default;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
EBookContacts = null;
|
||||
}
|
||||
|
||||
@ -150,7 +151,7 @@ const ContactsPlugin = GObject.registerClass({
|
||||
* See: https://github.com/mathiasbynens/quoted-printable/blob/master/src/quoted-printable.js
|
||||
*
|
||||
* @param {string} input - The QUOTED-PRINTABLE string
|
||||
* @return {string} The decoded string
|
||||
* @returns {string} The decoded string
|
||||
*/
|
||||
_decodeQuotedPrintable(input) {
|
||||
return input
|
||||
@ -171,7 +172,7 @@ const ContactsPlugin = GObject.registerClass({
|
||||
* See: https://github.com/kvz/locutus/blob/master/src/php/xml/utf8_decode.js
|
||||
*
|
||||
* @param {string} input - The UTF-8 string
|
||||
* @return {string} The decoded string
|
||||
* @returns {string} The decoded string
|
||||
*/
|
||||
_decodeUTF8(input) {
|
||||
try {
|
||||
@ -215,7 +216,7 @@ const ContactsPlugin = GObject.registerClass({
|
||||
return output.join('');
|
||||
|
||||
// Fallback to old unfaithful
|
||||
} catch (e) {
|
||||
} catch {
|
||||
try {
|
||||
return decodeURIComponent(escape(input));
|
||||
|
||||
@ -233,7 +234,7 @@ const ContactsPlugin = GObject.registerClass({
|
||||
* See: http://jsfiddle.net/ARTsinn/P2t2P/
|
||||
*
|
||||
* @param {string} vcard_data - The raw VCard data
|
||||
* @return {Object} dictionary of vCard data
|
||||
* @returns {object} dictionary of vCard data
|
||||
*/
|
||||
_parseVCard21(vcard_data) {
|
||||
// vcard skeleton
|
||||
|
||||
@ -183,7 +183,7 @@ const MousepadPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Handle a input event.
|
||||
*
|
||||
* @param {Object} input - The body of a `kdeconnect.mousepad.request`
|
||||
* @param {object} input - The body of a `kdeconnect.mousepad.request`
|
||||
*/
|
||||
_handleInput(input) {
|
||||
if (!this.settings.get_boolean('share-control'))
|
||||
@ -285,7 +285,7 @@ const MousepadPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Handle an echo/ACK of a event we sent, displaying it the dialog entry.
|
||||
*
|
||||
* @param {Object} input - The body of a `kdeconnect.mousepad.echo`
|
||||
* @param {object} input - The body of a `kdeconnect.mousepad.echo`
|
||||
*/
|
||||
_handleEcho(input) {
|
||||
if (!this._dialog || !this._dialog.visible)
|
||||
@ -308,7 +308,7 @@ const MousepadPlugin = GObject.registerClass({
|
||||
* Handle a state change from the remote keyboard. This is an indication
|
||||
* that the remote keyboard is ready to accept input.
|
||||
*
|
||||
* @param {Object} packet - A `kdeconnect.mousepad.keyboardstate` packet
|
||||
* @param {object} packet - A `kdeconnect.mousepad.keyboardstate` packet
|
||||
*/
|
||||
_handleState(packet) {
|
||||
this._state = !!packet.body.state;
|
||||
@ -318,7 +318,7 @@ const MousepadPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Send an echo/ACK of @input, if requested
|
||||
*
|
||||
* @param {Object} input - The body of a 'kdeconnect.mousepad.request'
|
||||
* @param {object} input - The body of a 'kdeconnect.mousepad.request'
|
||||
*/
|
||||
_sendEcho(input) {
|
||||
if (!input.sendAck)
|
||||
@ -335,8 +335,6 @@ const MousepadPlugin = GObject.registerClass({
|
||||
|
||||
/**
|
||||
* Send the local keyboard state
|
||||
*
|
||||
* @param {boolean} state - Whether we're ready to accept input
|
||||
*/
|
||||
_sendState() {
|
||||
this.device.sendPacket({
|
||||
|
||||
@ -8,6 +8,7 @@ import GObject from 'gi://GObject';
|
||||
|
||||
import * as Components from '../components/index.js';
|
||||
import Config from '../../config.js';
|
||||
import * as Core from '../core.js';
|
||||
import * as DBus from '../utils/dbus.js';
|
||||
import {Player} from '../components/mpris.js';
|
||||
import Plugin from '../plugin.js';
|
||||
@ -42,6 +43,8 @@ const MPRISPlugin = GObject.registerClass({
|
||||
this._transferring = new WeakSet();
|
||||
this._updating = new WeakSet();
|
||||
|
||||
this._queueTimers = new Map();
|
||||
|
||||
this._mpris = Components.acquire('mpris');
|
||||
|
||||
this._playerAddedId = this._mpris.connect(
|
||||
@ -75,6 +78,12 @@ const MPRISPlugin = GObject.registerClass({
|
||||
disconnected() {
|
||||
super.disconnected();
|
||||
|
||||
for (const [identity, timer] of this._queueTimers) {
|
||||
if (timer)
|
||||
GLib.source_remove(timer);
|
||||
this._queueTimers.delete(identity);
|
||||
}
|
||||
|
||||
for (const [identity, player] of this._players) {
|
||||
this._players.delete(identity);
|
||||
player.destroy();
|
||||
@ -144,7 +153,7 @@ const MPRISPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Handle an update for a remote player.
|
||||
*
|
||||
* @param {Object} packet - A `kdeconnect.mpris` packet
|
||||
* @param {object} packet - A `kdeconnect.mpris` packet
|
||||
*/
|
||||
_handlePlayerUpdate(packet) {
|
||||
const player = this._players.get(packet.body.player);
|
||||
@ -174,7 +183,7 @@ const MPRISPlugin = GObject.registerClass({
|
||||
* Handle a request for player information or action.
|
||||
*
|
||||
* @param {Core.Packet} packet - a `kdeconnect.mpris.request`
|
||||
* @return {undefined} no return value
|
||||
* @returns {undefined} no return value
|
||||
*/
|
||||
_handleRequest(packet) {
|
||||
// A request for the list of players
|
||||
@ -258,18 +267,35 @@ const MPRISPlugin = GObject.registerClass({
|
||||
}
|
||||
}
|
||||
|
||||
// Information Request
|
||||
let hasResponse = false;
|
||||
if (packet.body.hasOwnProperty('requestNowPlaying') ||
|
||||
packet.body.hasOwnProperty('requestVolume')) {
|
||||
const response = this._getUpdate(player.Identity, packet);
|
||||
this._sendUpdate(player, response);
|
||||
}
|
||||
|
||||
const response = {
|
||||
type: 'kdeconnect.mpris',
|
||||
body: {
|
||||
player: packet.body.player,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
debug(e, this.device.name);
|
||||
} finally {
|
||||
this._updating.delete(player);
|
||||
}
|
||||
}
|
||||
|
||||
// Respond to information request (or push updated information)
|
||||
_getUpdate(identity, packet) {
|
||||
|
||||
const player = this._mpris?.getPlayer(identity);
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
const response = {
|
||||
type: 'kdeconnect.mpris',
|
||||
body: {
|
||||
player: player.Identity,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
if (packet.body.hasOwnProperty('requestNowPlaying')) {
|
||||
hasResponse = true;
|
||||
|
||||
Object.assign(response.body, {
|
||||
pos: Math.floor(player.Position / 1000),
|
||||
@ -330,31 +356,61 @@ const MPRISPlugin = GObject.registerClass({
|
||||
}
|
||||
}
|
||||
|
||||
if (packet.body.hasOwnProperty('requestVolume')) {
|
||||
hasResponse = true;
|
||||
if (packet.body.hasOwnProperty('requestVolume'))
|
||||
response.body.volume = Math.floor(player.Volume * 100);
|
||||
}
|
||||
|
||||
if (hasResponse)
|
||||
this.device.sendPacket(response);
|
||||
return response;
|
||||
} catch (e) {
|
||||
debug(e, this.device.name);
|
||||
} finally {
|
||||
this._updating.delete(player);
|
||||
}
|
||||
}
|
||||
|
||||
_sendUpdate(player, packet = null) {
|
||||
if (!player || (!packet && this._updating.has(player)))
|
||||
return GLib.SOURCE_REMOVE;
|
||||
|
||||
debug(`Sending update for ${player.Identity}`);
|
||||
this._updating.add(player);
|
||||
|
||||
if (this._queueTimers.has(player.Identity)) {
|
||||
const timer_id = this._queueTimers.get(player.Identity);
|
||||
if (timer_id) {
|
||||
debug(`Stopping timer id ${timer_id}`);
|
||||
GLib.source_remove(timer_id);
|
||||
}
|
||||
this._queueTimers.delete(player.Identity);
|
||||
}
|
||||
if (!packet) {
|
||||
packet = this._getUpdate(player.Identity, {
|
||||
body: {
|
||||
requestNowPlaying: true,
|
||||
requestVolume: true,
|
||||
},
|
||||
}, false);
|
||||
}
|
||||
this.device.sendPacket(packet);
|
||||
|
||||
this._updating.delete(player);
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
_onPlayerChanged(mpris, player) {
|
||||
if (!this.settings.get_boolean('share-players'))
|
||||
return;
|
||||
|
||||
this._handleCommand({
|
||||
body: {
|
||||
player: player.Identity,
|
||||
requestNowPlaying: true,
|
||||
requestVolume: true,
|
||||
},
|
||||
});
|
||||
// Set a timer to send the updated state after a short delay.
|
||||
// Allows further state changes to be bundled into a single packet.
|
||||
if (this._queueTimers.has(player.Identity))
|
||||
return;
|
||||
this._queueTimers.set(player.Identity, 0);
|
||||
|
||||
const timer_id = GLib.timeout_add(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
250, // ms (0.25 seconds)
|
||||
this._sendUpdate.bind(this, player)
|
||||
);
|
||||
this._queueTimers.set(player.Identity, timer_id);
|
||||
debug(`Set update timer id ${timer_id} for ${player.Identity}`);
|
||||
}
|
||||
|
||||
_onPlayerSeeked(mpris, player, offset) {
|
||||
@ -434,6 +490,11 @@ const MPRISPlugin = GObject.registerClass({
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const [identity, timer] of this._queueTimers) {
|
||||
this._queueTimers.delete(identity);
|
||||
GLib.source_remove(timer);
|
||||
}
|
||||
|
||||
if (this._mpris !== undefined) {
|
||||
this._mpris.disconnect(this._playerAddedId);
|
||||
this._mpris.disconnect(this._playerRemovedId);
|
||||
|
||||
@ -8,6 +8,7 @@ import GObject from 'gi://GObject';
|
||||
import Gtk from 'gi://Gtk';
|
||||
|
||||
import * as Components from '../components/index.js';
|
||||
import * as Core from '../core.js';
|
||||
import Config from '../../config.js';
|
||||
import Plugin from '../plugin.js';
|
||||
import ReplyDialog from '../ui/notification.js';
|
||||
@ -103,7 +104,7 @@ const SMS_APPS = [
|
||||
* Try to determine if an notification is from an SMS app
|
||||
*
|
||||
* @param {Core.Packet} packet - A `kdeconnect.notification`
|
||||
* @return {boolean} Whether the notification is from an SMS app
|
||||
* @returns {boolean} Whether the notification is from an SMS app
|
||||
*/
|
||||
function _isSmsNotification(packet) {
|
||||
const id = packet.body.id;
|
||||
@ -123,8 +124,8 @@ function _isSmsNotification(packet) {
|
||||
/**
|
||||
* Remove a local libnotify or Gtk notification.
|
||||
*
|
||||
* @param {String|Number} id - Gtk (string) or libnotify id (uint32)
|
||||
* @param {String|null} application - Application Id if Gtk or null
|
||||
* @param {string | number} id - Gtk (string) or libnotify id (uint32)
|
||||
* @param {string | null} application - Application Id if Gtk or null
|
||||
*/
|
||||
function _removeNotification(id, application = null) {
|
||||
let name, path, method, variant;
|
||||
@ -416,7 +417,7 @@ const NotificationPlugin = GObject.registerClass({
|
||||
*
|
||||
* @param {Core.Packet} packet - A `kdeconnect.notification`
|
||||
* @param {Gio.Icon|string|null} icon - An icon or %null
|
||||
* @return {Promise} A promise for the operation
|
||||
* @returns {Promise} A promise for the operation
|
||||
*/
|
||||
_uploadIcon(packet, icon = null) {
|
||||
// Normalize strings into GIcons
|
||||
@ -438,7 +439,7 @@ const NotificationPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Send a local notification to the remote device.
|
||||
*
|
||||
* @param {Object} notif - A dictionary of notification parameters
|
||||
* @param {object} notif - A dictionary of notification parameters
|
||||
* @param {string} notif.appName - The notifying application
|
||||
* @param {string} notif.id - The notification ID
|
||||
* @param {string} notif.title - The notification title
|
||||
@ -631,7 +632,7 @@ const NotificationPlugin = GObject.registerClass({
|
||||
*
|
||||
* @param {string} uuid - The requestReplyId for the repliable notification
|
||||
* @param {string} message - The message to reply with
|
||||
* @param {Object} notification - The original notification packet
|
||||
* @param {object} notification - The original notification packet
|
||||
*/
|
||||
replyNotification(uuid, message, notification) {
|
||||
// If this happens for some reason, things will explode
|
||||
|
||||
@ -150,14 +150,14 @@ const RunCommandPlugin = GObject.registerClass({
|
||||
* Parse the response to a request for the remote command list. Remove the
|
||||
* command menu if there are no commands, otherwise amend the menu.
|
||||
*
|
||||
* @param {string|Object[]} commandList - A list of remote commands
|
||||
* @param {string | object[]} commandList - A list of remote commands
|
||||
*/
|
||||
_handleCommandList(commandList) {
|
||||
// See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051
|
||||
if (typeof commandList === 'string') {
|
||||
try {
|
||||
commandList = JSON.parse(commandList);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
commandList = {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import Config from '../../config.js';
|
||||
import * as Core from '../core.js';
|
||||
import Plugin from '../plugin.js';
|
||||
|
||||
|
||||
@ -246,7 +247,7 @@ const SFTPPlugin = GObject.registerClass({
|
||||
* Add GSConnect's private key identity to the authentication agent so our
|
||||
* identity can be verified by Android during private key authentication.
|
||||
*
|
||||
* @return {Promise} A promise for the operation
|
||||
* @returns {Promise} A promise for the operation
|
||||
*/
|
||||
async _addPrivateKey() {
|
||||
const ssh_add = this._launcher.spawnv([
|
||||
|
||||
@ -443,7 +443,7 @@ const FileChooserDialog = GObject.registerClass({
|
||||
chooser.preview_widget.pixbuf = pixbuf;
|
||||
chooser.preview_widget.visible = true;
|
||||
chooser.preview_widget_active = true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
chooser.preview_widget.visible = false;
|
||||
chooser.preview_widget_active = false;
|
||||
}
|
||||
|
||||
@ -206,7 +206,7 @@ const SMSPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Handle a digest of threads.
|
||||
*
|
||||
* @param {Object[]} messages - A list of message objects
|
||||
* @param {object[]} messages - A list of message objects
|
||||
* @param {string[]} thread_ids - A list of thread IDs as strings
|
||||
*/
|
||||
_handleDigest(messages, thread_ids) {
|
||||
@ -244,7 +244,7 @@ const SMSPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Handle a new single message
|
||||
*
|
||||
* @param {Object} message - A message object
|
||||
* @param {object} message - A message object
|
||||
*/
|
||||
_handleMessage(message) {
|
||||
let conversation = null;
|
||||
@ -261,7 +261,7 @@ const SMSPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Parse a conversation (thread of messages) and sort them
|
||||
*
|
||||
* @param {Object[]} thread - A list of sms message objects from a thread
|
||||
* @param {object[]} thread - A list of sms message objects from a thread
|
||||
*/
|
||||
_handleThread(thread) {
|
||||
// If there are no addresses this will cause major problems...
|
||||
@ -300,7 +300,7 @@ const SMSPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Handle a response to telephony.request_conversation(s)
|
||||
*
|
||||
* @param {Object[]} messages - A list of sms message objects
|
||||
* @param {object[]} messages - A list of sms message objects
|
||||
*/
|
||||
_handleMessages(messages) {
|
||||
try {
|
||||
@ -394,7 +394,7 @@ const SMSPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Send a message
|
||||
*
|
||||
* @param {Object[]} addresses - A list of address objects
|
||||
* @param {object[]} addresses - A list of address objects
|
||||
* @param {string} messageBody - The message text
|
||||
* @param {number} [event] - An event bitmask
|
||||
* @param {boolean} [forceSms] - Whether to force SMS
|
||||
@ -508,8 +508,8 @@ const SMSPlugin = GObject.registerClass({
|
||||
/**
|
||||
* Try to find a thread_id in @smsPlugin for @addresses.
|
||||
*
|
||||
* @param {Object[]} addresses - a list of address objects
|
||||
* @return {string|null} a thread ID
|
||||
* @param {object[]} addresses - a list of address objects
|
||||
* @returns {string|null} a thread ID
|
||||
*/
|
||||
getThreadIdForAddresses(addresses = []) {
|
||||
const threads = Object.values(this.threads);
|
||||
|
||||
@ -2,13 +2,27 @@
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import GIRepository from 'gi://GIRepository';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import * as Components from '../components/index.js';
|
||||
import Config from '../../config.js';
|
||||
import * as Core from '../core.js';
|
||||
import Plugin from '../plugin.js';
|
||||
|
||||
|
||||
let Gvc = null;
|
||||
try {
|
||||
// Add gnome-shell's typelib dir to the search path
|
||||
const typelibDir = GLib.build_filenamev([Config.GNOME_SHELL_LIBDIR, 'gnome-shell']);
|
||||
GIRepository.Repository.prepend_search_path(typelibDir);
|
||||
GIRepository.Repository.prepend_library_path(typelibDir);
|
||||
|
||||
Gvc = (await import('gi://Gvc')).default;
|
||||
} catch {}
|
||||
|
||||
|
||||
export const Metadata = {
|
||||
label: _('System Volume'),
|
||||
description: _('Enable the paired device to control the system volume'),
|
||||
@ -73,12 +87,6 @@ const SystemVolumePlugin = GObject.registerClass({
|
||||
}
|
||||
}
|
||||
|
||||
connected() {
|
||||
super.connected();
|
||||
|
||||
this._sendSinkList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to change an output
|
||||
*
|
||||
@ -121,7 +129,7 @@ const SystemVolumePlugin = GObject.registerClass({
|
||||
* Update the cache for @stream
|
||||
*
|
||||
* @param {Gvc.MixerStream} stream - The stream to cache
|
||||
* @return {Object} The updated cache object
|
||||
* @returns {object} The updated cache object
|
||||
*/
|
||||
_updateCache(stream) {
|
||||
const state = {
|
||||
|
||||
@ -8,6 +8,7 @@ import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import * as Components from '../components/index.js';
|
||||
import * as Core from '../core.js';
|
||||
import Plugin from '../plugin.js';
|
||||
|
||||
|
||||
@ -114,7 +115,7 @@ const TelephonyPlugin = GObject.registerClass({
|
||||
* Load a Gdk.Pixbuf from base64 encoded data
|
||||
*
|
||||
* @param {string} data - Base64 encoded JPEG data
|
||||
* @return {Gdk.Pixbuf|null} A contact photo
|
||||
* @returns {GdkPixbuf.Pixbuf|null} A contact photo
|
||||
*/
|
||||
_getThumbnailPixbuf(data) {
|
||||
const loader = new GdkPixbuf.PixbufLoader();
|
||||
|
||||
@ -16,7 +16,7 @@ import system from 'system';
|
||||
*
|
||||
* @param {*} [salt] - If not %null, will be used as salt for generating a color
|
||||
* @param {number} alpha - A value in the [0...1] range for the alpha channel
|
||||
* @return {Gdk.RGBA} A new Gdk.RGBA object generated from the input
|
||||
* @returns {Gdk.RGBA} A new Gdk.RGBA object generated from the input
|
||||
*/
|
||||
function randomRGBA(salt = null, alpha = 1.0) {
|
||||
let red, green, blue;
|
||||
@ -41,7 +41,7 @@ function randomRGBA(salt = null, alpha = 1.0) {
|
||||
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
*
|
||||
* @param {Gdk.RGBA} rgba - A GdkRGBA object
|
||||
* @return {number} The relative luminance of the color
|
||||
* @returns {number} The relative luminance of the color
|
||||
*/
|
||||
function relativeLuminance(rgba) {
|
||||
const {red, green, blue} = rgba;
|
||||
@ -59,7 +59,7 @@ function relativeLuminance(rgba) {
|
||||
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
||||
*
|
||||
* @param {Gdk.RGBA} rgba - A GdkRGBA object for the background color
|
||||
* @return {Gdk.RGBA} A GdkRGBA object for the foreground color
|
||||
* @returns {Gdk.RGBA} A GdkRGBA object for the foreground color
|
||||
*/
|
||||
function getFgRGBA(rgba) {
|
||||
const bgLuminance = relativeLuminance(rgba);
|
||||
@ -78,7 +78,7 @@ function getFgRGBA(rgba) {
|
||||
* @param {string} path - A local file path
|
||||
* @param {number} size - Size in pixels
|
||||
* @param {scale} [scale] - Scale factor for the size
|
||||
* @return {Gdk.Pixbuf} A pixbuf
|
||||
* @returns {Gdk.Pixbuf} A pixbuf
|
||||
*/
|
||||
function getPixbufForPath(path, size, scale = 1.0) {
|
||||
let data, loader;
|
||||
@ -107,6 +107,15 @@ function getPixbufForPath(path, size, scale = 1.0) {
|
||||
return pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the GdkPixbuf for a named icon
|
||||
*
|
||||
* @param {string} name - The icon name to load
|
||||
* @param {number} size - The pixel size requested
|
||||
* @param {number} scale - The scale multiplier
|
||||
* @param {string} bgColor - The background color the icon will be used against
|
||||
* @returns {GdkPixbuf.pixbuf|null} The icon image
|
||||
*/
|
||||
function getPixbufForIcon(name, size, scale, bgColor) {
|
||||
const color = getFgRGBA(bgColor);
|
||||
const theme = Gtk.IconTheme.get_default();
|
||||
@ -126,7 +135,7 @@ function getPixbufForIcon(name, size, scale, bgColor) {
|
||||
* See: http://www.ietf.org/rfc/rfc2426.txt
|
||||
*
|
||||
* @param {string} type - An RFC2426 phone number type
|
||||
* @return {string} A localized string like 'Mobile'
|
||||
* @returns {string} A localized string like 'Mobile'
|
||||
*/
|
||||
function getNumberTypeLabel(type) {
|
||||
if (type.includes('fax'))
|
||||
@ -152,9 +161,9 @@ function getNumberTypeLabel(type) {
|
||||
/**
|
||||
* Get a display number from @contact for @address.
|
||||
*
|
||||
* @param {Object} contact - A contact object
|
||||
* @param {object} contact - A contact object
|
||||
* @param {string} address - A phone number
|
||||
* @return {string} A (possibly) better display number for the address
|
||||
* @returns {string} A (possibly) better display number for the address
|
||||
*/
|
||||
export function getDisplayNumber(contact, address) {
|
||||
const number = address.toPhoneNumber();
|
||||
@ -623,7 +632,7 @@ export const ContactChooser = GObject.registerClass({
|
||||
/**
|
||||
* Get a dictionary of number-contact pairs for each selected phone number.
|
||||
*
|
||||
* @return {Object[]} A dictionary of contacts
|
||||
* @returns {object[]} A dictionary of contacts
|
||||
*/
|
||||
getSelected() {
|
||||
try {
|
||||
@ -639,4 +648,3 @@ export const ContactChooser = GObject.registerClass({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ const _cFormat = new Intl.DateTimeFormat('default', {
|
||||
* Return a human-readable timestamp, formatted for longer contexts.
|
||||
*
|
||||
* @param {number} time - Milliseconds since the epoch (local time)
|
||||
* @return {string} A localized timestamp similar to what Android Messages uses
|
||||
* @returns {string} A localized timestamp similar to what Android Messages uses
|
||||
*/
|
||||
function getTime(time) {
|
||||
const date = new Date(time);
|
||||
@ -134,7 +134,7 @@ function getTime(time) {
|
||||
* Return a human-readable timestamp, formatted for shorter contexts.
|
||||
*
|
||||
* @param {number} time - Milliseconds since the epoch (local time)
|
||||
* @return {string} A localized timestamp similar to what Android Messages uses
|
||||
* @returns {string} A localized timestamp similar to what Android Messages uses
|
||||
*/
|
||||
function getShortTime(time) {
|
||||
const date = new Date(time);
|
||||
@ -175,13 +175,19 @@ function getShortTime(time) {
|
||||
* Return a human-readable timestamp, similar to `strftime()` with `%c`.
|
||||
*
|
||||
* @param {number} time - Milliseconds since the epoch (local time)
|
||||
* @return {string} A localized timestamp
|
||||
* @returns {string} A localized timestamp
|
||||
*/
|
||||
function getDetailedTime(time) {
|
||||
return _cFormat.format(time);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make the avatar for an incoming message visible or invisible
|
||||
*
|
||||
* @param {ConversationMessage} row - The message row to modify
|
||||
* @param {boolean} visible - Whether the avatar should be visible
|
||||
*/
|
||||
function setAvatarVisible(row, visible) {
|
||||
const incoming = row.message.type === Sms.MessageBox.INBOX;
|
||||
|
||||
@ -546,8 +552,8 @@ const Conversation = GObject.registerClass({
|
||||
* Create a message row, ensuring a contact object has been retrieved or
|
||||
* generated for the message.
|
||||
*
|
||||
* @param {Object} message - A dictionary of message data
|
||||
* @return {ConversationMessage} A message row
|
||||
* @param {object} message - A dictionary of message data
|
||||
* @returns {ConversationMessage} A message row
|
||||
*/
|
||||
_createMessageRow(message) {
|
||||
// Ensure we have a contact
|
||||
@ -655,7 +661,7 @@ const Conversation = GObject.registerClass({
|
||||
/**
|
||||
* Log the next message in the conversation.
|
||||
*
|
||||
* @param {Object} message - A message object
|
||||
* @param {object} message - A message object
|
||||
*/
|
||||
logNext(message) {
|
||||
try {
|
||||
@ -1073,8 +1079,8 @@ export const Window = GObject.registerClass({
|
||||
/**
|
||||
* Find the thread row for @contacts
|
||||
*
|
||||
* @param {Object[]} contacts - A contact group
|
||||
* @return {ConversationSummary|null} The thread row or %null
|
||||
* @param {object[]} contacts - A contact group
|
||||
* @returns {ConversationSummary|null} The thread row or %null
|
||||
*/
|
||||
_getRowForContacts(contacts) {
|
||||
const addresses = Object.keys(contacts).map(address => {
|
||||
@ -1156,8 +1162,8 @@ export const Window = GObject.registerClass({
|
||||
/**
|
||||
* Try and find an existing conversation widget for @message.
|
||||
*
|
||||
* @param {Object} message - A message object
|
||||
* @return {Conversation|null} A conversation widget or %null
|
||||
* @param {object} message - A message object
|
||||
* @returns {Conversation|null} A conversation widget or %null
|
||||
*/
|
||||
getConversationForMessage(message) {
|
||||
// TODO: This shouldn't happen?
|
||||
@ -1317,4 +1323,3 @@ export const ConversationChooser = GObject.registerClass({
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -11,12 +11,26 @@ import GObject from 'gi://GObject';
|
||||
/*
|
||||
* Some utility methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a label from kebab-case to CamelCase.
|
||||
* Will also remove snake_case separators (without capitalizing)
|
||||
*
|
||||
* @param {string} string - The label to reformat
|
||||
* @returns {string} The CamelCased label
|
||||
*/
|
||||
function toDBusCase(string) {
|
||||
return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => {
|
||||
return ltr.toUpperCase();
|
||||
}).replace(/[\s_-]+/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a label from CamelCase to snake_case.
|
||||
*
|
||||
* @param {string} string - The label to reformat
|
||||
* @returns {string} - The snake_cased label
|
||||
*/
|
||||
function toUnderscoreCase(string) {
|
||||
return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => {
|
||||
if (ltr === '_')
|
||||
@ -95,7 +109,7 @@ export const Interface = GObject.registerClass({
|
||||
* Invoke an instance's method for a DBus method call.
|
||||
*
|
||||
* @param {Gio.DBusInterfaceInfo} info - The DBus interface
|
||||
* @param {DBus.Interface} iface - The DBus interface
|
||||
* @param {Gio.DBusInterface} iface - The DBus interface
|
||||
* @param {string} name - The DBus method name
|
||||
* @param {GLib.Variant} parameters - The method parameters
|
||||
* @param {Gio.DBusMethodInvocation} invocation - The method invocation info
|
||||
@ -231,7 +245,7 @@ export const Interface = GObject.registerClass({
|
||||
*
|
||||
* @param {Gio.BusType} [busType] - a Gio.BusType constant
|
||||
* @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable
|
||||
* @return {Promise<Gio.DBusConnection>} A new DBus connection
|
||||
* @returns {Promise<Gio.DBusConnection>} A new DBus connection
|
||||
*/
|
||||
export function newConnection(busType = Gio.BusType.SESSION, cancellable = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -252,4 +266,3 @@ export function newConnection(busType = Gio.BusType.SESSION, cancellable = null)
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ const _numberRegex = new RegExp(
|
||||
* the position within @str where the URL was found.
|
||||
*
|
||||
* @param {string} str - the string to search
|
||||
* @return {Object[]} the list of match objects, as described above
|
||||
* @returns {object[]} the list of match objects, as described above
|
||||
*/
|
||||
export function findUrls(str) {
|
||||
_urlRegexp.lastIndex = 0;
|
||||
@ -103,7 +103,7 @@ export function findUrls(str) {
|
||||
*
|
||||
* @param {string} str - The string to be modified
|
||||
* @param {string} [title] - An optional title (eg. alt text, tooltip)
|
||||
* @return {string} the modified text
|
||||
* @returns {string} the modified text
|
||||
*/
|
||||
export function linkify(str, title = null) {
|
||||
const text = GLib.markup_escape_text(str, -1);
|
||||
@ -166,4 +166,3 @@ export default class URI {
|
||||
return this.body ? `${uri}?body=${escape(this.body)}` : uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -172,7 +172,7 @@ export const Clipboard = GObject.registerClass({
|
||||
invocation.return_value(retval);
|
||||
|
||||
// Without a response, the client will wait for timeout
|
||||
} catch (e) {
|
||||
} catch {
|
||||
invocation.return_dbus_error(
|
||||
'org.gnome.gjs.JSError.ValueError',
|
||||
'Service implementation returned an incorrect value type'
|
||||
@ -183,7 +183,7 @@ export const Clipboard = GObject.registerClass({
|
||||
/**
|
||||
* Get the available mimetypes of the current clipboard content
|
||||
*
|
||||
* @return {Promise<string[]>} A list of mime-types
|
||||
* @returns {Promise<string[]>} A list of mime-types
|
||||
*/
|
||||
GetMimetypes() {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -202,7 +202,7 @@ export const Clipboard = GObject.registerClass({
|
||||
/**
|
||||
* Get the text content of the clipboard
|
||||
*
|
||||
* @return {Promise<string>} Text content of the clipboard
|
||||
* @returns {Promise<string>} Text content of the clipboard
|
||||
*/
|
||||
GetText() {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -241,7 +241,7 @@ export const Clipboard = GObject.registerClass({
|
||||
* Set the text content of the clipboard
|
||||
*
|
||||
* @param {string} text - text content to set
|
||||
* @return {Promise} A promise for the operation
|
||||
* @returns {Promise} A promise for the operation
|
||||
*/
|
||||
SetText(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -270,7 +270,7 @@ export const Clipboard = GObject.registerClass({
|
||||
* Get the content of the clipboard with the type @mimetype.
|
||||
*
|
||||
* @param {string} mimetype - the mimetype to request
|
||||
* @return {Promise<Uint8Array>} The content of the clipboard
|
||||
* @returns {Promise<Uint8Array>} The content of the clipboard
|
||||
*/
|
||||
GetValue(mimetype) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -300,7 +300,7 @@ export const Clipboard = GObject.registerClass({
|
||||
*
|
||||
* @param {Uint8Array} value - the value to set
|
||||
* @param {string} mimetype - the mimetype of the value
|
||||
* @return {Promise} - A promise for the operation
|
||||
* @returns {Promise} - A promise for the operation
|
||||
*/
|
||||
SetValue(value, mimetype) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -377,4 +377,3 @@ export function unwatchService() {
|
||||
_portalId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import St from 'gi://St';
|
||||
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
|
||||
import {getIcon} from './utils.js';
|
||||
import {HAS_ST_ORIENTATION, getIcon} from './utils.js';
|
||||
|
||||
import Tooltip from './tooltip.js';
|
||||
|
||||
@ -20,7 +20,7 @@ import Tooltip from './tooltip.js';
|
||||
*
|
||||
* @param {Gio.MenuModel} model - The menu model containing the item
|
||||
* @param {number} index - The index of the item in @model
|
||||
* @return {Object} A dictionary of the item's attributes
|
||||
* @returns {object} A dictionary of the item's attributes
|
||||
*/
|
||||
function getItemInfo(model, index) {
|
||||
const info = {
|
||||
@ -92,12 +92,19 @@ export class ListBox extends PopupMenu.PopupMenuSection {
|
||||
this.actor.add_child(this.box);
|
||||
|
||||
// Submenu Container
|
||||
this.sub = new St.BoxLayout({
|
||||
clip_to_allocation: true,
|
||||
vertical: false,
|
||||
visible: false,
|
||||
x_expand: true,
|
||||
});
|
||||
this.sub = HAS_ST_ORIENTATION
|
||||
? new St.BoxLayout({
|
||||
clip_to_allocation: true,
|
||||
orientation: Clutter.Orientation.HORIZONTAL, // GNOME 48
|
||||
visible: false,
|
||||
x_expand: true,
|
||||
})
|
||||
: new St.BoxLayout({
|
||||
clip_to_allocation: true,
|
||||
vertical: false, // GNOME 46/47
|
||||
visible: false,
|
||||
x_expand: true,
|
||||
});
|
||||
this.sub.set_pivot_point(1, 1);
|
||||
this.sub._delegate = this;
|
||||
this.actor.add_child(this.sub);
|
||||
@ -464,24 +471,38 @@ export class IconBox extends PopupMenu.PopupMenuSection {
|
||||
Object.assign(this, params);
|
||||
|
||||
// Main Actor
|
||||
this.actor = new St.BoxLayout({
|
||||
vertical: true,
|
||||
x_expand: true,
|
||||
});
|
||||
this.actor = HAS_ST_ORIENTATION
|
||||
? new St.BoxLayout({
|
||||
orientation: Clutter.Orientation.VERTICAL, // GNOME 48
|
||||
x_expand: true,
|
||||
})
|
||||
: new St.BoxLayout({
|
||||
vertical: true, // GNOME 46/47
|
||||
x_expand: true,
|
||||
});
|
||||
this.actor._delegate = this;
|
||||
|
||||
// Button Box
|
||||
this.box._delegate = this;
|
||||
this.box.style_class = 'gsconnect-icon-box';
|
||||
this.box.vertical = false;
|
||||
if (HAS_ST_ORIENTATION)
|
||||
this.box.orientation = Clutter.Orientation.HORIZONTAL; // GNOME 48
|
||||
else
|
||||
this.box.vertical = false; // GNOME 46/47
|
||||
this.actor.add_child(this.box);
|
||||
|
||||
// Submenu Container
|
||||
this.sub = new St.BoxLayout({
|
||||
clip_to_allocation: true,
|
||||
vertical: true,
|
||||
x_expand: true,
|
||||
});
|
||||
this.sub = HAS_ST_ORIENTATION
|
||||
? new St.BoxLayout({
|
||||
clip_to_allocation: true,
|
||||
orientation: Clutter.Orientation.VERTICAL, // GNOME 48
|
||||
x_expand: true,
|
||||
})
|
||||
: new St.BoxLayout({
|
||||
clip_to_allocation: true,
|
||||
vertical: true, // GNOME 46/47
|
||||
x_expand: true,
|
||||
});
|
||||
this.sub.connect('transitions-completed', this._onTransitionsCompleted);
|
||||
this.sub._delegate = this;
|
||||
this.actor.add_child(this.sub);
|
||||
@ -644,4 +665,3 @@ export class IconBox extends PopupMenu.PopupMenuSection {
|
||||
this._onItemsChanged(this.model, 0, 0, this.model.get_n_items());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ export class Manager {
|
||||
*
|
||||
* @param {string} accelerator - An accelerator in the form '<Control>q'
|
||||
* @param {Function} callback - A callback for the accelerator
|
||||
* @return {number} A non-zero action id on success, or 0 on failure
|
||||
* @returns {*} A non-zero action id on success, or 0 on failure
|
||||
*/
|
||||
add(accelerator, callback) {
|
||||
try {
|
||||
@ -100,4 +100,3 @@ export class Manager {
|
||||
this.removeAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,11 +9,14 @@ import St from 'gi://St';
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
|
||||
import * as Calendar from 'resource:///org/gnome/shell/ui/calendar.js';
|
||||
import * as NotificationDaemon from 'resource:///org/gnome/shell/ui/notificationDaemon.js';
|
||||
|
||||
import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
import {getIcon} from './utils.js';
|
||||
import {HAS_MESSAGELIST_NOTIFICATIONMESSAGE, getIcon} from './utils.js';
|
||||
|
||||
const {NotificationMessage} = HAS_MESSAGELIST_NOTIFICATIONMESSAGE
|
||||
? await import('resource:///org/gnome/shell/ui/messageList.js') // GNOME 48
|
||||
: await import('resource:///org/gnome/shell/ui/calendar.js'); // GNOME 46/47
|
||||
|
||||
const APP_ID = 'org.gnome.Shell.Extensions.GSConnect';
|
||||
const APP_PATH = '/org/gnome/Shell/Extensions/GSConnect';
|
||||
@ -29,6 +32,7 @@ const REPLY_REGEX = new RegExp(/^([^|]+)\|([\s\S]+)\|([0-9a-f]{8}-[0-9a-f]{4}-[1
|
||||
/**
|
||||
* Extracted from notificationDaemon.js, as it's no longer exported
|
||||
* https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/notificationDaemon.js#L556
|
||||
*
|
||||
* @returns {{ 'desktop-startup-id': string }} Object with ID containing current time
|
||||
*/
|
||||
function getPlatformData() {
|
||||
@ -45,7 +49,7 @@ const GtkNotificationDaemon = Main.notificationDaemon._gtkNotificationDaemon.con
|
||||
*/
|
||||
const NotificationBanner = GObject.registerClass({
|
||||
GTypeName: 'GSConnectNotificationBanner',
|
||||
}, class NotificationBanner extends Calendar.NotificationMessage {
|
||||
}, class NotificationBanner extends NotificationMessage {
|
||||
|
||||
constructor(notification) {
|
||||
super(notification);
|
||||
@ -147,7 +151,7 @@ const NotificationBanner = GObject.registerClass({
|
||||
(connection, res) => {
|
||||
try {
|
||||
connection.call_finish(res);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Silence errors
|
||||
}
|
||||
}
|
||||
@ -198,7 +202,7 @@ const Source = GObject.registerClass({
|
||||
(connection, res) => {
|
||||
try {
|
||||
connection.call_finish(res);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// If we fail, reset in case we can try again
|
||||
notification._remoteClosed = false;
|
||||
}
|
||||
@ -384,11 +388,17 @@ const _ensureAppSource = function (appId) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update the prototype for {@link GtkNotificationDaemon}.
|
||||
*/
|
||||
export function patchGtkNotificationDaemon() {
|
||||
GtkNotificationDaemon.prototype._ensureAppSource = _ensureAppSource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Restore the prototype for {@link GtkNotificationDaemon}.
|
||||
*/
|
||||
export function unpatchGtkNotificationDaemon() {
|
||||
GtkNotificationDaemon.prototype._ensureAppSource = __ensureAppSource;
|
||||
}
|
||||
@ -399,6 +409,9 @@ export function unpatchGtkNotificationDaemon() {
|
||||
*/
|
||||
const _addNotification = NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification;
|
||||
|
||||
/**
|
||||
* Update the prototype for {@link NotificationDaemon.GtkNotificationDaemonAppSource}.
|
||||
*/
|
||||
export function patchGtkNotificationSources() {
|
||||
// eslint-disable-next-line func-style
|
||||
const _withdrawGSConnectNotification = function (id, notification, reason) {
|
||||
@ -433,7 +446,7 @@ export function patchGtkNotificationSources() {
|
||||
(connection, res) => {
|
||||
try {
|
||||
connection.call_finish(res);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// If we fail, reset in case we can try again
|
||||
notification._remoteWithdrawn = false;
|
||||
}
|
||||
@ -445,8 +458,10 @@ export function patchGtkNotificationSources() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Restore the prototype for {@link NotificationDaemon.GtkNotificationDaemonAppSource}.
|
||||
*/
|
||||
export function unpatchGtkNotificationSources() {
|
||||
NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification = _addNotification;
|
||||
delete NotificationDaemon.GtkNotificationDaemonAppSource.prototype._withdrawGSConnectNotification;
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import St from 'gi://St';
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import {HAS_ST_ORIENTATION} from './utils.js';
|
||||
|
||||
/**
|
||||
* An StTooltip for ClutterActors
|
||||
@ -151,7 +152,15 @@ export default class Tooltip {
|
||||
if (this.custom) {
|
||||
this._bin.child = this.custom;
|
||||
} else {
|
||||
this._bin.child = new St.BoxLayout({vertical: false});
|
||||
if (HAS_ST_ORIENTATION) {
|
||||
// GNOME 48
|
||||
this._bin.child = new St.BoxLayout(
|
||||
{orientation: Clutter.Orientation.HORIZONTAL}
|
||||
);
|
||||
} else {
|
||||
// GNOME 46/47
|
||||
this._bin.child = new St.BoxLayout({vertical: false});
|
||||
}
|
||||
|
||||
if (this.gicon) {
|
||||
this._bin.child.icon = new St.Icon({
|
||||
|
||||
@ -3,22 +3,22 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import Gtk from 'gi://Gtk';
|
||||
import St from 'gi://St';
|
||||
|
||||
import Config from '../config.js';
|
||||
import {PACKAGE_VERSION} from 'resource:///org/gnome/shell/misc/config.js';
|
||||
|
||||
let St = null; // St is not available for prefs.js importing this file.
|
||||
try {
|
||||
St = (await import('gi://St')).default;
|
||||
} catch (e) { }
|
||||
|
||||
export const SHELL_MAJOR_VERSION = Number(PACKAGE_VERSION.split('.')[0]);
|
||||
|
||||
export const HAS_ST_ORIENTATION = SHELL_MAJOR_VERSION >= 48;
|
||||
export const HAS_MESSAGELIST_NOTIFICATIONMESSAGE = SHELL_MAJOR_VERSION >= 48;
|
||||
|
||||
/**
|
||||
* Get a themed icon, using fallbacks from GSConnect's GResource when necessary.
|
||||
*
|
||||
* @param {string} name - A themed icon name
|
||||
* @return {Gio.Icon} A themed icon
|
||||
* @returns {Gio.Icon} A themed icon
|
||||
*/
|
||||
export function getIcon(name) {
|
||||
if (getIcon._resource === undefined) {
|
||||
@ -64,220 +64,3 @@ export function getIcon(name) {
|
||||
// Fallback to hoping it's in the theme somewhere
|
||||
return new Gio.ThemedIcon({name: name});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where
|
||||
* necessary.
|
||||
*
|
||||
* @param {string} relativePath - A path relative to GSConnect's resource path
|
||||
* @return {string} The file contents as a string
|
||||
*/
|
||||
function getResource(relativePath) {
|
||||
try {
|
||||
const bytes = Gio.resources_lookup_data(
|
||||
GLib.build_filenamev([Config.APP_PATH, relativePath]),
|
||||
Gio.ResourceLookupFlags.NONE
|
||||
);
|
||||
|
||||
const source = new TextDecoder().decode(bytes.toArray());
|
||||
|
||||
return source.replace('@PACKAGE_DATADIR@', Config.PACKAGE_DATADIR);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Install file contents, to an absolute directory path.
|
||||
*
|
||||
* @param {string} dirname - An absolute directory path
|
||||
* @param {string} basename - The file name
|
||||
* @param {string} contents - The file contents
|
||||
* @return {boolean} A success boolean
|
||||
*/
|
||||
function _installFile(dirname, basename, contents) {
|
||||
try {
|
||||
const filename = GLib.build_filenamev([dirname, basename]);
|
||||
GLib.mkdir_with_parents(dirname, 0o755);
|
||||
|
||||
return GLib.file_set_contents(filename, contents);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install file contents from a GResource, to an absolute directory path.
|
||||
*
|
||||
* @param {string} dirname - An absolute directory path
|
||||
* @param {string} basename - The file name
|
||||
* @param {string} relativePath - A path relative to GSConnect's resource path
|
||||
* @return {boolean} A success boolean
|
||||
*/
|
||||
function _installResource(dirname, basename, relativePath) {
|
||||
try {
|
||||
const contents = getResource(relativePath);
|
||||
|
||||
return _installFile(dirname, basename, contents);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Gio.File to ensure a file's executable bits are set.
|
||||
*
|
||||
* @param {string} filepath - An absolute path to a file
|
||||
* @returns {boolean} - True if the file already was, or is now, executable
|
||||
*/
|
||||
function _setExecutable(filepath) {
|
||||
try {
|
||||
const file = Gio.File.new_for_path(filepath);
|
||||
const finfo = file.query_info(
|
||||
`${Gio.FILE_ATTRIBUTE_STANDARD_TYPE},${Gio.FILE_ATTRIBUTE_UNIX_MODE}`,
|
||||
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
||||
null);
|
||||
|
||||
if (!finfo.has_attribute(Gio.FILE_ATTRIBUTE_UNIX_MODE))
|
||||
return false;
|
||||
|
||||
const mode = finfo.get_attribute_uint32(
|
||||
Gio.FILE_ATTRIBUTE_UNIX_MODE);
|
||||
const new_mode = (mode | 0o111);
|
||||
if (mode === new_mode)
|
||||
return true;
|
||||
|
||||
return file.set_attribute_uint32(
|
||||
Gio.FILE_ATTRIBUTE_UNIX_MODE,
|
||||
new_mode,
|
||||
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
||||
null);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure critical files in the extension directory have the
|
||||
* correct permissions.
|
||||
*/
|
||||
export function ensurePermissions() {
|
||||
if (Config.IS_USER) {
|
||||
const executableFiles = [
|
||||
'gsconnect-preferences',
|
||||
'service/daemon.js',
|
||||
'service/nativeMessagingHost.js',
|
||||
];
|
||||
for (const file of executableFiles)
|
||||
_setExecutable(GLib.build_filenamev([Config.PACKAGE_DATADIR, file]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the files necessary for the GSConnect service to run.
|
||||
*/
|
||||
export function installService() {
|
||||
const settings = new Gio.Settings({
|
||||
settings_schema: Config.GSCHEMA.lookup(
|
||||
'org.gnome.Shell.Extensions.GSConnect',
|
||||
null
|
||||
),
|
||||
path: '/org/gnome/shell/extensions/gsconnect/',
|
||||
});
|
||||
|
||||
const confDir = GLib.get_user_config_dir();
|
||||
const dataDir = GLib.get_user_data_dir();
|
||||
const homeDir = GLib.get_home_dir();
|
||||
|
||||
// DBus Service
|
||||
const dbusDir = GLib.build_filenamev([dataDir, 'dbus-1', 'services']);
|
||||
const dbusFile = `${Config.APP_ID}.service`;
|
||||
|
||||
// Desktop Entry
|
||||
const appDir = GLib.build_filenamev([dataDir, 'applications']);
|
||||
const appFile = `${Config.APP_ID}.desktop`;
|
||||
const appPrefsFile = `${Config.APP_ID}.Preferences.desktop`;
|
||||
|
||||
// Application Icon
|
||||
const iconDir = GLib.build_filenamev([dataDir, 'icons', 'hicolor', 'scalable', 'apps']);
|
||||
const iconFull = `${Config.APP_ID}.svg`;
|
||||
const iconSym = `${Config.APP_ID}-symbolic.svg`;
|
||||
|
||||
// File Manager Extensions
|
||||
const fileManagers = [
|
||||
[`${dataDir}/nautilus-python/extensions`, 'nautilus-gsconnect.py'],
|
||||
[`${dataDir}/nemo-python/extensions`, 'nemo-gsconnect.py'],
|
||||
];
|
||||
|
||||
// WebExtension Manifests
|
||||
const manifestFile = 'org.gnome.shell.extensions.gsconnect.json';
|
||||
const google = getResource(`webextension/${manifestFile}.google.in`);
|
||||
const mozilla = getResource(`webextension/${manifestFile}.mozilla.in`);
|
||||
const manifests = [
|
||||
[`${confDir}/chromium/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/google-chrome/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/google-chrome-beta/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/google-chrome-unstable/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/BraveSoftware/Brave-Browser/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/BraveSoftware/Brave-Browser-Beta/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/BraveSoftware/Brave-Browser-Nightly/NativeMessagingHosts/`, google],
|
||||
[`${homeDir}/.mozilla/native-messaging-hosts/`, mozilla],
|
||||
[`${homeDir}/.config/microsoft-edge-dev/NativeMessagingHosts`, google],
|
||||
[`${homeDir}/.config/microsoft-edge-beta/NativeMessagingHosts`, google],
|
||||
];
|
||||
|
||||
// If running as a user extension, ensure the DBus service, desktop entry,
|
||||
// file manager scripts, and WebExtension manifests are installed.
|
||||
if (Config.IS_USER) {
|
||||
// DBus Service
|
||||
if (!_installResource(dbusDir, dbusFile, `${dbusFile}.in`))
|
||||
throw Error('GSConnect: Failed to install DBus Service');
|
||||
|
||||
// Desktop Entries
|
||||
_installResource(appDir, appFile, appFile);
|
||||
_installResource(appDir, appPrefsFile, appPrefsFile);
|
||||
|
||||
// Application Icon
|
||||
_installResource(iconDir, iconFull, `icons/${iconFull}`);
|
||||
_installResource(iconDir, iconSym, `icons/${iconSym}`);
|
||||
|
||||
// File Manager Extensions
|
||||
const target = `${Config.PACKAGE_DATADIR}/nautilus-gsconnect.py`;
|
||||
|
||||
for (const [dir, name] of fileManagers) {
|
||||
const script = Gio.File.new_for_path(GLib.build_filenamev([dir, name]));
|
||||
|
||||
if (!script.query_exists(null)) {
|
||||
GLib.mkdir_with_parents(dir, 0o755);
|
||||
script.make_symbolic_link(target, null);
|
||||
}
|
||||
}
|
||||
|
||||
// WebExtension Manifests
|
||||
if (settings.get_boolean('create-native-messaging-hosts')) {
|
||||
for (const [dirname, contents] of manifests)
|
||||
_installFile(dirname, manifestFile, contents);
|
||||
}
|
||||
|
||||
// Otherwise, if running as a system extension, ensure anything previously
|
||||
// installed when running as a user extension is removed.
|
||||
} else {
|
||||
GLib.unlink(GLib.build_filenamev([dbusDir, dbusFile]));
|
||||
GLib.unlink(GLib.build_filenamev([appDir, appFile]));
|
||||
GLib.unlink(GLib.build_filenamev([appDir, appPrefsFile]));
|
||||
GLib.unlink(GLib.build_filenamev([iconDir, iconFull]));
|
||||
GLib.unlink(GLib.build_filenamev([iconDir, iconSym]));
|
||||
|
||||
for (const [dir, name] of fileManagers)
|
||||
GLib.unlink(GLib.build_filenamev([dir, name]));
|
||||
|
||||
for (const manifest of manifests)
|
||||
GLib.unlink(GLib.build_filenamev([manifest[0], manifestFile]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,13 @@ const _PROPERTIES = {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initialize a Gio.DBusProxy in an awaitable manner
|
||||
*
|
||||
* @param {Gio.DBusProxy} proxy - The proxy object to initialize
|
||||
* @param {Gio.Cancellable} [cancellable] - An optional cancellable object
|
||||
* @returns {Promise} An awaitable Promise
|
||||
*/
|
||||
function _proxyInit(proxy, cancellable = null) {
|
||||
if (proxy.__initialized !== undefined)
|
||||
return Promise.resolve();
|
||||
@ -127,7 +134,7 @@ export const Device = GObject.registerClass({
|
||||
_get(name, fallback = null) {
|
||||
try {
|
||||
return this.get_cached_property(name).unpack();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@ -297,7 +304,7 @@ export const Service = GObject.registerClass({
|
||||
* org.freedesktop.DBus.ObjectManager.InterfacesAdded
|
||||
*
|
||||
* @param {string} object_path - Path interfaces have been added to
|
||||
* @param {Object} interfaces - A dictionary of interface objects
|
||||
* @param {object} interfaces - A dictionary of interface objects
|
||||
*/
|
||||
async _onInterfacesAdded(object_path, interfaces) {
|
||||
try {
|
||||
|
||||
@ -21,11 +21,228 @@ export function setupGettext() {
|
||||
globalThis.ngettext = GLib.dngettext.bind(null, Config.APP_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where
|
||||
* necessary.
|
||||
*
|
||||
* @param {string} relativePath - A path relative to GSConnect's resource path
|
||||
* @returns {string} The file contents as a string
|
||||
*/
|
||||
function getResource(relativePath) {
|
||||
try {
|
||||
const bytes = Gio.resources_lookup_data(
|
||||
GLib.build_filenamev([Config.APP_PATH, relativePath]),
|
||||
Gio.ResourceLookupFlags.NONE
|
||||
);
|
||||
|
||||
const source = new TextDecoder().decode(bytes.toArray());
|
||||
|
||||
return source.replace('@PACKAGE_DATADIR@', Config.PACKAGE_DATADIR);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Install file contents, to an absolute directory path.
|
||||
*
|
||||
* @param {string} dirname - An absolute directory path
|
||||
* @param {string} basename - The file name
|
||||
* @param {string} contents - The file contents
|
||||
* @returns {boolean} A success boolean
|
||||
*/
|
||||
function _installFile(dirname, basename, contents) {
|
||||
try {
|
||||
const filename = GLib.build_filenamev([dirname, basename]);
|
||||
GLib.mkdir_with_parents(dirname, 0o755);
|
||||
|
||||
return GLib.file_set_contents(filename, contents);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install file contents from a GResource, to an absolute directory path.
|
||||
*
|
||||
* @param {string} dirname - An absolute directory path
|
||||
* @param {string} basename - The file name
|
||||
* @param {string} relativePath - A path relative to GSConnect's resource path
|
||||
* @returns {boolean} A success boolean
|
||||
*/
|
||||
function _installResource(dirname, basename, relativePath) {
|
||||
try {
|
||||
const contents = getResource(relativePath);
|
||||
|
||||
return _installFile(dirname, basename, contents);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Gio.File to ensure a file's executable bits are set.
|
||||
*
|
||||
* @param {string} filepath - An absolute path to a file
|
||||
* @returns {boolean} - True if the file already was, or is now, executable
|
||||
*/
|
||||
function _setExecutable(filepath) {
|
||||
try {
|
||||
const file = Gio.File.new_for_path(filepath);
|
||||
const finfo = file.query_info(
|
||||
`${Gio.FILE_ATTRIBUTE_STANDARD_TYPE},${Gio.FILE_ATTRIBUTE_UNIX_MODE}`,
|
||||
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
||||
null);
|
||||
|
||||
if (!finfo.has_attribute(Gio.FILE_ATTRIBUTE_UNIX_MODE))
|
||||
return false;
|
||||
|
||||
const mode = finfo.get_attribute_uint32(
|
||||
Gio.FILE_ATTRIBUTE_UNIX_MODE);
|
||||
const new_mode = (mode | 0o111);
|
||||
if (mode === new_mode)
|
||||
return true;
|
||||
|
||||
return file.set_attribute_uint32(
|
||||
Gio.FILE_ATTRIBUTE_UNIX_MODE,
|
||||
new_mode,
|
||||
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
||||
null);
|
||||
} catch (e) {
|
||||
logError(e, 'GSConnect');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure critical files in the extension directory have the
|
||||
* correct permissions.
|
||||
*/
|
||||
export function ensurePermissions() {
|
||||
if (Config.IS_USER) {
|
||||
const executableFiles = [
|
||||
'gsconnect-preferences',
|
||||
'service/daemon.js',
|
||||
'service/nativeMessagingHost.js',
|
||||
];
|
||||
for (const file of executableFiles)
|
||||
_setExecutable(GLib.build_filenamev([Config.PACKAGE_DATADIR, file]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the files necessary for the GSConnect service to run.
|
||||
*/
|
||||
export function installService() {
|
||||
const settings = new Gio.Settings({
|
||||
settings_schema: Config.GSCHEMA.lookup(
|
||||
'org.gnome.Shell.Extensions.GSConnect',
|
||||
null
|
||||
),
|
||||
path: '/org/gnome/shell/extensions/gsconnect/',
|
||||
});
|
||||
|
||||
const confDir = GLib.get_user_config_dir();
|
||||
const dataDir = GLib.get_user_data_dir();
|
||||
const homeDir = GLib.get_home_dir();
|
||||
|
||||
// DBus Service
|
||||
const dbusDir = GLib.build_filenamev([dataDir, 'dbus-1', 'services']);
|
||||
const dbusFile = `${Config.APP_ID}.service`;
|
||||
|
||||
// Desktop Entry
|
||||
const appDir = GLib.build_filenamev([dataDir, 'applications']);
|
||||
const appFile = `${Config.APP_ID}.desktop`;
|
||||
const appPrefsFile = `${Config.APP_ID}.Preferences.desktop`;
|
||||
|
||||
// Application Icon
|
||||
const iconDir = GLib.build_filenamev([dataDir, 'icons', 'hicolor', 'scalable', 'apps']);
|
||||
const iconFull = `${Config.APP_ID}.svg`;
|
||||
const iconSym = `${Config.APP_ID}-symbolic.svg`;
|
||||
|
||||
// File Manager Extensions
|
||||
const fileManagers = [
|
||||
[`${dataDir}/nautilus-python/extensions`, 'nautilus-gsconnect.py'],
|
||||
[`${dataDir}/nemo-python/extensions`, 'nemo-gsconnect.py'],
|
||||
];
|
||||
|
||||
// WebExtension Manifests
|
||||
const manifestFile = 'org.gnome.shell.extensions.gsconnect.json';
|
||||
const google = getResource(`webextension/${manifestFile}.google.in`);
|
||||
const mozilla = getResource(`webextension/${manifestFile}.mozilla.in`);
|
||||
const manifests = [
|
||||
[`${confDir}/chromium/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/google-chrome/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/google-chrome-beta/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/google-chrome-unstable/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/BraveSoftware/Brave-Browser/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/BraveSoftware/Brave-Browser-Beta/NativeMessagingHosts/`, google],
|
||||
[`${confDir}/BraveSoftware/Brave-Browser-Nightly/NativeMessagingHosts/`, google],
|
||||
[`${homeDir}/.mozilla/native-messaging-hosts/`, mozilla],
|
||||
[`${homeDir}/.config/microsoft-edge-dev/NativeMessagingHosts`, google],
|
||||
[`${homeDir}/.config/microsoft-edge-beta/NativeMessagingHosts`, google],
|
||||
];
|
||||
|
||||
// If running as a user extension, ensure the DBus service, desktop entry,
|
||||
// file manager scripts, and WebExtension manifests are installed.
|
||||
if (Config.IS_USER) {
|
||||
// DBus Service
|
||||
if (!_installResource(dbusDir, dbusFile, `${dbusFile}.in`))
|
||||
throw Error('GSConnect: Failed to install DBus Service');
|
||||
|
||||
// Desktop Entries
|
||||
_installResource(appDir, appFile, appFile);
|
||||
_installResource(appDir, appPrefsFile, appPrefsFile);
|
||||
|
||||
// Application Icon
|
||||
_installResource(iconDir, iconFull, `icons/${iconFull}`);
|
||||
_installResource(iconDir, iconSym, `icons/${iconSym}`);
|
||||
|
||||
// File Manager Extensions
|
||||
const target = `${Config.PACKAGE_DATADIR}/nautilus-gsconnect.py`;
|
||||
|
||||
for (const [dir, name] of fileManagers) {
|
||||
const script = Gio.File.new_for_path(GLib.build_filenamev([dir, name]));
|
||||
|
||||
if (!script.query_exists(null)) {
|
||||
GLib.mkdir_with_parents(dir, 0o755);
|
||||
script.make_symbolic_link(target, null);
|
||||
}
|
||||
}
|
||||
|
||||
// WebExtension Manifests
|
||||
if (settings.get_boolean('create-native-messaging-hosts')) {
|
||||
for (const [dirname, contents] of manifests)
|
||||
_installFile(dirname, manifestFile, contents);
|
||||
}
|
||||
|
||||
// Otherwise, if running as a system extension, ensure anything previously
|
||||
// installed when running as a user extension is removed.
|
||||
} else {
|
||||
GLib.unlink(GLib.build_filenamev([dbusDir, dbusFile]));
|
||||
GLib.unlink(GLib.build_filenamev([appDir, appFile]));
|
||||
GLib.unlink(GLib.build_filenamev([appDir, appPrefsFile]));
|
||||
GLib.unlink(GLib.build_filenamev([iconDir, iconFull]));
|
||||
GLib.unlink(GLib.build_filenamev([iconDir, iconSym]));
|
||||
|
||||
for (const [dir, name] of fileManagers)
|
||||
GLib.unlink(GLib.build_filenamev([dir, name]));
|
||||
|
||||
for (const manifest of manifests)
|
||||
GLib.unlink(GLib.build_filenamev([manifest[0], manifestFile]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise and setup Config, GResources and GSchema.
|
||||
*
|
||||
* @param {string} extensionPath - The absolute path to the extension directory
|
||||
*/
|
||||
export default function setup(extensionPath) {
|
||||
export function setup(extensionPath) {
|
||||
// Ensure config.js is setup properly
|
||||
Config.PACKAGE_DATADIR = extensionPath;
|
||||
const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']);
|
||||
|
||||
@ -141,7 +141,7 @@ export const Clipboard = GObject.registerClass(
|
||||
invocation.return_value(retval);
|
||||
|
||||
// Without a response, the client will wait for timeout
|
||||
} catch (e) {
|
||||
} catch {
|
||||
invocation.return_dbus_error(
|
||||
'org.gnome.gjs.JSError.ValueError',
|
||||
'Service implementation returned an incorrect value type'
|
||||
@ -150,11 +150,10 @@ export const Clipboard = GObject.registerClass(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available mimetypes of the current clipboard content
|
||||
*
|
||||
* @return {Promise<string[]>} A list of mime-types
|
||||
*/
|
||||
|
||||
* Get the available mimetypes of the current clipboard content
|
||||
*
|
||||
* @returns {Promise<string[]>} A list of mime-types
|
||||
*/
|
||||
GetMimetypes() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = launcher.spawnv([
|
||||
@ -179,10 +178,10 @@ export const Clipboard = GObject.registerClass(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text content of the clipboard
|
||||
*
|
||||
* @return {Promise<string>} Text content of the clipboard
|
||||
*/
|
||||
* Get the text content of the clipboard
|
||||
*
|
||||
* @returns {Promise<string>} Text content of the clipboard
|
||||
*/
|
||||
GetText() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.GetMimetypes().then((mimetypes) => {
|
||||
@ -213,11 +212,11 @@ export const Clipboard = GObject.registerClass(
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text content of the clipboard
|
||||
*
|
||||
* @param {string} text - text content to set
|
||||
* @return {Promise} A promise for the operation
|
||||
*/
|
||||
* Set the text content of the clipboard
|
||||
*
|
||||
* @param {string} text - text content to set
|
||||
* @returns {Promise} A promise for the operation
|
||||
*/
|
||||
SetText(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user