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

View File

@@ -6,6 +6,7 @@ import options from "../lib/options";
import { stringify } from "../lib/utils";
import {
ReceiverSelection,
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "./receiverSelector";
@@ -25,12 +26,14 @@ const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE];
type MenuId = string | number;
let menuIdCast: MenuId;
let menuIdMediaCast: MenuId;
let menuIdCastMedia: MenuId;
let menuIdWhitelist: MenuId;
let menuIdWhitelistRecommended: MenuId;
/** Match patterns for the whitelist option menus. */
const whitelistChildMenuPatterns = new Map<MenuId, string>();
/** Handles initial menu setup. */
export async function initMenus() {
logger.info("init (menus)");
@@ -43,7 +46,7 @@ export async function initMenus() {
});
// <video>/<audio> "Cast..." context menu item
menuIdMediaCast = browser.menus.create({
menuIdCastMedia = browser.menus.create({
contexts: ["audio", "video", "image"],
title: _("contextCast"),
visible: opts.mediaEnabled,
@@ -67,9 +70,47 @@ export async function initMenus() {
type: "separator",
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) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
if (!pattern) {
@@ -88,113 +129,81 @@ browser.menus.onClicked.addListener(async (info, tab) => {
return;
}
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.");
}
// Handle cast menus
const castMenuClicked = info.menuItemId === menuIdCast;
const castMediaMenuClicked = info.menuItemId === menuIdCastMedia;
if (castMenuClicked || castMediaMenuClicked) {
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) {
case menuIdCast: {
const selection = await ReceiverSelectorManager.getSelection(
let selection: Nullable<ReceiverSelection> = null;
try {
selection = await ReceiverSelectorManager.getSelection(
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 (!selection) {
break;
}
if (castMenuClicked) {
CastManager.loadSender({
tabId: tab.id,
frameId: info.frameId,
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;
}
case menuIdMediaCast: {
const selection = await ReceiverSelectorManager.getSelection(
tab.id,
info.frameId,
true
);
// Selection cancelled
if (!selection) {
break;
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
});
}
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
* nothing to whitelist, so disable the menu and return.
* If page URL doesn't exist, we're not on a page and have nothing
* to whitelist, so disable the menu and return.
*/
if (!info.pageUrl) {
if (!pageUrl) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
@@ -203,7 +212,7 @@ browser.menus.onShown.addListener(async info => {
return;
}
const url = new URL(info.pageUrl);
const url = new URL(pageUrl);
const urlHasOrigin = url.origin !== "null";
/**
@@ -331,23 +340,4 @@ browser.menus.onShown.addListener(async info => {
);
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.availableMediaTypes === undefined
) {
logger.error("Popup receiver data not found.");
this.dispatchEvent(
new CustomEvent("error", {
detail: "Popup receiver data not found."
})
);
return;
}

View File

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

View File

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

View File

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