diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -30,6 +30,7 @@
Notifications
Runner
Activities
+ Purpose
FileMetaData
)
diff --git a/extension/_locales/en/messages.json b/extension/_locales/en/messages.json
--- a/extension/_locales/en/messages.json
+++ b/extension/_locales/en/messages.json
@@ -80,6 +80,15 @@
"message": "Make sure the “Browser Tabs” module is enabled in Plasma Search settings."
},
+ "options_plugin_purpose_title": {
+ "description": "Title for Purpose / Web Share plugin",
+ "message": "Content Sharing"
+ },
+ "options_plugin_purpose_description": {
+ "description": "Description for Purpose / Web Share plugin",
+ "message": "Adds a \"Share...\" context menu entry and allows websites to open a dialog for sharing contents using the Web Share API."
+ },
+
"options_plugin_breezeScrollBars_title": {
"description": "Title for Breeze style scroll bars plugin",
"message": "Use Breeze-style scroll bars"
@@ -125,6 +134,27 @@
"message": "Open on '$1'"
},
+ "purpose_share": {
+ "description": "Context menu, share link or page via Purpose framework",
+ "message": "Share..."
+ },
+ "purpose_share_finished_title": {
+ "description": "Title of share finished notification",
+ "message": "Content Shared"
+ },
+ "purpose_share_finished_text": {
+ "description": "Text of the share finished notification",
+ "message": "The shared content link ($1) has been copied to the clipboard."
+ },
+ "purpose_share_failed_title": {
+ "description": "Title of share failed notification",
+ "message": "Sharing Failed"
+ },
+ "purpose_share_failed_text": {
+ "description": "Text of share failed notification",
+ "message": "Could not share this content: $1"
+ },
+
"general_error_unknown": {
"description": "An unknown error occurred, usually used when an error message by the system is not provided",
"message": "Unknown Error"
diff --git a/extension/constants.js b/extension/constants.js
--- a/extension/constants.js
+++ b/extension/constants.js
@@ -33,6 +33,9 @@
tabsrunner: {
enabled: true
},
+ purpose: {
+ enabled: true
+ },
breezeScrollBars: {
// this breaks pages in interesting ways, disable by default
enabled: false
diff --git a/extension/content-script.js b/extension/content-script.js
--- a/extension/content-script.js
+++ b/extension/content-script.js
@@ -18,6 +18,14 @@
var callbacks = {};
+// from https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
+function generateGuid() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+ return v.toString(16);
+ });
+}
+
function addCallback(subsystem, action, callback)
{
if (!callbacks[subsystem]) {
@@ -62,6 +70,14 @@
loadMediaSessionsShim();
}
}
+
+ if (items.purpose.enabled) {
+ sendMessage("settings", "getSubsystemStatus").then((status) => {
+ if (status && status.purpose) {
+ loadPurpose();
+ }
+ });
+ }
});
// BREEZE SCROLL BARS
@@ -129,12 +145,7 @@
// ------------------------------------------------------------------------
//
-// we give our transfer div a "random id" for privacy
-// from https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
-var mediaSessionsTransferDivId ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
- var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
- return v.toString(16);
-});
+var mediaSessionsTransferDivId = generateGuid();
// also give the function a "random" name as we have to have it in global scope to be able
// to invoke callbacks from outside, UUID might start with a number, so prepend something
@@ -834,3 +845,124 @@
}
}
}
+
+// PURPOSE / WEB SHARE API
+// ------------------------------------------------------------------------
+//
+var purposeTransferDivId = generateGuid();
+var purposeTransferClassName = "p" + purposeTransferDivId.replace(/-/g, "");
+
+var purposeLoaded = false;
+function loadPurpose() {
+ if (purposeLoaded) {
+ return;
+ }
+
+ purposeLoaded = true;
+
+ // navigator.share must only be defined in secure (https) context
+ if (!window.isSecureContext) {
+ return;
+ }
+
+ window.addEventListener("pbiPurposeMessage", (e) => {
+ const data = e.detail || {};
+
+ const action = data.action;
+ const payload = data.payload;
+
+ if (action !== "share") {
+ return;
+ }
+
+ sendMessage("purpose", "share", payload).then((response) => {
+ executeScript(`
+ function() {
+ ${purposeTransferClassName}.pendingResolve();
+ }
+ `);
+ }, (err) => {
+ // Deliberately not giving any more details about why it got rejected
+ executeScript(`
+ function() {
+ ${purposeTransferClassName}.pendingReject(new DOMException("Share request aborted", "AbortError"));
+ }
+ `);
+ }).finally(() => {
+ executeScript(`
+ function() {
+ ${purposeTransferClassName}.reset();
+ }
+ `);
+ });;
+ });
+
+ executeScript(`
+ function() {
+ ${purposeTransferClassName} = function() {};
+ let transfer = ${purposeTransferClassName};
+ transfer.reset = () => {
+ transfer.pendingResolve = null;
+ transfer.pendingReject = null;
+ };
+ transfer.reset();
+
+ if (!navigator.canShare) {
+ navigator.canShare = (data) => {
+ if (!data) {
+ return false;
+ }
+
+ if (data.title === undefined && data.text === undefined && data.url === undefined) {
+ return false;
+ }
+
+ if (data.url) {
+ // check if URL is valid
+ try {
+ new URL(data.url, document.location.href);
+ } catch (e) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ if (!navigator.share) {
+ navigator.share = (data) => {
+ return new Promise((resolve, reject) => {
+ if (!navigator.canShare(data)) {
+ return reject(new TypeError());
+ }
+
+ if (data.url) {
+ // validity already checked in canShare, hence no catch
+ data.url = new URL(data.url, document.location.href).toString();
+ }
+
+ if (!window.event || !window.event.isTrusted) {
+ return reject(new DOMException("navigator.share can only be called in response to user interaction", "NotAllowedError"));
+ }
+
+ if (transfer.pendingResolve || transfer.pendingReject) {
+ return reject(new DOMException("A share is already in progress", "AbortError"));
+ }
+
+ transfer.pendingResolve = resolve;
+ transfer.pendingReject = reject;
+
+ const event = new CustomEvent("pbiPurposeMessage", {
+ detail: {
+ action: "share",
+ payload: data
+ }
+ });
+ window.dispatchEvent(event);
+ });
+ };
+ }
+ }
+ `);
+}
diff --git a/extension/extension-purpose.js b/extension/extension-purpose.js
new file mode 100644
--- /dev/null
+++ b/extension/extension-purpose.js
@@ -0,0 +1,105 @@
+/*
+ Copyright (C) 2019 Kai Uwe Broulik
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+let purposeShareMenuId = "purpose_share";
+
+function purposeShare(data) {
+ return new Promise((resolve, reject) => {
+ sendPortMessageWithReply("purpose", "share", {data}).then((reply) => {
+ if (!reply.success) {
+ if (!["BUSY", "CANCELED", "INVALID_ARGUMENT"].includes(reply.errorCode)
+ && reply.errorCode !== 1 /*ERR_USER_CANCELED*/) {
+ chrome.notifications.create(null, {
+ type: "basic",
+ title: chrome.i18n.getMessage("purpose_share_failed_title"),
+ message: chrome.i18n.getMessage("purpose_share_failed_text",
+ reply.errorMessage || chrome.i18n.getMessage("general_error_unknown")),
+ iconUrl: "icons/document-share-128.png" // add an "error" overlay?
+ });
+ }
+
+ reject();
+ return;
+ }
+
+ let url = reply.response.url;
+ if (url) {
+ chrome.notifications.create(null, {
+ type: "basic",
+ title: chrome.i18n.getMessage("purpose_share_finished_title"),
+ message: chrome.i18n.getMessage("purpose_share_finished_text", url),
+ iconUrl: "icons/document-share-128.png", // add an "ok tick" overlay?
+ });
+ }
+
+ resolve();
+ });
+ });
+}
+
+chrome.contextMenus.onClicked.addListener((info) => {
+ if (info.menuItemId !== purposeShareMenuId) {
+ return;
+ }
+
+ let url = info.linkUrl || info.srcUrl || info.pageUrl;
+ let selection = info.selectionText;
+ if (!url && !selection) {
+ return;
+ }
+
+ let shareData = {};
+ if (selection) {
+ shareData.text = selection;
+ } else if (url) {
+ shareData.url = url;
+ }
+
+ // We probably shared the current page, add its title to shareData
+ new Promise((resolve, reject) => {
+ if (!info.linkUrl && !info.srcUrl && info.pageUrl) {
+ chrome.tabs.query({
+ // more correct would probably be currentWindow + activeTab
+ url: info.pageUrl
+ }, (tabs) => {
+ if (tabs[0]) {
+ return resolve(tabs[0].title);
+ }
+ resolve("");
+ });
+ return;
+ }
+
+ resolve("");
+ }).then((title) => {
+ if (title) {
+ shareData.title = title;
+ }
+
+ purposeShare(shareData);
+ });
+});
+
+chrome.contextMenus.create({
+ id: purposeShareMenuId,
+ contexts: ["link", "page", "image", "audio", "video", "selection"],
+ title: chrome.i18n.getMessage("purpose_share")
+});
+
+addRuntimeCallback("purpose", "share", (message, sender, action) => {
+ return purposeShare(message);
+});
diff --git a/extension/manifest.json b/extension/manifest.json
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -28,6 +28,7 @@
"extension-mpris.js",
"extension-downloads.js",
"extension-tabsrunner.js",
+ "extension-purpose.js",
"extension.js"
],
diff --git a/extension/options.html b/extension/options.html
--- a/extension/options.html
+++ b/extension/options.html
@@ -72,6 +72,12 @@
I18N
+
+
+ I18N
+