Remove native receiver selector

This commit is contained in:
hensm
2021-05-26 17:39:12 +01:00
parent 6d80e258a9
commit ab00bcdd6a
30 changed files with 294 additions and 1704 deletions

View File

@@ -1,145 +0,0 @@
"use strict";
import bridge from "../../lib/bridge";
import knownApps from "../../lib/knownApps";
import logger from "../../lib/logger";
import { Message, Port } from "../../messaging";
import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/TypedEventTarget";
import { getWindowCenteredProps } from "../../lib/utils";
import { ReceiverDevice } from "../../types";
import ReceiverSelector, {
ReceiverSelection
, ReceiverSelectorMediaType } from "./ReceiverSelector";
const _ = browser.i18n.getMessage;
// TODO: Figure out lifetime properly
export default class NativeReceiverSelector extends ReceiverSelector {
private bridgePort: (Port | null) = null;
private wasReceiverSelected = false;
#isOpen = false;
constructor() {
super();
this.onBridgePortMessage = this.onBridgePortMessage.bind(this);
}
get isOpen() {
return this.#isOpen;
}
public async open(
receivers: ReceiverDevice[]
, defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType
, appId?: string): Promise<void> {
this.bridgePort = await bridge.connect();
this.bridgePort.onMessage.addListener(this.onBridgePortMessage);
this.bridgePort.onDisconnect.addListener(() => {
this.bridgePort = null;
this.wasReceiverSelected = false;
this.#isOpen = false;
});
// Current window to base centered position on
const openerWindow = await browser.windows.getCurrent();
const centeredProps = getWindowCenteredProps(openerWindow, 350, 0);
const closeIfFocusLost = await options.get(
"receiverSelectorCloseIfFocusLost");
this.bridgePort.postMessage({
subject: "bridge:openReceiverSelector"
, data: JSON.stringify({
receivers
, defaultMediaType
, availableMediaTypes
, closeIfFocusLost
, windowPositionX: centeredProps.left
, windowPositionY: centeredProps.top
, i18n_extensionName: _("extensionName")
, i18n_castButtonTitle: _("popupCastButtonTitle")
, i18n_stopButtonTitle: _("popupStopButtonTitle")
, i18n_mediaTypeApp:
(appId && knownApps[appId]?.name)
?? _("popupMediaTypeApp")
, i18n_mediaTypeTab: _("popupMediaTypeTab")
, i18n_mediaTypeScreen: _("popupMediaTypeScreen")
, i18n_mediaTypeFile: _("popupMediaTypeFile")
, i18n_mediaSelectCastLabel: _("popupMediaSelectCastLabel")
, i18n_mediaSelectToLabel: _("popupMediaSelectToLabel")
, i18n_noReceiversFound: _("popupNoReceiversFound")
})
});
this.#isOpen = true;
}
public update(): void {
// TODO: Implement this
}
public close(): void {
if (this.bridgePort) {
this.bridgePort.postMessage({
subject: "bridge:closeReceiverSelector"
});
}
this.#isOpen = false;
}
private async onBridgePortMessage(message: Message) {
switch (message.subject) {
case "main:receiverSelector/selected": {
this.wasReceiverSelected = true;
this.dispatchEvent(new CustomEvent("selected", {
detail: message.data
}));
if (!(await options.get("receiverSelectorWaitForConnection"))) {
this.close();
}
break;
}
case "main:receiverSelector/cancelled": {
if (!this.wasReceiverSelected) {
this.dispatchEvent(new CustomEvent("cancelled"));
}
if (this.bridgePort) {
this.bridgePort.disconnect();
}
this.bridgePort = null;
this.wasReceiverSelected = false;
this.#isOpen = false;
break;
}
case "main:receiverSelector/stopped": {
this.dispatchEvent(new CustomEvent("stop", {
detail: message.data
}));
break;
}
case "main:receiverSelector/error": {
logger.error("Native receiver selector error", message.data);
this.dispatchEvent(new CustomEvent("error"));
break;
}
}
}
}

View File

