Refactor menus module

This commit is contained in:
hensm
2022-04-18 04:41:29 +01:00
parent abf657c0d4
commit 3686340f25
6 changed files with 146 additions and 160 deletions

View File

@@ -16,8 +16,8 @@ const _ = browser.i18n.getMessage;
/** /**
* On install, set the default options before initializing the * On install, set the default options before initializing the
* extension. On update, handle any unset values and set to * extension. On update, handle any unset values and set to the new
* the new defaults. * defaults.
*/ */
browser.runtime.onInstalled.addListener(async details => { browser.runtime.onInstalled.addListener(async details => {
switch (details.reason) { switch (details.reason) {
@@ -39,9 +39,9 @@ browser.runtime.onInstalled.addListener(async details => {
}); });
/** /**
* Checks whether the bridge can be reached and is compatible * Checks whether the bridge can be reached and is compatible with the
* with the current version of the extension. If not, triggers * current version of the extension. If not, triggers a notification
* a notification with the appropriate info. * with the appropriate info.
*/ */
async function notifyBridgeCompat() { async function notifyBridgeCompat() {
logger.info("checking for bridge..."); logger.info("checking for bridge...");
@@ -89,9 +89,8 @@ async function init() {
} }
/** /**
* If options haven't been set yet, we can't properly * If options haven't been set yet, we can't properly initialize,
* initialize, so wait until init is called again in the * so wait until init is called again in the onInstalled listener.
* onInstalled listener.
*/ */
if (!(await options.getAll())) { if (!(await options.getAll())) {
return; return;
@@ -109,9 +108,9 @@ async function init() {
await initWhitelist(); await initWhitelist();
/** /**
* When the browser action is clicked, open a receiver * When the browser action is clicked, open a receiver selector and
* selector and load a sender for the response. The * load a sender for the response. The mirroring sender is loaded
* mirroring sender is loaded into the current tab at the * into the current tab at the
* top-level frame. * top-level frame.
*/ */
browser.browserAction.onClicked.addListener(async tab => { browser.browserAction.onClicked.addListener(async tab => {

View File

@@ -6,6 +6,7 @@ import options from "../lib/options";
import { stringify } from "../lib/utils"; import { stringify } from "../lib/utils";
import { import {
ReceiverSelection,
ReceiverSelectionActionType, ReceiverSelectionActionType,
ReceiverSelectorMediaType ReceiverSelectorMediaType
} from "./receiverSelector"; } from "./receiverSelector";
@@ -25,12 +26,14 @@ const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE];
type MenuId = string | number; type MenuId = string | number;
let menuIdCast: MenuId; let menuIdCast: MenuId;
let menuIdMediaCast: MenuId; let menuIdCastMedia: MenuId;
let menuIdWhitelist: MenuId; let menuIdWhitelist: MenuId;
let menuIdWhitelistRecommended: MenuId; let menuIdWhitelistRecommended: MenuId;
/** Match patterns for the whitelist option menus. */
const whitelistChildMenuPatterns = new Map<MenuId, string>(); const whitelistChildMenuPatterns = new Map<MenuId, string>();
/** Handles initial menu setup. */
export async function initMenus() { export async function initMenus() {
logger.info("init (menus)"); logger.info("init (menus)");
@@ -43,7 +46,7 @@ export async function initMenus() {
}); });
// <video>/<audio> "Cast..." context menu item // <video>/<audio> "Cast..." context menu item
menuIdMediaCast = browser.menus.create({ menuIdCastMedia = browser.menus.create({
contexts: ["audio", "video", "image"], contexts: ["audio", "video", "image"],
title: _("contextCast"), title: _("contextCast"),
visible: opts.mediaEnabled, visible: opts.mediaEnabled,
@@ -67,9 +70,47 @@ export async function initMenus() {
type: "separator", type: "separator",
parentId: menuIdWhitelist parentId: menuIdWhitelist
}); });
browser.menus.onShown.addListener(onMenuShown);
browser.menus.onClicked.addListener(onMenuClicked);
options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail;
const newOpts = await options.getAll();
if (menuIdCastMedia && alteredOpts.includes("mediaEnabled")) {
browser.menus.update(menuIdCastMedia, {
visible: newOpts.mediaEnabled
});
}
if (menuIdCastMedia && alteredOpts.includes("localMediaEnabled")) {
browser.menus.update(menuIdCastMedia, {
targetUrlPatterns: newOpts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
}
});
} }
browser.menus.onClicked.addListener(async (info, tab) => { /** 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)) {
updateWhitelistMenu(info.pageUrl);
return;
}
}
/** Handle menu click events */
async function onMenuClicked(
info: browser.menus.OnClickData,
tab?: browser.tabs.Tab
) {
// Handle whitelist menus
if (info.parentMenuItemId === menuIdWhitelist) { if (info.parentMenuItemId === menuIdWhitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId); const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
if (!pattern) { if (!pattern) {
@@ -88,113 +129,81 @@ browser.menus.onClicked.addListener(async (info, tab) => {
return; return;
} }
if (tab?.id === undefined) { // Handle cast menus
throw logger.error("Menu handler tab ID not found."); const castMenuClicked = info.menuItemId === menuIdCast;
} const castMediaMenuClicked = info.menuItemId === menuIdCastMedia;
if (!info.pageUrl) { if (castMenuClicked || castMediaMenuClicked) {
throw logger.error("Menu handler page URL not found."); if (tab?.id === undefined) {
} throw logger.error("Menu handler tab ID not found.");
}
if (!info.pageUrl) {
throw logger.error("Menu handler page URL not found.");
}
switch (info.menuItemId) { let selection: Nullable<ReceiverSelection> = null;
case menuIdCast: { try {
const selection = await ReceiverSelectorManager.getSelection( selection = await ReceiverSelectorManager.getSelection(
tab.id, tab.id,
info.frameId info.frameId,
{ withMediaSender: castMediaMenuClicked }
); );
} catch (err) {
logger.error("Failed to get receiver selection (cast menu)", err);
return;
}
// Invalid selection result
if (
!selection ||
selection.actionType !== ReceiverSelectionActionType.Cast
) {
return;
}
// Selection cancelled if (castMenuClicked) {
if (!selection) {
break;
}
CastManager.loadSender({ CastManager.loadSender({
tabId: tab.id, tabId: tab.id,
frameId: info.frameId, frameId: info.frameId,
selection selection
}); });
} else if (castMediaMenuClicked) {
/**
* If the selected media type is App, that refers to
* the media sender in this context, so load media
* sender.
*/
if (selection.mediaType === ReceiverSelectorMediaType.App) {
await browser.tabs.executeScript(tab.id, {
code: stringify`
window.receiver = ${selection.receiverDevice};
window.mediaUrl = ${info.srcUrl};
window.targetElementId = ${info.targetElementId};
`,
frameId: info.frameId
});
break; await browser.tabs.executeScript(tab.id, {
} file: "cast/senders/media/index.js",
frameId: info.frameId
case menuIdMediaCast: { });
const selection = await ReceiverSelectorManager.getSelection( } else {
tab.id, // Handle other responses
info.frameId, CastManager.loadSender({
true tabId: tab.id,
); frameId: info.frameId,
selection
// Selection cancelled });
if (!selection) {
break;
} }
switch (selection.actionType) {
case ReceiverSelectionActionType.Cast: {
/**
* If the selected media type is App, that refers to the
* media sender in this context, so load media sender.
*/
if (selection.mediaType === ReceiverSelectorMediaType.App) {
await browser.tabs.executeScript(tab.id, {
code: stringify`
window.receiver = ${selection.receiverDevice};
window.mediaUrl = ${info.srcUrl};
window.targetElementId = ${info.targetElementId};
`,
frameId: info.frameId
});
await browser.tabs.executeScript(tab.id, {
file: "cast/senders/media/index.js",
frameId: info.frameId
});
} else {
// Handle other responses
CastManager.loadSender({
tabId: tab.id,
frameId: info.frameId,
selection
});
}
break;
}
}
break;
} }
} }
}); }
// Hide cast item on extension pages
browser.menus.onShown.addListener(info => {
if (info.pageUrl?.startsWith(browser.runtime.getURL(""))) {
browser.menus.update(menuIdCast, {
visible: false
});
browser.menus.refresh();
}
});
browser.menus.onHidden.addListener(() => {
browser.menus.update(menuIdCast, {
visible: true
});
});
browser.menus.onShown.addListener(async info => {
// Only rebuild menus if whitelist menu present
// WebExt typings are broken again here, so ugly casting
const menuIds = info.menuIds as unknown as number[];
if (!menuIds.includes(menuIdWhitelist as number)) {
return;
}
/** Handles updating the whitelist menus for a given URL */
async function updateWhitelistMenu(pageUrl?: string) {
/** /**
* If page URL doesn't exist, we're not on a page and have * If page URL doesn't exist, we're not on a page and have nothing
* nothing to whitelist, so disable the menu and return. * to whitelist, so disable the menu and return.
*/ */
if (!info.pageUrl) { if (!pageUrl) {
browser.menus.update(menuIdWhitelist, { browser.menus.update(menuIdWhitelist, {
enabled: false enabled: false
}); });
@@ -203,7 +212,7 @@ browser.menus.onShown.addListener(async info => {
return; return;
} }
const url = new URL(info.pageUrl); const url = new URL(pageUrl);
const urlHasOrigin = url.origin !== "null"; const urlHasOrigin = url.origin !== "null";
/** /**
@@ -331,23 +340,4 @@ browser.menus.onShown.addListener(async info => {
); );
await browser.menus.refresh(); await browser.menus.refresh();
}); }
options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail;
const newOpts = await options.getAll();
if (menuIdMediaCast && alteredOpts.includes("mediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
visible: newOpts.mediaEnabled
});
}
if (menuIdMediaCast && alteredOpts.includes("localMediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
targetUrlPatterns: newOpts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
}
});

View File

@@ -194,7 +194,11 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
this.defaultMediaType === undefined || this.defaultMediaType === undefined ||
this.availableMediaTypes === undefined this.availableMediaTypes === undefined
) { ) {
logger.error("Popup receiver data not found."); this.dispatchEvent(
new CustomEvent("error", {
detail: "Popup receiver data not found."
})
);
return; return;
} }

View File

@@ -35,8 +35,8 @@ async function getSelector() {
} }
/** /**
* Opens a receiver selector with the specified * Opens a receiver selector with the specified default/available media
* default/available media types. * types.
* *
* Returns a promise that: * Returns a promise that:
* - Resolves to a ReceiverSelection object if selection is * - Resolves to a ReceiverSelection object if selection is
@@ -47,7 +47,7 @@ async function getSelector() {
async function getSelection( async function getSelection(
contextTabId: number, contextTabId: number,
contextFrameId = 0, contextFrameId = 0,
withMediaSender = false selectionOpts?: { withMediaSender?: boolean }
): Promise<ReceiverSelection | null> { ): Promise<ReceiverSelection | null> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let castInstance = CastManager.getInstance( let castInstance = CastManager.getInstance(
@@ -84,7 +84,7 @@ async function getSelection(
} }
// Enable app media type if initialized sender app is found // Enable app media type if initialized sender app is found
if (castInstance || withMediaSender) { if (castInstance || selectionOpts?.withMediaSender) {
defaultMediaType = ReceiverSelectorMediaType.App; defaultMediaType = ReceiverSelectorMediaType.App;
availableMediaTypes |= ReceiverSelectorMediaType.App; availableMediaTypes |= ReceiverSelectorMediaType.App;
} }
@@ -193,8 +193,8 @@ async function getSelection(
sharedSelector.addEventListener( sharedSelector.addEventListener(
"error", "error",
storeListener("error", () => { storeListener("error", ev => {
reject(); reject(ev.detail);
removeListeners(); removeListeners();
}) })
); );

View File

@@ -25,10 +25,7 @@ export interface CastInstance {
appId?: string; appId?: string;
} }
/** /** Keeps track of cast API instances and provides bridge messaging. */
* Keeps track of cast API instances and provides bridge
* messaging.
*/
export default new (class CastManager { export default new (class CastManager {
private activeInstances = new Set<CastInstance>(); private activeInstances = new Set<CastInstance>();
@@ -60,8 +57,7 @@ export default new (class CastManager {
} }
/** /**
* Finds a cast instance at the given tab (and optionally * Finds a cast instance at the given tab (and optionally frame) ID.
* frame) ID.
*/ */
public getInstance(tabId: number, frameId?: number) { public getInstance(tabId: number, frameId?: number) {
for (const instance of this.activeInstances) { for (const instance of this.activeInstances) {
@@ -77,8 +73,8 @@ export default new (class CastManager {
} }
/** /**
* Creates a cast instance with a given port and connects * Creates a cast instance with a given port and connects messaging
* messaging correctly depending on the type of port. * correctly depending on the type of port.
*/ */
public async createInstance(port: AnyPort) { public async createInstance(port: AnyPort) {
const instance = await (port instanceof MessagePort const instance = await (port instanceof MessagePort
@@ -95,9 +91,7 @@ export default new (class CastManager {
return instance; return instance;
} }
/** /** Creates a cast instance with a `MessagePort` content port. */
* Creates a cast instance with a `MessagePort` content port.
*/
private async createInstanceFromBackground( private async createInstanceFromBackground(
contentPort: MessagePort contentPort: MessagePort
): Promise<CastInstance> { ): Promise<CastInstance> {
@@ -125,8 +119,7 @@ export default new (class CastManager {
} }
/** /**
* Creates a cast instance with a WebExtension `Port` content * Creates a cast instance with a WebExtension `Port` content port.
* port.
*/ */
private async createInstanceFromContent( private async createInstanceFromContent(
contentPort: Port contentPort: Port
@@ -191,9 +184,9 @@ export default new (class CastManager {
} }
/** /**
* Handle content messages from the cast instance. These will * Handle content messages from the cast instance. These will either
* either be handled here in the background script or forwarded * be handled here in the background script or forwarded to the
* to the bridge associated with the cast instance. * bridge associated with the cast instance.
*/ */
private async handleContentMessage( private async handleContentMessage(
instance: CastInstance, instance: CastInstance,
@@ -249,9 +242,10 @@ export default new (class CastManager {
switch (selection.actionType) { switch (selection.actionType) {
case ReceiverSelectionActionType.Cast: { case ReceiverSelectionActionType.Cast: {
/** /**
* If the media type returned from the selector has * If the media type returned from the
* been changed, we need to cancel the current * selector has been changed, we need to
* sender and switch it out for the right one. * cancel the current sender and switch it
* out for the right one.
*/ */
if ( if (
selection.mediaType !== selection.mediaType !==
@@ -298,8 +292,8 @@ export default new (class CastManager {
} }
/** /**
* TODO: If we're closing a selector, make sure it's the same * TODO: If we're closing a selector, make sure it's the
* one that caused the session creation. * same one that caused the session creation.
*/ */
case "main:closeReceiverSelector": { case "main:closeReceiverSelector": {
const selector = await ReceiverSelectorManager.getSelector(); const selector = await ReceiverSelectorManager.getSelector();
@@ -317,8 +311,8 @@ export default new (class CastManager {
} }
/** /**
* Loads the appropriate sender for a given receiver * Loads the appropriate sender for a given receiver selector
* selector response. * response.
*/ */
public async loadSender(opts: { public async loadSender(opts: {
tabId: number; tabId: number;

View File

@@ -45,9 +45,8 @@ enum _MediaCommand {
} }
/** /**
* Takes a media object and a media status object and merges * Takes a media object and a media status object and merges the status
* the status with the existing media object, updating it with * with the existing media object, updating it with new properties.
* new properties.
*/ */
function updateMedia(media: Media, status: MediaStatus) { function updateMedia(media: Media, status: MediaStatus) {
if (status.currentTime) { if (status.currentTime) {
@@ -179,7 +178,7 @@ export default class Session {
/** /**
* Sends a media message to the app receiver. * Sends a media message to the app receiver.
* urn:x-cast:com.google.cast.media * `urn:x-cast:com.google.cast.media`
*/ */
#sendMediaMessage = ( #sendMediaMessage = (
message: DistributiveOmit<SenderMediaMessage, "requestId"> message: DistributiveOmit<SenderMediaMessage, "requestId">