mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-12 02:29:59 +00:00
Add context menu to receiver selector + other improvements
This commit is contained in:
@@ -41,11 +41,15 @@ Missing/outdated strings:
|
|||||||
- `optionsBridgeCompatible`
|
- `optionsBridgeCompatible`
|
||||||
- `optionsBridgeLikelyCompatible`
|
- `optionsBridgeLikelyCompatible`
|
||||||
- `optionsBridgeIncompatible`
|
- `optionsBridgeIncompatible`
|
||||||
|
- `popupCastMenuTitle`
|
||||||
|
- `popupStopMenuTitle`
|
||||||
|
|
||||||
- `es`
|
- `es`
|
||||||
|
|
||||||
- `popupWhitelistNotWhitelisted`
|
- `popupWhitelistNotWhitelisted`
|
||||||
- `popupWhitelistAddToWhitelist`
|
- `popupWhitelistAddToWhitelist`
|
||||||
|
- `popupCastMenuTitle`
|
||||||
|
- `popupStopMenuTitle`
|
||||||
|
|
||||||
- `nl`
|
- `nl`
|
||||||
|
|
||||||
@@ -63,10 +67,14 @@ Missing/outdated strings:
|
|||||||
- `optionsBridgeCompatible`
|
- `optionsBridgeCompatible`
|
||||||
- `optionsBridgeLikelyCompatible`
|
- `optionsBridgeLikelyCompatible`
|
||||||
- `optionsBridgeIncompatible`
|
- `optionsBridgeIncompatible`
|
||||||
|
- `popupCastMenuTitle`
|
||||||
|
- `popupStopMenuTitle`
|
||||||
|
|
||||||
- `no`
|
- `no`
|
||||||
- `popupWhitelistNotWhitelisted`
|
- `popupWhitelistNotWhitelisted`
|
||||||
- `popupWhitelistAddToWhitelist`
|
- `popupWhitelistAddToWhitelist`
|
||||||
|
- `popupCastMenuTitle`
|
||||||
|
- `popupStopMenuTitle`
|
||||||
|
|
||||||
### NSIS Installer Localization
|
### NSIS Installer Localization
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"popupStopButtonTitle": {
|
|
||||||
"message": "Stop",
|
"popupCastMenuTitle": {
|
||||||
"description": "Alternate action button text displayed instead of popupCastButtonTitle."
|
"message": "Cast to \"$deviceName$\"",
|
||||||
|
"description": "Menu text for cast item in context menu for receivers in receiver selector.",
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Living Room TV"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"popupStopMenuTitle": {
|
||||||
|
"message": "Stop \"$appName$\" on \"$deviceName$\"",
|
||||||
|
"description": "Menu text for stop item in context menu for receivers in receiver selector.",
|
||||||
|
"placeholders": {
|
||||||
|
"appName": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Netflix"
|
||||||
|
},
|
||||||
|
"deviceName": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "Living Room TV"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"contextCast": {
|
"contextCast": {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export interface ReceiverSelectionCast {
|
|||||||
actionType: ReceiverSelectionActionType.Cast;
|
actionType: ReceiverSelectionActionType.Cast;
|
||||||
receiverDevice: ReceiverDevice;
|
receiverDevice: ReceiverDevice;
|
||||||
mediaType: ReceiverSelectorMediaType;
|
mediaType: ReceiverSelectorMediaType;
|
||||||
filePath?: string;
|
|
||||||
}
|
}
|
||||||
export interface ReceiverSelectionStop {
|
export interface ReceiverSelectionStop {
|
||||||
actionType: ReceiverSelectionActionType.Stop;
|
actionType: ReceiverSelectionActionType.Stop;
|
||||||
@@ -39,6 +38,7 @@ interface ReceiverSelectorEvents {
|
|||||||
error: string;
|
error: string;
|
||||||
cancelled: void;
|
cancelled: void;
|
||||||
stop: ReceiverSelectionStop;
|
stop: ReceiverSelectionStop;
|
||||||
|
close: void;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Manages the receiver selector popup window and communication with the
|
* Manages the receiver selector popup window and communication with the
|
||||||
@@ -268,6 +268,8 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
|
|||||||
this.dispatchEvent(new CustomEvent("cancelled"));
|
this.dispatchEvent(new CustomEvent("cancelled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent("close"));
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
delete this.windowId;
|
delete this.windowId;
|
||||||
delete this.messagePort;
|
delete this.messagePort;
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ let menuIdCastMedia: MenuId;
|
|||||||
let menuIdWhitelist: MenuId;
|
let menuIdWhitelist: MenuId;
|
||||||
let menuIdWhitelistRecommended: MenuId;
|
let menuIdWhitelistRecommended: MenuId;
|
||||||
|
|
||||||
|
export const menuIdPopupCast = "popup_cast";
|
||||||
|
export const menuIdPopupStop = "popup_stop";
|
||||||
|
|
||||||
/** Match patterns for the whitelist option menus. */
|
/** Match patterns for the whitelist option menus. */
|
||||||
const whitelistChildMenuPatterns = new Map<MenuId, string>();
|
const whitelistChildMenuPatterns = new Map<MenuId, string>();
|
||||||
|
|
||||||
@@ -42,7 +45,9 @@ export async function initMenus() {
|
|||||||
// Global "Cast..." menu item
|
// Global "Cast..." menu item
|
||||||
menuIdCast = browser.menus.create({
|
menuIdCast = browser.menus.create({
|
||||||
contexts: ["browser_action", "page", "tools_menu"],
|
contexts: ["browser_action", "page", "tools_menu"],
|
||||||
title: _("contextCast")
|
title: _("contextCast"),
|
||||||
|
documentUrlPatterns: ["http://*/*", "https://*/*"],
|
||||||
|
icons: { "16": "icons/icon.svg" } // browser_action context
|
||||||
});
|
});
|
||||||
|
|
||||||
// <video>/<audio> "Cast..." context menu item
|
// <video>/<audio> "Cast..." context menu item
|
||||||
@@ -71,6 +76,19 @@ export async function initMenus() {
|
|||||||
parentId: menuIdWhitelist
|
parentId: menuIdWhitelist
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Popup context menus
|
||||||
|
const popupUrlPattern = `${browser.runtime.getURL("ui/popup")}/*`;
|
||||||
|
browser.menus.create({
|
||||||
|
id: menuIdPopupCast,
|
||||||
|
title: _("popupCastButtonTitle"),
|
||||||
|
documentUrlPatterns: [popupUrlPattern]
|
||||||
|
});
|
||||||
|
browser.menus.create({
|
||||||
|
id: menuIdPopupStop,
|
||||||
|
title: _("popupStopButtonTitle"),
|
||||||
|
documentUrlPatterns: [popupUrlPattern]
|
||||||
|
});
|
||||||
|
|
||||||
browser.menus.onShown.addListener(onMenuShown);
|
browser.menus.onShown.addListener(onMenuShown);
|
||||||
browser.menus.onClicked.addListener(onMenuClicked);
|
browser.menus.onClicked.addListener(onMenuClicked);
|
||||||
|
|
||||||
|
|||||||
@@ -126,12 +126,10 @@ async function getSelection(
|
|||||||
function onSelectorSelected(ev: CustomEvent<ReceiverSelectionCast>) {
|
function onSelectorSelected(ev: CustomEvent<ReceiverSelectionCast>) {
|
||||||
logger.info("Selected receiver", ev.detail);
|
logger.info("Selected receiver", ev.detail);
|
||||||
|
|
||||||
removeListeners();
|
|
||||||
resolve({
|
resolve({
|
||||||
actionType: ReceiverSelectionActionType.Cast,
|
actionType: ReceiverSelectionActionType.Cast,
|
||||||
receiverDevice: ev.detail.receiverDevice,
|
receiverDevice: ev.detail.receiverDevice,
|
||||||
mediaType: ev.detail.mediaType,
|
mediaType: ev.detail.mediaType
|
||||||
filePath: ev.detail.filePath
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function onSelectorStop(ev: CustomEvent<ReceiverSelectionStop>) {
|
function onSelectorStop(ev: CustomEvent<ReceiverSelectionStop>) {
|
||||||
@@ -139,7 +137,6 @@ async function getSelection(
|
|||||||
|
|
||||||
deviceManager.stopReceiverApp(ev.detail.receiverDevice.id);
|
deviceManager.stopReceiverApp(ev.detail.receiverDevice.id);
|
||||||
|
|
||||||
removeListeners();
|
|
||||||
resolve({
|
resolve({
|
||||||
actionType: ReceiverSelectionActionType.Stop,
|
actionType: ReceiverSelectionActionType.Stop,
|
||||||
receiverDevice: ev.detail.receiverDevice
|
receiverDevice: ev.detail.receiverDevice
|
||||||
@@ -148,11 +145,9 @@ async function getSelection(
|
|||||||
function onSelectorCancelled() {
|
function onSelectorCancelled() {
|
||||||
logger.info("Cancelled receiver selection");
|
logger.info("Cancelled receiver selection");
|
||||||
|
|
||||||
removeListeners();
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
function onSelectorError(ev: CustomEvent<string>) {
|
function onSelectorError(ev: CustomEvent<string>) {
|
||||||
removeListeners();
|
|
||||||
reject(ev.detail);
|
reject(ev.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +155,7 @@ async function getSelection(
|
|||||||
sharedSelector.addEventListener("stop", onSelectorStop);
|
sharedSelector.addEventListener("stop", onSelectorStop);
|
||||||
sharedSelector.addEventListener("cancelled", onSelectorCancelled);
|
sharedSelector.addEventListener("cancelled", onSelectorCancelled);
|
||||||
sharedSelector.addEventListener("error", onSelectorError);
|
sharedSelector.addEventListener("error", onSelectorError);
|
||||||
|
sharedSelector.addEventListener("close", removeListeners);
|
||||||
|
|
||||||
function removeListeners() {
|
function removeListeners() {
|
||||||
sharedSelector.removeEventListener("selected", onSelectorSelected);
|
sharedSelector.removeEventListener("selected", onSelectorSelected);
|
||||||
@@ -169,6 +165,7 @@ async function getSelection(
|
|||||||
onSelectorCancelled
|
onSelectorCancelled
|
||||||
);
|
);
|
||||||
sharedSelector.removeEventListener("error", onSelectorError);
|
sharedSelector.removeEventListener("error", onSelectorError);
|
||||||
|
sharedSelector.removeEventListener("close", removeListeners);
|
||||||
|
|
||||||
deviceManager.removeEventListener(
|
deviceManager.removeEventListener(
|
||||||
"receiverDeviceUp",
|
"receiverDeviceUp",
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import eventMessaging from "./eventMessaging";
|
|
||||||
|
|
||||||
import messaging, { Message } from "../messaging";
|
import messaging, { Message } from "../messaging";
|
||||||
|
import { PageEventMessenger, ExtensionEventMessenger } from "./eventMessaging";
|
||||||
|
|
||||||
|
// Create messengers manually instead of relying on getters
|
||||||
|
const eventMessaging = {
|
||||||
|
page: new PageEventMessenger(),
|
||||||
|
extension: new ExtensionEventMessenger()
|
||||||
|
};
|
||||||
|
|
||||||
// Message port to background script
|
// Message port to background script
|
||||||
export const backgroundPort = messaging.connect({ name: "cast" });
|
export const backgroundPort = messaging.connect({ name: "cast" });
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import { menuIdPopupCast, menuIdPopupStop } from "../../background/menus";
|
||||||
|
|
||||||
import knownApps, { KnownApp } from "../../cast/knownApps";
|
import knownApps, { KnownApp } from "../../cast/knownApps";
|
||||||
import options from "../../lib/options";
|
import options from "../../lib/options";
|
||||||
|
|
||||||
import messaging, { Message, Port } from "../../messaging";
|
import messaging, { Message, Port } from "../../messaging";
|
||||||
import { getNextEllipsis } from "../../lib/utils";
|
import { getNextEllipsis } from "../utils";
|
||||||
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -67,27 +69,40 @@ function hasRequiredCapabilities(
|
|||||||
|
|
||||||
interface PopupAppProps {}
|
interface PopupAppProps {}
|
||||||
interface PopupAppState {
|
interface PopupAppState {
|
||||||
|
/** List of devices to show in receiver list. */
|
||||||
receiverDevices: ReceiverDevice[];
|
receiverDevices: ReceiverDevice[];
|
||||||
mediaType: ReceiverSelectorMediaType;
|
|
||||||
availableMediaTypes: ReceiverSelectorMediaType;
|
|
||||||
isLoading: boolean;
|
|
||||||
|
|
||||||
filePath?: string;
|
/** Currently selected media type. */
|
||||||
|
mediaType: ReceiverSelectorMediaType;
|
||||||
|
/** Media types available to select. */
|
||||||
|
availableMediaTypes: ReceiverSelectorMediaType;
|
||||||
|
|
||||||
|
/** Sender app ID (if available). */
|
||||||
appId?: string;
|
appId?: string;
|
||||||
|
/** Page info (if launched from page context). */
|
||||||
pageInfo?: ReceiverSelectorPageInfo;
|
pageInfo?: ReceiverSelectorPageInfo;
|
||||||
|
|
||||||
|
/** App details (if matches known app). */
|
||||||
|
knownApp?: KnownApp;
|
||||||
|
/** Whether current page URL matches a whitelist pattern. */
|
||||||
|
isPageWhitelisted: boolean;
|
||||||
|
|
||||||
|
/** Whether casting to a device been initiated from this selector. */
|
||||||
|
isConnecting: boolean;
|
||||||
|
|
||||||
|
// Options
|
||||||
mirroringEnabled: boolean;
|
mirroringEnabled: boolean;
|
||||||
userAgentWhitelistEnabled: boolean;
|
userAgentWhitelistEnabled: boolean;
|
||||||
userAgentWhitelist: string[];
|
userAgentWhitelist: string[];
|
||||||
|
|
||||||
knownApp?: KnownApp;
|
|
||||||
isPageWhitelisted: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
||||||
private port?: Port;
|
private port?: Port;
|
||||||
private win?: browser.windows.Window;
|
private browserWindow?: browser.windows.Window;
|
||||||
private defaultMediaType?: ReceiverSelectorMediaType;
|
|
||||||
|
private resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.fitWindowHeight();
|
||||||
|
});
|
||||||
|
|
||||||
constructor(props: PopupAppProps) {
|
constructor(props: PopupAppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -96,136 +111,107 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
receiverDevices: [],
|
receiverDevices: [],
|
||||||
mediaType: ReceiverSelectorMediaType.App,
|
mediaType: ReceiverSelectorMediaType.App,
|
||||||
availableMediaTypes: ReceiverSelectorMediaType.App,
|
availableMediaTypes: ReceiverSelectorMediaType.App,
|
||||||
isLoading: false,
|
isPageWhitelisted: false,
|
||||||
|
isConnecting: false,
|
||||||
mirroringEnabled: false,
|
mirroringEnabled: false,
|
||||||
userAgentWhitelistEnabled: true,
|
userAgentWhitelistEnabled: true,
|
||||||
userAgentWhitelist: [],
|
userAgentWhitelist: []
|
||||||
isPageWhitelisted: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store window ref
|
// Store window ref
|
||||||
browser.windows.getCurrent().then(win => {
|
browser.windows.getCurrent().then(win => {
|
||||||
this.win = win;
|
this.browserWindow = win;
|
||||||
});
|
});
|
||||||
|
|
||||||
new ResizeObserver(() => {
|
this.onMessage = this.onMessage.bind(this);
|
||||||
this.updateWindowHeight();
|
|
||||||
}).observe(document.body);
|
|
||||||
|
|
||||||
this.onAddToWhitelist = this.onAddToWhitelist.bind(this);
|
this.onAddToWhitelist = this.onAddToWhitelist.bind(this);
|
||||||
this.onSelectChange = this.onSelectChange.bind(this);
|
this.onReceiverCast = this.onReceiverCast.bind(this);
|
||||||
this.onCast = this.onCast.bind(this);
|
this.onReceiverStop = this.onReceiverStop.bind(this);
|
||||||
this.onStop = this.onStop.bind(this);
|
|
||||||
|
this.onContextMenu = this.onContextMenu.bind(this);
|
||||||
|
this.onMenuShown = this.onMenuShown.bind(this);
|
||||||
|
this.onMenuClicked = this.onMenuClicked.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateWindowHeight() {
|
private onMessage(message: Message) {
|
||||||
if (this.win?.id === undefined) {
|
switch (message.subject) {
|
||||||
|
case "popup:init":
|
||||||
|
this.setState({
|
||||||
|
appId: message.data?.appId,
|
||||||
|
pageInfo: message.data?.pageInfo
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "popup:close":
|
||||||
|
window.close();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "popup:update": {
|
||||||
|
this.setState({
|
||||||
|
/**
|
||||||
|
* Filter receiver devices without the required
|
||||||
|
* capabilities.
|
||||||
|
*/
|
||||||
|
receiverDevices: message.data.receiverDevices.filter(
|
||||||
|
receiverDevice => {
|
||||||
|
return hasRequiredCapabilities(
|
||||||
|
receiverDevice,
|
||||||
|
this.state.pageInfo?.sessionRequest
|
||||||
|
?.capabilities
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { availableMediaTypes, defaultMediaType } = message.data;
|
||||||
|
if (
|
||||||
|
availableMediaTypes !== undefined &&
|
||||||
|
defaultMediaType !== undefined
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
availableMediaTypes,
|
||||||
|
mediaType: defaultMediaType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateKnownApp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resize browser window to fit content height. */
|
||||||
|
private fitWindowHeight() {
|
||||||
|
if (this.browserWindow?.id === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const frameHeight = window.outerHeight - window.innerHeight;
|
browser.windows.update(this.browserWindow.id, {
|
||||||
const windowHeight = document.body.clientHeight + frameHeight;
|
height:
|
||||||
|
document.body.clientHeight +
|
||||||
browser.windows.update(this.win.id, {
|
(window.outerHeight - window.innerHeight)
|
||||||
height: windowHeight
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidMount() {
|
|
||||||
this.port = messaging.connect({ name: "popup" });
|
|
||||||
|
|
||||||
this.port.onMessage.addListener((message: Message) => {
|
|
||||||
switch (message.subject) {
|
|
||||||
case "popup:init": {
|
|
||||||
this.setState({
|
|
||||||
appId: message.data?.appId,
|
|
||||||
pageInfo: message.data?.pageInfo
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "popup:update": {
|
|
||||||
const {
|
|
||||||
receiverDevices,
|
|
||||||
availableMediaTypes,
|
|
||||||
defaultMediaType
|
|
||||||
} = message.data;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
/**
|
|
||||||
* Filter receiver devices without the required
|
|
||||||
* capabilities.
|
|
||||||
*/
|
|
||||||
receiverDevices: receiverDevices.filter(
|
|
||||||
receiverDevice => {
|
|
||||||
return hasRequiredCapabilities(
|
|
||||||
receiverDevice,
|
|
||||||
this.state.pageInfo?.sessionRequest
|
|
||||||
?.capabilities
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
availableMediaTypes !== undefined &&
|
|
||||||
defaultMediaType !== undefined
|
|
||||||
) {
|
|
||||||
this.defaultMediaType = defaultMediaType;
|
|
||||||
this.setState({
|
|
||||||
availableMediaTypes,
|
|
||||||
mediaType: defaultMediaType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateKnownApp();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "popup:close": {
|
|
||||||
window.close();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const opts = await options.getAll();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
mirroringEnabled: opts.mirroringEnabled,
|
|
||||||
userAgentWhitelistEnabled: opts.userAgentWhitelistEnabled,
|
|
||||||
userAgentWhitelist: opts.userAgentWhitelist
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateKnownApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate() {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateWindowHeight();
|
|
||||||
}, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateKnownApp() {
|
private updateKnownApp() {
|
||||||
const isAppMediaTypeAvailable = !!(
|
const isAppMediaTypeAvailable = !!(
|
||||||
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
||||||
);
|
);
|
||||||
|
|
||||||
let knownApp: Nullable<KnownApp> = null;
|
let knownApp: KnownApp | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check knownApps for an app with an ID matching the registered
|
* Check knownApps for an app with an ID matching the registered
|
||||||
* app on the target page.
|
* app on the target page.
|
||||||
* Or if there isn't an registered app, check for an app with a
|
|
||||||
* match pattern matching the target page URL.
|
|
||||||
*/
|
*/
|
||||||
if (isAppMediaTypeAvailable && this.state.appId) {
|
if (isAppMediaTypeAvailable && this.state.appId) {
|
||||||
knownApp = knownApps[this.state.appId];
|
knownApp = knownApps[this.state.appId];
|
||||||
} else if (this.state.pageInfo) {
|
} else if (this.state.pageInfo) {
|
||||||
const pageUrl = this.state.pageInfo.url;
|
const pageUrl = this.state.pageInfo.url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Or if there isn't an registered app, check for an app
|
||||||
|
* with a match pattern matching the target page URL.
|
||||||
|
*/
|
||||||
for (const [, app] of Object.entries(knownApps)) {
|
for (const [, app] of Object.entries(knownApps)) {
|
||||||
if (!app.matches) {
|
if (!app.matches) {
|
||||||
continue;
|
continue;
|
||||||
@@ -241,9 +227,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
|
|
||||||
let isPageWhitelisted = false;
|
let isPageWhitelisted = false;
|
||||||
|
|
||||||
/**
|
// Check if target page URL is whitelisted.
|
||||||
* Check if target page URL is whitelisted.
|
|
||||||
*/
|
|
||||||
if (this.state.pageInfo) {
|
if (this.state.pageInfo) {
|
||||||
for (const patternString of this.state.userAgentWhitelist) {
|
for (const patternString of this.state.userAgentWhitelist) {
|
||||||
const pattern = new RemoteMatchPattern(patternString);
|
const pattern = new RemoteMatchPattern(patternString);
|
||||||
@@ -254,29 +238,174 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({ knownApp, isPageWhitelisted });
|
||||||
knownApp: knownApp ?? undefined,
|
}
|
||||||
isPageWhitelisted
|
|
||||||
|
private async onAddToWhitelist(
|
||||||
|
app: KnownApp,
|
||||||
|
pageInfo: ReceiverSelectorPageInfo
|
||||||
|
) {
|
||||||
|
if (!app.matches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const whitelist = await options.get("userAgentWhitelist");
|
||||||
|
if (!whitelist.includes(app.matches)) {
|
||||||
|
whitelist.push(app.matches);
|
||||||
|
await options.set("userAgentWhitelist", whitelist);
|
||||||
|
|
||||||
|
await browser.tabs.reload(pageInfo.tabId);
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onReceiverCast(receiverDevice: ReceiverDevice) {
|
||||||
|
this.setState({ isConnecting: true });
|
||||||
|
|
||||||
|
this.port?.postMessage({
|
||||||
|
subject: "receiverSelector:selected",
|
||||||
|
data: {
|
||||||
|
receiverDevice,
|
||||||
|
actionType: ReceiverSelectionActionType.Cast,
|
||||||
|
mediaType: this.state.mediaType
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
private onReceiverStop(receiverDevice: ReceiverDevice) {
|
||||||
/*
|
this.port?.postMessage({
|
||||||
|
subject: "receiverSelector:stop",
|
||||||
|
data: {
|
||||||
|
receiverDevice,
|
||||||
|
actionType: ReceiverSelectionActionType.Stop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Add file support back to popup
|
private onContextMenu(ev: MouseEvent) {
|
||||||
|
if (!(ev.target instanceof Element)) return;
|
||||||
|
|
||||||
let truncatedFileName: string;
|
const receiverElement = ev.target.closest(".receiver");
|
||||||
|
if (receiverElement) {
|
||||||
if (this.state.filePath) {
|
browser.menus.overrideContext({
|
||||||
const filePath = this.state.filePath;
|
showDefaults: false
|
||||||
const fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
|
});
|
||||||
|
|
||||||
truncatedFileName = fileName.length > 12
|
|
||||||
? `${fileName.substring(0, 12)}...`
|
|
||||||
: fileName;
|
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
private getDeviceFromElement(target: Element) {
|
||||||
|
const receiverElement = target.closest(".receiver");
|
||||||
|
if (!receiverElement) return;
|
||||||
|
|
||||||
|
const receiverElementIndex = [
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
...receiverElement.parentElement!.children
|
||||||
|
].indexOf(receiverElement);
|
||||||
|
|
||||||
|
// Match by index rendered receiver element to device array
|
||||||
|
if (receiverElementIndex > -1) {
|
||||||
|
return this.state.receiverDevices[receiverElementIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle show events for receiver context menus. */
|
||||||
|
private onMenuShown(info: browser.menus._OnShownInfo) {
|
||||||
|
if (!info.targetElementId) return;
|
||||||
|
const target = browser.menus.getTargetElement(info.targetElementId);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const device = this.getDeviceFromElement(target);
|
||||||
|
if (!device) {
|
||||||
|
browser.menus.update(menuIdPopupCast, { visible: false });
|
||||||
|
browser.menus.update(menuIdPopupStop, { visible: false });
|
||||||
|
} else {
|
||||||
|
const app = device.status?.applications?.[0];
|
||||||
|
const isAppRunning = !!(app && !app.isIdleScreen);
|
||||||
|
|
||||||
|
browser.menus.update(menuIdPopupCast, {
|
||||||
|
visible: true,
|
||||||
|
title: _("popupCastMenuTitle", device.friendlyName),
|
||||||
|
enabled: !this.state.isConnecting
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.menus.update(menuIdPopupStop, {
|
||||||
|
visible: isAppRunning,
|
||||||
|
title: isAppRunning
|
||||||
|
? _("popupStopMenuTitle", [
|
||||||
|
app.displayName,
|
||||||
|
device.friendlyName
|
||||||
|
])
|
||||||
|
: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.menus.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle click events for receiver context menus. */
|
||||||
|
private onMenuClicked(info: browser.menus.OnClickData) {
|
||||||
|
if (
|
||||||
|
info.menuItemId !== menuIdPopupCast &&
|
||||||
|
info.menuItemId !== menuIdPopupStop
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info.targetElementId) return;
|
||||||
|
const target = browser.menus.getTargetElement(info.targetElementId);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const device = this.getDeviceFromElement(target);
|
||||||
|
if (!device) return;
|
||||||
|
|
||||||
|
switch (info.menuItemId) {
|
||||||
|
case menuIdPopupCast:
|
||||||
|
this.onReceiverCast(device);
|
||||||
|
break;
|
||||||
|
case menuIdPopupStop:
|
||||||
|
this.onReceiverStop(device);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
this.port = messaging.connect({ name: "popup" });
|
||||||
|
this.port.onMessage.addListener(this.onMessage);
|
||||||
|
|
||||||
|
// Start observing content size changes
|
||||||
|
this.resizeObserver.observe(document.body);
|
||||||
|
|
||||||
|
options.getAll().then(opts => {
|
||||||
|
this.setState({
|
||||||
|
mirroringEnabled: opts.mirroringEnabled,
|
||||||
|
userAgentWhitelistEnabled: opts.userAgentWhitelistEnabled,
|
||||||
|
userAgentWhitelist: opts.userAgentWhitelist
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateKnownApp();
|
||||||
|
|
||||||
|
window.addEventListener("contextmenu", this.onContextMenu);
|
||||||
|
browser.menus.onClicked.addListener(this.onMenuClicked);
|
||||||
|
browser.menus.onShown.addListener(this.onMenuShown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.port?.disconnect();
|
||||||
|
this.resizeObserver.disconnect();
|
||||||
|
|
||||||
|
window.removeEventListener("contextmenu", this.onContextMenu);
|
||||||
|
browser.menus.onClicked.removeListener(this.onMenuClicked);
|
||||||
|
browser.menus.onShown.removeListener(this.onMenuShown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fitWindowHeight();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
const isAppMediaTypeSelected =
|
const isAppMediaTypeSelected =
|
||||||
this.state.mediaType === ReceiverSelectorMediaType.App;
|
this.state.mediaType === ReceiverSelectorMediaType.App;
|
||||||
const isTabMediaTypeSelected =
|
const isTabMediaTypeSelected =
|
||||||
@@ -284,9 +413,6 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
const isScreenMediaTypeSelected =
|
const isScreenMediaTypeSelected =
|
||||||
this.state.mediaType === ReceiverSelectorMediaType.Screen;
|
this.state.mediaType === ReceiverSelectorMediaType.Screen;
|
||||||
|
|
||||||
const isSelectedMediaTypeAvailable = !!(
|
|
||||||
this.state.availableMediaTypes & this.state.mediaType
|
|
||||||
);
|
|
||||||
const isAppMediaTypeAvailable = !!(
|
const isAppMediaTypeAvailable = !!(
|
||||||
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
||||||
);
|
);
|
||||||
@@ -294,7 +420,7 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="whitelist-suggest"
|
className="whitelist-banner"
|
||||||
hidden={
|
hidden={
|
||||||
// If we don't know the app
|
// If we don't know the app
|
||||||
!this.state.knownApp ||
|
!this.state.knownApp ||
|
||||||
@@ -304,7 +430,10 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
(this.state.userAgentWhitelistEnabled &&
|
(this.state.userAgentWhitelistEnabled &&
|
||||||
this.state.isPageWhitelisted) ||
|
this.state.isPageWhitelisted) ||
|
||||||
// If an app is already loaded on the page
|
// If an app is already loaded on the page
|
||||||
isAppMediaTypeAvailable
|
!!(
|
||||||
|
this.state.availableMediaTypes &
|
||||||
|
ReceiverSelectorMediaType.App
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img src="photon_info.svg" />
|
<img src="photon_info.svg" />
|
||||||
@@ -327,14 +456,19 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
{_("popupWhitelistAddToWhitelist")}
|
{_("popupWhitelistAddToWhitelist")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-select">
|
|
||||||
<div className="media-select__label-cast">
|
<div className="media-type-select">
|
||||||
|
<div className="media-type-select__label-cast">
|
||||||
{_("popupMediaSelectCastLabel")}
|
{_("popupMediaSelectCastLabel")}
|
||||||
</div>
|
</div>
|
||||||
<div className="select-wrapper">
|
<div className="select-wrapper">
|
||||||
<select
|
<select
|
||||||
onChange={this.onSelectChange}
|
onChange={ev =>
|
||||||
className="media-select__dropdown"
|
this.setState({
|
||||||
|
mediaType: parseInt(ev.target.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="media-type-select__dropdown"
|
||||||
disabled={
|
disabled={
|
||||||
this.state.availableMediaTypes ===
|
this.state.availableMediaTypes ===
|
||||||
ReceiverSelectorMediaType.None
|
ReceiverSelectorMediaType.None
|
||||||
@@ -379,212 +513,119 @@ class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-select__label-to">
|
<div className="media-type-select__label-to">
|
||||||
{_("popupMediaSelectToLabel")}
|
{_("popupMediaSelectToLabel")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="receivers">
|
<ul className="receivers">
|
||||||
{this.state.receiverDevices &&
|
{!this.state.receiverDevices.length ? (
|
||||||
this.state.receiverDevices.length ? (
|
|
||||||
this.state.receiverDevices.map((receiver, i) => (
|
|
||||||
<ReceiverEntry
|
|
||||||
receiverDevice={receiver}
|
|
||||||
onCast={this.onCast}
|
|
||||||
onStop={this.onStop}
|
|
||||||
isLoading={this.state.isLoading}
|
|
||||||
canCast={isSelectedMediaTypeAvailable}
|
|
||||||
key={i}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="receivers__not-found">
|
<div className="receivers__not-found">
|
||||||
{_("popupNoReceiversFound")}
|
{_("popupNoReceiversFound")}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
this.state.receiverDevices.map((device, i) => (
|
||||||
|
<Receiver
|
||||||
|
details={device}
|
||||||
|
isAnyConnecting={this.state.isConnecting}
|
||||||
|
isMediaTypeAvailable={
|
||||||
|
!!(
|
||||||
|
this.state.availableMediaTypes &
|
||||||
|
this.state.mediaType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onCast={this.onReceiverCast}
|
||||||
|
onStop={this.onReceiverStop}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onAddToWhitelist(
|
|
||||||
app: KnownApp,
|
|
||||||
pageInfo: ReceiverSelectorPageInfo
|
|
||||||
) {
|
|
||||||
if (!app.matches) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const whitelist = await options.get("userAgentWhitelist");
|
|
||||||
if (!whitelist.includes(app.matches)) {
|
|
||||||
whitelist.push(app.matches);
|
|
||||||
await options.set("userAgentWhitelist", whitelist);
|
|
||||||
|
|
||||||
await browser.tabs.reload(pageInfo.tabId);
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCast(receiverDevice: ReceiverDevice) {
|
|
||||||
this.setState({
|
|
||||||
isLoading: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.port?.postMessage({
|
|
||||||
subject: "receiverSelector:selected",
|
|
||||||
data: {
|
|
||||||
receiverDevice,
|
|
||||||
actionType: ReceiverSelectionActionType.Cast,
|
|
||||||
mediaType: this.state.mediaType,
|
|
||||||
filePath: this.state.filePath
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onStop(receiverDevice: ReceiverDevice) {
|
|
||||||
this.port?.postMessage({
|
|
||||||
subject: "receiverSelector:stop",
|
|
||||||
data: {
|
|
||||||
receiverDevice,
|
|
||||||
actionType: ReceiverSelectionActionType.Stop
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onSelectChange(ev: React.ChangeEvent<HTMLSelectElement>) {
|
|
||||||
const mediaType = parseInt(ev.target.value);
|
|
||||||
|
|
||||||
if (mediaType === ReceiverSelectorMediaType.File) {
|
|
||||||
const fileUrl = window.prompt();
|
|
||||||
if (fileUrl) {
|
|
||||||
this.setState({
|
|
||||||
mediaType,
|
|
||||||
filePath: fileUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set media type to default if failed to set filePath
|
|
||||||
if (this.defaultMediaType) {
|
|
||||||
this.setState({
|
|
||||||
mediaType: this.defaultMediaType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
mediaType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
filePath: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReceiverEntryProps {
|
interface ReceiverProps {
|
||||||
receiverDevice: ReceiverDevice;
|
details: ReceiverDevice;
|
||||||
isLoading: boolean;
|
isMediaTypeAvailable: boolean;
|
||||||
canCast: boolean;
|
isAnyConnecting: boolean;
|
||||||
|
|
||||||
|
// Events
|
||||||
onCast(receiverDevice: ReceiverDevice): void;
|
onCast(receiverDevice: ReceiverDevice): void;
|
||||||
onStop(receiverDevice: ReceiverDevice): void;
|
onStop(receiverDevice: ReceiverDevice): void;
|
||||||
}
|
}
|
||||||
|
interface ReceiverState {
|
||||||
interface ReceiverEntryState {
|
isConnecting: boolean;
|
||||||
ellipsis: string;
|
connectingEllipsis: string;
|
||||||
isLoading: boolean;
|
|
||||||
showAlternateAction: boolean;
|
|
||||||
}
|
}
|
||||||
|
class Receiver extends Component<ReceiverProps, ReceiverState> {
|
||||||
|
private ellipsisInterval?: number;
|
||||||
|
|
||||||
class ReceiverEntry extends Component<ReceiverEntryProps, ReceiverEntryState> {
|
constructor(props: ReceiverProps) {
|
||||||
constructor(props: ReceiverEntryProps) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
ellipsis: "",
|
isConnecting: false,
|
||||||
isLoading: false,
|
connectingEllipsis: ""
|
||||||
showAlternateAction: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleActionKeyEvents = (ev: KeyboardEvent) => {
|
|
||||||
if (ev.key === "Alt" || ev.key === "Shift") {
|
|
||||||
this.setState({
|
|
||||||
// Only enable on keydown, otherwise disable
|
|
||||||
showAlternateAction: ev.type === "keydown"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleActionKeyEvents);
|
|
||||||
window.addEventListener("keyup", handleActionKeyEvents);
|
|
||||||
|
|
||||||
window.addEventListener("blur", () => {
|
|
||||||
this.setState({
|
|
||||||
showAlternateAction: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.handleCast = this.handleCast.bind(this);
|
this.handleCast = this.handleCast.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
private handleCast() {
|
||||||
const { status } = this.props.receiverDevice;
|
if (!this.props.details.status) {
|
||||||
const application = status?.applications?.[0];
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ellipsisInterval = window.setInterval(() => {
|
||||||
|
this.setState(state => ({
|
||||||
|
connectingEllipsis: getNextEllipsis(state.connectingEllipsis)
|
||||||
|
}));
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
this.setState({ isConnecting: true });
|
||||||
|
this.props.onCast(this.props.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.clearInterval(this.ellipsisInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const application = this.props.details.status?.applications?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="receiver">
|
<li className="receiver">
|
||||||
<div className="receiver__name">
|
<div className="receiver__name">
|
||||||
{this.props.receiverDevice.friendlyName}
|
{this.props.details.friendlyName}
|
||||||
</div>
|
</div>
|
||||||
<div className="receiver__address">
|
<div className="receiver__address">
|
||||||
{application && !application.isIdleScreen
|
{application && !application.isIdleScreen
|
||||||
? application.statusText
|
? application.statusText
|
||||||
: `${this.props.receiverDevice.host}:${this.props.receiverDevice.port}`}
|
: `${this.props.details.host}:${this.props.details.port}`}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="button receiver__connect"
|
className="button receiver__connect"
|
||||||
onClick={this.handleCast}
|
onClick={this.handleCast}
|
||||||
disabled={
|
disabled={
|
||||||
this.state.showAlternateAction
|
this.props.isAnyConnecting ||
|
||||||
? !application || application.isIdleScreen
|
this.state.isConnecting ||
|
||||||
: this.props.isLoading || !this.props.canCast
|
!this.props.isMediaTypeAvailable
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{this.state.isLoading
|
{this.state.isConnecting
|
||||||
? _(
|
? _(
|
||||||
"popupCastingButtonTitle",
|
"popupCastingButtonTitle",
|
||||||
this.state.isLoading ? this.state.ellipsis : ""
|
this.state.isConnecting
|
||||||
|
? this.state.connectingEllipsis
|
||||||
|
: ""
|
||||||
)
|
)
|
||||||
: this.state.showAlternateAction
|
|
||||||
? _("popupStopButtonTitle")
|
|
||||||
: _("popupCastButtonTitle")}
|
: _("popupCastButtonTitle")}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCast() {
|
|
||||||
const { status } = this.props.receiverDevice;
|
|
||||||
if (!status) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.showAlternateAction) {
|
|
||||||
this.props.onStop(this.props.receiverDevice);
|
|
||||||
} else {
|
|
||||||
this.props.onCast(this.props.receiverDevice);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isLoading: true
|
|
||||||
});
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
this.setState(state => ({
|
|
||||||
ellipsis: getNextEllipsis(state.ellipsis)
|
|
||||||
}));
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render after CSS has loaded
|
// Render after CSS has loaded
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.media-select,
|
.media-type-select,
|
||||||
.receiver:not(:last-child) {
|
.receiver:not(:last-child) {
|
||||||
border-bottom-color: var(--grey-50) !important;
|
border-bottom-color: var(--grey-50) !important;
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.whitelist-suggest {
|
.whitelist-banner {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--blue-50-a30);
|
background-color: var(--blue-50-a30);
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||||
@@ -29,7 +29,7 @@ body {
|
|||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
padding: 0.5em 0.75em;
|
padding: 0.5em 0.75em;
|
||||||
}
|
}
|
||||||
.whitelist-suggest > button {
|
.whitelist-banner > button {
|
||||||
--button-background: hsla(0, 0%, 50%, 0.3);
|
--button-background: hsla(0, 0%, 50%, 0.3);
|
||||||
--button-background-hover: hsla(0, 0%, 30%, 0.3);
|
--button-background-hover: hsla(0, 0%, 30%, 0.3);
|
||||||
--button-background-active: hsla(0, 0%, 10%, 0.3);
|
--button-background-active: hsla(0, 0%, 10%, 0.3);
|
||||||
@@ -37,17 +37,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.whitelist-suggest {
|
.whitelist-banner {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
.whitelist-suggest > button {
|
.whitelist-banner > button {
|
||||||
--button-background: hsla(0, 0%, 50%, 0.3);
|
--button-background: hsla(0, 0%, 50%, 0.3);
|
||||||
--button-background-hover: hsla(0, 0%, 70%, 0.3);
|
--button-background-hover: hsla(0, 0%, 70%, 0.3);
|
||||||
--button-background-active: hsla(0, 0%, 90%, 0.3);
|
--button-background-active: hsla(0, 0%, 90%, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-select {
|
.media-type-select {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -55,14 +55,14 @@ body {
|
|||||||
padding: 0.75em 0;
|
padding: 0.75em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-select__label-cast,
|
.media-type-select__label-cast,
|
||||||
.media-select__label-to {
|
.media-type-select__label-to {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.media-select__label-cast:not(:empty) {
|
.media-type-select__label-cast:not(:empty) {
|
||||||
margin-inline-end: 0.5em;
|
margin-inline-end: 0.5em;
|
||||||
}
|
}
|
||||||
.media-select__label-to:not(:empty) {
|
.media-type-select__label-to:not(:empty) {
|
||||||
margin-inline-start: 0.5em;
|
margin-inline-start: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
ext/src/ui/utils.tsx
Normal file
37
ext/src/ui/utils.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function getNextEllipsis(ellipsis: string): string {
|
||||||
|
if (ellipsis === "") return ".";
|
||||||
|
if (ellipsis === ".") return "..";
|
||||||
|
if (ellipsis === "..") return "...";
|
||||||
|
if (ellipsis === "...") return "";
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadingIndicatorProps {
|
||||||
|
text: string;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
export function LoadingIndicator(props: LoadingIndicatorProps) {
|
||||||
|
const [ellipsis, setEllipsis] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
setEllipsis(prev => getNextEllipsis(prev));
|
||||||
|
}, props.duration ?? 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="loading">
|
||||||
|
{props.text}
|
||||||
|
{ellipsis}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user