mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Refactor menus module
This commit is contained in:
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user