diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 4fe2f84..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "root": true, - "extends": [ - "prettier" - ], - "plugins": [], - "rules": {} -} \ No newline at end of file diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 98321b8..aa6d3f7 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -82,6 +82,9 @@ "OptionsPerDeviceWarning": { "message": "Note: All settings are per-device. If you have YouTube Popout Player installed on multiple devices, you can configure settings differently for each device." }, + "OptionsPageForceDarkMode": { + "message": "Force Dark Mode for Options panel" + }, "OptionsTabNameBehavior": { "message": "Behavior" }, @@ -161,7 +164,10 @@ "message": "Autoplay Video(s)" }, "OptionsBehaviorAutoplayDescription": { - "message": "This setting controls whether or not the video in the popout player begins playing automatically when the popout player is opened. The default for this setting is enabled.
Note: Autoplaying videos with sound are commonly blocked by default in modern browsers; you may need to manually enable/allow autoplay through your browser's settings for this to work correctly.
" + "message": "This setting controls whether or not the video in the popout player begins playing automatically when the popout player is opened. The default for this setting is enabled." + }, + "AutoplayVideosBlockedTip": { + "message": "Note: Autoplaying videos with sound are commonly blocked by default in modern browsers; you may need to manually enable/allow autoplay through your browser's settings for this to work correctly." }, "OptionsBehaviorLoopLabel": { "message": "Loop Video(s)" @@ -283,6 +289,12 @@ "HeightLabel": { "message": "Height" }, + "TopLabel": { + "message": "Top" + }, + "LeftLabel": { + "message": "Left" + }, "InfoCurrentScreenResolutionLabel": { "message": "Current Screen Resolution" }, diff --git a/app/images/icon-128.png b/app/images/icon-128.png index 0064f15..f7c6947 100644 Binary files a/app/images/icon-128.png and b/app/images/icon-128.png differ diff --git a/app/images/icon-16.png b/app/images/icon-16.png index 603d3a3..b811307 100644 Binary files a/app/images/icon-16.png and b/app/images/icon-16.png differ diff --git a/app/images/icon-19.png b/app/images/icon-19.png index b05649c..f75a19b 100644 Binary files a/app/images/icon-19.png and b/app/images/icon-19.png differ diff --git a/app/images/icon-24.png b/app/images/icon-24.png index 834251e..1a3da71 100644 Binary files a/app/images/icon-24.png and b/app/images/icon-24.png differ diff --git a/app/images/icon-32.png b/app/images/icon-32.png index f73051e..91f29a5 100644 Binary files a/app/images/icon-32.png and b/app/images/icon-32.png differ diff --git a/app/images/icon-38.png b/app/images/icon-38.png index aad3e11..88930db 100644 Binary files a/app/images/icon-38.png and b/app/images/icon-38.png differ diff --git a/app/images/icon-48.png b/app/images/icon-48.png index 8b3cfd9..b66192f 100644 Binary files a/app/images/icon-48.png and b/app/images/icon-48.png differ diff --git a/app/images/icon-64.png b/app/images/icon-64.png index b8547b2..c1842a5 100644 Binary files a/app/images/icon-64.png and b/app/images/icon-64.png differ diff --git a/app/images/icon-96.png b/app/images/icon-96.png index d96f35a..09bb3f4 100644 Binary files a/app/images/icon-96.png and b/app/images/icon-96.png differ diff --git a/app/manifest.json b/app/manifest.json index c02e436..e8fb525 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -2,7 +2,7 @@ "name": "__MSG_ExtensionName__", "short_name": "__MSG_ExtensionShortName__", "description": "__MSG_ExtensionDescription__", - "manifest_version": 2, + "manifest_version": 3, "default_locale": "en", "author": "Ryan Thaut", "homepage_url": "https://rthaut.github.io/YouTubePopoutPlayer/", @@ -18,11 +18,10 @@ "128": "images/icon-128.png" }, "background": { - "scripts": [ - "scripts/background.js" - ] + "__chrome|edge__service_worker": "scripts/background.js", + "__firefox__scripts": ["scripts/background.js"] }, - "browser_action": { + "action": { "default_icon": { "16": "images/icon-16.png", "19": "images/icon-19.png", @@ -90,14 +89,22 @@ "__firefox__page": "pages/options.html?vendor=firefox", "open_in_tab": false }, - "permissions": [ + "declarative_net_request" : { + "rule_resources" : [{ + "id": "rules", + "enabled": true, + "path": "rules.json" + }] + }, + "host_permissions": [ "*://*.youtube.com/*", - "*://*.youtube-nocookie.com/*", + "*://*.youtube-nocookie.com/*" + ], + "permissions": [ "contextMenus", + "declarativeNetRequest", "notifications", - "storage", - "webRequest", - "webRequestBlocking" + "storage" ], "optional_permissions": [ "tabs" @@ -106,12 +113,12 @@ "cookies", "tabs" ], - "__chrome__minimum_chrome_version": "51.0", - "__edge__minimum_chrome_version": "79.0.309", - "__firefox__applications": { + "__chrome__minimum_chrome_version": "90", + "__edge__minimum_chrome_version": "91", + "__firefox__browser_specific_settings": { "gecko": { "id": "{85b42b8f-49cd-4935-aeca-a6b32dd6ac9f}", - "strict_min_version": "62.0" + "strict_min_version": "109.0" } } } diff --git a/app/rules.json b/app/rules.json new file mode 100644 index 0000000..1934099 --- /dev/null +++ b/app/rules.json @@ -0,0 +1,44 @@ +[ + { + "id": 1, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "Referer", + "operation": "set", + "value": "https://www.youtube.com/" + } + ] + }, + "condition": { + "urlFilter": "||youtube.com/embed*popout=1", + "resourceTypes": [ + "main_frame", + "sub_frame" + ] + } + }, + { + "id": 2, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "Referer", + "operation": "set", + "value": "https://www.youtube-nocookie.com/" + } + ] + }, + "condition": { + "urlFilter": "||youtube-nocookie.com/embed*popout=1", + "resourceTypes": [ + "main_frame", + "sub_frame" + ] + } + } +] \ No newline at end of file diff --git a/app/scripts/.eslintrc.json b/app/scripts/.eslintrc.json index a4a5c64..1066767 100644 --- a/app/scripts/.eslintrc.json +++ b/app/scripts/.eslintrc.json @@ -11,6 +11,7 @@ }, "extends": [ "eslint:recommended", + "prettier", "plugin:react/recommended" ], "parserOptions": { diff --git a/app/scripts/background.js b/app/scripts/background.js index 4b00b76..6c23580 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -2,16 +2,10 @@ import { OnCommandEventHandler } from "./background/commands"; import { InitMenus } from "./background/menus"; import { OpenPopoutBackgroundHelper } from "./background/popout"; import { OnInstalled, OnRuntimeMessage } from "./background/runtime"; -import { - GetExtraInfoSpec, - GetFilter, - OnBeforeSendHeaders, - OnSendHeaders, -} from "./background/webRequest"; import Options from "./helpers/options"; import { IsVideoURL } from "./helpers/youtube"; -browser.browserAction.onClicked.addListener(() => { +browser.action.onClicked.addListener(() => { if (browser.runtime.openOptionsPage) { browser.runtime.openOptionsPage(); } else { @@ -29,18 +23,6 @@ browser.runtime.onInstalled.addListener(OnInstalled); browser.runtime.onMessage.addListener(OnRuntimeMessage); -browser.webRequest.onBeforeSendHeaders.addListener( - OnBeforeSendHeaders, - GetFilter("onBeforeSendHeaders"), - GetExtraInfoSpec("onBeforeSendHeaders"), -); - -browser.webRequest.onSendHeaders.addListener( - OnSendHeaders, - GetFilter("onSendHeaders"), - GetExtraInfoSpec("onSendHeaders"), -); - browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => { if (changeInfo.url && IsVideoURL(changeInfo.url, false)) { if (await Options.GetLocalOption("advanced", "autoOpen")) { diff --git a/app/scripts/background/menus.js b/app/scripts/background/menus.js index 7531527..336a240 100644 --- a/app/scripts/background/menus.js +++ b/app/scripts/background/menus.js @@ -5,74 +5,86 @@ import { import Options from "../helpers/options"; import { OpenPopoutBackgroundHelper } from "./popout"; -export const GetMenus = async () => { +const GetMenus = async () => { const menus = [ { + id: "OpenVideo", title: browser.i18n.getMessage("LinkContextMenuEntry_OpenVideo_Text"), contexts: ["link"], targetUrlPatterns: YOUTUBE_VIDEO_URL_PATTERNS, - onclick: (info, tab) => - OpenPopoutBackgroundHelper(info.linkUrl, tab.id, true, false), }, { + id: "OpenPlaylist", title: browser.i18n.getMessage("LinkContextMenuEntry_OpenPlaylist_Text"), contexts: ["link"], targetUrlPatterns: YOUTUBE_PLAYLIST_URL_PATTERNS, - onclick: (info, tab) => - OpenPopoutBackgroundHelper(info.linkUrl, tab.id, true, false), }, ]; - const showRotationMenus = await Options.GetLocalOption( - "behavior", - "showRotationMenus", - ); + const showRotationMenus = await Options.GetLocalOption("behavior", "showRotationMenus"); + if (showRotationMenus !== false) { menus.push({ - title: browser.i18n.getMessage( - "LinkContextMenuEntry_OpenVideoRotateLeft_Text", - ), + id: "OpenVideoRotateLeft", + title: browser.i18n.getMessage("LinkContextMenuEntry_OpenVideoRotateLeft_Text"), contexts: ["link"], targetUrlPatterns: YOUTUBE_VIDEO_URL_PATTERNS, - onclick: (info, tab) => - OpenPopoutBackgroundHelper(info.linkUrl, tab.id, true, false, 270), }); menus.push({ - title: browser.i18n.getMessage( - "LinkContextMenuEntry_OpenVideoRotateRight_Text", - ), + id: "OpenVideoRotateRight", + title: browser.i18n.getMessage("LinkContextMenuEntry_OpenVideoRotateRight_Text"), contexts: ["link"], targetUrlPatterns: YOUTUBE_VIDEO_URL_PATTERNS, - onclick: (info, tab) => - OpenPopoutBackgroundHelper(info.linkUrl, tab.id, true, false, 90), }); } return menus; }; +const OnMenuClicked = async (info, tab) => { + switch (info.menuItemId) { + case "OpenVideo": + case "OpenPlaylist": + OpenPopoutBackgroundHelper(info.linkUrl, tab.id, true, false); + break; + + case "OpenVideoRotateLeft": + OpenPopoutBackgroundHelper(info.linkUrl, tab.id, true, false, 270) + break; + + case "OpenVideoRotateRight": + OpenPopoutBackgroundHelper(info.linkUrl, tab.id, true, false, 90) + break; + } +} + +const CreateMenus = async (reset = true) => { + try { + if (reset) { + await browser.contextMenus.removeAll(); + } + + if (!browser.contextMenus.onClicked.hasListener(OnMenuClicked)) { + browser.contextMenus.onClicked.addListener(OnMenuClicked); + } + + const menus = await GetMenus(); + menus.forEach((menu) => browser.contextMenus.create(menu)); + } catch (ex) { + console.error("Failed to initialize context menus", ex); + } +}; + /** * Initializes menus and event handlers */ export const InitMenus = async () => { console.log("[Background] InitMenus()"); - const createMenus = async (reset = true) => { - try { - if (reset) { - await browser.contextMenus.removeAll(); - } - const menus = await GetMenus(); - menus.forEach((menu) => browser.contextMenus.create(menu)); - } catch (ex) { - console.error("Failed to initialize context menus", ex); - } - }; - - createMenus(); + CreateMenus(); browser.storage.local.onChanged.addListener((changes) => { if (Object.keys(changes).includes("behavior.showRotationMenus")) { - createMenus(true); + CreateMenus(true); } }); }; diff --git a/app/scripts/background/webRequest.js b/app/scripts/background/webRequest.js deleted file mode 100644 index 4d2cf93..0000000 --- a/app/scripts/background/webRequest.js +++ /dev/null @@ -1,130 +0,0 @@ -import { - YOUTUBE_EMBED_URL, - YOUTUBE_NOCOOKIE_EMBED_URL, -} from "../helpers/constants"; - -/** - * Returns the `filter` object for the specified web request event - * @param {string} eventName name of the web request event - * @returns {object} the `filter` object - */ -export const GetFilter = (eventName) => { - console.log("[Background] GetFilters()", eventName); - - const filter = {}; - - switch (eventName) { - default: - filter["urls"] = [ - YOUTUBE_EMBED_URL + "*", - YOUTUBE_NOCOOKIE_EMBED_URL + "*", - ]; - break; - } - - console.log("[Background] GetFilters() :: Return", filter); - return filter; -}; - -/** - * Returns the `extraInfoSpec` options for the specified web request event - * @param {string} eventName name of the web request event - * @returns {string[]} the `extraInfoSpec` options - */ -export const GetExtraInfoSpec = (eventName) => { - console.log("[Background] GetExtraInfoSpec()", eventName); - - const extraInfoSpec = []; - - /** - * Adds the `extraHeaders` option for browsers that need it to set certain headers - */ - const _addExtraHeadersOption = () => { - if ( - Object.prototype.hasOwnProperty.call( - chrome?.webRequest?.OnBeforeSendHeadersOptions, - "EXTRA_HEADERS", - ) - ) { - extraInfoSpec.push("extraHeaders"); - } - }; - - switch (eventName) { - case "onBeforeSendHeaders": - extraInfoSpec.push("blocking", "requestHeaders"); - _addExtraHeadersOption(); - break; - - case "onSendHeaders": - extraInfoSpec.push("requestHeaders"); - _addExtraHeadersOption(); - break; - } - - console.log("[Background] GetExtraInfoSpec() :: Return", extraInfoSpec); - return extraInfoSpec; -}; - -/** - * Modifies requests to the YouTube Embedded Player to ensure the necessary headers are set - * @param {object} details details of the request - */ -export const OnBeforeSendHeaders = (details) => { - console.log("[Background] OnBeforeSendHeaders()", details); - - const url = new URL(details.url); - let { requestHeaders } = details; - - const HasHeader = (name) => - requestHeaders.some( - (header) => header.name.toLowerCase() === name.toLowerCase(), - ); - - const AddHeader = (header) => { - if (!HasHeader(header.name)) { - console.log( - `[Background] OnBeforeSendHeaders() :: Setting "${header.name}" header`, - header, - ); - requestHeaders.push(header); - } - }; - - // only if the request is for the popout player (identified by a custom GET parameter in the query string) - if (parseInt(url.searchParams.get("popout"), 10) === 1) { - console.log( - "[Background] OnBeforeSendHeaders() :: Request is for popout player", - url, - ); - - // the `Referer` header is required to avoid the "Video unavailable" error in the popout player - AddHeader({ - name: "Referer", - value: url.origin + url.pathname, - }); - - // AddHeader({ - // name: "Host", - // value: url.hostname, - // }); - } - - console.group("[Background] OnBeforeSendHeaders() :: Return"); - console.table(requestHeaders, ["name", "value"]); - console.groupEnd(); - - return { - requestHeaders, - }; -}; - -/** - * Logs the headers that are actually sent in the request (for debugging) - * @param {object} details details of the request - */ -export const OnSendHeaders = ({ requestHeaders }) => { - console.group("[Background] OnSendHeaders() :: Request Headers"); - console.table(requestHeaders, ["name", "value"]); - console.groupEnd(); -}; diff --git a/app/scripts/content/YouTubeCustomControls.js b/app/scripts/content/YouTubeCustomControls.js index 5c43de5..36f59ab 100644 --- a/app/scripts/content/YouTubeCustomControls.js +++ b/app/scripts/content/YouTubeCustomControls.js @@ -34,9 +34,11 @@ export const InsertControlsAndWatch = async () => { export const InsertControls = async () => { console.group("[YouTubeCustomControls] InsertControls()"); - InsertPopoutEntryIntoContextMenu(); - await InsertPopoutButtonIntoPlayerControls(); - await InsertRotationButtonsIntoPlayerControls(); + if (document.querySelector("video")) { + InsertPopoutEntryIntoContextMenu(); + await InsertPopoutButtonIntoPlayerControls(); + await InsertRotationButtonsIntoPlayerControls(); + } }; /** diff --git a/app/scripts/options.js b/app/scripts/options.js index 47b853a..45e1b8d 100644 --- a/app/scripts/options.js +++ b/app/scripts/options.js @@ -1,18 +1,11 @@ -import React from "react"; - -/* global process */ -if (process.env.NODE_ENV === "development") { - const whyDidYouRender = require('@welldone-software/why-did-you-render'); - whyDidYouRender(React, { - trackAllPureComponents: true, - }); -} - -import ReactDOM from "react-dom"; +import * as React from "react"; +import { createRoot } from "react-dom/client"; import OptionsApp from "./options/OptionsApp"; -ReactDOM.render(, document.querySelector("#root")); +const container = document.querySelector("#root"); +const root = createRoot(container); +root.render(); const params = new URL(document.location).searchParams; const vendor = params.get("vendor"); diff --git a/app/scripts/options/OptionsApp.jsx b/app/scripts/options/OptionsApp.jsx index 3c36dd0..2806899 100644 --- a/app/scripts/options/OptionsApp.jsx +++ b/app/scripts/options/OptionsApp.jsx @@ -1,22 +1,24 @@ import React from "react"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import { createTheme, ThemeProvider } from "@material-ui/core/styles"; -import blue from "@material-ui/core/colors/blue"; -import red from "@material-ui/core/colors/red"; +import CssBaseline from "@mui/material/CssBaseline"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { createTheme, ThemeProvider, StyledEngineProvider } from "@mui/material/styles"; +import { blue, red } from "@mui/material/colors"; -import Alert from "@material-ui/lab/Alert"; -import AppBar from "@material-ui/core/AppBar"; -import Box from "@material-ui/core/Box"; -import Paper from "@material-ui/core/Paper"; -import Tab from "@material-ui/core/Tab"; -import TabContext from "@material-ui/lab/TabContext"; -import TabList from "@material-ui/lab/TabList"; -import TabPanel from "@material-ui/lab/TabPanel"; -import Typography from "@material-ui/core/Typography"; +import Alert from "@mui/lab/Alert"; +import AppBar from "@mui/material/AppBar"; +import Box from "@mui/material/Box"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Paper from "@mui/material/Paper"; +import Stack from "@mui/material/Stack"; +import Switch from "@mui/material/Switch"; +import Tab from "@mui/material/Tab"; +import TabContext from "@mui/lab/TabContext"; +import TabList from "@mui/lab/TabList"; +import TabPanel from "@mui/lab/TabPanel"; +import Typography from "@mui/material/Typography"; -import CloudOffIcon from "@material-ui/icons/CloudOff"; +import CloudOffIcon from "@mui/icons-material/CloudOff"; import ResetOptions from "./components/ResetOptions"; @@ -31,13 +33,33 @@ import AdvancedTab, { } from "./components/tabs/AdvancedTab"; export default function OptionsApp() { + const [forceDarkMode, setForceDarkMode] = React.useState( + localStorage.getItem("force-dark-mode"), + ); const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + const [mode, setMode] = React.useState( + forceDarkMode ? "dark" : prefersDarkMode ? "dark" : "light", + ); + + React.useEffect(() => { + try { + if (forceDarkMode) { + setMode("dark"); + localStorage.setItem("force-dark-mode", true); + } else { + setMode(prefersDarkMode ? "dark" : "light"); + localStorage.removeItem("force-dark-mode"); + } + } catch (error) { + console.warn("Failed to persist force dark mode to local storage"); + } + }, [prefersDarkMode, forceDarkMode]); const theme = React.useMemo( () => createTheme({ palette: { - type: prefersDarkMode ? "dark" : "light", + mode, primary: { main: blue[800], }, @@ -45,23 +67,11 @@ export default function OptionsApp() { main: red["A700"], }, background: { - default: prefersDarkMode ? "#303030" : "#fff", - }, - }, - overrides: { - MuiCssBaseline: { - "@global": { - a: { - color: prefersDarkMode ? "white" : "black", - "&:hover": { - textDecoration: "none", - }, - }, - }, + default: mode === "dark" ? "#303030" : "#fff", }, }, }), - [prefersDarkMode] + [mode], ); React.useEffect(() => { @@ -81,44 +91,61 @@ export default function OptionsApp() { }; return ( - - - - - - - {Object.keys(tabs).map((tab) => ( - + + + + + + + {Object.keys(tabs).map((tab) => ( + + ))} + + + {Object.entries(tabs).map(([domain, panel]) => ( + + {panel} + + ))} + + + + } + // onClose={() => {}} // TODO: use a cookie here? or an extension storage item? how/when to re-display it? + > + + + + + + + setForceDarkMode(event.target.checked)} /> - ))} - - - {Object.entries(tabs).map(([domain, panel]) => ( - - {panel} - - ))} - - - - } - // onClose={() => {}} // TODO: use a cookie here? or an extension storage item? how/when to re-display it? - > - - - - - - - + } + label={browser.i18n.getMessage("OptionsPageForceDarkMode")} + /> + + + + ); } diff --git a/app/scripts/options/components/ResetOptions.jsx b/app/scripts/options/components/ResetOptions.jsx index 7ce36c7..a46d96f 100644 --- a/app/scripts/options/components/ResetOptions.jsx +++ b/app/scripts/options/components/ResetOptions.jsx @@ -1,15 +1,15 @@ import React from "react"; -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import Grid from "@material-ui/core/Grid"; -import Snackbar from "@material-ui/core/Snackbar"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import Snackbar from "@mui/material/Snackbar"; -import HighlightOffIcon from "@material-ui/icons/HighlightOff"; +import HighlightOffIcon from "@mui/icons-material/HighlightOff"; import useOptionsStore from "../stores/optionsStore"; @@ -43,24 +43,16 @@ export default function ResetOptions() { return ( <> - - - - - + + + {browser.i18n.getMessage("ConfirmSettingsResetHeading")} @@ -73,7 +65,7 @@ export default function ResetOptions() { /> -