diff --git a/extension/_locales/en/messages.json b/extension/_locales/en/messages.json --- a/extension/_locales/en/messages.json +++ b/extension/_locales/en/messages.json @@ -9,6 +9,15 @@ "message": "Plasma Browser Integration" }, + "browseraction_mpris_title": { + "description": "Title for Media controls in popup", + "message": "Media Controls" + }, + "browseraction_mpris_enable_on": { + "description": "Heading for list of domains to enable media controls on", + "message": "Enable media controls on:" + }, + "options_title": { "description": "Title for settings page", "message": "Plasma Integration Settings" diff --git a/extension/action_popup.html b/extension/action_popup.html --- a/extension/action_popup.html +++ b/extension/action_popup.html @@ -7,6 +7,8 @@ + + @@ -41,7 +43,15 @@ diff --git a/extension/action_popup.css b/extension/action_popup.css --- a/extension/action_popup.css +++ b/extension/action_popup.css @@ -16,10 +16,14 @@ text-align: center; } +section { + margin: 0 0.5em; +} + section > header { background: #F0F0F0; color: #757777; - margin: 0 -1em .5em -1em; + margin: 0 -1em -0.5em -1em; } .message { @@ -31,6 +35,9 @@ display: block; background: center no-repeat; } +.message.with-icon.general::before { + background-image: url('icons/plasma.svg'); +} .message.with-icon.error::before { background-image: url('icons/sad-face.svg'); } @@ -44,3 +51,19 @@ filter: invert(1); } } + +/* Media controls blacklist */ +.mpris-blacklist-info { + padding: 0.5em 0; +} +.mpris-blacklist-info p { + padding: 0 0.5em; +} +.mpris-blacklist-info ul { + display: block; + padding: 0 0.5em; +} +.mpris-blacklist-info ul > li { + display: block; + margin-bottom: 0.5em; +} diff --git a/extension/action_popup.js b/extension/action_popup.js --- a/extension/action_popup.js +++ b/extension/action_popup.js @@ -15,6 +15,141 @@ 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) => { @@ -39,6 +174,9 @@ 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; @@ -55,4 +193,79 @@ 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/constants.js b/extension/constants.js --- a/extension/constants.js +++ b/extension/constants.js @@ -18,7 +18,8 @@ DEFAULT_EXTENSION_SETTINGS = { mpris: { - enabled: true + enabled: true, + websiteSettings: {} }, mprisMediaSessions: { enabled: true @@ -46,3 +47,8 @@ // NOTE if you change this, make sure to adjust the error message shown in action_popup.html SUPPORTED_PLATFORMS = ["linux", "openbsd", "freebsd"]; + +// Default MPRIS settings for websites +const MPRIS_WEBSITE_SETTINGS = { + //"https://www.example.com": false +}; diff --git a/extension/content-script.js b/extension/content-script.js --- a/extension/content-script.js +++ b/extension/content-script.js @@ -65,9 +65,23 @@ const mpris = items.mpris; if (mpris.enabled) { - loadMpris(); - if (items.mprisMediaSessions.enabled) { - loadMediaSessionsShim(); + const origin = window.location.origin; + + const websiteSettings = mpris.websiteSettings || {}; + + let mprisAllowed = true; + if (typeof MPRIS_WEBSITE_SETTINGS[origin] === "boolean") { + mprisAllowed = MPRIS_WEBSITE_SETTINGS[origin]; + } + if (typeof websiteSettings[origin] === "boolean") { + mprisAllowed = websiteSettings[origin]; + } + + if (mprisAllowed) { + loadMpris(); + if (items.mprisMediaSessions.enabled) { + loadMediaSessionsShim(); + } } } diff --git a/extension/extension-mpris.js b/extension/extension-mpris.js --- a/extension/extension-mpris.js +++ b/extension/extension-mpris.js @@ -195,3 +195,11 @@ sendPortMessage("mpris", action, payload); } }); + +addRuntimeCallback("mpris", "hasTabPlayer", (message) => { + const playersOnTab = playerIds.filter((playerId) => { + return playerId.startsWith(message.tabId + "-"); + }); + + return Promise.resolve(playersOnTab); +}); diff --git a/extension/extension.js b/extension/extension.js --- a/extension/extension.js +++ b/extension/extension.js @@ -86,7 +86,6 @@ var portLastErrorMessage = undefined; function updateBrowserAction() { - let enableAction = false; if (portStatus === "UNSUPPORTED_OS" || portStatus === "STARTUP_FAILED") { chrome.browserAction.setIcon({ path: { @@ -96,22 +95,14 @@ "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();