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,237 @@
"use strict";
import bridge from "../lib/bridge";
import loadSender from "../lib/loadSender";
import options from "../lib/options";
import { TypedEventTarget } from "../lib/typedEvents";
import { Message } from "../types";
import { ReceiverSelectorMediaType } from "./receiverSelector";
import ReceiverSelectorManager
from "./receiverSelector/ReceiverSelectorManager";
import StatusManager from "./StatusManager";
type Port = browser.runtime.Port | MessagePort;
export interface Shim {
bridgePort: browser.runtime.Port;
contentPort: Port;
contentTabId?: number;
contentFrameId?: number;
}
// tslint:disable-next-line:new-parens
export default new class ShimManager {
private activeShims = new Set<Shim>();
public async init () {
await StatusManager.init();
await this.initStatusListeners();
}
public async createShim (port: Port) {
const shim = await (port instanceof MessagePort
? this.createShimFromBackground(port)
: this.createShimFromContent(port));
shim.contentPort.postMessage({
subject: "shim:/initialized"
, data: await bridge.getInfo()
});
this.activeShims.add(shim);
}
private async createShimFromBackground (
contentPort: MessagePort): Promise<Shim> {
const shim: Shim = {
bridgePort: await bridge.connect()
, contentPort
};
shim.bridgePort.onDisconnect.addListener(() => {
contentPort.close();
this.activeShims.delete(shim);
});
shim.bridgePort.onMessage.addListener((message: Message) => {
contentPort.postMessage(message);
});
contentPort.onmessage = ev => {
const message = ev.data as Message;
this.handleContentMessage(shim, message);
};
return shim;
}
private async createShimFromContent (
contentPort: browser.runtime.Port): Promise<Shim> {
/**
* If there's already an active shim for the sender
* tab/frame ID, disconnect it.
*/
for (const activeShim of this.activeShims) {
if (activeShim.contentTabId === contentPort.sender.tab.id
&& activeShim.contentFrameId === contentPort.sender.frameId) {
activeShim.bridgePort.disconnect();
}
}
const shim: Shim = {
bridgePort: await bridge.connect()
, contentPort
, contentTabId: contentPort.sender.tab.id
, contentFrameId: contentPort.sender.frameId
};
const onContentPortMessage = (message: Message) => {
this.handleContentMessage(shim, message);
};
const onBridgePortMessage = (message: Message) => {
contentPort.postMessage(message);
};
const onDisconnect = () => {
shim.bridgePort.onMessage.removeListener(onBridgePortMessage);
contentPort.onMessage.removeListener(onContentPortMessage);
shim.bridgePort.disconnect();
contentPort.disconnect();
this.activeShims.delete(shim);
};
shim.bridgePort.onDisconnect.addListener(onDisconnect);
shim.bridgePort.onMessage.addListener(onBridgePortMessage);
contentPort.onDisconnect.addListener(onDisconnect);
contentPort.onMessage.addListener(onContentPortMessage);
return shim;
}
private async handleContentMessage (shim: Shim, message: Message) {
const [ destination ] = message.subject.split(":/");
if (destination === "bridge") {
shim.bridgePort.postMessage(message);
}
switch (message.subject) {
case "main:/shimInitialized": {
for (const receiver of StatusManager.getReceivers()) {
shim.contentPort.postMessage({
subject: "shim:/serviceUp"
, data: { id: receiver.id }
});
}
break;
}
case "main:/selectReceiverBegin": {
const allMediaTypes =
ReceiverSelectorMediaType.App
| ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File;
try {
const selection = await ReceiverSelectorManager
.getSelection(
ReceiverSelectorMediaType.App
, allMediaTypes);
// Handle cancellation
if (!selection) {
shim.contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
break;
}
/**
* If the media type returned from the selector has been
* changed, we need to cancel the current sender and switch
* it out for the right one.
*/
if (selection.mediaType !== ReceiverSelectorMediaType.App) {
shim.contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
loadSender({
tabId: shim.contentTabId
, frameId: shim.contentFrameId
, selection
});
break;
}
// Pass selection back to shim
shim.contentPort.postMessage({
subject: "shim:/selectReceiverEnd"
, data: selection
});
} catch (err) {
// TODO: Report errors properly
shim.contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
}
break;
}
/**
* TODO: If we're closing a selector, make sure it's the
* same one that caused the session creation.
*/
case "main:/sessionCreated": {
const selector = await ReceiverSelectorManager.getSelector();
const shouldClose = await options.get(
"receiverSelectorWaitForConnection");
if (selector.isOpen && shouldClose) {
selector.close();
}
break;
}
}
}
private async initStatusListeners () {
StatusManager.addEventListener("serviceUp", ev => {
for (const shim of this.activeShims) {
shim.contentPort.postMessage({
subject: "shim:/serviceUp"
, data: { id: ev.detail.id }
});
}
});
StatusManager.addEventListener("serviceDown", ev => {
for (const shim of this.activeShims) {
shim.contentPort.postMessage({
subject: "shim:/serviceDown"
, data: { id: ev.detail.id }
});
}
});
}
};

