Move some background modules to a separate folder and fix init order

This commit is contained in:
hensm
2019-07-28 06:07:57 +01:00
parent 36b391606a
commit 8c9ac7b1d5
20 changed files with 412 additions and 390 deletions

View File

@@ -0,0 +1,161 @@
"use strict";
import bridge from "../../lib/bridge";
import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/typedEvents";
import { getWindowCenteredProps } from "../../lib/utils";
import { Message, Receiver } from "../../types";
import ReceiverSelector, {
ReceiverSelection
, ReceiverSelectorEvents
, ReceiverSelectorMediaType } from "./ReceiverSelector";
const _ = browser.i18n.getMessage;
interface NativeReceiverSelectorSelectedMessage extends Message {
subject: "main:/receiverSelector/selected";
data: ReceiverSelection;
}
interface NativeReceiverSelectorCloseMessage extends Message {
subject: "main:/receiverSelector/error";
data: string;
}
interface NativeReceiverSelectorErrorMessage extends Message {
subject: "main:/receiverSelector/error";
data: string;
}
// TODO: Figure out lifetime properly
export default class NativeReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents>
implements ReceiverSelector {
private bridgePort: browser.runtime.Port;
private wasReceiverSelected: boolean = false;
private _isOpen: boolean = false;
get isOpen () {
return this._isOpen;
}
public async open (
receivers: Receiver[]
, defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType): Promise<void> {
this.bridgePort = await bridge.connect();
this.bridgePort.onMessage.addListener((message: Message) => {
switch (message.subject) {
case "main:/receiverSelector/selected": {
this.onBridgePortMessageSelected(
message as NativeReceiverSelectorSelectedMessage);
break;
}
case "main:/receiverSelector/error": {
this.onBridgePortMessageError(
message as NativeReceiverSelectorErrorMessage);
break;
}
case "main:/receiverSelector/close": {
this.onBridgePortMessageClose(
message as NativeReceiverSelectorCloseMessage);
break;
}
}
});
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:/receiverSelector/open"
, data: JSON.stringify({
receivers
, defaultMediaType
, availableMediaTypes
, closeIfFocusLost
, windowPositionX: centeredProps.left
, windowPositionY: centeredProps.top
, i18n_extensionName: _("extensionName")
, i18n_castButtonTitle: _("popupCastButtonTitle")
, i18n_mediaTypeApp: _("popupMediaTypeApp")
, i18n_mediaTypeTab: _("popupMediaTypeTab")
, i18n_mediaTypeScreen: _("popupMediaTypeScreen")
, i18n_mediaTypeFile: _("popupMediaTypeFile")
, i18n_mediaSelectCastLabel: _("popupMediaSelectCastLabel")
, i18n_mediaSelectToLabel: _("popupMediaSelectToLabel")
})
});
this._isOpen = true;
}
public close (): void {
if (this.bridgePort) {
this.bridgePort.postMessage({
subject: "bridge:/receiverSelector/close"
});
}
this._isOpen = false;
}
private async onBridgePortMessageSelected (
message: NativeReceiverSelectorSelectedMessage) {
this.wasReceiverSelected = true;
this.dispatchEvent(new CustomEvent("selected", {
detail: message.data
}));
if (!(await options.get("receiverSelectorWaitForConnection"))) {
this.close();
}
}
private async onBridgePortMessageError (
message: NativeReceiverSelectorErrorMessage) {
this.dispatchEvent(new CustomEvent("error"));
}
private async onBridgePortMessageClose (
message: NativeReceiverSelectorCloseMessage) {
if (!this.wasReceiverSelected) {
this.dispatchEvent(new CustomEvent("cancelled"));
}
if (this.bridgePort) {
this.bridgePort.disconnect();
}
this.bridgePort = null;
this.wasReceiverSelected = false;
this._isOpen = false;
}
}

View File

