/* eslint-disable max-len */ "use strict"; import React, { Component } from "react"; import ReactDOM from "react-dom"; import knownApps, { KnownApp } from "../../cast/knownApps"; import options from "../../lib/options"; import messaging, { Message, Port } from "../../messaging"; import { getNextEllipsis } from "../../lib/utils"; import { RemoteMatchPattern } from "../../lib/matchPattern"; import { ReceiverDevice, ReceiverDeviceCapabilities, ReceiverSelectionActionType, ReceiverSelectorMediaType } from "../../types"; import { ReceiverSelectorPageInfo } from "../../background/ReceiverSelector"; import { Capability } from "../../cast/sdk/enums"; const _ = browser.i18n.getMessage; // macOS styles browser.runtime.getPlatformInfo().then(platformInfo => { if (platformInfo.os === "mac") { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = "styles/mac.css"; document.head.appendChild(link); } }); /** * Check receiver device capabilities bitflags against array of * capability strings requested by the sender application. */ function hasRequiredCapabilities( receiverDevice: ReceiverDevice, capabilities: Capability[] = [] ) { const { capabilities: deviceCapabilities } = receiverDevice; return capabilities.every(capability => { switch (capability) { case Capability.AUDIO_IN: return deviceCapabilities & ReceiverDeviceCapabilities.AUDIO_IN; case Capability.AUDIO_OUT: return ( deviceCapabilities & ReceiverDeviceCapabilities.AUDIO_OUT ); case Capability.MULTIZONE_GROUP: return ( deviceCapabilities & ReceiverDeviceCapabilities.MULTIZONE_GROUP ); case Capability.VIDEO_IN: return deviceCapabilities & ReceiverDeviceCapabilities.VIDEO_IN; case Capability.VIDEO_OUT: return ( deviceCapabilities & ReceiverDeviceCapabilities.VIDEO_OUT ); } }); } interface PopupAppProps {} interface PopupAppState { receiverDevices: ReceiverDevice[]; mediaType: ReceiverSelectorMediaType; availableMediaTypes: ReceiverSelectorMediaType; isLoading: boolean; filePath?: string; appId?: string; pageInfo?: ReceiverSelectorPageInfo; mirroringEnabled: boolean; userAgentWhitelistEnabled: boolean; userAgentWhitelist: string[]; knownApp?: KnownApp; isPageWhitelisted: boolean; } class PopupApp extends Component { private port?: Port; private win?: browser.windows.Window; private defaultMediaType?: ReceiverSelectorMediaType; constructor(props: PopupAppProps) { super(props); this.state = { receiverDevices: [], mediaType: ReceiverSelectorMediaType.App, availableMediaTypes: ReceiverSelectorMediaType.App, isLoading: false, mirroringEnabled: false, userAgentWhitelistEnabled: true, userAgentWhitelist: [], isPageWhitelisted: false }; // Store window ref browser.windows.getCurrent().then(win => { this.win = win; }); new ResizeObserver(() => { this.updateWindowHeight(); }).observe(document.body); this.onAddToWhitelist = this.onAddToWhitelist.bind(this); this.onSelectChange = this.onSelectChange.bind(this); this.onCast = this.onCast.bind(this); this.onStop = this.onStop.bind(this); } public updateWindowHeight() { if (this.win?.id === undefined) { return; } const frameHeight = window.outerHeight - window.innerHeight; const windowHeight = document.body.clientHeight + frameHeight; browser.windows.update(this.win.id, { height: windowHeight }); } public async componentDidMount() { this.port = messaging.connect({ name: "popup" }); this.port.onMessage.addListener((message: Message) => { switch (message.subject) { case "popup:init": { this.setState({ appId: message.data?.appId, pageInfo: message.data?.pageInfo }); break; } case "popup:update": { const { receiverDevices, availableMediaTypes, defaultMediaType } = message.data; this.setState({ /** * Filter receiver devices without the required * capabilities. */ receiverDevices: receiverDevices.filter( receiverDevice => { return hasRequiredCapabilities( receiverDevice, this.state.pageInfo?.sessionRequest ?.capabilities ); } ) }); if ( availableMediaTypes !== undefined && defaultMediaType !== undefined ) { this.defaultMediaType = defaultMediaType; this.setState({ availableMediaTypes, mediaType: defaultMediaType }); } this.updateKnownApp(); break; } case "popup:close": { window.close(); break; } } }); const opts = await options.getAll(); this.setState({ mirroringEnabled: opts.mirroringEnabled, userAgentWhitelistEnabled: opts.userAgentWhitelistEnabled, userAgentWhitelist: opts.userAgentWhitelist }); this.updateKnownApp(); } public componentDidUpdate() { setTimeout(() => { this.updateWindowHeight(); }, 1); } private updateKnownApp() { const isAppMediaTypeAvailable = !!( this.state.availableMediaTypes & ReceiverSelectorMediaType.App ); let knownApp: Nullable = 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() { /* // TODO: Add file support back to popup let truncatedFileName: string; if (this.state.filePath) { const filePath = this.state.filePath; const fileName = filePath.substring(filePath.lastIndexOf("/") + 1); truncatedFileName = fileName.length > 12 ? `${fileName.substring(0, 12)}...` : fileName; } */ const isAppMediaTypeSelected = this.state.mediaType === ReceiverSelectorMediaType.App; const isTabMediaTypeSelected = this.state.mediaType === ReceiverSelectorMediaType.Tab; const isScreenMediaTypeSelected = this.state.mediaType === ReceiverSelectorMediaType.Screen; const isSelectedMediaTypeAvailable = !!( this.state.availableMediaTypes & this.state.mediaType ); const isAppMediaTypeAvailable = !!( this.state.availableMediaTypes & ReceiverSelectorMediaType.App ); return ( <>
{_("popupMediaSelectCastLabel")}
{_("popupMediaSelectToLabel")}
); } private async onAddToWhitelist( app: KnownApp, pageInfo: ReceiverSelectorPageInfo ) { 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(receiverDevice: ReceiverDevice) { this.setState({ isLoading: true }); this.port?.postMessage({ subject: "receiverSelector:selected", data: { receiverDevice, actionType: ReceiverSelectionActionType.Cast, mediaType: this.state.mediaType, filePath: this.state.filePath } }); } private onStop(receiverDevice: ReceiverDevice) { this.port?.postMessage({ subject: "receiverSelector:stop", data: { receiverDevice, actionType: ReceiverSelectionActionType.Stop } }); } private onSelectChange(ev: React.ChangeEvent) { const mediaType = parseInt(ev.target.value); if (mediaType === ReceiverSelectorMediaType.File) { const fileUrl = window.prompt(); if (fileUrl) { this.setState({ mediaType, filePath: fileUrl }); return; } // Set media type to default if failed to set filePath if (this.defaultMediaType) { this.setState({ mediaType: this.defaultMediaType }); } } else { this.setState({ mediaType }); } this.setState({ filePath: undefined }); } } interface ReceiverEntryProps { receiverDevice: ReceiverDevice; isLoading: boolean; canCast: boolean; onCast(receiverDevice: ReceiverDevice): void; onStop(receiverDevice: ReceiverDevice): void; } interface ReceiverEntryState { ellipsis: string; isLoading: boolean; showAlternateAction: boolean; } class ReceiverEntry extends Component { constructor(props: ReceiverEntryProps) { super(props); this.state = { ellipsis: "", isLoading: false, showAlternateAction: false }; const handleActionKeyEvents = (ev: KeyboardEvent) => { if (ev.key === "Alt" || ev.key === "Shift") { this.setState({ // Only enable on keydown, otherwise disable showAlternateAction: ev.type === "keydown" }); } }; window.addEventListener("keydown", handleActionKeyEvents); window.addEventListener("keyup", handleActionKeyEvents); window.addEventListener("blur", () => { this.setState({ showAlternateAction: false }); }); this.handleCast = this.handleCast.bind(this); } public render() { const { status } = this.props.receiverDevice; const application = status?.applications?.[0]; return (
  • {this.props.receiverDevice.friendlyName}
    {application && !application.isIdleScreen ? application.statusText : `${this.props.receiverDevice.host}:${this.props.receiverDevice.port}`}
  • ); } private handleCast() { const { status } = this.props.receiverDevice; if (!status) { return; } if (this.state.showAlternateAction) { this.props.onStop(this.props.receiverDevice); } else { this.props.onCast(this.props.receiverDevice); this.setState({ isLoading: true }); setInterval(() => { this.setState(state => ({ ellipsis: getNextEllipsis(state.ellipsis) })); }, 500); } } } // Render after CSS has loaded window.addEventListener("load", () => { ReactDOM.render(, document.querySelector("#root")); });