2024-07-08 22:46:35 +02:00

252 lines
7.0 KiB
JavaScript
Executable File

// 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;