Convert extension to manifest v3

This commit is contained in:
hensm
2026-03-08 12:40:56 +00:00
parent c5846a05d9
commit d6099f6f08
19 changed files with 343 additions and 360 deletions

17
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Bridge (daemon)",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/dist/bridge/src/main.js",
"args": ["--__name", "fx_cast_bridge", "--daemon"],
"env": {
"NODE_PATH": "${workspaceFolder}/bridge/node_modules"
},
"cwd": "${workspaceFolder}/dist/bridge",
"console": "integratedTerminal"
}
]
}

View File

@@ -64,16 +64,20 @@ export default class Remote extends CastClient {
if (!application || application.isIdleScreen) { if (!application || application.isIdleScreen) {
// Handle app close // Handle app close
if (this.transportClient) { if (this.transportClient) {
this.transportClient.disconnect();
this.transportClient = undefined; this.transportClient = undefined;
this.options?.onApplicationClose?.(); this.options?.onApplicationClose?.();
} }
this.options?.onReceiverStatusUpdate?.(message.status);
return;
} }
// Update status before possible transport init // Update status before possible transport init
this.options?.onReceiverStatusUpdate?.(message.status); this.options?.onReceiverStatusUpdate?.(message.status);
// Handle app creation/discovery // Handle app creation/discovery
if (application && !this.transportClient) { if (!this.transportClient) {
this.transportClient = new RemoteTransport( this.transportClient = new RemoteTransport(
application.transportId, application.transportId,
message => this.onMediaMessage(message) message => this.onMediaMessage(message)

View File

@@ -55,7 +55,7 @@ const outPath = argv.package ? unpackedPath : distPath;
/** @type esbuild.BuildOptions */ /** @type esbuild.BuildOptions */
const buildOpts = { const buildOpts = {
bundle: true, bundle: true,
target: "firefox64", target: "firefox109",
logLevel: "info", logLevel: "info",
sourcemap: "inline", sourcemap: "inline",
@@ -113,10 +113,13 @@ const buildOpts = {
}) })
); );
manifest.content_security_policy = // In development, allow eval for source maps
argv.mode === "production" if (argv.mode !== "production") {
? "script-src 'self'; object-src 'self'" manifest.content_security_policy = {
: "script-src 'self' 'unsafe-eval'; object-src 'self'"; extension_pages:
"script-src 'self' 'unsafe-eval'; object-src 'self'"
};
}
fs.writeFileSync( fs.writeFileSync(
`${outPath}/manifest.json`, `${outPath}/manifest.json`,

View File

@@ -5,7 +5,7 @@
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"@types/firefox-webext-browser": "^94.0.1", "@types/firefox-webext-browser": "^143.0.0",
"@types/semver": "^7.3.9", "@types/semver": "^7.3.9",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
@@ -881,10 +881,11 @@
"peer": true "peer": true
}, },
"node_modules/@types/firefox-webext-browser": { "node_modules/@types/firefox-webext-browser": {
"version": "94.0.1", "version": "143.0.0",
"resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-94.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-143.0.0.tgz",
"integrity": "sha512-I6iHRQJSTZ+gYt2IxdH2RRAMvcUyK8v5Ig7fHQR0IwUNYP7hz9+cziBVIKxLCO6XI7fiyRsNOWObfl3/4Js2Lg==", "integrity": "sha512-865dYKMOP0CllFyHmgXV4IQgVL51OSQQCwSoihQ17EwugePKFSAZRc0EI+y7Ly4q7j5KyURlA7LgRpFieO4JOw==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/@types/http-cache-semantics": { "node_modules/@types/http-cache-semantics": {
"version": "4.0.1", "version": "4.0.1",
@@ -7509,9 +7510,9 @@
"peer": true "peer": true
}, },
"@types/firefox-webext-browser": { "@types/firefox-webext-browser": {
"version": "94.0.1", "version": "143.0.0",
"resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-94.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-143.0.0.tgz",
"integrity": "sha512-I6iHRQJSTZ+gYt2IxdH2RRAMvcUyK8v5Ig7fHQR0IwUNYP7hz9+cziBVIKxLCO6XI7fiyRsNOWObfl3/4Js2Lg==", "integrity": "sha512-865dYKMOP0CllFyHmgXV4IQgVL51OSQQCwSoihQ17EwugePKFSAZRc0EI+y7Ly4q7j5KyURlA7LgRpFieO4JOw==",
"dev": true "dev": true
}, },
"@types/http-cache-semantics": { "@types/http-cache-semantics": {

View File

@@ -8,7 +8,7 @@
"lint": "eslint src --ext .ts,.tsx" "lint": "eslint src --ext .ts,.tsx"
}, },
"devDependencies": { "devDependencies": {
"@types/firefox-webext-browser": "^94.0.1", "@types/firefox-webext-browser": "^143.0.0",
"@types/semver": "^7.3.9", "@types/semver": "^7.3.9",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",

View File

@@ -40,8 +40,8 @@ export function updateActionState(state: ActionState, tabId?: number) {
break; break;
} }
browser.browserAction.setTitle({ tabId, title }); browser.action.setTitle({ tabId, title });
browser.browserAction.setIcon({ tabId, path }); browser.action.setIcon({ tabId, path });
} }
export function initAction() { export function initAction() {
@@ -49,7 +49,7 @@ export function initAction() {
updateActionState(ActionState.Default); updateActionState(ActionState.Default);
browser.browserAction.onClicked.addListener(async tab => { browser.action.onClicked.addListener(async tab => {
if (tab.id === undefined) { if (tab.id === undefined) {
logger.error("Tab ID not found in browser action handler."); logger.error("Tab ID not found in browser action handler.");
return; return;

View File

@@ -1,8 +1,8 @@
import logger from "../lib/logger"; import logger from "../lib/logger";
import options from "../lib/options"; import options from "../lib/options";
import { stringify } from "../lib/utils";
import * as menuIds from "../menuIds"; import * as menuIds from "../menuIds";
import { MenuId } from "../menuIds";
import castManager from "./castManager"; import castManager from "./castManager";
@@ -15,32 +15,30 @@ const URL_PATTERN_FILE = "file://*/*";
const URL_PATTERNS_REMOTE = [URL_PATTERN_HTTP, URL_PATTERN_HTTPS]; const URL_PATTERNS_REMOTE = [URL_PATTERN_HTTP, URL_PATTERN_HTTPS];
const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE]; const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE];
type MenuId = string | number;
let menuIdCast: MenuId;
let menuIdCastMedia: MenuId;
let menuIdWhitelist: MenuId;
let menuIdWhitelistRecommended: MenuId;
/** Match patterns for the whitelist option menus. */ /** Match patterns for the whitelist option menus. */
const whitelistChildMenuPatterns = new Map<MenuId, string>(); const whitelistChildMenuPatterns = new Map<string | number, string>();
/** Handles initial menu setup. */ /** Handles initial menu setup. */
export async function initMenus() { export async function initMenus() {
logger.info("init (menus)"); logger.info("init (menus)");
// Clear any existing menus from a previous event page load
await browser.menus.removeAll();
const opts = await options.getAll(); const opts = await options.getAll();
// Global "Cast..." menu item // Global "Cast..." menu item
menuIdCast = browser.menus.create({ browser.menus.create({
contexts: ["browser_action", "page", "tools_menu"], id: MenuId.Cast,
contexts: ["action", "page", "tools_menu"],
title: _("contextCast"), title: _("contextCast"),
documentUrlPatterns: ["http://*/*", "https://*/*"], documentUrlPatterns: ["http://*/*", "https://*/*"],
icons: { "16": "icons/icon.svg" } // browser_action context icons: { "16": "icons/icon.svg" }
}); });
// <video>/<audio> "Cast..." context menu item // <video>/<audio> "Cast..." context menu item
menuIdCastMedia = browser.menus.create({ browser.menus.create({
id: MenuId.CastMedia,
contexts: ["audio", "video", "image"], contexts: ["audio", "video", "image"],
title: _("contextCast"), title: _("contextCast"),
visible: opts.mediaEnabled, visible: opts.mediaEnabled,
@@ -49,67 +47,81 @@ export async function initMenus() {
: URL_PATTERNS_REMOTE : URL_PATTERNS_REMOTE
}); });
menuIdWhitelist = browser.menus.create({ // Whitelist menu parent item
contexts: ["browser_action"], browser.menus.create({
id: MenuId.Whitelist,
contexts: ["action"],
title: _("contextAddToWhitelist"), title: _("contextAddToWhitelist"),
enabled: false enabled: false
}); });
// Top item in the whitelist submenu, which is the recommended pattern based
menuIdWhitelistRecommended = browser.menus.create({ // on the current page URL and is always present.
browser.menus.create({
id: MenuId.WhitelistRecommended,
title: _("contextAddToWhitelistRecommended"), title: _("contextAddToWhitelistRecommended"),
parentId: menuIdWhitelist parentId: MenuId.Whitelist
}); });
// Separator between recommended and advanced patterns
browser.menus.create({ browser.menus.create({
id: MenuId.WhitelistSeparator,
type: "separator", type: "separator",
parentId: menuIdWhitelist parentId: MenuId.Whitelist
}); });
// Popup context menus const popupMenuProps = {
const createPopupMenu = (props: browser.menus._CreateCreateProperties) =>
browser.menus.create({
visible: false, visible: false,
documentUrlPatterns: [`${browser.runtime.getURL("ui/popup")}/*`], documentUrlPatterns: [`${browser.runtime.getURL("ui/popup")}/*`]
...props } satisfies browser.menus._CreateCreateProperties;
});
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_MEDIA_PLAY_PAUSE, ...popupMenuProps,
id: MenuId.PopupMediaPlayPause,
title: _("popupMediaPlay") title: _("popupMediaPlay")
}); });
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_MEDIA_MUTE, ...popupMenuProps,
id: MenuId.PopupMediaMute,
type: "checkbox", type: "checkbox",
title: _("popupMediaMute") title: _("popupMediaMute")
}); });
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_MEDIA_SKIP_PREVIOUS, ...popupMenuProps,
id: MenuId.PopupMediaSkipPrevious,
title: _("popupMediaSkipPrevious") title: _("popupMediaSkipPrevious")
}); });
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_MEDIA_SKIP_NEXT, ...popupMenuProps,
id: MenuId.PopupMediaSkipNext,
title: _("popupMediaSkipNext") title: _("popupMediaSkipNext")
}); });
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_MEDIA_CC, ...popupMenuProps,
id: MenuId.PopupMediaCaptions,
title: _("popupMediaSubtitlesCaptions") title: _("popupMediaSubtitlesCaptions")
}); });
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_MEDIA_CC_OFF, ...popupMenuProps,
parentId: menuIds.POPUP_MEDIA_CC, id: MenuId.PopupMediaCaptionsOff,
parentId: MenuId.PopupMediaCaptions,
type: "radio", type: "radio",
title: _("popupMediaSubtitlesCaptionsOff") title: _("popupMediaSubtitlesCaptionsOff")
}); });
createPopupMenu({ id: menuIds.POPUP_MEDIA_SEPARATOR, type: "separator" }); browser.menus.create({
...popupMenuProps,
id: MenuId.PopupMediaSeparator,
type: "separator"
});
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_CAST, ...popupMenuProps,
id: MenuId.PopupCast,
title: _("popupCastButtonTitle"), title: _("popupCastButtonTitle"),
icons: { 16: "icons/icon.svg" } icons: { 16: "icons/icon.svg" }
}); });
createPopupMenu({ browser.menus.create({
id: menuIds.POPUP_STOP, ...popupMenuProps,
id: MenuId.PopupStop,
title: _("popupStopButtonTitle") title: _("popupStopButtonTitle")
}); });
@@ -120,14 +132,13 @@ export async function initMenus() {
const alteredOpts = ev.detail; const alteredOpts = ev.detail;
const newOpts = await options.getAll(); const newOpts = await options.getAll();
if (menuIdCastMedia && alteredOpts.includes("mediaEnabled")) { if (MenuId.CastMedia && alteredOpts.includes("mediaEnabled")) {
browser.menus.update(menuIdCastMedia, { browser.menus.update(MenuId.CastMedia, {
visible: newOpts.mediaEnabled visible: newOpts.mediaEnabled
}); });
} }
if (MenuId.CastMedia && alteredOpts.includes("localMediaEnabled")) {
if (menuIdCastMedia && alteredOpts.includes("localMediaEnabled")) { browser.menus.update(MenuId.CastMedia, {
browser.menus.update(menuIdCastMedia, {
targetUrlPatterns: newOpts.localMediaEnabled targetUrlPatterns: newOpts.localMediaEnabled
? URL_PATTERNS_ALL ? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE : URL_PATTERNS_REMOTE
@@ -138,10 +149,8 @@ export async function initMenus() {
/** Handle updating menus when shown. */ /** Handle updating menus when shown. */
async function onMenuShown(info: browser.menus._OnShownInfo) { async function onMenuShown(info: browser.menus._OnShownInfo) {
const menuIds = info.menuIds as unknown as number[];
// Only rebuild menus if whitelist menu present // Only rebuild menus if whitelist menu present
if (menuIds.includes(menuIdWhitelist as number)) { if (info.menuIds.includes(MenuId.Whitelist)) {
updateWhitelistMenu(info.pageUrl); updateWhitelistMenu(info.pageUrl);
return; return;
} }
@@ -153,53 +162,53 @@ async function onMenuClicked(
tab?: browser.tabs.Tab tab?: browser.tabs.Tab
) { ) {
// Handle whitelist menus // Handle whitelist menus
if (info.parentMenuItemId === menuIdWhitelist) { if (info.parentMenuItemId === MenuId.Whitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId); const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
if (!pattern) { if (!pattern) {
throw logger.error( throw logger.error(
`Whitelist pattern not found for menu item ID ${info.menuItemId}.` `Whitelist pattern not found for menu item ID ${info.menuItemId}.`
); );
} }
const whitelist = await options.get("siteWhitelist"); const whitelist = await options.get("siteWhitelist");
if (!whitelist.find(item => item.pattern === pattern)) { if (!whitelist.find(item => item.pattern === pattern)) {
// Add to whitelist and update options // Add to whitelist and update options
whitelist.push({ pattern, isEnabled: true }); whitelist.push({ pattern, isEnabled: true });
await options.set("siteWhitelist", whitelist); await options.set("siteWhitelist", whitelist);
} }
return;
}
if (tab?.id === undefined) {
logger.error("Menu handler tab ID not found.");
return; return;
} }
if (tab?.id !== undefined) {
switch (info.menuItemId) { switch (info.menuItemId) {
case menuIdCast: { case MenuId.Cast: {
castManager.triggerCast(tab.id, info.frameId); castManager.triggerCast(tab.id, info.frameId);
break; break;
} }
case menuIdCastMedia: case MenuId.CastMedia: {
if (info.srcUrl) { if (info.srcUrl) {
await browser.tabs.executeScript(tab.id, { const frameIds = info.frameId ? [info.frameId] : undefined;
code: stringify` await browser.scripting.executeScript({
window.mediaUrl = ${info.srcUrl}; target: { tabId: tab.id, frameIds },
window.targetElementId = ${info.targetElementId}; func: (
`, mediaUrl: string,
frameId: info.frameId targetElementId: number | undefined
) => {
(window as any).mediaUrl = mediaUrl;
(window as any).targetElementId = targetElementId;
},
args: [info.srcUrl, info.targetElementId]
}); });
await browser.scripting.executeScript({
await browser.tabs.executeScript(tab.id, { target: { tabId: tab.id, frameIds },
file: "cast/senders/media.js", files: ["cast/senders/media.js"]
frameId: info.frameId
}); });
} }
break; break;
} }
} }
}
}
/** Handles updating the whitelist menus for a given URL */ /** Handles updating the whitelist menus for a given URL */
async function updateWhitelistMenu(pageUrl?: string) { async function updateWhitelistMenu(pageUrl?: string) {
@@ -208,7 +217,7 @@ async function updateWhitelistMenu(pageUrl?: string) {
* to whitelist, so disable the menu and return. * to whitelist, so disable the menu and return.
*/ */
if (!pageUrl) { if (!pageUrl) {
browser.menus.update(menuIdWhitelist, { browser.menus.update(MenuId.Whitelist, {
enabled: false enabled: false
}); });
@@ -225,7 +234,7 @@ async function updateWhitelistMenu(pageUrl?: string) {
* menu and return. * menu and return.
*/ */
if (!urlHasOrigin) { if (!urlHasOrigin) {
browser.menus.update(menuIdWhitelist, { browser.menus.update(MenuId.Whitelist, {
enabled: false enabled: false
}); });
@@ -234,15 +243,14 @@ async function updateWhitelistMenu(pageUrl?: string) {
} }
// Enable the whitelist menu // Enable the whitelist menu
browser.menus.update(menuIdWhitelist, { browser.menus.update(MenuId.Whitelist, {
enabled: true enabled: true
}); });
for (const [menuId] of whitelistChildMenuPatterns) { for (const [menuId] of whitelistChildMenuPatterns) {
// Clear all page-specific temporary menus // Clear all page-specific temporary menus
if (menuId !== menuIdWhitelistRecommended) { if (menuId !== MenuId.WhitelistRecommended)
browser.menus.remove(menuId); browser.menus.remove(menuId);
}
whitelistChildMenuPatterns.delete(menuId); whitelistChildMenuPatterns.delete(menuId);
} }
@@ -262,22 +270,23 @@ async function updateWhitelistMenu(pageUrl?: string) {
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`; const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
// Update recommended menu item // Update recommended menu item
browser.menus.update(menuIdWhitelistRecommended, { browser.menus.update(MenuId.WhitelistRecommended, {
title: _("contextAddToWhitelistRecommended", patternRecommended) title: _("contextAddToWhitelistRecommended", patternRecommended)
}); });
whitelistChildMenuPatterns.set( whitelistChildMenuPatterns.set(
menuIdWhitelistRecommended, MenuId.WhitelistRecommended,
patternRecommended patternRecommended
); );
if (url.search) { if (url.search) {
const whitelistSearchMenuId = browser.menus.create({ whitelistChildMenuPatterns.set(
browser.menus.create({
id: MenuId.WhitelistSearch,
title: _("contextAddToWhitelistAdvancedAdd", patternSearch), title: _("contextAddToWhitelistAdvancedAdd", patternSearch),
parentId: menuIdWhitelist parentId: MenuId.Whitelist
}); }),
patternSearch
whitelistChildMenuPatterns.set(whitelistSearchMenuId, patternSearch); );
} }
/** /**
@@ -300,9 +309,11 @@ async function updateWhitelistMenu(pageUrl?: string) {
const pattern = `${portlessOrigin}/${partialPath}/*`; const pattern = `${portlessOrigin}/${partialPath}/*`;
const partialPathMenuId = browser.menus.create({ const partialPathMenuId = `${MenuId.WhitelistPath}-${i}`;
browser.menus.create({
id: partialPathMenuId,
title: _("contextAddToWhitelistAdvancedAdd", pattern), title: _("contextAddToWhitelistAdvancedAdd", pattern),
parentId: menuIdWhitelist parentId: MenuId.Whitelist
}); });
whitelistChildMenuPatterns.set(partialPathMenuId, pattern); whitelistChildMenuPatterns.set(partialPathMenuId, pattern);
@@ -310,36 +321,38 @@ async function updateWhitelistMenu(pageUrl?: string) {
} }
} }
const wildcardProtocolMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardProtocol),
parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set( whitelistChildMenuPatterns.set(
wildcardProtocolMenuId, browser.menus.create({
id: MenuId.WhitelistWildcardProtocol,
title: _(
"contextAddToWhitelistAdvancedAdd",
patternWildcardProtocol
),
parentId: MenuId.Whitelist
}),
patternWildcardProtocol patternWildcardProtocol
); );
const wildcardSubdomainMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardSubdomain),
parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set( whitelistChildMenuPatterns.set(
wildcardSubdomainMenuId, browser.menus.create({
id: MenuId.WhitelistWildcardSubdomain,
title: _(
"contextAddToWhitelistAdvancedAdd",
patternWildcardSubdomain
),
parentId: MenuId.Whitelist
}),
patternWildcardSubdomain patternWildcardSubdomain
); );
whitelistChildMenuPatterns.set(
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({ browser.menus.create({
id: MenuId.WhitelistWildcardProtocolAndSubdomain,
title: _( title: _(
"contextAddToWhitelistAdvancedAdd", "contextAddToWhitelistAdvancedAdd",
patternWildcardProtocolAndSubdomain patternWildcardProtocolAndSubdomain
), ),
parentId: menuIdWhitelist parentId: MenuId.Whitelist
}); }),
whitelistChildMenuPatterns.set(
wildcardProtocolAndSubdomainMenuId,
patternWildcardProtocolAndSubdomain patternWildcardProtocolAndSubdomain
); );

View File

@@ -194,20 +194,19 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
} }
} }
await browser.tabs.executeScript(details.tabId, { await browser.scripting.executeScript({
code: ` target: { tabId: details.tabId, frameIds: [details.frameId] },
window.isFramework = ${ func: (isFramework: boolean) => {
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL (window as any).isFramework = isFramework;
}; },
`, args: [details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL],
frameId: details.frameId, injectImmediately: true
runAt: "document_start"
}); });
await browser.tabs.executeScript(details.tabId, { await browser.scripting.executeScript({
file: "cast/contentBridge.js", target: { tabId: details.tabId, frameIds: [details.frameId] },
frameId: details.frameId, files: ["cast/contentBridge.js"],
runAt: "document_start" injectImmediately: true
}); });
return { return {
@@ -255,15 +254,23 @@ async function registerSiteWhitelist() {
["blocking", "requestHeaders"] ["blocking", "requestHeaders"]
); );
browser.contentScripts.register({ try {
await browser.scripting.unregisterContentScripts({
ids: ["whitelist-content"]
});
} catch {}
await browser.scripting.registerContentScripts([
{
id: "whitelist-content",
matches: siteWhitelist.map(item => item.pattern), matches: siteWhitelist.map(item => item.pattern),
js: [{ file: "cast/contentInitial.js" }], js: ["cast/contentInitial.js"],
runAt: "document_start", runAt: "document_start",
allFrames: true allFrames: true
}); }
]);
} }
function unregisterSiteWhitelist() { async function unregisterSiteWhitelist() {
browser.webRequest.onBeforeSendHeaders.removeListener( browser.webRequest.onBeforeSendHeaders.removeListener(
onWhitelistedBeforeSendHeaders onWhitelistedBeforeSendHeaders
); );
@@ -271,4 +278,9 @@ function unregisterSiteWhitelist() {
onWhitelistedChildBeforeSendHeaders onWhitelistedChildBeforeSendHeaders
); );
browser.webRequest.onBeforeRequest.removeListener(onBeforeCastSDKRequest); browser.webRequest.onBeforeRequest.removeListener(onBeforeCastSDKRequest);
try {
await browser.scripting.unregisterContentScripts({
ids: ["whitelist-content"]
});
} catch {}
} }

View File

@@ -23,7 +23,13 @@ if (document.currentScript) {
); );
if (currentScriptParams.get("loadCastFramework") === "1") { if (currentScriptParams.get("loadCastFramework") === "1") {
frameworkScriptPromise = loadScript(CAST_FRAMEWORK_SCRIPT_URL); frameworkScriptPromise = new Promise((resolve, reject) => {
const scriptEl = document.createElement("script");
scriptEl.src = CAST_FRAMEWORK_SCRIPT_URL;
(document.head ?? document.documentElement).append(scriptEl);
scriptEl.addEventListener("load", () => resolve(scriptEl));
scriptEl.addEventListener("error", () => reject());
});
frameworkScriptPromise.catch(() => { frameworkScriptPromise.catch(() => {
logger.error("Failed to load CAF script!"); logger.error("Failed to load CAF script!");
}); });

View File

@@ -17,7 +17,6 @@ let existingInstance = new CastSDK();
export default existingInstance; export default existingInstance;
interface EnsureInitOpts { interface EnsureInitOpts {
contextTabId?: number;
/** Skip receiver selection. */ /** Skip receiver selection. */
receiverDevice?: ReceiverDevice; receiverDevice?: ReceiverDevice;
} }
@@ -32,7 +31,7 @@ interface EnsureInitOpts {
* provides a messaging port so consumers of this module can communicate * provides a messaging port so consumers of this module can communicate
* with the cast manager. * with the cast manager.
*/ */
export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> { export function ensureInit(opts?: EnsureInitOpts): Promise<CastPort> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// If already initialized // If already initialized
if (existingPort) { if (existingPort) {
@@ -40,67 +39,6 @@ export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> {
existingInstance = new CastSDK(); existingInstance = new CastSDK();
} }
/**
* If imported into a background script context, the location
* will be the internal extension URL, whereas in a content
* script, it will be the content page URL.
*/
if (
window.location.protocol === "moz-extension:" &&
window.location.pathname === "_generated_background_page.html"
) {
const { default: castManager } = await import(
"../background/castManager"
);
/**
* port1 will handle cast manager messages.
* port2 will handle cast instance messages.
*/
const { port1: managerPort, port2: instancePort } =
new MessageChannel();
/**
* Provide cast manager with a port to send messages to
* cast instance.
*/
if (opts.contextTabId) {
await castManager.createInstance(instancePort, {
tabId: opts.contextTabId,
frameId: 0
});
} else {
await castManager.createInstance(instancePort);
}
// cast manager -> cast instance
managerPort.addEventListener("message", ev => {
const message = ev.data as Message;
if (message.subject === "cast:instanceCreated") {
if (message.data.isAvailable) {
resolve(existingPort);
} else {
reject();
}
}
pageMessaging.extension.sendMessage(message);
});
managerPort.start();
// Cast instance -> cast manager
pageMessaging.extension.addListener(message => {
// Skip receiver selection
if (opts.receiverDevice) {
message = rewriteTrustedRequestSession(
message,
opts.receiverDevice
);
}
managerPort.postMessage(message);
});
} else {
const managerPort = messaging.connect({ name: "trusted-cast" }); const managerPort = messaging.connect({ name: "trusted-cast" });
// Cast manager -> cast instance // Cast manager -> cast instance
@@ -119,7 +57,7 @@ export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> {
// Cast instance -> cast manager // Cast instance -> cast manager
pageMessaging.extension.addListener(message => { pageMessaging.extension.addListener(message => {
// Skip receiver selection // Skip receiver selection
if (opts.receiverDevice) { if (opts?.receiverDevice) {
message = rewriteTrustedRequestSession( message = rewriteTrustedRequestSession(
message, message,
opts.receiverDevice opts.receiverDevice
@@ -134,7 +72,6 @@ export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> {
}); });
existingPort = pageMessaging.page.messagePort; existingPort = pageMessaging.page.messagePort;
}
}); });
} }

View File

@@ -14,7 +14,6 @@ const logger = new Logger("fx_cast [media sender]");
interface MediaSenderOpts { interface MediaSenderOpts {
mediaUrl: string; mediaUrl: string;
contextTabId?: number;
mediaElement?: HTMLMediaElement; mediaElement?: HTMLMediaElement;
} }
@@ -22,7 +21,6 @@ export default class MediaSender {
private port?: CastPort; private port?: CastPort;
private mediaUrl: string; private mediaUrl: string;
private contextTabId?: number;
/** Target media element if loaded as a content script. */ /** Target media element if loaded as a content script. */
private mediaElement?: HTMLMediaElement; private mediaElement?: HTMLMediaElement;
@@ -38,7 +36,6 @@ export default class MediaSender {
constructor(opts: MediaSenderOpts) { constructor(opts: MediaSenderOpts) {
this.mediaUrl = opts.mediaUrl; this.mediaUrl = opts.mediaUrl;
this.contextTabId = opts.contextTabId;
this.mediaElement = opts.mediaElement; this.mediaElement = opts.mediaElement;
this.init(); this.init();
@@ -51,7 +48,7 @@ export default class MediaSender {
private async init() { private async init() {
try { try {
this.port = await ensureInit({ contextTabId: this.contextTabId }); this.port = await ensureInit();
} catch (err) { } catch (err) {
logger.error("Failed to initialize cast API", err); logger.error("Failed to initialize cast API", err);
} }

View File

@@ -81,20 +81,3 @@ declare namespace browser.events {
removeListener(...args: any[]): void | Promise<void>; removeListener(...args: any[]): void | Promise<void>;
} }
} }
declare namespace browser.runtime {
interface Port {
error?: { message: string };
/**
* https://git.io/fjmzb
* addListener cb `() => void` is wrong
*/
onMessage: browser.events.Event;
}
function connect(connectInfo: {
name?: string;
includeTlsChannelId?: boolean;
}): browser.runtime.Port;
}

View File

@@ -7,26 +7,6 @@ export function getNextEllipsis(ellipsis: string): string {
return ""; return "";
} }
/**
* Template literal tag function, JSON-encodes substitutions.
*/
export function stringify(
templateStrings: TemplateStringsArray,
...substitutions: unknown[]
) {
let formattedString = "";
for (const templateString of templateStrings) {
if (formattedString) {
formattedString += JSON.stringify(substitutions.shift());
}
formattedString += templateString;
}
return formattedString;
}
export function loadScript( export function loadScript(
scriptUrl: string, scriptUrl: string,
doc: Document = document doc: Document = document

View File

@@ -7,14 +7,14 @@
"url": "https://matt.tf/" "url": "https://matt.tf/"
}, },
"applications": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "fx_cast@matt.tf", "id": "fx_cast@matt.tf",
"strict_min_version": "64.0", "strict_min_version": "109.0",
"update_url": "https://hensm.github.io/fx_cast/updates.json" "update_url": "https://hensm.github.io/fx_cast/updates.json"
} }
}, },
"browser_action": { "action": {
"theme_icons": [ "theme_icons": [
{ {
"light": "icons/cast-default-light.svg", "light": "icons/cast-default-light.svg",
@@ -35,7 +35,7 @@
"96": "icons/icon.svg" "96": "icons/icon.svg"
}, },
"manifest_version": 2, "manifest_version": 3,
"options_ui": { "options_ui": {
"page": "ui/options/index.html" "page": "ui/options/index.html"
@@ -46,12 +46,18 @@
"menus.overrideContext", "menus.overrideContext",
"nativeMessaging", "nativeMessaging",
"notifications", "notifications",
"scripting",
"storage", "storage",
"tabs", "tabs",
"webNavigation", "webNavigation",
"webRequest", "webRequest",
"webRequestBlocking", "webRequestBlocking"
"<all_urls>"
], ],
"web_accessible_resources": ["cast/content.js"] "host_permissions": ["<all_urls>"],
"web_accessible_resources": [
{
"resources": ["cast/content.js"],
"matches": ["<all_urls>"]
}
]
} }

View File

@@ -1,30 +1,21 @@
export const POPUP_CAST = "popupCastMenuId"; export enum MenuId {
export const POPUP_STOP = "popupStopMenuId"; Cast = "cast",
CastMedia = "cast_media",
export const POPUP_MEDIA_SEPARATOR = "popupMediaSeparatorMenuId"; Whitelist = "whitelist",
export const POPUP_MEDIA_PLAY_PAUSE = "popupMediaPlayPauseMenuId"; WhitelistRecommended = "whitelist_recommended",
export const POPUP_MEDIA_MUTE = "popupMediaMuteMenuId"; WhitelistSeparator = "whitelist_separator",
export const POPUP_MEDIA_SKIP_PREVIOUS = "popupMediaSkipPreviousMenuId"; WhitelistSearch = "whitelist_search",
export const POPUP_MEDIA_SKIP_NEXT = "popupMediaSkipNextMenuId"; WhitelistPath = "whitelist_path",
export const POPUP_MEDIA_CC = "popupMediaSubtitlesCaptionsMenuId"; WhitelistWildcardProtocol = "whitelist_wildcard_protocol",
export const POPUP_MEDIA_CC_OFF = "popupMediaSubtitlesCaptionsOffMenuId"; WhitelistWildcardSubdomain = "whitelist_wildcard_subdomain",
WhitelistWildcardProtocolAndSubdomain = "whitelist_wildcard_protocol_and_subdomain",
export const receiverMenuIds = [ PopupCast = "popup_cast",
POPUP_CAST, PopupStop = "popup_stop",
POPUP_STOP, PopupMediaSeparator = "popup_media_separator",
POPUP_MEDIA_SEPARATOR, PopupMediaPlayPause = "popup_media_play_pause",
POPUP_MEDIA_PLAY_PAUSE, PopupMediaMute = "popup_media_mute",
POPUP_MEDIA_MUTE, PopupMediaSkipPrevious = "popup_media_skip_previous",
POPUP_MEDIA_SKIP_PREVIOUS, PopupMediaSkipNext = "popup_media_skip_next",
POPUP_MEDIA_SKIP_NEXT, PopupMediaCaptions = "popup_media_captions",
POPUP_MEDIA_CC PopupMediaCaptionsOff = "popup_media_captions_off"
]; }
export const mediaMenuIds = [
POPUP_MEDIA_SEPARATOR,
POPUP_MEDIA_PLAY_PAUSE,
POPUP_MEDIA_MUTE,
POPUP_MEDIA_SKIP_PREVIOUS,
POPUP_MEDIA_SKIP_NEXT,
POPUP_MEDIA_CC
];

View File

@@ -56,8 +56,10 @@
async function onEditKeydown(ev: KeyboardEvent) { async function onEditKeydown(ev: KeyboardEvent) {
key: switch (ev.key) { key: switch (ev.key) {
// Finish editing on enter // Finish editing on enter, stopping propagation so we don't
// immediately begin editing again.
case "Enter": case "Enter":
ev.stopPropagation();
finishEditing(); finishEditing();
break; break;
@@ -145,10 +147,20 @@
bind:checked={item.isEnabled} bind:checked={item.isEnabled}
/> />
{/if} {/if}
<div <div
class="whitelist__title" class="whitelist__title"
on:dblclick={() => beginEditing(i)} on:dblclick={() => beginEditing(i)}
on:keydown={ev => {
switch (ev.key) {
case "Enter":
case " ":
ev.preventDefault();
beginEditing(i);
break;
}
}}
role="button"
tabindex="0"
> >
{#if isEditingItem} {#if isEditingItem}
<input <input

View File

@@ -10,6 +10,7 @@
body { body {
background: var(--box-background) !important; background: var(--box-background) !important;
color: var(--box-color) !important; color: var(--box-color) !important;
font-family: system-ui;
font-size: 13px; font-size: 13px;
overflow-y: hidden; overflow-y: hidden;
padding: 20px 10px; padding: 20px 10px;
@@ -417,7 +418,7 @@ fieldset:disabled .option__description {
} }
.whitelist__input-pattern { .whitelist__input-pattern {
font: inherit; font-family: monospace;
width: -moz-available; width: -moz-available;
} }

View File

@@ -6,7 +6,7 @@
import options, { type Options } from "../../lib/options"; import options, { type Options } from "../../lib/options";
import { RemoteMatchPattern } from "../../lib/matchPattern"; import { RemoteMatchPattern } from "../../lib/matchPattern";
import { receiverMenuIds } from "../../menuIds"; import { MenuId } from "../../menuIds";
import { import {
type ReceiverDevice, type ReceiverDevice,
@@ -274,6 +274,17 @@
/** Device ID associated with the last receiver menu that was shown. */ /** Device ID associated with the last receiver menu that was shown. */
let lastMenuShownDeviceId: string; let lastMenuShownDeviceId: string;
const receiverMenuIds = [
MenuId.PopupCast,
MenuId.PopupStop,
MenuId.PopupMediaSeparator,
MenuId.PopupMediaPlayPause,
MenuId.PopupMediaMute,
MenuId.PopupMediaSkipPrevious,
MenuId.PopupMediaSkipNext,
MenuId.PopupMediaCaptions
];
/** Handle show events for receiver context menus. */ /** Handle show events for receiver context menus. */
function onMenuShown(info: browser.menus._OnShownInfo) { function onMenuShown(info: browser.menus._OnShownInfo) {
// Only handle menu events on this page // Only handle menu events on this page
@@ -287,10 +298,8 @@
const receiverElement = targetElement.closest(".receiver"); const receiverElement = targetElement.closest(".receiver");
if (!receiverElement) { if (!receiverElement) {
for (const menuId of receiverMenuIds) { for (const menuId of receiverMenuIds)
browser.menus.update(menuId, { visible: false }); browser.menus.update(menuId, { visible: false });
}
browser.menus.refresh(); browser.menus.refresh();
} }
} }

View File

@@ -4,10 +4,13 @@
import type { Options } from "../../lib/options"; import type { Options } from "../../lib/options";
import { type ReceiverDevice, ReceiverDeviceCapabilities } from "../../types"; import {
type ReceiverDevice,
ReceiverDeviceCapabilities
} from "../../types";
import type { Port } from "../../messaging"; import type { Port } from "../../messaging";
import * as menuIds from "../../menuIds"; import { MenuId } from "../../menuIds";
import type { Volume } from "../../cast/sdk/classes"; import type { Volume } from "../../cast/sdk/classes";
import { PlayerState, TrackType } from "../../cast/sdk/media/enums"; import { PlayerState, TrackType } from "../../cast/sdk/media/enums";
@@ -169,7 +172,7 @@
lastMenuShownDeviceId = device.id; lastMenuShownDeviceId = device.id;
browser.menus.update(menuIds.POPUP_CAST, { browser.menus.update(MenuId.PopupCast, {
visible: true, visible: true,
title: _("popupCastMenuTitle", device.friendlyName), title: _("popupCastMenuTitle", device.friendlyName),
enabled: enabled:
@@ -181,7 +184,7 @@
isAnyMediaTypeAvailable isAnyMediaTypeAvailable
}); });
browser.menus.update(menuIds.POPUP_STOP, { browser.menus.update(MenuId.PopupStop, {
visible: !!application && !application.isIdleScreen, visible: !!application && !application.isIdleScreen,
title: application?.displayName title: application?.displayName
? _("popupStopMenuTitle", [ ? _("popupStopMenuTitle", [
@@ -234,10 +237,10 @@
if (!isTarget(info)) return; if (!isTarget(info)) return;
switch (info.menuItemId) { switch (info.menuItemId) {
case menuIds.POPUP_MEDIA_PLAY_PAUSE: case MenuId.PopupMediaPlayPause:
handleMediaPlayPause(); handleMediaPlayPause();
break; break;
case menuIds.POPUP_MEDIA_MUTE: case MenuId.PopupMediaMute:
if ( if (
!device.status?.volume.muted && !device.status?.volume.muted &&
device.status?.volume.level === 0 device.status?.volume.level === 0
@@ -247,24 +250,24 @@
handleVolumeChange({ muted: !device.status?.volume.muted }); handleVolumeChange({ muted: !device.status?.volume.muted });
} }
break; break;
case menuIds.POPUP_MEDIA_SKIP_PREVIOUS: case MenuId.PopupMediaSkipPrevious:
handleMediaSkipPrevious(); handleMediaSkipPrevious();
break; break;
case menuIds.POPUP_MEDIA_SKIP_NEXT: case MenuId.PopupMediaSkipNext:
handleMediaSkipNext(); handleMediaSkipNext();
break; break;
case menuIds.POPUP_CAST: case MenuId.PopupCast:
isConnecting = true; isConnecting = true;
dispatch("cast", { device }); dispatch("cast", { device });
break; break;
case menuIds.POPUP_STOP: case MenuId.PopupStop:
dispatch("stop", { device }); dispatch("stop", { device });
break; break;
} }
// Handle caption submenu items // Handle caption submenu items
if (info.parentMenuItemId === menuIds.POPUP_MEDIA_CC) { if (info.parentMenuItemId === MenuId.PopupMediaCaptions) {
// Filter and append active track IDs array // Filter and append active track IDs array
if (!mediaStatus?.activeTrackIds) return; if (!mediaStatus?.activeTrackIds) return;
const activeTrackIds = mediaStatus.activeTrackIds.filter( const activeTrackIds = mediaStatus.activeTrackIds.filter(
@@ -284,6 +287,15 @@
browser.menus.overrideContext({ showDefaults: false }); browser.menus.overrideContext({ showDefaults: false });
} }
const mediaMenuIds = [
MenuId.PopupMediaSeparator,
MenuId.PopupMediaPlayPause,
MenuId.PopupMediaMute,
MenuId.PopupMediaSkipPrevious,
MenuId.PopupMediaSkipNext,
MenuId.PopupMediaCaptions
];
/** Updates media menu items from media status. */ /** Updates media menu items from media status. */
function updateMediaMenus(shownMenuIds: (number | string)[] = []) { function updateMediaMenus(shownMenuIds: (number | string)[] = []) {
// Clear caption submenu for re-build // Clear caption submenu for re-build
@@ -306,19 +318,18 @@
// Hide all media menu items if no media status // Hide all media menu items if no media status
if (!mediaStatus) { if (!mediaStatus) {
for (const menuId of menuIds.mediaMenuIds) { for (const menuId of mediaMenuIds)
browser.menus.update(menuId, { visible: false }); browser.menus.update(menuId, { visible: false });
}
return; return;
} }
browser.menus.update(menuIds.POPUP_MEDIA_SEPARATOR, { browser.menus.update(MenuId.PopupMediaSeparator, {
visible: true visible: true
}); });
// Play/pause menu item // Play/pause menu item
if (mediaStatus.supportedMediaCommands & _MediaCommand.PAUSE) { if (mediaStatus.supportedMediaCommands & _MediaCommand.PAUSE) {
browser.menus.update(menuIds.POPUP_MEDIA_PLAY_PAUSE, { browser.menus.update(MenuId.PopupMediaPlayPause, {
visible: true, visible: true,
title: title:
mediaStatus.playerState === PlayerState.PLAYING || mediaStatus.playerState === PlayerState.PLAYING ||
@@ -330,7 +341,7 @@
mediaStatus.playerState === PlayerState.PAUSED mediaStatus.playerState === PlayerState.PAUSED
}); });
} else { } else {
browser.menus.update(menuIds.POPUP_MEDIA_PLAY_PAUSE, { browser.menus.update(MenuId.PopupMediaPlayPause, {
visible: false visible: false
}); });
} }
@@ -339,24 +350,24 @@
if (device.status?.volume) { if (device.status?.volume) {
const volume = device.status.volume; const volume = device.status.volume;
browser.menus.update(menuIds.POPUP_MEDIA_MUTE, { browser.menus.update(MenuId.PopupMediaMute, {
visible: true, visible: true,
title: _("popupMediaMute"), title: _("popupMediaMute"),
checked: volume.muted || volume.level === 0, checked: volume.muted || volume.level === 0,
enabled: "muted" in volume enabled: "muted" in volume
}); });
} else { } else {
browser.menus.update(menuIds.POPUP_MEDIA_MUTE, { browser.menus.update(MenuId.PopupMediaMute, {
visible: false visible: false
}); });
} }
browser.menus.update(menuIds.POPUP_MEDIA_SKIP_PREVIOUS, { browser.menus.update(MenuId.PopupMediaSkipPrevious, {
visible: !!( visible: !!(
mediaStatus.supportedMediaCommands & _MediaCommand.QUEUE_PREV mediaStatus.supportedMediaCommands & _MediaCommand.QUEUE_PREV
) )
}); });
browser.menus.update(menuIds.POPUP_MEDIA_SKIP_NEXT, { browser.menus.update(MenuId.PopupMediaSkipNext, {
visible: !!( visible: !!(
mediaStatus.supportedMediaCommands & _MediaCommand.QUEUE_NEXT mediaStatus.supportedMediaCommands & _MediaCommand.QUEUE_NEXT
) )
@@ -367,8 +378,8 @@
textTracks?.length && textTracks?.length &&
mediaStatus.supportedMediaCommands & _MediaCommand.EDIT_TRACKS mediaStatus.supportedMediaCommands & _MediaCommand.EDIT_TRACKS
) { ) {
browser.menus.update(menuIds.POPUP_MEDIA_CC, { visible: true }); browser.menus.update(MenuId.PopupMediaCaptions, { visible: true });
browser.menus.update(menuIds.POPUP_MEDIA_CC_OFF, { browser.menus.update(MenuId.PopupMediaCaptionsOff, {
visible: true, visible: true,
checked: activeTextTrackId === undefined checked: activeTextTrackId === undefined
}); });
@@ -377,7 +388,7 @@
const menuId = browser.menus.create({ const menuId = browser.menus.create({
id: `subtitle-${track.trackId}`, id: `subtitle-${track.trackId}`,
title: track.name ?? track.trackId.toString(), title: track.name ?? track.trackId.toString(),
parentId: menuIds.POPUP_MEDIA_CC, parentId: MenuId.PopupMediaCaptions,
type: "radio", type: "radio",
checked: track.trackId === activeTextTrackId checked: track.trackId === activeTextTrackId
}); });
@@ -385,7 +396,7 @@
captionSubmenus.set(menuId, track.trackId); captionSubmenus.set(menuId, track.trackId);
} }
} else { } else {
browser.menus.update(menuIds.POPUP_MEDIA_CC, { browser.menus.update(MenuId.PopupMediaCaptions, {
visible: false visible: false
}); });
} }