mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 16:49:58 +00:00
Refactor extension menus module
This commit is contained in:
@@ -15,27 +15,27 @@ import ReceiverSelectorManager
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
|
||||
const URL_PATTERN_HTTP = "http://*/*";
|
||||
const URL_PATTERN_HTTPS = "https://*/*";
|
||||
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 menuIdMediaCast: MenuId;
|
||||
let menuIdWhitelist: MenuId;
|
||||
let menuIdWhitelistRecommended: MenuId;
|
||||
|
||||
const whitelistChildMenuPatterns = new Map<MenuId, string>();
|
||||
|
||||
|
||||
export async function initMenus () {
|
||||
logger.info("init (menus)");
|
||||
|
||||
const URL_PATTERN_HTTP = "http://*/*";
|
||||
const URL_PATTERN_HTTPS = "https://*/*";
|
||||
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 menuIdMediaCast: MenuId;
|
||||
let menuIdWhitelist: MenuId;
|
||||
let menuIdWhitelistRecommended: MenuId;
|
||||
|
||||
const whitelistChildMenuPatterns = new Map<MenuId, string>();
|
||||
|
||||
|
||||
const opts = await options.getAll();
|
||||
|
||||
// Global "Cast..." menu item
|
||||
@@ -70,298 +70,298 @@ export async function initMenus () {
|
||||
type: "separator"
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.parentMenuItemId === menuIdWhitelist) {
|
||||
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
|
||||
if (!pattern) {
|
||||
throw logger.error(`Whitelist pattern not found for menu item ID ${info.menuItemId}.`);
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.parentMenuItemId === menuIdWhitelist) {
|
||||
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("userAgentWhitelist");
|
||||
if (!whitelist.includes(pattern)) {
|
||||
// Add to whitelist and update options
|
||||
whitelist.push(pattern);
|
||||
await options.set("userAgentWhitelist", whitelist);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
|
||||
const availableMediaTypes = getMediaTypesForPageUrl(info.pageUrl);
|
||||
|
||||
switch (info.menuItemId) {
|
||||
case menuIdCast: {
|
||||
const selection = await ReceiverSelectorManager.getSelection(
|
||||
tab.id, info.frameId);
|
||||
|
||||
// Selection cancelled
|
||||
if (!selection) {
|
||||
break;
|
||||
}
|
||||
|
||||
const whitelist = await options.get("userAgentWhitelist");
|
||||
if (!whitelist.includes(pattern)) {
|
||||
// Add to whitelist and update options
|
||||
whitelist.push(pattern);
|
||||
await options.set("userAgentWhitelist", whitelist);
|
||||
loadSender({
|
||||
tabId: tab.id
|
||||
, frameId: info.frameId
|
||||
, selection
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case menuIdMediaCast: {
|
||||
const selection = await ReceiverSelectorManager.getSelection(
|
||||
tab.id, info.frameId, true);
|
||||
|
||||
// Selection cancelled
|
||||
if (!selection) {
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
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.receiver};
|
||||
window.mediaUrl = ${info.srcUrl};
|
||||
window.targetElementId = ${
|
||||
info.targetElementId};
|
||||
`
|
||||
, frameId: info.frameId
|
||||
});
|
||||
|
||||
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.");
|
||||
}
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "senders/media/bundle.js"
|
||||
, frameId: info.frameId
|
||||
});
|
||||
} else {
|
||||
|
||||
// Handle other responses
|
||||
loadSender({
|
||||
tabId: tab.id
|
||||
, frameId: info.frameId
|
||||
, selection
|
||||
});
|
||||
}
|
||||
|
||||
const availableMediaTypes = getMediaTypesForPageUrl(info.pageUrl);
|
||||
|
||||
switch (info.menuItemId) {
|
||||
case menuIdCast: {
|
||||
const selection = await ReceiverSelectorManager.getSelection(
|
||||
tab.id, info.frameId);
|
||||
|
||||
// Selection cancelled
|
||||
if (!selection) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
loadSender({
|
||||
tabId: tab.id
|
||||
, frameId: info.frameId
|
||||
, selection
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: false
|
||||
});
|
||||
|
||||
browser.menus.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const url = new URL(info.pageUrl);
|
||||
const urlHasOrigin = url.origin !== "null";
|
||||
|
||||
/**
|
||||
* If the page URL doesn't have an origin, we're not on a
|
||||
* remote page and have nothing to whitelist, so disable the
|
||||
* menu and return.
|
||||
*/
|
||||
if (!urlHasOrigin) {
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: false
|
||||
});
|
||||
|
||||
browser.menus.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Enable the whitelist menu
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: true
|
||||
});
|
||||
|
||||
|
||||
for (const [ menuId ] of whitelistChildMenuPatterns) {
|
||||
// Clear all page-specific temporary menus
|
||||
if (menuId !== menuIdWhitelistRecommended) {
|
||||
browser.menus.remove(menuId);
|
||||
}
|
||||
|
||||
whitelistChildMenuPatterns.delete(menuId);
|
||||
}
|
||||
|
||||
|
||||
// If there is more than one subdomain, get the base domain
|
||||
const baseDomain = (url.hostname.match(/\./g) || []).length > 1
|
||||
? url.hostname.substring(url.hostname.indexOf(".") + 1)
|
||||
: url.hostname;
|
||||
|
||||
const portlessOrigin = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
const patternRecommended = `${portlessOrigin}/*`;
|
||||
const patternSearch = `${portlessOrigin}${url.pathname}${url.search}`;
|
||||
const patternWildcardProtocol = `*://${url.hostname}/*`;
|
||||
const patternWildcardSubdomain = `${url.protocol}//*.${baseDomain}/*`;
|
||||
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
|
||||
|
||||
|
||||
// Update recommended menu item
|
||||
browser.menus.update(menuIdWhitelistRecommended, {
|
||||
title: _("contextAddToWhitelistRecommended", patternRecommended)
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
menuIdWhitelistRecommended, patternRecommended);
|
||||
|
||||
|
||||
if (url.search) {
|
||||
const whitelistSearchMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
whitelistSearchMenuId, patternSearch);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Split URL path into segments and add menu items for each
|
||||
* partial path as the segments are removed.
|
||||
*/
|
||||
{
|
||||
const pathTrimmed = url.pathname.endsWith("/")
|
||||
? url.pathname.substring(0, url.pathname.length - 1)
|
||||
: url.pathname;
|
||||
|
||||
const pathSegments = pathTrimmed.split("/")
|
||||
.filter(segment => segment)
|
||||
.reverse();
|
||||
|
||||
if (pathSegments.length) {
|
||||
for (let i = 0; i < pathSegments.length; i++) {
|
||||
const partialPath = pathSegments
|
||||
.slice(i)
|
||||
.reverse()
|
||||
.join("/");
|
||||
|
||||
const pattern = `${portlessOrigin}/${partialPath}/*`;
|
||||
|
||||
const partialPathMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", pattern)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case menuIdMediaCast: {
|
||||
const selection = await ReceiverSelectorManager.getSelection(
|
||||
tab.id, info.frameId, true);
|
||||
|
||||
// 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.receiver};
|
||||
window.mediaUrl = ${info.srcUrl};
|
||||
window.targetElementId = ${
|
||||
info.targetElementId};
|
||||
`
|
||||
, frameId: info.frameId
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "senders/media/bundle.js"
|
||||
, frameId: info.frameId
|
||||
});
|
||||
} else {
|
||||
|
||||
// Handle other responses
|
||||
loadSender({
|
||||
tabId: tab.id
|
||||
, frameId: info.frameId
|
||||
, selection
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
whitelistChildMenuPatterns.set(
|
||||
partialPathMenuId, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const wildcardProtocolMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardProtocol)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
// Hide cast item on extension pages
|
||||
browser.menus.onShown.addListener(info => {
|
||||
if (info.pageUrl?.startsWith(browser.runtime.getURL(""))) {
|
||||
browser.menus.update(menuIdCast, {
|
||||
visible: false
|
||||
});
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolMenuId, patternWildcardProtocol);
|
||||
|
||||
browser.menus.refresh();
|
||||
}
|
||||
});
|
||||
browser.menus.onHidden.addListener(() => {
|
||||
browser.menus.update(menuIdCast, {
|
||||
visible: true
|
||||
});
|
||||
|
||||
const wildcardSubdomainMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardSubdomain)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardSubdomainMenuId, patternWildcardSubdomain);
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: false
|
||||
});
|
||||
|
||||
browser.menus.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const url = new URL(info.pageUrl);
|
||||
const urlHasOrigin = url.origin !== "null";
|
||||
|
||||
/**
|
||||
* If the page URL doesn't have an origin, we're not on a
|
||||
* remote page and have nothing to whitelist, so disable the
|
||||
* menu and return.
|
||||
*/
|
||||
if (!urlHasOrigin) {
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: false
|
||||
});
|
||||
|
||||
browser.menus.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Enable the whitelist menu
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: true
|
||||
});
|
||||
|
||||
|
||||
for (const [ menuId ] of whitelistChildMenuPatterns) {
|
||||
// Clear all page-specific temporary menus
|
||||
if (menuId !== menuIdWhitelistRecommended) {
|
||||
browser.menus.remove(menuId);
|
||||
}
|
||||
|
||||
whitelistChildMenuPatterns.delete(menuId);
|
||||
}
|
||||
|
||||
|
||||
// If there is more than one subdomain, get the base domain
|
||||
const baseDomain = (url.hostname.match(/\./g) || []).length > 1
|
||||
? url.hostname.substring(url.hostname.indexOf(".") + 1)
|
||||
: url.hostname;
|
||||
|
||||
const portlessOrigin = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
const patternRecommended = `${portlessOrigin}/*`;
|
||||
const patternSearch = `${portlessOrigin}${url.pathname}${url.search}`;
|
||||
const patternWildcardProtocol = `*://${url.hostname}/*`;
|
||||
const patternWildcardSubdomain = `${url.protocol}//*.${baseDomain}/*`;
|
||||
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
|
||||
|
||||
|
||||
// Update recommended menu item
|
||||
browser.menus.update(menuIdWhitelistRecommended, {
|
||||
title: _("contextAddToWhitelistRecommended", patternRecommended)
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
menuIdWhitelistRecommended, patternRecommended);
|
||||
|
||||
|
||||
if (url.search) {
|
||||
const whitelistSearchMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
whitelistSearchMenuId, patternSearch);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Split URL path into segments and add menu items for each
|
||||
* partial path as the segments are removed.
|
||||
*/
|
||||
{
|
||||
const pathTrimmed = url.pathname.endsWith("/")
|
||||
? url.pathname.substring(0, url.pathname.length - 1)
|
||||
: url.pathname;
|
||||
|
||||
const pathSegments = pathTrimmed.split("/")
|
||||
.filter(segment => segment)
|
||||
.reverse();
|
||||
|
||||
if (pathSegments.length) {
|
||||
for (let i = 0; i < pathSegments.length; i++) {
|
||||
const partialPath = pathSegments
|
||||
.slice(i)
|
||||
.reverse()
|
||||
.join("/");
|
||||
|
||||
const pattern = `${portlessOrigin}/${partialPath}/*`;
|
||||
|
||||
const partialPathMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", pattern)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
partialPathMenuId, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const wildcardProtocolMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardProtocol)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolMenuId, patternWildcardProtocol);
|
||||
|
||||
|
||||
const wildcardSubdomainMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardSubdomain)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardSubdomainMenuId, patternWildcardSubdomain);
|
||||
|
||||
|
||||
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardProtocolAndSubdomain)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolAndSubdomainMenuId
|
||||
, patternWildcardProtocolAndSubdomain);
|
||||
|
||||
|
||||
await browser.menus.refresh();
|
||||
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardProtocolAndSubdomain)
|
||||
, parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolAndSubdomainMenuId
|
||||
, patternWildcardProtocolAndSubdomain);
|
||||
|
||||
options.addEventListener("changed", async ev => {
|
||||
const alteredOpts = ev.detail;
|
||||
const newOpts = await options.getAll();
|
||||
|
||||
if (alteredOpts.includes("mediaEnabled")) {
|
||||
browser.menus.update(menuIdMediaCast, {
|
||||
visible: newOpts.mediaEnabled
|
||||
});
|
||||
}
|
||||
await browser.menus.refresh();
|
||||
});
|
||||
|
||||
if (alteredOpts.includes("localMediaEnabled")) {
|
||||
browser.menus.update(menuIdMediaCast, {
|
||||
targetUrlPatterns: newOpts.localMediaEnabled
|
||||
? URL_PATTERNS_ALL
|
||||
: URL_PATTERNS_REMOTE
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
options.addEventListener("changed", async ev => {
|
||||
const alteredOpts = ev.detail;
|
||||
const newOpts = await options.getAll();
|
||||
|
||||
if (alteredOpts.includes("mediaEnabled")) {
|
||||
browser.menus.update(menuIdMediaCast, {
|
||||
visible: newOpts.mediaEnabled
|
||||
});
|
||||
}
|
||||
|
||||
if (alteredOpts.includes("localMediaEnabled")) {
|
||||
browser.menus.update(menuIdMediaCast, {
|
||||
targetUrlPatterns: newOpts.localMediaEnabled
|
||||
? URL_PATTERNS_ALL
|
||||
: URL_PATTERNS_REMOTE
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user