Implement receiver selector whitelist suggestion banner

This commit is contained in:
hensm
2022-04-16 12:02:19 +01:00
committed by Matt Hensman
parent 124a5eb92d
commit 1da709eb5e
14 changed files with 751 additions and 415 deletions

View File

@@ -1,370 +1,381 @@
{
"extensionName": {
"message": "fx_cast"
, "description": "Name of the extension and the native receiver selector window title."
}
, "extensionDescription": {
"message": "Enables Chromecast support for casting web apps (like Netflix or BBC iPlayer), HTML5 video and screen/tab sharing."
, "description": "Description of the extension shown in the add-ons manager."
}
"message": "fx_cast",
"description": "Name of the extension and the native receiver selector window title."
},
"extensionDescription": {
"message": "Enables Chromecast support for casting web apps (like Netflix or BBC iPlayer), HTML5 video and screen/tab sharing.",
"description": "Description of the extension shown in the add-ons manager."
},
"popupWhitelistNotWhitelisted": {
"message": "$appName$ is not whitelisted",
"description": "Receiver selector whitelist suggestion banner label.",
"placeholders": {
"appName": {
"content": "$1",
"example": "Netflix"
}
}
},
"popupWhitelistAddToWhitelist": {
"message": "Add to Whitelist",
"description": "Receiver selector whitelist suggestion banner button label."
},
"popupMediaTypeApp": {
"message": "this site's app",
"description": "Receiver selector media type <option> text for current site's sender application."
},
"popupMediaTypeAppNotFound": {
"message": "this site's app (not found)",
"description": "Receiver selector media type <option> text for current site's sender application if none found."
},
"popupMediaTypeAppMedia": {
"message": "this media",
"description": "Receiver selector media type <option> text for media casting."
},
"popupMediaTypeTab": {
"message": "Tab",
"description": "Receiver selector media type <option> text for current tab."
},
"popupMediaTypeScreen": {
"message": "Screen",
"description": "Receiver selector media type <option> text for screen."
},
"popupMediaTypeFile": {
"message": "Browse...",
"description": "Receiver selector media type <option> text for opening a file selector dialog."
},
, "popupMediaTypeApp": {
"message": "this site's app"
, "description": "Receiver selector media type <option> text for current site's sender application."
}
, "popupMediaTypeAppNotFound": {
"message": "this site's app (not found)"
, "description": "Receiver selector media type <option> text for current site's sender application if none found."
}
, "popupMediaTypeAppMedia": {
"message": "this media"
, "description": "Receiver selector media type <option> text for media casting."
}
, "popupMediaTypeTab": {
"message": "Tab"
, "description": "Receiver selector media type <option> text for current tab."
}
, "popupMediaTypeScreen": {
"message": "Screen"
, "description": "Receiver selector media type <option> text for screen."
}
, "popupMediaTypeFile": {
"message": "Browse..."
, "description": "Receiver selector media type <option> text for opening a file selector dialog."
}
, "popupMediaSelectCastLabel": {
"message": "Cast"
, "description": "(Cast) <select> to:"
}
, "popupMediaSelectToLabel": {
"message": "to:"
, "description": "Cast <select> (to:)"
}
, "popupNoReceiversFound": {
"message": "No receiver devices found"
, "description": "Message displayed in the receiver selector if there are no available receivers."
}
, "popupCastButtonTitle": {
"message": "Cast"
, "description": "Button text for each receiver entry in the receiver selector."
}
, "popupCastingButtonTitle": {
"message": "Casting$ellipsis$"
, "description": "Button text while establishing a session in the receiver selector. Ellipsis cycles (. → .. → ...) as loading indicator."
, "placeholders": {
"popupMediaSelectCastLabel": {
"message": "Cast",
"description": "(Cast) <select> to:"
},
"popupMediaSelectToLabel": {
"message": "to:",
"description": "Cast <select> (to:)"
},
"popupNoReceiversFound": {
"message": "No receiver devices found",
"description": "Message displayed in the receiver selector if there are no available receivers."
},
"popupCastButtonTitle": {
"message": "Cast",
"description": "Button text for each receiver entry in the receiver selector."
},
"popupCastingButtonTitle": {
"message": "Casting$ellipsis$",
"description": "Button text while establishing a session in the receiver selector. Ellipsis cycles (. → .. → ...) as loading indicator.",
"placeholders": {
"ellipsis": {
"content": "$1"
, "example": "..."
"content": "$1",
"example": "..."
}
}
}
, "popupStopButtonTitle": {
"message": "Stop"
, "description": "Alternate action button text displayed instead of popupCastButtonTitle."
}
},
"popupStopButtonTitle": {
"message": "Stop",
"description": "Alternate action button text displayed instead of popupCastButtonTitle."
},
"contextCast": {
"message": "Cast...",
"description": "Main context menu item title. Ellipsis indicates additional information required as it triggers opening of receiver selector."
},
, "contextCast": {
"message": "Cast..."
, "description": "Main context menu item title. Ellipsis indicates additional information required as it triggers opening of receiver selector."
}
, "contextAddToWhitelist": {
"message": "Add to Whitelist"
, "description": "Top-level whitelist context menu item title."
}
, "contextAddToWhitelistRecommended": {
"message": "Add $matchPattern$ (Recommended)"
, "description": "Context menu item title for recomended match pattern."
, "placeholders": {
"contextAddToWhitelist": {
"message": "Add to Whitelist",
"description": "Top-level whitelist context menu item title."
},
"contextAddToWhitelistRecommended": {
"message": "Add $matchPattern$ (Recommended)",
"description": "Context menu item title for recomended match pattern.",
"placeholders": {
"matchPattern": {
"content": "$1"
, "example": "https://example.com/*"
"content": "$1",
"example": "https://example.com/*"
}
}
}
, "contextAddToWhitelistAdvancedAdd": {
"message": "Add $matchPattern$"
, "description": "Context menu item title for all other match patterns."
, "placeholders": {
},
"contextAddToWhitelistAdvancedAdd": {
"message": "Add $matchPattern$",
"description": "Context menu item title for all other match patterns.",
"placeholders": {
"matchPattern": {
"content": "$1"
, "example": "*://*.example.com/*"
"content": "$1",
"example": "*://*.example.com/*"
}
}
}
},
"optionsBridgeLoading": {
"message": "Loading bridge info...",
"description": "Loading placeholder text for bridge section on options page."
},
"optionsBridgeFoundStatusTitle": {
"message": "Bridge found",
"description": "Bridge OK status title text."
},
"optionsBridgeIssueStatusTitle": {
"message": "Bridge issue",
"description": "Bridge error status title text."
},
"optionsBridgeNotFoundStatusTitle": {
"message": "Bridge not found",
"description": "Bridge missing status title text."
},
"optionsBridgeNotFoundStatusText": {
"message": "Try downloading and installing the latest version.",
"description": "Bridge not found additional description text"
},
, "optionsBridgeLoading": {
"message": "Loading bridge info..."
, "description": "Loading placeholder text for bridge section on options page."
}
, "optionsBridgeFoundStatusTitle": {
"message": "Bridge found"
, "description": "Bridge OK status title text."
}
, "optionsBridgeIssueStatusTitle": {
"message": "Bridge issue"
, "description": "Bridge error status title text."
}
, "optionsBridgeNotFoundStatusTitle": {
"message": "Bridge not found"
, "description": "Bridge missing status title text."
}
, "optionsBridgeNotFoundStatusText": {
"message": "Try downloading and installing the latest version."
, "description": "Bridge not found additional description text"
}
, "optionsBridgeStatsName": {
"message": "Name:"
, "description": "Bridge stats name row title."
}
, "optionsBridgeStatsVersion": {
"message": "Version:"
, "description": "Bridge stats version row title."
}
, "optionsBridgeStatsExpectedVersion": {
"message": "Expected version:"
, "description": "Bridge stats expected version row title."
}
, "optionsBridgeStatsCompatibility": {
"message": "Compatibility:"
, "description": "Bridge stats compatibility row title."
}
, "optionsBridgeStatsRecommendedAction": {
"message": "Recommended action:"
, "description": "Bridge stats recommended action row title."
}
, "optionsBridgeCompatible": {
"message": "Compatible"
, "description": "Compatibility status is definitely compatible."
}
, "optionsBridgeLikelyCompatible": {
"message": "Likely compatible"
, "description": "Compatibility status is probably compatible."
}
, "optionsBridgeIncompatible": {
"message": "Incompatible"
, "description": "Compatibility status is definitely incompatible."
}
, "optionsBridgeOlderAction": {
"message": "Bridge version older than expected, try updating bridge to the latest version."
, "description": "Recommended action for when the installed bridge version is older than the installed extension version."
}
, "optionsBridgeNewerAction": {
"message": "Bridge version newer than expected, try updating extension to the latest version."
, "description": "Recommended action for when the installed bridge version is newer than the installed extension version."
}
, "optionsBridgeNoAction": {
"message": "No action needed."
, "description": "Recommended action for when both bridge and extension versions are compatible or likely compatible."
}
, "optionsBridgeUpdateCheck": {
"message": "Check for Updates"
, "description": "Update check button title."
}
, "optionsBridgeUpdateChecking": {
"message": "Checking for Updates$ellipsis$"
, "description": "Update check button title while in progress. Ellipsis cycles (. → .. → ...) as loading indicator."
, "placeholders": {
"optionsBridgeStatsName": {
"message": "Name:",
"description": "Bridge stats name row title."
},
"optionsBridgeStatsVersion": {
"message": "Version:",
"description": "Bridge stats version row title."
},
"optionsBridgeStatsExpectedVersion": {
"message": "Expected version:",
"description": "Bridge stats expected version row title."
},
"optionsBridgeStatsCompatibility": {
"message": "Compatibility:",
"description": "Bridge stats compatibility row title."
},
"optionsBridgeStatsRecommendedAction": {
"message": "Recommended action:",
"description": "Bridge stats recommended action row title."
},
"optionsBridgeCompatible": {
"message": "Compatible",
"description": "Compatibility status is definitely compatible."
},
"optionsBridgeLikelyCompatible": {
"message": "Likely compatible",
"description": "Compatibility status is probably compatible."
},
"optionsBridgeIncompatible": {
"message": "Incompatible",
"description": "Compatibility status is definitely incompatible."
},
"optionsBridgeOlderAction": {
"message": "Bridge version older than expected, try updating bridge to the latest version.",
"description": "Recommended action for when the installed bridge version is older than the installed extension version."
},
"optionsBridgeNewerAction": {
"message": "Bridge version newer than expected, try updating extension to the latest version.",
"description": "Recommended action for when the installed bridge version is newer than the installed extension version."
},
"optionsBridgeNoAction": {
"message": "No action needed.",
"description": "Recommended action for when both bridge and extension versions are compatible or likely compatible."
},
"optionsBridgeUpdateCheck": {
"message": "Check for Updates",
"description": "Update check button title."
},
"optionsBridgeUpdateChecking": {
"message": "Checking for Updates$ellipsis$",
"description": "Update check button title while in progress. Ellipsis cycles (. → .. → ...) as loading indicator.",
"placeholders": {
"ellipsis": {
"content": "$1"
, "example": ".."
"content": "$1",
"example": ".."
}
}
}
, "optionsBridgeUpdateStatusNoUpdates": {
"message": "No updates available"
, "description": "Update status if no updates are found."
}
, "optionsBridgeUpdateStatusError": {
"message": "Error checking for updates"
, "description": "Update status if an error was encountered checking for updates."
}
, "optionsBridgeUpdateAvailable": {
"message": "An update is available:"
, "description": "Update status if an update was found."
}
, "optionsBridgeUpdate": {
"message": "Update Now..."
, "description": "Update now button title. Ellipsis indicates additional information as it triggers an update window popup."
}
},
"optionsBridgeUpdateStatusNoUpdates": {
"message": "No updates available",
"description": "Update status if no updates are found."
},
"optionsBridgeUpdateStatusError": {
"message": "Error checking for updates",
"description": "Update status if an error was encountered checking for updates."
},
"optionsBridgeUpdateAvailable": {
"message": "An update is available:",
"description": "Update status if an update was found."
},
"optionsBridgeUpdate": {
"message": "Update Now...",
"description": "Update now button title. Ellipsis indicates additional information as it triggers an update window popup."
},
, "optionsBridgeBackupEnabled": {
"message": "Enable backup daemon connection on $hostPort$"
, "description": "Backup daemon checkbox label. Host/port inputs are inserted inline at the hostPort substitution."
, "placeholders": {
"optionsBridgeBackupEnabled": {
"message": "Enable backup daemon connection on $hostPort$",
"description": "Backup daemon checkbox label. Host/port inputs are inserted inline at the hostPort substitution.",
"placeholders": {
"hostPort": {
"content": "$1"
}
}
}
, "optionsBridgeBackupEnabledDescription": {
"message": "If the regular bridge connection fails, attempt to connect to a bridge running in daemon mode."
, "description": "Backup daemon checkbox description."
}
},
"optionsBridgeBackupEnabledDescription": {
"message": "If the regular bridge connection fails, attempt to connect to a bridge running in daemon mode.",
"description": "Backup daemon checkbox description."
},
, "optionsMediaCategoryName": {
"message": "Media casting"
, "description": "Options page media casting category title."
}
, "optionsMediaCategoryDescription": {
"message": "HTML5 video/audio media casting."
, "description": "Options page media casting category description."
}
, "optionsMediaEnabled": {
"message": "Enable media casting"
, "description": "Media casting enabled checkbox label."
}
, "optionsMediaSyncElement": {
"message": "Sync receiver state with media element"
, "description": "Media casting sync checkbox label."
}
, "optionsMediaSyncElementDescription": {
"message": "Synchronize state (playback, volume, captions, etc...) between the media element and the receiver device."
, "description": "Media casting sync option description."
}
, "optionsMediaStopOnUnload": {
"message": "Stop receiver playback on page unload"
, "description": "Media stop on unload checkbox label."
}
"optionsMediaCategoryName": {
"message": "Media casting",
"description": "Options page media casting category title."
},
"optionsMediaCategoryDescription": {
"message": "HTML5 video/audio media casting.",
"description": "Options page media casting category description."
},
"optionsMediaEnabled": {
"message": "Enable media casting",
"description": "Media casting enabled checkbox label."
},
"optionsMediaSyncElement": {
"message": "Sync receiver state with media element",
"description": "Media casting sync checkbox label."
},
"optionsMediaSyncElementDescription": {
"message": "Synchronize state (playback, volume, captions, etc...) between the media element and the receiver device.",
"description": "Media casting sync option description."
},
"optionsMediaStopOnUnload": {
"message": "Stop receiver playback on page unload",
"description": "Media stop on unload checkbox label."
},
, "optionsLocalMediaCategoryName": {
"message": "Local media casting"
, "description": "Options page local media category title."
}
, "optionsLocalMediaCategoryDescription": {
"message": "HTTP server started by the bridge app to stream local media files to the cast receiver."
, "description": "Options page local media category description."
}
, "optionsLocalMediaEnabled": {
"message": "Enable local media casting"
, "description": "Local media enabled checkbox label."
}
, "optionsLocalMediaServerPort": {
"message": "HTTP server port:"
, "description": "HTTP server port input label."
}
"optionsLocalMediaCategoryName": {
"message": "Local media casting",
"description": "Options page local media category title."
},
"optionsLocalMediaCategoryDescription": {
"message": "HTTP server started by the bridge app to stream local media files to the cast receiver.",
"description": "Options page local media category description."
},
"optionsLocalMediaEnabled": {
"message": "Enable local media casting",
"description": "Local media enabled checkbox label."
},
"optionsLocalMediaServerPort": {
"message": "HTTP server port:",
"description": "HTTP server port input label."
},
, "optionsReceiverSelectorCategoryName": {
"message": "Receiver selector"
, "description": "Options page receiver selector category title."
}
, "optionsReceiverSelectorCategoryDescription": {
"message": "Receiver device selection interface."
, "description": "Options page receiver selector category description."
}
, "optionsReceiverSelectorWaitForConnection": {
"message": "Wait for connection"
, "description": "Receiver selector wait for connection option checkbox label."
}
, "optionsReceiverSelectorWaitForConnectionDescription": {
"message": "Keep receiver selector open until the session is established or connection fails."
, "description": "Receiver selector wait for connection option description."
}
, "optionsReceiverSelectorCloseIfFocusLost": {
"message": "Close after losing focus"
, "description": "Receiver selector close if focus lost option checkbox label."
}
"optionsReceiverSelectorCategoryName": {
"message": "Receiver selector",
"description": "Options page receiver selector category title."
},
"optionsReceiverSelectorCategoryDescription": {
"message": "Receiver device selection interface.",
"description": "Options page receiver selector category description."
},
"optionsReceiverSelectorWaitForConnection": {
"message": "Wait for connection",
"description": "Receiver selector wait for connection option checkbox label."
},
"optionsReceiverSelectorWaitForConnectionDescription": {
"message": "Keep receiver selector open until the session is established or connection fails.",
"description": "Receiver selector wait for connection option description."
},
"optionsReceiverSelectorCloseIfFocusLost": {
"message": "Close after losing focus",
"description": "Receiver selector close if focus lost option checkbox label."
},
, "optionsUserAgentWhitelistCategoryName": {
"message": "User agent whitelist"
, "description": "Options page whitelist category title."
}
, "optionsUserAgentWhitelistCategoryDescription": {
"message": "Sites for which to replace the user agent with a Chrome version for compatibility. Must be valid match patterns."
, "description": "Options page whitelist category description."
}
, "optionsUserAgentWhitelistEnabled": {
"message": "Enable site whitelist"
, "description": "Whitelist enabled checkbox label."
}
, "optionsUserAgentWhitelistRestrictedEnabled": {
"message": "Enable restricted mode"
, "description": "Whitelist restricted mode enabled checkbox label."
}
, "optionsUserAgentWhitelistRestrictedEnabledDescription": {
"message": "Also apply whitelist restrictions to sites attempting to load cast functionality regardless of the current user agent."
, "description": "Whitelist restricted mode enabled description."
}
, "optionsUserAgentWhitelistContent": {
"message": "Match patterns:"
, "description": "Match patterns editor widget label."
}
, "optionsUserAgentWhitelistBasicView": {
"message": "Basic View"
, "description": "Switch to basic view button title."
}
, "optionsUserAgentWhitelistRawView": {
"message": "Raw View"
, "description": "Switch to raw view button title."
}
, "optionsUserAgentWhitelistSaveRaw": {
"message": "Save Raw"
, "description": "Save raw view edits button title."
}
, "optionsUserAgentWhitelistAddItem": {
"message": "Add Item"
, "description": "Add new whitelist item button title."
}
, "optionsUserAgentWhitelistEditItem": {
"message": "Edit"
, "description": "Edit whitelist item button title. Displayed on each item."
}
, "optionsUserAgentWhitelistRemoveItem": {
"message": "Remove"
, "description": "Remove whitelist item button title. Displayed on each item."
}
, "optionsUserAgentWhitelistInvalidMatchPattern": {
"message": "Invalid match pattern $matchPattern$"
, "description": "Error displayed by input indicating an invalid match pattern."
, "placeholders": {
"optionsUserAgentWhitelistCategoryName": {
"message": "User agent whitelist",
"description": "Options page whitelist category title."
},
"optionsUserAgentWhitelistCategoryDescription": {
"message": "Sites for which to replace the user agent with a Chrome version for compatibility. Must be valid match patterns.",
"description": "Options page whitelist category description."
},
"optionsUserAgentWhitelistEnabled": {
"message": "Enable site whitelist",
"description": "Whitelist enabled checkbox label."
},
"optionsUserAgentWhitelistRestrictedEnabled": {
"message": "Enable restricted mode",
"description": "Whitelist restricted mode enabled checkbox label."
},
"optionsUserAgentWhitelistRestrictedEnabledDescription": {
"message": "Also apply whitelist restrictions to sites attempting to load cast functionality regardless of the current user agent.",
"description": "Whitelist restricted mode enabled description."
},
"optionsUserAgentWhitelistContent": {
"message": "Match patterns:",
"description": "Match patterns editor widget label."
},
"optionsUserAgentWhitelistBasicView": {
"message": "Basic View",
"description": "Switch to basic view button title."
},
"optionsUserAgentWhitelistRawView": {
"message": "Raw View",
"description": "Switch to raw view button title."
},
"optionsUserAgentWhitelistSaveRaw": {
"message": "Save Raw",
"description": "Save raw view edits button title."
},
"optionsUserAgentWhitelistAddItem": {
"message": "Add Item",
"description": "Add new whitelist item button title."
},
"optionsUserAgentWhitelistEditItem": {
"message": "Edit",
"description": "Edit whitelist item button title. Displayed on each item."
},
"optionsUserAgentWhitelistRemoveItem": {
"message": "Remove",
"description": "Remove whitelist item button title. Displayed on each item."
},
"optionsUserAgentWhitelistInvalidMatchPattern": {
"message": "Invalid match pattern $matchPattern$",
"description": "Error displayed by input indicating an invalid match pattern.",
"placeholders": {
"matchPattern": {
"content": "$1"
, "example": "http://example"
"content": "$1",
"example": "http://example"
}
}
}
},
, "optionsMirroringCategoryName": {
"message": "Screen/tab casting"
, "description": "Options page mirroring category name."
}
, "optionsMirroringCategoryDescription": {
"message": "Mirroring to a Chromecast receiver app."
, "description": "Options page mirroring category description."
}
, "optionsMirroringEnabled": {
"message": "Enable screen/tab casting (experimental)"
, "description": "Mirroring enabled checkbox label."
}
, "optionsMirroringAppId": {
"message": "Mirroring app ID:"
, "description": "Mirroring app ID input label."
}
, "optionsMirroringAppIdDescription": {
"message": "App ID for a registered Chromecast receiver application. Advanced use only. Must be compatible with the default app (see GitHub repo)."
, "description": "Mirroring app ID option description."
}
"optionsMirroringCategoryName": {
"message": "Screen/tab casting",
"description": "Options page mirroring category name."
},
"optionsMirroringCategoryDescription": {
"message": "Mirroring to a Chromecast receiver app.",
"description": "Options page mirroring category description."
},
"optionsMirroringEnabled": {
"message": "Enable screen/tab casting (experimental)",
"description": "Mirroring enabled checkbox label."
},
"optionsMirroringAppId": {
"message": "Mirroring app ID:",
"description": "Mirroring app ID input label."
},
"optionsMirroringAppIdDescription": {
"message": "App ID for a registered Chromecast receiver application. Advanced use only. Must be compatible with the default app (see GitHub repo).",
"description": "Mirroring app ID option description."
},
, "optionsOptionRecommended": {
"message": "recommended"
, "description": "Badge next to option label indicating boolean option is recommended enabled."
}
"optionsOptionRecommended": {
"message": "recommended",
"description": "Badge next to option label indicating boolean option is recommended enabled."
},
, "optionsReset": {
"message": "Restore Defaults"
, "description": "Restore default options button label."
}
, "optionsSave": {
"message": "Save"
, "description": "Save options button label."
}
, "optionsSaved": {
"message": "Saved!"
, "description": "Status text displayed by save button once options have been successfully saved."
"optionsReset": {
"message": "Restore Defaults",
"description": "Restore default options button label."
},
"optionsSave": {
"message": "Save",
"description": "Save options button label."
},
"optionsSaved": {
"message": "Saved!",
"description": "Status text displayed by save button once options have been successfully saved."
}
}

View File

@@ -6,11 +6,11 @@ import logger from "../lib/logger";
import options from "../lib/options";
import bridge, { BridgeInfo } from "../lib/bridge";
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
import { RemoteMatchPattern } from "../lib/matchPattern";
import CastManager from "./CastManager";
import receiverDevices from "./receiverDevices";
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
import { initMenus } from "./menus";
import { initWhitelist } from "./whitelist";
@@ -41,7 +41,6 @@ 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

View File

@@ -23,6 +23,12 @@ interface ReceiverSelectorEvents {
stop: ReceiverSelectionStop;
}
export interface PageInfo {
url: string;
tabId: number;
frameId: number;
}
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
private windowId?: number;
@@ -36,6 +42,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
private wasReceiverSelected = false;
private appId?: string;
private pageInfo?: PageInfo;
#isOpen = false;
@@ -65,9 +72,11 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
receivers: ReceiverDevice[],
defaultMediaType: ReceiverSelectorMediaType,
availableMediaTypes: ReceiverSelectorMediaType,
appId?: string
appId?: string,
pageInfo?: PageInfo
): Promise<void> {
this.appId = appId;
this.pageInfo = pageInfo;
// If popup already exists, close it
if (this.windowId) {
@@ -176,7 +185,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
this.messagePort.postMessage({
subject: "popup:init",
data: { appId: this.appId }
data: { appId: this.appId, pageInfo: this.pageInfo }
});
this.messagePort.postMessage({

View File

@@ -66,13 +66,16 @@ async function getSelection(
let defaultMediaType = ReceiverSelectorMediaType.Tab;
let availableMediaTypes;
let pageUrl: string | undefined;
try {
const { url } = await browser.webNavigation.getFrame({
tabId: contextTabId,
frameId: contextFrameId
});
pageUrl = (
await browser.webNavigation.getFrame({
tabId: contextTabId,
frameId: contextFrameId
})
).url;
availableMediaTypes = getMediaTypesForPageUrl(url);
availableMediaTypes = getMediaTypesForPageUrl(pageUrl);
} catch {
logger.error(
"Failed to locate frame, falling back to default available media types."
@@ -213,11 +216,20 @@ async function getSelection(
// Ensure status manager is initialized
await receiverDevices.init();
const pageInfo = pageUrl
? {
url: pageUrl,
tabId: contextTabId,
frameId: contextFrameId
}
: undefined;
sharedSelector.open(
receiverDevices.getDevices(),
defaultMediaType,
availableMediaTypes,
castInstance?.appId
castInstance?.appId,
pageInfo
);
});
}

View File

@@ -2,7 +2,7 @@
const _ = browser.i18n.getMessage;
interface KnownApp {
export interface KnownApp {
name: string;
matches?: string;
}

136
ext/src/lib/matchPattern.ts Normal file
View File

@@ -0,0 +1,136 @@
"use strict";
const WILDCARD_SCHEMES = ["http", "https", "ws", "wss"];
export const REMOTE_MATCH_PATTERN_REGEX =
/^(?:(?:(\*|https?|wss?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
/**
* Partial implementation of WebExtension match patterns. Only handles
* remote patterns, as we don't need local matching and it's more
* complex to implement.
*/
export class RemoteMatchPattern {
private partScheme: string;
private partHost: string;
private partPath: string;
/** Matching schemes */
private schemes: string[] = [];
/** Base domain for subdomain matching */
private domain?: string;
/** Host part includes subdomain wildcard */
private matchSubdomains = false;
constructor(public pattern: string) {
// Parse match pattern parts
const matches = pattern.match(REMOTE_MATCH_PATTERN_REGEX);
if (!matches) {
throw new Error("Invalid match pattern");
}
[, this.partScheme, this.partHost, this.partPath] = matches;
if (pattern === "<all_urls>") {
this.schemes = WILDCARD_SCHEMES;
return;
}
// Scheme
this.schemes =
this.partScheme === "*" ? WILDCARD_SCHEMES : [this.partScheme];
// Host
if (this.partHost.startsWith("*.")) {
this.domain = this.partHost.slice(2);
this.matchSubdomains = true;
} else if (this.partHost !== "*") {
this.domain = this.partHost;
}
}
/**
* Test domain string against match pattern.
*/
private matchesDomain(domain: string) {
// If wildcard or exact match
if (this.partHost === "*" || this.domain === domain) {
return true;
}
if (this.matchSubdomains) {
// Should exist here
if (!this.domain) return false;
// Starting offset of pattern in url host string
const offset = domain.length - this.domain.length;
if (
offset > 0 &&
domain[offset - 1] === "." &&
domain.slice(offset) === this.domain
) {
return true;
}
}
return false;
}
/**
* Tests URL string against match pattern and returns boolean
* result.
*/
matches(urlString: string) {
let url: URL;
try {
url = new URL(urlString);
} catch (err) {
return false;
}
// If URL does not have a matching scheme
if (!this.schemes.includes(url.protocol.slice(0, -1))) {
return false;
}
// If pattern host is not a wildcard
if (!this.matchesDomain(url.host)) {
return false;
}
const urlPath = `${url.pathname}${url.search}`;
// If pattern path is not a wildcard
if (this.partPath !== "/*") {
// And if paths don't match
if (this.partPath !== urlPath) {
const specialChars = ".+*?^${}()|[]\\";
/**
* Create regular expression from pattern path, escaping
* any special characters.
*/
let pathRegexString = "";
for (const c of this.partPath) {
if (c === "*") {
pathRegexString += ".*";
} else {
if (specialChars.includes(c)) {
pathRegexString += "\\";
}
pathRegexString += c;
}
}
// Test compiled expression against path
if (!new RegExp(`^${pathRegexString}$`).test(urlPath)) {
return false;
}
}
}
return true;
}
}

View File

@@ -111,9 +111,6 @@ export function getWindowCenteredProps(
};
}
export const REMOTE_MATCH_PATTERN_REGEX =
/^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
export function loadScript(
scriptUrl: string,
doc: Document = document
@@ -122,7 +119,7 @@ export function loadScript(
const scriptEl = doc.createElement("script");
scriptEl.src = scriptUrl;
(doc.head || doc.documentElement).append(scriptEl);
scriptEl.addEventListener("load", () => resolve(scriptEl));
scriptEl.addEventListener("error", () => reject());
});

View File

@@ -40,6 +40,11 @@ import { ReceiverDevice } from "./types";
type ExtMessageDefinitions = {
"popup:init": {
appId?: string;
pageInfo?: {
url: string;
tabId: number;
frameId: number;
};
};
"popup:update": {
receivers: ReceiverDevice[];

View File

@@ -12,7 +12,7 @@ import EditableList from "./EditableList";
import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge";
import logger from "../../lib/logger";
import options, { Options } from "../../lib/options";
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/utils";
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/matchPattern";
const _ = browser.i18n.getMessage;

View File

@@ -2,14 +2,13 @@
--shadow-10: 0 1px 4px rgba(12, 12, 13, 0.1);
--shadow-20: 0 2px 8px rgba(12, 12, 13, 0.1);
--shadow-30: 0 4px 16px rgba(12, 12, 13, 0.1);
--focus-border-color: var(--blue-50);
--box-background: var(--white-100);
--box-color: var(--grey-90);
--focus-box-shadow:
0 0 0 1px var(--focus-border-color);
--focus-box-shadow: 0 0 0 1px var(--focus-border-color);
--button-background: var(--grey-90-a10);
--button-background-hover: var(--grey-90-a20);
@@ -26,12 +25,10 @@
--field-border-color: var(--grey-90-a20);
--field-border-color-hover: var(--grey-90-a30);
--field-box-shadow-warning:
0 0 0 1px var(--yellow-60)
, 0 0 0 4px var(--yellow-60-a30);
--field-box-shadow-error:
0 0 0 1px var(--red-60)
, 0 0 0 4px var(--red-60-a30);
--field-box-shadow-warning: 0 0 0 1px var(--yellow-60),
0 0 0 4px var(--yellow-60-a30);
--field-box-shadow-error: 0 0 0 1px var(--red-60),
0 0 0 4px var(--red-60-a30);
}
@media (prefers-color-scheme: dark) {
@@ -102,7 +99,9 @@ textarea:invalid {
}
button:disabled,
input:disabled {
input:disabled,
textarea:disabled,
select:disabled {
opacity: 0.35;
}
@@ -111,6 +110,7 @@ input,
textarea,
select {
padding: 4px 8px;
font: inherit;
}
/* No inset for spinbox control */
@@ -130,13 +130,14 @@ button:default:hover:active {
}
.select-wrapper {
--arrow-width: 20px;
--arrow-width: 16px;
position: relative;
display: inline-block;
}
.select-wrapper::after {
align-items: center;
content: "▼";
opacity: 0.5;
display: flex;
height: 100%;
margin-right: 4px;

View File

@@ -4,17 +4,19 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import knownApps from "../../cast/knownApps";
import knownApps, { KnownApp } from "../../cast/knownApps";
import options from "../../lib/options";
import messaging, { Message, Port } from "../../messaging";
import { getNextEllipsis } from "../../lib/utils";
import { RemoteMatchPattern } from "../../lib/matchPattern";
import { ReceiverDevice } from "../../types";
import {
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "../../background/receiverSelector";
import { PageInfo } from "../../background/receiverSelector/ReceiverSelector";
const _ = browser.i18n.getMessage;
@@ -37,8 +39,14 @@ interface PopupAppState {
filePath?: string;
appId?: string;
pageInfo?: PageInfo;
mirroringEnabled: boolean;
userAgentWhitelistEnabled: boolean;
userAgentWhitelist: string[];
knownApp?: KnownApp;
isPageWhitelisted: boolean;
}
class PopupApp extends Component<PopupAppProps, PopupAppState> {
@@ -54,7 +62,10 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
mediaType: ReceiverSelectorMediaType.App,
availableMediaTypes: ReceiverSelectorMediaType.App,
isLoading: false,
mirroringEnabled: false
mirroringEnabled: false,
userAgentWhitelistEnabled: true,
userAgentWhitelist: [],
isPageWhitelisted: false
};
// Store window ref
@@ -66,6 +77,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
this.updateWindowHeight();
}).observe(document.body);
this.onAddToWhitelist = this.onAddToWhitelist.bind(this);
this.onSelectChange = this.onSelectChange.bind(this);
this.onCast = this.onCast.bind(this);
this.onStop = this.onStop.bind(this);
@@ -91,7 +103,8 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
switch (message.subject) {
case "popup:init": {
this.setState({
appId: message.data?.appId
appId: message.data?.appId,
pageInfo: message.data?.pageInfo
});
break;
@@ -114,6 +127,8 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
});
}
this.updateKnownApp();
break;
}
@@ -124,9 +139,15 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
}
});
const opts = await options.getAll();
this.setState({
mirroringEnabled: await options.get("mirroringEnabled")
mirroringEnabled: opts.mirroringEnabled,
userAgentWhitelistEnabled: opts.userAgentWhitelistEnabled,
userAgentWhitelist: opts.userAgentWhitelist
});
this.updateKnownApp();
}
public componentDidUpdate() {
@@ -135,6 +156,58 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
}, 1);
}
private updateKnownApp() {
const isAppMediaTypeAvailable = !!(
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
);
let knownApp: Nullable<KnownApp> = null;
/**
* Check knownApps for an app with an ID matching the registered
* app on the target page.
* Or if there isn't an registered app, check for an app with a
* match pattern matching the target page URL.
*/
if (isAppMediaTypeAvailable && this.state.appId) {
knownApp = knownApps[this.state.appId];
} else if (this.state.pageInfo) {
const pageUrl = this.state.pageInfo.url;
for (const [, app] of Object.entries(knownApps)) {
if (!app.matches) {
continue;
}
const pattern = new RemoteMatchPattern(app.matches);
if (pattern.matches(pageUrl)) {
knownApp = app;
break;
}
}
}
let isPageWhitelisted = false;
/**
* Check if target page URL is whitelisted.
*/
if (this.state.pageInfo) {
for (const patternString of this.state.userAgentWhitelist) {
const pattern = new RemoteMatchPattern(patternString);
if (pattern.matches(this.state.pageInfo.url)) {
isPageWhitelisted = true;
break;
}
}
}
this.setState({
knownApp: knownApp ?? undefined,
isPageWhitelisted
});
}
public render() {
/*
@@ -166,9 +239,42 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
);
return (
<>
<div
className="whitelist-suggest"
hidden={
// If we don't know the app
!this.state.knownApp ||
// If the whitelist is disabled
!this.state.userAgentWhitelistEnabled ||
// If the whitelist is enabled, and the page is whitelisted
(this.state.userAgentWhitelistEnabled &&
this.state.isPageWhitelisted) ||
// If an app is already loaded on the page
isAppMediaTypeAvailable
}
>
<img src="photon_info.svg" />
{_(
"popupWhitelistNotWhitelisted",
this.state.knownApp?.name
)}
<button
onClick={() => {
if (!this.state.knownApp || !this.state.pageInfo) {
return;
}
this.onAddToWhitelist(
this.state.knownApp,
this.state.pageInfo
);
}}
>
{_("popupWhitelistAddToWhitelist")}
</button>
</div>
<div className="media-select">
<div className="media-select__label-cast">
{_("popupMediaSelectCastLabel")}
@@ -187,8 +293,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
selected={isAppMediaTypeSelected}
disabled={!isAppMediaTypeAvailable}
>
{(this.state.appId &&
knownApps[this.state.appId]?.name) ??
{this.state.knownApp?.name ??
_("popupMediaTypeApp")}
</option>
@@ -248,6 +353,21 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
);
}
private async onAddToWhitelist(app: KnownApp, pageInfo: PageInfo) {
if (!app.matches) {
return;
}
const whitelist = await options.get("userAgentWhitelist");
if (!whitelist.includes(app.matches)) {
whitelist.push(app.matches);
await options.set("userAgentWhitelist", whitelist);
await browser.tabs.reload(pageInfo.tabId);
window.close();
}
}
private onCast(receiver: ReceiverDevice) {
this.setState({
isLoading: true

View File

@@ -0,0 +1,13 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<style>
@media (prefers-color-scheme: dark) {
path {
fill: rgba(249, 249, 250, .8);
}
}
</style>
<path fill="rgba(12, 12, 13, .8)" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm0-7a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1zm0-3.188A1.188 1.188 0 1 0 9.188 5 1.188 1.188 0 0 0 8 3.812z"></path>
</svg>

After

Width:  |  Height:  |  Size: 710 B

View File

@@ -1,9 +1,3 @@
:root {
--button-background: #474749;
--button-background-hover: #505054;
--button-background-active: #5c5c5e;
}
body {
background: var(--grey-10);
color: var(--grey-90);
@@ -12,6 +6,10 @@ body {
font-size: 13px;
}
[hidden] {
display: none !important;
}
@media (prefers-color-scheme: dark) {
body {
background: var(--grey-80) !important;
@@ -26,8 +24,35 @@ body {
}
}
.media-select {
.whitelist-suggest {
align-items: center;
background-color: var(--blue-50-a30);
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
display: flex;
font-size: 0.9em;
gap: 0.5em;
padding: 0.75em;
}
.whitelist-suggest > button {
--button-background: hsla(0, 0%, 50%, 0.3);
--button-background-hover: hsla(0, 0%, 30%, 0.3);
--button-background-active: hsla(0, 0%, 10%, 0.3);
margin-left: auto;
}
@media (prefers-color-scheme: dark) {
.whitelist-suggest {
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
}
.whitelist-suggest > button {
--button-background: hsla(0, 0%, 50%, 0.3);
--button-background-hover: hsla(0, 0%, 70%, 0.3);
--button-background-active: hsla(0, 0%, 90%, 0.3);
}
}
.media-select {
align-items: baseline;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
display: flex;
margin: 0 1em;
@@ -45,11 +70,6 @@ body {
margin-inline-start: 0.5em;
}
.media-select__dropdown {
padding-top: 2px;
padding-bottom: 2px;
}
.receivers {
list-style: none;
margin: initial;