252 lines
7.0 KiB
JavaScript
252 lines
7.0 KiB
JavaScript
|
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||
|
//
|
||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
|
||
|
import Gio from 'gi://Gio';
|
||
|
import GLib from 'gi://GLib';
|
||
|
import GObject from 'gi://GObject';
|
||
|
|
||
|
import Config from '../config.js';
|
||
|
import plugins from './plugins/index.js';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Base class for device plugins.
|
||
|
*/
|
||
|
const Plugin = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectPlugin',
|
||
|
Properties: {
|
||
|
'device': GObject.ParamSpec.object(
|
||
|
'device',
|
||
|
'Device',
|
||
|
'The device that owns this plugin',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
GObject.Object
|
||
|
),
|
||
|
'name': GObject.ParamSpec.string(
|
||
|
'name',
|
||
|
'Name',
|
||
|
'The device name',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
null
|
||
|
),
|
||
|
},
|
||
|
}, class Plugin extends GObject.Object {
|
||
|
|
||
|
_init(device, name, meta = null) {
|
||
|
super._init();
|
||
|
|
||
|
this._device = device;
|
||
|
this._name = name;
|
||
|
this._meta = meta;
|
||
|
|
||
|
if (this._meta === null)
|
||
|
this._meta = plugins[name].Metadata;
|
||
|
|
||
|
// GSettings
|
||
|
const schema = Config.GSCHEMA.lookup(this._meta.id, false);
|
||
|
|
||
|
if (schema !== null) {
|
||
|
this.settings = new Gio.Settings({
|
||
|
settings_schema: schema,
|
||
|
path: `${device.settings.path}plugin/${name}/`,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// GActions
|
||
|
this._gactions = [];
|
||
|
|
||
|
if (this._meta.actions) {
|
||
|
const menu = this.device.settings.get_strv('menu-actions');
|
||
|
|
||
|
for (const name in this._meta.actions) {
|
||
|
const info = this._meta.actions[name];
|
||
|
this._registerAction(name, menu.indexOf(name), info);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get cancellable() {
|
||
|
if (this._cancellable === undefined)
|
||
|
this._cancellable = new Gio.Cancellable();
|
||
|
|
||
|
return this._cancellable;
|
||
|
}
|
||
|
|
||
|
get device() {
|
||
|
return this._device;
|
||
|
}
|
||
|
|
||
|
get name() {
|
||
|
return this._name;
|
||
|
}
|
||
|
|
||
|
_activateAction(action, parameter) {
|
||
|
try {
|
||
|
let args = null;
|
||
|
|
||
|
if (parameter instanceof GLib.Variant)
|
||
|
args = parameter.full_unpack();
|
||
|
|
||
|
if (Array.isArray(args))
|
||
|
this[action.name](...args);
|
||
|
else
|
||
|
this[action.name](args);
|
||
|
} catch (e) {
|
||
|
logError(e, action.name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_registerAction(name, menuIndex, info) {
|
||
|
try {
|
||
|
// Device Action
|
||
|
const action = new Gio.SimpleAction({
|
||
|
name: name,
|
||
|
parameter_type: info.parameter_type,
|
||
|
enabled: false,
|
||
|
});
|
||
|
action.connect('activate', this._activateAction.bind(this));
|
||
|
|
||
|
this.device.add_action(action);
|
||
|
|
||
|
// Menu
|
||
|
if (menuIndex > -1) {
|
||
|
this.device.addMenuAction(
|
||
|
action,
|
||
|
menuIndex,
|
||
|
info.label,
|
||
|
info.icon_name
|
||
|
);
|
||
|
}
|
||
|
|
||
|
this._gactions.push(action);
|
||
|
} catch (e) {
|
||
|
logError(e, `${this.device.name}: ${this.name}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called when the device connects.
|
||
|
*/
|
||
|
connected() {
|
||
|
// Enabled based on device capabilities, which might change
|
||
|
const incoming = this.device.settings.get_strv('incoming-capabilities');
|
||
|
const outgoing = this.device.settings.get_strv('outgoing-capabilities');
|
||
|
|
||
|
for (const action of this._gactions) {
|
||
|
const info = this._meta.actions[action.name];
|
||
|
|
||
|
if (info.incoming.every(type => outgoing.includes(type)) &&
|
||
|
info.outgoing.every(type => incoming.includes(type)))
|
||
|
action.set_enabled(true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called when the device disconnects.
|
||
|
*/
|
||
|
disconnected() {
|
||
|
for (const action of this._gactions)
|
||
|
action.set_enabled(false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called when a packet is received that the plugin is a handler for.
|
||
|
*
|
||
|
* @param {Core.Packet} packet - A KDE Connect packet
|
||
|
*/
|
||
|
handlePacket(packet) {
|
||
|
throw new GObject.NotImplementedError();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cache JSON parseable properties on this object for persistence. The
|
||
|
* filename ~/.cache/gsconnect/<device-id>/<plugin-name>.json will be used
|
||
|
* to store the properties and values.
|
||
|
*
|
||
|
* Calling cacheProperties() opens a JSON cache file and reads any stored
|
||
|
* properties and values onto the current instance. When destroy()
|
||
|
* is called the properties are automatically stored in the same file.
|
||
|
*
|
||
|
* @param {Array} names - A list of this object's property names to cache
|
||
|
*/
|
||
|
async cacheProperties(names) {
|
||
|
try {
|
||
|
this._cacheProperties = names;
|
||
|
|
||
|
// Ensure the device's cache directory exists
|
||
|
const cachedir = GLib.build_filenamev([
|
||
|
Config.CACHEDIR,
|
||
|
this.device.id,
|
||
|
]);
|
||
|
GLib.mkdir_with_parents(cachedir, 448);
|
||
|
|
||
|
this._cacheFile = Gio.File.new_for_path(
|
||
|
GLib.build_filenamev([cachedir, `${this.name}.json`]));
|
||
|
|
||
|
// Read the cache from disk
|
||
|
const [contents] = await this._cacheFile.load_contents_async(
|
||
|
this.cancellable);
|
||
|
const cache = JSON.parse(new TextDecoder().decode(contents));
|
||
|
Object.assign(this, cache);
|
||
|
} catch (e) {
|
||
|
debug(e.message, `${this.device.name}: ${this.name}`);
|
||
|
} finally {
|
||
|
this.cacheLoaded();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* An overridable function that is invoked when the on-disk cache is being
|
||
|
* cleared. Implementations should use this function to clear any in-memory
|
||
|
* cache data.
|
||
|
*/
|
||
|
clearCache() {}
|
||
|
|
||
|
/**
|
||
|
* An overridable function that is invoked when the cache is done loading
|
||
|
*/
|
||
|
cacheLoaded() {}
|
||
|
|
||
|
/**
|
||
|
* Unregister plugin actions, write the cache (if applicable) and destroy
|
||
|
* any dangling signal handlers.
|
||
|
*/
|
||
|
destroy() {
|
||
|
// Cancel any pending plugin operations
|
||
|
if (this._cancellable !== undefined)
|
||
|
this._cancellable.cancel();
|
||
|
|
||
|
for (const action of this._gactions) {
|
||
|
this.device.removeMenuAction(`device.${action.name}`);
|
||
|
this.device.remove_action(action.name);
|
||
|
}
|
||
|
|
||
|
// Write the cache to disk synchronously
|
||
|
if (this._cacheFile !== undefined) {
|
||
|
try {
|
||
|
// Build the cache
|
||
|
const cache = {};
|
||
|
|
||
|
for (const name of this._cacheProperties)
|
||
|
cache[name] = this[name];
|
||
|
|
||
|
this._cacheFile.replace_contents(
|
||
|
JSON.stringify(cache, null, 2),
|
||
|
null,
|
||
|
false,
|
||
|
Gio.FileCreateFlags.REPLACE_DESTINATION,
|
||
|
null
|
||
|
);
|
||
|
} catch (e) {
|
||
|
debug(e.message, `${this.device.name}: ${this.name}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
GObject.signal_handlers_destroy(this);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
export default Plugin;
|
||
|
|