@@ -1,250 +0,0 @@
"use strict";
import ReceiverSelector, {
ReceiverSelectorMediaType } from "./ReceiverSelector";
import logger from "../../lib/logger";
import messaging, { Port, Message } from "../../messaging";
import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/TypedEventTarget";
import { getWindowCenteredProps, WindowCenteredProps } from "../../lib/utils";
import { ReceiverDevice } from "../../types";
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
export default class PopupReceiverSelector extends ReceiverSelector {
private windowId?: number;
private messagePort?: Port;
private messagePortDisconnected?: boolean;
private receivers?: ReceiverDevice[];
private defaultMediaType?: ReceiverSelectorMediaType;
private availableMediaTypes?: ReceiverSelectorMediaType;
private wasReceiverSelected = false;
private appId?: string;
#isOpen = false;
constructor() {
super();
// Bind methods to pass to addListener
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;
}
public async open(
receivers: ReceiverDevice[]
, defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType
, appId?: string): Promise<void> {
this.appId = appId;
// If popup already exists, close it
if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this.receivers = receivers;
this.defaultMediaType = defaultMediaType;
this.availableMediaTypes = availableMediaTypes;
let centeredProps: WindowCenteredProps = {
left: 100
, top: 100
, width: 350
, height: 200
};
try {
// Calculate centered size/position based on current window
centeredProps = getWindowCenteredProps(
await browser.windows.getCurrent()
, centeredProps.width, centeredProps.height);
} catch {
// Shouldn't ever hit this, but defaults are provided in case
}
const popup = await browser.windows.create({
url: POPUP_URL
, type: "popup"
, ...centeredProps
});
if (popup?.id === undefined) {
throw logger.error("Failed to create receiver selector popup.");
}
this.#isOpen = true;
this.windowId = popup.id;
// Size/position not set correctly on creation (bug?)
await browser.windows.update(this.windowId, {
...centeredProps
});
const closeIfFocusLost = await options.get(
"receiverSelectorCloseIfFocusLost");
if (closeIfFocusLost) {
// Add focus listener
browser.windows.onFocusChanged.addListener(
this.onWindowsFocusChanged);
}
}
public update(receivers: ReceiverDevice[]) {
this.receivers = receivers;
this.messagePort?.postMessage({
subject: "popup:update"
, data: {
receivers: this.receivers
}
});
}
public async close(): Promise<void> {
if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this.#isOpen = false;
this.appId = undefined;
if (this.messagePort && !this.messagePortDisconnected) {
this.messagePort.disconnect();
}
}
private onConnect(port: Port) {
browser.history.deleteUrl({ url: POPUP_URL });
if (port.name !== "popup") {
return;
}
if (this.messagePort) {
this.messagePort.disconnect();
}
this.messagePort = port;
this.messagePort.onMessage.addListener(this.onPopupMessage);
this.messagePort.onDisconnect.addListener(() => {
this.messagePortDisconnected = true;
});
if (!this.receivers
|| !this.defaultMediaType
|| !this.availableMediaTypes) {
throw logger.error("Popup receiver data not found.");
}
this.messagePort.postMessage({
subject: "popup:init"
, data: { appId: this.appId }
});
this.messagePort.postMessage({
subject: "popup:update"
, data: {
receivers: this.receivers
, defaultMediaType: this.defaultMediaType
, availableMediaTypes: this.availableMediaTypes
}
});
messaging.onConnect.removeListener(this.onConnect);
}
/**
* Handles popup messages.
*/
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.receivers = 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);
}
}
}
}

View File

