mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Use extension popup instead of content script for mirroring sender
This commit is contained in:
@@ -75,6 +75,7 @@ const buildOpts = {
|
|||||||
path.join(srcPath, "/cast/senders/mirroring.ts"),
|
path.join(srcPath, "/cast/senders/mirroring.ts"),
|
||||||
// UI
|
// UI
|
||||||
path.join(srcPath, "ui/popup/index.ts"),
|
path.join(srcPath, "ui/popup/index.ts"),
|
||||||
|
path.join(srcPath, "ui/mirroring/index.ts"),
|
||||||
path.join(srcPath, "ui/options/index.ts")
|
path.join(srcPath, "ui/options/index.ts")
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
|
|||||||
@@ -559,5 +559,40 @@
|
|||||||
"optionsShowAdvancedOptions": {
|
"optionsShowAdvancedOptions": {
|
||||||
"message": "Show advanced options",
|
"message": "Show advanced options",
|
||||||
"description": "Show advanced options checkbox label."
|
"description": "Show advanced options checkbox label."
|
||||||
|
},
|
||||||
|
|
||||||
|
"mirroringPopupTitle": {
|
||||||
|
"message": "Mirroring",
|
||||||
|
"description": "Mirroring popup window title."
|
||||||
|
},
|
||||||
|
"mirroringPopupWaitingForConnection": {
|
||||||
|
"message": "Waiting for connection",
|
||||||
|
"description": "Mirroring popup loading text."
|
||||||
|
},
|
||||||
|
"mirroringPopupConnectedTo": {
|
||||||
|
"message": "Connected to $deviceName$",
|
||||||
|
"description": "Mirroring popup label displayed whilst session connected before mirroring.",
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mirroringPopupMirroringTo": {
|
||||||
|
"message": "Mirroring to $deviceName$",
|
||||||
|
"description": "Mirroring popup label displayed whilst mirroring.",
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mirroringPopupChooseSource": {
|
||||||
|
"message": "Choose Source",
|
||||||
|
"description": "Mirroring popup choose media source button label."
|
||||||
|
},
|
||||||
|
"mirroringPopupStopMirroring": {
|
||||||
|
"message": "Stop Mirroring",
|
||||||
|
"description": "Mirroring popup stop mirroring button label."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
import logger from "../lib/logger";
|
import logger from "../lib/logger";
|
||||||
import messaging, { Message, Port } from "../messaging";
|
import messaging, { Message, Port } from "../messaging";
|
||||||
import options from "../lib/options";
|
import options from "../lib/options";
|
||||||
import { getMediaTypesForPageUrl, stringify } from "../lib/utils";
|
|
||||||
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ReceiverDevice,
|
||||||
ReceiverSelectorAppInfo,
|
ReceiverSelectorAppInfo,
|
||||||
ReceiverSelectorMediaType,
|
ReceiverSelectorMediaType,
|
||||||
ReceiverSelectorPageInfo
|
ReceiverSelectorPageInfo
|
||||||
@@ -515,19 +515,7 @@ const castManager = new (class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case ReceiverSelectorMediaType.Screen:
|
case ReceiverSelectorMediaType.Screen:
|
||||||
await browser.tabs.executeScript(contentContext.tabId, {
|
await createMirroringPopup(selection.device);
|
||||||
code: stringify`
|
|
||||||
window.receiverDevice = ${selection.device};
|
|
||||||
window.contextTabId = ${contentContext.tabId};
|
|
||||||
`,
|
|
||||||
frameId: contentContext.frameId
|
|
||||||
});
|
|
||||||
|
|
||||||
await browser.tabs.executeScript(contentContext.tabId, {
|
|
||||||
file: "cast/senders/mirroring.js",
|
|
||||||
frameId: contentContext.frameId
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,7 +548,7 @@ async function getReceiverSelection(selectionOpts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let defaultMediaType = ReceiverSelectorMediaType.Screen;
|
let defaultMediaType = ReceiverSelectorMediaType.Screen;
|
||||||
let availableMediaTypes = ReceiverSelectorMediaType.None;
|
let availableMediaTypes = ReceiverSelectorMediaType.Screen;
|
||||||
|
|
||||||
// Default frame ID
|
// Default frame ID
|
||||||
if (selectionOpts.frameId === undefined) selectionOpts.frameId = 0;
|
if (selectionOpts.frameId === undefined) selectionOpts.frameId = 0;
|
||||||
@@ -616,12 +604,8 @@ async function getReceiverSelection(selectionOpts: {
|
|||||||
})
|
})
|
||||||
).url
|
).url
|
||||||
};
|
};
|
||||||
|
} catch (err) {
|
||||||
availableMediaTypes = getMediaTypesForPageUrl(pageInfo.url);
|
logger.error("Failed to locate frame!", err);
|
||||||
} catch {
|
|
||||||
logger.error(
|
|
||||||
"Failed to locate frame, falling back to default available media types."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,4 +775,38 @@ function createSelector() {
|
|||||||
return selector;
|
return selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createMirroringPopup(device: ReceiverDevice) {
|
||||||
|
let popup: browser.windows.Window;
|
||||||
|
try {
|
||||||
|
popup = await browser.windows.create({
|
||||||
|
url: browser.runtime.getURL("ui/mirroring/index.html"),
|
||||||
|
type: "popup",
|
||||||
|
width: 400,
|
||||||
|
height: 150
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to create mirroring popup!", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMirroringPopupMessage = (port: Port) => {
|
||||||
|
if (
|
||||||
|
port.sender?.tab?.windowId !== popup.id ||
|
||||||
|
port.name !== "mirroring"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
port.postMessage({ subject: "mirroringPopup:init", data: { device } });
|
||||||
|
};
|
||||||
|
|
||||||
|
messaging.onConnect.addListener(onMirroringPopupMessage);
|
||||||
|
|
||||||
|
browser.windows.onRemoved.addListener(function onWindowRemoved(windowId) {
|
||||||
|
if (windowId !== popup.id) return;
|
||||||
|
messaging.onConnect.removeListener(onMirroringPopupMessage);
|
||||||
|
browser.windows.onRemoved.removeListener(onWindowRemoved);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default castManager;
|
export default castManager;
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import messaging, { Message } from "../messaging";
|
|||||||
import type { ReceiverDevice } from "../types";
|
import type { ReceiverDevice } from "../types";
|
||||||
|
|
||||||
import pageMessenging from "./pageMessenging";
|
import pageMessenging from "./pageMessenging";
|
||||||
|
|
||||||
|
// Ensure extension-side is initialized first
|
||||||
|
void pageMessenging.extension;
|
||||||
|
|
||||||
import CastSDK from "./sdk";
|
import CastSDK from "./sdk";
|
||||||
|
|
||||||
export type CastPort = TypedMessagePort<Message>;
|
export type CastPort = TypedMessagePort<Message>;
|
||||||
@@ -41,7 +45,10 @@ export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> {
|
|||||||
* will be the internal extension URL, whereas in a content
|
* will be the internal extension URL, whereas in a content
|
||||||
* script, it will be the content page URL.
|
* script, it will be the content page URL.
|
||||||
*/
|
*/
|
||||||
if (window.location.protocol === "moz-extension:") {
|
if (
|
||||||
|
window.location.protocol === "moz-extension:" &&
|
||||||
|
window.location.pathname === "_generated_background_page.html"
|
||||||
|
) {
|
||||||
const { default: castManager } = await import(
|
const { default: castManager } = await import(
|
||||||
"../background/castManager"
|
"../background/castManager"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,13 +19,17 @@ type MirroringAppMessage =
|
|||||||
| { subject: "close" };
|
| { subject: "close" };
|
||||||
|
|
||||||
interface MirroringSenderOpts {
|
interface MirroringSenderOpts {
|
||||||
contextTabId?: number;
|
receiverDevice: ReceiverDevice;
|
||||||
receiverDevice?: ReceiverDevice;
|
onSessionCreated: () => void;
|
||||||
|
onMirroringConnected: () => void;
|
||||||
|
onMirroringStopped: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MirroringSender {
|
export default class MirroringSender {
|
||||||
private contextTabId?: number;
|
private receiverDevice: ReceiverDevice;
|
||||||
private receiverDevice?: ReceiverDevice;
|
private sessionCreatedCallback: () => void;
|
||||||
|
private mirroringConnectedCallback: () => void;
|
||||||
|
private mirroringStoppedCallback: () => void;
|
||||||
|
|
||||||
private session?: Session;
|
private session?: Session;
|
||||||
private wasSessionRequested = false;
|
private wasSessionRequested = false;
|
||||||
@@ -40,18 +44,17 @@ class MirroringSender {
|
|||||||
private streamMaxResolution: { width?: number; height?: number } = {};
|
private streamMaxResolution: { width?: number; height?: number } = {};
|
||||||
|
|
||||||
constructor(opts: MirroringSenderOpts) {
|
constructor(opts: MirroringSenderOpts) {
|
||||||
this.contextTabId = opts.contextTabId;
|
|
||||||
this.receiverDevice = opts.receiverDevice;
|
this.receiverDevice = opts.receiverDevice;
|
||||||
|
this.sessionCreatedCallback = opts.onSessionCreated;
|
||||||
|
this.mirroringConnectedCallback = opts.onMirroringConnected;
|
||||||
|
this.mirroringStoppedCallback = opts.onMirroringStopped;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
try {
|
try {
|
||||||
await ensureInit({
|
await ensureInit({ receiverDevice: this.receiverDevice });
|
||||||
contextTabId: this.contextTabId,
|
|
||||||
receiverDevice: this.receiverDevice
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Failed to initialize cast API", err);
|
logger.error("Failed to initialize cast API", err);
|
||||||
}
|
}
|
||||||
@@ -82,11 +85,6 @@ class MirroringSender {
|
|||||||
cast.initialize(apiConfig);
|
cast.initialize(apiConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.peerConnection?.close();
|
|
||||||
this.session?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sessionListener() {
|
private sessionListener() {
|
||||||
// Unused
|
// Unused
|
||||||
}
|
}
|
||||||
@@ -98,7 +96,7 @@ class MirroringSender {
|
|||||||
cast.requestSession(
|
cast.requestSession(
|
||||||
session => {
|
session => {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.createMirroringConnection();
|
this.sessionCreatedCallback();
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
logger.error("Session request failed", err);
|
logger.error("Session request failed", err);
|
||||||
@@ -112,7 +110,14 @@ class MirroringSender {
|
|||||||
this.session.sendMessage(NS_FX_CAST, message);
|
this.session.sendMessage(NS_FX_CAST, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createMirroringConnection() {
|
stop() {
|
||||||
|
this.peerConnection?.close();
|
||||||
|
this.session?.stop();
|
||||||
|
|
||||||
|
this.mirroringStoppedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMirroringConnection(stream: MediaStream) {
|
||||||
const pc = new RTCPeerConnection();
|
const pc = new RTCPeerConnection();
|
||||||
this.peerConnection = pc;
|
this.peerConnection = pc;
|
||||||
|
|
||||||
@@ -152,6 +157,7 @@ class MirroringSender {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mirroringConnectedCallback();
|
||||||
applyParameters();
|
applyParameters();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,26 +206,9 @@ class MirroringSender {
|
|||||||
await sender.setParameters(params);
|
await sender.setParameters(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream: MediaStream;
|
const [track] = stream.getVideoTracks();
|
||||||
try {
|
pc.addTrack(track, stream);
|
||||||
// Add screen media stream
|
track.addEventListener("ended", () => this.stop());
|
||||||
|
|
||||||
stream = await navigator.mediaDevices.getDisplayMedia({
|
|
||||||
video: {
|
|
||||||
cursor: "motion",
|
|
||||||
frameRate: this.streamMaxFrameRate
|
|
||||||
},
|
|
||||||
audio: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const [track] = stream.getVideoTracks();
|
|
||||||
pc.addTrack(track, stream);
|
|
||||||
track.addEventListener("ended", () => this.stop());
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to add display media stream!", err);
|
|
||||||
this.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use a video element to get stream resize events and update
|
* Use a video element to get stream resize events and update
|
||||||
@@ -231,19 +220,3 @@ class MirroringSender {
|
|||||||
video.play();
|
video.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If loaded as a content script, opts are stored on the window object.
|
|
||||||
*/
|
|
||||||
if (window.location.protocol !== "moz-extension:") {
|
|
||||||
const window_ = window as any;
|
|
||||||
|
|
||||||
const sender = new MirroringSender({
|
|
||||||
contextTabId: window_.contextTabId,
|
|
||||||
receiverDevice: window_.receiverDevice
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
sender.stop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,47 +29,6 @@ export function stringify(
|
|||||||
return formattedString;
|
return formattedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMediaTypesForPageUrl(
|
|
||||||
pageUrl: string
|
|
||||||
): ReceiverSelectorMediaType {
|
|
||||||
const url = new URL(pageUrl);
|
|
||||||
let availableMediaTypes = ReceiverSelectorMediaType.None;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content scripts are prohibited from running on some
|
|
||||||
* Mozilla domains.
|
|
||||||
*/
|
|
||||||
const blockedHosts = [
|
|
||||||
"accounts-static.cdn.mozilla.net",
|
|
||||||
"accounts.firefox.com",
|
|
||||||
"addons.cdn.mozilla.net",
|
|
||||||
"addons.mozilla.org",
|
|
||||||
"api.accounts.firefox.com",
|
|
||||||
"content.cdn.mozilla.net",
|
|
||||||
"discovery.addons.mozilla.org",
|
|
||||||
"install.mozilla.org",
|
|
||||||
"oauth.accounts.firefox.com",
|
|
||||||
"profile.accounts.firefox.com",
|
|
||||||
"support.mozilla.org",
|
|
||||||
"sync.services.mozilla.com"
|
|
||||||
];
|
|
||||||
|
|
||||||
if (blockedHosts.includes(url.host)) {
|
|
||||||
return availableMediaTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When on an insecure origin, MediaDevices.getDisplayMedia
|
|
||||||
* will not exist (and legacy MediaDevices.getUserMedia
|
|
||||||
* mediaSource constraint will fail).
|
|
||||||
*/
|
|
||||||
if (url.protocol === "https:") {
|
|
||||||
availableMediaTypes |= ReceiverSelectorMediaType.Screen;
|
|
||||||
}
|
|
||||||
|
|
||||||
return availableMediaTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadScript(
|
export function loadScript(
|
||||||
scriptUrl: string,
|
scriptUrl: string,
|
||||||
doc: Document = document
|
doc: Document = document
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ type ExtMessageDefinitions = {
|
|||||||
* to the bridge.
|
* to the bridge.
|
||||||
*/
|
*/
|
||||||
"main:refreshDeviceManager": void;
|
"main:refreshDeviceManager": void;
|
||||||
|
|
||||||
|
"mirroringPopup:init": { device: ReceiverDevice };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,4 +26,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="ellipsis">{ellipsis}</span>
|
<span class="indicator">
|
||||||
|
<slot />{ellipsis}
|
||||||
|
</span>
|
||||||
|
|||||||
101
ext/src/ui/mirroring/MirroringPopup.svelte
Normal file
101
ext/src/ui/mirroring/MirroringPopup.svelte
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import MirroringSender from "../../cast/senders/mirroring";
|
||||||
|
import logger from "../../lib/logger";
|
||||||
|
|
||||||
|
import options, { Options } from "../../lib/options";
|
||||||
|
import messaging, { Port } from "../../messaging";
|
||||||
|
|
||||||
|
import type { ReceiverDevice } from "../../types";
|
||||||
|
import LoadingIndicator from "../LoadingIndicator.svelte";
|
||||||
|
|
||||||
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
document.title = _("mirroringPopupTitle");
|
||||||
|
|
||||||
|
let port: Optional<Port>;
|
||||||
|
let opts: Optional<Options>;
|
||||||
|
|
||||||
|
let device: ReceiverDevice;
|
||||||
|
|
||||||
|
let mirroringSender: Optional<MirroringSender>;
|
||||||
|
let isSessionCreated = false;
|
||||||
|
let isMirroringConnected = false;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
port = messaging.connect({ name: "mirroring" });
|
||||||
|
port.onMessage.addListener(message => {
|
||||||
|
switch (message.subject) {
|
||||||
|
case "mirroringPopup:init":
|
||||||
|
device = message.data.device;
|
||||||
|
|
||||||
|
mirroringSender = new MirroringSender({
|
||||||
|
receiverDevice: device,
|
||||||
|
onSessionCreated() {
|
||||||
|
isSessionCreated = true;
|
||||||
|
},
|
||||||
|
onMirroringConnected() {
|
||||||
|
isMirroringConnected = true;
|
||||||
|
},
|
||||||
|
onMirroringStopped() {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
opts = await options.getAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
mirroringSender?.stop();
|
||||||
|
port?.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function requestDisplayMedia() {
|
||||||
|
if (!mirroringSender) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
mirroringSender.createMirroringConnection(
|
||||||
|
await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: {
|
||||||
|
cursor: "motion",
|
||||||
|
frameRate: opts?.mirroringStreamMaxFrameRate
|
||||||
|
},
|
||||||
|
// Currently not implemented in Firefox
|
||||||
|
audio: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to create mirroring connection!", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isSessionCreated}
|
||||||
|
<div class="mirroring-status">
|
||||||
|
{#if isMirroringConnected}
|
||||||
|
<p>
|
||||||
|
{_("mirroringPopupMirroringTo", device.friendlyName)}
|
||||||
|
</p>
|
||||||
|
<button on:click={() => window.close()}>
|
||||||
|
{_("mirroringPopupStopMirroring")}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
{_("mirroringPopupConnectedTo", device.friendlyName)}
|
||||||
|
</p>
|
||||||
|
<button on:click={requestDisplayMedia} class="button">
|
||||||
|
{_("mirroringPopupChooseSource")}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
<LoadingIndicator
|
||||||
|
>{_("mirroringPopupWaitingForConnection")}</LoadingIndicator
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
15
ext/src/ui/mirroring/index.html
Normal file
15
ext/src/ui/mirroring/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../photon-colors.css" />
|
||||||
|
<link rel="stylesheet" href="../photon-widgets.css" />
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
|
||||||
|
<script src="index.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
ext/src/ui/mirroring/index.ts
Normal file
9
ext/src/ui/mirroring/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import MirroringPopup from "./MirroringPopup.svelte";
|
||||||
|
|
||||||
|
const target = document.getElementById("root");
|
||||||
|
if (target) {
|
||||||
|
const mirroringPopup = new MirroringPopup({ target });
|
||||||
|
window.addEventListener("beforeunload", () => {
|
||||||
|
mirroringPopup.$destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
30
ext/src/ui/mirroring/style.css
Normal file
30
ext/src/ui/mirroring/style.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
body,
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--box-background);
|
||||||
|
color: var(--box-color);
|
||||||
|
font: message-box;
|
||||||
|
font-size: 15px;
|
||||||
|
margin: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirroring-status {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.mirroring-status > p {
|
||||||
|
margin: initial;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user