Implement receiver selector whitelist suggestion banner

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

View File

@@ -2,75 +2,88 @@
Contributions welcome. 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 `$\`.

View File

@@ -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."
} }
} }

View File

@@ -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

View File

@@ -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({

View File

@@ -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
); );
}); });
} }

View File

@@ -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
View File

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

View File

@@ -111,9 +111,6 @@ export function getWindowCenteredProps(
}; };
} }
export const REMOTE_MATCH_PATTERN_REGEX =
/^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
export function loadScript( 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());
}); });

View File

@@ -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[];

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

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

After

Width:  |  Height:  |  Size: 710 B

View File

@@ -1,9 +1,3 @@
:root {
--button-background: #474749;
--button-background-hover: #505054;
--button-background-active: #5c5c5e;
}
body { 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;