@@ -1,33 +1,20 @@
"use strict";
import logger from "../../lib/logger";
import messaging, { Port, Message } from "../../messaging";
import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/TypedEventTarget";
import { getWindowCenteredProps, WindowCenteredProps } from "../../lib/utils";
import { ReceiverDevice } from "../../types";
import { ReceiverSelectionCast
, ReceiverSelectionStop
, ReceiverSelectorMediaType } from "./index";
export enum ReceiverSelectorMediaType {
App = 1
, Tab = 2
, Screen = 4
, File = 8
}
export enum ReceiverSelectionActionType {
Cast = 1
, Stop = 2
}
export interface ReceiverSelectionCast {
actionType: ReceiverSelectionActionType.Cast;
receiver: ReceiverDevice;
mediaType: ReceiverSelectorMediaType;
filePath?: string;
}
export interface ReceiverSelectionStop {
actionType: ReceiverSelectionActionType.Stop;
receiver: ReceiverDevice;
}
export type ReceiverSelection = ReceiverSelectionCast | ReceiverSelectionStop;
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
interface ReceiverSelectorEvents {
@@ -37,18 +24,239 @@ interface ReceiverSelectorEvents {
"stop": ReceiverSelectionStop;
}
export default abstract class ReceiverSelector
export default class ReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents> {
abstract readonly isOpen: boolean;
private windowId?: number;
abstract open (
private messagePort?: Port;
private messagePortDisconnected?: boolean;
private receivers?: ReceiverDevice[];
private defaultMediaType?: ReceiverSelectorMediaType;
private availableMediaTypes?: ReceiverSelectorMediaType;
private wasReceiverSelected = false;
private appId?: string;
#isOpen = false;
constructor() {
super();
// Bind methods to pass to addListener
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;
}
public async open(
receivers: ReceiverDevice[]
, defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType
, appId?: string): void;
, appId?: string): Promise<void> {
abstract update (receivers: ReceiverDevice[]): void;
this.appId = appId;
abstract close (): void;
// If popup already exists, close it
if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this.receivers = receivers;
this.defaultMediaType = defaultMediaType;
this.availableMediaTypes = availableMediaTypes;
let centeredProps: WindowCenteredProps = {
left: 100
, top: 100
, width: 350
, height: 200
};
try {
// Calculate centered size/position based on current window
centeredProps = getWindowCenteredProps(
await browser.windows.getCurrent()
, centeredProps.width, centeredProps.height);
} catch {
// Shouldn't ever hit this, but defaults are provided in case
}
const popup = await browser.windows.create({
url: POPUP_URL
, type: "popup"
, ...centeredProps
});
if (popup?.id === undefined) {
throw logger.error("Failed to create receiver selector popup.");
}
this.#isOpen = true;
this.windowId = popup.id;
// Size/position not set correctly on creation (bug?)
await browser.windows.update(this.windowId, {
...centeredProps
});
const closeIfFocusLost = await options.get(
"receiverSelectorCloseIfFocusLost");
if (closeIfFocusLost) {
// Add focus listener
browser.windows.onFocusChanged.addListener(
this.onWindowsFocusChanged);
}
}
public update(receivers: ReceiverDevice[]) {
this.receivers = receivers;
this.messagePort?.postMessage({
subject: "popup:update"
, data: {
receivers: this.receivers
}
});
}
public async close(): Promise<void> {
if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this.#isOpen = false;
this.appId = undefined;
if (this.messagePort && !this.messagePortDisconnected) {
this.messagePort.disconnect();
}
}
private onConnect(port: Port) {
browser.history.deleteUrl({ url: POPUP_URL });
if (port.name !== "popup") {
return;
}
if (this.messagePort) {
this.messagePort.disconnect();
}
this.messagePort = port;
this.messagePort.onMessage.addListener(this.onPopupMessage);
this.messagePort.onDisconnect.addListener(() => {
this.messagePortDisconnected = true;
});
if (!this.receivers
|| !this.defaultMediaType
|| !this.availableMediaTypes) {
throw logger.error("Popup receiver data not found.");
}
this.messagePort.postMessage({
subject: "popup:init"
, data: { appId: this.appId }
});
this.messagePort.postMessage({
subject: "popup:update"
, data: {
receivers: this.receivers
, defaultMediaType: this.defaultMediaType
, availableMediaTypes: this.availableMediaTypes
}
});
messaging.onConnect.removeListener(this.onConnect);
}
/**
* Handles popup messages.
*/
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.receivers = 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);
}
}
}
}

View File

@@ -8,26 +8,15 @@ import receiverDevices from "../receiverDevices";
import { getMediaTypesForPageUrl } from "../../lib/utils";
import { ReceiverSelector
, ReceiverSelectorType } from "./";
import { ReceiverSelection
, ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "./ReceiverSelector";
, ReceiverSelectorMediaType } from "./index";
import NativeReceiverSelector from "./NativeReceiverSelector";
import PopupReceiverSelector from "./PopupReceiverSelector";
import ReceiverSelector from "./ReceiverSelector";
async function createSelector() {
const type = await options.get("receiverSelectorType");
const platformInfo = await browser.runtime.getPlatformInfo();
if (platformInfo.os === "mac"
&& type === ReceiverSelectorType.Native) {
return new NativeReceiverSelector();
}
return new PopupReceiverSelector();
return new ReceiverSelector();
}

View File

@@ -1,18 +1,31 @@
"use strict";
import NativeReceiverSelector from "./NativeReceiverSelector";
import PopupReceiverSelector from "./PopupReceiverSelector";
export type ReceiverSelector =
NativeReceiverSelector
| PopupReceiverSelector;
export enum ReceiverSelectorType {
Popup
, Native
}
export { ReceiverSelection
, ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "./ReceiverSelector";
export enum ReceiverSelectorMediaType {
App = 1
, Tab = 2
, Screen = 4
, File = 8
}
export enum ReceiverSelectionActionType {
Cast = 1
, Stop = 2
}
export interface ReceiverSelectionCast {
actionType: ReceiverSelectionActionType.Cast;
receiver: ReceiverDevice;
mediaType: ReceiverSelectorMediaType;
filePath?: string;
}
export interface ReceiverSelectionStop {
actionType: ReceiverSelectionActionType.Stop;
receiver: ReceiverDevice;
}
export type ReceiverSelection = ReceiverSelectionCast | ReceiverSelectionStop;