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:
237
ext/src/background/ShimManager.ts
Normal file
237
ext/src/background/ShimManager.ts
Normal 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 }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
162
ext/src/background/StatusManager.ts
Normal file
162
ext/src/background/StatusManager.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
278
ext/src/background/createMenus.ts
Normal file
278
ext/src/background/createMenus.ts
Normal 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();
|
||||
});
|
||||
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