mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-13 11:00:01 +00:00
Rewrite popup with Svelte
This commit is contained in:
@@ -47,24 +47,6 @@ const unpackedPath = path.join(distPath, "unpacked");
|
|||||||
|
|
||||||
const outPath = argv.package ? unpackedPath : distPath;
|
const outPath = argv.package ? unpackedPath : distPath;
|
||||||
|
|
||||||
/** @type esbuild.Plugin */
|
|
||||||
const preactCompatPlugin = {
|
|
||||||
/**
|
|
||||||
* Handle react/react-dom preact compat modules.
|
|
||||||
*/
|
|
||||||
name: "preact-compat",
|
|
||||||
setup(build) {
|
|
||||||
const preactPath = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../node_modules/preact/compat/dist/compat.module.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
build.onResolve({ filter: /^(react|react-dom)$/ }, () => ({
|
|
||||||
path: preactPath
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type esbuild.BuildOptions */
|
/** @type esbuild.BuildOptions */
|
||||||
const buildOpts = {
|
const buildOpts = {
|
||||||
bundle: true,
|
bundle: true,
|
||||||
@@ -87,7 +69,7 @@ const buildOpts = {
|
|||||||
// Mirroring sender
|
// Mirroring sender
|
||||||
path.join(srcPath, "/cast/senders/mirroring.ts"),
|
path.join(srcPath, "/cast/senders/mirroring.ts"),
|
||||||
// UI
|
// UI
|
||||||
path.join(srcPath, "ui/popup/index.tsx"),
|
path.join(srcPath, "ui/popup/index.ts"),
|
||||||
path.join(srcPath, "ui/options/index.ts")
|
path.join(srcPath, "ui/options/index.ts")
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
@@ -96,7 +78,6 @@ const buildOpts = {
|
|||||||
MIRRORING_APP_ID: `"${argv.mirroringAppId}"`
|
MIRRORING_APP_ID: `"${argv.mirroringAppId}"`
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
preactCompatPlugin,
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
sveltePlugin({
|
sveltePlugin({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -107,7 +88,7 @@ const buildOpts = {
|
|||||||
copyFilesPlugin({
|
copyFilesPlugin({
|
||||||
src: srcPath,
|
src: srcPath,
|
||||||
dest: outPath,
|
dest: outPath,
|
||||||
excludePattern: /^(manifest\.json|.*\.(ts|tsx|js|jsx|svelte))$/
|
excludePattern: /^(manifest\.json|.*\.(ts|js|svelte))$/
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
18
ext/package-lock.json
generated
18
ext/package-lock.json
generated
@@ -6,12 +6,10 @@
|
|||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/firefox-webext-browser": "^94.0.1",
|
"@types/firefox-webext-browser": "^94.0.1",
|
||||||
"@types/react": "^18.0.6",
|
|
||||||
"@types/react-dom": "^18.0.2",
|
"@types/react-dom": "^18.0.2",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"esbuild": "^0.14.38",
|
"esbuild": "^0.14.38",
|
||||||
"preact": "^10.7.1",
|
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"ts-loader": "^9.2.8",
|
"ts-loader": "^9.2.8",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
@@ -5556,16 +5554,6 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
|
||||||
"version": "10.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.7.1.tgz",
|
|
||||||
"integrity": "sha512-MufnRFz39aIhs9AMFisonjzTud1PK1bY+jcJLo6m2T9Uh8AqjD77w11eAAawmjUogoGOnipECq7e/1RClIKsxg==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/preact"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -11812,12 +11800,6 @@
|
|||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preact": {
|
|
||||||
"version": "10.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.7.1.tgz",
|
|
||||||
"integrity": "sha512-MufnRFz39aIhs9AMFisonjzTud1PK1bY+jcJLo6m2T9Uh8AqjD77w11eAAawmjUogoGOnipECq7e/1RClIKsxg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"prelude-ls": {
|
"prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|||||||
@@ -8,12 +8,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/firefox-webext-browser": "^94.0.1",
|
"@types/firefox-webext-browser": "^94.0.1",
|
||||||
"@types/react": "^18.0.6",
|
|
||||||
"@types/react-dom": "^18.0.2",
|
"@types/react-dom": "^18.0.2",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"esbuild": "^0.14.38",
|
"esbuild": "^0.14.38",
|
||||||
"preact": "^10.7.1",
|
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"ts-loader": "^9.2.8",
|
"ts-loader": "^9.2.8",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
|
|||||||
29
ext/src/cast/utils.ts
Normal file
29
ext/src/cast/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ReceiverDevice, ReceiverDeviceCapabilities } from "../types";
|
||||||
|
import { Capability } from "./sdk/enums";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check receiver device capabilities bitflags against array of
|
||||||
|
* capability strings requested by the sender application.
|
||||||
|
*/
|
||||||
|
export function hasRequiredCapabilities(
|
||||||
|
receiverDevice: ReceiverDevice,
|
||||||
|
requiredCapabilities: Capability[] = []
|
||||||
|
) {
|
||||||
|
const { capabilities } = receiverDevice;
|
||||||
|
return requiredCapabilities.every(capability => {
|
||||||
|
switch (capability) {
|
||||||
|
case Capability.AUDIO_IN:
|
||||||
|
return capabilities & ReceiverDeviceCapabilities.AUDIO_IN;
|
||||||
|
case Capability.AUDIO_OUT:
|
||||||
|
return capabilities & ReceiverDeviceCapabilities.AUDIO_OUT;
|
||||||
|
case Capability.MULTIZONE_GROUP:
|
||||||
|
return (
|
||||||
|
capabilities & ReceiverDeviceCapabilities.MULTIZONE_GROUP
|
||||||
|
);
|
||||||
|
case Capability.VIDEO_IN:
|
||||||
|
return capabilities & ReceiverDeviceCapabilities.VIDEO_IN;
|
||||||
|
case Capability.VIDEO_OUT:
|
||||||
|
return capabilities & ReceiverDeviceCapabilities.VIDEO_OUT;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
29
ext/src/ui/LoadingIndicator.svelte
Normal file
29
ext/src/ui/LoadingIndicator.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
function getNextEllipsis(ellipsis: string): string {
|
||||||
|
if (ellipsis === "") return ".";
|
||||||
|
if (ellipsis === ".") return "..";
|
||||||
|
if (ellipsis === "..") return "...";
|
||||||
|
if (ellipsis === "...") return "";
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export let interval = 500;
|
||||||
|
|
||||||
|
let ellipsis = "";
|
||||||
|
let intervalId: number;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
intervalId = window.setInterval(() => {
|
||||||
|
ellipsis = getNextEllipsis(ellipsis);
|
||||||
|
}, interval);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="ellipsis">{ellipsis}</span>
|
||||||
@@ -2,11 +2,12 @@
|
|||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
import LoadingIndicator from "../LoadingIndicator.svelte";
|
||||||
|
|
||||||
import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge";
|
import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge";
|
||||||
import logger from "../../lib/logger";
|
import logger from "../../lib/logger";
|
||||||
|
|
||||||
import { Options } from "../../lib/options";
|
import { Options } from "../../lib/options";
|
||||||
import { getNextEllipsis } from "../utils";
|
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
@@ -68,8 +69,6 @@
|
|||||||
let isCheckingUpdate = false;
|
let isCheckingUpdate = false;
|
||||||
let isUpdateAvailable = false;
|
let isUpdateAvailable = false;
|
||||||
|
|
||||||
let checkUpdateEllipsis = "";
|
|
||||||
|
|
||||||
interface GitHubRelease {
|
interface GitHubRelease {
|
||||||
url: string;
|
url: string;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
@@ -83,10 +82,6 @@
|
|||||||
async function checkUpdate() {
|
async function checkUpdate() {
|
||||||
isCheckingUpdate = true;
|
isCheckingUpdate = true;
|
||||||
|
|
||||||
const checkUpdateTimeout = window.setInterval(() => {
|
|
||||||
checkUpdateEllipsis = getNextEllipsis(checkUpdateEllipsis);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
let releases: GitHubRelease[];
|
let releases: GitHubRelease[];
|
||||||
try {
|
try {
|
||||||
releases = await fetch(
|
releases = await fetch(
|
||||||
@@ -96,8 +91,6 @@
|
|||||||
isCheckingUpdate = false;
|
isCheckingUpdate = false;
|
||||||
updateStatus = _("optionsBridgeUpdateStatusError");
|
updateStatus = _("optionsBridgeUpdateStatusError");
|
||||||
return;
|
return;
|
||||||
} finally {
|
|
||||||
window.clearTimeout(checkUpdateTimeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure valid response
|
// Ensure valid response
|
||||||
@@ -278,7 +271,8 @@
|
|||||||
on:click={checkUpdate}
|
on:click={checkUpdate}
|
||||||
>
|
>
|
||||||
{#if isCheckingUpdate}
|
{#if isCheckingUpdate}
|
||||||
{_("optionsBridgeUpdateChecking", checkUpdateEllipsis)}
|
{_("optionsBridgeUpdateChecking", "")}<LoadingIndicator
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
{_("optionsBridgeUpdateCheck")}
|
{_("optionsBridgeUpdateCheck")}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
451
ext/src/ui/popup/Popup.svelte
Normal file
451
ext/src/ui/popup/Popup.svelte
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { afterUpdate, onMount, tick } from "svelte";
|
||||||
|
|
||||||
|
import LoadingIndicator from "../LoadingIndicator.svelte";
|
||||||
|
|
||||||
|
import { ReceiverSelectorPageInfo } from "../../background/ReceiverSelector";
|
||||||
|
import { menuIdPopupCast, menuIdPopupStop } from "../../background/menus";
|
||||||
|
|
||||||
|
import messaging, { Message, Port } from "../../messaging";
|
||||||
|
import options, { Options } from "../../lib/options";
|
||||||
|
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ReceiverDevice,
|
||||||
|
ReceiverSelectionActionType,
|
||||||
|
ReceiverSelectorMediaType
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
|
import knownApps, { KnownApp } from "../../cast/knownApps";
|
||||||
|
import { hasRequiredCapabilities } from "../../cast/utils";
|
||||||
|
|
||||||
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
/** List of devices to show in receiver list. */
|
||||||
|
let receiverDevices: ReceiverDevice[] = [];
|
||||||
|
|
||||||
|
/** Currently selected media type. */
|
||||||
|
let mediaType = ReceiverSelectorMediaType.App;
|
||||||
|
/** Media types available to select. */
|
||||||
|
let availableMediaTypes = ReceiverSelectorMediaType.App;
|
||||||
|
|
||||||
|
/** Sender app ID (if available). */
|
||||||
|
let appId: Optional<string>;
|
||||||
|
/** Page info (if launched from page context). */
|
||||||
|
let pageInfo: Optional<ReceiverSelectorPageInfo>;
|
||||||
|
|
||||||
|
/** App details (if matches known app). */
|
||||||
|
let knownApp: Nullable<KnownApp> = null;
|
||||||
|
|
||||||
|
/** Whether current page URL matches a whitelist pattern. */
|
||||||
|
let isPageWhitelisted = false;
|
||||||
|
|
||||||
|
/** Whether casting to a device been initiated from this selector. */
|
||||||
|
let isConnecting = false;
|
||||||
|
let connectingId: Nullable<string> = null;
|
||||||
|
|
||||||
|
/** Extension options */
|
||||||
|
let opts: Nullable<Options> = null;
|
||||||
|
|
||||||
|
$: isMediaTypeAvailable = !!(availableMediaTypes & mediaType);
|
||||||
|
$: isAppMediaTypeAvailable = !!(
|
||||||
|
availableMediaTypes & ReceiverSelectorMediaType.App
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Whether to display whitelist suggestion banner. */
|
||||||
|
$: shouldSuggestWhitelist =
|
||||||
|
// If we know the app
|
||||||
|
knownApp &&
|
||||||
|
// If the whitelist is enabled
|
||||||
|
opts?.siteWhitelistEnabled &&
|
||||||
|
// If the page is not whitelisted
|
||||||
|
!isPageWhitelisted &&
|
||||||
|
// If an app is not already loaded on the page
|
||||||
|
!(availableMediaTypes & ReceiverSelectorMediaType.App);
|
||||||
|
|
||||||
|
let port: Nullable<Port> = null;
|
||||||
|
let browserWindow: Nullable<browser.windows.Window> = null;
|
||||||
|
let resizeObserver = new ResizeObserver(() => fitWindowHeight());
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
port = messaging.connect({ name: "popup" });
|
||||||
|
port.onMessage.addListener(onMessage);
|
||||||
|
|
||||||
|
browserWindow = await browser.windows.getCurrent();
|
||||||
|
|
||||||
|
opts = await options.getAll();
|
||||||
|
options.addEventListener("changed", async ev => {
|
||||||
|
opts = await options.getAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update available media types and ensure selected media
|
||||||
|
* type is valid.
|
||||||
|
*/
|
||||||
|
if (ev.detail.includes("mirroringEnabled")) {
|
||||||
|
const mirroringMediaTypes =
|
||||||
|
ReceiverSelectorMediaType.Tab |
|
||||||
|
ReceiverSelectorMediaType.Screen;
|
||||||
|
|
||||||
|
if (!opts.mirroringEnabled) {
|
||||||
|
availableMediaTypes &= ~mirroringMediaTypes;
|
||||||
|
} else {
|
||||||
|
availableMediaTypes |= mirroringMediaTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(availableMediaTypes & mediaType)) {
|
||||||
|
if (availableMediaTypes & ReceiverSelectorMediaType.App) {
|
||||||
|
mediaType = ReceiverSelectorMediaType.App;
|
||||||
|
} else if (
|
||||||
|
availableMediaTypes & ReceiverSelectorMediaType.Tab
|
||||||
|
) {
|
||||||
|
mediaType = ReceiverSelectorMediaType.Tab;
|
||||||
|
} else {
|
||||||
|
mediaType = ReceiverSelectorMediaType.App;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateKnownApp();
|
||||||
|
|
||||||
|
window.addEventListener("contextmenu", onContextMenu);
|
||||||
|
browser.menus.onClicked.addListener(onMenuClicked);
|
||||||
|
browser.menus.onShown.addListener(onMenuShown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
port?.disconnect();
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
|
||||||
|
window.addEventListener("contextmenu", onContextMenu);
|
||||||
|
browser.menus.onClicked.removeListener(onMenuClicked);
|
||||||
|
browser.menus.onShown.removeListener(onMenuShown);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterUpdate(async () => {
|
||||||
|
await tick();
|
||||||
|
fitWindowHeight();
|
||||||
|
});
|
||||||
|
|
||||||
|
function onMessage(message: Message) {
|
||||||
|
switch (message.subject) {
|
||||||
|
case "popup:init":
|
||||||
|
appId = message.data.appId;
|
||||||
|
pageInfo = message.data.pageInfo;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "popup:close":
|
||||||
|
window.close();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "popup:update": {
|
||||||
|
/**
|
||||||
|
* Filter receiver devices without the required
|
||||||
|
* capabilities.
|
||||||
|
*/
|
||||||
|
receiverDevices = message.data.receiverDevices.filter(device =>
|
||||||
|
hasRequiredCapabilities(
|
||||||
|
device,
|
||||||
|
pageInfo?.sessionRequest?.capabilities
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.data.availableMediaTypes !== undefined &&
|
||||||
|
message.data.defaultMediaType !== undefined
|
||||||
|
) {
|
||||||
|
console.log(message);
|
||||||
|
availableMediaTypes = message.data.availableMediaTypes;
|
||||||
|
|
||||||
|
if (availableMediaTypes & message.data.defaultMediaType) {
|
||||||
|
mediaType = message.data.defaultMediaType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateKnownApp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resize browser window to fit content height. */
|
||||||
|
function fitWindowHeight() {
|
||||||
|
if (browserWindow?.id === undefined) return;
|
||||||
|
browser.windows.update(browserWindow.id, {
|
||||||
|
height:
|
||||||
|
document.body.clientHeight +
|
||||||
|
(window.outerHeight - window.innerHeight)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateKnownApp() {
|
||||||
|
let newKnownApp: Nullable<KnownApp> = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check knownApps for an app with an ID matching the registered
|
||||||
|
* app on the target page.
|
||||||
|
*/
|
||||||
|
if (isAppMediaTypeAvailable && appId) {
|
||||||
|
newKnownApp = knownApps[appId];
|
||||||
|
} else if (pageInfo) {
|
||||||
|
const pageUrl = 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)) {
|
||||||
|
if (!app.matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = new RemoteMatchPattern(app.matches);
|
||||||
|
if (pattern.matches(pageUrl)) {
|
||||||
|
newKnownApp = app;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target page URL is whitelisted.
|
||||||
|
if (pageInfo && opts?.siteWhitelist) {
|
||||||
|
for (const item of opts.siteWhitelist) {
|
||||||
|
const pattern = new RemoteMatchPattern(item.pattern);
|
||||||
|
if (pattern.matches(pageInfo.url)) {
|
||||||
|
isPageWhitelisted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
knownApp = newKnownApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToWhitelist(
|
||||||
|
app: KnownApp,
|
||||||
|
pageInfo: ReceiverSelectorPageInfo
|
||||||
|
) {
|
||||||
|
if (!app.matches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const whitelist = await options.get("siteWhitelist");
|
||||||
|
if (!whitelist.find(item => item.pattern === app.matches)) {
|
||||||
|
whitelist.push({ pattern: app.matches });
|
||||||
|
await options.set("siteWhitelist", whitelist);
|
||||||
|
|
||||||
|
await browser.tabs.reload(pageInfo.tabId);
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContextMenu(ev: MouseEvent) {
|
||||||
|
if (!(ev.target instanceof Element)) return;
|
||||||
|
|
||||||
|
const receiverElement = ev.target.closest(".receiver");
|
||||||
|
if (receiverElement) {
|
||||||
|
browser.menus.overrideContext({
|
||||||
|
showDefaults: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 receiverDevices[receiverElementIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle show events for receiver context menus. */
|
||||||
|
function onMenuShown(info: browser.menus._OnShownInfo) {
|
||||||
|
if (!info.targetElementId) return;
|
||||||
|
const target = browser.menus.getTargetElement(info.targetElementId);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const device = 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:
|
||||||
|
// Not already connecting to a receiver
|
||||||
|
!isConnecting &&
|
||||||
|
// Selected media type available
|
||||||
|
isMediaTypeAvailable
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.menus.update(menuIdPopupStop, {
|
||||||
|
visible: isAppRunning,
|
||||||
|
title: isAppRunning
|
||||||
|
? _("popupStopMenuTitle", [
|
||||||
|
app.displayName,
|
||||||
|
device.friendlyName
|
||||||
|
])
|
||||||
|
: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.menus.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle click events for receiver context menus. */
|
||||||
|
function 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 = getDeviceFromElement(target);
|
||||||
|
if (!device) return;
|
||||||
|
|
||||||
|
switch (info.menuItemId) {
|
||||||
|
case menuIdPopupCast:
|
||||||
|
onReceiverCast(device);
|
||||||
|
break;
|
||||||
|
case menuIdPopupStop:
|
||||||
|
onReceiverStop(device);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReceiverCast(receiverDevice: ReceiverDevice) {
|
||||||
|
console.log(receiverDevice);
|
||||||
|
isConnecting = true;
|
||||||
|
connectingId = receiverDevice.id;
|
||||||
|
|
||||||
|
console.log(port);
|
||||||
|
|
||||||
|
port?.postMessage({
|
||||||
|
subject: "receiverSelector:selected",
|
||||||
|
data: {
|
||||||
|
receiverDevice,
|
||||||
|
actionType: ReceiverSelectionActionType.Cast,
|
||||||
|
mediaType
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReceiverStop(receiverDevice: ReceiverDevice) {
|
||||||
|
port?.postMessage({
|
||||||
|
subject: "receiverSelector:stop",
|
||||||
|
data: {
|
||||||
|
receiverDevice,
|
||||||
|
actionType: ReceiverSelectionActionType.Stop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="whitelist-banner" hidden={!shouldSuggestWhitelist}>
|
||||||
|
<img src="photon_info.svg" alt="icon, info" />
|
||||||
|
{_("popupWhitelistNotWhitelisted", knownApp?.name)}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
if (!knownApp || !pageInfo) return;
|
||||||
|
addToWhitelist(knownApp, pageInfo);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{_("popupWhitelistAddToWhitelist")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="media-type-select">
|
||||||
|
<div class="media-type-select__label-cast">
|
||||||
|
{_("popupMediaSelectCastLabel")}
|
||||||
|
</div>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select
|
||||||
|
class="media-type-select__dropdown"
|
||||||
|
bind:value={mediaType}
|
||||||
|
disabled={availableMediaTypes === ReceiverSelectorMediaType.None}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value={ReceiverSelectorMediaType.App}
|
||||||
|
disabled={!isAppMediaTypeAvailable}
|
||||||
|
>
|
||||||
|
{knownApp?.name ?? _("popupMediaTypeApp")}
|
||||||
|
</option>
|
||||||
|
|
||||||
|
{#if opts?.mirroringEnabled}
|
||||||
|
<option
|
||||||
|
value={ReceiverSelectorMediaType.Tab}
|
||||||
|
disabled={!(
|
||||||
|
availableMediaTypes & ReceiverSelectorMediaType.Tab
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{_("popupMediaTypeTab")}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value={ReceiverSelectorMediaType.Screen}
|
||||||
|
disabled={!(
|
||||||
|
availableMediaTypes & ReceiverSelectorMediaType.Screen
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{_("popupMediaTypeScreen")}
|
||||||
|
</option>
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="media-type-select__label-to">
|
||||||
|
{_("popupMediaSelectToLabel")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="receivers">
|
||||||
|
{#if !receiverDevices.length}
|
||||||
|
<div class="receivers__not-found">
|
||||||
|
{_("popupNoReceiversFound")}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each receiverDevices as device}
|
||||||
|
{@const application = device.status?.applications?.[0]}
|
||||||
|
{@const isDeviceConnecting =
|
||||||
|
isConnecting && connectingId === device.id}
|
||||||
|
|
||||||
|
<li class="receiver">
|
||||||
|
<div class="receiver__name">
|
||||||
|
{device.friendlyName}
|
||||||
|
</div>
|
||||||
|
<div class="receiver__address">
|
||||||
|
{application && !application.isIdleScreen
|
||||||
|
? application.statusText
|
||||||
|
: `${device.host}:${device.port}`}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="button receiver__connect"
|
||||||
|
on:click={() => onReceiverCast(device)}
|
||||||
|
disabled={isConnecting ||
|
||||||
|
isDeviceConnecting ||
|
||||||
|
!isMediaTypeAvailable}
|
||||||
|
>
|
||||||
|
{#if isDeviceConnecting}
|
||||||
|
{_("popupCastingButtonTitle", "")}<LoadingIndicator />
|
||||||
|
{:else}
|
||||||
|
{_("popupCastButtonTitle")}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
16
ext/src/ui/popup/index.ts
Normal file
16
ext/src/ui/popup/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Popup from "./Popup.svelte";
|
||||||
|
|
||||||
|
// macOS styles
|
||||||
|
browser.runtime.getPlatformInfo().then(platformInfo => {
|
||||||
|
if (platformInfo.os === "mac") {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = "styles/mac.css";
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const target = document.getElementById("root");
|
||||||
|
if (target) {
|
||||||
|
new Popup({ target });
|
||||||
|
}
|
||||||
@@ -1,639 +0,0 @@
|
|||||||
/* eslint-disable max-len */
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import React, { Component } from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
|
|
||||||
import { menuIdPopupCast, menuIdPopupStop } from "../../background/menus";
|
|
||||||
import type { ReceiverSelectorPageInfo } from "../../background/ReceiverSelector";
|
|
||||||
import type { WhitelistItemData } from "../../background/whitelist";
|
|
||||||
|
|
||||||
import knownApps, { KnownApp } from "../../cast/knownApps";
|
|
||||||
import options from "../../lib/options";
|
|
||||||
|
|
||||||
import messaging, { Message, Port } from "../../messaging";
|
|
||||||
import { getNextEllipsis } from "../utils";
|
|
||||||
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ReceiverDevice,
|
|
||||||
ReceiverDeviceCapabilities,
|
|
||||||
ReceiverSelectionActionType,
|
|
||||||
ReceiverSelectorMediaType
|
|
||||||
} from "../../types";
|
|
||||||
|
|
||||||
import { Capability } from "../../cast/sdk/enums";
|
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
|
||||||
|
|
||||||
// macOS styles
|
|
||||||
browser.runtime.getPlatformInfo().then(platformInfo => {
|
|
||||||
if (platformInfo.os === "mac") {
|
|
||||||
const link = document.createElement("link");
|
|
||||||
link.rel = "stylesheet";
|
|
||||||
link.href = "styles/mac.css";
|
|
||||||
document.head.appendChild(link);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check receiver device capabilities bitflags against array of
|
|
||||||
* capability strings requested by the sender application.
|
|
||||||
*/
|
|
||||||
function hasRequiredCapabilities(
|
|
||||||
receiverDevice: ReceiverDevice,
|
|
||||||
capabilities: Capability[] = []
|
|
||||||
) {
|
|
||||||
const { capabilities: deviceCapabilities } = receiverDevice;
|
|
||||||
return capabilities.every(capability => {
|
|
||||||
switch (capability) {
|
|
||||||
case Capability.AUDIO_IN:
|
|
||||||
return deviceCapabilities & ReceiverDeviceCapabilities.AUDIO_IN;
|
|
||||||
case Capability.AUDIO_OUT:
|
|
||||||
return (
|
|
||||||
deviceCapabilities & ReceiverDeviceCapabilities.AUDIO_OUT
|
|
||||||
);
|
|
||||||
case Capability.MULTIZONE_GROUP:
|
|
||||||
return (
|
|
||||||
deviceCapabilities &
|
|
||||||
ReceiverDeviceCapabilities.MULTIZONE_GROUP
|
|
||||||
);
|
|
||||||
case Capability.VIDEO_IN:
|
|
||||||
return deviceCapabilities & ReceiverDeviceCapabilities.VIDEO_IN;
|
|
||||||
case Capability.VIDEO_OUT:
|
|
||||||
return (
|
|
||||||
deviceCapabilities & ReceiverDeviceCapabilities.VIDEO_OUT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PopupAppProps {}
|
|
||||||
interface PopupAppState {
|
|
||||||
/** List of devices to show in receiver list. */
|
|
||||||
receiverDevices: ReceiverDevice[];
|
|
||||||
|
|
||||||
/** Currently selected media type. */
|
|
||||||
mediaType: ReceiverSelectorMediaType;
|
|
||||||
/** Media types available to select. */
|
|
||||||
availableMediaTypes: ReceiverSelectorMediaType;
|
|
||||||
|
|
||||||
/** Sender app ID (if available). */
|
|
||||||
appId?: string;
|
|
||||||
/** Page info (if launched from page context). */
|
|
||||||
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;
|
|
||||||
siteWhitelistEnabled: boolean;
|
|
||||||
siteWhitelist: WhitelistItemData[];
|
|
||||||
}
|
|
||||||
|
|
||||||
class PopupApp extends Component<PopupAppProps, PopupAppState> {
|
|
||||||
private port?: Port;
|
|
||||||
private browserWindow?: browser.windows.Window;
|
|
||||||
|
|
||||||
private resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.fitWindowHeight();
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(props: PopupAppProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
receiverDevices: [],
|
|
||||||
mediaType: ReceiverSelectorMediaType.App,
|
|
||||||
availableMediaTypes: ReceiverSelectorMediaType.App,
|
|
||||||
isPageWhitelisted: false,
|
|
||||||
isConnecting: false,
|
|
||||||
mirroringEnabled: false,
|
|
||||||
siteWhitelistEnabled: true,
|
|
||||||
siteWhitelist: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store window ref
|
|
||||||
browser.windows.getCurrent().then(win => {
|
|
||||||
this.browserWindow = win;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onMessage = this.onMessage.bind(this);
|
|
||||||
this.onAddToWhitelist = this.onAddToWhitelist.bind(this);
|
|
||||||
this.onReceiverCast = this.onReceiverCast.bind(this);
|
|
||||||
this.onReceiverStop = this.onReceiverStop.bind(this);
|
|
||||||
|
|
||||||
this.onContextMenu = this.onContextMenu.bind(this);
|
|
||||||
this.onMenuShown = this.onMenuShown.bind(this);
|
|
||||||
this.onMenuClicked = this.onMenuClicked.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMessage(message: Message) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
browser.windows.update(this.browserWindow.id, {
|
|
||||||
height:
|
|
||||||
document.body.clientHeight +
|
|
||||||
(window.outerHeight - window.innerHeight)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateKnownApp() {
|
|
||||||
const isAppMediaTypeAvailable = !!(
|
|
||||||
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
|
||||||
);
|
|
||||||
|
|
||||||
let knownApp: KnownApp | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check knownApps for an app with an ID matching the registered
|
|
||||||
* app on the target page.
|
|
||||||
*/
|
|
||||||
if (isAppMediaTypeAvailable && this.state.appId) {
|
|
||||||
knownApp = knownApps[this.state.appId];
|
|
||||||
} else if (this.state.pageInfo) {
|
|
||||||
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)) {
|
|
||||||
if (!app.matches) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pattern = new RemoteMatchPattern(app.matches);
|
|
||||||
if (pattern.matches(pageUrl)) {
|
|
||||||
knownApp = app;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let isPageWhitelisted = false;
|
|
||||||
|
|
||||||
// Check if target page URL is whitelisted.
|
|
||||||
if (this.state.pageInfo) {
|
|
||||||
for (const item of this.state.siteWhitelist) {
|
|
||||||
const pattern = new RemoteMatchPattern(item.pattern);
|
|
||||||
if (pattern.matches(this.state.pageInfo.url)) {
|
|
||||||
isPageWhitelisted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ knownApp, isPageWhitelisted });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async onAddToWhitelist(
|
|
||||||
app: KnownApp,
|
|
||||||
pageInfo: ReceiverSelectorPageInfo
|
|
||||||
) {
|
|
||||||
if (!app.matches) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const whitelist = await options.get("siteWhitelist");
|
|
||||||
if (!whitelist.find(item => item.pattern === app.matches)) {
|
|
||||||
whitelist.push({ pattern: app.matches });
|
|
||||||
await options.set("siteWhitelist", 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onReceiverStop(receiverDevice: ReceiverDevice) {
|
|
||||||
this.port?.postMessage({
|
|
||||||
subject: "receiverSelector:stop",
|
|
||||||
data: {
|
|
||||||
receiverDevice,
|
|
||||||
actionType: ReceiverSelectionActionType.Stop
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onContextMenu(ev: MouseEvent) {
|
|
||||||
if (!(ev.target instanceof Element)) return;
|
|
||||||
|
|
||||||
const receiverElement = ev.target.closest(".receiver");
|
|
||||||
if (receiverElement) {
|
|
||||||
browser.menus.overrideContext({
|
|
||||||
showDefaults: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
|
||||||
// Not already connecting to a receiver
|
|
||||||
!this.state.isConnecting &&
|
|
||||||
// Selected media type available
|
|
||||||
!!(this.state.availableMediaTypes & this.state.mediaType)
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
siteWhitelistEnabled: opts.siteWhitelistEnabled,
|
|
||||||
siteWhitelist: opts.siteWhitelist
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 =
|
|
||||||
this.state.mediaType === ReceiverSelectorMediaType.App;
|
|
||||||
const isTabMediaTypeSelected =
|
|
||||||
this.state.mediaType === ReceiverSelectorMediaType.Tab;
|
|
||||||
const isScreenMediaTypeSelected =
|
|
||||||
this.state.mediaType === ReceiverSelectorMediaType.Screen;
|
|
||||||
|
|
||||||
const isAppMediaTypeAvailable = !!(
|
|
||||||
this.state.availableMediaTypes & ReceiverSelectorMediaType.App
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="whitelist-banner"
|
|
||||||
hidden={
|
|
||||||
// If we don't know the app
|
|
||||||
!this.state.knownApp ||
|
|
||||||
// If the whitelist is disabled
|
|
||||||
!this.state.siteWhitelistEnabled ||
|
|
||||||
// If the whitelist is enabled, and the page is whitelisted
|
|
||||||
(this.state.siteWhitelistEnabled &&
|
|
||||||
this.state.isPageWhitelisted) ||
|
|
||||||
// If an app is already loaded on the page
|
|
||||||
!!(
|
|
||||||
this.state.availableMediaTypes &
|
|
||||||
ReceiverSelectorMediaType.App
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img src="photon_info.svg" />
|
|
||||||
{_(
|
|
||||||
"popupWhitelistNotWhitelisted",
|
|
||||||
this.state.knownApp?.name
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!this.state.knownApp || !this.state.pageInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onAddToWhitelist(
|
|
||||||
this.state.knownApp,
|
|
||||||
this.state.pageInfo
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{_("popupWhitelistAddToWhitelist")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="media-type-select">
|
|
||||||
<div className="media-type-select__label-cast">
|
|
||||||
{_("popupMediaSelectCastLabel")}
|
|
||||||
</div>
|
|
||||||
<div className="select-wrapper">
|
|
||||||
<select
|
|
||||||
onChange={ev =>
|
|
||||||
this.setState({
|
|
||||||
mediaType: parseInt(ev.target.value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="media-type-select__dropdown"
|
|
||||||
disabled={
|
|
||||||
this.state.availableMediaTypes ===
|
|
||||||
ReceiverSelectorMediaType.None
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
value={ReceiverSelectorMediaType.App}
|
|
||||||
selected={isAppMediaTypeSelected}
|
|
||||||
disabled={!isAppMediaTypeAvailable}
|
|
||||||
>
|
|
||||||
{this.state.knownApp?.name ??
|
|
||||||
_("popupMediaTypeApp")}
|
|
||||||
</option>
|
|
||||||
|
|
||||||
{this.state.mirroringEnabled && (
|
|
||||||
<>
|
|
||||||
<option
|
|
||||||
value={ReceiverSelectorMediaType.Tab}
|
|
||||||
selected={isTabMediaTypeSelected}
|
|
||||||
disabled={
|
|
||||||
!(
|
|
||||||
this.state.availableMediaTypes &
|
|
||||||
ReceiverSelectorMediaType.Tab
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{_("popupMediaTypeTab")}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value={ReceiverSelectorMediaType.Screen}
|
|
||||||
selected={isScreenMediaTypeSelected}
|
|
||||||
disabled={
|
|
||||||
!(
|
|
||||||
this.state.availableMediaTypes &
|
|
||||||
ReceiverSelectorMediaType.Screen
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{_("popupMediaTypeScreen")}
|
|
||||||
</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="media-type-select__label-to">
|
|
||||||
{_("popupMediaSelectToLabel")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="receivers">
|
|
||||||
{!this.state.receiverDevices.length ? (
|
|
||||||
<div className="receivers__not-found">
|
|
||||||
{_("popupNoReceiversFound")}
|
|
||||||
</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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReceiverProps {
|
|
||||||
details: ReceiverDevice;
|
|
||||||
isMediaTypeAvailable: boolean;
|
|
||||||
isAnyConnecting: boolean;
|
|
||||||
|
|
||||||
// Events
|
|
||||||
onCast(receiverDevice: ReceiverDevice): void;
|
|
||||||
onStop(receiverDevice: ReceiverDevice): void;
|
|
||||||
}
|
|
||||||
interface ReceiverState {
|
|
||||||
isConnecting: boolean;
|
|
||||||
connectingEllipsis: string;
|
|
||||||
}
|
|
||||||
class Receiver extends Component<ReceiverProps, ReceiverState> {
|
|
||||||
private ellipsisInterval?: number;
|
|
||||||
|
|
||||||
constructor(props: ReceiverProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isConnecting: false,
|
|
||||||
connectingEllipsis: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleCast = this.handleCast.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCast() {
|
|
||||||
if (!this.props.details.status) {
|
|
||||||
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 (
|
|
||||||
<li className="receiver">
|
|
||||||
<div className="receiver__name">
|
|
||||||
{this.props.details.friendlyName}
|
|
||||||
</div>
|
|
||||||
<div className="receiver__address">
|
|
||||||
{application && !application.isIdleScreen
|
|
||||||
? application.statusText
|
|
||||||
: `${this.props.details.host}:${this.props.details.port}`}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="button receiver__connect"
|
|
||||||
onClick={this.handleCast}
|
|
||||||
disabled={
|
|
||||||
this.props.isAnyConnecting ||
|
|
||||||
this.state.isConnecting ||
|
|
||||||
!this.props.isMediaTypeAvailable
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{this.state.isConnecting
|
|
||||||
? _(
|
|
||||||
"popupCastingButtonTitle",
|
|
||||||
this.state.isConnecting
|
|
||||||
? this.state.connectingEllipsis
|
|
||||||
: ""
|
|
||||||
)
|
|
||||||
: _("popupCastButtonTitle")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render after CSS has loaded
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
ReactDOM.render(<PopupApp />, document.querySelector("#root"));
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig",
|
"extends": "../tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react",
|
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user