Enable strict mode for extension build

This commit is contained in:
hensm
2020-01-23 00:58:33 +00:00
parent 3553912584
commit 7f84b90431
59 changed files with 526 additions and 331 deletions

View File

@@ -4,7 +4,4 @@
"./src/**/*" "./src/**/*"
, "./@types/**/*" , "./@types/**/*"
] ]
, "compilerOptions": {
"strict": true
}
} }

View File

@@ -2,6 +2,7 @@
import bridge from "../lib/bridge"; import bridge from "../lib/bridge";
import loadSender from "../lib/loadSender"; import loadSender from "../lib/loadSender";
import logger from "../lib/logger";
import options from "../lib/options"; import options from "../lib/options";
import { Message } from "../types"; import { Message } from "../types";
@@ -87,6 +88,11 @@ export default new class ShimManager {
private async createShimFromContent ( private async createShimFromContent (
contentPort: browser.runtime.Port): Promise<Shim> { contentPort: browser.runtime.Port): Promise<Shim> {
if (contentPort.sender?.tab?.id === undefined
|| contentPort.sender?.frameId === undefined) {
throw logger.error("Content shim created with an invalid port context.");
}
/** /**
* If there's already an active shim for the sender * If there's already an active shim for the sender
* tab/frame ID, disconnect it. * tab/frame ID, disconnect it.
@@ -135,6 +141,11 @@ export default new class ShimManager {
} }
private async handleContentMessage (shim: Shim, message: Message) { private async handleContentMessage (shim: Shim, message: Message) {
if (shim.contentTabId === undefined
|| shim.contentFrameId === undefined) {
throw logger.error("Shim associated with content sender missing tab/frame ID");
}
const [ destination ] = message.subject.split(":/"); const [ destination ] = message.subject.split(":/");
if (destination === "bridge") { if (destination === "bridge") {
shim.bridgePort.postMessage(message); shim.bridgePort.postMessage(message);

View File

@@ -1,6 +1,7 @@
"use strict"; "use strict";
import bridge from "../lib/bridge"; import bridge from "../lib/bridge";
import logger from "../lib/logger";
import { TypedEventTarget } from "../lib/typedEvents"; import { TypedEventTarget } from "../lib/typedEvents";
import { Message, Receiver, ReceiverStatus } from "../types"; import { Message, Receiver, ReceiverStatus } from "../types";
@@ -38,7 +39,7 @@ interface EventMap {
export default new class StatusManager export default new class StatusManager
extends TypedEventTarget<EventMap> { extends TypedEventTarget<EventMap> {
private bridgePort: browser.runtime.Port; private bridgePort: (browser.runtime.Port | null) = null;
private receivers = new Map<string, Receiver>(); private receivers = new Map<string, Receiver>();
constructor () { constructor () {
@@ -131,6 +132,10 @@ export default new class StatusManager
const receiver = this.receivers.get(id); const receiver = this.receivers.get(id);
if (!receiver) {
throw logger.error(`Could not find receiver (${id}) specified in status message.`);
}
// Merge with existing // Merge with existing
this.receivers.set(id, { this.receivers.set(id, {
...receiver ...receiver

View File

@@ -2,6 +2,7 @@
import defaultOptions from "../defaultOptions"; import defaultOptions from "../defaultOptions";
import loadSender from "../lib/loadSender"; import loadSender from "../lib/loadSender";
import logger from "../lib/logger";
import options from "../lib/options"; import options from "../lib/options";
import { getChromeUserAgent } from "../lib/userAgents"; import { getChromeUserAgent } from "../lib/userAgents";
@@ -67,6 +68,10 @@ function initBrowserAction () {
* top-level frame. * top-level frame.
*/ */
browser.browserAction.onClicked.addListener(async tab => { browser.browserAction.onClicked.addListener(async tab => {
if (tab.id === undefined) {
throw logger.error("Tab ID not found in browser action handler.");
}
const selection = await ReceiverSelectorManager.getSelection(tab.id); const selection = await ReceiverSelectorManager.getSelection(tab.id);
if (selection) { if (selection) {
@@ -140,6 +145,10 @@ async function initMenus () {
browser.menus.onClicked.addListener(async (info, tab) => { browser.menus.onClicked.addListener(async (info, tab) => {
if (info.parentMenuItemId === menuIdWhitelist) { if (info.parentMenuItemId === menuIdWhitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId); const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
if (!pattern) {
throw logger.error(`Whitelist pattern not found for menu item ID ${info.menuItemId}.`);
}
const whitelist = await options.get("userAgentWhitelist"); const whitelist = await options.get("userAgentWhitelist");
// Add to whitelist and update options // Add to whitelist and update options
@@ -150,6 +159,17 @@ async function initMenus () {
} }
if (tab?.id === undefined) {
throw logger.error("Menu handler tab ID not found.");
}
if (info.frameId === undefined) {
throw logger.error("Menu handler frame ID not found.");
}
if (!info.pageUrl) {
throw logger.error("Menu handler page URL not found.");
}
const availableMediaTypes = getMediaTypesForPageUrl(info.pageUrl); const availableMediaTypes = getMediaTypesForPageUrl(info.pageUrl);
switch (info.menuItemId) { switch (info.menuItemId) {
@@ -157,6 +177,11 @@ async function initMenus () {
const selection = await ReceiverSelectorManager.getSelection( const selection = await ReceiverSelectorManager.getSelection(
tab.id, info.frameId); tab.id, info.frameId);
// Selection cancelled
if (!selection) {
break;
}
loadSender({ loadSender({
tabId: tab.id tabId: tab.id
, frameId: info.frameId , frameId: info.frameId
@@ -447,6 +472,10 @@ function initWhitelist () {
const { os } = await browser.runtime.getPlatformInfo(); const { os } = await browser.runtime.getPlatformInfo();
const chromeUserAgent = getChromeUserAgent(os); const chromeUserAgent = getChromeUserAgent(os);
if (!details.requestHeaders) {
throw logger.error("OnBeforeSendHeaders handler details missing requestHeaders.");
}
const host = details.requestHeaders.find( const host = details.requestHeaders.find(
header => header.name === "Host"); header => header.name === "Host");
@@ -457,7 +486,7 @@ function initWhitelist () {
* so pretend to be an old version of Chrome to get the old * so pretend to be an old version of Chrome to get the old
* site. * site.
*/ */
if (host.value === "www.youtube.com") { if (host?.value === "www.youtube.com") {
header.value = getChromeUserAgent(os, true); header.value = getChromeUserAgent(os, true);
break; break;
} }

View File

@@ -33,7 +33,7 @@ export default class NativeReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents> extends TypedEventTarget<ReceiverSelectorEvents>
implements ReceiverSelector { implements ReceiverSelector {
private bridgePort: browser.runtime.Port; private bridgePort: (browser.runtime.Port | null) = null;
private wasReceiverSelected: boolean = false; private wasReceiverSelected: boolean = false;
private _isOpen: boolean = false; private _isOpen: boolean = false;

View File

@@ -4,10 +4,11 @@ import ReceiverSelector, {
ReceiverSelectorEvents ReceiverSelectorEvents
, ReceiverSelectorMediaType } from "./ReceiverSelector"; , ReceiverSelectorMediaType } from "./ReceiverSelector";
import logger from "../../lib/logger";
import options from "../../lib/options"; import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/typedEvents"; import { TypedEventTarget } from "../../lib/typedEvents";
import { getWindowCenteredProps } from "../../lib/utils"; import { getWindowCenteredProps, WindowCenteredProps } from "../../lib/utils";
import { Message, Receiver } from "../../types"; import { Message, Receiver } from "../../types";
@@ -15,19 +16,19 @@ export default class PopupReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents> extends TypedEventTarget<ReceiverSelectorEvents>
implements ReceiverSelector { implements ReceiverSelector {
private windowId: number; private windowId?: number;
private messagePort: browser.runtime.Port; private messagePort?: browser.runtime.Port;
private messagePortDisconnected: boolean; private messagePortDisconnected?: boolean;
private receivers: Receiver[]; private receivers?: Receiver[];
private defaultMediaType: ReceiverSelectorMediaType; private defaultMediaType?: ReceiverSelectorMediaType;
private availableMediaTypes: ReceiverSelectorMediaType; private availableMediaTypes?: ReceiverSelectorMediaType;
private wasReceiverSelected: boolean = false; private wasReceiverSelected: boolean = false;
private _isOpen: boolean = false; private _isOpen: boolean = false;
private requestedAppId: string; private requestedAppId?: string;
constructor () { constructor () {
@@ -97,9 +98,22 @@ export default class PopupReceiverSelector
this.defaultMediaType = defaultMediaType; this.defaultMediaType = defaultMediaType;
this.availableMediaTypes = availableMediaTypes; this.availableMediaTypes = availableMediaTypes;
// Calculate centered size/position based on current window
const centeredProps = getWindowCenteredProps( let centeredProps: WindowCenteredProps = {
await browser.windows.getCurrent(), 350, 200); 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({ const popup = await browser.windows.create({
url: "ui/popup/index.html" url: "ui/popup/index.html"
@@ -107,6 +121,10 @@ export default class PopupReceiverSelector
, ...centeredProps , ...centeredProps
}); });
if (popup?.id === undefined) {
throw logger.error("Failed to create receiver selector popup.");
}
this._isOpen = true; this._isOpen = true;
this.windowId = popup.id; this.windowId = popup.id;
@@ -132,7 +150,7 @@ export default class PopupReceiverSelector
} }
this._isOpen = false; this._isOpen = false;
this.requestedAppId = null; this.requestedAppId = undefined;
if (this.messagePort && !this.messagePortDisconnected) { if (this.messagePort && !this.messagePortDisconnected) {
this.messagePort.disconnect(); this.messagePort.disconnect();
@@ -183,10 +201,11 @@ export default class PopupReceiverSelector
} }
// Cleanup // Cleanup
this.windowId = null; this.windowId = undefined;
this.messagePort = null; this.messagePort = undefined;
this.receivers = null; this.receivers = undefined;
this.defaultMediaType = null; this.defaultMediaType = undefined;
this.availableMediaTypes = undefined;
this.wasReceiverSelected = false; this.wasReceiverSelected = false;
} }
@@ -203,7 +222,9 @@ export default class PopupReceiverSelector
browser.windows.onFocusChanged.removeListener( browser.windows.onFocusChanged.removeListener(
this.onWindowsFocusChanged); this.onWindowsFocusChanged);
browser.windows.remove(this.windowId); if (this.windowId) {
browser.windows.remove(this.windowId);
}
} }
} }
} }

View File

@@ -1,6 +1,7 @@
"use strict"; "use strict";
import options from "../../lib/options"; import options from "../../lib/options";
import logger from "../../lib/logger";
import ShimManager from "../ShimManager"; import ShimManager from "../ShimManager";
import StatusManager from "../StatusManager"; import StatusManager from "../StatusManager";
@@ -38,7 +39,11 @@ let sharedSelector: ReceiverSelector;
async function getSelector () { async function getSelector () {
if (!sharedSelector) { if (!sharedSelector) {
sharedSelector = await createSelector(); try {
sharedSelector = await createSelector();
} catch (err) {
throw logger.error("Failed to create receiver selector.");
}
} }
return sharedSelector; return sharedSelector;
@@ -59,7 +64,7 @@ async function getSelection (
contextTabId: number contextTabId: number
, contextFrameId = 0 , contextFrameId = 0
, withMediaSender = false) , withMediaSender = false)
: Promise<ReceiverSelection> { : Promise<ReceiverSelection | null> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let currentShim = ShimManager.getShim( let currentShim = ShimManager.getShim(
@@ -71,7 +76,7 @@ async function getSelection (
*/ */
if (currentShim?.requestedAppId === if (currentShim?.requestedAppId ===
await options.get("mirroringAppId")) { await options.get("mirroringAppId")) {
currentShim = null; currentShim = undefined;
} }
let defaultMediaType = ReceiverSelectorMediaType.Tab; let defaultMediaType = ReceiverSelectorMediaType.Tab;
@@ -164,8 +169,7 @@ async function getSelection (
Array.from(StatusManager.getReceivers()) Array.from(StatusManager.getReceivers())
, defaultMediaType , defaultMediaType
, availableMediaTypes , availableMediaTypes
, currentShim?.requestedAppId , currentShim?.requestedAppId ?? DEFAULT_MEDIA_RECEIVER_APP_ID);
?? (withMediaSender && DEFAULT_MEDIA_RECEIVER_APP_ID));
}); });
} }

View File

@@ -2,6 +2,7 @@
import semver from "semver"; import semver from "semver";
import logger from "./logger";
import nativeMessaging from "./nativeMessaging"; import nativeMessaging from "./nativeMessaging";
import options from "./options"; import options from "./options";
@@ -13,7 +14,7 @@ async function connect (): Promise<browser.runtime.Port> {
bridgePort.onDisconnect.addListener(() => { bridgePort.onDisconnect.addListener(() => {
if (bridgePort.error) { if (bridgePort.error) {
console.error(`${applicationName} disconnected:` console.error(`${applicationName} disconnected:`
, this.bridgePort.error.message); , bridgePort.error.message);
} else { } else {
console.info(`${applicationName} disconnected`); console.info(`${applicationName} disconnected`);
} }
@@ -35,6 +36,10 @@ export interface BridgeInfo {
async function getInfo (): Promise<BridgeInfo> { async function getInfo (): Promise<BridgeInfo> {
const applicationName = await options.get("bridgeApplicationName"); const applicationName = await options.get("bridgeApplicationName");
if (!applicationName) {
throw logger.error("Bridge application name not found.");
}
let applicationVersion: string; let applicationVersion: string;
try { try {
@@ -45,7 +50,7 @@ async function getInfo (): Promise<BridgeInfo> {
, { subject: "bridge:/getInfo" , { subject: "bridge:/getInfo"
, data: version }); , data: version });
} catch (err) { } catch (err) {
return null; throw logger.error("Failed to connect to bridge application");
} }
/** /**

View File

@@ -1,5 +1,6 @@
"use strict"; "use strict";
import logger from "./logger";
import { stringify } from "./utils"; import { stringify } from "./utils";
import { ReceiverSelection import { ReceiverSelection
@@ -27,6 +28,11 @@ export default async function loadSender (opts: LoadSenderOptions) {
switch (opts.selection.mediaType) { switch (opts.selection.mediaType) {
case ReceiverSelectorMediaType.App: { case ReceiverSelectorMediaType.App: {
const shim = ShimManager.getShim(opts.tabId, opts.frameId); const shim = ShimManager.getShim(opts.tabId, opts.frameId);
if (!shim) {
throw logger.error(`Shim not found at tabId ${
opts.tabId} / frameId ${opts.frameId}`)
}
shim.contentPort.postMessage({ shim.contentPort.postMessage({
subject: "shim:/launchApp" subject: "shim:/launchApp"
, data: { receiver: opts.selection.receiver } , data: { receiver: opts.selection.receiver }

19
ext/src/lib/logger.ts Normal file
View File

@@ -0,0 +1,19 @@
"use strict";
export class Logger {
constructor (private prefix: string) {}
log (message: string) {
console.log(`${this.prefix} (Log): ${message}`);
}
debug (message: string) {
console.debug(`${this.prefix} (Debug): ${message}`);
}
error (message: string) {
const formattedMessage = `${this.prefix} (Error): ${message}`;
console.error(formattedMessage);
return new Error(formattedMessage);
}
}
export default new Logger("fx_cast");

View File

@@ -116,7 +116,7 @@ function connectNative (application: string) {
socket.addEventListener("close", ev => { socket.addEventListener("close", ev => {
if (ev.code !== 1000) { if (ev.code !== 1000) {
this.error = { portObject.error = {
// TODO: Set a proper error message // TODO: Set a proper error message
message: "" message: ""
}; };

View File

@@ -2,6 +2,8 @@
import defaultOptions from "../defaultOptions"; import defaultOptions from "../defaultOptions";
import logger from "./logger";
import { ReceiverSelectorType } from "../background/receiverSelector"; import { ReceiverSelectorType } from "../background/receiverSelector";
import { TypedEventTarget } from "./typedEvents"; import { TypedEventTarget } from "./typedEvents";
import { TypedStorageArea } from "./typedStorage"; import { TypedStorageArea } from "./typedStorage";
@@ -116,6 +118,8 @@ export default new class extends TypedEventTarget<EventMap> {
if (options.hasOwnProperty(name)) { if (options.hasOwnProperty(name)) {
return options[name]; return options[name];
} else {
throw logger.error(`Failed to find option ${name} in storage.`);
} }
} }
@@ -139,20 +143,16 @@ export default new class extends TypedEventTarget<EventMap> {
* storage are set. Does not override any existing options. * storage are set. Does not override any existing options.
*/ */
public async update (defaults = defaultOptions): Promise<void> { public async update (defaults = defaultOptions): Promise<void> {
const oldOpts = await this.getAll(); const newOpts = await this.getAll();
const newOpts: Partial<Options> = {};
// Find options not already in storage // Find options not already in storage
for (const [ optName, optVal ] of Object.entries(defaults)) { for (const [ optName, optVal ] of Object.entries(defaults)) {
if (!oldOpts.hasOwnProperty(optName)) { if (!newOpts.hasOwnProperty(optName)) {
newOpts[optName] = optVal; newOpts[optName] = optVal;
} }
} }
// Update storage with default values of new options // Update storage with default values of new options
return this.setAll({ return this.setAll(newOpts);
...oldOpts
, ...newOpts
});
} }
}; };

View File

@@ -5,13 +5,17 @@ export interface TypedEvents {
} }
export class TypedEventTarget<T extends TypedEvents> extends EventTarget { export class TypedEventTarget<T extends TypedEvents> extends EventTarget {
// @ts-ignore
public addEventListener<K extends keyof T> ( public addEventListener<K extends keyof T> (
type: K, listener: (ev: CustomEvent<T[K]>) => void): void { type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
// @ts-ignore
super.addEventListener(type as string, listener); super.addEventListener(type as string, listener);
} }
// @ts-ignore
public removeEventListener<K extends keyof T> ( public removeEventListener<K extends keyof T> (
type: K, listener: (ev: CustomEvent<T[K]>) => void): void { type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
// @ts-ignore
super.removeEventListener(type as string, listener); super.removeEventListener(type as string, listener);
} }

View File

@@ -11,7 +11,7 @@ const UA_CHROME_LEGACY = "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.288
const UA_SAFARI = "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Safari/605.1.15"; const UA_SAFARI = "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Safari/605.1.15";
function getPlatformComponent (platform: string): string { function getPlatformComponent (platform: string): string | undefined {
switch (platform) { switch (platform) {
case "mac": return PLATFORM_MAC; break; case "mac": return PLATFORM_MAC; break;
case "win": return PLATFORM_WIN; break; case "win": return PLATFORM_WIN; break;

View File

@@ -1,5 +1,7 @@
"use strict"; "use strict";
import logger from "./logger";
import { ReceiverSelectorMediaType } from "../background/receiverSelector"; import { ReceiverSelectorMediaType } from "../background/receiverSelector";
@@ -10,6 +12,8 @@ export function getNextEllipsis (ellipsis: string): string {
if (ellipsis === "..") return "..."; if (ellipsis === "..") return "...";
if (ellipsis === "...") return ""; if (ellipsis === "...") return "";
/* tslint:enable:curly */ /* tslint:enable:curly */
return "";
} }
/** /**
@@ -79,7 +83,7 @@ export function getMediaTypesForPageUrl (
} }
interface WindowCenteredProps { export interface WindowCenteredProps {
width: number; width: number;
height: number; height: number;
left: number; left: number;
@@ -91,6 +95,11 @@ export function getWindowCenteredProps (
, width: number , width: number
, height: number): WindowCenteredProps { , height: number): WindowCenteredProps {
if (refWin.left === undefined || refWin.width === undefined
|| refWin.top === undefined || refWin.height === undefined) {
throw logger.error("refWin missing positional attributes.");
}
const centerX = refWin.left + (refWin.width / 2); const centerX = refWin.left + (refWin.width / 2);
const centerY = refWin.top + (refWin.height / 3); const centerY = refWin.top + (refWin.height / 3);

View File

@@ -1,5 +1,6 @@
"use strict"; "use strict";
import logger from "../../lib/logger";
import options from "../../lib/options"; import options from "../../lib/options";
import cast, { ensureInit } from "../../shim/export"; import cast, { ensureInit } from "../../shim/export";
@@ -8,7 +9,7 @@ import { Message, Receiver } from "../../types";
function getLocalAddress () { function getLocalAddress () {
const pc = new RTCPeerConnection(); const pc = new RTCPeerConnection();
pc.createDataChannel(null); pc.createDataChannel("");
pc.createOffer().then(pc.setLocalDescription.bind(pc)); pc.createOffer().then(pc.setLocalDescription.bind(pc));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -90,6 +91,9 @@ function getSession (opts: InitOptions): Promise<cast.Session> {
} }
} }
// TODO: Handle this
function sessionListener () {}
function onRequestSessionSuccess (session: cast.Session) { function onRequestSessionSuccess (session: cast.Session) {
resolve(session); resolve(session);
} }
@@ -103,7 +107,7 @@ function getSession (opts: InitOptions): Promise<cast.Session> {
const apiConfig = new cast.ApiConfig( const apiConfig = new cast.ApiConfig(
sessionRequest sessionRequest
, null // sessionListener , sessionListener // sessionListener
, receiverListener); // receiverListener , receiverListener); // receiverListener
@@ -112,7 +116,7 @@ function getSession (opts: InitOptions): Promise<cast.Session> {
} }
function getMedia (opts: InitOptions): Promise<cast.media.Media> { function getMedia (opts: InitOptions): Promise<cast.media.Media> {
return new Promise(async resolve => { return new Promise(async (resolve, reject) => {
let mediaUrl = new URL(opts.mediaUrl); let mediaUrl = new URL(opts.mediaUrl);
let subtitleUrls: URL[] = []; let subtitleUrls: URL[] = [];
@@ -137,14 +141,13 @@ function getMedia (opts: InitOptions): Promise<cast.media.Media> {
path => new URL(path, baseUrl)); path => new URL(path, baseUrl));
} catch (err) { } catch (err) {
console.error("Failed to start media server"); throw logger.error("Failed to start media server");
return;
} }
} }
const activeTrackIds: number[] = []; const activeTrackIds: number[] = [];
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, null); const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, "");
mediaInfo.metadata = new cast.media.GenericMediaMetadata(); mediaInfo.metadata = new cast.media.GenericMediaMetadata();
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC; mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
@@ -222,7 +225,7 @@ function getMedia (opts: InitOptions): Promise<cast.media.Media> {
} }
// Add track to mediaInfo // Add track to mediaInfo
mediaInfo.tracks.push(castTrack); mediaInfo.tracks?.push(castTrack);
// If enabled, mark as active track for load request // If enabled, mark as active track for load request
if (track.mode === "showing" || trackElement.default) { if (track.mode === "showing" || trackElement.default) {
@@ -238,9 +241,7 @@ function getMedia (opts: InitOptions): Promise<cast.media.Media> {
loadRequest.autoplay = false; loadRequest.autoplay = false;
loadRequest.activeTrackIds = activeTrackIds; loadRequest.activeTrackIds = activeTrackIds;
currentSession.loadMedia(loadRequest currentSession.loadMedia(loadRequest, resolve, reject);
, (media) => resolve(media)
, null);
}); });
} }
@@ -266,11 +267,11 @@ async function registerMediaElementListeners () {
mediaElement.addEventListener("play", () => { mediaElement.addEventListener("play", () => {
currentMedia.play(null, null, null); currentMedia.play();
}); });
mediaElement.addEventListener("pause", () => { mediaElement.addEventListener("pause", () => {
currentMedia.pause(null, null, null); currentMedia.pause();
}); });
mediaElement.addEventListener("suspend", () => { mediaElement.addEventListener("suspend", () => {
@@ -281,7 +282,7 @@ async function registerMediaElementListeners () {
const seekRequest = new cast.media.SeekRequest(); const seekRequest = new cast.media.SeekRequest();
seekRequest.currentTime = mediaElement.currentTime; seekRequest.currentTime = mediaElement.currentTime;
currentMedia.seek(seekRequest, null, null); currentMedia.seek(seekRequest);
}); });
mediaElement.addEventListener("ratechange", () => { mediaElement.addEventListener("ratechange", () => {
@@ -394,7 +395,7 @@ export async function init (opts: InitOptions) {
}); });
if (await options.get("mediaStopOnUnload")) { if (await options.get("mediaStopOnUnload")) {
currentSession.stop(null, null); currentSession.stop(() => {}, () => {});
} }
}); });
} }

View File

@@ -202,11 +202,11 @@ class AudioPlayerElement extends PlayerElement {}
class VideoPlayerElement extends PlayerElement { class VideoPlayerElement extends PlayerElement {
set overlayHidden (val: boolean) { set overlayHidden (val: boolean) {
const shadowRoot = internalShadowRoots.get(this); const shadowRoot = internalShadowRoots.get(this);
(shadowRoot.querySelector(".overlay") as HTMLDivElement).hidden = val; (shadowRoot?.querySelector(".overlay") as HTMLDivElement).hidden = val;
} }
get overlayHidden () { get overlayHidden () {
const shadowRoot = internalShadowRoots.get(this); const shadowRoot = internalShadowRoots.get(this);
return (shadowRoot.querySelector(".overlay") as HTMLDivElement).hidden; return (shadowRoot?.querySelector(".overlay") as HTMLDivElement).hidden;
} }
} }

View File

@@ -35,7 +35,7 @@ function sendAppMessage (subject: string, data: any) {
session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, { session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, {
subject subject
, data , data
}, null, null); }, () => {}, () => {});
} }
@@ -76,6 +76,11 @@ async function onRequestSessionSuccess (newSession: cast.Session) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
// Shouldn't be possible
if (!ctx) {
break;
}
// Set initial size // Set initial size
canvas.width = window.innerWidth; canvas.width = window.innerWidth;
canvas.height = window.innerHeight; canvas.height = window.innerHeight;

View File

@@ -4,6 +4,6 @@
export default class DialRequest { export default class DialRequest {
constructor ( constructor (
public appName: string public appName: string
, public launchParameter: string = null) { , public launchParameter: (string | null) = null) {
} }
} }

View File

@@ -4,7 +4,7 @@
export default class Error { export default class Error {
constructor ( constructor (
public code: string public code: string
, public description: string = null , public description: (string | null) = null
, public details: any = null) { , public details: any = null) {
} }
} }

View File

@@ -2,8 +2,8 @@
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Image // https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Image
export default class Image { export default class Image {
public width: number = null; public width: (number | null) = null;
public height: number = null; public height: (number | null) = null;
constructor (public url: string) {} constructor (public url: string) {}
} }

View File

@@ -8,14 +8,14 @@ import { ReceiverType } from "../enums";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Receiver // https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Receiver
export default class Receiver { export default class Receiver {
public displayStatus: ReceiverDisplayStatus = null; public displayStatus: (ReceiverDisplayStatus | null) = null;
public isActiveInput: boolean = null; public isActiveInput: (boolean | null) = null;
public receiverType: string = ReceiverType.CAST; public receiverType: string = ReceiverType.CAST;
constructor ( constructor (
public label: string public label: string
, public friendlyName: string , public friendlyName: string
, public capabilities: string[] = [] , public capabilities: string[] = []
, public volume: Volume = null) { , public volume: (Volume | null) = null) {
} }
} }

View File

@@ -5,7 +5,7 @@ import Image from "./Image";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ReceiverDisplayStatus // https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ReceiverDisplayStatus
export default class ReceiverDisplayStatus { export default class ReceiverDisplayStatus {
public showStop: boolean = null; public showStop: (boolean | null) = null;
constructor ( constructor (
public statusText: string public statusText: string

View File

@@ -2,8 +2,8 @@
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SenderApplication // https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SenderApplication
export default class SenderApplication { export default class SenderApplication {
public packageId: string = null; public packageId: (string | null) = null;
public url: string = null; public url: (string | null) = null;
constructor (public platform: string) {} constructor (public platform: string) {}
} }

View File

@@ -50,7 +50,7 @@ export default class Session {
public namespaces: Array<{ name: "string" }>; public namespaces: Array<{ name: "string" }>;
public senderApps: SenderApplication[]; public senderApps: SenderApplication[];
public status: SessionStatus; public status: SessionStatus;
public statusText: string; public statusText: string | null;
public transportId: string; public transportId: string;
constructor ( constructor (
@@ -102,12 +102,15 @@ export default class Session {
switch (message.subject) { switch (message.subject) {
case "shim:/session/stopped": { case "shim:/session/stopped": {
// Disconnect from extension messages // Disconnect from extension messages
_listener.get(this).disconnect(); _listener.get(this)?.disconnect();
this.status = SessionStatus.STOPPED; this.status = SessionStatus.STOPPED;
for (const listener of _updateListeners.get(this)) { const updateListeners = _updateListeners.get(this);
listener(false); if (updateListeners) {
for (const listener of updateListeners) {
listener(false);
}
} }
break; break;
@@ -147,8 +150,11 @@ export default class Session {
} }
} }
for (const listener of _updateListeners.get(this)) { const updateListeners = _updateListeners.get(this);
listener(true); if (updateListeners) {
for (const listener of updateListeners) {
listener(true);
}
} }
break; break;
@@ -156,19 +162,28 @@ export default class Session {
case "shim:/session/impl_addMessageListener": { case "shim:/session/impl_addMessageListener": {
const { namespace, data } = message.data; const { namespace, data }
for (const listener of : { namespace: string, data: string } = message.data;
_messageListeners.get(this).get(namespace)) {
listener(namespace, data); const messageListeners = _messageListeners
.get(this)?.get(namespace);
if (messageListeners) {
for (const listener of messageListeners) {
listener(namespace, data);
}
} }
break; break;
} }
case "shim:/session/impl_sendMessage": { case "shim:/session/impl_sendMessage": {
const { messageId, error } = message.data; const { messageId, error }
const [ successCallback, errorCallback ] : { messageId: string, error: boolean } = message.data;
= _sendMessageCallbacks.get(this).get(messageId);
const [ successCallback, errorCallback ] =
_sendMessageCallbacks
.get(this)?.get(messageId) ?? [];
if (error && errorCallback) { if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR)); errorCallback(new _Error(ErrorCode.SESSION_ERROR));
@@ -176,17 +191,16 @@ export default class Session {
successCallback(); successCallback();
} }
_sendMessageCallbacks.get(this).delete(messageId); _sendMessageCallbacks.get(this)?.delete(messageId);
break; break;
} }
case "shim:/session/impl_setReceiverMuted": { case "shim:/session/impl_setReceiverMuted": {
const { volumeId, error } = message.data; const { volumeId, error } = message.data;
const [ successCallback, errorCallback ] const [ successCallback, errorCallback ] =
= _setReceiverMutedCallbacks _setReceiverMutedCallbacks
.get(this) .get(this)?.get(volumeId) ?? [];
.get(volumeId);
if (error && errorCallback) { if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR)); errorCallback(new _Error(ErrorCode.SESSION_ERROR));
@@ -194,16 +208,16 @@ export default class Session {
successCallback(); successCallback();
} }
_setReceiverMutedCallbacks.get(this).delete(volumeId); _setReceiverMutedCallbacks.get(this)?.delete(volumeId);
break; break;
} }
case "shim:/session/impl_setReceiverVolumeLevel": { case "shim:/session/impl_setReceiverVolumeLevel": {
const { volumeId, error } = message.data; const { volumeId, error } = message.data;
const [ successCallback, errorCallback ] const [ successCallback, errorCallback ] =
= _setReceiverVolumeLevelCallbacks.get(this) _setReceiverVolumeLevelCallbacks
.get(volumeId); .get(this)?.get(volumeId) ?? [];
if (error && errorCallback) { if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR)); errorCallback(new _Error(ErrorCode.SESSION_ERROR));
@@ -211,7 +225,8 @@ export default class Session {
successCallback(); successCallback();
} }
_setReceiverVolumeLevelCallbacks.get(this).delete(volumeId); _setReceiverVolumeLevelCallbacks
.get(this)?.delete(volumeId);
break; break;
} }
@@ -219,18 +234,21 @@ export default class Session {
case "shim:/session/impl_stop": { case "shim:/session/impl_stop": {
const { stopId, error } = message.data; const { stopId, error } = message.data;
const [ successCallback, errorCallback ] const [ successCallback, errorCallback ]
= _stopCallbacks.get(this).get(stopId); = _stopCallbacks.get(this)?.get(stopId) ?? [];
// Disconnect from extension messages // Disconnect from extension messages
_listener.get(this).disconnect(); _listener.get(this)?.disconnect();
if (error && errorCallback) { if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR)); errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else { } else {
this.status = SessionStatus.STOPPED; this.status = SessionStatus.STOPPED;
for (const listener of _updateListeners.get(this)) { const updateListeners = _updateListeners.get(this);
listener(false); if (updateListeners) {
for (const listener of updateListeners) {
listener(false);
}
} }
if (successCallback) { if (successCallback) {
@@ -238,7 +256,7 @@ export default class Session {
} }
} }
_stopCallbacks.get(this).delete(stopId); _stopCallbacks.get(this)?.delete(stopId);
break; break;
} }
@@ -257,11 +275,11 @@ export default class Session {
namespace: string namespace: string
, listener: MessageListener) { , listener: MessageListener) {
if (!_messageListeners.get(this).has(namespace)) { if (!_messageListeners.get(this)?.has(namespace)) {
_messageListeners.get(this).set(namespace, new Set()); _messageListeners.get(this)?.set(namespace, new Set());
} }
_messageListeners.get(this).get(namespace).add(listener); _messageListeners.get(this)?.get(namespace)?.add(listener);
sendMessageResponse({ sendMessageResponse({
subject: "bridge:/session/impl_addMessageListener" subject: "bridge:/session/impl_addMessageListener"
@@ -271,7 +289,7 @@ export default class Session {
} }
public addUpdateListener (listener: UpdateListener) { public addUpdateListener (listener: UpdateListener) {
_updateListeners.get(this).add(listener); _updateListeners.get(this)?.add(listener);
} }
public leave ( public leave (
@@ -286,7 +304,7 @@ export default class Session {
, _id: _id.get(this) , _id: _id.get(this)
}); });
_leaveCallbacks.get(this).set(id, [ _leaveCallbacks.get(this)?.set(id, [
successCallback successCallback
, errorCallback , errorCallback
]); ]);
@@ -322,12 +340,17 @@ export default class Session {
const message = JSON.parse(data); const message = JSON.parse(data);
if (message.status && message.status.length > 0) { if (message.status && message.status.length > 0) {
const sessionId = _id.get(this);
if (!sessionId) {
return;
}
hasResponded = true; hasResponded = true;
const media = new Media( const media = new Media(
this.sessionId this.sessionId
, message.status[0].mediaSessionId , message.status[0].mediaSessionId
, _id.get(this)); , sessionId);
media.media = loadRequest.media; media.media = loadRequest.media;
this.media = [ media ]; this.media = [ media ];
@@ -361,14 +384,14 @@ export default class Session {
namespace: string namespace: string
, listener: MessageListener): void { , listener: MessageListener): void {
_messageListeners.get(this).get(namespace).delete(listener); _messageListeners.get(this)?.get(namespace)?.delete(listener);
} }
public removeUpdateListener ( public removeUpdateListener (
_namespace: string _namespace: string
, listener: UpdateListener): void { , listener: UpdateListener): void {
_updateListeners.get(this).delete(listener); _updateListeners.get(this)?.delete(listener);
} }
public sendMessage ( public sendMessage (
@@ -389,7 +412,7 @@ export default class Session {
, _id: _id.get(this) , _id: _id.get(this)
}); });
_sendMessageCallbacks.get(this).set(messageId, [ _sendMessageCallbacks.get(this)?.set(messageId, [
successCallback successCallback
, errorCallback , errorCallback
]); ]);
@@ -408,7 +431,7 @@ export default class Session {
, _id: _id.get(this) , _id: _id.get(this)
}); });
_setReceiverMutedCallbacks.get(this).set(volumeId, [ _setReceiverMutedCallbacks.get(this)?.set(volumeId, [
successCallback successCallback
, errorCallback , errorCallback
]); ]);
@@ -427,7 +450,7 @@ export default class Session {
, _id: _id.get(this) , _id: _id.get(this)
}); });
_setReceiverVolumeLevelCallbacks.get(this).set(volumeId, [ _setReceiverVolumeLevelCallbacks.get(this)?.set(volumeId, [
successCallback successCallback
, errorCallback , errorCallback
]); ]);
@@ -445,7 +468,7 @@ export default class Session {
, _id: _id.get(this) , _id: _id.get(this)
}); });
_stopCallbacks.get(this).set(stopId, [ _stopCallbacks.get(this)?.set(stopId, [
successCallback successCallback
, errorCallback , errorCallback
]); ]);
@@ -456,6 +479,6 @@ export default class Session {
this.sendMessage( this.sendMessage(
"urn:x-cast:com.google.cast.media" "urn:x-cast:com.google.cast.media"
, message , message
, null, null); , () => {}, () => {});
} }
} }

View File

@@ -6,7 +6,7 @@ import Timeout from "./Timeout";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SessionRequest // https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SessionRequest
export default class SessionRequest { export default class SessionRequest {
public language: string = null; public language: (string | null) = null;
public dialRequest: any = null; public dialRequest: any = null;
constructor ( constructor (

View File

@@ -5,11 +5,11 @@ import { VolumeControlType } from "../enums";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Volume // https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Volume
export default class Volume { export default class Volume {
public controlType: VolumeControlType; public controlType?: VolumeControlType;
public stepInterval: number; public stepInterval?: number;
constructor ( constructor (
public level: number = null public level: (number | null) = null
, public muted: boolean = null) { , public muted: (boolean | null) = null) {
} }
} }

View File

@@ -129,8 +129,8 @@ export function removeReceiverActionListener (
} }
export function requestSession ( export function requestSession (
successCallback?: RequestSessionSuccessCallback successCallback: RequestSessionSuccessCallback
, errorCallback?: ErrorCallback , errorCallback: ErrorCallback
, _sessionRequest: SessionRequest = apiConfig.sessionRequest): void { , _sessionRequest: SessionRequest = apiConfig.sessionRequest): void {
console.info("fx_cast (Debug): cast.requestSession"); console.info("fx_cast (Debug): cast.requestSession");
@@ -176,8 +176,8 @@ export function requestSession (
export function _requestSession ( export function _requestSession (
_receiver: Receiver _receiver: Receiver
, successCallback?: RequestSessionSuccessCallback , successCallback: RequestSessionSuccessCallback
, errorCallback?: ErrorCallback): void { , errorCallback: ErrorCallback): void {
console.info("fx_cast (Debug): cast._requestSession"); console.info("fx_cast (Debug): cast._requestSession");
@@ -241,7 +241,7 @@ export function _requestSession (
const lastSession = sessionList[sessionList.length - 1]; const lastSession = sessionList[sessionList.length - 1];
if (lastSession.status !== SessionStatus.STOPPED) { if (lastSession.status !== SessionStatus.STOPPED) {
lastSession.stop(createSession, null); lastSession.stop(createSession, () => {});
} }
} else { } else {
createSession(); createSession();
@@ -354,7 +354,7 @@ onMessage(async message => {
const lastSession = sessionList[sessionList.length - 1]; const lastSession = sessionList[sessionList.length - 1];
if (lastSession.status !== SessionStatus.STOPPED) { if (lastSession.status !== SessionStatus.STOPPED) {
lastSession.stop(createSession, null); lastSession.stop(createSession, () => {});
} }
} else { } else {
createSession(); createSession();
@@ -380,9 +380,11 @@ onMessage(async message => {
case "shim:/launchApp": { case "shim:/launchApp": {
const receiver: Receiver = message.data.receiver; const receiver: Receiver = message.data.receiver;
_requestSession(receiver, session => { _requestSession(receiver
apiConfig.sessionListener(session); , session => {
}); apiConfig.sessionListener(session);
}
, () => {});
break; break;
} }

View File

@@ -4,7 +4,7 @@ export default class EditTracksInfoRequest {
public requestId = 0; public requestId = 0;
constructor ( constructor (
public activeTrackIds: number[] = null public activeTrackIds: (number[] | null) = null
, public textTrackStyle: string = null) { , public textTrackStyle: (string | null) = null) {
} }
} }

View File

@@ -6,11 +6,11 @@ import { MetadataType } from "../enums";
export default class GenericMediaMetadata { export default class GenericMediaMetadata {
public images: Image[] = undefined; public images: (Image[] | undefined) = undefined;
public metadataType: number = MetadataType.GENERIC; public metadataType: number = MetadataType.GENERIC;
public releaseDate: string = undefined; public releaseDate: (string | undefined) = undefined;
public releaseYear: number = undefined; public releaseYear: (number | undefined) = undefined;
public subtitle: string = undefined; public subtitle: (string | undefined) = undefined;
public title: string = undefined; public title: (string | undefined) = undefined;
public type: number = MetadataType.GENERIC; public type: number = MetadataType.GENERIC;
} }

View File

@@ -4,13 +4,13 @@ import MediaInfo from "./MediaInfo";
export default class LoadRequest { export default class LoadRequest {
public activeTrackIds: number[] = null; public activeTrackIds: (number[] | null) = null;
public autoplay: boolean = true; public autoplay: (boolean | null) = true;
public currentTime: number = null; public currentTime: (number | null) = null;
public customData: any = null; public customData: any = null;
public media: MediaInfo; public media: MediaInfo;
public requestId: number = 0; public requestId: number = 0;
public sessionId: string = null; public sessionId: (string | null) = null;
public type: string = "LOAD"; public type: string = "LOAD";
constructor (mediaInfo: MediaInfo) { constructor (mediaInfo: MediaInfo) {

View File

@@ -40,17 +40,17 @@ const _lastCurrentTime = new WeakMap<Media, number>();
export default class Media { export default class Media {
public activeTrackIds: number[] = null; public activeTrackIds: (number[] | null) = null;
public currentItemId: number = null; public currentItemId: (number | null) = null;
public customData: any = null; public customData: any = null;
public currentTime: number = 0; public currentTime: number = 0;
public idleReason: string = null; public idleReason: (string | null) = null;
public items: QueueItem[] = null; public items: (QueueItem[] | null) = null;
public loadingItemId: number = null; public loadingItemId: (number | null) = null;
public media: MediaInfo = null; public media: (MediaInfo | null) = null;
public playbackRate: number = 1; public playbackRate: number = 1;
public playerState: string = PlayerState.IDLE; public playerState: string = PlayerState.IDLE;
public preloadedItemId: number = null; public preloadedItemId: (number | null) = null;
public repeatMode: string = RepeatMode.OFF; public repeatMode: string = RepeatMode.OFF;
public supportedMediaCommands: string[] = []; public supportedMediaCommands: string[] = [];
public volume: Volume = new Volume(); public volume: Volume = new Volume();
@@ -66,8 +66,6 @@ export default class Media {
_updateListeners.set(this, new Set()); _updateListeners.set(this, new Set());
_sendMediaMessageCallbacks.set(this, new Map()); _sendMediaMessageCallbacks.set(this, new Map());
_lastCurrentTime.set(this, undefined);
sendMessageResponse({ sendMessageResponse({
subject: "bridge:/media/initialize" subject: "bridge:/media/initialize"
@@ -109,19 +107,23 @@ export default class Media {
} }
// Call update listeners // Call update listeners
for (const listener of _updateListeners.get(this)) { const updateListeners = _updateListeners.get(this);
listener(true); if (updateListeners) {
for (const listener of updateListeners) {
listener(true);
}
} }
break; break;
} }
case "shim:/media/sendMediaMessageResponse": { case "shim:/media/sendMediaMessageResponse": {
const { messageId, error } = message.data; const { messageId, error }
: { messageId: string, error: any } = message.data;
const [ successCallback, errorCallback ] const [ successCallback, errorCallback ]
= _sendMediaMessageCallbacks = _sendMediaMessageCallbacks
.get(this) .get(this)?.get(messageId) ?? [];
.get(messageId);
if (error && errorCallback) { if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR)); errorCallback(new _Error(ErrorCode.SESSION_ERROR));
@@ -137,7 +139,7 @@ export default class Media {
} }
public addUpdateListener (listener: UpdateListener): void { public addUpdateListener (listener: UpdateListener): void {
_updateListeners.get(this).add(listener); _updateListeners.get(this)?.add(listener);
} }
public editTracksInfo ( public editTracksInfo (
@@ -149,12 +151,13 @@ export default class Media {
} }
public getEstimatedTime (): number { public getEstimatedTime (): number {
if (!this.currentTime) { const lastTime = _lastCurrentTime.get(this);
if (this.currentTime === undefined || lastTime === undefined) {
return 0; return 0;
} }
return this.currentTime return this.currentTime + ((Date.now() / 1000) - lastTime);
+ ((Date.now() / 1000) - _lastCurrentTime.get(this));
} }
public getStatus ( public getStatus (
@@ -167,7 +170,7 @@ export default class Media {
} }
public pause ( public pause (
_pauseRequest: PauseRequest _pauseRequest?: PauseRequest
, successCallback?: SuccessCallback , successCallback?: SuccessCallback
, errorCallback?: ErrorCallback): void { , errorCallback?: ErrorCallback): void {
@@ -254,7 +257,7 @@ export default class Media {
} }
public removeUpdateListener (listener: UpdateListener) { public removeUpdateListener (listener: UpdateListener) {
_updateListeners.get(this).delete(listener); _updateListeners.get(this)?.delete(listener);
} }
public seek ( public seek (
@@ -307,7 +310,7 @@ export default class Media {
const messageId = uuid(); const messageId = uuid();
_sendMediaMessageCallbacks.get(this).set(messageId, [ _sendMediaMessageCallbacks.get(this)?.set(messageId, [
successCallback successCallback
, errorCallback , errorCallback
]); ]);

View File

@@ -20,12 +20,12 @@ type Metadata =
| TvShowMediaMetadata; | TvShowMediaMetadata;
export default class MediaInfo { export default class MediaInfo {
public customData: string = null; public customData: any = null;
public duration: number = null; public duration: (number | null) = null;
public metadata: Metadata = null; public metadata: (Metadata | null) = null;
public streamType: string = StreamType.BUFFERED; public streamType: string = StreamType.BUFFERED;
public textTrackStyle: TextTrackStyle = null; public textTrackStyle: (TextTrackStyle | null) = null;
public tracks: Track[] = null; public tracks: (Track[] | null) = null;
constructor ( constructor (
public contentId: string public contentId: string

View File

@@ -6,12 +6,12 @@ import { MetadataType } from "../enums";
export default class MovieMediaMetadata { export default class MovieMediaMetadata {
public images: Image[] = undefined; public images: (Image[] | undefined) = undefined;
public metadataType: number = MetadataType.MOVIE; public metadataType: number = MetadataType.MOVIE;
public releaseDate: string = undefined; public releaseDate: (string | undefined) = undefined;
public releaseYear: number = undefined; public releaseYear: (number | undefined) = undefined;
public studio: string = undefined; public studio: (string | undefined) = undefined;
public subtitle: string = undefined; public subtitle: (string | undefined) = undefined;
public title: string = undefined; public title: (string | undefined) = undefined;
public type: number = MetadataType.MOVIE; public type: number = MetadataType.MOVIE;
} }

View File

@@ -6,18 +6,18 @@ import { MetadataType } from "../enums";
export default class MusicTrackMediaMetadata { export default class MusicTrackMediaMetadata {
public albumArtist: string = undefined; public albumArtist: (string | undefined) = undefined;
public albumName: string = undefined; public albumName: (string | undefined) = undefined;
public artist: string = undefined; public artist: (string | undefined) = undefined;
public artistName: string = undefined; public artistName: (string | undefined) = undefined;
public composer: string = undefined; public composer: (string | undefined) = undefined;
public discNumber: number = undefined; public discNumber: (number | undefined) = undefined;
public images: Image[] = undefined; public images: (Image[] | undefined) = undefined;
public metadataType: number = MetadataType.MUSIC_TRACK; public metadataType: number = MetadataType.MUSIC_TRACK;
public releaseDate: string = undefined; public releaseDate: (string | undefined) = undefined;
public releaseYear: number = undefined; public releaseYear: (number | undefined) = undefined;
public songName: string = undefined; public songName: (string | undefined) = undefined;
public title: string = undefined; public title: (string | undefined) = undefined;
public trackNumber: number = undefined; public trackNumber: (number | undefined) = undefined;
public type: number = MetadataType.MUSIC_TRACK; public type: number = MetadataType.MUSIC_TRACK;
} }

View File

@@ -6,15 +6,15 @@ import { MetadataType } from "../enums";
export default class PhotoMediaMetadata { export default class PhotoMediaMetadata {
public artist: string = undefined; public artist: (string | undefined) = undefined;
public creationDateTime: string = undefined; public creationDateTime: (string | undefined) = undefined;
public height: number = undefined; public height: (number | undefined) = undefined;
public images: Image[] = undefined; public images: (Image[] | undefined) = undefined;
public latitude: number = undefined; public latitude: (number | undefined) = undefined;
public location: string = undefined; public location: (string | undefined) = undefined;
public longitude: number = undefined; public longitude: (number | undefined) = undefined;
public metadataType: number = MetadataType.PHOTO; public metadataType: number = MetadataType.PHOTO;
public title: string = undefined; public title: (string | undefined) = undefined;
public type: number = MetadataType.PHOTO; public type: number = MetadataType.PHOTO;
public width: number = undefined; public width: (number | undefined) = undefined;
} }

View File

@@ -5,9 +5,9 @@ import QueueItem from "./QueueItem";
export default class QueueInsertItemsRequest { export default class QueueInsertItemsRequest {
public customData: any = null; public customData: any = null;
public insertBefore: number = null; public insertBefore: (number | null) = null;
public requestId: number = null; public requestId: (number | null) = null;
public sessionId: string = null; public sessionId: (string | null) = null;
public type: string = "QUEUE_INSERT"; public type: string = "QUEUE_INSERT";
constructor ( constructor (

View File

@@ -4,12 +4,12 @@ import MediaInfo from "./MediaInfo";
export default class QueueItem { export default class QueueItem {
public activeTrackIds: number[] = null; public activeTrackIds: (number[] | null) = null;
public autoplay: boolean = true; public autoplay: boolean = true;
public customData: any = null; public customData: any = null;
public itemId: number = null; public itemId: (number | null) = null;
public media: MediaInfo; public media: MediaInfo;
public playbackDuration: number = null; public playbackDuration: (number | null) = null;
public preloadTime: number = 0; public preloadTime: number = 0;
public startTime: number = 0; public startTime: number = 0;

View File

@@ -8,8 +8,8 @@ import { RepeatMode } from "../enums";
export default class QueueLoadRequest { export default class QueueLoadRequest {
public customData: any = null; public customData: any = null;
public repeatMode: string = RepeatMode.OFF; public repeatMode: string = RepeatMode.OFF;
public requestId: number = null; public requestId: (number | null) = null;
public sessionId: string = null; public sessionId: (string | null) = null;
public startIndex: number = 0; public startIndex: number = 0;
public type: string = "QUEUE_LOAD"; public type: string = "QUEUE_LOAD";

View File

@@ -2,8 +2,8 @@
export default class QueueRemoveItemsRequest { export default class QueueRemoveItemsRequest {
public customData: any = null; public customData: any = null;
public requestId: number = null; public requestId: (number | null) = null;
public sessionId: string = null; public sessionId: (string | null) = null;
public type: string = "QUEUE_REMOVE"; public type: string = "QUEUE_REMOVE";
constructor ( constructor (

View File

@@ -2,9 +2,9 @@
export default class QueueReorderItemsRequest { export default class QueueReorderItemsRequest {
public customData: any = null; public customData: any = null;
public insertBefore: number = null; public insertBefore: (number | null) = null;
public requestId: number = null; public requestId: (number | null) = null;
public sessionId: string = null; public sessionId: (string | null) = null;
public type: string = "QUEUE_REORDER"; public type: string = "QUEUE_REORDER";
constructor ( constructor (

View File

@@ -2,8 +2,8 @@
export default class QueueSetPropertiesRequest { export default class QueueSetPropertiesRequest {
public customData: any = null; public customData: any = null;
public repeatMode: string = null; public repeatMode: (string | null) = null;
public requestId: number = null; public requestId: (number | null) = null;
public sessionId: string = null; public sessionId: (string | null) = null;
public type: string = "QUEUE_UPDATE"; public type: string = "QUEUE_UPDATE";
} }

View File

@@ -5,8 +5,8 @@ import QueueItem from "./QueueItem";
export default class QueueUpdateItemsRequest { export default class QueueUpdateItemsRequest {
public customData: any = null; public customData: any = null;
public requestId: number = null; public requestId: (number | null) = null;
public sessionId: string = null; public sessionId: (string | null) = null;
public type: string = "QUEUE_UPDATE"; public type: string = "QUEUE_UPDATE";
constructor ( constructor (

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
export default class SeekRequest { export default class SeekRequest {
public currentTime: number = null; public currentTime: (number | null) = null;
public customData: any = null; public customData: any = null;
public resumeState: string = null; public resumeState: (string | null) = null;
} }

View File

@@ -1,16 +1,16 @@
"use strict"; "use strict";
export default class TextTrackStyle { export default class TextTrackStyle {
public backgroundColor: string = null; public backgroundColor: (string | null) = null;
public customData: any = null; public customData: any = null;
public edgeColor: string = null; public edgeColor: (string | null) = null;
public edgeType: string = null; public edgeType: (string | null) = null;
public fontFamily: string = null; public fontFamily: (string | null) = null;
public fontGenericFamily: string = null; public fontGenericFamily: (string | null) = null;
public fontScale: number = null; public fontScale: (number | null) = null;
public fontStyle: string = null; public fontStyle: (string | null) = null;
public foregroundColor: string = null; public foregroundColor: (string | null) = null;
public windowColor: string = null; public windowColor: (string | null) = null;
public windowRoundedCornerRadius: number = null; public windowRoundedCornerRadius: (number | null) = null;
public windowType: string = null; public windowType: (string | null) = null;
} }

View File

@@ -2,11 +2,11 @@
export default class Track { export default class Track {
public customData: any = null; public customData: any = null;
public language: string = null; public language: (string | null) = null;
public name: string = null; public name: (string | null) = null;
public subtype: string = null; public subtype: (string | null) = null;
public trackContentId: string = null; public trackContentId: (string | null) = null;
public trackContentType: string = null; public trackContentType: (string | null) = null;
constructor ( constructor (
public trackId: number public trackId: number

View File

@@ -6,16 +6,16 @@ import { MetadataType } from "../enums";
export default class TvShowMediaMetadata { export default class TvShowMediaMetadata {
public episode: number = undefined; public episode: (number | undefined) = undefined;
public episodeNumber: number = undefined; public episodeNumber: (number | undefined) = undefined;
public episodeTitle: string = undefined; public episodeTitle: (string | undefined) = undefined;
public images: Image[] = undefined; public images: (Image[] | undefined) = undefined;
public metadataType: number = MetadataType.TV_SHOW; public metadataType: number = MetadataType.TV_SHOW;
public originalAirdate: string = undefined; public originalAirdate: (string | undefined) = undefined;
public releaseYear: number = undefined; public releaseYear: (number | undefined) = undefined;
public season: number = undefined; public season: (number | undefined) = undefined;
public seasonNumber: number = undefined; public seasonNumber: (number | undefined) = undefined;
public seriesTitle: string = undefined; public seriesTitle: (string | undefined) = undefined;
public title: string = undefined; public title: (string | undefined) = undefined;
public type: number = MetadataType.TV_SHOW; public type: number = MetadataType.TV_SHOW;
} }

View File

@@ -15,7 +15,7 @@ import { CAST_LOADER_SCRIPT_URL
* URLs, replace it with the standard API URL, the request for * URLs, replace it with the standard API URL, the request for
* which is handled in the main script. * which is handled in the main script.
*/ */
const { get, set } = Reflect.getOwnPropertyDescriptor( const desc = Reflect.getOwnPropertyDescriptor(
HTMLScriptElement.prototype.wrappedJSObject, "src"); HTMLScriptElement.prototype.wrappedJSObject, "src");
Reflect.defineProperty( Reflect.defineProperty(
@@ -23,13 +23,13 @@ Reflect.defineProperty(
configurable: true configurable: true
, enumerable: true , enumerable: true
, get , get: desc?.get
, set: exportFunction(function (value) { , set: exportFunction(function setFunc (this: HTMLScriptElement, value) {
if (CAST_SCRIPT_URLS.includes(value)) { if (CAST_SCRIPT_URLS.includes(value)) {
return set.call(this, CAST_LOADER_SCRIPT_URL); return desc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
} }
return set.call(this, value); return desc?.set?.call(this, value);
}, window) }, window)
}); });

View File

@@ -25,12 +25,14 @@ export function onMessage (listener: ListenerFunc): ListenerObject {
ev.stopPropagation(); ev.stopPropagation();
} }
// @ts-ignore
document.addEventListener( document.addEventListener(
"__castMessage" "__castMessage"
, on__castMessage, true); , on__castMessage, true);
return { return {
disconnect () { disconnect () {
// @ts-ignore
document.removeEventListener( document.removeEventListener(
"__castMessage" "__castMessage"
, on__castMessage, true); , on__castMessage, true);
@@ -52,12 +54,14 @@ export function onMessageResponse (listener: ListenerFunc): ListenerObject {
listener(JSON.parse(ev.detail)); listener(JSON.parse(ev.detail));
} }
// @ts-ignore
document.addEventListener( document.addEventListener(
"__castMessageResponse" "__castMessageResponse"
, on__castMessageResponse, true); , on__castMessageResponse, true);
return { return {
disconnect () { disconnect () {
// @ts-ignore
document.removeEventListener( document.removeEventListener(
"__castMessageResponse" "__castMessageResponse"
, on__castMessageResponse, true); , on__castMessageResponse, true);

View File

@@ -5,8 +5,8 @@ import * as cast from "../../cast";
export default class CastOptions { export default class CastOptions {
public autoJoinPolicy: string = cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED; public autoJoinPolicy: string = cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
public language: string = null; public language: (string | null) = null;
public receiverApplicationId: string = null; public receiverApplicationId: (string | null) = null;
public resumeSavedSession: boolean = true; public resumeSavedSession: boolean = true;
constructor (options: CastOptions = ({} as CastOptions)) { constructor (options: CastOptions = ({} as CastOptions)) {

View File

@@ -15,19 +15,19 @@ export default class RemotePlayer {
public canControlVolume = false; public canControlVolume = false;
public canPause = false; public canPause = false;
public canSeek = false; public canSeek = false;
public controller: RemotePlayerController = null; public controller: (RemotePlayerController | null) = null;
public currentTime = 0; public currentTime = 0;
public displayName = ""; public displayName = "";
public displayStatus = ""; public displayStatus = "";
public duration = 0; public duration = 0;
public imageUrl: string = null; public imageUrl: (string | null) = null;
public isConnected = false; public isConnected = false;
public isMediaLoaded = false; public isMediaLoaded = false;
public isMuted = false; public isMuted = false;
public isPaused = false; public isPaused = false;
public mediaInfo: cast.media.MediaInfo = null; public mediaInfo: (cast.media.MediaInfo | null) = null;
public playerState: string = null; public playerState: (string | null) = null;
public savedPlayerState: SavedPlayerState = null; public savedPlayerState: (SavedPlayerState | null) = null;
public statusText = ""; public statusText = "";
public title = ""; public title = "";
public volumeLevel = 1; public volumeLevel = 1;

View File

@@ -10,7 +10,7 @@ export default class SessionStateEventData extends EventData {
constructor ( constructor (
public session: CastSession public session: CastSession
, public sessionState: string , public sessionState: string
, public errorCode: string = null) { , public errorCode: (string | null) = null) {
super(SessionEventType.APPLICATION_STATUS_CHANGED); super(SessionEventType.APPLICATION_STATUS_CHANGED);
} }

View File

@@ -11,5 +11,5 @@ export type MessageListener = (namespace: string, message: string) => void;
export type UpdateListener = (isAlive: boolean) => void; export type UpdateListener = (isAlive: boolean) => void;
export type LoadSuccessCallback = (media: Media) => void; export type LoadSuccessCallback = (media: Media) => void;
export type Callbacks = [ SuccessCallback, ErrorCallback ]; export type Callbacks = [ SuccessCallback?, ErrorCallback? ];
export type CallbacksMap = Map<string, Callbacks>; export type CallbacksMap = Map<string, Callbacks>;

View File

@@ -68,12 +68,12 @@ interface BridgeState {
isUpdateAvailable: boolean; isUpdateAvailable: boolean;
wasErrorCheckingUpdates: boolean; wasErrorCheckingUpdates: boolean;
checkUpdatesEllipsis: string; checkUpdatesEllipsis: string;
updateStatus: string; updateStatus?: string;
} }
export default class Bridge extends Component<BridgeProps, BridgeState> { export default class Bridge extends Component<BridgeProps, BridgeState> {
private updateData: any; private updateData: any;
private updateStatusTimeout: number; private updateStatusTimeout?: number;
constructor (props: BridgeProps) { constructor (props: BridgeProps) {
super(props); super(props);
@@ -83,7 +83,6 @@ export default class Bridge extends Component<BridgeProps, BridgeState> {
, isUpdateAvailable: false , isUpdateAvailable: false
, wasErrorCheckingUpdates: false , wasErrorCheckingUpdates: false
, checkUpdatesEllipsis: "..." , checkUpdatesEllipsis: "..."
, updateStatus: null
}; };
this.onCheckUpdates = this.onCheckUpdates.bind(this); this.onCheckUpdates = this.onCheckUpdates.bind(this);
@@ -142,7 +141,7 @@ export default class Bridge extends Component<BridgeProps, BridgeState> {
let statusIcon: string; let statusIcon: string;
let statusTitle: string; let statusTitle: string;
let statusText: string; let statusText: (string | null);
if (!this.props.info) { if (!this.props.info) {
statusIcon = "assets/icons8-cancel-120.png"; statusIcon = "assets/icons8-cancel-120.png";
@@ -156,6 +155,8 @@ export default class Bridge extends Component<BridgeProps, BridgeState> {
statusIcon = "assets/icons8-warn-120.png"; statusIcon = "assets/icons8-warn-120.png";
statusTitle = _("optionsBridgeIssueStatusTitle"); statusTitle = _("optionsBridgeIssueStatusTitle");
} }
statusText = null;
} }
return ( return (
@@ -213,11 +214,14 @@ export default class Bridge extends Component<BridgeProps, BridgeState> {
this.setState({ this.setState({
isCheckingUpdates: false isCheckingUpdates: false
, isUpdateAvailable , isUpdateAvailable
, updateStatus: !isUpdateAvailable
? _("optionsBridgeUpdateStatusNoUpdates")
: null
}); });
if (!isUpdateAvailable) {
this.setState({
updateStatus: _("optionsBridgeUpdateStatusNoUpdates")
});
}
this.showUpdateStatus(); this.showUpdateStatus();
} }
@@ -237,7 +241,7 @@ export default class Bridge extends Component<BridgeProps, BridgeState> {
} }
this.updateStatusTimeout = window.setTimeout(() => { this.updateStatusTimeout = window.setTimeout(() => {
this.setState({ this.setState({
updateStatus: null updateStatus: undefined
}); });
}, 1500); }, 1500);
} }

View File

@@ -23,7 +23,7 @@ interface EditableListState {
export default class EditableList extends Component< export default class EditableList extends Component<
EditableListProps, EditableListState> { EditableListProps, EditableListState> {
private rawViewTextArea: HTMLTextAreaElement; private rawViewTextArea: (HTMLTextAreaElement | null) = null;
constructor (props: EditableListProps) { constructor (props: EditableListProps) {
super(props); super(props);
@@ -140,13 +140,13 @@ export default class EditableList extends Component<
if ("itemPattern" in this.props) { if ("itemPattern" in this.props) {
for (const item of newItems) { for (const item of newItems) {
if (!this.props.itemPattern.test(item)) { if (!this.props.itemPattern.test(item)) {
this.rawViewTextArea.setCustomValidity( this.rawViewTextArea?.setCustomValidity(
this.props.itemPatternError(item)); this.props.itemPatternError(item));
return; return;
} }
} }
this.rawViewTextArea.setCustomValidity(""); this.rawViewTextArea?.setCustomValidity("");
} }
this.props.onChange(newItems); this.props.onChange(newItems);
@@ -154,6 +154,10 @@ export default class EditableList extends Component<
} }
private handleRawViewTextAreaChange (ev: React.ChangeEvent<HTMLTextAreaElement>) { private handleRawViewTextAreaChange (ev: React.ChangeEvent<HTMLTextAreaElement>) {
if (!this.rawViewTextArea) {
return;
}
if (this.rawViewTextArea.scrollHeight > this.rawViewTextArea.clientHeight) { if (this.rawViewTextArea.scrollHeight > this.rawViewTextArea.clientHeight) {
this.rawViewTextArea.style.height = `${this.rawViewTextArea.scrollHeight}px`; this.rawViewTextArea.style.height = `${this.rawViewTextArea.scrollHeight}px`;
} }

View File

@@ -23,7 +23,7 @@ interface EditableListItemState {
export default class EditableListItem extends Component< export default class EditableListItem extends Component<
EditableListItemProps, EditableListItemState> { EditableListItemProps, EditableListItemState> {
private input: HTMLInputElement; private input: (HTMLInputElement | null) = null;
constructor (props: EditableListItemProps) { constructor (props: EditableListItemProps) {
super(props); super(props);
@@ -96,7 +96,7 @@ export default class EditableListItem extends Component<
editing: true editing: true
, editValue: this.props.text , editValue: this.props.text
}, () => { }, () => {
this.input.focus(); this.input?.focus();
}); });
} }

View File

@@ -10,6 +10,7 @@ import Bridge from "./Bridge";
import EditableList from "./EditableList"; import EditableList from "./EditableList";
import bridge, { BridgeInfo } from "../../lib/bridge"; import bridge, { BridgeInfo } from "../../lib/bridge";
import logger from "../../lib/logger";
import options, { Options } from "../../lib/options"; import options, { Options } from "../../lib/options";
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/utils"; import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/utils";
@@ -83,25 +84,23 @@ function getInputValue (input: HTMLInputElement) {
interface OptionsAppState { interface OptionsAppState {
hasLoaded: boolean; hasLoaded: boolean;
options: Options;
bridgeInfo: BridgeInfo;
platform: string;
bridgeLoading: boolean; bridgeLoading: boolean;
isFormValid: boolean; isFormValid: boolean;
hasSaved: boolean; hasSaved: boolean;
options?: Options;
bridgeInfo?: BridgeInfo;
platform?: string;
} }
class OptionsApp extends Component<{}, OptionsAppState> { class OptionsApp extends Component<{}, OptionsAppState> {
private form: HTMLFormElement; private form: (HTMLFormElement | null) = null;
constructor (props: {}) { constructor (props: {}) {
super(props); super(props);
this.state = { this.state = {
hasLoaded: false hasLoaded: false
, options: null
, bridgeInfo: null
, platform: null
, bridgeLoading: true , bridgeLoading: true
, isFormValid: true , isFormValid: true
, hasSaved: false , hasSaved: false
@@ -113,11 +112,11 @@ class OptionsApp extends Component<{}, OptionsAppState> {
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handleWhitelistChange = this.handleWhitelistChange.bind(this); this.handleWhitelistChange = this.handleWhitelistChange.bind(this);
this.handleReceiverSelectorTypeChange this.handleReceiverSelectorTypeChange =
= this.handleReceiverSelectorTypeChange.bind(this); this.handleReceiverSelectorTypeChange.bind(this);
this.getWhitelistItemPatternError this.getWhitelistItemPatternError =
= this.getWhitelistItemPatternError.bind(this); this.getWhitelistItemPatternError.bind(this);
} }
public async componentDidMount () { public async componentDidMount () {
@@ -126,14 +125,18 @@ class OptionsApp extends Component<{}, OptionsAppState> {
, options: await options.getAll() , options: await options.getAll()
}); });
const bridgeInfo = await bridge.getInfo(); try {
const { os } = await browser.runtime.getPlatformInfo(); const bridgeInfo = await bridge.getInfo();
const { os } = await browser.runtime.getPlatformInfo();
this.setState({ this.setState({
bridgeInfo bridgeInfo
, platform: os , platform: os
, bridgeLoading: false , bridgeLoading: false
}); });
} catch {
logger.error("Failed to fetch bridge/platform info.");
}
} }
public render () { public render () {
@@ -143,9 +146,10 @@ class OptionsApp extends Component<{}, OptionsAppState> {
return ( return (
<div> <div>
<Bridge info={ this.state.bridgeInfo } { this.state.bridgeInfo && this.state.platform &&
platform={ this.state.platform } <Bridge info={ this.state.bridgeInfo }
loading={ this.state.bridgeLoading } /> platform={ this.state.platform }
loading={ this.state.bridgeLoading } /> }
<form id="form" ref={ form => { this.form = form; }} <form id="form" ref={ form => { this.form = form; }}
onSubmit={ this.handleFormSubmit } onSubmit={ this.handleFormSubmit }
@@ -163,7 +167,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="mediaEnabled" <input name="mediaEnabled"
type="checkbox" type="checkbox"
checked={ this.state.options.mediaEnabled } checked={ this.state.options?.mediaEnabled }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -175,7 +179,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="mediaSyncElement" <input name="mediaSyncElement"
type="checkbox" type="checkbox"
checked={ this.state.options.mediaSyncElement } checked={ this.state.options?.mediaSyncElement }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -190,7 +194,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="mediaStopOnUnload" <input name="mediaStopOnUnload"
type="checkbox" type="checkbox"
checked={ this.state.options.mediaStopOnUnload } checked={ this.state.options?.mediaStopOnUnload }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -202,7 +206,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="mediaOverlayEnabled" <input name="mediaOverlayEnabled"
type="checkbox" type="checkbox"
checked={ this.state.options.mediaOverlayEnabled } checked={ this.state.options?.mediaOverlayEnabled }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -219,7 +223,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="localMediaEnabled" <input name="localMediaEnabled"
type="checkbox" type="checkbox"
checked={ this.state.options.localMediaEnabled } checked={ this.state.options?.localMediaEnabled }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -240,7 +244,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
required required
min="1025" min="1025"
max="65535" max="65535"
value={ this.state.options.localMediaServerPort } value={ this.state.options?.localMediaServerPort }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
</label> </label>
@@ -258,7 +262,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="mirroringEnabled" <input name="mirroringEnabled"
type="checkbox" type="checkbox"
checked={ this.state.options.mirroringEnabled } checked={ this.state.options?.mirroringEnabled }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -274,7 +278,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<input name="mirroringAppId" <input name="mirroringAppId"
type="text" type="text"
required required
value={ this.state.options.mirroringAppId } value={ this.state.options?.mirroringAppId }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
<div className="option__description"> <div className="option__description">
{ _("optionsMirroringAppIdDescription") } { _("optionsMirroringAppIdDescription") }
@@ -298,7 +302,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
</div> </div>
<div className="option__control"> <div className="option__control">
<select name="receiverSelectorType" <select name="receiverSelectorType"
value={ this.state.options.receiverSelectorType } value={ this.state.options?.receiverSelectorType }
onChange={ this.handleReceiverSelectorTypeChange }> onChange={ this.handleReceiverSelectorTypeChange }>
<option value={ ReceiverSelectorType.Popup }> <option value={ ReceiverSelectorType.Popup }>
{ _("optionsReceiverSelectorTypeBrowser") } { _("optionsReceiverSelectorTypeBrowser") }
@@ -314,7 +318,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="receiverSelectorWaitForConnection" <input name="receiverSelectorWaitForConnection"
type="checkbox" type="checkbox"
checked={ this.state.options.receiverSelectorWaitForConnection } checked={ this.state.options?.receiverSelectorWaitForConnection }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -329,7 +333,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="receiverSelectorCloseIfFocusLost" <input name="receiverSelectorCloseIfFocusLost"
type="checkbox" type="checkbox"
checked={ this.state.options.receiverSelectorCloseIfFocusLost } checked={ this.state.options?.receiverSelectorCloseIfFocusLost }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -350,7 +354,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
<div className="option__control"> <div className="option__control">
<input name="userAgentWhitelistEnabled" <input name="userAgentWhitelistEnabled"
type="checkbox" type="checkbox"
checked={ this.state.options.userAgentWhitelistEnabled } checked={ this.state.options?.userAgentWhitelistEnabled }
onChange={ this.handleInputChange } /> onChange={ this.handleInputChange } />
</div> </div>
<div className="option__label"> <div className="option__label">
@@ -363,10 +367,11 @@ class OptionsApp extends Component<{}, OptionsAppState> {
{ _("optionsUserAgentWhitelistContent") } { _("optionsUserAgentWhitelistContent") }
</div> </div>
<div className="option__control"> <div className="option__control">
<EditableList data={ this.state.options.userAgentWhitelist } { this.state.options?.userAgentWhitelist &&
onChange={ this.handleWhitelistChange } <EditableList data={ this.state.options.userAgentWhitelist }
itemPattern={ REMOTE_MATCH_PATTERN_REGEX } onChange={ this.handleWhitelistChange }
itemPatternError={ this.getWhitelistItemPatternError } /> itemPattern={ REMOTE_MATCH_PATTERN_REGEX }
itemPatternError={ this.getWhitelistItemPatternError } /> }
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -400,20 +405,22 @@ class OptionsApp extends Component<{}, OptionsAppState> {
private async handleFormSubmit (ev: React.FormEvent<HTMLFormElement>) { private async handleFormSubmit (ev: React.FormEvent<HTMLFormElement>) {
ev.preventDefault(); ev.preventDefault();
this.form.reportValidity(); this.form?.reportValidity();
try { try {
await options.setAll(this.state.options); if (this.state.options) {
await options.setAll(this.state.options);
this.setState({ this.setState({
hasSaved: true hasSaved: true
}, () => { }, () => {
window.setTimeout(() => { window.setTimeout(() => {
this.setState({ this.setState({
hasSaved: false hasSaved: false
}); });
}, 1000); }, 1000);
}); });
}
} catch (err) { } catch (err) {
console.error("Failed to save options"); console.error("Failed to save options");
} }
@@ -422,14 +429,20 @@ class OptionsApp extends Component<{}, OptionsAppState> {
private handleFormChange (ev: React.FormEvent<HTMLFormElement>) { private handleFormChange (ev: React.FormEvent<HTMLFormElement>) {
ev.preventDefault(); ev.preventDefault();
this.setState({ const isFormValid = this.form?.checkValidity();
isFormValid: this.form.checkValidity() if (isFormValid !== undefined) {
}); this.setState({
isFormValid
});
}
} }
private handleInputChange (ev: React.ChangeEvent<HTMLInputElement>) { private handleInputChange (ev: React.ChangeEvent<HTMLInputElement>) {
this.setState(currentState => { this.setState(currentState => {
currentState.options[ev.target.name] = getInputValue(ev.target); if (currentState.options) {
currentState.options[ev.target.name] = getInputValue(ev.target);
}
return currentState; return currentState;
}); });
} }
@@ -438,14 +451,20 @@ class OptionsApp extends Component<{}, OptionsAppState> {
ev: React.ChangeEvent<HTMLSelectElement>) { ev: React.ChangeEvent<HTMLSelectElement>) {
this.setState(currentState => { this.setState(currentState => {
currentState.options[ev.target.name] = parseInt(ev.target.value); if (currentState.options) {
currentState.options[ev.target.name] = parseInt(ev.target.value);
}
return currentState; return currentState;
}); });
} }
private handleWhitelistChange (whitelist: string[]) { private handleWhitelistChange (whitelist: string[]) {
this.setState(currentState => { this.setState(currentState => {
currentState.options.userAgentWhitelist = whitelist; if (currentState.options) {
currentState.options.userAgentWhitelist = whitelist;
}
return currentState; return currentState;
}); });
} }

View File

@@ -31,14 +31,15 @@ interface PopupAppState {
mediaType: ReceiverSelectorMediaType; mediaType: ReceiverSelectorMediaType;
availableMediaTypes: ReceiverSelectorMediaType; availableMediaTypes: ReceiverSelectorMediaType;
isLoading: boolean; isLoading: boolean;
filePath: string;
requestedAppId: string; filePath?: string;
requestedAppId?: string;
} }
class PopupApp extends Component<{}, PopupAppState> { class PopupApp extends Component<{}, PopupAppState> {
private port: browser.runtime.Port; private port?: browser.runtime.Port;
private win: browser.windows.Window; private win?: browser.windows.Window;
private defaultMediaType: ReceiverSelectorMediaType; private defaultMediaType?: ReceiverSelectorMediaType;
constructor (props: {}) { constructor (props: {}) {
super(props); super(props);
@@ -48,8 +49,6 @@ class PopupApp extends Component<{}, PopupAppState> {
, mediaType: ReceiverSelectorMediaType.App , mediaType: ReceiverSelectorMediaType.App
, availableMediaTypes: ReceiverSelectorMediaType.App , availableMediaTypes: ReceiverSelectorMediaType.App
, isLoading: false , isLoading: false
, filePath: null
, requestedAppId: null
}; };
// Store window ref // Store window ref
@@ -78,7 +77,12 @@ class PopupApp extends Component<{}, PopupAppState> {
} }
case "popup:/populateReceiverList": { case "popup:/populateReceiverList": {
this.defaultMediaType = message.data.defaultMediaType; const { receivers, availableMediaTypes, defaultMediaType }
: { receivers: Receiver[]
, availableMediaTypes: ReceiverSelectorMediaType
, defaultMediaType: ReceiverSelectorMediaType } = message.data;
this.defaultMediaType = defaultMediaType;
this.setState({ this.setState({
receivers: message.data.receivers receivers: message.data.receivers
@@ -99,6 +103,10 @@ class PopupApp extends Component<{}, PopupAppState> {
public componentDidUpdate () { public componentDidUpdate () {
setTimeout(() => { setTimeout(() => {
if (this.win?.id === undefined) {
return;
}
// Fit window to content height // Fit window to content height
const frameHeight = window.outerHeight - window.innerHeight; const frameHeight = window.outerHeight - window.innerHeight;
const windowHeight = document.body.clientHeight + frameHeight; const windowHeight = document.body.clientHeight + frameHeight;
@@ -137,8 +145,9 @@ class PopupApp extends Component<{}, PopupAppState> {
<option value={ ReceiverSelectorMediaType.App } <option value={ ReceiverSelectorMediaType.App }
disabled={ !(this.state.availableMediaTypes disabled={ !(this.state.availableMediaTypes
& ReceiverSelectorMediaType.App) }> & ReceiverSelectorMediaType.App) }>
{ knownApps[this.state.requestedAppId] { (this.state.requestedAppId
?? _("popupMediaTypeApp") } && knownApps[this.state.requestedAppId])
?? _("popupMediaTypeApp") }
</option> </option>
<option value={ ReceiverSelectorMediaType.Tab } <option value={ ReceiverSelectorMediaType.Tab }
disabled={ !(this.state.availableMediaTypes disabled={ !(this.state.availableMediaTypes
@@ -157,7 +166,7 @@ class PopupApp extends Component<{}, PopupAppState> {
disabled={ !(this.state.availableMediaTypes disabled={ !(this.state.availableMediaTypes
& ReceiverSelectorMediaType.File) }> & ReceiverSelectorMediaType.File) }>
{ this.state.filePath { this.state.filePath
? truncatedFileName ? truncatedFileName!
: _("popupMediaTypeFile") } : _("popupMediaTypeFile") }
</option> </option>
</select> </select>
@@ -188,7 +197,7 @@ class PopupApp extends Component<{}, PopupAppState> {
isLoading: true isLoading: true
}); });
this.port.postMessage({ this.port?.postMessage({
subject: "receiverSelector:/selected" subject: "receiverSelector:/selected"
, data: { , data: {
receiver receiver
@@ -199,7 +208,7 @@ class PopupApp extends Component<{}, PopupAppState> {
} }
private onStop (receiver: Receiver) { private onStop (receiver: Receiver) {
this.port.postMessage({ this.port?.postMessage({
subject: "receiverSelector:/stop" subject: "receiverSelector:/stop"
, data: { receiver } , data: { receiver }
}); });
@@ -220,9 +229,11 @@ class PopupApp extends Component<{}, PopupAppState> {
} }
// Set media type to default if failed to set filePath // Set media type to default if failed to set filePath
this.setState({ if (this.defaultMediaType) {
mediaType: this.defaultMediaType this.setState({
}); mediaType: this.defaultMediaType
});
}
} else { } else {
this.setState({ this.setState({
mediaType mediaType
@@ -230,7 +241,7 @@ class PopupApp extends Component<{}, PopupAppState> {
} }
this.setState({ this.setState({
filePath: null filePath: undefined
}); });
} }
} }
@@ -279,6 +290,10 @@ class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
} }
public render () { public render () {
if (!this.props.receiver.status) {
return;
}
const { application } = this.props.receiver.status; const { application } = this.props.receiver.status;
return ( return (
@@ -287,7 +302,7 @@ class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
{ this.props.receiver.friendlyName } { this.props.receiver.friendlyName }
</div> </div>
<div className="receiver__address" <div className="receiver__address"
title={ !application.isIdleScreen && application.statusText }> title={ !application.isIdleScreen ? application.statusText : "" }>
{ application.isIdleScreen { application.isIdleScreen
? `${this.props.receiver.host}:${this.props.receiver.port}` ? `${this.props.receiver.host}:${this.props.receiver.port}`
: application.statusText } : application.statusText }
@@ -311,6 +326,10 @@ class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
} }
private handleCast () { private handleCast () {
if (!this.props.receiver.status) {
return;
}
const { application } = this.props.receiver.status; const { application } = this.props.receiver.status;
if (!application.isIdleScreen && this.state.showAlternateAction) { if (!application.isIdleScreen && this.state.showAlternateAction) {

View File

@@ -7,5 +7,6 @@
, "removeComments": true , "removeComments": true
, "resolveJsonModule": true , "resolveJsonModule": true
, "target": "es6" , "target": "es6"
. "strict": true
} }
} }