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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ let existingInstance = new CastSDK();
export default existingInstance;
interface EnsureInitOpts {
contextTabId?: number;
/** Skip receiver selection. */
receiverDevice?: ReceiverDevice;
}
@@ -32,7 +31,7 @@ interface EnsureInitOpts {
* provides a messaging port so consumers of this module can communicate
* with the cast manager.
*/
export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> {
export function ensureInit(opts?: EnsureInitOpts): Promise<CastPort> {
return new Promise(async (resolve, reject) => {
// If already initialized
if (existingPort) {
@@ -40,101 +39,39 @@ export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> {
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"
);
const managerPort = messaging.connect({ name: "trusted-cast" });
/**
* 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.onMessage.addListener(message => {
if (message.subject === "cast:instanceCreated") {
if (message.data.isAvailable) {
resolve(pageMessaging.page.messagePort);
} else {
reject();
}
}
// 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);
});
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
);
}
// Cast instance -> cast manager
pageMessaging.extension.addListener(message => {
// Skip receiver selection
if (opts.receiverDevice) {
message = rewriteTrustedRequestSession(
message,
opts.receiverDevice
);
}
managerPort.postMessage(message);
});
managerPort.postMessage(message);
});
} else {
const managerPort = messaging.connect({ name: "trusted-cast" });
managerPort.onDisconnect.addListener(() => {
pageMessaging.extension.close();
});
// Cast manager -> cast instance
managerPort.onMessage.addListener(message => {
if (message.subject === "cast:instanceCreated") {
if (message.data.isAvailable) {
resolve(pageMessaging.page.messagePort);
} else {
reject();
}
}
pageMessaging.extension.sendMessage(message);
});
// Cast instance -> cast manager
pageMessaging.extension.addListener(message => {
// Skip receiver selection
if (opts.receiverDevice) {
message = rewriteTrustedRequestSession(
message,
opts.receiverDevice
);
}
managerPort.postMessage(message);
});
managerPort.onDisconnect.addListener(() => {
pageMessaging.extension.close();
});
existingPort = pageMessaging.page.messagePort;
}
existingPort = pageMessaging.page.messagePort;
});
}

View File

@@ -14,7 +14,6 @@ const logger = new Logger("fx_cast [media sender]");
interface MediaSenderOpts {
mediaUrl: string;
contextTabId?: number;
mediaElement?: HTMLMediaElement;
}
@@ -22,7 +21,6 @@ export default class MediaSender {
private port?: CastPort;
private mediaUrl: string;
private contextTabId?: number;
/** Target media element if loaded as a content script. */
private mediaElement?: HTMLMediaElement;
@@ -38,7 +36,6 @@ export default class MediaSender {
constructor(opts: MediaSenderOpts) {
this.mediaUrl = opts.mediaUrl;
this.contextTabId = opts.contextTabId;
this.mediaElement = opts.mediaElement;
this.init();
@@ -51,7 +48,7 @@ export default class MediaSender {
private async init() {
try {
this.port = await ensureInit({ contextTabId: this.contextTabId });
this.port = await ensureInit();
} catch (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>;
}
}
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 "";
}
/**
* 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(
scriptUrl: string,
doc: Document = document

View File

@@ -7,14 +7,14 @@
"url": "https://matt.tf/"
},
"applications": {
"browser_specific_settings": {
"gecko": {
"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"
}
},
"browser_action": {
"action": {
"theme_icons": [
{
"light": "icons/cast-default-light.svg",
@@ -35,7 +35,7 @@
"96": "icons/icon.svg"
},
"manifest_version": 2,
"manifest_version": 3,
"options_ui": {
"page": "ui/options/index.html"
@@ -46,12 +46,18 @@
"menus.overrideContext",
"nativeMessaging",
"notifications",
"scripting",
"storage",
"tabs",
"webNavigation",
"webRequest",
"webRequestBlocking",
"<all_urls>"
"webRequestBlocking"
],
"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 const POPUP_STOP = "popupStopMenuId";
export const POPUP_MEDIA_SEPARATOR = "popupMediaSeparatorMenuId";
export const POPUP_MEDIA_PLAY_PAUSE = "popupMediaPlayPauseMenuId";
export const POPUP_MEDIA_MUTE = "popupMediaMuteMenuId";
export const POPUP_MEDIA_SKIP_PREVIOUS = "popupMediaSkipPreviousMenuId";
export const POPUP_MEDIA_SKIP_NEXT = "popupMediaSkipNextMenuId";
export const POPUP_MEDIA_CC = "popupMediaSubtitlesCaptionsMenuId";
export const POPUP_MEDIA_CC_OFF = "popupMediaSubtitlesCaptionsOffMenuId";
export const receiverMenuIds = [
POPUP_CAST,
POPUP_STOP,
POPUP_MEDIA_SEPARATOR,
POPUP_MEDIA_PLAY_PAUSE,
POPUP_MEDIA_MUTE,
POPUP_MEDIA_SKIP_PREVIOUS,
POPUP_MEDIA_SKIP_NEXT,
POPUP_MEDIA_CC
];
export const mediaMenuIds = [
POPUP_MEDIA_SEPARATOR,
POPUP_MEDIA_PLAY_PAUSE,
POPUP_MEDIA_MUTE,
POPUP_MEDIA_SKIP_PREVIOUS,
POPUP_MEDIA_SKIP_NEXT,
POPUP_MEDIA_CC
];
export enum MenuId {
Cast = "cast",
CastMedia = "cast_media",
Whitelist = "whitelist",
WhitelistRecommended = "whitelist_recommended",
WhitelistSeparator = "whitelist_separator",
WhitelistSearch = "whitelist_search",
WhitelistPath = "whitelist_path",
WhitelistWildcardProtocol = "whitelist_wildcard_protocol",
WhitelistWildcardSubdomain = "whitelist_wildcard_subdomain",
WhitelistWildcardProtocolAndSubdomain = "whitelist_wildcard_protocol_and_subdomain",
PopupCast = "popup_cast",
PopupStop = "popup_stop",
PopupMediaSeparator = "popup_media_separator",
PopupMediaPlayPause = "popup_media_play_pause",
PopupMediaMute = "popup_media_mute",
PopupMediaSkipPrevious = "popup_media_skip_previous",
PopupMediaSkipNext = "popup_media_skip_next",
PopupMediaCaptions = "popup_media_captions",
PopupMediaCaptionsOff = "popup_media_captions_off"
}

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
import options, { type Options } from "../../lib/options";
import { RemoteMatchPattern } from "../../lib/matchPattern";
import { receiverMenuIds } from "../../menuIds";
import { MenuId } from "../../menuIds";
import {
type ReceiverDevice,
@@ -274,6 +274,17 @@
/** Device ID associated with the last receiver menu that was shown. */
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. */
function onMenuShown(info: browser.menus._OnShownInfo) {
// Only handle menu events on this page
@@ -287,10 +298,8 @@
const receiverElement = targetElement.closest(".receiver");
if (!receiverElement) {
for (const menuId of receiverMenuIds) {
for (const menuId of receiverMenuIds)
browser.menus.update(menuId, { visible: false });
}
browser.menus.refresh();
}
}

View File

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