diff --git a/extension/_locales/en/messages.json b/extension/_locales/en/messages.json index e413ed97..bdcd6848 100644 --- a/extension/_locales/en/messages.json +++ b/extension/_locales/en/messages.json @@ -1,131 +1,158 @@ { "store_description": { "description": "The extension description on the extension store", "message": "Multitask efficiently by controlling browser functions from the desktop, even while Chrome is in the background. Manage audio and video playback, check downloads in the notification area, send files to your phone using KDE Connect and more inside the KDE Plasma Desktop!\\n\\nThe plasma-browser-integration package must be installed for this extension to work. It should be available from your distribution's package manager when running Plasma 5.13 or later.\\n\\nNOTE: This extension is not supported on Debian." }, + + "browseraction_title": { + "description": "Title for toolbar popup", + "message": "Plasma Browser Integration" + }, + "browseraction_not_supported_os_title": { + "message": "Unsupported operating system" + }, + "browseraction_not_supported_os": { + "message": "This extension is only supported on Linux and FreeBSD." + }, + "browseraction_startup_failed_title": { + "description": "Title for failure to start plasma-browser-integration-host binary", + "message": "Failed to connect to the native host." + }, + "browseraction_startup_failed": { + "description": "Description for failure to start plasma-browser-integration-host binary", + "message": "Make sure the 'plasma-browser-integration' package is installed correctly and that you are running Plasma 5.13 or later." + }, + "browseraction_startup_failed_wiki_link": { + "message": "Visit project wiki page for more information" + }, + "browseraction_host_disconnected_title": { + "description": "Title for plasma-browser-integration-host binary unexpectedly closing/crashing", + "message": "The native host disconnected unexpectedly." + }, + "options_title": { "description": "Title for settings page", "message": "Plasma Integration Settings" }, "options_save_failed": { "message": "Saving settings failed" }, "options_save_success": { "message": "Settings successfully saved" }, "options_not_supported_os": { "message": "This extension is not supported on this operating system." }, "options_tab_general": { "description": "The 'General settings' tab in settings", "message": "General" }, "options_tab_about": { "description": "The 'About this plugin' tab in settings", "message": "About" }, "options_plugin_mpris_title": { "description": "Title for Media Controls plugin", "message": "Media Controls" }, "options_plugin_mpris_description": { "description": "Description for Media Controls plugin", "message": "Lets you control video and audio players in websites using the Media Controller plasmoid." }, "options_plugin_mpris_media_sessions_title": { "description": "Title for MediaSessions API Control plugin", "message": "Enhanced Media Controls" }, "options_plugin_mpris_media_sessions_description": { "description": "Description for MediaSessions API Control plugin", "message": "Extract metadata and thumbnails of currently playing content." }, "options_plugin_kdeconnect_title": { "description": "Title for KDE Connect plugin", "message": "Send via KDE Connect" }, "options_plugin_kdeconnect_description": { "description": "Description for KDE Connect plugin", "message": "Adds a context menu entry to links enabling you to send them to your phone and other paired devices using KDE Connect." }, "options_plugin_downloads_title": { "description": "Title for Downloads plugin", "message": "Show downloads in notification area" }, "options_plugin_tabsrunner_title": { "description": "Title for Browser Tabs KRunner plugin", "message": "Find browser tabs in “Run Command” window" }, "options_plugin_tabsrunner_description": { "description": "Description for Browser Tabs KRunner plugin", "message": "Make sure the “Browser Tabs” module is enabled in Plasma Search settings." }, "options_plugin_breezeScrollBars_title": { "description": "Title for Breeze style scroll bars plugin", "message": "Use Breeze-style scroll bars" }, "options_plugin_breezeScrollBars_description": { "description": "Description for Breeze style scroll bars plugin", "message": "This may interfere with the appearance of websites that already apply a custom styling to their scroll bars." }, "options_about_copyright": { "message": "© 2017-2019 Kai Uwe Broulik and David Edmundson" }, "options_about_license": { "message": "License: GNU General Public License Version 3" }, "options_about_translated_by": { "message": "Translated by: $1" }, "options_about_translators": { "description": "Name of translators", "message": "Your names" }, "options_about_created_by_kde": { "message": "This browser extension was created by the KDE Community. You can find more information about this project on the KDE Community Wiki." }, "options_about_bugs": { "message": "If you find an issue, please check the list of open bugs and then file a bug report." }, "options_about_kde": { "description": "KDE description taken from kaboutkdedialog_p.h in kmxlgui", "message": "KDE is a world-wide community of software engineers, artists, writers, translators and creators who are committed to Free Software development. KDE produces the Plasma desktop environment, hundreds of applications, and the many software libraries that support them. KDE is a cooperative enterprise: no single entity controls its direction or products. Instead, we work together to achieve the common goal of building the world's finest Free Software. Everyone is welcome to join and contribute to KDE, including you. Visit $3 for more information about the KDE community and the software we produce." }, "options_about_donate": { "message": "If you like what you saw, please consider donating to KDE, so we can continue to make the best free software possible." }, "kdeconnect_open_via": { "description": "Context menu, open link on device whose name we don't (yet) know", "message": "Open via KDE Connect" }, "kdeconnect_open_device": { "description": "Context menu, open link on device $1, similar to 'Open in New Tab'", "message": "Open on '$1'" }, "general_error_title": { "description": "Title message for most error notifications", "message": "Plasma Browser Integration Error" }, "general_error_unknown": { "description": "An unknown error occurred, usually used when an error message by the system is not provided", "message": "Unknown Error" }, "general_error_port_disconnect": { "description": "When the binary bridge between browser and Plasma quit (usually it crashed or was otherwise killed), placeholder is the reason", "message": "The native host disconnected unexpectedly: $1" }, "general_error_port_startupfail": { "description": "When the binary bridge between browser and Plasma failed to start (e.g. not installed)", "message": "Failed to connect to the native host. Make sure the 'plasma-browser-integration' package is installed." } } diff --git a/extension/action_popup.css b/extension/action_popup.css new file mode 100644 index 00000000..5662b902 --- /dev/null +++ b/extension/action_popup.css @@ -0,0 +1,39 @@ +body { + width: 24em; + /* prevent scroll bars*/ + overflow: hidden; + margin: 0; + /* apply the chrome default styling explicitly so it also works in Firefox */ + font-family: "Noto Sans", sans-serif; + font-size: 75%; +} + +.hidden { + display: none; +} + +header { + background: #1d99f3; + padding: 2px; + color: #fff; + text-align: center; +} + +section > header { + background: #F0F0F0; + color: #757777; + margin: 0 -1em .5em -1em; +} + +.message { + padding: 10px; +} +.message.with-icon::before { + content: ''; + height: 48px; + display: block; + background: center no-repeat; +} +.message.with-icon.error::before { + background-image: url('icons/sad-face-48.png'); +} diff --git a/extension/action_popup.html b/extension/action_popup.html new file mode 100644 index 00000000..26930147 --- /dev/null +++ b/extension/action_popup.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + +
+ I18N +
+ +
+ + + + + + + + + +
+ + + + diff --git a/extension/action_popup.js b/extension/action_popup.js new file mode 100644 index 00000000..a2510d59 --- /dev/null +++ b/extension/action_popup.js @@ -0,0 +1,58 @@ +/* + 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 . + */ + +document.addEventListener("DOMContentLoaded", () => { + + sendMessage("browserAction", "getStatus").then((status) => { + + switch (status.portStatus) { + case "UNSUPPORTED_OS": + document.getElementById("unsupported_os_error").classList.remove("hidden"); + break; + + case "STARTUP_FAILED": + document.getElementById("startup_error").classList.remove("hidden"); + break; + + default: + document.getElementById("main").classList.remove("hidden"); + + let errorText = status.portLastErrorMessage; + if (errorText === "UNKNOWN") { + errorText = chrome.i18n.getMessage("general_error_unknown"); + } + + if (errorText) { + document.getElementById("runtime_error_text").innerText = errorText; + document.getElementById("runtime_error").classList.remove("hidden"); + } + + break; + } + + // HACK so the extension can tell we closed, see "browserAction" "ready" callback in extension.js + chrome.runtime.onConnect.addListener((port) => { + if (port.name !== "browserActionPort") { + return; + } + + // do we need to do something with the port here? + }); + sendMessage("browserAction", "ready"); + }); + +}); diff --git a/extension/constants.js b/extension/constants.js index bed0f986..fee2332d 100644 --- a/extension/constants.js +++ b/extension/constants.js @@ -1,43 +1,44 @@ /* Copyright (C) 2017 Kai Uwe Broulik Copyright (C) 2018 David Edmundson 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 . */ DEFAULT_EXTENSION_SETTINGS = { mpris: { enabled: true }, mprisMediaSessions: { enabled: true }, kdeconnect: { enabled: true }, downloads: { enabled: true }, tabsrunner: { enabled: true }, breezeScrollBars: { // this breaks pages in interesting ways, disable by default enabled: false } }; IS_FIREFOX = (typeof InstallTrigger !== "undefined"); // heh. +// NOTE if you change this, make sure to adjust the error message shown in action_popup.html SUPPORTED_PLATFORMS = ["linux", "openbsd"]; diff --git a/extension/extension.js b/extension/extension.js index b027a524..6e35f88a 100644 --- a/extension/extension.js +++ b/extension/extension.js @@ -1,187 +1,254 @@ /* 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() { storage.get(DEFAULT_EXTENSION_SETTINGS, function (items) { if (chrome.runtime.lastError) { console.warn("Failed to load settings"); return; } 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() { + let enableAction = false; + 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" + } + }); + enableAction = true; + } + + if (portLastErrorMessage) { + chrome.browserAction.setBadgeText({ text: "!" }); + chrome.browserAction.setBadgeBackgroundColor({ color: "#da4453" }); // breeze "negative" color + enableAction = true; + } else { + chrome.browserAction.setBadgeText({ text: "" }); + } + + if (enableAction) { + chrome.browserAction.enable(); + } else { + chrome.browserAction.disable(); + } +} +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() { var error = chrome.runtime.lastError; console.warn("Host disconnected", error); // Remove all kde connect menu entries since they won't work without a host - for (let device of kdeConnectDevices) { - chrome.contextMenus.remove(kdeConnectMenuIdPrefix + device); + try { + for (let device of kdeConnectDevices) { + chrome.contextMenus.remove(kdeConnectMenuIdPrefix + device); + } + } catch (e) { + console.warn("Failed to cleanup after port disconnect", e); } kdeConnectDevices = []; - var reason = chrome.i18n.getMessage("general_error_unknown"); - if (error && error.message) { - reason = error.message; - } - - var message = receivedMessageOnce ? chrome.i18n.getMessage("general_error_port_disconnect", reason) - : chrome.i18n.getMessage("general_error_port_startupfail"); - - chrome.notifications.create(null, { - type: "basic", - title: chrome.i18n.getMessage("general_error_title"), - message: message, - iconUrl: "icons/sad-face-128.png" - }); - if (receivedMessageOnce) { + portLastErrorMessage = error && error.message || "UNKNOWN"; + portStatus = "DISCONNECTED"; + console.log("Auto-restarting it"); connectHost(); } else { + portLastErrorMessage = ""; + 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(); } addRuntimeCallback("settings", "changed", function () { // we could also just reload our extension :) // but this also causes the settings dialog to quit //chrome.runtime.reload(); sendSettings(); }); addRuntimeCallback("settings", "openKRunnerSettings", function () { sendPortMessage("settings", "openKRunnerSettings"); }); addRuntimeCallback("settings", "getSubsystemStatus", (message, sender, action) => { return sendPortMessageWithReply("settings", "getSubsystemStatus"); }); + +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. + portLastErrorMessage = ""; + updateBrowserAction(); + }); +}); diff --git a/extension/manifest.json b/extension/manifest.json index 96777e22..eefab78b 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,67 +1,71 @@ { "manifest_version": 2, "name": "Plasma Integration", "short_name": "Plasma", "description": "Provides better integration with the KDE Plasma 5 desktop.", "version": "1.5", "default_locale": "en", "author": "Kai Uwe Broulik ", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Wz6JigIdPBxPJvYrnjuKM3X4YEaUHgyOW2TM1G1Maxook1HO8vLFGhBR7g3jEQ7Yw9PTa6ZAa8J/I9a/1EQz0Ssx2+m3R9SvJfdKSCrCth8nwMpB8V8I5EhiUoW57kM0v9r/18Buem5cBIo0EnSAVCbmNG95R5jtg62P4+LNGEwHk5L7RSblXUN2hhUwXty4A98EXD6T2Pnpu1m8nRF2B1t5AcspSMV5ECnL9x8kT5bup1gJo0FGTz6C+1huDNRaI3OY1YDWyvCVRFXlAhuFERTW6siMdghK++kYM43H7cvJMFYnVVuffD9WNRsUCI9V0SrqYbPre/2nEl+8VUXVwIDAQAB", "icons": { "16": "icons/plasma-16.png", "32": "icons/plasma-32.png", "48": "icons/plasma-48.png", "128": "icons/plasma-128.png" }, "background": { "scripts": [ "constants.js", "extension-utils.js", "extension-kdeconnect.js", "extension-mpris.js", "extension-downloads.js", "extension-tabsrunner.js", "extension.js" ], "persistent": false }, + "browser_action": { + "default_popup": "action_popup.html" + }, + "content_scripts": [ { "matches": ["*://*/*"], "js": ["constants.js", "content-utils.js", "content-script.js"], "run_at":"document_start", "all_frames": true, "match_about_blank": true } ], "options_ui": { "page": "options.html", "chrome_style": true }, "permissions": [ "nativeMessaging", "notifications", "storage", "downloads", "tabs", "", "contextMenus" ], "applications": { "gecko": { "id": "plasma-browser-integration@kde.org", "strict_min_version": "50.0" } }, "optional_permissions": [ "webRequest" ] }