diff --git a/extension/extension-purpose.js b/extension/extension-purpose.js index cad3fcf0..0def980a 100644 --- a/extension/extension-purpose.js +++ b/extension/extension-purpose.js @@ -1,130 +1,178 @@ /* 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"; +const purposeShareMenuId = "purpose_share"; +let hasPurposeMenu = false; // Stores so that when you click the finished // notification it will open the URL let purposeNotificationUrls = {}; 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-failed.png" }); } 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.png" }, (notificationId) => { if (chrome.runtime.lastError) { return; } purposeNotificationUrls[notificationId] = url; }); } resolve(); }); }); } +function checkPurposeEnabled() { + return Promise.all([ + sendPortMessageWithReply("settings", "getSubsystemStatus"), + SettingsUtils.get() + ]).then((result) => { + + const subsystemStatus = result[0]; + const settings = result[1]; + + // HACK Unfortunately I removed the loaded/unloaded signals for plugins + // so we can't reliably know on settings change whether a module is enabled + // sending settings is also legacy done without a reply we could wait for. + // Instead, check whether the module is known and enabled in settings, + // which should be close enough, since purpose plugin also has no additional + // dependencies that could make it fail to load. + return subsystemStatus.hasOwnProperty("purpose") + && settings.purpose && settings.purpose.enabled; + }); +} + +function updatePurposeMenu() { + checkPurposeEnabled().then((enabled) => { + if (enabled && !hasPurposeMenu) { + chrome.contextMenus.create({ + id: purposeShareMenuId, + contexts: ["link", "page", "image", "audio", "video", "selection"], + title: chrome.i18n.getMessage("purpose_share") + }, () => { + const error = chrome.runtime.lastError; + if (error) { + console.warn("Error creating purpose context menu", error.message); + return; + } + hasPurposeMenu = true; + }); + } else if (!enabled && hasPurposeMenu) { + chrome.contextMenus.remove(purposeShareMenuId, () => { + const error = chrome.runtime.lastError; + if (error) { + console.warn("Error removing purpose context menu", error.message); + return; + } + hasPurposeMenu = false; + }); + } + }); +} + 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; if (info.linkText && info.linkText != url) { shareData.title = info.linkText; } } // 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); }); }); -// FIXME only add context menu if purpose is enabled and supported -/*chrome.contextMenus.create({ - id: purposeShareMenuId, - contexts: ["link", "page", "image", "audio", "video", "selection"], - title: chrome.i18n.getMessage("purpose_share") -});*/ +SettingsUtils.onChanged().addListener((delta) => { + if (delta.purpose) { + updatePurposeMenu(); + } +}); addRuntimeCallback("purpose", "share", (message, sender, action) => { return purposeShare(message); }); chrome.notifications.onClicked.addListener((notificationId) => { const url = purposeNotificationUrls[notificationId]; if (url) { chrome.tabs.create({url}); } }); chrome.notifications.onClosed.addListener((notificationId) => { delete purposeNotificationUrls[notificationId]; }); diff --git a/extension/extension.js b/extension/extension.js index 666d2798..87148e3e 100644 --- a/extension/extension.js +++ b/extension/extension.js @@ -1,250 +1,252 @@ /* Copyright (C) 2017 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 . */ function sendEnvironment() { var browser = ""; var ua = navigator.userAgent; // Try to match the most derived first if (ua.match(/vivaldi/i)) { browser = "vivaldi"; } else if(ua.match(/OPR/i)) { browser = "opera"; } else if(ua.match(/chrome/i)) { browser = "chromium"; // Apparently there is no better way to distinuish chromium from chrome for (i in window.navigator.plugins) { if (window.navigator.plugins[i].name === "Chrome PDF Viewer") { browser = "chrome"; break; } } } else if(ua.match(/firefox/i)) { browser = "firefox"; } sendPortMessage("settings", "setEnvironment", {browserName: browser}); } function sendSettings() { SettingsUtils.get().then((items) => { sendPortMessage("settings", "changed", items); }); } // activates giveb tab and raises its window, used by tabs runner and mpris Raise command function raiseTab(tabId) { // first activate the tab, this means it's current in its window chrome.tabs.update(tabId, {active: true}, function (tab) { if (chrome.runtime.lastError || !tab) { // this "lastError" stuff feels so archaic // failed to update return; } // then raise the tab's window too chrome.windows.update(tab.windowId, {focused: true}); }); } // Debug // ------------------------------------------------------------------------ // addCallback("debug", "debug", function(payload) { console.log("From host:", payload.message); } ) addCallback("debug", "warning", function(payload) { console.warn("From host:", payload.message); } ) // System // ------------------------------------------------------------------------ // // When connecting to native host fails (e.g. not installed), we immediately get a disconnect // event immediately afterwards. Also avoid infinite restart loop then. var receivedMessageOnce = false; var portStatus = ""; var portLastErrorMessage = undefined; function updateBrowserAction() { if (portStatus === "UNSUPPORTED_OS" || portStatus === "STARTUP_FAILED") { chrome.browserAction.setIcon({ path: { "16": "icons/plasma-disabled-16.png", "32": "icons/plasma-disabled-32.png", "48": "icons/plasma-disabled-48.png", "128": "icons/plasma-disabled-128.png" } }); } if (portLastErrorMessage && receivedMessageOnce) { chrome.browserAction.setBadgeText({ text: "!" }); chrome.browserAction.setBadgeBackgroundColor({ color: "#da4453" }); // breeze "negative" color } else { chrome.browserAction.setBadgeText({ text: "" }); } } updateBrowserAction(); // Check for supported platform to avoid loading it on e.g. Windows and then failing // when the extension got synced to another device and then failing chrome.runtime.getPlatformInfo(function (info) { if (!SUPPORTED_PLATFORMS.includes(info.os)) { console.log("This extension is not supported on", info.os); portStatus = "UNSUPPORTED_OS"; updateBrowserAction(); return; } connectHost(); }); function connectHost() { port = chrome.runtime.connectNative("org.kde.plasma.browser_integration"); port.onMessage.addListener(function (message) { var subsystem = message.subsystem; var action = message.action; let isReply = message.hasOwnProperty("replyToSerial"); let replyToSerial = message.replyToSerial; if (!isReply && (!subsystem || !action)) { return; } if (portStatus) { portStatus = ""; updateBrowserAction(); } receivedMessageOnce = true; if (isReply) { let replyResolver = pendingMessageReplyResolvers[replyToSerial]; if (replyResolver) { replyResolver(message.payload); delete pendingMessageReplyResolvers[replyToSerial]; } else { console.warn("There is no reply resolver for message with serial", replyToSerial); } return; } if (callbacks[subsystem] && callbacks[subsystem][action]) { callbacks[subsystem][action](message.payload, action); } else { console.warn("Don't know what to do with host message", subsystem, action); } }); port.onDisconnect.addListener(function(port) { var error = chrome.runtime.lastError; // Firefox passes in the port which may then have an error set if (port && port.error) { error = port.error; } console.warn("Host disconnected", error && error.message); // Remove all kde connect menu entries since they won't work without a host try { for (let device in kdeConnectDevices) { if (!kdeConnectDevices.hasOwnProperty(device)) { continue; } chrome.contextMenus.remove(kdeConnectMenuIdPrefix + device); } } catch (e) { console.warn("Failed to cleanup after port disconnect", e); } kdeConnectDevices = {}; portLastErrorMessage = error && error.message || "UNKNOWN"; if (receivedMessageOnce) { portStatus = "DISCONNECTED"; console.log("Auto-restarting it"); connectHost(); } else { portStatus = "STARTUP_FAILED"; console.warn("Not auto-restarting host as we haven't received any message from it before. Check that it's working/installed correctly"); } updateBrowserAction(); }); sendEnvironment(); sendSettings(); sendDownloads(); + + updatePurposeMenu(); } SettingsUtils.onChanged().addListener(() => { sendSettings(); }); addRuntimeCallback("settings", "openKRunnerSettings", function () { sendPortMessage("settings", "openKRunnerSettings"); }); addRuntimeCallback("settings", "getSubsystemStatus", (message, sender, action) => { return sendPortMessageWithReply("settings", "getSubsystemStatus"); }); addRuntimeCallback("settings", "getVersion", () => { return sendPortMessageWithReply("settings", "getVersion"); }); addRuntimeCallback("browserAction", "getStatus", (message) => { let info = { portStatus, portLastErrorMessage }; return Promise.resolve(info); }); addRuntimeCallback("browserAction", "ready", () => { // HACK there's no way to tell whether the browser action popup got closed // None of onunload, onbeforeunload, onvisibilitychanged are fired. // Instead, we create a port once the browser action is ready and then // listen for the port being disconnected. let browserActionPort = chrome.runtime.connect({ name: "browserActionPort" }); browserActionPort.onDisconnect.addListener((port) => { if (port.name !== "browserActionPort") { return; } // disabling the browser action immediately when opening it // causes opening to fail on Firefox, so clear the error only when it's being closed. // Only clear error when it was a transient error, not a startup failure if (receivedMessageOnce) { portLastErrorMessage = ""; updateBrowserAction(); } }); });