522 lines
16 KiB
JavaScript
522 lines
16 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const GdkPixbuf = imports.gi.GdkPixbuf;
|
||
|
const Gio = imports.gi.Gio;
|
||
|
const GLib = imports.gi.GLib;
|
||
|
const GObject = imports.gi.GObject;
|
||
|
const Gtk = imports.gi.Gtk;
|
||
|
|
||
|
const PluginsBase = imports.service.plugins.base;
|
||
|
const URI = imports.utils.uri;
|
||
|
|
||
|
|
||
|
var Metadata = {
|
||
|
label: _('Share'),
|
||
|
id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Share',
|
||
|
incomingCapabilities: ['kdeconnect.share.request'],
|
||
|
outgoingCapabilities: ['kdeconnect.share.request'],
|
||
|
actions: {
|
||
|
share: {
|
||
|
label: _('Share'),
|
||
|
icon_name: 'send-to-symbolic',
|
||
|
|
||
|
parameter_type: null,
|
||
|
incoming: [],
|
||
|
outgoing: ['kdeconnect.share.request']
|
||
|
},
|
||
|
shareFile: {
|
||
|
label: _('Share File'),
|
||
|
icon_name: 'document-send-symbolic',
|
||
|
|
||
|
parameter_type: new GLib.VariantType('(sb)'),
|
||
|
incoming: [],
|
||
|
outgoing: ['kdeconnect.share.request']
|
||
|
},
|
||
|
shareText: {
|
||
|
label: _('Share Text'),
|
||
|
icon_name: 'send-to-symbolic',
|
||
|
|
||
|
parameter_type: new GLib.VariantType('s'),
|
||
|
incoming: [],
|
||
|
outgoing: ['kdeconnect.share.request']
|
||
|
},
|
||
|
shareUri: {
|
||
|
label: _('Share Link'),
|
||
|
icon_name: 'send-to-symbolic',
|
||
|
|
||
|
parameter_type: new GLib.VariantType('s'),
|
||
|
incoming: [],
|
||
|
outgoing: ['kdeconnect.share.request']
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Share Plugin
|
||
|
* https://github.com/KDE/kdeconnect-kde/tree/master/plugins/share
|
||
|
*
|
||
|
* TODO: receiving 'text' TODO: Window with textview & 'Copy to Clipboard..
|
||
|
* https://github.com/KDE/kdeconnect-kde/commit/28f11bd5c9a717fb9fbb3f02ddd6cea62021d055
|
||
|
*/
|
||
|
var Plugin = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectSharePlugin',
|
||
|
}, class Plugin extends PluginsBase.Plugin {
|
||
|
|
||
|
_init(device) {
|
||
|
super._init(device, 'share');
|
||
|
}
|
||
|
|
||
|
_ensureReceiveDirectory() {
|
||
|
let receiveDir = this.settings.get_string('receive-directory');
|
||
|
|
||
|
// Ensure a directory is set
|
||
|
if (!receiveDir) {
|
||
|
receiveDir = GLib.get_user_special_dir(
|
||
|
GLib.UserDirectory.DIRECTORY_DOWNLOAD
|
||
|
);
|
||
|
|
||
|
// Fallback to ~/Downloads
|
||
|
let homeDir = GLib.get_home_dir();
|
||
|
|
||
|
if (!receiveDir || receiveDir === homeDir) {
|
||
|
receiveDir = GLib.build_filenamev([homeDir, 'Downloads']);
|
||
|
}
|
||
|
|
||
|
this.settings.set_string('receive-directory', receiveDir);
|
||
|
}
|
||
|
|
||
|
// Ensure the directory exists
|
||
|
if (!GLib.file_test(receiveDir, GLib.FileTest.IS_DIR)) {
|
||
|
GLib.mkdir_with_parents(receiveDir, 448);
|
||
|
}
|
||
|
|
||
|
return receiveDir;
|
||
|
}
|
||
|
|
||
|
_getFile(filename) {
|
||
|
let dirpath = this._ensureReceiveDirectory();
|
||
|
let basepath = GLib.build_filenamev([dirpath, filename]);
|
||
|
let filepath = basepath;
|
||
|
let copyNum = 0;
|
||
|
|
||
|
while (GLib.file_test(filepath, GLib.FileTest.EXISTS)) {
|
||
|
copyNum += 1;
|
||
|
filepath = `${basepath} (${copyNum})`;
|
||
|
}
|
||
|
|
||
|
return Gio.File.new_for_path(filepath);
|
||
|
}
|
||
|
|
||
|
async _refuseFile(packet) {
|
||
|
try {
|
||
|
await this.device.rejectTransfer(packet);
|
||
|
|
||
|
this.device.showNotification({
|
||
|
id: `${Date.now()}`,
|
||
|
title: _('Transfer Failed'),
|
||
|
// TRANSLATORS: eg. Google Pixel is not allowed to upload files
|
||
|
body: _('%s is not allowed to upload files').format(
|
||
|
this.device.name
|
||
|
),
|
||
|
icon: new Gio.ThemedIcon({name: 'dialog-error-symbolic'})
|
||
|
});
|
||
|
} catch (e) {
|
||
|
logError(e, this.device.name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async _handleFile(packet) {
|
||
|
let file, stream, success, transfer;
|
||
|
let title, body, iconName;
|
||
|
let buttons = [];
|
||
|
|
||
|
try {
|
||
|
file = this._getFile(packet.body.filename);
|
||
|
|
||
|
stream = await new Promise((resolve, reject) => {
|
||
|
file.replace_async(null, false, 0, 0, null, (file, res) => {
|
||
|
try {
|
||
|
resolve(file.replace_finish(res));
|
||
|
} catch (e) {
|
||
|
reject(e);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
transfer = this.device.createTransfer(Object.assign({
|
||
|
output_stream: stream,
|
||
|
size: packet.payloadSize
|
||
|
}, packet.payloadTransferInfo));
|
||
|
|
||
|
// Notify that we're about to start the transfer
|
||
|
this.device.showNotification({
|
||
|
id: transfer.uuid,
|
||
|
title: _('Transferring File'),
|
||
|
// TRANSLATORS: eg. Receiving 'book.pdf' from Google Pixel
|
||
|
body: _('Receiving “%s” from %s').format(
|
||
|
packet.body.filename,
|
||
|
this.device.name
|
||
|
),
|
||
|
buttons: [{
|
||
|
label: _('Cancel'),
|
||
|
action: 'cancelTransfer',
|
||
|
parameter: new GLib.Variant('s', transfer.uuid)
|
||
|
}],
|
||
|
icon: new Gio.ThemedIcon({name: 'document-save-symbolic'})
|
||
|
});
|
||
|
|
||
|
// Start transfer
|
||
|
success = await transfer.download();
|
||
|
this.device.hideNotification(transfer.uuid);
|
||
|
|
||
|
// We've been asked to open this directly
|
||
|
if (success && packet.body.open) {
|
||
|
let uri = file.get_uri();
|
||
|
Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// We'll show a notification (success or failure)
|
||
|
if (success) {
|
||
|
title = _('Transfer Successful');
|
||
|
// TRANSLATORS: eg. Received 'book.pdf' from Google Pixel
|
||
|
body = _('Received “%s” from %s').format(
|
||
|
packet.body.filename,
|
||
|
this.device.name
|
||
|
);
|
||
|
buttons = [
|
||
|
{
|
||
|
label: _('Open Folder'),
|
||
|
action: 'openPath',
|
||
|
parameter: new GLib.Variant('s', file.get_parent().get_uri())
|
||
|
},
|
||
|
{
|
||
|
label: _('Open File'),
|
||
|
action: 'openPath',
|
||
|
parameter: new GLib.Variant('s', file.get_uri())
|
||
|
}
|
||
|
];
|
||
|
iconName = 'document-save-symbolic';
|
||
|
} else {
|
||
|
title = _('Transfer Failed');
|
||
|
// TRANSLATORS: eg. Failed to receive 'book.pdf' from Google Pixel
|
||
|
body = _('Failed to receive “%s” from %s').format(
|
||
|
packet.body.filename,
|
||
|
this.device.name
|
||
|
);
|
||
|
iconName = 'dialog-warning-symbolic';
|
||
|
|
||
|
// Clean up the downloaded file on failure
|
||
|
file.delete(null);
|
||
|
}
|
||
|
|
||
|
this.device.showNotification({
|
||
|
id: transfer.uuid,
|
||
|
title: title,
|
||
|
body: body,
|
||
|
buttons: buttons,
|
||
|
icon: new Gio.ThemedIcon({name: iconName})
|
||
|
});
|
||
|
} catch (e) {
|
||
|
logError(e, this.device.name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_handleUri(packet) {
|
||
|
let uri = packet.body.url;
|
||
|
Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
|
||
|
}
|
||
|
|
||
|
_handleText(packet) {
|
||
|
let dialog = new Gtk.MessageDialog({
|
||
|
text: _('Text Shared By %s').format(this.device.name),
|
||
|
secondary_text: URI.linkify(packet.body.text),
|
||
|
secondary_use_markup: true,
|
||
|
buttons: Gtk.ButtonsType.CLOSE
|
||
|
});
|
||
|
dialog.message_area.get_children()[1].selectable = true;
|
||
|
dialog.set_keep_above(true);
|
||
|
dialog.connect('response', (dialog) => dialog.destroy());
|
||
|
dialog.show();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Packet dispatch
|
||
|
*/
|
||
|
handlePacket(packet) {
|
||
|
if (packet.body.hasOwnProperty('filename')) {
|
||
|
if (this.settings.get_boolean('receive-files')) {
|
||
|
this._handleFile(packet);
|
||
|
} else {
|
||
|
this._refuseFile(packet);
|
||
|
}
|
||
|
} else if (packet.body.hasOwnProperty('text')) {
|
||
|
this._handleText(packet);
|
||
|
} else if (packet.body.hasOwnProperty('url')) {
|
||
|
this._handleUri(packet);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remote methods
|
||
|
*/
|
||
|
share() {
|
||
|
let dialog = new FileChooserDialog(this.device);
|
||
|
dialog.show();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Share local file path or URI
|
||
|
*
|
||
|
* @param {string} path - Local file path or URI
|
||
|
* @param {boolean} open - Whether the file should be opened after transfer
|
||
|
*/
|
||
|
async shareFile(path, open = false) {
|
||
|
let file, stream, success, transfer;
|
||
|
let title, body, iconName;
|
||
|
|
||
|
try {
|
||
|
if (path.includes('://')) {
|
||
|
file = Gio.File.new_for_uri(path);
|
||
|
} else {
|
||
|
file = Gio.File.new_for_path(path);
|
||
|
}
|
||
|
|
||
|
stream = await new Promise((resolve, reject) => {
|
||
|
file.read_async(GLib.PRIORITY_DEFAULT, null, (file, res) => {
|
||
|
try {
|
||
|
resolve(file.read_finish(res));
|
||
|
} catch (e) {
|
||
|
reject(e);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
transfer = this.device.createTransfer({
|
||
|
input_stream: stream,
|
||
|
size: file.query_info('standard::size', 0, null).get_size()
|
||
|
});
|
||
|
|
||
|
// Notify that we're about to start the transfer
|
||
|
this.device.showNotification({
|
||
|
id: transfer.uuid,
|
||
|
title: _('Transferring File'),
|
||
|
// TRANSLATORS: eg. Sending 'book.pdf' to Google Pixel
|
||
|
body: _('Sending “%s” to %s').format(
|
||
|
file.get_basename(),
|
||
|
this.device.name
|
||
|
),
|
||
|
buttons: [{
|
||
|
label: _('Cancel'),
|
||
|
action: 'cancelTransfer',
|
||
|
parameter: new GLib.Variant('s', transfer.uuid)
|
||
|
}],
|
||
|
icon: new Gio.ThemedIcon({name: 'document-send-symbolic'})
|
||
|
});
|
||
|
|
||
|
success = await transfer.upload({
|
||
|
type: 'kdeconnect.share.request',
|
||
|
body: {
|
||
|
filename: file.get_basename(),
|
||
|
open: open
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (success) {
|
||
|
title = _('Transfer Successful');
|
||
|
// TRANSLATORS: eg. Sent "book.pdf" to Google Pixel
|
||
|
body = _('Sent “%s” to %s').format(
|
||
|
file.get_basename(),
|
||
|
this.device.name
|
||
|
);
|
||
|
iconName = 'document-send-symbolic';
|
||
|
} else {
|
||
|
title = _('Transfer Failed');
|
||
|
// TRANSLATORS: eg. Failed to send "book.pdf" to Google Pixel
|
||
|
body = _('Failed to send “%s” to %s').format(
|
||
|
file.get_basename(),
|
||
|
this.device.name
|
||
|
);
|
||
|
iconName = 'dialog-warning-symbolic';
|
||
|
}
|
||
|
|
||
|
this.device.hideNotification(transfer.uuid);
|
||
|
this.device.showNotification({
|
||
|
id: transfer.uuid,
|
||
|
title: title,
|
||
|
body: body,
|
||
|
icon: new Gio.ThemedIcon({name: iconName})
|
||
|
});
|
||
|
} catch (e) {
|
||
|
debug(e, this.device.name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Share a string of text. Remote behaviour is undefined.
|
||
|
*
|
||
|
* @param {string} text - A string of unicode text
|
||
|
*/
|
||
|
shareText(text) {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.share.request',
|
||
|
body: {text: text}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Share a URI. Generally the remote device opens it with the scheme default
|
||
|
*
|
||
|
* @param {string} uri - Currently http(s) and tel: URIs are supported
|
||
|
*/
|
||
|
shareUri(uri) {
|
||
|
switch (true) {
|
||
|
// Currently only pass http(s)/tel URIs
|
||
|
case uri.startsWith('http://'):
|
||
|
case uri.startsWith('https://'):
|
||
|
case uri.startsWith('tel:'):
|
||
|
break;
|
||
|
|
||
|
// Redirect local file URIs
|
||
|
case uri.startsWith('file://'):
|
||
|
return this.sendFile(uri);
|
||
|
|
||
|
// Assume HTTPS
|
||
|
default:
|
||
|
uri = `https://${uri}`;
|
||
|
}
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.share.request',
|
||
|
body: {url: uri}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
|
||
|
/** A simple FileChooserDialog for sharing files */
|
||
|
var FileChooserDialog = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectShareFileChooserDialog',
|
||
|
}, class FileChooserDialog extends Gtk.FileChooserDialog {
|
||
|
|
||
|
_init(device) {
|
||
|
super._init({
|
||
|
// TRANSLATORS: eg. Send files to Google Pixel
|
||
|
title: _('Send files to %s').format(device.name),
|
||
|
select_multiple: true,
|
||
|
extra_widget: new Gtk.CheckButton({
|
||
|
// TRANSLATORS: Mark the file to be opened once completed
|
||
|
label: _('Open when done'),
|
||
|
visible: true
|
||
|
}),
|
||
|
use_preview_label: false
|
||
|
});
|
||
|
|
||
|
this.device = device;
|
||
|
|
||
|
// Align checkbox with sidebar
|
||
|
let box = this.get_content_area().get_children()[0].get_children()[0];
|
||
|
let paned = box.get_children()[0];
|
||
|
paned.bind_property(
|
||
|
'position',
|
||
|
this.extra_widget,
|
||
|
'margin-left',
|
||
|
GObject.BindingFlags.SYNC_CREATE
|
||
|
);
|
||
|
|
||
|
// Preview Widget
|
||
|
this.preview_widget = new Gtk.Image();
|
||
|
this.preview_widget_active = false;
|
||
|
this.connect('update-preview', this._onUpdatePreview);
|
||
|
|
||
|
// URI entry
|
||
|
this._uriEntry = new Gtk.Entry({
|
||
|
placeholder_text: 'https://',
|
||
|
hexpand: true,
|
||
|
visible: true
|
||
|
});
|
||
|
this._uriEntry.connect('activate', this._sendLink.bind(this));
|
||
|
|
||
|
// URI/File toggle
|
||
|
this._uriButton = new Gtk.ToggleButton({
|
||
|
image: new Gtk.Image({
|
||
|
icon_name: 'web-browser-symbolic',
|
||
|
pixel_size: 16
|
||
|
}),
|
||
|
valign: Gtk.Align.CENTER,
|
||
|
// TRANSLATORS: eg. Send a link to Google Pixel
|
||
|
tooltip_text: _('Send a link to %s').format(device.name),
|
||
|
visible: true
|
||
|
});
|
||
|
this._uriButton.connect('toggled', this._onUriButtonToggled.bind(this));
|
||
|
|
||
|
this.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
|
||
|
let sendButton = this.add_button(_('Send'), Gtk.ResponseType.OK);
|
||
|
sendButton.connect('clicked', this._sendLink.bind(this));
|
||
|
|
||
|
this.get_header_bar().pack_end(this._uriButton);
|
||
|
this.set_default_response(Gtk.ResponseType.OK);
|
||
|
}
|
||
|
|
||
|
_onUpdatePreview(chooser) {
|
||
|
try {
|
||
|
let pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
|
||
|
chooser.get_preview_filename(),
|
||
|
chooser.get_scale_factor() * 128,
|
||
|
-1
|
||
|
);
|
||
|
chooser.preview_widget.pixbuf = pixbuf;
|
||
|
chooser.preview_widget.visible = true;
|
||
|
chooser.preview_widget_active = true;
|
||
|
} catch (e) {
|
||
|
chooser.preview_widget.visible = false;
|
||
|
chooser.preview_widget_active = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_onUriButtonToggled(button) {
|
||
|
let header = this.get_header_bar();
|
||
|
|
||
|
// Show the URL entry
|
||
|
if (button.active) {
|
||
|
this.extra_widget.sensitive = false;
|
||
|
header.set_custom_title(this._uriEntry);
|
||
|
this.set_response_sensitive(Gtk.ResponseType.OK, true);
|
||
|
|
||
|
// Hide the URL entry
|
||
|
} else {
|
||
|
header.set_custom_title(null);
|
||
|
this.set_response_sensitive(
|
||
|
Gtk.ResponseType.OK,
|
||
|
this.get_uris().length > 1
|
||
|
);
|
||
|
this.extra_widget.sensitive = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_sendLink(widget) {
|
||
|
if (this._uriButton.active && this._uriEntry.text.length) {
|
||
|
this.response(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
vfunc_response(response_id) {
|
||
|
if (response_id === Gtk.ResponseType.OK) {
|
||
|
this.get_uris().map(uri => {
|
||
|
let parameter = new GLib.Variant(
|
||
|
'(sb)',
|
||
|
[uri, this.extra_widget.active]
|
||
|
);
|
||
|
this.device.activate_action('shareFile', parameter);
|
||
|
});
|
||
|
} else if (response_id === 1) {
|
||
|
let parameter = new GLib.Variant('s', this._uriEntry.text);
|
||
|
this.device.activate_action('shareUri', parameter);
|
||
|
}
|
||
|
|
||
|
this.destroy();
|
||
|
}
|
||
|
});
|
||
|
|