mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-10 17:49:58 +00:00
Move some background modules to a separate folder and fix init order
This commit is contained in:
161
ext/src/background/receiverSelector/NativeReceiverSelector.ts
Normal file
161
ext/src/background/receiverSelector/NativeReceiverSelector.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
194
ext/src/background/receiverSelector/PopupReceiverSelector.ts
Normal file
194
ext/src/background/receiverSelector/PopupReceiverSelector.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
ext/src/background/receiverSelector/ReceiverSelector.ts
Normal file
37
ext/src/background/receiverSelector/ReceiverSelector.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
17
ext/src/background/receiverSelector/index.ts
Normal file
17
ext/src/background/receiverSelector/index.ts
Normal 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";
|
||||
Reference in New Issue
Block a user