mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-14 03:20:00 +00:00
Rewrite popup with Svelte
This commit is contained in:
451
ext/src/ui/popup/Popup.svelte
Normal file
451
ext/src/ui/popup/Popup.svelte
Normal file
@@ -0,0 +1,451 @@
|
||||
<script lang="ts">
|
||||
import { afterUpdate, onMount, tick } from "svelte";
|
||||
|
||||
import LoadingIndicator from "../LoadingIndicator.svelte";
|
||||
|
||||
import { ReceiverSelectorPageInfo } from "../../background/ReceiverSelector";
|
||||
import { menuIdPopupCast, menuIdPopupStop } from "../../background/menus";
|
||||
|
||||
import messaging, { Message, Port } from "../../messaging";
|
||||
import options, { Options } from "../../lib/options";
|
||||
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
||||
|
||||
import {
|
||||
ReceiverDevice,
|
||||
ReceiverSelectionActionType,
|
||||
ReceiverSelectorMediaType
|
||||
} from "../../types";
|
||||
|
||||
import knownApps, { KnownApp } from "../../cast/knownApps";
|
||||
import { hasRequiredCapabilities } from "../../cast/utils";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
/** List of devices to show in receiver list. */
|
||||
let receiverDevices: ReceiverDevice[] = [];
|
||||
|
||||
/** Currently selected media type. */
|
||||
let mediaType = ReceiverSelectorMediaType.App;
|
||||
/** Media types available to select. */
|
||||
let availableMediaTypes = ReceiverSelectorMediaType.App;
|
||||
|
||||
/** Sender app ID (if available). */
|
||||
let appId: Optional<string>;
|
||||
/** Page info (if launched from page context). */
|
||||
let pageInfo: Optional<ReceiverSelectorPageInfo>;
|
||||
|
||||
/** App details (if matches known app). */
|
||||
let knownApp: Nullable<KnownApp> = null;
|
||||
|
||||
/** Whether current page URL matches a whitelist pattern. */
|
||||
let isPageWhitelisted = false;
|
||||
|
||||
/** Whether casting to a device been initiated from this selector. */
|
||||
let isConnecting = false;
|
||||
let connectingId: Nullable<string> = null;
|
||||
|
||||
/** Extension options */
|
||||
let opts: Nullable<Options> = null;
|
||||
|
||||
$: isMediaTypeAvailable = !!(availableMediaTypes & mediaType);
|
||||
$: isAppMediaTypeAvailable = !!(
|
||||
availableMediaTypes & ReceiverSelectorMediaType.App
|
||||
);
|
||||
|
||||
/** Whether to display whitelist suggestion banner. */
|
||||
$: shouldSuggestWhitelist =
|
||||
// If we know the app
|
||||
knownApp &&
|
||||
// If the whitelist is enabled
|
||||
opts?.siteWhitelistEnabled &&
|
||||
// If the page is not whitelisted
|
||||
!isPageWhitelisted &&
|
||||
// If an app is not already loaded on the page
|
||||
!(availableMediaTypes & ReceiverSelectorMediaType.App);
|
||||
|
||||
let port: Nullable<Port> = null;
|
||||
let browserWindow: Nullable<browser.windows.Window> = null;
|
||||
let resizeObserver = new ResizeObserver(() => fitWindowHeight());
|
||||
|
||||
onMount(async () => {
|
||||
port = messaging.connect({ name: "popup" });
|
||||
port.onMessage.addListener(onMessage);
|
||||
|
||||
browserWindow = await browser.windows.getCurrent();
|
||||
|
||||
opts = await options.getAll();
|
||||
options.addEventListener("changed", async ev => {
|
||||
opts = await options.getAll();
|
||||
|
||||
/**
|
||||
* Update available media types and ensure selected media
|
||||
* type is valid.
|
||||
*/
|
||||
if (ev.detail.includes("mirroringEnabled")) {
|
||||
const mirroringMediaTypes =
|
||||
ReceiverSelectorMediaType.Tab |
|
||||
ReceiverSelectorMediaType.Screen;
|
||||
|
||||
if (!opts.mirroringEnabled) {
|
||||
availableMediaTypes &= ~mirroringMediaTypes;
|
||||
} else {
|
||||
availableMediaTypes |= mirroringMediaTypes;
|
||||
}
|
||||
|
||||
if (!(availableMediaTypes & mediaType)) {
|
||||
if (availableMediaTypes & ReceiverSelectorMediaType.App) {
|
||||
mediaType = ReceiverSelectorMediaType.App;
|
||||
} else if (
|
||||
availableMediaTypes & ReceiverSelectorMediaType.Tab
|
||||
) {
|
||||
mediaType = ReceiverSelectorMediaType.Tab;
|
||||
} else {
|
||||
mediaType = ReceiverSelectorMediaType.App;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateKnownApp();
|
||||
|
||||
window.addEventListener("contextmenu", onContextMenu);
|
||||
browser.menus.onClicked.addListener(onMenuClicked);
|
||||
browser.menus.onShown.addListener(onMenuShown);
|
||||
|
||||
return () => {
|
||||
port?.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
|
||||
window.addEventListener("contextmenu", onContextMenu);
|
||||
browser.menus.onClicked.removeListener(onMenuClicked);
|
||||
browser.menus.onShown.removeListener(onMenuShown);
|
||||
};
|
||||
});
|
||||
|
||||
afterUpdate(async () => {
|
||||
await tick();
|
||||
fitWindowHeight();
|
||||
});
|
||||
|
||||
function onMessage(message: Message) {
|
||||
switch (message.subject) {
|
||||
case "popup:init":
|
||||
appId = message.data.appId;
|
||||
pageInfo = message.data.pageInfo;
|
||||
break;
|
||||
|
||||
case "popup:close":
|
||||
window.close();
|
||||
break;
|
||||
|
||||
case "popup:update": {
|
||||
/**
|
||||
* Filter receiver devices without the required
|
||||
* capabilities.
|
||||
*/
|
||||
receiverDevices = message.data.receiverDevices.filter(device =>
|
||||
hasRequiredCapabilities(
|
||||
device,
|
||||
pageInfo?.sessionRequest?.capabilities
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
message.data.availableMediaTypes !== undefined &&
|
||||
message.data.defaultMediaType !== undefined
|
||||
) {
|
||||
console.log(message);
|
||||
availableMediaTypes = message.data.availableMediaTypes;
|
||||
|
||||
if (availableMediaTypes & message.data.defaultMediaType) {
|
||||
mediaType = message.data.defaultMediaType;
|
||||
}
|
||||
}
|
||||
|
||||
updateKnownApp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Resize browser window to fit content height. */
|
||||
function fitWindowHeight() {
|
||||
if (browserWindow?.id === undefined) return;
|
||||
browser.windows.update(browserWindow.id, {
|
||||
height:
|
||||
document.body.clientHeight +
|
||||
(window.outerHeight - window.innerHeight)
|
||||
});
|
||||
}
|
||||
|
||||
function updateKnownApp() {
|
||||
let newKnownApp: Nullable<KnownApp> = null;
|
||||
|
||||
/**
|
||||
* Check knownApps for an app with an ID matching the registered
|
||||
* app on the target page.
|
||||
*/
|
||||
if (isAppMediaTypeAvailable && appId) {
|
||||
newKnownApp = knownApps[appId];
|
||||
} else if (pageInfo) {
|
||||
const pageUrl = pageInfo.url;
|
||||
|
||||
/**
|
||||
* Or if there isn't an registered app, check for an app
|
||||
* with a match pattern matching the target page URL.
|
||||
*/
|
||||
for (const [, app] of Object.entries(knownApps)) {
|
||||
if (!app.matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pattern = new RemoteMatchPattern(app.matches);
|
||||
if (pattern.matches(pageUrl)) {
|
||||
newKnownApp = app;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if target page URL is whitelisted.
|
||||
if (pageInfo && opts?.siteWhitelist) {
|
||||
for (const item of opts.siteWhitelist) {
|
||||
const pattern = new RemoteMatchPattern(item.pattern);
|
||||
if (pattern.matches(pageInfo.url)) {
|
||||
isPageWhitelisted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
knownApp = newKnownApp;
|
||||
}
|
||||
|
||||
async function addToWhitelist(
|
||||
app: KnownApp,
|
||||
pageInfo: ReceiverSelectorPageInfo
|
||||
) {
|
||||
if (!app.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
const whitelist = await options.get("siteWhitelist");
|
||||
if (!whitelist.find(item => item.pattern === app.matches)) {
|
||||
whitelist.push({ pattern: app.matches });
|
||||
await options.set("siteWhitelist", whitelist);
|
||||
|
||||
await browser.tabs.reload(pageInfo.tabId);
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
function onContextMenu(ev: MouseEvent) {
|
||||
if (!(ev.target instanceof Element)) return;
|
||||
|
||||
const receiverElement = ev.target.closest(".receiver");
|
||||
if (receiverElement) {
|
||||
browser.menus.overrideContext({
|
||||
showDefaults: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getDeviceFromElement(target: Element) {
|
||||
const receiverElement = target.closest(".receiver");
|
||||
if (!receiverElement) return;
|
||||
|
||||
const receiverElementIndex = [
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
...receiverElement.parentElement!.children
|
||||
].indexOf(receiverElement);
|
||||
|
||||
// Match by index rendered receiver element to device array
|
||||
if (receiverElementIndex > -1) {
|
||||
return receiverDevices[receiverElementIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle show events for receiver context menus. */
|
||||
function onMenuShown(info: browser.menus._OnShownInfo) {
|
||||
if (!info.targetElementId) return;
|
||||
const target = browser.menus.getTargetElement(info.targetElementId);
|
||||
if (!target) return;
|
||||
|
||||
const device = getDeviceFromElement(target);
|
||||
if (!device) {
|
||||
browser.menus.update(menuIdPopupCast, { visible: false });
|
||||
browser.menus.update(menuIdPopupStop, { visible: false });
|
||||
} else {
|
||||
const app = device.status?.applications?.[0];
|
||||
const isAppRunning = !!(app && !app.isIdleScreen);
|
||||
|
||||
browser.menus.update(menuIdPopupCast, {
|
||||
visible: true,
|
||||
title: _("popupCastMenuTitle", device.friendlyName),
|
||||
enabled:
|
||||
// Not already connecting to a receiver
|
||||
!isConnecting &&
|
||||
// Selected media type available
|
||||
isMediaTypeAvailable
|
||||
});
|
||||
|
||||
browser.menus.update(menuIdPopupStop, {
|
||||
visible: isAppRunning,
|
||||
title: isAppRunning
|
||||
? _("popupStopMenuTitle", [
|
||||
app.displayName,
|
||||
device.friendlyName
|
||||
])
|
||||
: ""
|
||||
});
|
||||
}
|
||||
|
||||
browser.menus.refresh();
|
||||
}
|
||||
|
||||
/** Handle click events for receiver context menus. */
|
||||
function onMenuClicked(info: browser.menus.OnClickData) {
|
||||
if (
|
||||
info.menuItemId !== menuIdPopupCast &&
|
||||
info.menuItemId !== menuIdPopupStop
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.targetElementId) return;
|
||||
const target = browser.menus.getTargetElement(info.targetElementId);
|
||||
if (!target) return;
|
||||
|
||||
const device = getDeviceFromElement(target);
|
||||
if (!device) return;
|
||||
|
||||
switch (info.menuItemId) {
|
||||
case menuIdPopupCast:
|
||||
onReceiverCast(device);
|
||||
break;
|
||||
case menuIdPopupStop:
|
||||
onReceiverStop(device);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onReceiverCast(receiverDevice: ReceiverDevice) {
|
||||
console.log(receiverDevice);
|
||||
isConnecting = true;
|
||||
connectingId = receiverDevice.id;
|
||||
|
||||
console.log(port);
|
||||
|
||||
port?.postMessage({
|
||||
subject: "receiverSelector:selected",
|
||||
data: {
|
||||
receiverDevice,
|
||||
actionType: ReceiverSelectionActionType.Cast,
|
||||
mediaType
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onReceiverStop(receiverDevice: ReceiverDevice) {
|
||||
port?.postMessage({
|
||||
subject: "receiverSelector:stop",
|
||||
data: {
|
||||
receiverDevice,
|
||||
actionType: ReceiverSelectionActionType.Stop
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="whitelist-banner" hidden={!shouldSuggestWhitelist}>
|
||||
<img src="photon_info.svg" alt="icon, info" />
|
||||
{_("popupWhitelistNotWhitelisted", knownApp?.name)}
|
||||
<button
|
||||
on:click={() => {
|
||||
if (!knownApp || !pageInfo) return;
|
||||
addToWhitelist(knownApp, pageInfo);
|
||||
}}
|
||||
>
|
||||
{_("popupWhitelistAddToWhitelist")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="media-type-select">
|
||||
<div class="media-type-select__label-cast">
|
||||
{_("popupMediaSelectCastLabel")}
|
||||
</div>
|
||||
<div class="select-wrapper">
|
||||
<select
|
||||
class="media-type-select__dropdown"
|
||||
bind:value={mediaType}
|
||||
disabled={availableMediaTypes === ReceiverSelectorMediaType.None}
|
||||
>
|
||||
<option
|
||||
value={ReceiverSelectorMediaType.App}
|
||||
disabled={!isAppMediaTypeAvailable}
|
||||
>
|
||||
{knownApp?.name ?? _("popupMediaTypeApp")}
|
||||
</option>
|
||||
|
||||
{#if opts?.mirroringEnabled}
|
||||
<option
|
||||
value={ReceiverSelectorMediaType.Tab}
|
||||
disabled={!(
|
||||
availableMediaTypes & ReceiverSelectorMediaType.Tab
|
||||
)}
|
||||
>
|
||||
{_("popupMediaTypeTab")}
|
||||
</option>
|
||||
<option
|
||||
value={ReceiverSelectorMediaType.Screen}
|
||||
disabled={!(
|
||||
availableMediaTypes & ReceiverSelectorMediaType.Screen
|
||||
)}
|
||||
>
|
||||
{_("popupMediaTypeScreen")}
|
||||
</option>
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
<div class="media-type-select__label-to">
|
||||
{_("popupMediaSelectToLabel")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="receivers">
|
||||
{#if !receiverDevices.length}
|
||||
<div class="receivers__not-found">
|
||||
{_("popupNoReceiversFound")}
|
||||
</div>
|
||||
{:else}
|
||||
{#each receiverDevices as device}
|
||||
{@const application = device.status?.applications?.[0]}
|
||||
{@const isDeviceConnecting =
|
||||
isConnecting && connectingId === device.id}
|
||||
|
||||
<li class="receiver">
|
||||
<div class="receiver__name">
|
||||
{device.friendlyName}
|
||||
</div>
|
||||
<div class="receiver__address">
|
||||
{application && !application.isIdleScreen
|
||||
? application.statusText
|
||||
: `${device.host}:${device.port}`}
|
||||
</div>
|
||||
<button
|
||||
class="button receiver__connect"
|
||||
on:click={() => onReceiverCast(device)}
|
||||
disabled={isConnecting ||
|
||||
isDeviceConnecting ||
|
||||
!isMediaTypeAvailable}
|
||||
>
|
||||
{#if isDeviceConnecting}
|
||||
{_("popupCastingButtonTitle", "")}<LoadingIndicator />
|
||||
{:else}
|
||||
{_("popupCastButtonTitle")}
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
Reference in New Issue
Block a user