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();
+ }
});
});