View File

@@ -0,0 +1,162 @@
"use strict";
import bridge from "../lib/bridge";
import options from "../lib/options";
import { TypedEventTarget } from "../lib/typedEvents";
import { Message, Receiver, ReceiverStatus } from "../types";
interface ReceiverStatusMessage extends Message {
subject: "receiverStatus";
data: {
id: string;
status: ReceiverStatus;
};
}
interface ServiceDownMessage extends Message {
subject: "shim:/serviceDown";
data: {
id: string;
};
}
interface ServiceUpMessage extends Message {
subject: "shim:/serviceUp";
data: Receiver;
}
interface EventMap {
"serviceUp": ServiceUpMessage["data"];
"serviceDown": ServiceDownMessage["data"];
"statusUpdate": ReceiverStatusMessage["data"];
}
// tslint:disable-next-line:new-parens
export default new class StatusManager
extends TypedEventTarget<EventMap> {
private bridgePort: browser.runtime.Port;
private receivers = new Map<string, Receiver>();
constructor () {
super();
// Bind listeners
this.onBridgePortMessage = this.onBridgePortMessage.bind(this);
this.onBridgePortDisconnect = this.onBridgePortDisconnect.bind(this);
}
public async init () {
if (!this.bridgePort) {
await this.createBridgePort();
}
}
public getReceivers () {
return Array.from(this.receivers.values());
}
private async createBridgePort () {
const bridgePort = await bridge.connect();
bridgePort.onMessage.addListener(this.onBridgePortMessage);
bridgePort.onDisconnect.addListener(this.onBridgePortDisconnect);
bridgePort.postMessage({
subject: "bridge:/initialize"
, data: {
shouldWatchStatus: true
}
});
return bridgePort;
}
/**
* Handles incoming bridge status messages, manages the
* receiver list, and dispatches events.
*/
private onBridgePortMessage (message: Message) {
switch (message.subject) {
case "shim:/serviceUp": {
const { data: receiver } = (message as ServiceUpMessage);
this.receivers.set(receiver.id, receiver);
const serviceUpEvent = new CustomEvent("serviceUp", {
detail: receiver
});
this.dispatchEvent(serviceUpEvent);
break;
}
case "shim:/serviceDown": {
const { data: { id }} = (message as ServiceDownMessage);
if (this.receivers.has(id)) {
this.receivers.delete(id);
}
const serviceDownEvent = new CustomEvent("serviceDown", {
detail: { id }
});
this.dispatchEvent(serviceDownEvent);
break;
}
case "receiverStatus": {
const { data: { id, status }}
= (message as ReceiverStatusMessage);
const receiver = this.receivers.get(id);
// Merge with existing
this.receivers.set(id, {
...receiver
, status: {
...receiver.status
, ...status
}
});
}
}
}
/**
* Runs once the status bridge has disconnected. Sends
* serviceDown messages for all receivers to all shims to
* update receiver availability, then clears the receiver
* list.
*
* Attempts to reinitialize the status bridge after 10
* seconds. If it fails immediately, this handler will be
* triggered again and the timer is reset for another 10
* seconds.
*/
private onBridgePortDisconnect () {
for (const [, receiver] of this.receivers) {
const serviceDownEvent = new CustomEvent("serviceDown", {
detail: { id: receiver.id }
});
this.dispatchEvent(serviceDownEvent);
}
// Cleanup
this.receivers.clear();
this.bridgePort.onDisconnect.removeListener(
this.onBridgePortDisconnect);
this.bridgePort.onMessage.removeListener(this.onBridgePortMessage);
this.bridgePort = null;
window.setTimeout(async () => {
this.bridgePort = await this.createBridgePort();
}, 10000);
}
};

View File

