This commit is contained in:
2024-07-08 22:46:35 +02:00
parent 02f44c49d2
commit 27254d817a
56249 changed files with 808097 additions and 1 deletions

View File

@ -0,0 +1,255 @@
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later
import Gio from 'gi://Gio';
import GjsPrivate from 'gi://GjsPrivate';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
/*
* Some utility methods
*/
function toDBusCase(string) {
return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => {
return ltr.toUpperCase();
}).replace(/[\s_-]+/g, '');
}
function toUnderscoreCase(string) {
return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => {
if (ltr === '_')
return '';
return (offset > 0) ? `_${ltr.toLowerCase()}` : ltr.toLowerCase();
}).replace(/[\s-]+/g, '');
}
/**
* DBus.Interface represents a DBus interface bound to an object instance, meant
* to be exported over DBus.
*/
export const Interface = GObject.registerClass({
GTypeName: 'GSConnectDBusInterface',
Implements: [Gio.DBusInterface],
Properties: {
'g-instance': GObject.ParamSpec.object(
'g-instance',
'Instance',
'The delegate GObject',
GObject.ParamFlags.READWRITE,
GObject.Object.$gtype
),
},
}, class Interface extends GjsPrivate.DBusImplementation {
_init(params) {
super._init({
g_instance: params.g_instance,
g_interface_info: params.g_interface_info,
});
// Cache member lookups
this._instanceHandlers = [];
this._instanceMethods = {};
this._instanceProperties = {};
const info = this.get_info();
this.connect('handle-method-call', this._call.bind(this._instance, info));
this.connect('handle-property-get', this._get.bind(this._instance, info));
this.connect('handle-property-set', this._set.bind(this._instance, info));
// Automatically forward known signals
const id = this._instance.connect('notify', this._notify.bind(this));
this._instanceHandlers.push(id);
for (const signal of info.signals) {
const type = `(${signal.args.map(arg => arg.signature).join('')})`;
const id = this._instance.connect(
signal.name,
this._emit.bind(this, signal.name, type)
);
this._instanceHandlers.push(id);
}
// Export if connection and object path were given
if (params.g_connection && params.g_object_path)
this.export(params.g_connection, params.g_object_path);
}
get g_instance() {
if (this._instance === undefined)
this._instance = null;
return this._instance;
}
set g_instance(instance) {
this._instance = instance;
}
/**
* 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 {string} name - The DBus method name
* @param {GLib.Variant} parameters - The method parameters
* @param {Gio.DBusMethodInvocation} invocation - The method invocation info
*/
async _call(info, iface, name, parameters, invocation) {
let retval;
// Invoke the instance method
try {
const args = parameters.unpack().map(parameter => {
if (parameter.get_type_string() === 'h') {
const message = invocation.get_message();
const fds = message.get_unix_fd_list();
const idx = parameter.deepUnpack();
return fds.get(idx);
} else {
return parameter.recursiveUnpack();
}
});
retval = await this[name](...args);
} catch (e) {
if (e instanceof GLib.Error) {
invocation.return_gerror(e);
} else {
// likely to be a normal JS error
if (!e.name.includes('.'))
e.name = `org.gnome.gjs.JSError.${e.name}`;
invocation.return_dbus_error(e.name, e.message);
}
logError(e, `${this}: ${name}`);
return;
}
// `undefined` is an empty tuple on DBus
if (retval === undefined)
retval = new GLib.Variant('()', []);
// Return the instance result or error
try {
if (!(retval instanceof GLib.Variant)) {
const args = info.lookup_method(name).out_args;
retval = new GLib.Variant(
`(${args.map(arg => arg.signature).join('')})`,
(args.length === 1) ? [retval] : retval
);
}
invocation.return_value(retval);
} catch (e) {
invocation.return_dbus_error(
'org.gnome.gjs.JSError.ValueError',
'Service implementation returned an incorrect value type'
);
logError(e, `${this}: ${name}`);
}
}
_nativeProp(obj, name) {
if (this._instanceProperties[name] === undefined) {
let propName = name;
if (propName in obj)
this._instanceProperties[name] = propName;
if (this._instanceProperties[name] === undefined) {
propName = toUnderscoreCase(name);
if (propName in obj)
this._instanceProperties[name] = propName;
}
}
return this._instanceProperties[name];
}
_emit(name, type, obj, ...args) {
this.emit_signal(name, new GLib.Variant(type, args));
}
_get(info, iface, name) {
const nativeValue = this[iface._nativeProp(this, name)];
const propertyInfo = info.lookup_property(name);
if (nativeValue === undefined || propertyInfo === null)
return null;
return new GLib.Variant(propertyInfo.signature, nativeValue);
}
_set(info, iface, name, value) {
const nativeValue = value.recursiveUnpack();
this[iface._nativeProp(this, name)] = nativeValue;
}
_notify(obj, pspec) {
const name = toDBusCase(pspec.name);
const propertyInfo = this.get_info().lookup_property(name);
if (propertyInfo === null)
return;
this.emit_property_changed(
name,
new GLib.Variant(
propertyInfo.signature,
// Adjust for GJS's '-'/'_' conversion
this._instance[pspec.name.replace(/-/gi, '_')]
)
);
}
destroy() {
try {
for (const id of this._instanceHandlers)
this._instance.disconnect(id);
this._instanceHandlers = [];
this.flush();
this.unexport();
} catch (e) {
logError(e);
}
}
});
/**
* Get a new, dedicated DBus connection on @busType
*
* @param {Gio.BusType} [busType] - a Gio.BusType constant
* @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable
* @return {Promise<Gio.DBusConnection>} A new DBus connection
*/
export function newConnection(busType = Gio.BusType.SESSION, cancellable = null) {
return new Promise((resolve, reject) => {
Gio.DBusConnection.new_for_address(
Gio.dbus_address_get_for_bus_sync(busType, cancellable),
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT |
Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
null,
cancellable,
(connection, res) => {
try {
resolve(Gio.DBusConnection.new_for_address_finish(res));
} catch (e) {
reject(e);
}
}
);
});
}

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later
import Gdk from 'gi://Gdk';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Gtk from 'gi://Gtk';
import Config from '../../config.js';
/*
* Window State
*/
Gtk.Window.prototype.restoreGeometry = function (context = 'default') {
this._windowState = new Gio.Settings({
settings_schema: Config.GSCHEMA.lookup(
'org.gnome.Shell.Extensions.GSConnect.WindowState',
true
),
path: `/org/gnome/shell/extensions/gsconnect/${context}/`,
});
// Size
const [width, height] = this._windowState.get_value('window-size').deepUnpack();
if (width && height)
this.set_default_size(width, height);
// Maximized State
if (this._windowState.get_boolean('window-maximized'))
this.maximize();
};
Gtk.Window.prototype.saveGeometry = function () {
const state = this.get_window().get_state();
// Maximized State
const maximized = (state & Gdk.WindowState.MAXIMIZED);
this._windowState.set_boolean('window-maximized', maximized);
// Leave the size at the value before maximizing
if (maximized || (state & Gdk.WindowState.FULLSCREEN))
return;
// Size
const size = this.get_size();
this._windowState.set_value('window-size', new GLib.Variant('(ii)', size));
};