@@ -0,0 +1,194 @@
"use strict";
import ReceiverSelector, {
ReceiverSelectorEvents
, ReceiverSelectorMediaType } from "./ReceiverSelector";
import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/typedEvents";
import { getWindowCenteredProps } from "../../lib/utils";
import { Message, Receiver } from "../../types";
export default class PopupReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents>
implements ReceiverSelector {
private windowId: number;
private openerWindowId: number;
private messagePort: browser.runtime.Port;
private messagePortDisconnected: boolean;
private receivers: Receiver[];
private defaultMediaType: ReceiverSelectorMediaType;
private availableMediaTypes: ReceiverSelectorMediaType;
private wasReceiverSelected: boolean = false;
private _isOpen: boolean = false;
constructor () {
super();
// Bind methods to pass to addListener
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.
*/
browser.runtime.onConnect.addListener(port => {
if (port.name !== "popup") {
return;
}
// Disconnect existing port
if (this.messagePort) {
this.messagePort.disconnect();
}
this.messagePort = port;
this.messagePort.onMessage.addListener(this.onPopupMessage);
this.messagePort.onDisconnect.addListener(() => {
this.messagePortDisconnected = true;
});
this.messagePort.postMessage({
subject: "popup:/populateReceiverList"
, data: {
receivers: this.receivers
, defaultMediaType: this.defaultMediaType
, availableMediaTypes: this.availableMediaTypes
}
});
});
}
get isOpen () {
return this._isOpen;
}
public async open (
receivers: Receiver[]
, defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType): Promise<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;
// Current window to base centered position on
const openerWindow = await browser.windows.getCurrent();
const centeredProps = getWindowCenteredProps(openerWindow, 350, 200);
const popup = await browser.windows.create({
url: "ui/popup/index.html"
, type: "popup"
, ...centeredProps
});
this._isOpen = true;
this.windowId = popup.id;
this.openerWindowId = openerWindow.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 async close (): Promise<void> {
if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this._isOpen = false;
if (this.messagePort && !this.messagePortDisconnected) {
this.messagePort.disconnect();
}
}
/**
* 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;
}
}
}
/**
* 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.onFocusChanged.removeListener(
this.onWindowsFocusChanged);
if (!this.wasReceiverSelected) {
this.dispatchEvent(new CustomEvent("cancelled"));
}
// Cleanup
this.windowId = null;
this.openerWindowId = null;
this.messagePort = null;
this.receivers = null;
this.defaultMediaType = null;
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);
browser.windows.remove(this.windowId);
}
}
}

View File

@@ -0,0 +1,37 @@
"use strict";
import { TypedEventTarget } from "../../lib/typedEvents";
import { Receiver } from "../../types";
export enum ReceiverSelectorMediaType {
App = 1
, Tab = 2
, Screen = 4
, File = 8
}
export interface ReceiverSelection {
receiver: Receiver;
mediaType: ReceiverSelectorMediaType;
filePath?: string;
}
export interface ReceiverSelectorEvents {
"selected": ReceiverSelection;
"error": void;
"cancelled": void;
}
export default interface ReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents> {
readonly isOpen: boolean;
open (receivers: Receiver[]
, defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType): void;
close (): void;
}

View File

@@ -0,0 +1,99 @@
"use strict";
import options from "../../lib/options";
import StatusManager from "../StatusManager";
import { ReceiverSelector
, ReceiverSelectorType } from "./";
import { ReceiverSelection
, ReceiverSelectorMediaType } from "./ReceiverSelector";
import NativeReceiverSelector from "./NativeReceiverSelector";
import PopupReceiverSelector from "./PopupReceiverSelector";
async function createSelector () {
const type = await options.get("receiverSelectorType");
switch (type) {
case ReceiverSelectorType.Native: {
return new NativeReceiverSelector();
}
case ReceiverSelectorType.Popup: {
return new PopupReceiverSelector();
}
}
}
let sharedSelector: ReceiverSelector;
async function getSelector () {
if (!sharedSelector) {
sharedSelector = await createSelector();
}
return sharedSelector;
}
/**
* Opens a receiver selector with the specified
* default/available media types.
*
* Returns a promise that:
* - Resolves to a ReceiverSelection object if selection is
* successful.
* - Resolves to null if the selection is cancelled.
* - Rejects if the selection fails.
*/
async function getSelection (
defaultMediaType =
ReceiverSelectorMediaType.Tab
, availableMediaTypes =
ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File)
: Promise<ReceiverSelection> {
return new Promise(async (resolve, reject) => {
// Close an existing open selector
if (sharedSelector && sharedSelector.isOpen) {
sharedSelector.close();
}
// Get a new selector for each selection
sharedSelector = await createSelector();
sharedSelector.addEventListener("selected", ev => {
console.info("fx_cast (Debug): Selected receiver", ev.detail);
resolve(ev.detail);
});
sharedSelector.addEventListener("cancelled", ev => {
console.info("fx_cast (Debug): Cancelled receiver selection");
resolve(null);
});
sharedSelector.addEventListener("error", ev => {
console.error("fx_cast (Debug): Failed to select receiver");
reject();
});
// Ensure status manager is initialized
await StatusManager.init();
sharedSelector.open(
StatusManager.getReceivers()
, defaultMediaType
, availableMediaTypes);
});
}
export default {
getSelection
, getSelector
};

View File

@@ -0,0 +1,17 @@
"use strict";
import NativeReceiverSelector from "./NativeReceiverSelector";
import PopupReceiverSelector from "./PopupReceiverSelector";
export type ReceiverSelector =
NativeReceiverSelector
| PopupReceiverSelector;
export enum ReceiverSelectorType {
Popup
, Native
}
export { ReceiverSelection
, ReceiverSelectorMediaType } from "./ReceiverSelector";