@@ -0,0 +1,278 @@
"use strict";
import options from "../lib/options";
import { TypedEventTarget } from "../lib/typedEvents";
const _ = browser.i18n.getMessage;
const URL_PATTERN_HTTP = "http://*/*";
const URL_PATTERN_HTTPS = "https://*/*";
const URL_PATTERN_FILE = "file://*/*";
const URL_PATTERNS_REMOTE = [ URL_PATTERN_HTTP, URL_PATTERN_HTTPS ];
const URL_PATTERNS_ALL = [ ...URL_PATTERNS_REMOTE, URL_PATTERN_FILE ];
type MenuId = string | number;
let menuIdMediaCast: MenuId;
let menuIdMirroringCast: MenuId;
let menuIdWhitelist: MenuId;
let menuIdWhitelistRecommended: MenuId;
const whitelistChildMenuPatterns = new Map<MenuId, string>();
let hasCreatedMenus = false;
export default async function createMenus () {
if (!hasCreatedMenus) {
hasCreatedMenus = true;
const opts = await options.getAll();
// <video>/<audio> "Cast..." context menu item
menuIdMediaCast = await browser.menus.create({
contexts: [ "audio", "video" ]
, title: _("contextCast")
, visible: opts.mediaEnabled
, targetUrlPatterns: opts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
// Screen/Tab mirroring "Cast..." context menu item
menuIdMirroringCast = await browser.menus.create({
contexts: [ "browser_action", "page", "tools_menu" ]
, title: _("contextCast")
, visible: opts.mirroringEnabled
// Mirroring doesn't work from file:// urls
, documentUrlPatterns: URL_PATTERNS_REMOTE
});
menuIdWhitelist = await browser.menus.create({
contexts: [ "browser_action" ]
, title: _("contextAddToWhitelist")
, enabled: false
});
menuIdWhitelistRecommended = await browser.menus.create({
title: _("contextAddToWhitelistRecommended")
, parentId: menuIdWhitelist
});
await browser.menus.create({
type: "separator"
, parentId: menuIdWhitelist
});
}
return {
menuIdMediaCast
, menuIdMirroringCast
, menuIdWhitelist
, menuIdWhitelistRecommended
, whitelistChildMenuPatterns
};
}
options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail;
const opts = await options.getAll();
if (alteredOpts.includes("mirroringEnabled")) {
browser.menus.update(menuIdMirroringCast, {
visible: opts.mirroringEnabled
});
}
if (alteredOpts.includes("mediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
visible: opts.mediaEnabled
});
}
if (alteredOpts.includes("localMediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
targetUrlPatterns: opts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
}
});
browser.menus.onClicked.addListener(async info => {
if (info.parentMenuItemId === menuIdWhitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
const whitelist = await options.get("userAgentWhitelist");
// Add to whitelist and update options
whitelist.push(pattern);
await options.set("userAgentWhitelist", whitelist);
}
});
browser.menus.onShown.addListener(async info => {
// Only rebuild menus if whitelist menu present
// WebExt typings are broken again here, so ugly casting
const menuIds = info.menuIds as unknown as number[];
if (menuIds.includes(menuIdWhitelist as number)) {
return;
}
/**
* If page URL doesn't exist, we're not on a page and have
* nothing to whitelist, so disable the menu and return.
*/
if (!info.pageUrl) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
const url = new URL(info.pageUrl);
const urlHasOrigin = url.origin !== "null";
/**
* If the page URL doesn't have an origin, we're not on a
* remote page and have nothing to whitelist, so disable the
* menu and return.
*/
if (!urlHasOrigin) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
// Enable the whitelist menu
browser.menus.update(menuIdWhitelist, {
enabled: true
});
for (const [ menuId ] of whitelistChildMenuPatterns) {
// Clear all page-specific temporary menus
if (menuId !== menuIdWhitelistRecommended) {
browser.menus.remove(menuId);
}
whitelistChildMenuPatterns.delete(menuId);
}
// If there is more than one subdomain, get the base domain
const baseDomain = (url.host.match(/\./g) || []).length > 1
? url.host.substring(url.host.indexOf(".") + 1)
: url.host;
const patternRecommended = `${url.origin}/*`;
const patternSearch = `${url.origin}${url.pathname}${url.search}`;
const patternWildcardProtocol = `*://${url.host}/*`;
const patternWildcardSubdomain = `${url.protocol}//*.${baseDomain}/*`;
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
// Update recommended menu item
browser.menus.update(menuIdWhitelistRecommended, {
title: _("contextAddToWhitelistRecommended", patternRecommended)
});
whitelistChildMenuPatterns.set(
menuIdWhitelistRecommended, patternRecommended);
if (url.search) {
const whitelistSearchMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
whitelistSearchMenuId, patternSearch);
}
/**
* Split URL path into segments and add menu items for each
* partial path as the segments are removed.
*/
{
const pathTrimmed = url.pathname.endsWith("/")
? url.pathname.substring(0, url.pathname.length - 1)
: url.pathname;
const pathSegments = pathTrimmed.split("/")
.filter(segment => segment)
.reverse();
if (pathSegments.length) {
let index = 0;
for (const pathSegment of pathSegments) {
const partialPath = pathSegments
.slice(index)
.reverse()
.join("/");
const pattern = `${url.origin}/${partialPath}/*`;
const partialPathMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", pattern)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
partialPathMenuId, pattern);
index++;
}
}
}
const wildcardProtocolMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocol)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolMenuId, patternWildcardProtocol);
const wildcardSubdomainMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardSubdomainMenuId, patternWildcardSubdomain);
const wildcardProtocolAndSubdomainMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocolAndSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolAndSubdomainMenuId
, patternWildcardProtocolAndSubdomain);
await browser.menus.refresh();
});

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";