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:
@@ -2,75 +2,88 @@
|
|||||||
|
|
||||||
Contributions welcome.
|
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.
|
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.
|
issues if you're going to work on something to avoid duplication of effort.
|
||||||
|
|
||||||
Submit an issue for new features before submitting a PR.
|
Submit an issue for new features before submitting a PR.
|
||||||
|
|
||||||
|
|
||||||
## Bug Reports
|
## 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:
|
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/Web_Console
|
||||||
* https://developer.mozilla.org/en-US/docs/Tools/Browser_Console
|
- https://developer.mozilla.org/en-US/docs/Tools/Browser_Console
|
||||||
* https://extensionworkshop.com/documentation/develop/debugging/
|
- https://extensionworkshop.com/documentation/develop/debugging/
|
||||||
|
|
||||||
|
|
||||||
## Compatibility Reports
|
## 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).
|
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
|
## Localizations
|
||||||
|
|
||||||
Either fork and edit the messages files manually or to easily add/edit localizations, use the web-ext-translator tool:
|
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/
|
https://lusito.github.io/web-ext-translator/?gh=https://github.com/hensm/fx_cast/
|
||||||
|
|
||||||
|
|
||||||
Missing/outdated strings:
|
Missing/outdated strings:
|
||||||
|
|
||||||
* `de`
|
- `de`
|
||||||
* `optionsMirroringCategoryName`
|
|
||||||
* `optionsMirroringCategoryDescription`
|
|
||||||
* `optionsMirroringEnabled`
|
|
||||||
* `optionsMirroringAppId`
|
|
||||||
* `popupMediaTypeAppNotFound`
|
|
||||||
* `optionsBridgeCompatible`
|
|
||||||
* `optionsBridgeLikelyCompatible`
|
|
||||||
* `optionsBridgeIncompatible`
|
|
||||||
|
|
||||||
* `nl`
|
- `optionsMirroringCategoryName`
|
||||||
* `optionsBridgeBackupEnabled`
|
- `optionsMirroringCategoryDescription`
|
||||||
* `optionsUserAgentWhitelistRestrictedEnabled`
|
- `optionsMirroringEnabled`
|
||||||
* `optionsUserAgentWhitelistRestrictedEnabledDescription`
|
- `optionsMirroringAppId`
|
||||||
* `optionsOptionRecommended`
|
- `popupWhitelistNotWhitelisted`
|
||||||
* `optionsMirroringCategoryName`
|
- `popupWhitelistAddToWhitelist`
|
||||||
* `optionsMirroringCategoryDescription`
|
- `popupMediaTypeAppNotFound`
|
||||||
* `optionsMirroringEnabled`
|
- `optionsBridgeCompatible`
|
||||||
* `optionsMirroringAppId`
|
- `optionsBridgeLikelyCompatible`
|
||||||
* `popupMediaTypeAppNotFound`
|
- `optionsBridgeIncompatible`
|
||||||
* `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
|
### 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.
|
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/)):
|
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"
|
!insertmacro MUI_LANGUAGE "German"
|
||||||
````
|
```
|
||||||
|
|
||||||
Then, provide each version of the existing `LangString` commands with that language grouped under the existing strings:
|
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_STRING1 ${LANG_GERMAN} "Hallo"
|
||||||
LangString MSG__EXAMPLE_STRING2 ${LANG_GERMAN} "Welt"
|
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 `$\`.
|
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": {
|
"extensionName": {
|
||||||
"message": "fx_cast"
|
"message": "fx_cast",
|
||||||
, "description": "Name of the extension and the native receiver selector window title."
|
"description": "Name of the extension and the native receiver selector window title."
|
||||||
}
|
},
|
||||||
, "extensionDescription": {
|
"extensionDescription": {
|
||||||
"message": "Enables Chromecast support for casting web apps (like Netflix or BBC iPlayer), HTML5 video and screen/tab sharing."
|
"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."
|
"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": {
|
"popupMediaSelectCastLabel": {
|
||||||
"message": "this site's app"
|
"message": "Cast",
|
||||||
, "description": "Receiver selector media type <option> text for current site's sender application."
|
"description": "(Cast) <select> to:"
|
||||||
}
|
},
|
||||||
, "popupMediaTypeAppNotFound": {
|
"popupMediaSelectToLabel": {
|
||||||
"message": "this site's app (not found)"
|
"message": "to:",
|
||||||
, "description": "Receiver selector media type <option> text for current site's sender application if none found."
|
"description": "Cast <select> (to:)"
|
||||||
}
|
},
|
||||||
, "popupMediaTypeAppMedia": {
|
"popupNoReceiversFound": {
|
||||||
"message": "this media"
|
"message": "No receiver devices found",
|
||||||
, "description": "Receiver selector media type <option> text for media casting."
|
"description": "Message displayed in the receiver selector if there are no available receivers."
|
||||||
}
|
},
|
||||||
, "popupMediaTypeTab": {
|
"popupCastButtonTitle": {
|
||||||
"message": "Tab"
|
"message": "Cast",
|
||||||
, "description": "Receiver selector media type <option> text for current tab."
|
"description": "Button text for each receiver entry in the receiver selector."
|
||||||
}
|
},
|
||||||
, "popupMediaTypeScreen": {
|
"popupCastingButtonTitle": {
|
||||||
"message": "Screen"
|
"message": "Casting$ellipsis$",
|
||||||
, "description": "Receiver selector media type <option> text for screen."
|
"description": "Button text while establishing a session in the receiver selector. Ellipsis cycles (. → .. → ...) as loading indicator.",
|
||||||
}
|
"placeholders": {
|
||||||
, "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": {
|
|
||||||
"ellipsis": {
|
"ellipsis": {
|
||||||
"content": "$1"
|
"content": "$1",
|
||||||
, "example": "..."
|
"example": "..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
, "popupStopButtonTitle": {
|
"popupStopButtonTitle": {
|
||||||
"message": "Stop"
|
"message": "Stop",
|
||||||
, "description": "Alternate action button text displayed instead of popupCastButtonTitle."
|
"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": {
|
"contextAddToWhitelist": {
|
||||||
"message": "Cast..."
|
"message": "Add to Whitelist",
|
||||||
, "description": "Main context menu item title. Ellipsis indicates additional information required as it triggers opening of receiver selector."
|
"description": "Top-level whitelist context menu item title."
|
||||||
}
|
},
|
||||||
|
"contextAddToWhitelistRecommended": {
|
||||||
, "contextAddToWhitelist": {
|
"message": "Add $matchPattern$ (Recommended)",
|
||||||
"message": "Add to Whitelist"
|
"description": "Context menu item title for recomended match pattern.",
|
||||||
, "description": "Top-level whitelist context menu item title."
|
"placeholders": {
|
||||||
}
|
|
||||||
, "contextAddToWhitelistRecommended": {
|
|
||||||
"message": "Add $matchPattern$ (Recommended)"
|
|
||||||
, "description": "Context menu item title for recomended match pattern."
|
|
||||||
, "placeholders": {
|
|
||||||
"matchPattern": {
|
"matchPattern": {
|
||||||
"content": "$1"
|
"content": "$1",
|
||||||
, "example": "https://example.com/*"
|
"example": "https://example.com/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
, "contextAddToWhitelistAdvancedAdd": {
|
"contextAddToWhitelistAdvancedAdd": {
|
||||||
"message": "Add $matchPattern$"
|
"message": "Add $matchPattern$",
|
||||||
, "description": "Context menu item title for all other match patterns."
|
"description": "Context menu item title for all other match patterns.",
|
||||||
, "placeholders": {
|
"placeholders": {
|
||||||
"matchPattern": {
|
"matchPattern": {
|
||||||
"content": "$1"
|
"content": "$1",
|
||||||
, "example": "*://*.example.com/*"
|
"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": {
|
"optionsBridgeStatsName": {
|
||||||
"message": "Loading bridge info..."
|
"message": "Name:",
|
||||||
, "description": "Loading placeholder text for bridge section on options page."
|
"description": "Bridge stats name row title."
|
||||||
}
|
},
|
||||||
, "optionsBridgeFoundStatusTitle": {
|
"optionsBridgeStatsVersion": {
|
||||||
"message": "Bridge found"
|
"message": "Version:",
|
||||||
, "description": "Bridge OK status title text."
|
"description": "Bridge stats version row title."
|
||||||
}
|
},
|
||||||
, "optionsBridgeIssueStatusTitle": {
|
"optionsBridgeStatsExpectedVersion": {
|
||||||
"message": "Bridge issue"
|
"message": "Expected version:",
|
||||||
, "description": "Bridge error status title text."
|
"description": "Bridge stats expected version row title."
|
||||||
}
|
},
|
||||||
, "optionsBridgeNotFoundStatusTitle": {
|
"optionsBridgeStatsCompatibility": {
|
||||||
"message": "Bridge not found"
|
"message": "Compatibility:",
|
||||||
, "description": "Bridge missing status title text."
|
"description": "Bridge stats compatibility row title."
|
||||||
}
|
},
|
||||||
, "optionsBridgeNotFoundStatusText": {
|
"optionsBridgeStatsRecommendedAction": {
|
||||||
"message": "Try downloading and installing the latest version."
|
"message": "Recommended action:",
|
||||||
, "description": "Bridge not found additional description text"
|
"description": "Bridge stats recommended action row title."
|
||||||
}
|
},
|
||||||
|
"optionsBridgeCompatible": {
|
||||||
, "optionsBridgeStatsName": {
|
"message": "Compatible",
|
||||||
"message": "Name:"
|
"description": "Compatibility status is definitely compatible."
|
||||||
, "description": "Bridge stats name row title."
|
},
|
||||||
}
|
"optionsBridgeLikelyCompatible": {
|
||||||
, "optionsBridgeStatsVersion": {
|
"message": "Likely compatible",
|
||||||
"message": "Version:"
|
"description": "Compatibility status is probably compatible."
|
||||||
, "description": "Bridge stats version row title."
|
},
|
||||||
}
|
"optionsBridgeIncompatible": {
|
||||||
, "optionsBridgeStatsExpectedVersion": {
|
"message": "Incompatible",
|
||||||
"message": "Expected version:"
|
"description": "Compatibility status is definitely incompatible."
|
||||||
, "description": "Bridge stats expected version row title."
|
},
|
||||||
}
|
"optionsBridgeOlderAction": {
|
||||||
, "optionsBridgeStatsCompatibility": {
|
"message": "Bridge version older than expected, try updating bridge to the latest version.",
|
||||||
"message": "Compatibility:"
|
"description": "Recommended action for when the installed bridge version is older than the installed extension version."
|
||||||
, "description": "Bridge stats compatibility row title."
|
},
|
||||||
}
|
"optionsBridgeNewerAction": {
|
||||||
, "optionsBridgeStatsRecommendedAction": {
|
"message": "Bridge version newer than expected, try updating extension to the latest version.",
|
||||||
"message": "Recommended action:"
|
"description": "Recommended action for when the installed bridge version is newer than the installed extension version."
|
||||||
, "description": "Bridge stats recommended action row title."
|
},
|
||||||
}
|
"optionsBridgeNoAction": {
|
||||||
, "optionsBridgeCompatible": {
|
"message": "No action needed.",
|
||||||
"message": "Compatible"
|
"description": "Recommended action for when both bridge and extension versions are compatible or likely compatible."
|
||||||
, "description": "Compatibility status is definitely compatible."
|
},
|
||||||
}
|
"optionsBridgeUpdateCheck": {
|
||||||
, "optionsBridgeLikelyCompatible": {
|
"message": "Check for Updates",
|
||||||
"message": "Likely compatible"
|
"description": "Update check button title."
|
||||||
, "description": "Compatibility status is probably compatible."
|
},
|
||||||
}
|
"optionsBridgeUpdateChecking": {
|
||||||
, "optionsBridgeIncompatible": {
|
"message": "Checking for Updates$ellipsis$",
|
||||||
"message": "Incompatible"
|
"description": "Update check button title while in progress. Ellipsis cycles (. → .. → ...) as loading indicator.",
|
||||||
, "description": "Compatibility status is definitely incompatible."
|
"placeholders": {
|
||||||
}
|
|
||||||
, "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": {
|
"ellipsis": {
|
||||||
"content": "$1"
|
"content": "$1",
|
||||||
, "example": ".."
|
"example": ".."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
, "optionsBridgeUpdateStatusNoUpdates": {
|
"optionsBridgeUpdateStatusNoUpdates": {
|
||||||
"message": "No updates available"
|
"message": "No updates available",
|
||||||
, "description": "Update status if no updates are found."
|
"description": "Update status if no updates are found."
|
||||||
}
|
},
|
||||||
, "optionsBridgeUpdateStatusError": {
|
"optionsBridgeUpdateStatusError": {
|
||||||
"message": "Error checking for updates"
|
"message": "Error checking for updates",
|
||||||
, "description": "Update status if an error was encountered checking for updates."
|
"description": "Update status if an error was encountered checking for updates."
|
||||||
}
|
},
|
||||||
, "optionsBridgeUpdateAvailable": {
|
"optionsBridgeUpdateAvailable": {
|
||||||
"message": "An update is available:"
|
"message": "An update is available:",
|
||||||
, "description": "Update status if an update was found."
|
"description": "Update status if an update was found."
|
||||||
}
|
},
|
||||||
, "optionsBridgeUpdate": {
|
"optionsBridgeUpdate": {
|
||||||
"message": "Update Now..."
|
"message": "Update Now...",
|
||||||
, "description": "Update now button title. Ellipsis indicates additional information as it triggers an update window popup."
|
"description": "Update now button title. Ellipsis indicates additional information as it triggers an update window popup."
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsBridgeBackupEnabled": {
|
"optionsBridgeBackupEnabled": {
|
||||||
"message": "Enable backup daemon connection on $hostPort$"
|
"message": "Enable backup daemon connection on $hostPort$",
|
||||||
, "description": "Backup daemon checkbox label. Host/port inputs are inserted inline at the hostPort substitution."
|
"description": "Backup daemon checkbox label. Host/port inputs are inserted inline at the hostPort substitution.",
|
||||||
, "placeholders": {
|
"placeholders": {
|
||||||
"hostPort": {
|
"hostPort": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
, "optionsBridgeBackupEnabledDescription": {
|
"optionsBridgeBackupEnabledDescription": {
|
||||||
"message": "If the regular bridge connection fails, attempt to connect to a bridge running in daemon mode."
|
"message": "If the regular bridge connection fails, attempt to connect to a bridge running in daemon mode.",
|
||||||
, "description": "Backup daemon checkbox description."
|
"description": "Backup daemon checkbox description."
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsMediaCategoryName": {
|
"optionsMediaCategoryName": {
|
||||||
"message": "Media casting"
|
"message": "Media casting",
|
||||||
, "description": "Options page media casting category title."
|
"description": "Options page media casting category title."
|
||||||
}
|
},
|
||||||
, "optionsMediaCategoryDescription": {
|
"optionsMediaCategoryDescription": {
|
||||||
"message": "HTML5 video/audio media casting."
|
"message": "HTML5 video/audio media casting.",
|
||||||
, "description": "Options page media casting category description."
|
"description": "Options page media casting category description."
|
||||||
}
|
},
|
||||||
, "optionsMediaEnabled": {
|
"optionsMediaEnabled": {
|
||||||
"message": "Enable media casting"
|
"message": "Enable media casting",
|
||||||
, "description": "Media casting enabled checkbox label."
|
"description": "Media casting enabled checkbox label."
|
||||||
}
|
},
|
||||||
, "optionsMediaSyncElement": {
|
"optionsMediaSyncElement": {
|
||||||
"message": "Sync receiver state with media element"
|
"message": "Sync receiver state with media element",
|
||||||
, "description": "Media casting sync checkbox label."
|
"description": "Media casting sync checkbox label."
|
||||||
}
|
},
|
||||||
, "optionsMediaSyncElementDescription": {
|
"optionsMediaSyncElementDescription": {
|
||||||
"message": "Synchronize state (playback, volume, captions, etc...) between the media element and the receiver device."
|
"message": "Synchronize state (playback, volume, captions, etc...) between the media element and the receiver device.",
|
||||||
, "description": "Media casting sync option description."
|
"description": "Media casting sync option description."
|
||||||
}
|
},
|
||||||
, "optionsMediaStopOnUnload": {
|
"optionsMediaStopOnUnload": {
|
||||||
"message": "Stop receiver playback on page unload"
|
"message": "Stop receiver playback on page unload",
|
||||||
, "description": "Media stop on unload checkbox label."
|
"description": "Media stop on unload checkbox label."
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsLocalMediaCategoryName": {
|
"optionsLocalMediaCategoryName": {
|
||||||
"message": "Local media casting"
|
"message": "Local media casting",
|
||||||
, "description": "Options page local media category title."
|
"description": "Options page local media category title."
|
||||||
}
|
},
|
||||||
, "optionsLocalMediaCategoryDescription": {
|
"optionsLocalMediaCategoryDescription": {
|
||||||
"message": "HTTP server started by the bridge app to stream local media files to the cast receiver."
|
"message": "HTTP server started by the bridge app to stream local media files to the cast receiver.",
|
||||||
, "description": "Options page local media category description."
|
"description": "Options page local media category description."
|
||||||
}
|
},
|
||||||
, "optionsLocalMediaEnabled": {
|
"optionsLocalMediaEnabled": {
|
||||||
"message": "Enable local media casting"
|
"message": "Enable local media casting",
|
||||||
, "description": "Local media enabled checkbox label."
|
"description": "Local media enabled checkbox label."
|
||||||
}
|
},
|
||||||
, "optionsLocalMediaServerPort": {
|
"optionsLocalMediaServerPort": {
|
||||||
"message": "HTTP server port:"
|
"message": "HTTP server port:",
|
||||||
, "description": "HTTP server port input label."
|
"description": "HTTP server port input label."
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsReceiverSelectorCategoryName": {
|
"optionsReceiverSelectorCategoryName": {
|
||||||
"message": "Receiver selector"
|
"message": "Receiver selector",
|
||||||
, "description": "Options page receiver selector category title."
|
"description": "Options page receiver selector category title."
|
||||||
}
|
},
|
||||||
, "optionsReceiverSelectorCategoryDescription": {
|
"optionsReceiverSelectorCategoryDescription": {
|
||||||
"message": "Receiver device selection interface."
|
"message": "Receiver device selection interface.",
|
||||||
, "description": "Options page receiver selector category description."
|
"description": "Options page receiver selector category description."
|
||||||
}
|
},
|
||||||
, "optionsReceiverSelectorWaitForConnection": {
|
"optionsReceiverSelectorWaitForConnection": {
|
||||||
"message": "Wait for connection"
|
"message": "Wait for connection",
|
||||||
, "description": "Receiver selector wait for connection option checkbox label."
|
"description": "Receiver selector wait for connection option checkbox label."
|
||||||
}
|
},
|
||||||
, "optionsReceiverSelectorWaitForConnectionDescription": {
|
"optionsReceiverSelectorWaitForConnectionDescription": {
|
||||||
"message": "Keep receiver selector open until the session is established or connection fails."
|
"message": "Keep receiver selector open until the session is established or connection fails.",
|
||||||
, "description": "Receiver selector wait for connection option description."
|
"description": "Receiver selector wait for connection option description."
|
||||||
}
|
},
|
||||||
, "optionsReceiverSelectorCloseIfFocusLost": {
|
"optionsReceiverSelectorCloseIfFocusLost": {
|
||||||
"message": "Close after losing focus"
|
"message": "Close after losing focus",
|
||||||
, "description": "Receiver selector close if focus lost option checkbox label."
|
"description": "Receiver selector close if focus lost option checkbox label."
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsUserAgentWhitelistCategoryName": {
|
"optionsUserAgentWhitelistCategoryName": {
|
||||||
"message": "User agent whitelist"
|
"message": "User agent whitelist",
|
||||||
, "description": "Options page whitelist category title."
|
"description": "Options page whitelist category title."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistCategoryDescription": {
|
"optionsUserAgentWhitelistCategoryDescription": {
|
||||||
"message": "Sites for which to replace the user agent with a Chrome version for compatibility. Must be valid match patterns."
|
"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."
|
"description": "Options page whitelist category description."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistEnabled": {
|
"optionsUserAgentWhitelistEnabled": {
|
||||||
"message": "Enable site whitelist"
|
"message": "Enable site whitelist",
|
||||||
, "description": "Whitelist enabled checkbox label."
|
"description": "Whitelist enabled checkbox label."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistRestrictedEnabled": {
|
"optionsUserAgentWhitelistRestrictedEnabled": {
|
||||||
"message": "Enable restricted mode"
|
"message": "Enable restricted mode",
|
||||||
, "description": "Whitelist restricted mode enabled checkbox label."
|
"description": "Whitelist restricted mode enabled checkbox label."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistRestrictedEnabledDescription": {
|
"optionsUserAgentWhitelistRestrictedEnabledDescription": {
|
||||||
"message": "Also apply whitelist restrictions to sites attempting to load cast functionality regardless of the current user agent."
|
"message": "Also apply whitelist restrictions to sites attempting to load cast functionality regardless of the current user agent.",
|
||||||
, "description": "Whitelist restricted mode enabled description."
|
"description": "Whitelist restricted mode enabled description."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistContent": {
|
"optionsUserAgentWhitelistContent": {
|
||||||
"message": "Match patterns:"
|
"message": "Match patterns:",
|
||||||
, "description": "Match patterns editor widget label."
|
"description": "Match patterns editor widget label."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistBasicView": {
|
"optionsUserAgentWhitelistBasicView": {
|
||||||
"message": "Basic View"
|
"message": "Basic View",
|
||||||
, "description": "Switch to basic view button title."
|
"description": "Switch to basic view button title."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistRawView": {
|
"optionsUserAgentWhitelistRawView": {
|
||||||
"message": "Raw View"
|
"message": "Raw View",
|
||||||
, "description": "Switch to raw view button title."
|
"description": "Switch to raw view button title."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistSaveRaw": {
|
"optionsUserAgentWhitelistSaveRaw": {
|
||||||
"message": "Save Raw"
|
"message": "Save Raw",
|
||||||
, "description": "Save raw view edits button title."
|
"description": "Save raw view edits button title."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistAddItem": {
|
"optionsUserAgentWhitelistAddItem": {
|
||||||
"message": "Add Item"
|
"message": "Add Item",
|
||||||
, "description": "Add new whitelist item button title."
|
"description": "Add new whitelist item button title."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistEditItem": {
|
"optionsUserAgentWhitelistEditItem": {
|
||||||
"message": "Edit"
|
"message": "Edit",
|
||||||
, "description": "Edit whitelist item button title. Displayed on each item."
|
"description": "Edit whitelist item button title. Displayed on each item."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistRemoveItem": {
|
"optionsUserAgentWhitelistRemoveItem": {
|
||||||
"message": "Remove"
|
"message": "Remove",
|
||||||
, "description": "Remove whitelist item button title. Displayed on each item."
|
"description": "Remove whitelist item button title. Displayed on each item."
|
||||||
}
|
},
|
||||||
, "optionsUserAgentWhitelistInvalidMatchPattern": {
|
"optionsUserAgentWhitelistInvalidMatchPattern": {
|
||||||
"message": "Invalid match pattern $matchPattern$"
|
"message": "Invalid match pattern $matchPattern$",
|
||||||
, "description": "Error displayed by input indicating an invalid match pattern."
|
"description": "Error displayed by input indicating an invalid match pattern.",
|
||||||
, "placeholders": {
|
"placeholders": {
|
||||||
"matchPattern": {
|
"matchPattern": {
|
||||||
"content": "$1"
|
"content": "$1",
|
||||||
, "example": "http://example"
|
"example": "http://example"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsMirroringCategoryName": {
|
"optionsMirroringCategoryName": {
|
||||||
"message": "Screen/tab casting"
|
"message": "Screen/tab casting",
|
||||||
, "description": "Options page mirroring category name."
|
"description": "Options page mirroring category name."
|
||||||
}
|
},
|
||||||
, "optionsMirroringCategoryDescription": {
|
"optionsMirroringCategoryDescription": {
|
||||||
"message": "Mirroring to a Chromecast receiver app."
|
"message": "Mirroring to a Chromecast receiver app.",
|
||||||
, "description": "Options page mirroring category description."
|
"description": "Options page mirroring category description."
|
||||||
}
|
},
|
||||||
, "optionsMirroringEnabled": {
|
"optionsMirroringEnabled": {
|
||||||
"message": "Enable screen/tab casting (experimental)"
|
"message": "Enable screen/tab casting (experimental)",
|
||||||
, "description": "Mirroring enabled checkbox label."
|
"description": "Mirroring enabled checkbox label."
|
||||||
}
|
},
|
||||||
, "optionsMirroringAppId": {
|
"optionsMirroringAppId": {
|
||||||
"message": "Mirroring app ID:"
|
"message": "Mirroring app ID:",
|
||||||
, "description": "Mirroring app ID input label."
|
"description": "Mirroring app ID input label."
|
||||||
}
|
},
|
||||||
, "optionsMirroringAppIdDescription": {
|
"optionsMirroringAppIdDescription": {
|
||||||
"message": "App ID for a registered Chromecast receiver application. Advanced use only. Must be compatible with the default app (see GitHub repo)."
|
"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."
|
"description": "Mirroring app ID option description."
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsOptionRecommended": {
|
"optionsOptionRecommended": {
|
||||||
"message": "recommended"
|
"message": "recommended",
|
||||||
, "description": "Badge next to option label indicating boolean option is recommended enabled."
|
"description": "Badge next to option label indicating boolean option is recommended enabled."
|
||||||
}
|
},
|
||||||
|
|
||||||
, "optionsReset": {
|
"optionsReset": {
|
||||||
"message": "Restore Defaults"
|
"message": "Restore Defaults",
|
||||||
, "description": "Restore default options button label."
|
"description": "Restore default options button label."
|
||||||
}
|
},
|
||||||
, "optionsSave": {
|
"optionsSave": {
|
||||||
"message": "Save"
|
"message": "Save",
|
||||||
, "description": "Save options button label."
|
"description": "Save options button label."
|
||||||
}
|
},
|
||||||
, "optionsSaved": {
|
"optionsSaved": {
|
||||||
"message": "Saved!"
|
"message": "Saved!",
|
||||||
, "description": "Status text displayed by save button once options have been successfully 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 options from "../lib/options";
|
||||||
import bridge, { BridgeInfo } from "../lib/bridge";
|
import bridge, { BridgeInfo } from "../lib/bridge";
|
||||||
|
|
||||||
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
import { RemoteMatchPattern } from "../lib/matchPattern";
|
||||||
|
|
||||||
import CastManager from "./CastManager";
|
import CastManager from "./CastManager";
|
||||||
|
|
||||||
import receiverDevices from "./receiverDevices";
|
import receiverDevices from "./receiverDevices";
|
||||||
|
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
||||||
|
|
||||||
import { initMenus } from "./menus";
|
import { initMenus } from "./menus";
|
||||||
import { initWhitelist } from "./whitelist";
|
import { initWhitelist } from "./whitelist";
|
||||||
@@ -41,7 +41,6 @@ browser.runtime.onInstalled.addListener(async details => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the bridge can be reached and is compatible
|
* Checks whether the bridge can be reached and is compatible
|
||||||
* with the current version of the extension. If not, triggers
|
* with the current version of the extension. If not, triggers
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ interface ReceiverSelectorEvents {
|
|||||||
stop: ReceiverSelectionStop;
|
stop: ReceiverSelectionStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageInfo {
|
||||||
|
url: string;
|
||||||
|
tabId: number;
|
||||||
|
frameId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
|
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
|
||||||
private windowId?: number;
|
private windowId?: number;
|
||||||
|
|
||||||
@@ -36,6 +42,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
|||||||
private wasReceiverSelected = false;
|
private wasReceiverSelected = false;
|
||||||
|
|
||||||
private appId?: string;
|
private appId?: string;
|
||||||
|
private pageInfo?: PageInfo;
|
||||||
|
|
||||||
#isOpen = false;
|
#isOpen = false;
|
||||||
|
|
||||||
@@ -65,9 +72,11 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
|||||||
receivers: ReceiverDevice[],
|
receivers: ReceiverDevice[],
|
||||||
defaultMediaType: ReceiverSelectorMediaType,
|
defaultMediaType: ReceiverSelectorMediaType,
|
||||||
availableMediaTypes: ReceiverSelectorMediaType,
|
availableMediaTypes: ReceiverSelectorMediaType,
|
||||||
appId?: string
|
appId?: string,
|
||||||
|
pageInfo?: PageInfo
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
|
this.pageInfo = pageInfo;
|
||||||
|
|
||||||
// If popup already exists, close it
|
// If popup already exists, close it
|
||||||
if (this.windowId) {
|
if (this.windowId) {
|
||||||
@@ -176,7 +185,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
|||||||
|
|
||||||
this.messagePort.postMessage({
|
this.messagePort.postMessage({
|
||||||
subject: "popup:init",
|
subject: "popup:init",
|
||||||
data: { appId: this.appId }
|
data: { appId: this.appId, pageInfo: this.pageInfo }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.messagePort.postMessage({
|
this.messagePort.postMessage({
|
||||||
|
|||||||
@@ -66,13 +66,16 @@ async function getSelection(
|
|||||||
let defaultMediaType = ReceiverSelectorMediaType.Tab;
|
let defaultMediaType = ReceiverSelectorMediaType.Tab;
|
||||||
let availableMediaTypes;
|
let availableMediaTypes;
|
||||||
|
|
||||||
|
let pageUrl: string | undefined;
|
||||||
try {
|
try {
|
||||||
const { url } = await browser.webNavigation.getFrame({
|
pageUrl = (
|
||||||
tabId: contextTabId,
|
await browser.webNavigation.getFrame({
|
||||||
frameId: contextFrameId
|
tabId: contextTabId,
|
||||||
});
|
frameId: contextFrameId
|
||||||
|
})
|
||||||
|
).url;
|
||||||
|
|
||||||
availableMediaTypes = getMediaTypesForPageUrl(url);
|
availableMediaTypes = getMediaTypesForPageUrl(pageUrl);
|
||||||
} catch {
|
} catch {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed to locate frame, falling back to default available media types."
|
"Failed to locate frame, falling back to default available media types."
|
||||||
@@ -213,11 +216,20 @@ async function getSelection(
|
|||||||
// Ensure status manager is initialized
|
// Ensure status manager is initialized
|
||||||
await receiverDevices.init();
|
await receiverDevices.init();
|
||||||
|
|
||||||
|
const pageInfo = pageUrl
|
||||||
|
? {
|
||||||
|
url: pageUrl,
|
||||||
|
tabId: contextTabId,
|
||||||
|
frameId: contextFrameId
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
sharedSelector.open(
|
sharedSelector.open(
|
||||||
receiverDevices.getDevices(),
|
receiverDevices.getDevices(),
|
||||||
defaultMediaType,
|
defaultMediaType,
|
||||||
availableMediaTypes,
|
availableMediaTypes,
|
||||||
castInstance?.appId
|
castInstance?.appId,
|
||||||
|
pageInfo
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
interface KnownApp {
|
export interface KnownApp {
|
||||||
name: string;
|
name: string;
|
||||||
matches?: 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(
|
export function loadScript(
|
||||||
scriptUrl: string,
|
scriptUrl: string,
|
||||||
doc: Document = document
|
doc: Document = document
|
||||||
@@ -122,7 +119,7 @@ export function loadScript(
|
|||||||
const scriptEl = doc.createElement("script");
|
const scriptEl = doc.createElement("script");
|
||||||
scriptEl.src = scriptUrl;
|
scriptEl.src = scriptUrl;
|
||||||
(doc.head || doc.documentElement).append(scriptEl);
|
(doc.head || doc.documentElement).append(scriptEl);
|
||||||
|
|
||||||
scriptEl.addEventListener("load", () => resolve(scriptEl));
|
scriptEl.addEventListener("load", () => resolve(scriptEl));
|
||||||
scriptEl.addEventListener("error", () => reject());
|
scriptEl.addEventListener("error", () => reject());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ import { ReceiverDevice } from "./types";
|
|||||||
type ExtMessageDefinitions = {
|
type ExtMessageDefinitions = {
|
||||||
"popup:init": {
|
"popup:init": {
|
||||||
appId?: string;
|
appId?: string;
|
||||||
|
pageInfo?: {
|
||||||
|
url: string;
|
||||||
|
tabId: number;
|
||||||
|
frameId: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
"popup:update": {
|
"popup:update": {
|
||||||
receivers: ReceiverDevice[];
|
receivers: ReceiverDevice[];
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import EditableList from "./EditableList";
|
|||||||
import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge";
|
import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge";
|
||||||
import logger from "../../lib/logger";
|
import logger from "../../lib/logger";
|
||||||
import options, { Options } from "../../lib/options";
|
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;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
--shadow-10: 0 1px 4px rgba(12, 12, 13, 0.1);
|
--shadow-10: 0 1px 4px rgba(12, 12, 13, 0.1);
|
||||||
--shadow-20: 0 2px 8px 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);
|
--shadow-30: 0 4px 16px rgba(12, 12, 13, 0.1);
|
||||||
|
|
||||||
--focus-border-color: var(--blue-50);
|
--focus-border-color: var(--blue-50);
|
||||||
|
|
||||||
--box-background: var(--white-100);
|
--box-background: var(--white-100);
|
||||||
--box-color: var(--grey-90);
|
--box-color: var(--grey-90);
|
||||||
|
|
||||||
--focus-box-shadow:
|
--focus-box-shadow: 0 0 0 1px var(--focus-border-color);
|
||||||
0 0 0 1px var(--focus-border-color);
|
|
||||||
|
|
||||||
--button-background: var(--grey-90-a10);
|
--button-background: var(--grey-90-a10);
|
||||||
--button-background-hover: var(--grey-90-a20);
|
--button-background-hover: var(--grey-90-a20);
|
||||||
@@ -26,12 +25,10 @@
|
|||||||
--field-border-color: var(--grey-90-a20);
|
--field-border-color: var(--grey-90-a20);
|
||||||
--field-border-color-hover: var(--grey-90-a30);
|
--field-border-color-hover: var(--grey-90-a30);
|
||||||
|
|
||||||
--field-box-shadow-warning:
|
--field-box-shadow-warning: 0 0 0 1px var(--yellow-60),
|
||||||
0 0 0 1px var(--yellow-60)
|
0 0 0 4px var(--yellow-60-a30);
|
||||||
, 0 0 0 4px var(--yellow-60-a30);
|
--field-box-shadow-error: 0 0 0 1px var(--red-60),
|
||||||
--field-box-shadow-error:
|
0 0 0 4px var(--red-60-a30);
|
||||||
0 0 0 1px var(--red-60)
|
|
||||||
, 0 0 0 4px var(--red-60-a30);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -102,7 +99,9 @@ textarea:invalid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button:disabled,
|
button:disabled,
|
||||||
input:disabled {
|
input:disabled,
|
||||||
|
textarea:disabled,
|
||||||
|
select:disabled {
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +110,7 @@ input,
|
|||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* No inset for spinbox control */
|
/* No inset for spinbox control */
|
||||||
@@ -130,13 +130,14 @@ button:default:hover:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.select-wrapper {
|
.select-wrapper {
|
||||||
--arrow-width: 20px;
|
--arrow-width: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.select-wrapper::after {
|
.select-wrapper::after {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
content: "▼";
|
content: "▼";
|
||||||
|
opacity: 0.5;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
|||||||
@@ -4,17 +4,19 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
import knownApps from "../../cast/knownApps";
|
import knownApps, { KnownApp } from "../../cast/knownApps";
|
||||||
import options from "../../lib/options";
|
import options from "../../lib/options";
|
||||||
|
|
||||||
import messaging, { Message, Port } from "../../messaging";
|
import messaging, { Message, Port } from "../../messaging";
|
||||||
import { getNextEllipsis } from "../../lib/utils";
|
import { getNextEllipsis } from "../../lib/utils";
|
||||||
|
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
||||||
import { ReceiverDevice } from "../../types";
|
import { ReceiverDevice } from "../../types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ReceiverSelectionActionType,
|
ReceiverSelectionActionType,
|
||||||
ReceiverSelectorMediaType
|
ReceiverSelectorMediaType
|
||||||
} from "../../background/receiverSelector";
|
} from "../../background/receiverSelector";
|
||||||
|
import { PageInfo } from "../../background/receiverSelector/ReceiverSelector";
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
@@ -37,8 +39,14 @@ interface PopupAppState {
|
|||||||
|
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
|
pageInfo?: PageInfo;
|
||||||
|
|
||||||
mirroringEnabled: boolean;
|
mirroringEnabled: boolean;
|
||||||
|
userAgentWhitelistEnabled: boolean;
|
||||||
|
userAgentWhitelist: string[];
|
||||||
|
|
||||||
|
knownApp?: KnownApp;
|
||||||
|
isPageWhitelisted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
||||||
@@ -54,7 +62,10 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
mediaType: ReceiverSelectorMediaType.App,
|
mediaType: ReceiverSelectorMediaType.App,
|
||||||
availableMediaTypes: ReceiverSelectorMediaType.App,
|
availableMediaTypes: ReceiverSelectorMediaType.App,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
mirroringEnabled: false
|
mirroringEnabled: false,
|
||||||
|
userAgentWhitelistEnabled: true,
|
||||||
|
userAgentWhitelist: [],
|
||||||
|
isPageWhitelisted: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store window ref
|
// Store window ref
|
||||||
@@ -66,6 +77,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
this.updateWindowHeight();
|
this.updateWindowHeight();
|
||||||
}).observe(document.body);
|
}).observe(document.body);
|
||||||
|
|
||||||
|
this.onAddToWhitelist = this.onAddToWhitelist.bind(this);
|
||||||
this.onSelectChange = this.onSelectChange.bind(this);
|
this.onSelectChange = this.onSelectChange.bind(this);
|
||||||
this.onCast = this.onCast.bind(this);
|
this.onCast = this.onCast.bind(this);
|
||||||
this.onStop = this.onStop.bind(this);
|
this.onStop = this.onStop.bind(this);
|
||||||
@@ -91,7 +103,8 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
switch (message.subject) {
|
switch (message.subject) {
|
||||||
case "popup:init": {
|
case "popup:init": {
|
||||||
this.setState({
|
this.setState({
|
||||||
appId: message.data?.appId
|
appId: message.data?.appId,
|
||||||
|
pageInfo: message.data?.pageInfo
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -114,6 +127,8 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateKnownApp();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +139,15 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const opts = await options.getAll();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
mirroringEnabled: await options.get("mirroringEnabled")
|
mirroringEnabled: opts.mirroringEnabled,
|
||||||
|
userAgentWhitelistEnabled: opts.userAgentWhitelistEnabled,
|
||||||
|
userAgentWhitelist: opts.userAgentWhitelist
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updateKnownApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate() {
|
||||||
@@ -135,6 +156,58 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
}, 1);
|
}, 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() {
|
public render() {
|
||||||
/*
|
/*
|
||||||
|
|
||||||
@@ -166,9 +239,42 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
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">
|
||||||
<div className="media-select__label-cast">
|
<div className="media-select__label-cast">
|
||||||
{_("popupMediaSelectCastLabel")}
|
{_("popupMediaSelectCastLabel")}
|
||||||
@@ -187,8 +293,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
selected={isAppMediaTypeSelected}
|
selected={isAppMediaTypeSelected}
|
||||||
disabled={!isAppMediaTypeAvailable}
|
disabled={!isAppMediaTypeAvailable}
|
||||||
>
|
>
|
||||||
{(this.state.appId &&
|
{this.state.knownApp?.name ??
|
||||||
knownApps[this.state.appId]?.name) ??
|
|
||||||
_("popupMediaTypeApp")}
|
_("popupMediaTypeApp")}
|
||||||
</option>
|
</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) {
|
private onCast(receiver: ReceiverDevice) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: true
|
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 {
|
body {
|
||||||
background: var(--grey-10);
|
background: var(--grey-10);
|
||||||
color: var(--grey-90);
|
color: var(--grey-90);
|
||||||
@@ -12,6 +6,10 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body {
|
body {
|
||||||
background: var(--grey-80) !important;
|
background: var(--grey-80) !important;
|
||||||
@@ -26,8 +24,35 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-select {
|
.whitelist-suggest {
|
||||||
align-items: center;
|
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);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 1em;
|
margin: 0 1em;
|
||||||
@@ -45,11 +70,6 @@ body {
|
|||||||
margin-inline-start: 0.5em;
|
margin-inline-start: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-select__dropdown {
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receivers {
|
.receivers {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: initial;
|
margin: initial;
|
||||||
|
|||||||
Reference in New Issue
Block a user