mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Implement receiver selector whitelist suggestion banner
This commit is contained in:
@@ -10,67 +10,80 @@ 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 `$\`.
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
136
ext/src/lib/matchPattern.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
--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;
|
||||
|
||||
@@ -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
|
||||
|
||||
13
ext/src/ui/popup/photon_info.svg
Normal file
13
ext/src/ui/popup/photon_info.svg
Normal 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 |
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user