diff --git a/extension/action_popup.html b/extension/action_popup.html index 57d95def..02b98124 100644 --- a/extension/action_popup.html +++ b/extension/action_popup.html @@ -1,61 +1,63 @@
I18N
diff --git a/extension/action_popup.js b/extension/action_popup.js index 1711a40d..20028829 100644 --- a/extension/action_popup.js +++ b/extension/action_popup.js @@ -1,271 +1,281 @@ /* 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 . */ class TabUtils { // Gets the currently viewed tab static getCurrentTab() { return new Promise((resolve, reject) => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { const error = chrome.runtime.lastError; if (error) { return reject(error.message); } const tab = tabs[0]; if (!tab) { return reject("NO_TAB"); } resolve(tab); }); }); } // Gets the URLs of the currently viewed tab including all of its iframes static getCurrentTabFramesUrls() { return new Promise((resolve, reject) => { TabUtils.getCurrentTab().then((tab) => { chrome.tabs.executeScript({ allFrames: true, // so we also catch iframe videos code: `window.location.href`, runAt: "document_start" }, (result) => { const error = chrome.runtime.lastError; if (error) { return reject(error.message); } resolve(result); }); }); }); } }; class MPrisBlocker { getAllowed() { return new Promise((resolve, reject) => { Promise.all([ SettingsUtils.get(), TabUtils.getCurrentTabFramesUrls() ]).then((result) => { const settings = result[0]; const currentUrls = result[1]; const mprisSettings = settings.mpris; if (!mprisSettings.enabled) { return reject("MPRIS_DISABLED"); } if (!currentUrls) { // can this happen? return reject("NO_URLS"); } const origins = currentUrls.map((url) => { try { return new URL(url).origin; } catch (e) { console.warn("Invalid url", url); return ""; } }).filter((origin) => { return !!origin; }); if (origins.length === 0) { return reject("NO_ORIGINS"); } const uniqueOrigins = [...new Set(origins)]; const websiteSettings = mprisSettings.websiteSettings || {}; let response = { origins: {}, mprisSettings }; for (const origin of uniqueOrigins) { let allowed = true; if (typeof MPRIS_WEBSITE_SETTINGS[origin] === "boolean") { allowed = MPRIS_WEBSITE_SETTINGS[origin]; } if (typeof websiteSettings[origin] === "boolean") { allowed = websiteSettings[origin]; } response.origins[origin] = allowed; } resolve(response); }, reject); }); } setAllowed(origin, allowed) { return SettingsUtils.get().then((settings) => { const mprisSettings = settings.mpris; if (!mprisSettings.enabled) { return reject("MPRIS_DISABLED"); } let websiteSettings = mprisSettings.websiteSettings || {}; let implicitAllowed = true; if (typeof MPRIS_WEBSITE_SETTINGS[origin] === "boolean") { implicitAllowed = MPRIS_WEBSITE_SETTINGS[origin]; } if (allowed !== implicitAllowed) { websiteSettings[origin] = allowed; } else { delete websiteSettings[origin]; } mprisSettings.websiteSettings = websiteSettings; return SettingsUtils.set({ mpris: mprisSettings }); }); } }; 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": + case "STARTUP_FAILED": { document.getElementById("startup_error").classList.remove("hidden"); + + const errorText = status.portLastErrorMessage; + // Don't show generic error on startup failure. There's already an explanation. + if (errorText && errorText !== "UNKNOWN") { + const errorTextItem = document.getElementById("startup_error_text"); + errorTextItem.innerText = errorText; + errorTextItem.classList.remove("hidden"); + } break; + } - default: + 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"); // There's some content, hide dummy placeholder document.getElementById("dummy-main").classList.add("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"); }); // MPris blocker checkboxes const blocker = new MPrisBlocker(); blocker.getAllowed().then((result) => { const origins = result.origins; if (Object.entries(origins).length === 0) { // "isEmpty" return; } // To keep media controls setting from always showing up, only show them, if: // - There is actually a player anywhere on this tab // or, since when mpris is disabled, there are never any players // - when media controls are disabled for any origin on this tab new Promise((resolve, reject) => { for (let origin in origins) { if (origins[origin] === false) { return resolve("HAS_BLOCKED"); } } TabUtils.getCurrentTab().then((tab) => { return sendMessage("mpris", "hasTabPlayer", { tabId: tab.id }); }).then((playerIds) => { if (playerIds.length > 0) { return resolve("HAS_PLAYER"); } reject("NO_PLAYER_NO_BLOCKED"); }); }).then(() => { // There's some content, hide dummy placeholder document.getElementById("dummy-main").classList.add("hidden"); let blacklistInfoElement = document.querySelector(".mpris-blacklist-info"); blacklistInfoElement.classList.remove("hidden"); let originsListElement = blacklistInfoElement.querySelector("ul.mpris-blacklist-origins"); for (const origin in origins) { const originAllowed = origins[origin]; let blockListElement = document.createElement("li"); let labelElement = document.createElement("label"); labelElement.innerText = origin; let checkboxElement = document.createElement("input"); checkboxElement.type = "checkbox"; checkboxElement.checked = (originAllowed === true); checkboxElement.addEventListener("click", (e) => { // Let us handle (un)checking the checkbox when setAllowed succeeds e.preventDefault(); const allowed = checkboxElement.checked; blocker.setAllowed(origin, allowed).then(() => { checkboxElement.checked = allowed; }, (err) => { console.warn("Failed to change media controls settings:", err); }); }); labelElement.insertBefore(checkboxElement, labelElement.firstChild); blockListElement.appendChild(labelElement); originsListElement.appendChild(blockListElement); } }, (err) => { console.log("Not showing media controls settings because", err); }); }, (err) => { console.warn("Failed to check for whether media controls are blocked", err); }); }); diff --git a/extension/extension.js b/extension/extension.js index 754c7242..666d2798 100644 --- a/extension/extension.js +++ b/extension/extension.js @@ -1,248 +1,250 @@ /* 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) { + 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) { - 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(); } 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. - portLastErrorMessage = ""; - updateBrowserAction(); + // Only clear error when it was a transient error, not a startup failure + if (receivedMessageOnce) { + portLastErrorMessage = ""; + updateBrowserAction(); + } }); });