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

@@ -2,75 +2,88 @@
Contributions welcome.
Implementation notes are at [IMPLEMENTATION.md](IMPLEMENTATION.md).
Implementation notes are at [IMPLEMENTATION.md](IMPLEMENTATION.md).
If you're unsure about anything, feel free to ask.
Just submit a PR for small changes (bugfixes, typos, etc...). Comment first on existing
Just submit a PR for small changes (bugfixes, typos, etc...). Comment first on existing
issues if you're going to work on something to avoid duplication of effort.
Submit an issue for new features before submitting a PR.
## Bug Reports
Follow the bug report issue template and provide as much info as possible. Logs can be found in various locations depending on the component at fault:
* https://developer.mozilla.org/en-US/docs/Tools/Web_Console
* https://developer.mozilla.org/en-US/docs/Tools/Browser_Console
* https://extensionworkshop.com/documentation/develop/debugging/
- https://developer.mozilla.org/en-US/docs/Tools/Web_Console
- https://developer.mozilla.org/en-US/docs/Tools/Browser_Console
- https://extensionworkshop.com/documentation/develop/debugging/
## Compatibility Reports
Compatibility reports are always helpful. They're tracked in a separate repository, so go to [@hensm/fx_cast-compat](https://github.com/hensm/fx_cast-compat) and use the "Compatibility Report" issue template. Ensure you have a working environment and that the site is in the whitelist (check options page).
## Localizations
Either fork and edit the messages files manually or to easily add/edit localizations, use the web-ext-translator tool:
https://lusito.github.io/web-ext-translator/?gh=https://github.com/hensm/fx_cast/
Missing/outdated strings:
* `de`
* `optionsMirroringCategoryName`
* `optionsMirroringCategoryDescription`
* `optionsMirroringEnabled`
* `optionsMirroringAppId`
* `popupMediaTypeAppNotFound`
* `optionsBridgeCompatible`
* `optionsBridgeLikelyCompatible`
* `optionsBridgeIncompatible`
- `de`
* `nl`
* `optionsBridgeBackupEnabled`
* `optionsUserAgentWhitelistRestrictedEnabled`
* `optionsUserAgentWhitelistRestrictedEnabledDescription`
* `optionsOptionRecommended`
* `optionsMirroringCategoryName`
* `optionsMirroringCategoryDescription`
* `optionsMirroringEnabled`
* `optionsMirroringAppId`
* `popupMediaTypeAppNotFound`
* `optionsBridgeCompatible`
* `optionsBridgeLikelyCompatible`
* `optionsBridgeIncompatible`
- `optionsMirroringCategoryName`
- `optionsMirroringCategoryDescription`
- `optionsMirroringEnabled`
- `optionsMirroringAppId`
- `popupWhitelistNotWhitelisted`
- `popupWhitelistAddToWhitelist`
- `popupMediaTypeAppNotFound`
- `optionsBridgeCompatible`
- `optionsBridgeLikelyCompatible`
- `optionsBridgeIncompatible`
- `es`
- `popupWhitelistNotWhitelisted`
- `popupWhitelistAddToWhitelist`
- `nl`
- `optionsBridgeBackupEnabled`
- `optionsUserAgentWhitelistRestrictedEnabled`
- `optionsUserAgentWhitelistRestrictedEnabledDescription`
- `optionsOptionRecommended`
- `optionsMirroringCategoryName`
- `optionsMirroringCategoryDescription`
- `optionsMirroringEnabled`
- `optionsMirroringAppId`
- `popupWhitelistNotWhitelisted`
- `popupWhitelistAddToWhitelist`
- `popupMediaTypeAppNotFound`
- `optionsBridgeCompatible`
- `optionsBridgeLikelyCompatible`
- `optionsBridgeIncompatible`
- `no`
- `popupWhitelistNotWhitelisted`
- `popupWhitelistAddToWhitelist`
### NSIS Installer Localization
If you're comfortable editing and compiling NSIS installer scripts, use the following guide, otherwise just provide translated strings in an issue or PR comment.
To localize Windows installer strings, first add the relevant `MUI_LANGUAGE` macro to the end of the existing list (list of language names can be found [here](https://sourceforge.net/p/nsis/code/HEAD/tree/NSIS/trunk/Contrib/Language%20files/)):
````nsi
```nsi
!insertmacro MUI_LANGUAGE "German"
````
```
Then, provide each version of the existing `LangString` commands with that language grouped under the existing strings:
````nsi
```nsi
LangString MSG__EXAMPLE_STRING1 ${LANG_GERMAN} "Hallo"
LangString MSG__EXAMPLE_STRING2 ${LANG_GERMAN} "Welt"
````
```
Try to keep the line length under 80 characters by splitting lines within the string with a backslash at the end of the line and a double indent on the next line. To escape characters (like other double quotes), prepend with a `$\`.

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;