Add some configs from generic Ubuntu

This commit is contained in:
2020-05-11 05:16:27 -04:00
parent f32c1048d1
commit 754f64f135
16037 changed files with 205635 additions and 137 deletions

View File

@@ -0,0 +1,299 @@
'use strict';
imports.gi.versions.Atspi = '2.0';
const Atspi = imports.gi.Atspi;
const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
/**
* Printable ASCII range
*/
const _ASCII = /[\x20-\x7E]/;
/**
* Modifier Keycode Defaults
*/
const XKeycode = {
Alt_L: 0x40,
Control_L: 0x25,
Shift_L: 0x32,
Super_L: 0x85
};
var Controller = class {
constructor() {
// Atspi.init() return 2 on fail, but still marks itself as inited. We
// uninit before throwing an error otherwise any future call to init()
// will appear successful and other calls will cause GSConnect to exit.
// See: https://gitlab.gnome.org/GNOME/at-spi2-core/blob/master/atspi/atspi-misc.c
if (Atspi.init() === 2) {
this.destroy();
throw new Error('Failed to start AT-SPI');
}
try {
this._display = Gdk.Display.get_default();
this._seat = this._display.get_default_seat();
this._pointer = this._seat.get_pointer();
} catch (e) {
this.destroy();
throw e;
}
// Try to read modifier keycodes from Gdk
try {
let keymap = Gdk.Keymap.get_for_display(this._display);
let modifier;
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Alt_L)[1][0];
XKeycode.Alt_L = modifier.keycode;
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Control_L)[1][0];
XKeycode.Control_L = modifier.keycode;
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Shift_L)[1][0];
XKeycode.Shift_L = modifier.keycode;
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0];
XKeycode.Super_L = modifier.keycode;
} catch (e) {
debug('using default modifier keycodes');
}
}
/**
* Pointer events
*/
clickPointer(button) {
try {
let [, x, y] = this._pointer.get_position();
let monitor = this._display.get_monitor_at_point(x, y);
let scale = monitor.get_scale_factor();
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}c`);
} catch (e) {
logError(e);
}
}
doubleclickPointer(button) {
try {
let [, x, y] = this._pointer.get_position();
let monitor = this._display.get_monitor_at_point(x, y);
let scale = monitor.get_scale_factor();
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}d`);
} catch (e) {
logError(e);
}
}
movePointer(dx, dy) {
try {
let [, x, y] = this._pointer.get_position();
let monitor = this._display.get_monitor_at_point(x, y);
let scale = monitor.get_scale_factor();
Atspi.generate_mouse_event(scale * dx, scale * dy, 'rel');
} catch (e) {
logError(e);
}
}
pressPointer(button) {
try {
let [, x, y] = this._pointer.get_position();
let monitor = this._display.get_monitor_at_point(x, y);
let scale = monitor.get_scale_factor();
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}p`);
} catch (e) {
logError(e);
}
}
releasePointer(button) {
try {
let [, x, y] = this._pointer.get_position();
let monitor = this._display.get_monitor_at_point(x, y);
let scale = monitor.get_scale_factor();
Atspi.generate_mouse_event(scale * x, scale * y, `b${button}r`);
} catch (e) {
logError(e);
}
}
scrollPointer(dx, dy) {
if (dy > 0) {
this.clickPointer(4);
} else if (dy < 0) {
this.clickPointer(5);
}
}
/**
* Phony virtual keyboard helpers
*/
_modeLock(keycode) {
Atspi.generate_keyboard_event(
keycode,
null,
Atspi.KeySynthType.PRESS
);
}
_modeUnlock(keycode) {
Atspi.generate_keyboard_event(
keycode,
null,
Atspi.KeySynthType.RELEASE
);
}
/**
* Simulate a printable-ASCII character.
*
*/
_pressASCII(key, modifiers) {
try {
// Press Modifiers
if (modifiers & Gdk.ModifierType.MOD1_MASK) this._modeLock(XKeycode.Alt_L);
if (modifiers & Gdk.ModifierType.CONTROL_MASK) this._modeLock(XKeycode.Control_L);
if (modifiers & Gdk.ModifierType.SHIFT_MASK) this._modeLock(XKeycode.Shift_L);
if (modifiers & Gdk.ModifierType.SUPER_MASK) this._modeLock(XKeycode.Super_L);
Atspi.generate_keyboard_event(
0,
key,
Atspi.KeySynthType.STRING
);
// Release Modifiers
if (modifiers & Gdk.ModifierType.MOD1_MASK) this._modeUnlock(XKeycode.Alt_L);
if (modifiers & Gdk.ModifierType.CONTROL_MASK) this._modeUnlock(XKeycode.Control_L);
if (modifiers & Gdk.ModifierType.SHIFT_MASK) this._modeUnlock(XKeycode.Shift_L);
if (modifiers & Gdk.ModifierType.SUPER_MASK) this._modeUnlock(XKeycode.Super_L);
} catch (e) {
logError(e);
}
}
_pressKeysym(keysym, modifiers) {
try {
// Press Modifiers
if (modifiers & Gdk.ModifierType.MOD1_MASK) this._modeLock(XKeycode.Alt_L);
if (modifiers & Gdk.ModifierType.CONTROL_MASK) this._modeLock(XKeycode.Control_L);
if (modifiers & Gdk.ModifierType.SHIFT_MASK) this._modeLock(XKeycode.Shift_L);
if (modifiers & Gdk.ModifierType.SUPER_MASK) this._modeLock(XKeycode.Super_L);
Atspi.generate_keyboard_event(
keysym,
null,
Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM
);
// Release Modifiers
if (modifiers & Gdk.ModifierType.MOD1_MASK) this._modeUnlock(XKeycode.Alt_L);
if (modifiers & Gdk.ModifierType.CONTROL_MASK) this._modeUnlock(XKeycode.Control_L);
if (modifiers & Gdk.ModifierType.SHIFT_MASK) this._modeUnlock(XKeycode.Shift_L);
if (modifiers & Gdk.ModifierType.SUPER_MASK) this._modeUnlock(XKeycode.Super_L);
} catch (e) {
logError(e);
}
}
/**
* Simulate the composition of a unicode character with:
* Control+Shift+u, [hex], Return
*
* @param {object} input - 'body' of a 'kdeconnect.mousepad.request' packet
*/
_pressUnicode(key, modifiers) {
try {
if (modifiers > 0) {
log('GSConnect: ignoring modifiers for unicode keyboard event');
}
// TODO: Using Control and Shift keysym is not working (it triggers
// key release). Probably using LOCKMODIFIERS will not work either
// as unlocking the modifier will not trigger a release
// Activate compose sequence
this._modeLock(XKeycode.Control_L);
this._modeLock(XKeycode.Shift_L);
this.pressreleaseKeysym(Gdk.KEY_U);
this._modeUnlock(XKeycode.Control_L);
this._modeUnlock(XKeycode.Shift_L);
// Enter the unicode sequence
let ucode = key.charCodeAt(0).toString(16);
let keysym;
for (let h = 0, len = ucode.length; h < len; h++) {
keysym = Gdk.unicode_to_keyval(ucode.charAt(h).codePointAt(0));
this.pressreleaseKeysym(keysym);
}
// Finish the compose sequence
this.pressreleaseKeysym(Gdk.KEY_Return);
} catch (e) {
logError(e);
}
}
/**
* Keyboard Events
*/
pressKeysym(keysym) {
Atspi.generate_keyboard_event(
keysym,
null,
Atspi.KeySynthType.PRESS | Atspi.KeySynthType.SYM
);
}
releaseKeysym(keysym) {
Atspi.generate_keyboard_event(
keysym,
null,
Atspi.KeySynthType.RELEASE | Atspi.KeySynthType.SYM
);
}
pressreleaseKeysym(keysym) {
Atspi.generate_keyboard_event(
keysym,
null,
Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM
);
}
pressKey(input, modifiers) {
// We were passed a keysym
if (typeof input === 'number') {
this._pressKeysym(input, modifiers);
// Regular ASCII
} else if (_ASCII.test(input)) {
this._pressASCII(input, modifiers);
// Unicode
} else {
this._pressUnicode(input, modifiers);
}
}
destroy() {
try {
Atspi.exit();
} catch (e) {
// Silence errors
}
}
};

View File

@@ -0,0 +1,241 @@
'use strict';
const Gdk = imports.gi.Gdk;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const ClipboardProxy = GObject.registerClass({
GTypeName: 'GSConnectClipboardProxy',
Implements: [Gio.DBusInterface],
Properties: {
'text': GObject.ParamSpec.string(
'text',
'Text Content',
'The current text content of the clipboard',
GObject.ParamFlags.READWRITE,
''
)
}
}, class ClipboardProxy extends Gio.DBusProxy {
_init() {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: 'org.gnome.Shell.Extensions.GSConnect.Clipboard',
g_object_path: '/org/gnome/Shell/Extensions/GSConnect/Clipboard',
g_interface_name: 'org.gnome.Shell.Extensions.GSConnect.Clipboard'
});
}
vfunc_g_properties_changed(changed, invalidated) {
let properties = changed.deepUnpack();
if (properties.hasOwnProperty('Text')) {
let content = this.get_cached_property('Text').unpack();
if (this.text !== content) {
this._text = content;
this.notify('text');
}
}
}
get text() {
if (this._text === undefined) {
this._text = this.get_cached_property('Text').unpack();
}
return this._text;
}
set text(content) {
if (this.text !== content) {
this._text = content;
this.notify('text');
this._setProperty('Text', 's', content);
}
}
_setProperty(name, signature, value) {
let variant = new GLib.Variant(signature, value);
this.set_cached_property(name, variant);
this.call(
'org.freedesktop.DBus.Properties.Set',
new GLib.Variant('(ssv)', [this.g_interface_name, name, variant]),
Gio.DBusCallFlags.NONE,
-1,
null,
null
);
}
destroy() {
if (this.__disposed === undefined) {
this.__disposed = true;
this.run_dispose();
}
}
});
var Clipboard = GObject.registerClass({
GTypeName: 'GSConnectClipboard',
Properties: {
'text': GObject.ParamSpec.string(
'text',
'Text Content',
'The current text content of the clipboard',
GObject.ParamFlags.READWRITE,
''
)
}
}, class Clipboard extends GObject.Object {
_init() {
super._init();
try {
this._clipboard = null;
// On Wayland we use a small DBus server exported from the Shell
if (_WAYLAND) {
this._nameWatcherId = Gio.bus_watch_name(
Gio.BusType.SESSION,
'org.gnome.Shell.Extensions.GSConnect.Clipboard',
Gio.BusNameWatcherFlags.NONE,
this._onNameAppeared.bind(this),
this._onNameVanished.bind(this)
);
// If we're in X11/Xorg we're just a wrapper around GtkClipboard
} else {
let display = Gdk.Display.get_default();
this._clipboard = Gtk.Clipboard.get_default(display);
this._ownerChangeId = this._clipboard.connect(
'owner-change',
this._onOwnerChange.bind(this)
);
}
} catch (e) {
this.destroy();
throw e;
}
}
get text() {
if (this._text === undefined) {
this._text = '';
}
return this._text;
}
set text(content) {
if (this.text !== content) {
this._text = content;
this.notify('text');
if (!_WAYLAND && content !== null) {
this._clipboard.set_text(content, -1);
}
}
}
async _onNameAppeared(connection, name, name_owner) {
try {
this._clipboard = new ClipboardProxy();
await new Promise((resolve, reject) => {
this._clipboard.init_async(
GLib.PRIORITY_DEFAULT,
null,
(proxy, res) => {
try {
proxy.init_finish(res);
resolve();
} catch (e) {
this._clipboard = null;
reject(e);
}
}
);
});
this._clipboard.bind_property(
'text',
this,
'text',
(GObject.BindingFlags.BIDIRECTIONAL |
GObject.BindingFlags.SYNC_CREATE)
);
} catch (e) {
logError(e);
}
}
_onNameVanished(connection, name) {
try {
if (this._clipboard !== null) {
this._clipboard.destroy();
this._clipboard = null;
}
} catch (e) {
logError(e);
}
}
_onTextReceived(clipboard, text) {
if (typeof text === 'string' && this.text !== text) {
this._text = text;
this.notify('text');
}
}
_onTargetsReceived(clipboard, atoms) {
// Empty clipboard
if (atoms.length === 0) {
this._onTextReceived(clipboard, '');
return;
}
// As a special case we need to ignore copied files (eg. in Nautilus)
if (atoms.includes('text/uri-list')) {
return;
}
// Let GtkClipboard filter for supported types
clipboard.request_text(this._onTextReceived.bind(this));
}
_onOwnerChange(clipboard, event) {
clipboard.request_targets(this._onTargetsReceived.bind(this));
}
destroy() {
if (this._nameWatcherId) {
Gio.bus_unwatch_name(this._nameWatcherId);
if (this._clipboard !== null) {
this._clipboard.destroy();
}
}
if (this._ownerChangeId) {
this._clipboard.disconnect(this._ownerChangeId);
}
}
});
/**
* The service class for this component
*/
var Component = Clipboard;

View File

@@ -0,0 +1,700 @@
'use strict';
const GdkPixbuf = imports.gi.GdkPixbuf;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
var HAVE_EDS = true;
try {
var EBook = imports.gi.EBook;
var EBookContacts = imports.gi.EBookContacts;
var EDataServer = imports.gi.EDataServer;
} catch (e) {
HAVE_EDS = false;
}
/**
* A store for contacts
*/
var Store = GObject.registerClass({
GTypeName: 'GSConnectContactsStore',
Properties: {
'context': GObject.ParamSpec.string(
'context',
'Context',
'Used as the cache directory, relative to gsconnect.cachedir',
GObject.ParamFlags.CONSTRUCT_ONLY | GObject.ParamFlags.READWRITE,
null
)
},
Signals: {
'contact-added': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_STRING]
},
'contact-removed': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_STRING]
},
'contact-changed': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_STRING]
}
}
}, class Store extends GObject.Object {
_init(context = null) {
super._init({
context: context
});
this.__cache_data = {};
// If Evolution Data Server is available, load it now
if (context === null && HAVE_EDS) {
this._ebooks = new Map();
this._initEvolutionDataServer();
}
}
/**
* Parse an EContact and return a small Object
*
* @param {EBookContacts.Contact} econtact - an EContact to parse
* @param {string} [origin] - an optional origin string
* @returns {Object} - a small JSON serializable object
*/
async _parseEContact(econtact, origin = 'desktop') {
try {
let contact = {
id: econtact.id,
name: _('Unknown Contact'),
numbers: [],
origin: origin,
timestamp: 0
};
// Try to get a contact name
if (econtact.full_name)
contact.name = econtact.full_name;
// Parse phone numbers
let nums = econtact.get_attributes(EBookContacts.ContactField.TEL);
for (let attr of nums) {
let number = {
value: attr.get_value(),
type: 'unknown'
};
if (attr.has_type('CELL'))
number.type = 'cell';
else if (attr.has_type('HOME'))
number.type = 'home';
else if (attr.has_type('WORK'))
number.type = 'work';
contact.numbers.push(number);
}
// Try and get a contact photo
let photo = econtact.photo;
if (photo) {
if (photo.type === EBookContacts.ContactPhotoType.INLINED) {
let data = photo.get_inlined()[0];
contact.avatar = await this.storeAvatar(data);
} else if (photo.type === EBookContacts.ContactPhotoType.URI) {
let uri = econtact.photo.get_uri();
contact.avatar = uri.replace('file://', '');
}
}
return contact;
} catch (e) {
logError(e, `Failed to parse VCard contact ${econtact.id}`);
return undefined;
}
}
/*
* EDS Helpers
*/
_getEBookClient(source, cancellable = null) {
return new Promise((resolve, reject) => {
EBook.BookClient.connect(source, 0, cancellable, (source, res) => {
try {
resolve(EBook.BookClient.connect_finish(res));
} catch (e) {
reject(e);
}
});
});
}
_getEBookView(client, query = '', cancellable = null) {
return new Promise((resolve, reject) => {
client.get_view(query, cancellable, (client, res) => {
try {
resolve(client.get_view_finish(res)[1]);
} catch (e) {
reject(e);
}
});
});
}
_getEContacts(client, query = '', cancellable = null) {
return new Promise((resolve, reject) => {
client.get_contacts(query, cancellable, (client, res) => {
try {
resolve(client.get_contacts_finish(res)[1]);
} catch (e) {
debug(e);
reject([]);
}
});
});
}
_getESourceRegistry(cancellable = null) {
return new Promise ((resolve, reject) => {
EDataServer.SourceRegistry.new(cancellable, (registry, res) => {
try {
resolve(EDataServer.SourceRegistry.new_finish(res));
} catch (e) {
reject(e);
}
});
});
}
/*
* AddressBook DBus callbacks
*/
async _onObjectsAdded(connection, sender, path, iface, signal, params) {
try {
let adds = params.get_child_value(0).get_strv();
// NOTE: sequential pairs of vcard, id
for (let i = 0, len = adds.length; i < len; i += 2) {
try {
let vcard = adds[i];
let econtact = EBookContacts.Contact.new_from_vcard(vcard);
let contact = await this._parseEContact(econtact);
if (contact !== undefined) {
this.add(contact, false);
}
} catch (e) {
debug(e);
}
}
} catch (e) {
debug(e);
}
}
_onObjectsRemoved(connection, sender, path, iface, signal, params) {
try {
let changes = params.get_child_value(0).get_strv();
for (let id of changes) {
try {
this.remove(id, false);
} catch (e) {
debug(e);
}
}
} catch (e) {
debug(e);
}
}
async _onObjectsModified(connection, sender, path, iface, signal, params) {
try {
let changes = params.get_child_value(0).get_strv();
// NOTE: sequential pairs of vcard, id
for (let i = 0, len = changes.length; i < len; i += 2) {
try {
let vcard = changes[i];
let econtact = EBookContacts.Contact.new_from_vcard(vcard);
let contact = await this._parseEContact(econtact);
if (contact !== undefined) {
this.add(contact, false);
}
} catch (e) {
debug(e);
}
}
} catch (e) {
debug(e);
}
}
/*
* SourceRegistryWatcher callbacks
*/
async _onAppeared(watcher, source) {
try {
// Get an EBookClient and EBookView
let uid = source.get_uid();
let client = await this._getEBookClient(source);
let view = await this._getEBookView(client, 'exists "tel"');
// Watch the view for changes to the address book
let connection = view.get_connection();
let objectPath = view.get_object_path();
view._objectsAddedId = connection.signal_subscribe(
null,
'org.gnome.evolution.dataserver.AddressBookView',
'ObjectsAdded',
objectPath,
null,
Gio.DBusSignalFlags.NONE,
this._onObjectsAdded.bind(this)
);
view._objectsRemovedId = connection.signal_subscribe(
null,
'org.gnome.evolution.dataserver.AddressBookView',
'ObjectsRemoved',
objectPath,
null,
Gio.DBusSignalFlags.NONE,
this._onObjectsRemoved.bind(this)
);
view._objectsModifiedId = connection.signal_subscribe(
null,
'org.gnome.evolution.dataserver.AddressBookView',
'ObjectsModified',
objectPath,
null,
Gio.DBusSignalFlags.NONE,
this._onObjectsModified.bind(this)
);
view.start();
// Store the EBook in a map
this._ebooks.set(uid, {
source: source,
client: client,
view: view
});
} catch (e) {
debug(e);
}
}
_onDisappeared(watcher, source) {
try {
let uid = source.get_uid();
let ebook = this._ebooks.get(uid);
if (ebook === undefined)
return;
// Disconnect and dispose the EBookView
if (ebook.view) {
let connection = ebook.view.get_connection();
connection.signal_unsubscribe(ebook.view._objectsAddedId);
connection.signal_unsubscribe(ebook.view._objectsRemovedId);
connection.signal_unsubscribe(ebook.view._objectsModifiedId);
ebook.view.stop();
ebook.view.run_dispose();
ebook.view = null;
}
// Dispose the EBookClient
if (ebook.client) {
ebook.client.run_dispose();
ebook.client = null;
}
// Drop the EBook
this._ebooks.delete(uid);
} catch (e) {
debug(e);
}
}
async _initEvolutionDataServer() {
try {
// Get the current EBooks
let registry = await this._getESourceRegistry();
for (let source of registry.list_sources('Address Book')) {
await this._onAppeared(null, source);
}
// Watch for new and removed sources
this._watcher = new EDataServer.SourceRegistryWatcher({
registry: registry,
extension_name: 'Address Book'
});
this._appearedId = this._watcher.connect(
'appeared',
this._onAppeared.bind(this)
);
this._disappearedId = this._watcher.connect(
'disappeared',
this._onDisappeared.bind(this)
);
} catch (e) {
logError(e);
}
}
*[Symbol.iterator]() {
let contacts = this.contacts;
for (let i = 0, len = contacts.length; i < len; i++) {
yield contacts[i];
}
}
get contacts() {
return Object.values(this.__cache_data);
}
get context() {
if (this._context === undefined) {
this._context = null;
}
return this._context;
}
set context(context) {
this._context = context;
if (context === null) {
this.__cache_dir = Gio.File.new_for_path(gsconnect.cachedir);
} else {
this.__cache_dir = Gio.File.new_for_path(
GLib.build_filenamev([gsconnect.cachedir, context])
);
}
GLib.mkdir_with_parents(this.__cache_dir.get_path(), 448);
this.__cache_file = this.__cache_dir.get_child('contacts.json');
}
/**
* Save a ByteArray to file and return the path
*
* @param {ByteArray} contents - An image ByteArray
* @return {string|undefined} - File path or %undefined on failure
*/
storeAvatar(contents) {
return new Promise((resolve, reject) => {
let md5 = GLib.compute_checksum_for_data(
GLib.ChecksumType.MD5,
contents
);
let file = this.__cache_dir.get_child(`${md5}`);
if (file.query_exists(null)) {
resolve(file.get_path());
} else {
file.replace_contents_bytes_async(
new GLib.Bytes(contents),
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null,
(file, res) => {
try {
file.replace_contents_finish(res);
resolve(file.get_path());
} catch (e) {
debug(e, 'Storing avatar');
resolve(undefined);
}
}
);
}
});
}
/**
* Query the Store for a contact by name and/or number.
*
* @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
*/
query(query) {
// First look for an existing contact by number
let contacts = this.contacts;
let matches = [];
let qnumber = query.number.toPhoneNumber();
for (let i = 0, len = contacts.length; i < len; i++) {
let contact = contacts[i];
for (let num of contact.numbers) {
let cnumber = num.value.toPhoneNumber();
if (qnumber.endsWith(cnumber) || cnumber.endsWith(qnumber)) {
// If no query name or exact match, return immediately
if (!query.name || query.name === contact.name) {
return contact;
}
// Otherwise we might find an exact name match that shares
// the number with another contact
matches.push(contact);
}
}
}
// Return the first match (pretty much what Android does)
if (matches.length > 0) return matches[0];
// No match; return a mock contact with a unique ID
let id = GLib.uuid_string_random();
while (this.__cache_data.hasOwnProperty(id)) {
id = GLib.uuid_string_random();
}
return {
id: id,
name: query.name || query.number,
numbers: [{value: query.number, type: 'unknown'}],
origin: 'gsconnect'
};
}
get_contact(position) {
try {
return (this.__cache_data[position]) ? this.__cache_data[position] : null;
} catch (e) {
return null;
}
}
/**
* Add a contact, checking for validity
*
* @param {object} contact - A contact object
* @param {boolean} write - Write to disk
*/
add(contact, write = true) {
// Ensure the contact has a unique id
if (!contact.id) {
let id = GLib.uuid_string_random();
while (this.__cache_data[id]) {
id = GLib.uuid_string_random();
}
contact.id = id;
}
// Ensure the contact has an origin
if (!contact.origin) {
contact.origin = 'gsconnect';
}
// This is an updated contact
if (this.__cache_data[contact.id]) {
this.__cache_data[contact.id] = contact;
this.emit('contact-changed', contact.id);
// This is a new contact
} else {
this.__cache_data[contact.id] = contact;
this.emit('contact-added', contact.id);
}
// Write if requested
if (write) {
this.save();
}
}
/**
* Remove a contact by id
*
* @param {string} id - The id of the contact to delete
* @param {boolean} write - Write to disk
*/
remove(id, write = true) {
// Only remove if the contact actually exists
if (this.__cache_data[id]) {
delete this.__cache_data[id];
this.emit('contact-removed', id);
// Write if requested
if (write) {
this.save();
}
}
}
/**
* Lookup a contact for each address object in @addresses and return a
* dictionary of address (eg. phone number) to contact object.
*
* { "555-5555": { "name": "...", "numbers": [], ... } }
*
* @param {Array of object} addresses - A list of address objects
* @return {object} - A dictionary of phone numbers and contacts
*/
lookupAddresses(addresses) {
let contacts = {};
// Lookup contacts for each address
for (let i = 0, len = addresses.length; i < len; i++) {
let address = addresses[i].address;
contacts[address] = this.query({
number: address
});
}
return contacts;
}
async clear() {
try {
let contacts = this.contacts;
for (let i = 0, len = contacts.length; i < len; i++) {
await this.remove(contacts[i].id, false);
}
await this.save();
} catch (e) {
debug(e, 'Clearing contacts');
}
}
/**
* Update the contact store from a dictionary of our custom contact objects.
*
* @param {Object} json - an Object of contact Objects
*/
async update(json = {}) {
try {
let contacts = Object.values(json);
for (let i = 0, len = contacts.length; i < len; i++) {
let new_contact = contacts[i];
let contact = this.__cache_data[new_contact.id];
if (!contact || new_contact.timestamp !== contact.timestamp) {
await this.add(new_contact, false);
}
}
// Prune contacts
contacts = this.contacts;
for (let i = 0, len = contacts.length; i < len; i++) {
let contact = contacts[i];
if (!json[contact.id]) {
await this.remove(contact.id, false);
}
}
await this.save();
} catch (e) {
debug(e, 'Updating contacts');
}
}
/**
* Fetch and update the contact store from its source. The default function
* simply logs a debug message if EDS is unavailable, while derived classes
* should request an update from the remote source.
*/
async fetch() {
try {
if (HAVE_EDS === false) {
throw new Error('Evolution Data Server not available');
}
} catch (e) {
debug(e);
}
}
/**
* Load the contacts from disk.
*/
async load() {
try {
this.__cache_data = await JSON.load(this.__cache_file);
} catch (e) {
debug(e);
} finally {
this.notify('context');
}
}
/**
* Save the contacts to disk.
*/
async save() {
if (this.context === null && HAVE_EDS) {
return;
}
if (this.__cache_lock) {
this.__cache_queue = true;
return;
}
try {
this.__cache_lock = true;
await JSON.dump(this.__cache_data, this.__cache_file);
} catch (e) {
debug(e);
} finally {
this.__cache_lock = false;
if (this.__cache_queue) {
this.__cache_queue = false;
this.save();
}
}
}
destroy() {
if (this.__disposed === undefined) {
this.__disposed = true;
if (HAVE_EDS) {
this._watcher.disconnect(this._appearedId);
this._watcher.disconnect(this._disappearedId);
this._watcher.run_dispose();
for (let ebook of this._ebooks.values()) {
this._onDisappeared(null, ebook.source);
}
}
this.run_dispose();
}
}
});
/**
* The service class for this component
*/
var Component = Store;

View File

@@ -0,0 +1,567 @@
'use strict';
const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const SESSION_TIMEOUT = 15;
const RemoteSession = GObject.registerClass({
GTypeName: 'GSConnectRemoteSession',
Implements: [Gio.DBusInterface],
Signals: {
'closed': {
flags: GObject.SignalFlags.RUN_FIRST
}
}
}, class RemoteSession extends Gio.DBusProxy {
_init(objectPath) {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: 'org.gnome.Mutter.RemoteDesktop',
g_object_path: objectPath,
g_interface_name: 'org.gnome.Mutter.RemoteDesktop.Session',
g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES
});
this._started = false;
}
vfunc_g_signal(sender_name, signal_name, parameters) {
if (signal_name === 'Closed') {
this.emit('closed');
}
}
_call(name, parameters = null) {
if (!this._started) return;
this.call(name, parameters, Gio.DBusCallFlags.NONE, -1, null, null);
}
async start() {
try {
if (this._started) return;
// Initialize the proxy
await new Promise((resolve, reject) => {
this.init_async(
GLib.PRIORITY_DEFAULT,
null,
(proxy, res) => {
try {
proxy.init_finish(res);
resolve();
} catch (e) {
reject(e);
}
}
);
});
// Start the session
await new Promise((resolve, reject) => {
this.call(
'Start',
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(proxy, res) => {
try {
resolve(proxy.call_finish(res));
} catch (e) {
reject(e);
}
}
);
});
this._started = true;
} catch (e) {
this.destroy();
Gio.DBusError.strip_remote_error(e);
throw e;
}
}
stop() {
if (this._started) {
this._started = false;
this.call('Stop', null, Gio.DBusCallFlags.NONE, -1, null, null);
}
}
_translateButton(button) {
switch (button) {
case Gdk.BUTTON_PRIMARY:
return 0x110;
case Gdk.BUTTON_MIDDLE:
return 0x112;
case Gdk.BUTTON_SECONDARY:
return 0x111;
case 4:
return 0; // FIXME
case 5:
return 0x10F; // up
}
}
movePointer(dx, dy) {
this._call(
'NotifyPointerMotionRelative',
GLib.Variant.new('(dd)', [dx, dy])
);
}
pressPointer(button) {
button = this._translateButton(button);
this._call(
'NotifyPointerButton',
GLib.Variant.new('(ib)', [button, true])
);
}
releasePointer(button) {
button = this._translateButton(button);
this._call(
'NotifyPointerButton',
GLib.Variant.new('(ib)', [button, false])
);
}
clickPointer(button) {
button = this._translateButton(button);
this._call(
'NotifyPointerButton',
GLib.Variant.new('(ib)', [button, true])
);
this._call(
'NotifyPointerButton',
GLib.Variant.new('(ib)', [button, false])
);
}
doubleclickPointer(button) {
this.clickPointer(button);
this.clickPointer(button);
}
scrollPointer(dx, dy) {
// TODO: NotifyPointerAxis only seems to work on Wayland, but maybe
// NotifyPointerAxisDiscrete is the better choice anyways
if (_WAYLAND) {
this._call(
'NotifyPointerAxis',
GLib.Variant.new('(ddu)', [dx, dy, 0])
);
this._call(
'NotifyPointerAxis',
GLib.Variant.new('(ddu)', [0, 0, 1])
);
} else {
if (dy > 0) {
this._call(
'NotifyPointerAxisDiscrete',
GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, 1])
);
} else if (dy < 0) {
this._call(
'NotifyPointerAxisDiscrete',
GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, -1])
);
}
}
}
/**
* Keyboard Events
*/
pressKeysym(keysym) {
this._call(
'NotifyKeyboardKeysym',
GLib.Variant.new('(ub)', [keysym, true])
);
}
releaseKeysym(keysym) {
this._call(
'NotifyKeyboardKeysym',
GLib.Variant.new('(ub)', [keysym, false])
);
}
pressreleaseKeysym(keysym) {
this._call(
'NotifyKeyboardKeysym',
GLib.Variant.new('(ub)', [keysym, true])
);
this._call(
'NotifyKeyboardKeysym',
GLib.Variant.new('(ub)', [keysym, false])
);
}
/**
* High-level keyboard input
*/
pressKey(input, modifiers) {
// Press Modifiers
if (modifiers & Gdk.ModifierType.MOD1_MASK) this.pressKeysym(Gdk.KEY_Alt_L);
if (modifiers & Gdk.ModifierType.CONTROL_MASK) this.pressKeysym(Gdk.KEY_Control_L);
if (modifiers & Gdk.ModifierType.SHIFT_MASK) this.pressKeysym(Gdk.KEY_Shift_L);
if (modifiers & Gdk.ModifierType.SUPER_MASK) this.pressKeysym(Gdk.KEY_Super_L);
if (typeof input === 'string') {
let keysym = Gdk.unicode_to_keyval(input.codePointAt(0));
this.pressreleaseKeysym(keysym);
} else {
this.pressreleaseKeysym(input);
}
// Release Modifiers
if (modifiers & Gdk.ModifierType.MOD1_MASK) this.releaseKeysym(Gdk.KEY_Alt_L);
if (modifiers & Gdk.ModifierType.CONTROL_MASK) this.releaseKeysym(Gdk.KEY_Control_L);
if (modifiers & Gdk.ModifierType.SHIFT_MASK) this.releaseKeysym(Gdk.KEY_Shift_L);
if (modifiers & Gdk.ModifierType.SUPER_MASK) this.releaseKeysym(Gdk.KEY_Super_L);
}
destroy() {
if (this.__disposed === undefined) {
this.__disposed = true;
this.run_dispose();
}
}
});
const Controller = class Controller {
constructor() {
this._nameAppearedId = 0;
this._session = null;
this._sessionCloseId = 0;
this._sessionExpiry = 0;
this._sessionExpiryId = 0;
this._sessionStarting = false;
// Watch for the RemoteDesktop portal
this._nameWatcherId = Gio.bus_watch_name(
Gio.BusType.SESSION,
'org.gnome.Mutter.RemoteDesktop',
Gio.BusNameWatcherFlags.NONE,
this._onNameAppeared.bind(this),
this._onNameVanished.bind(this)
);
}
get connection() {
if (this._connection === undefined) {
this._connection = null;
}
return this._connection;
}
_checkWayland() {
if (_WAYLAND) {
// eslint-disable-next-line no-global-assign
HAVE_REMOTEINPUT = false;
let service = Gio.Application.get_default();
// First we're going to disabled the mousepad plugin on all devices
for (let device of service.devices) {
let supported = device.settings.get_strv('supported-plugins');
supported = supported.splice(supported.indexOf('mousepad'), 1);
device.settings.set_strv('supported-plugins', supported);
}
// Second we need to amend the service identity and broadcast
service._identity = undefined;
service._identify();
return true;
}
return false;
}
_onNameAppeared(connection, name, name_owner) {
try {
this._connection = connection;
} catch (e) {
logError(e);
}
}
_onNameVanished(connection, name) {
try {
if (this._session !== null) {
this._onSessionClosed(this._session);
}
} catch (e) {
logError(e);
}
}
_onSessionClosed(session) {
// Disconnect from the session
if (this._sessionClosedId > 0) {
session.disconnect(this._sessionClosedId);
this._sessionClosedId = 0;
}
// Destroy the session
session.destroy();
this._session = null;
}
_onSessionExpired() {
// If the session has been used recently, schedule a new expiry
let remainder = Math.floor(this._sessionExpiry - (Date.now() / 1000));
if (remainder > 0) {
this._sessionExpiryId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT,
remainder,
this._onSessionExpired.bind(this)
);
return GLib.SOURCE_REMOVE;
}
// Otherwise if there's an active session, close it
if (this._session !== null) {
this._session.stop();
}
// Reset the GSource Id
this._sessionExpiryId = 0;
return GLib.SOURCE_REMOVE;
}
_createSession() {
return new Promise((resolve, reject) => {
if (this.connection === null) {
reject(new Error('No DBus connection'));
return;
}
this.connection.call(
'org.gnome.Mutter.RemoteDesktop',
'/org/gnome/Mutter/RemoteDesktop',
'org.gnome.Mutter.RemoteDesktop',
'CreateSession',
null,
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(connection, res) => {
try {
res = connection.call_finish(res);
resolve(res.deepUnpack()[0]);
} catch (e) {
reject(e);
}
}
);
});
}
async _ensureAdapter() {
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');
// If we got here in Wayland, we need to re-adjust and bail
if (this._checkWayland()) return;
let fallback = imports.service.components.atspi;
this._session = new fallback.Controller();
// Mutter is available and there isn't another session starting
} else if (this._sessionStarting === false) {
this._sessionStarting = true;
debug('Creating Mutter RemoteDesktop session');
let objectPath = await this._createSession();
this._session = new RemoteSession(objectPath);
await this._session.start();
this._sessionClosedId = this._session.connect(
'closed',
this._onSessionClosed.bind(this)
);
if (this._sessionExpiryId === 0) {
this._sessionExpiryId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT,
SESSION_TIMEOUT,
this._onSessionExpired.bind(this)
);
}
this._sessionStarting = false;
}
} catch (e) {
logError(e);
if (this._session !== null) {
this._session.destroy();
this._session = null;
}
this._sessionStarting = false;
}
}
/**
* Pointer Events
*/
movePointer(dx, dy) {
try {
if (dx === 0 && dy === 0) return;
this._ensureAdapter();
this._session.movePointer(dx, dy);
} catch (e) {
debug(e);
}
}
pressPointer(button) {
try {
this._ensureAdapter();
this._session.pressPointer(button);
} catch (e) {
debug(e);
}
}
releasePointer(button) {
try {
this._ensureAdapter();
this._session.releasePointer(button);
} catch (e) {
debug(e);
}
}
clickPointer(button) {
try {
this._ensureAdapter();
this._session.clickPointer(button);
} catch (e) {
debug(e);
}
}
doubleclickPointer(button) {
try {
this._ensureAdapter();
this._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);
}
}
/**
* Keyboard Events
*/
pressKeysym(keysym) {
try {
this._ensureAdapter();
this._session.pressKeysym(keysym);
} catch (e) {
debug(e);
}
}
releaseKeysym(keysym) {
try {
this._ensureAdapter();
this._session.releaseKeysym(keysym);
} catch (e) {
debug(e);
}
}
pressreleaseKeysym(keysym) {
try {
this._ensureAdapter();
this._session.pressreleaseKeysym(keysym);
} catch (e) {
debug(e);
}
}
/**
* High-level keyboard input
*/
pressKey(input, modifiers) {
try {
this._ensureAdapter();
this._session.pressKey(input, modifiers);
} catch (e) {
debug(e);
}
}
destroy() {
if (this._session !== null) {
// Disconnect from the session
if (this._sessionClosedId > 0) {
this._session.disconnect(this._sessionClosedId);
this._sessionClosedId = 0;
}
this._session.destroy();
this._session = null;
}
if (this._nameWatcherId > 0) {
Gio.bus_unwatch_name(this._nameWatcherId);
this._nameWatcherId = 0;
}
}
};
/**
* The service class for this component
*/
var Component = Controller;

View File

@@ -0,0 +1,781 @@
'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
var Player = GObject.registerClass({
GTypeName: 'GSConnectMPRISPlayer',
Implements: [Gio.DBusInterface],
Properties: {
// Application Properties
'CanQuit': GObject.ParamSpec.boolean(
'CanQuit',
'Can Quit',
'Whether the client can call the Quit method.',
GObject.ParamFlags.READABLE,
false
),
'Fullscreen': GObject.ParamSpec.boolean(
'Fullscreen',
'Fullscreen',
'Whether the player is in fullscreen mode.',
GObject.ParamFlags.READWRITE,
false
),
'CanSetFullscreen': GObject.ParamSpec.boolean(
'CanSetFullscreen',
'Can Set Fullscreen',
'Whether the client can set the Fullscreen property.',
GObject.ParamFlags.READABLE,
false
),
'CanRaise': GObject.ParamSpec.boolean(
'CanRaise',
'Can Raise',
'Whether the client can call the Raise method.',
GObject.ParamFlags.READABLE,
false
),
'HasTrackList': GObject.ParamSpec.boolean(
'HasTrackList',
'Has Track List',
'Whether the player has a track list.',
GObject.ParamFlags.READABLE,
false
),
'Identity': GObject.ParamSpec.string(
'Identity',
'Identity',
'The application name.',
GObject.ParamFlags.READABLE,
null
),
'DesktopEntry': GObject.ParamSpec.string(
'DesktopEntry',
'DesktopEntry',
'The basename of an installed .desktop file.',
GObject.ParamFlags.READABLE,
null
),
'SupportedUriSchemes': GObject.param_spec_variant(
'SupportedUriSchemes',
'Supported URI Schemes',
'The URI schemes supported by the media player.',
new GLib.VariantType('as'),
null,
GObject.ParamFlags.READABLE
),
'SupportedMimeTypes': GObject.param_spec_variant(
'SupportedMimeTypes',
'Supported MIME Types',
'The mime-types supported by the media player.',
new GLib.VariantType('as'),
null,
GObject.ParamFlags.READABLE
),
// Player Properties
'PlaybackStatus': GObject.ParamSpec.string(
'PlaybackStatus',
'Playback Status',
'The current playback status.',
GObject.ParamFlags.READABLE,
null
),
'LoopStatus': GObject.ParamSpec.string(
'LoopStatus',
'Loop Status',
'The current loop status.',
GObject.ParamFlags.READWRITE,
null
),
'Rate': GObject.ParamSpec.double(
'Rate',
'Rate',
'The current playback rate.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'MinimumRate': GObject.ParamSpec.double(
'MinimumRate',
'Minimum Rate',
'The minimum playback rate.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'MaximimRate': GObject.ParamSpec.double(
'MaximumRate',
'Maximum Rate',
'The maximum playback rate.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'Shuffle': GObject.ParamSpec.boolean(
'Shuffle',
'Shuffle',
'Whether track changes are linear.',
GObject.ParamFlags.READWRITE,
null
),
'Metadata': GObject.param_spec_variant(
'Metadata',
'Metadata',
'The metadata of the current element.',
new GLib.VariantType('a{sv}'),
null,
GObject.ParamFlags.READABLE
),
'Volume': GObject.ParamSpec.double(
'Volume',
'Volume',
'The volume level.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'Position': GObject.ParamSpec.int64(
'Position',
'Position',
'The current track position in microseconds.',
GObject.ParamFlags.READABLE,
0, Number.MAX_SAFE_INTEGER,
0
),
'CanGoNext': GObject.ParamSpec.boolean(
'CanGoNext',
'Can Go Next',
'Whether the client can call the Next method.',
GObject.ParamFlags.READABLE,
false
),
'CanGoPrevious': GObject.ParamSpec.boolean(
'CanGoPrevious',
'Can Go Previous',
'Whether the client can call the Previous method.',
GObject.ParamFlags.READABLE,
false
),
'CanPlay': GObject.ParamSpec.boolean(
'CanPlay',
'Can Play',
'Whether playback can be started using Play or PlayPause.',
GObject.ParamFlags.READABLE,
false
),
'CanPause': GObject.ParamSpec.boolean(
'CanPause',
'Can Pause',
'Whether playback can be paused using Play or PlayPause.',
GObject.ParamFlags.READABLE,
false
),
'CanSeek': GObject.ParamSpec.boolean(
'CanSeek',
'Can Seek',
'Whether the client can control the playback position using Seek and SetPosition.',
GObject.ParamFlags.READABLE,
false
),
'CanControl': GObject.ParamSpec.boolean(
'CanControl',
'Can Control',
'Whether the media player may be controlled over this interface.',
GObject.ParamFlags.READABLE,
false
)
},
Signals: {
'Seeked': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_INT64]
}
}
}, class Player extends Gio.DBusProxy {
_init(name) {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: name,
g_object_path: '/org/mpris/MediaPlayer2',
g_interface_name: 'org.mpris.MediaPlayer2.Player'
});
this._application = new Gio.DBusProxy({
g_bus_type: Gio.BusType.SESSION,
g_name: name,
g_object_path: '/org/mpris/MediaPlayer2',
g_interface_name: 'org.mpris.MediaPlayer2'
});
this._propertiesChangedId = this._application.connect(
'g-properties-changed',
this._onPropertiesChanged.bind(this)
);
this._cancellable = new Gio.Cancellable();
}
vfunc_g_properties_changed(changed, invalidated) {
try {
if (this.__disposed !== undefined)
return;
for (let name in changed.deepUnpack()) {
this.notify(name);
}
} catch (e) {
debug(e, this.g_name);
}
}
vfunc_g_signal(sender_name, signal_name, parameters) {
try {
if (signal_name === 'Seeked') {
this.emit('Seeked', parameters.deepUnpack()[0]);
}
} catch (e) {
debug(e, this.g_name);
}
}
_call(name, parameters = null) {
this.call(
name,
parameters,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
this._cancellable,
(proxy, result) => {
try {
proxy.call_finish(result);
} catch (e) {
Gio.DBusError.strip_remote_error(e);
logError(e, this.g_name);
}
}
);
}
_get(name, fallback = null) {
try {
return this.get_cached_property(name).recursiveUnpack();
} catch (e) {
return fallback;
}
}
_set(name, value) {
try {
this.set_cached_property(name, value);
this.call(
'org.freedesktop.DBus.Properties.Set',
new GLib.Variant('(ssv)', [this.g_interface_name, name, value]),
Gio.DBusCallFlags.NO_AUTO_START,
-1,
this._cancellable,
(proxy, result) => {
try {
proxy.call_finish(result);
} catch (e) {
logError(e);
}
}
);
} catch (e) {
logError(e, this.g_name);
}
}
_onPropertiesChanged(proxy, changed, invalidated) {
try {
if (this.__disposed !== undefined)
return;
for (let name in changed.deepUnpack()) {
this.notify(name);
}
} catch (e) {
logError(e, this.g_name);
}
}
initPromise() {
let player = new Promise((resolve, reject) => {
this.init_async(0, this._cancellable, (proxy, res) => {
try {
resolve(proxy.init_finish(res));
} catch (e) {
reject(e);
}
});
});
let application = new Promise((resolve, reject) => {
this._application.init_async(0, this._cancellable, (proxy, res) => {
try {
resolve(proxy.init_finish(res));
} catch (e) {
reject(e);
}
});
});
return Promise.all([player, application]);
}
/*
* The org.mpris.MediaPlayer2 Interface
*/
get CanQuit() {
return this._get.call(this._application, 'CanQuit', false);
}
get Fullscreen() {
return this._get.call(this._application, 'Fullscreen', false);
}
set Fullscreen(mode) {
this._set.call(this._application, 'Fullscreen', new GLib.Variant('b', mode));
}
get CanSetFullscreen() {
return this._get.call(this._application, 'CanSetFullscreen', false);
}
get CanRaise() {
return this._get.call(this._application, 'CanRaise', false);
}
get HasTrackList() {
return this._get.call(this._application, 'HasTrackList', false);
}
get Identity() {
return this._get.call(this._application, 'Identity', _('Unknown'));
}
get DesktopEntry() {
return this._get.call(this._application, 'DesktopEntry', null);
}
get SupportedUriSchemes() {
return this._get.call(this._application, 'SupportedUriSchemes', []);
}
get SupportedMimeTypes() {
return this._get.call(this._application, 'SupportedMimeTypes', []);
}
Raise() {
this._call.call(this._application, 'Raise');
}
Quit() {
this._call.call(this._application, 'Quit');
}
/*
* The org.mpris.MediaPlayer2.Player Interface
*/
get PlaybackStatus() {
return this._get('PlaybackStatus', 'Stopped');
}
// 'None', 'Track', 'Playlist'
get LoopStatus() {
return this._get('LoopStatus', 'None');
}
set LoopStatus(status) {
this._set('LoopStatus', new GLib.Variant('s', status));
}
get Rate() {
return this._get('Rate', 1.0);
}
set Rate(rate) {
this._set('Rate', new GLib.Variant('d', rate));
}
get Shuffle() {
return this._get('Shuffle', false);
}
set Shuffle(mode) {
this._set('Shuffle', new GLib.Variant('b', mode));
}
get Metadata() {
if (this._metadata == undefined) {
this._metadata = {
'xesam:artist': [_('Unknown')],
'xesam:album': _('Unknown'),
'xesam:title': _('Unknown'),
'mpris:length': 0
};
}
return this._get('Metadata', this._metadata);
}
get Volume() {
return this._get('Volume', 1.0);
}
set Volume(level) {
this._set('Volume', new GLib.Variant('d', level));
}
// g-properties-changed is not emitted for this property
get Position() {
try {
let reply = this.call_sync(
'org.freedesktop.DBus.Properties.Get',
new GLib.Variant('(ss)', [this.g_interface_name, 'Position']),
Gio.DBusCallFlags.NONE,
-1,
null
);
return reply.recursiveUnpack()[0];
} catch (e) {
return 0;
}
}
get MinimumRate() {
return this._get('MinimumRate', 1.0);
}
get MaximumRate() {
return this._get('MaximumRate', 1.0);
}
get CanGoNext() {
return this._get('CanGoNext', false);
}
get CanGoPrevious() {
return this._get('CanGoPrevious', false);
}
get CanPlay() {
return this._get('CanPlay', false);
}
get CanPause() {
return this._get('CanPause', false);
}
get CanSeek() {
return this._get('CanSeek', false);
}
get CanControl() {
return this._get('CanControl', false);
}
Next() {
this._call('Next');
}
Previous() {
this._call('Previous');
}
Pause() {
this._call('Pause');
}
PlayPause() {
this._call('PlayPause');
}
Stop() {
this._call('Stop');
}
Play() {
this._call('Play');
}
Seek(offset) {
this._call('Seek', new GLib.Variant('(x)', [offset]));
}
SetPosition(trackId, position) {
this._call('SetPosition', new GLib.Variant('(ox)', [trackId, position]));
}
OpenUri(uri) {
this._call('OpenUri', new GLib.Variant('(s)', [uri]));
}
destroy() {
if (this.__disposed === undefined) {
this.__disposed = true;
this._cancellable.cancel();
this._application.disconnect(this._propertiesChangedId);
this._application.run_dispose();
this.run_dispose();
}
}
});
var Manager = GObject.registerClass({
GTypeName: 'GSConnectMPRISManager',
Implements: [Gio.DBusInterface],
Properties: {
'identities': GObject.param_spec_variant(
'identities',
'IdentityList',
'A list of MediaPlayer2.Identity for each player',
new GLib.VariantType('as'),
null,
GObject.ParamFlags.READABLE
),
// Actually returns an Object of MediaPlayer2Proxy objects,
// Player.Identity as key
'players': GObject.param_spec_variant(
'players',
'PlayerList',
'A list of known devices',
new GLib.VariantType('a{sv}'),
null,
GObject.ParamFlags.READABLE
)
},
Signals: {
'player-changed': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_OBJECT]
},
'player-seeked': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_OBJECT]
}
}
}, class Manager extends Gio.DBusProxy {
_init() {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: 'org.freedesktop.DBus',
g_object_path: '/org/freedesktop/DBus'
});
// Asynchronous setup
this._cancellable = new Gio.Cancellable();
this._init_async();
}
async _init_async() {
try {
await new Promise((resolve, reject) => {
this.init_async(0, this._cancellable, (proxy, res) => {
try {
resolve(proxy.init_finish(res));
} catch (e) {
reject(e);
}
});
});
let names = await new Promise((resolve, reject) => {
this.call(
'org.freedesktop.DBus.ListNames',
null,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
this._cancellable,
(proxy, res) => {
try {
resolve(proxy.call_finish(res).deepUnpack()[0]);
} catch (e) {
reject(e);
}
}
);
});
for (let i = 0, len = names.length; i < len; i++) {
let name = names[i];
if (name.startsWith('org.mpris.MediaPlayer2') &&
!name.includes('GSConnect')) {
this._addPlayer(name);
}
}
} catch (e) {
// FIXME: if something goes wrong the component will appear active
logError(e);
this.destroy();
}
}
get identities() {
let identities = [];
for (let player of this.players.values()) {
let identity = player.Identity;
if (identity)
identities.push(identity);
}
return identities;
}
get players() {
if (this._players === undefined) {
this._players = new Map();
}
return this._players;
}
get paused() {
if (this._paused === undefined) {
this._paused = new Map();
}
return this._paused;
}
vfunc_g_signal(sender_name, signal_name, parameters) {
try {
if (signal_name === 'NameOwnerChanged') {
let [name, old_owner, new_owner] = parameters.deepUnpack();
if (name.startsWith('org.mpris.MediaPlayer2') &&
!name.includes('GSConnect')) {
if (new_owner.length) {
this._addPlayer(name);
} else if (old_owner.length) {
this._removePlayer(name);
}
}
}
} catch (e) {
debug(e);
}
}
async _addPlayer(name) {
try {
if (!this.players.has(name)) {
let player = new Player(name);
await player.initPromise();
player.__propertiesId = player.connect(
'g-properties-changed',
(player) => this.emit('player-changed', player)
);
player.__seekedId = player.connect(
'Seeked',
(player) => this.emit('player-seeked', player)
);
this.players.set(name, player);
this.notify('players');
}
} catch (e) {
debug(e);
}
}
async _removePlayer(name) {
try {
let player = this.players.get(name);
if (player !== undefined) {
debug(`Removing MPRIS Player ${name}`);
player.disconnect(player.__propertiesId);
player.disconnect(player.__seekedId);
player.destroy();
this.paused.delete(name);
this.players.delete(name);
this.notify('players');
}
} catch (e) {
debug(e);
}
}
/**
* Get a player by its Identity.
*/
getPlayer(identity) {
for (let player of this.players.values()) {
if (player.Identity === identity) {
return player;
}
}
return null;
}
/**
* A convenience function for pausing all players currently playing.
*/
pauseAll() {
for (let [name, player] of this.players.entries()) {
if (player.PlaybackStatus === 'Playing' && player.CanPause) {
player.Pause();
this.paused.set(name, player);
}
}
}
/**
* A convenience function for restarting all players paused with pauseAll().
*/
unpauseAll() {
for (let player of this.paused.values()) {
if (player.PlaybackStatus === 'Paused' && player.CanPlay) {
player.Play();
}
}
this.paused.clear();
}
destroy() {
if (this.__disposed == undefined) {
this.__disposed = true;
this._cancellable.cancel();
for (let player of this.players.values()) {
player.disconnect(player.__propertiesId);
player.disconnect(player.__seekedId);
player.destroy();
}
this.players.clear();
this.run_dispose();
}
}
});
/**
* The service class for this component
*/
var Component = Manager;

View File

@@ -0,0 +1,420 @@
'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GjsPrivate = imports.gi.GjsPrivate;
const DBus = imports.utils.dbus;
let _nodeInfo = Gio.DBusNodeInfo.new_for_xml(`
<node>
<interface name="org.freedesktop.Notifications">
<method name="Notify">
<arg name="appName" type="s" direction="in"/>
<arg name="replacesId" type="u" direction="in"/>
<arg name="iconName" type="s" direction="in"/>
<arg name="summary" type="s" direction="in"/>
<arg name="body" type="s" direction="in"/>
<arg name="actions" type="as" direction="in"/>
<arg name="hints" type="a{sv}" direction="in"/>
<arg name="timeout" type="i" direction="in"/>
</method>
</interface>
<interface name="org.gtk.Notifications">
<method name="AddNotification">
<arg type="s" direction="in"/>
<arg type="s" direction="in"/>
<arg type="a{sv}" direction="in"/>
</method>
<method name="RemoveNotification">
<arg type="s" direction="in"/>
<arg type="s" direction="in"/>
</method>
</interface>
</node>
`);
const FDO_IFACE = _nodeInfo.lookup_interface('org.freedesktop.Notifications');
const FDO_MATCH = "interface='org.freedesktop.Notifications',member='Notify',type='method_call'";
const GTK_IFACE = _nodeInfo.lookup_interface('org.gtk.Notifications');
const GTK_MATCH = "interface='org.gtk.Notifications',member='AddNotification',type='method_call'";
/**
* A class for snooping Freedesktop (libnotify) and Gtk (GNotification)
* notifications and forwarding them to supporting devices.
*/
var Listener = class Listener {
constructor() {
// Respect desktop notification settings
this._settings = new Gio.Settings({
schema_id: 'org.gnome.desktop.notifications'
});
// Watch for new application policies
this._settingsId = this._settings.connect(
'changed::application-children',
this._onSettingsChanged.bind(this)
);
this._onSettingsChanged();
// Cache for appName->desktop-id lookups
this._names = {};
// Asynchronous setup
this._init_async();
}
get application() {
return Gio.Application.get_default();
}
get applications() {
if (this._applications === undefined) {
this._applications = {};
}
return this._applications;
}
/**
* Update application notification settings
*/
_onSettingsChanged() {
this._applications = {};
for (let app of this._settings.get_strv('application-children')) {
let appSettings = new Gio.Settings({
schema_id: 'org.gnome.desktop.notifications.application',
path: `/org/gnome/desktop/notifications/application/${app}/`
});
let appInfo = Gio.DesktopAppInfo.new(
appSettings.get_string('application-id')
);
if (appInfo !== null) {
this._applications[appInfo.get_name()] = appSettings;
}
}
}
_listNames() {
return new Promise((resolve, reject) => {
this._session.call(
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus',
'ListNames',
null,
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(connection, res) => {
try {
res = connection.call_finish(res);
resolve(res.deepUnpack()[0]);
} catch (e) {
reject(e);
}
}
);
});
}
_getNameOwner(name) {
return new Promise((resolve, reject) => {
this._session.call(
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus',
'GetNameOwner',
new GLib.Variant('(s)', [name]),
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(connection, res) => {
try {
res = connection.call_finish(res);
resolve(res.deepUnpack()[0]);
} catch (e) {
reject(e);
}
}
);
});
}
/**
* Try and find a well-known name for @sender on the session bus
*
* @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
*/
async _getAppId(sender, appName) {
try {
// Get a list of well-known names, ignoring @sender
let names = await this._listNames();
names.splice(names.indexOf(sender), 1);
// Make a short list for substring matches (fractal/org.gnome.Fractal)
let appLower = appName.toLowerCase();
let shortList = names.filter(name => {
return name.toLowerCase().includes(appLower);
});
// Run the short list first
for (let name of shortList) {
let nameOwner = await this._getNameOwner(name);
if (nameOwner === sender) {
return name;
}
names.splice(names.indexOf(name), 1);
}
// Run the full list
for (let name of names) {
let nameOwner = await this._getNameOwner(name);
if (nameOwner === sender) {
return name;
}
}
return null;
} catch (e) {
debug(e);
return null;
}
}
/**
* Try and find the application name for @sender
*
* @param {string} sender - A DBus unique name
* @param {string} appName - (Optional) appName supplied by Notify()
* @return {string} - A well-known name or %null
*/
async _getAppName(sender, appName) {
// Check the cache first
if (appName && this._names.hasOwnProperty(appName)) {
return this._names[appName];
}
let appId, appInfo;
try {
appId = await this._getAppId(sender, appName);
appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
this._names[appName] = appInfo.get_name();
appName = appInfo.get_name();
} catch (e) {
// Silence errors
}
return appName;
}
/**
* Callback for AddNotification()/Notify()
*/
async _onHandleMethodCall(impl, name, parameters, invocation) {
try {
// Check if notifications are disabled in desktop settings
if (!this._settings.get_boolean('show-banners')) {
return;
}
parameters = parameters.full_unpack();
// GNotification
if (name === 'AddNotification') {
this.AddNotification(...parameters);
// libnotify
} else if (name === 'Notify') {
// Try to brute-force an application name using DBus
if (!this.applications.hasOwnProperty(parameters[0])) {
let sender = invocation.get_sender();
parameters[0] = await this._getAppName(sender, parameters[0]);
}
this.Notify(...parameters);
}
} catch (e) {
debug(e);
}
}
/**
* Export interfaces for proxying notifications and become a monitor
*/
_monitorConnection() {
return new Promise((resolve, reject) => {
// libnotify Interface
this._fdoNotifications = new GjsPrivate.DBusImplementation({
g_interface_info: FDO_IFACE
});
this._fdoMethodCallId = this._fdoNotifications.connect(
'handle-method-call',
this._onHandleMethodCall.bind(this)
);
this._fdoNotifications.export(
this._monitor,
'/org/freedesktop/Notifications'
);
// GNotification Interface
this._gtkNotifications = new GjsPrivate.DBusImplementation({
g_interface_info: GTK_IFACE
});
this._gtkMethodCallId = this._gtkNotifications.connect(
'handle-method-call',
this._onHandleMethodCall.bind(this)
);
this._gtkNotifications.export(
this._monitor,
'/org/gtk/Notifications'
);
// Become a monitor for Fdo & Gtk notifications
this._monitor.call(
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus.Monitoring',
'BecomeMonitor',
new GLib.Variant('(asu)', [[FDO_MATCH, GTK_MATCH], 0]),
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(connection, res) => {
try {
resolve(connection.call_finish(res));
} catch (e) {
reject(e);
}
}
);
});
}
async _init_async() {
try {
this._session = await DBus.getConnection();
this._monitor = await DBus.newConnection();
await this._monitorConnection();
} catch (e) {
// FIXME: if something goes wrong the component will appear active
logError(e);
this.destroy();
}
}
_sendNotification(notif) {
// Check if this application is disabled in desktop settings
let appSettings = this.applications[notif.appName];
if (appSettings && !appSettings.get_boolean('enable')) {
return;
}
// Send the notification to each supporting device
let variant = GLib.Variant.full_pack(notif);
for (let device of this.application._devices.values()) {
device.activate_action('sendNotification', variant);
}
}
Notify(appName, replacesId, iconName, summary, body, actions, hints, timeout) {
try {
// Ignore notifications without an appName
if (!appName) {
return;
}
this._sendNotification({
appName: appName,
id: `fdo|null|${replacesId}`,
title: summary,
text: body,
ticker: `${summary}: ${body}`,
isClearable: (replacesId !== 0),
icon: iconName
});
} catch (e) {
debug(e);
}
}
AddNotification(application, id, notification) {
try {
// Ignore our own GNotifications
if (application === 'org.gnome.Shell.Extensions.GSConnect') {
return;
}
let appInfo = Gio.DesktopAppInfo.new(`${application}.desktop`);
// Try to get an icon for the notification
if (!notification.hasOwnProperty('icon')) {
notification.icon = appInfo.get_icon() || undefined;
}
this._sendNotification({
appName: appInfo.get_name(),
id: `gtk|${application}|${id}`,
title: notification.title,
text: notification.body,
ticker: `${notification.title}: ${notification.body}`,
isClearable: true,
icon: notification.icon
});
} catch (e) {
debug(e);
}
}
destroy() {
try {
if (this._fdoNotifications) {
this._fdoNotifications.disconnect(this._fdoMethodCallId);
this._fdoNotifications.flush();
this._fdoNotifications.unexport();
}
if (this._gtkNotifications) {
this._gtkNotifications.disconnect(this._gtkMethodCallId);
this._gtkNotifications.flush();
this._gtkNotifications.unexport();
}
if (this._settings) {
this._settings.disconnect(this._settingsId);
this._settings.run_dispose();
}
// TODO: Gio.IOErrorEnum: The connection is closed
//this._monitor.close_sync(null);
} catch (e) {
debug(e);
}
}
};
/**
* The service class for this component
*/
var Component = Listener;

View File

@@ -0,0 +1,253 @@
'use strict';
const Tweener = imports.tweener.tweener;
const Gio = imports.gi.Gio;
const GIRepository = imports.gi.GIRepository;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
// Add gnome-shell's typelib dir to the search path
let typelibDir = GLib.build_filenamev([gsconnect.libdir, 'gnome-shell']);
GIRepository.Repository.prepend_search_path(typelibDir);
GIRepository.Repository.prepend_library_path(typelibDir);
const Gvc = imports.gi.Gvc;
/**
* Extend Gvc.MixerStream with a property for returning a user-visible name
*/
Object.defineProperty(Gvc.MixerStream.prototype, 'display_name', {
get: function() {
try {
if (!this.get_ports().length) return this.description;
return `${this.get_port().human_port} (${this.description})`;
} catch (e) {
return this.description;
}
}
});
/**
* A convenience wrapper for Gvc.MixerStream
*/
class Stream {
constructor(mixer, stream) {
this._mixer = mixer;
this._stream = stream;
this._max = mixer.get_vol_max_norm();
}
get muted() {
return this._stream.is_muted;
}
set muted(bool) {
this._stream.change_is_muted(bool);
}
// Volume is a double in the range 0-1
get volume() {
return Math.floor(100 * this._stream.volume / this._max) / 100;
}
set volume(num) {
this._stream.volume = Math.floor(num * this._max);
this._stream.push_volume();
}
/**
* Gradually raise or lower the stream volume to @value
*
* @param {number} value - A number in the range 0-1
* @param {number} [duration] - Duration to fade in seconds
*/
fade(value, duration = 1) {
Tweener.removeTweens(this);
this._mixer.fading = true;
if (this._stream.volume > value) {
Tweener.addTween(this, {
volume: value,
time: duration,
transition: 'easeOutCubic',
onComplete: () => this._mixer.fading = false
});
} else if (this._stream.volume < value) {
Tweener.addTween(this, {
volume: value,
time: duration,
transition: 'easeInCubic',
onComplete: () => this._mixer.fading = false
});
}
}
}
/**
* A subclass of Gvc.MixerControl with convenience functions for controlling the
* default input/output volumes.
*
* The Mixer class uses GNOME Shell's Gvc library to control the system volume
* and offers a few convenience functions.
*/
var Mixer = GObject.registerClass({
GTypeName: 'GSConnectAudioMixer'
}, class Mixer extends Gvc.MixerControl {
_init(params) {
super._init({name: 'GSConnect'});
this.open();
this._previousVolume = undefined;
this._volumeMuted = false;
this._microphoneMuted = false;
}
get fading() {
if (this._fading === undefined) {
this._fading = false;
}
return this._fading;
}
set fading(bool) {
if (this.fading !== bool) {
this._fading = bool;
if (!this.fading) {
this.emit('stream-changed', this._output._stream.id);
}
}
}
get input() {
if (this._input === undefined) {
this.vfunc_default_source_changed();
}
return this._input;
}
get output() {
if (this._output === undefined) {
this.vfunc_default_sink_changed();
}
return this._output;
}
vfunc_default_sink_changed(id) {
try {
let sink = this.get_default_sink();
this._output = (sink) ? new Stream(this, sink) : null;
} catch (e) {
logError(e);
}
}
vfunc_default_source_changed(id) {
try {
let source = this.get_default_source();
this._input = (source) ? new Stream(this, source) : null;
} catch (e) {
logError(e);
}
}
vfunc_state_changed(new_state) {
try {
if (new_state === Gvc.MixerControlState.READY) {
this.vfunc_default_sink_changed(null);
this.vfunc_default_source_changed(null);
}
} catch (e) {
logError(e);
}
}
/**
* Store the current output volume then lower it to %15
*/
lowerVolume(duration = 1) {
try {
if (this.output.volume > 0.15) {
this._previousVolume = Number(this.output.volume);
this.output.fade(0.15, duration);
}
} catch (e) {
logError(e);
}
}
/**
* Mute the output volume (speakers)
*/
muteVolume() {
try {
if (!this.output.muted) {
this.output.muted = true;
this._volumeMuted = true;
}
} catch (e) {
logError(e);
}
}
/**
* Mute the input volume (microphone)
*/
muteMicrophone() {
try {
if (!this.input.muted) {
this.input.muted = true;
this._microphoneMuted = true;
}
} catch (e) {
logError(e);
}
}
/**
* Restore all mixer levels to their previous state
*/
restore() {
try {
// If we muted the microphone, unmute it before restoring the volume
if (this._microphoneMuted) {
this.input.muted = false;
this._microphoneMuted = false;
}
// If we muted the volume, unmute it before restoring the volume
if (this._volumeMuted) {
this.output.muted = false;
this._volumeMuted = false;
}
// If a previous volume is defined, raise it back up to that level
if (this._previousVolume !== undefined) {
this.output.fade(this._previousVolume);
this._previousVolume = undefined;
}
} catch (e) {
logError(e);
}
}
destroy() {
}
});
/**
* The service class for this component
*/
var Component = Mixer;

View File

@@ -0,0 +1,120 @@
'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Session = class {
constructor() {
this._connection = Gio.DBus.system;
this._session = null;
this._initAsync();
}
async _initAsync() {
try {
let userName = GLib.get_user_name();
let sessions = await this._listSessions();
let sessionPath = '/org/freedesktop/login1/session/auto';
// eslint-disable-next-line no-unused-vars
for (let [num, uid, name, seat, objectPath] of sessions) {
if (name === userName) {
sessionPath = objectPath;
break;
}
}
this._session = await this._getSession(sessionPath);
} catch (e) {
this._session = null;
logError(e);
}
}
get idle() {
if (this._session === null) {
return false;
}
return this._session.get_cached_property('IdleHint').unpack();
}
get locked() {
if (this._session === null) {
return false;
}
return this._session.get_cached_property('LockedHint').unpack();
}
get active() {
// Active if not idle and not locked
return !(this.idle || this.locked);
}
_listSessions() {
return new Promise((resolve, reject) => {
this._connection.call(
'org.freedesktop.login1',
'/org/freedesktop/login1',
'org.freedesktop.login1.Manager',
'ListSessions',
null,
null,
Gio.DBusCallFlags.NONE,
-1,
null,
(connection, res) => {
try {
res = connection.call_finish(res);
resolve(res.deepUnpack()[0]);
} catch (e) {
reject(e);
}
}
);
});
}
async _getSession(objectPath) {
let session = new Gio.DBusProxy({
g_connection: this._connection,
g_name: 'org.freedesktop.login1',
g_object_path: objectPath,
g_interface_name: 'org.freedesktop.login1.Session'
});
// Initialize the proxy
await new Promise((resolve, reject) => {
session.init_async(
GLib.PRIORITY_DEFAULT,
null,
(proxy, res) => {
try {
resolve(proxy.init_finish(res));
} catch (e) {
Gio.DBusError.strip_remote_error(e);
reject(e);
}
}
);
});
return session;
}
destroy() {
if (this._session !== null) {
this._session.run_dispose();
}
}
};
/**
* The service class for this component
*/
var Component = Session;

View File

@@ -0,0 +1,201 @@
'use strict';
const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
/**
* Used to ensure 'audible-bell' is enabled for fallback
*/
const WM_SETTINGS = new Gio.Settings({
schema_id: 'org.gnome.desktop.wm.preferences',
path: '/org/gnome/desktop/wm/preferences/'
});
var Sound = class Sound {
constructor() {
}
get backend() {
if (this._backend === undefined) {
// Prefer GSound
try {
this._gsound = new imports.gi.GSound.Context();
this._gsound.init(null);
this._backend = 'gsound';
// Try falling back to libcanberra, otherwise just re-run the test
// in case one or the other is installed later
} catch (e) {
if (GLib.find_program_in_path('canberra-gtk-play') !== null) {
this._canberra = new Gio.SubprocessLauncher({
flags: Gio.SubprocessFlags.NONE
});
this._backend = 'libcanberra';
} else {
return null;
}
}
}
return this._backend;
}
get playing() {
if (this._playing === undefined) {
this._playing = new Set();
}
return this._playing;
}
_canberraPlaySound(name, cancellable) {
return new Promise((resolve, reject) => {
let proc = this._canberra.spawnv(['canberra-gtk-play', '-i', name]);
proc.wait_check_async(cancellable, (proc, res) => {
try {
resolve(proc.wait_check_finish(res));
} catch (e) {
reject(e);
}
});
});
}
async _canberraLoopSound(name, cancellable) {
while (!cancellable.is_cancelled()) {
await this._canberraPlaySound(name, cancellable);
}
}
_gsoundPlaySound(name, cancellable) {
return new Promise((resolve, reject) => {
this._gsound.play_full(
{'event.id': name},
cancellable,
(source, res) => {
try {
resolve(source.play_full_finish(res));
} catch (e) {
reject(e);
}
}
);
});
}
async _gsoundLoopSound(name, cancellable) {
while (!cancellable.is_cancelled()) {
await this._gsoundPlaySound(name, cancellable);
}
}
_gdkPlaySound(name, cancellable) {
if (this._display === undefined) {
this._display = Gdk.Display.get_default();
}
let count = 0;
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
try {
if (count++ < 4 && !cancellable.is_cancelled()) {
this._display.beep();
return GLib.SOURCE_CONTINUE;
}
return GLib.SOURCE_REMOVE;
} catch (e) {
logError(e);
return GLib.SOURCE_REMOVE;
}
});
return !cancellable.is_cancelled();
}
_gdkLoopSound(name, cancellable) {
this._gdkPlaySound(name, cancellable);
GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
1500,
this._gdkPlaySound.bind(this, name, cancellable)
);
}
async playSound(name, cancellable) {
try {
if (!(cancellable instanceof Gio.Cancellable)) {
cancellable = new Gio.Cancellable();
}
this.playing.add(cancellable);
switch (this.backend) {
case 'gsound':
await this._gsoundPlaySound(name, cancellable);
break;
case 'canberra':
await this._canberraPlaySound(name, cancellable);
break;
default:
await this._gdkPlaySound(name, cancellable);
}
} catch (e) {
if (!e.code || e.code !== Gio.IOErrorEnum.CANCELLED) {
logError(e);
}
} finally {
this.playing.delete(cancellable);
}
}
async loopSound(name, cancellable) {
try {
if (!(cancellable instanceof Gio.Cancellable)) {
cancellable = new Gio.Cancellable();
}
this.playing.add(cancellable);
switch (this.backend) {
case 'gsound':
await this._gsoundLoopSound(name, cancellable);
break;
case 'canberra':
await this._canberraLoopSound(name, cancellable);
break;
default:
await this._gdkLoopSound(name, cancellable);
}
} catch (e) {
if (!e.code || e.code !== Gio.IOErrorEnum.CANCELLED) {
logError(e);
}
} finally {
this.playing.delete(cancellable);
}
}
destroy() {
for (let cancellable of this.playing) {
cancellable.cancel();
}
}
};
/**
* The service class for this component
*/
var Component = Sound;

View File

@@ -0,0 +1,62 @@
'use strict';
const GObject = imports.gi.GObject;
const UPower = imports.gi.UPowerGlib;
var Battery = GObject.registerClass({
GTypeName: 'GSConnectSystemBattery',
Signals: {
'changed': {flags: GObject.SignalFlags.RUN_FIRST}
}
}, class Battery extends UPower.Device {
_init() {
super._init();
// This may throw an exception?
this.set_object_path_sync(
'/org/freedesktop/UPower/devices/DisplayDevice',
null
);
}
vfunc_notify(pspec) {
try {
switch (pspec.get_name()) {
case 'percentage':
case 'state':
case 'warning-level':
this.emit('changed');
}
} catch (e) {
}
}
get charging() {
return (this.state !== UPower.DeviceState.DISCHARGING);
}
get level() {
return this.percentage;
}
// TODO: reset on charging
get threshold() {
if (!this.charging && this.warning_level >= UPower.DeviceLevel.LOW) {
return 1;
} else {
return 0;
}
}
destroy() {
}
});
/**
* The service class for this component
*/
var Component = Battery;