View File

@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later
import GLib from 'gi://GLib';
/**
* The same regular expression used in GNOME Shell
*
* http://daringfireball.net/2010/07/improved_regex_for_matching_urls
*/
const _balancedParens = '\\((?:[^\\s()<>]+|(?:\\(?:[^\\s()<>]+\\)))*\\)';
const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]';
const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u201C\u201D\u2018\u2019]';
const _urlRegexp = new RegExp(
'(^|' + _leadingJunk + ')' +
'(' +
'(?:' +
'(?:http|https)://' + // scheme://
'|' +
'www\\d{0,3}[.]' + // www.
'|' +
'[a-z0-9.\\-]+[.][a-z]{2,4}/' + // foo.xx/
')' +
'(?:' + // one or more:
'[^\\s()<>]+' + // run of non-space non-()
'|' + // or
_balancedParens + // balanced parens
')+' +
'(?:' + // end with:
_balancedParens + // balanced parens
'|' + // or
_notTrailingJunk + // last non-junk char
')' +
')', 'gi');
/**
* sms/tel URI RegExp (https://tools.ietf.org/html/rfc5724)
*
* A fairly lenient regexp for sms: URIs that allows tel: numbers with chars
* from global-number, local-number (without phone-context) and single spaces.
* This allows passing numbers directly from libfolks or GData without
* pre-processing. It also makes an allowance for URIs passed from Gio.File
* that always come in the form "sms:///".
*/
const _smsParam = "[\\w.!~*'()-]+=(?:[\\w.!~*'()-]|%[0-9A-F]{2})*";
const _telParam = ";[a-zA-Z0-9-]+=(?:[\\w\\[\\]/:&+$.!~*'()-]|%[0-9A-F]{2})+";
const _lenientDigits = '[+]?(?:[0-9A-F*#().-]| (?! )|%20(?!%20))+';
const _lenientNumber = `${_lenientDigits}(?:${_telParam})*`;
const _smsRegex = new RegExp(
'^' +
'sms:' + // scheme
'(?:[/]{2,3})?' + // Gio.File returns ":///"
'(' + // one or more...
_lenientNumber + // phone numbers
'(?:,' + _lenientNumber + ')*' + // separated by commas
')' +
'(?:\\?(' + // followed by optional...
_smsParam + // parameters...
'(?:&' + _smsParam + ')*' + // separated by "&" (unescaped)
'))?' +
'$', 'g'); // fragments (#foo) not allowed
const _numberRegex = new RegExp(
'^' +
'(' + _lenientDigits + ')' + // phone number digits
'((?:' + _telParam + ')*)' + // followed by optional parameters
'$', 'g');
/**
* Searches @str for URLs and returns an array of objects with %url
* properties showing the matched URL string, and %pos properties indicating
* 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
*/
export function findUrls(str) {
_urlRegexp.lastIndex = 0;
const res = [];
let match;
while ((match = _urlRegexp.exec(str))) {
const name = match[2];
const url = GLib.uri_parse_scheme(name) ? name : `http://${name}`;
res.push({name, url, pos: match.index + match[1].length});
}
return res;
}
/**
* Return a string with URLs couched in <a> tags, parseable by Pango and
* using the same RegExp as GNOME Shell.
*
* @param {string} str - The string to be modified
* @param {string} [title] - An optional title (eg. alt text, tooltip)
* @return {string} the modified text
*/
export function linkify(str, title = null) {
const text = GLib.markup_escape_text(str, -1);
_urlRegexp.lastIndex = 0;
if (title) {
return text.replace(
_urlRegexp,
`$1<a href="$2" title="${title}">$2</a>`
);
} else {
return text.replace(_urlRegexp, '$1<a href="$2">$2</a>');
}
}
/**
* A simple parsing class for sms: URI's (https://tools.ietf.org/html/rfc5724)
*/
export default class URI {
constructor(uri) {
_smsRegex.lastIndex = 0;
const [, recipients, query] = _smsRegex.exec(uri);
this.recipients = recipients.split(',').map(recipient => {
_numberRegex.lastIndex = 0;
const [, number, params] = _numberRegex.exec(recipient);
if (params) {
for (const param of params.substr(1).split(';')) {
const [key, value] = param.split('=');
// add phone-context to beginning of
if (key === 'phone-context' && value.startsWith('+'))
return value + unescape(number);
}
}
return unescape(number);
});
if (query) {
for (const field of query.split('&')) {
const [key, value] = field.split('=');
if (key === 'body') {
if (this.body)
throw URIError('duplicate "body" field');
this.body = value ? decodeURIComponent(value) : undefined;
}
}
}
}
toString() {
const uri = `sms:${this.recipients.join(',')}`;
return this.body ? `${uri}?body=${escape(this.body)}` : uri;
}
}