Files
fx_cast/ext/src/background/receiverSelector/ReceiverSelector.ts
2022-04-17 11:17:24 +01:00

291 lines
8.2 KiB
TypeScript

"use strict";
import logger from "../../lib/logger";
import messaging, { Port, Message } from "../../messaging";
import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/TypedEventTarget";
import { ReceiverDevice } from "../../types";
import {
ReceiverSelectionCast,
ReceiverSelectionStop,
ReceiverSelectorMediaType
} from "./index";
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
interface ReceiverSelectorEvents {
selected: ReceiverSelectionCast;
error: string;
cancelled: void;
stop: ReceiverSelectionStop;
}
export interface PageInfo {
url: string;
tabId: number;
frameId: number;
}
/**
* Manages the receiver selector popup window and communication with the
* extension page hosted within.
*/
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
private windowId?: number;
private messagePort?: Port;
private messagePortDisconnected?: boolean;
private receiverDevices?: ReceiverDevice[];
private defaultMediaType?: ReceiverSelectorMediaType;
private availableMediaTypes?: ReceiverSelectorMediaType;
private wasReceiverSelected = false;
private appId?: string;
private pageInfo?: PageInfo;
#isOpen = false;
constructor() {
super();
this.onConnect = this.onConnect.bind(this);
this.onPopupMessage = this.onPopupMessage.bind(this);
this.onWindowsRemoved = this.onWindowsRemoved.bind(this);
this.onWindowsFocusChanged = this.onWindowsFocusChanged.bind(this);
browser.windows.onRemoved.addListener(this.onWindowsRemoved);
/**
* Handle incoming message channel connection from popup
* window script.
*/
messaging.onConnect.addListener(this.onConnect);
}
get isOpen() {
return this.#isOpen;
}
/**
* Creates and opens a receiver selector window.
*/
public async open(
receiverDevices: ReceiverDevice[],
defaultMediaType: ReceiverSelectorMediaType,
availableMediaTypes: ReceiverSelectorMediaType,
appId?: string,
pageInfo?: PageInfo
) {
this.appId = appId;
this.pageInfo = pageInfo;
// If popup already exists, close it
if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this.receiverDevices = receiverDevices;
this.defaultMediaType = defaultMediaType;
this.availableMediaTypes = availableMediaTypes;
const popupSizePosition = {
width: 350,
height: 200,
left: 100,
top: 100
};
/**
* Get current browser window and calculate relative centered
* left/top positions for the popup.
*/
const refWin = await browser.windows.getCurrent();
if (refWin.width && refWin.height && refWin.left && refWin.top) {
const centerX = refWin.left + refWin.width / 2;
const centerY = refWin.top + refWin.height / 3;
popupSizePosition.left = Math.floor(
centerX - popupSizePosition.width / 2
);
popupSizePosition.top = Math.floor(
centerY - popupSizePosition.height / 2
);
} else {
logger.log("Reference window missing positional properties.");
}
// Create popup window
const popup = await browser.windows.create({
url: POPUP_URL,
type: "popup",
...popupSizePosition
});
if (popup?.id === undefined) {
throw logger.error("Failed to create receiver selector popup.");
}
// Size/position not set correctly on creation (bug 1396881)
await browser.windows.update(popup.id, {
...popupSizePosition
});
this.#isOpen = true;
this.windowId = popup.id;
// Add focus listener
if (await options.get("receiverSelectorCloseIfFocusLost")) {
browser.windows.onFocusChanged.addListener(
this.onWindowsFocusChanged
);
}
}
/** Updates receiver devices displayed in the receiver selector. */
public update(receiverDevices: ReceiverDevice[]) {
this.receiverDevices = receiverDevices;
this.messagePort?.postMessage({
subject: "popup:update",
data: {
receiverDevices: this.receiverDevices
}
});
}
/** Closes the receiver selector (if open). */
public async close() {
if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this.#isOpen = false;
this.appId = undefined;
if (this.messagePort && !this.messagePortDisconnected) {
this.messagePort.disconnect();
}
}
/**
* Handles incoming port connection from the extension page and
* sends init data.
*/
private onConnect(port: Port) {
// Keep history state clean
browser.history.deleteUrl({ url: POPUP_URL });
if (port.name !== "popup") {
return;
}
this.messagePort?.disconnect();
this.messagePort = port;
this.messagePort.onMessage.addListener(this.onPopupMessage);
this.messagePort.onDisconnect.addListener(() => {
this.messagePortDisconnected = true;
});
if (
this.receiverDevices === undefined ||
this.defaultMediaType === undefined ||
this.availableMediaTypes === undefined
) {
logger.error("Popup receiver data not found.");
return;
}
this.messagePort.postMessage({
subject: "popup:init",
data: { appId: this.appId, pageInfo: this.pageInfo }
});
this.messagePort.postMessage({
subject: "popup:update",
data: {
receiverDevices: this.receiverDevices,
defaultMediaType: this.defaultMediaType,
availableMediaTypes: this.availableMediaTypes
}
});
}
/** Handles messages from the popup extension page. */
private onPopupMessage(message: Message) {
switch (message.subject) {
case "receiverSelector:selected": {
this.wasReceiverSelected = true;
this.dispatchEvent(
new CustomEvent("selected", {
detail: message.data
})
);
break;
}
case "receiverSelector:stop": {
this.dispatchEvent(
new CustomEvent("stop", {
detail: message.data
})
);
break;
}
}
}
/**
* Handles cancellation state where the popup window is closed
* before a receiver is selected.
*/
private onWindowsRemoved(windowId: number) {
// Only care about popup window
if (windowId !== this.windowId) {
return;
}
browser.windows.onRemoved.removeListener(this.onWindowsRemoved);
browser.windows.onFocusChanged.removeListener(
this.onWindowsFocusChanged
);
if (!this.wasReceiverSelected) {
this.dispatchEvent(new CustomEvent("cancelled"));
}
// Cleanup
this.windowId = undefined;
this.messagePort = undefined;
this.receiverDevices = undefined;
this.defaultMediaType = undefined;
this.availableMediaTypes = undefined;
this.wasReceiverSelected = false;
}
/**
* Closes popup window if another browser window is brought
* into focus. Doesn't apply if no window is focused
* `WINDOW_ID_NONE` or if the popup window is re-focused.
*/
private onWindowsFocusChanged(windowId: number) {
if (
windowId !== browser.windows.WINDOW_ID_NONE &&
windowId !== this.windowId
) {
// Only run once
browser.windows.onFocusChanged.removeListener(
this.onWindowsFocusChanged
);
if (this.windowId) {
browser.windows.remove(this.windowId);
}
}
}
}