diff --git a/extension/extension.js b/extension/extension.js index e8cf9318..4110ae85 100644 --- a/extension/extension.js +++ b/extension/extension.js @@ -1,246 +1,246 @@ /* 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() { 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 try { for (let device of kdeConnectDevices) { chrome.contextMenus.remove(kdeConnectMenuIdPrefix + device); } } catch (e) { console.warn("Failed to cleanup after port disconnect", e); } kdeConnectDevices = []; 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 () { +SettingsUtils.onChanged().addListener(() => { 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/options.js b/extension/options.js index 46d53c2c..e3ae2dc1 100644 --- a/extension/options.js +++ b/extension/options.js @@ -1,252 +1,250 @@ /* 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 . */ function tabClicked(tabbar, tabbutton) { tabbar.buttons.forEach(function (button) { var tablink = button.dataset.tabLink var tabtarget = document.querySelector("[data-tab-id=" + tablink + "]"); if (tabbutton == button) { button.classList.add("active"); tabtarget.classList.add("active"); } else { button.classList.remove("active"); tabtarget.classList.remove("active"); } }); } function loadSettings() { SettingsUtils.get().then((items) => { for (let key in items) { if (!items.hasOwnProperty(key)) { continue; } let controls = document.querySelectorAll("[data-extension=" + key + "]"); for (let control of controls) { let settingsKey = control.dataset.settingsKey; if (!settingsKey) { console.warn("Invalid settings key in", control, "cannot load this"); continue; } let value = items[key][settingsKey] if (control.type === "checkbox") { control.checked = !!value; } else { if (value === true) { control.value = "TRUE"; } else if (value === false) { control.value = "FALSE"; } else { control.value = value; } } updateDependencies(control, key, settingsKey); control.addEventListener("change", () => { let saveFailureInfoElement = document.getElementById("save-failure-info"); saveFailureInfoElement.classList.add("hidden"); updateDependencies(control, key, settingsKey); saveSettings((error) => { if (error) { saveFailureInfoElement.classList.remove("hidden"); let saveMessageElement = document.getElementById("save-message"); try { saveMessageElement.innerText = chrome.i18n.getMessage("options_save_failed"); } catch (e) { // When the extension is reloaded, any call to extension APIs throws, make sure we show at least some form of error saveMessageElement.innerText = "Saving settings failed (" + (error || e) + ")"; } return; } - - sendMessage("settings", "changed"); }); }); } } }); } function saveSettings(cb) { var settings = {}; let controls = document.querySelectorAll("[data-extension]"); for (let control of controls) { let extension = control.dataset.extension; if (!DEFAULT_EXTENSION_SETTINGS.hasOwnProperty(extension)) { console.warn("Cannot save settings for extension", extension, "which isn't in DEFAULT_EXTENSION_SETTINGS"); continue; } let settingsKey = control.dataset.settingsKey; if (!settingsKey) { console.warn("Invalid settings key in", control, "cannot save this"); continue; } if (!settings[extension]) { settings[extension] = {}; } if (!DEFAULT_EXTENSION_SETTINGS[extension].hasOwnProperty(settingsKey)) { console.warn("Cannot save settings key", settingsKey, "in extension", extension, "which isn't in DEFAULT_EXTENSION_SETTINGS"); continue; } if (control.type === "checkbox") { settings[extension][settingsKey] = control.checked; } else { let value = control.value; if (value === "TRUE") { value = true; } else if (value === "FALSE") { value = false; } settings[extension][settingsKey] = value; } } SettingsUtils.set(settings).then(() => { cb(); }, (err) => { cb(err); }); } function updateDependencies(control, extension, settingsKey) { // Update all depending controls let value = control.type === "checkbox" ? control.checked : control.value; if (value === true) { value = "TRUE"; } else if (value === false) { value = "FALSE"; } let dependencies = document.querySelectorAll("[data-depends-extension=" + extension + "][data-depends-settings-key=" + settingsKey + "]"); for (let dependency of dependencies) { dependency.disabled = (value != dependency.dataset.dependsSettingsValue); } } document.addEventListener("DOMContentLoaded", function () { // poor man's tab widget :) document.querySelectorAll(".tabbar").forEach(function (tabbar) { tabbar.buttons = []; tabbar.querySelectorAll("[data-tab-link]").forEach(function (button) { var tablink = button.dataset.tabLink var tabtarget = document.querySelector("[data-tab-id=" + tablink + "]"); button.addEventListener("click", function (event) { tabClicked(tabbar, button); event.preventDefault(); }); tabbar.buttons.push(button); // start with the one tab page that is active if (tabtarget.classList.contains("active")) { tabClicked(tabbar, button); } }); }); if (IS_FIREFOX) { document.querySelectorAll("[data-not-show-in=firefox]").forEach(function (item) { item.style.display = "none"; }); } // check whether the platform is supported before loading and activating settings chrome.runtime.getPlatformInfo(function (info) { if (!SUPPORTED_PLATFORMS.includes(info.os)) { document.body.classList.add("os-not-supported"); return; } loadSettings(); // When getSubsystemStatus fails we assume it's an old host without any of the new features // for which we added the requires-extension attributes. Disable all of them initially // and then have the supported ones enabled below. document.querySelectorAll("[data-requires-extension]").forEach((item) => { item.classList.add("not-supported", "by-host"); }); sendMessage("settings", "getSubsystemStatus").then((status) => { document.querySelectorAll("[data-requires-extension]").forEach((item) => { let requiresExtension = item.dataset.requiresExtension; if (requiresExtension && !status.hasOwnProperty(requiresExtension)) { console.log("Extension", requiresExtension, "is not supported by this version of the host"); return; // continue } let requiresMinimumVersion = Number(item.dataset.requiresExtensionVersionMinimum); if (requiresMinimumVersion) { let runningVersion = status[requiresExtension].version; if (runningVersion < requiresMinimumVersion) { console.log("Extension", requiresExtension, "of version", requiresMinimumVersion, "is required but only", runningVersion, "is present in the host"); return; // continue } } item.classList.remove("not-supported", "by-host"); }); }).catch((e) => { // The host is most likely not working correctly // If we run this against an older host which doesn't support message replies // this handler is never entered, so we really encountered an error just now! console.warn("Failed to determine subsystem status", e); document.body.classList.add("startup-failure"); }); }); document.getElementById("open-krunner-settings").addEventListener("click", function (event) { sendMessage("settings", "openKRunnerSettings"); event.preventDefault(); }); // Make translators credit behave like the one in KAboutData var translatorsAboutData = ""; var translators = chrome.i18n.getMessage("options_about_translators"); if (translators && translators !== "Your names") { translatorsAboutData = chrome.i18n.getMessage("options_about_translated_by", translators) } var translatorsAboutDataItem = document.getElementById("translators-aboutdata"); if (translatorsAboutData) { translatorsAboutDataItem.innerText = translatorsAboutData; } else { translatorsAboutDataItem.style.display = "none"; } }); diff --git a/extension/utils.js b/extension/utils.js index 30eba54c..20b2d49d 100644 --- a/extension/utils.js +++ b/extension/utils.js @@ -1,54 +1,56 @@ /* 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 SettingsUtils { static storage() { return (IS_FIREFOX ? chrome.storage.local : chrome.storage.sync); } static get() { return new Promise((resolve, reject) => { SettingsUtils.storage().get(DEFAULT_EXTENSION_SETTINGS, (items) => { const error = chrome.runtime.lastError; if (error) { return reject(error); } resolve(items); }); }); } static set(settings) { return new Promise((resolve, reject) => { try { SettingsUtils.storage().set(settings, () => { const error = chrome.runtime.lastError; if (error) { return reject(error); } resolve(); }); } catch (e) { reject(e); } }); } -} - + static onChanged() { + return chrome.storage.onChanged; + } +}