mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Rewrite options page with Svelte
This commit is contained in:
@@ -6,6 +6,8 @@ const path = require("path");
|
|||||||
|
|
||||||
const esbuild = require("esbuild");
|
const esbuild = require("esbuild");
|
||||||
const minimist = require("minimist");
|
const minimist = require("minimist");
|
||||||
|
const sveltePlugin = require("esbuild-svelte");
|
||||||
|
const sveltePreprocess = require("svelte-preprocess");
|
||||||
const webExt = require("web-ext");
|
const webExt = require("web-ext");
|
||||||
|
|
||||||
const { copyFilesPlugin } = require("./lib/copyFilesPlugin.js");
|
const { copyFilesPlugin } = require("./lib/copyFilesPlugin.js");
|
||||||
@@ -86,7 +88,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.tsx"),
|
path.join(srcPath, "ui/popup/index.tsx"),
|
||||||
path.join(srcPath, "ui/options/index.tsx")
|
path.join(srcPath, "ui/options/index.ts")
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
BRIDGE_NAME: `"${BRIDGE_NAME}"`,
|
BRIDGE_NAME: `"${BRIDGE_NAME}"`,
|
||||||
@@ -95,12 +97,17 @@ const buildOpts = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
preactCompatPlugin,
|
preactCompatPlugin,
|
||||||
|
// @ts-ignore
|
||||||
|
sveltePlugin({
|
||||||
|
// @ts-ignore
|
||||||
|
preprocess: sveltePreprocess()
|
||||||
|
}),
|
||||||
|
|
||||||
// Copy static files
|
// Copy static files
|
||||||
copyFilesPlugin({
|
copyFilesPlugin({
|
||||||
src: srcPath,
|
src: srcPath,
|
||||||
dest: outPath,
|
dest: outPath,
|
||||||
excludePattern: /^(manifest\.json|.*\.(ts|tsx|js|jsx))$/
|
excludePattern: /^(manifest\.json|.*\.(ts|tsx|js|jsx|svelte))$/
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -117,7 +124,7 @@ if (argv.mode === "production") {
|
|||||||
* @param {esbuild.BuildResult} result
|
* @param {esbuild.BuildResult} result
|
||||||
*/
|
*/
|
||||||
function onBuildResult(result) {
|
function onBuildResult(result) {
|
||||||
if (result.errors.length) {
|
if (result?.errors.length) {
|
||||||
console.error("Build error!");
|
console.error("Build error!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,7 +339,7 @@
|
|||||||
"description": "Add new whitelist item button title."
|
"description": "Add new whitelist item button title."
|
||||||
},
|
},
|
||||||
"optionsSiteWhitelistUserAgent": {
|
"optionsSiteWhitelistUserAgent": {
|
||||||
"message": "Enable UA",
|
"message": "Disable UA",
|
||||||
"description": "Whitelist item user agent checkbox title."
|
"description": "Whitelist item user agent checkbox title."
|
||||||
},
|
},
|
||||||
"optionsSiteWhitelistEditItem": {
|
"optionsSiteWhitelistEditItem": {
|
||||||
@@ -350,15 +350,9 @@
|
|||||||
"message": "Remove",
|
"message": "Remove",
|
||||||
"description": "Remove whitelist item button title. Displayed on each item."
|
"description": "Remove whitelist item button title. Displayed on each item."
|
||||||
},
|
},
|
||||||
"optionsSiteWhitelistInvalidMatchPattern": {
|
"optionsSiteWhitelistInvalidDuplicatePattern": {
|
||||||
"message": "Invalid match pattern $matchPattern$",
|
"message": "Match pattern already exists!",
|
||||||
"description": "Error displayed by input indicating an invalid match pattern.",
|
"description": "Error displayed by input indicating a duplicate match pattern."
|
||||||
"placeholders": {
|
|
||||||
"matchPattern": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "http://example"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"optionsMirroringCategoryName": {
|
"optionsMirroringCategoryName": {
|
||||||
|
|||||||
1
ext/src/global.d.ts
vendored
1
ext/src/global.d.ts
vendored
@@ -5,6 +5,7 @@ declare const BRIDGE_NAME: string;
|
|||||||
declare const MIRRORING_APP_ID: string;
|
declare const MIRRORING_APP_ID: string;
|
||||||
|
|
||||||
declare type Nullable<T> = T | null;
|
declare type Nullable<T> = T | null;
|
||||||
|
declare type Optional<T> = T | undefined;
|
||||||
|
|
||||||
declare type DistributiveOmit<T, K extends keyof any> = T extends any
|
declare type DistributiveOmit<T, K extends keyof any> = T extends any
|
||||||
? Omit<T, K>
|
? Omit<T, K>
|
||||||
|
|||||||
295
ext/src/ui/options/Bridge.svelte
Normal file
295
ext/src/ui/options/Bridge.svelte
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import semver from "semver";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge";
|
||||||
|
import logger from "../../lib/logger";
|
||||||
|
|
||||||
|
import { Options } from "../../lib/options";
|
||||||
|
import { getNextEllipsis } from "../utils";
|
||||||
|
|
||||||
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
export let opts: Options;
|
||||||
|
|
||||||
|
let bridgeInfo: Nullable<BridgeInfo> = null;
|
||||||
|
let isLoadingInfo = true;
|
||||||
|
let isLoadingInfoTimedOut = false;
|
||||||
|
|
||||||
|
// Status
|
||||||
|
let infoClass = "bridge__info";
|
||||||
|
let statusIcon: string;
|
||||||
|
let statusTitle: string;
|
||||||
|
let statusText: Nullable<string> = null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
bridgeInfo = await bridge.getInfo();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to fetch bridge/platform info.");
|
||||||
|
if (err instanceof BridgeTimedOutError) {
|
||||||
|
isLoadingInfoTimedOut = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingInfo = false;
|
||||||
|
|
||||||
|
infoClass += ` ${
|
||||||
|
!bridgeInfo
|
||||||
|
? isLoadingInfoTimedOut
|
||||||
|
? "bridge__info--timed-out"
|
||||||
|
: "bridge__info--not-found"
|
||||||
|
: "bridge__info--found"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
if (!bridgeInfo) {
|
||||||
|
statusIcon = "assets/icons8-cancel-120.png";
|
||||||
|
statusTitle = _("optionsBridgeNotFoundStatusTitle");
|
||||||
|
statusText = _("optionsBridgeNotFoundStatusText");
|
||||||
|
} else if (isLoadingInfoTimedOut) {
|
||||||
|
statusIcon = "assets/icons8-warn-120.png";
|
||||||
|
statusTitle = _("optionsBridgeIssueStatusTitle");
|
||||||
|
} else {
|
||||||
|
if (bridgeInfo.isVersionCompatible) {
|
||||||
|
statusIcon = "assets/icons8-ok-120.png";
|
||||||
|
statusTitle = _("optionsBridgeFoundStatusTitle");
|
||||||
|
} else {
|
||||||
|
statusIcon = "assets/icons8-warn-120.png";
|
||||||
|
statusTitle = _("optionsBridgeIssueStatusTitle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Updates
|
||||||
|
let updateData: Nullable<GitHubRelease> = null;
|
||||||
|
let updateStatus: Nullable<string> = null;
|
||||||
|
let updateStatusTimeout: number;
|
||||||
|
|
||||||
|
let isCheckingUpdate = false;
|
||||||
|
let isUpdateAvailable = false;
|
||||||
|
|
||||||
|
let checkUpdateEllipsis = "";
|
||||||
|
|
||||||
|
interface GitHubRelease {
|
||||||
|
url: string;
|
||||||
|
tag_name: string;
|
||||||
|
html_url: string;
|
||||||
|
assets: Array<{
|
||||||
|
content_type: string;
|
||||||
|
html_url: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkUpdate() {
|
||||||
|
isCheckingUpdate = true;
|
||||||
|
|
||||||
|
const checkUpdateTimeout = window.setInterval(() => {
|
||||||
|
checkUpdateEllipsis = getNextEllipsis(checkUpdateEllipsis);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
let releases: GitHubRelease[];
|
||||||
|
try {
|
||||||
|
releases = await fetch(
|
||||||
|
"https://api.github.com/repos/hensm/fx_cast/releases"
|
||||||
|
).then(res => res.json());
|
||||||
|
} catch (err) {
|
||||||
|
isCheckingUpdate = false;
|
||||||
|
updateStatus = _("optionsBridgeUpdateStatusError");
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(checkUpdateTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure valid response
|
||||||
|
if (!Array.isArray(releases)) {
|
||||||
|
throw logger.error("Check update response is not array.", releases);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First non-extension-only release
|
||||||
|
const latestBridgeRelease = releases.find(release =>
|
||||||
|
release.assets.find(
|
||||||
|
asset => asset.content_type !== "application/x-xpinstall"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!latestBridgeRelease) {
|
||||||
|
throw logger.error(
|
||||||
|
"Check update response does not contain release info."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update available if no bridge found or bridge version lower
|
||||||
|
* than fetched release version.
|
||||||
|
*/
|
||||||
|
isUpdateAvailable =
|
||||||
|
!bridgeInfo ||
|
||||||
|
semver.lt(bridgeInfo.version, latestBridgeRelease.tag_name);
|
||||||
|
|
||||||
|
if (isUpdateAvailable) {
|
||||||
|
updateData = latestBridgeRelease;
|
||||||
|
} else {
|
||||||
|
updateStatus = _("optionsBridgeUpdateStatusNoUpdates");
|
||||||
|
}
|
||||||
|
|
||||||
|
isCheckingUpdate = false;
|
||||||
|
|
||||||
|
if (updateStatusTimeout) {
|
||||||
|
window.clearTimeout(updateStatusTimeout);
|
||||||
|
}
|
||||||
|
updateStatusTimeout = window.setTimeout(() => {
|
||||||
|
updateStatus = null;
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdate() {
|
||||||
|
// Open downloads page
|
||||||
|
if (updateData?.html_url) {
|
||||||
|
browser.tabs.create({ url: updateData.html_url });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backupMessageStart, backupMessageEnd] = _(
|
||||||
|
"optionsBridgeBackupEnabled",
|
||||||
|
"\0"
|
||||||
|
).split("\0");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bridge">
|
||||||
|
{#if isLoadingInfo}
|
||||||
|
<div class="bridge__loading">
|
||||||
|
{_("optionsBridgeLoading")}
|
||||||
|
<progress />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class={infoClass}>
|
||||||
|
<div class="bridge__status">
|
||||||
|
<img
|
||||||
|
class="bridge__status-icon"
|
||||||
|
width="60"
|
||||||
|
height="60"
|
||||||
|
src={statusIcon}
|
||||||
|
alt="icon, bridge status"
|
||||||
|
/>
|
||||||
|
<h2 class="bridge__status-title">{statusTitle}</h2>
|
||||||
|
{#if statusText}
|
||||||
|
<p class="bridge__status-text">{statusText}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if bridgeInfo}
|
||||||
|
<table class="bridge__stats">
|
||||||
|
<tr>
|
||||||
|
<th>{_("optionsBridgeStatsName")}</th>
|
||||||
|
<td>{bridgeInfo.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{_("optionsBridgeStatsVersion")}</th>
|
||||||
|
<td>{bridgeInfo.version}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{_("optionsBridgeStatsExpectedVersion")}</th>
|
||||||
|
<td>{bridgeInfo.expectedVersion}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{_("optionsBridgeStatsCompatibility")}</th>
|
||||||
|
<td>
|
||||||
|
{bridgeInfo.isVersionCompatible
|
||||||
|
? bridgeInfo.isVersionExact
|
||||||
|
? _("optionsBridgeCompatible")
|
||||||
|
: _("optionsBridgeLikelyCompatible")
|
||||||
|
: _("optionsBridgeIncompatible")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{_("optionsBridgeStatsRecommendedAction")}</th>
|
||||||
|
<td>
|
||||||
|
{bridgeInfo.isVersionCompatible
|
||||||
|
? _("optionsBridgeNoAction")
|
||||||
|
: bridgeInfo.isVersionOlder
|
||||||
|
? _("optionsBridgeOlderAction")
|
||||||
|
: bridgeInfo.isVersionNewer
|
||||||
|
? _("optionsBridgeNewerAction")
|
||||||
|
: _("optionsBridgeNoAction")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bridge__options">
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="bridgeBackupEnabled"
|
||||||
|
id="bridgeBackupEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.bridgeBackupEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="bridgeBackupEnabled">
|
||||||
|
{backupMessageStart}
|
||||||
|
<input
|
||||||
|
class="bridge__backup-host"
|
||||||
|
name="bridgeBackupHost"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
bind:value={opts.bridgeBackupHost}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<input
|
||||||
|
class="bridge__backup-port"
|
||||||
|
name="bridgeBackupPort"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
min="1025"
|
||||||
|
max="65535"
|
||||||
|
bind:value={opts.bridgeBackupPort}
|
||||||
|
/>
|
||||||
|
{backupMessageEnd}
|
||||||
|
</label>
|
||||||
|
<div class="option__description">
|
||||||
|
{_("optionsBridgeBackupEnabledDescription")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bridge__update-info">
|
||||||
|
{#if isUpdateAvailable}
|
||||||
|
<div class="bridge__update">
|
||||||
|
<p class="bridge__update-label">
|
||||||
|
{_("optionsBridgeUpdateAvailable")}
|
||||||
|
</p>
|
||||||
|
<div class="bridge__update-options">
|
||||||
|
<button
|
||||||
|
class="bridge__update-start"
|
||||||
|
type="button"
|
||||||
|
on:click={getUpdate}
|
||||||
|
>
|
||||||
|
{_("optionsBridgeUpdate")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="bridge__update-check"
|
||||||
|
type="button"
|
||||||
|
disabled={isCheckingUpdate}
|
||||||
|
on:click={checkUpdate}
|
||||||
|
>
|
||||||
|
{#if isCheckingUpdate}
|
||||||
|
{_("optionsBridgeUpdateChecking", checkUpdateEllipsis)}
|
||||||
|
{:else}
|
||||||
|
{_("optionsBridgeUpdateCheck")}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if updateStatus && !isUpdateAvailable}
|
||||||
|
<div class="bridge--update-status">
|
||||||
|
{updateStatus}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
/* eslint-disable max-len */
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import React, { Component } from "react";
|
|
||||||
import semver from "semver";
|
|
||||||
|
|
||||||
import { Options } from "../../lib/options";
|
|
||||||
|
|
||||||
import { BridgeInfo } from "../../lib/bridge";
|
|
||||||
import { getNextEllipsis } from "../../lib/utils";
|
|
||||||
import logger from "../../lib/logger";
|
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
|
||||||
|
|
||||||
interface Release {
|
|
||||||
url: string;
|
|
||||||
tag_name: string;
|
|
||||||
html_url: string;
|
|
||||||
assets: Array<{
|
|
||||||
content_type: string;
|
|
||||||
html_url: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BridgeStatsProps {
|
|
||||||
info: BridgeInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BridgeStats = (props: BridgeStatsProps) => (
|
|
||||||
<table className="bridge__stats">
|
|
||||||
<tr>
|
|
||||||
<th>{_("optionsBridgeStatsName")}</th>
|
|
||||||
<td>{props.info.name}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{_("optionsBridgeStatsVersion")}</th>
|
|
||||||
<td>{props.info.version}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{_("optionsBridgeStatsExpectedVersion")}</th>
|
|
||||||
<td>{props.info.expectedVersion}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{_("optionsBridgeStatsCompatibility")}</th>
|
|
||||||
<td>
|
|
||||||
{props.info.isVersionCompatible
|
|
||||||
? props.info.isVersionExact
|
|
||||||
? _("optionsBridgeCompatible")
|
|
||||||
: _("optionsBridgeLikelyCompatible")
|
|
||||||
: _("optionsBridgeIncompatible")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{_("optionsBridgeStatsRecommendedAction")}</th>
|
|
||||||
<td>
|
|
||||||
{props.info.isVersionCompatible
|
|
||||||
? _("optionsBridgeNoAction")
|
|
||||||
: props.info.isVersionOlder
|
|
||||||
? _("optionsBridgeOlderAction")
|
|
||||||
: props.info.isVersionNewer
|
|
||||||
? _("optionsBridgeNewerAction")
|
|
||||||
: _("optionsBridgeNoAction")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface BridgeProps {
|
|
||||||
info?: BridgeInfo;
|
|
||||||
loading: boolean;
|
|
||||||
loadingTimedOut: boolean;
|
|
||||||
options?: Options;
|
|
||||||
onChange: (ev: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BridgeState {
|
|
||||||
isCheckingUpdates: boolean;
|
|
||||||
isUpdateAvailable: boolean;
|
|
||||||
wasErrorCheckingUpdates: boolean;
|
|
||||||
checkUpdatesEllipsis: string;
|
|
||||||
updateStatus?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Bridge extends Component<BridgeProps, BridgeState> {
|
|
||||||
private updateData?: Release;
|
|
||||||
private updateStatusTimeout?: number;
|
|
||||||
|
|
||||||
constructor(props: BridgeProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isCheckingUpdates: false,
|
|
||||||
isUpdateAvailable: false,
|
|
||||||
wasErrorCheckingUpdates: false,
|
|
||||||
checkUpdatesEllipsis: "..."
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onCheckUpdates = this.onCheckUpdates.bind(this);
|
|
||||||
this.onCheckUpdatesResponse = this.onCheckUpdatesResponse.bind(this);
|
|
||||||
this.onCheckUpdatesError = this.onCheckUpdatesError.bind(this);
|
|
||||||
this.onUpdate = this.onUpdate.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const [backupMessageStart, backupMessageEnd] = _(
|
|
||||||
"optionsBridgeBackupEnabled",
|
|
||||||
"\0"
|
|
||||||
).split("\0");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bridge">
|
|
||||||
{this.props.loading ? (
|
|
||||||
<div className="bridge__loading">
|
|
||||||
{_("optionsBridgeLoading")}
|
|
||||||
<progress></progress>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
this.renderStatus()
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!this.props.loading && this.props.options && (
|
|
||||||
<div className="bridge__options">
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="bridgeBackupEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.props.options.bridgeBackupEnabled
|
|
||||||
}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{backupMessageStart}
|
|
||||||
<input
|
|
||||||
className="bridge__backup-host"
|
|
||||||
name="bridgeBackupHost"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={this.props.options.bridgeBackupHost}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<input
|
|
||||||
className="bridge__backup-port"
|
|
||||||
name="bridgeBackupPort"
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
min="1025"
|
|
||||||
max="65535"
|
|
||||||
value={this.props.options.bridgeBackupPort}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
/>
|
|
||||||
{backupMessageEnd}
|
|
||||||
</div>
|
|
||||||
<div className="option__description">
|
|
||||||
{_("optionsBridgeBackupEnabledDescription")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!this.props.loading && (
|
|
||||||
<div className="bridge__update-info">
|
|
||||||
{this.state.isUpdateAvailable ? (
|
|
||||||
<div className="bridge__update">
|
|
||||||
<p className="bridge__update-label">
|
|
||||||
{_("optionsBridgeUpdateAvailable")}
|
|
||||||
</p>
|
|
||||||
<div className="bridge__update-options">
|
|
||||||
<button
|
|
||||||
className="bridge__update-start"
|
|
||||||
type="button"
|
|
||||||
onClick={this.onUpdate}
|
|
||||||
>
|
|
||||||
{_("optionsBridgeUpdate")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="bridge__update-check"
|
|
||||||
type="button"
|
|
||||||
disabled={this.state.isCheckingUpdates}
|
|
||||||
onClick={this.onCheckUpdates}
|
|
||||||
>
|
|
||||||
{this.state.isCheckingUpdates
|
|
||||||
? _(
|
|
||||||
"optionsBridgeUpdateChecking",
|
|
||||||
getNextEllipsis(
|
|
||||||
this.state.checkUpdatesEllipsis
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: _("optionsBridgeUpdateCheck")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="bridge--update-status">
|
|
||||||
{this.state.updateStatus &&
|
|
||||||
!this.state.isUpdateAvailable &&
|
|
||||||
this.state.updateStatus}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderStatus() {
|
|
||||||
const infoClasses = `bridge__info ${
|
|
||||||
!this.props.info
|
|
||||||
? this.props.loadingTimedOut
|
|
||||||
? "bridge__info--timed-out"
|
|
||||||
: "bridge__info--not-found"
|
|
||||||
: "bridge__info--found"
|
|
||||||
}`;
|
|
||||||
|
|
||||||
let statusIcon: string;
|
|
||||||
let statusTitle: string;
|
|
||||||
let statusText: string | null = null;
|
|
||||||
|
|
||||||
if (this.props.loadingTimedOut) {
|
|
||||||
statusIcon = "assets/icons8-warn-120.png";
|
|
||||||
statusTitle = _("optionsBridgeIssueStatusTitle");
|
|
||||||
} else if (!this.props.info) {
|
|
||||||
statusIcon = "assets/icons8-cancel-120.png";
|
|
||||||
statusTitle = _("optionsBridgeNotFoundStatusTitle");
|
|
||||||
statusText = _("optionsBridgeNotFoundStatusText");
|
|
||||||
} else {
|
|
||||||
if (this.props.info.isVersionCompatible) {
|
|
||||||
statusIcon = "assets/icons8-ok-120.png";
|
|
||||||
statusTitle = _("optionsBridgeFoundStatusTitle");
|
|
||||||
} else {
|
|
||||||
statusIcon = "assets/icons8-warn-120.png";
|
|
||||||
statusTitle = _("optionsBridgeIssueStatusTitle");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={infoClasses}>
|
|
||||||
<div className="bridge__status">
|
|
||||||
<img
|
|
||||||
className="bridge__status-icon"
|
|
||||||
width="60"
|
|
||||||
height="60"
|
|
||||||
src={statusIcon}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 className="bridge__status-title">{statusTitle}</h2>
|
|
||||||
|
|
||||||
{statusText && (
|
|
||||||
<p className="bridge__status-text">{statusText}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.props.info && <BridgeStats info={this.props.info} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCheckUpdates() {
|
|
||||||
this.setState({
|
|
||||||
isCheckingUpdates: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = window.setInterval(() => {
|
|
||||||
this.setState(state => ({
|
|
||||||
checkUpdatesEllipsis: getNextEllipsis(
|
|
||||||
state.checkUpdatesEllipsis
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
fetch("https://api.github.com/repos/hensm/fx_cast/releases")
|
|
||||||
.then(res => {
|
|
||||||
window.clearTimeout(timeout);
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then(this.onCheckUpdatesResponse)
|
|
||||||
.catch(this.onCheckUpdatesError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async onCheckUpdatesResponse(res: Release[]) {
|
|
||||||
if (!Array.isArray(res)) {
|
|
||||||
throw logger.error("Check update response is not array.", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
let latestBridgeRelease;
|
|
||||||
for (const release of res) {
|
|
||||||
if (
|
|
||||||
release.assets.find(
|
|
||||||
asset => asset.content_type !== "application/x-xpinstall"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
latestBridgeRelease = release;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!latestBridgeRelease) {
|
|
||||||
throw logger.error(
|
|
||||||
"Check update response does not contain release info."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update available if no bridge found or bridge version lower
|
|
||||||
* than fetched release version.
|
|
||||||
*/
|
|
||||||
const isUpdateAvailable =
|
|
||||||
!this.props.info ||
|
|
||||||
semver.lt(this.props.info.version, latestBridgeRelease.tag_name);
|
|
||||||
|
|
||||||
if (isUpdateAvailable) {
|
|
||||||
this.updateData = latestBridgeRelease;
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
updateStatus: _("optionsBridgeUpdateStatusNoUpdates")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isCheckingUpdates: false,
|
|
||||||
isUpdateAvailable
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showUpdateStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCheckUpdatesError() {
|
|
||||||
this.setState({
|
|
||||||
isCheckingUpdates: false,
|
|
||||||
wasErrorCheckingUpdates: true,
|
|
||||||
updateStatus: _("optionsBridgeUpdateStatusError")
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showUpdateStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private showUpdateStatus() {
|
|
||||||
if (this.updateStatusTimeout) {
|
|
||||||
window.clearTimeout(this.updateStatusTimeout);
|
|
||||||
}
|
|
||||||
this.updateStatusTimeout = window.setTimeout(() => {
|
|
||||||
this.setState({
|
|
||||||
updateStatus: undefined
|
|
||||||
});
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async onUpdate() {
|
|
||||||
// Open downloads page
|
|
||||||
if (this.updateData?.html_url) {
|
|
||||||
browser.tabs.create({
|
|
||||||
url: this.updateData.html_url
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
305
ext/src/ui/options/Options.svelte
Normal file
305
ext/src/ui/options/Options.svelte
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { afterUpdate, beforeUpdate, onMount } from "svelte";
|
||||||
|
|
||||||
|
import Bridge from "./Bridge.svelte";
|
||||||
|
import Whitelist from "./Whitelist.svelte";
|
||||||
|
void Bridge, Whitelist;
|
||||||
|
|
||||||
|
import logger from "../../lib/logger";
|
||||||
|
|
||||||
|
import options, { Options } from "../../lib/options";
|
||||||
|
import defaultOptions from "../../defaultOptions";
|
||||||
|
|
||||||
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
let formElement: HTMLFormElement;
|
||||||
|
let isFormValid = true;
|
||||||
|
let showSavedIndicator = false;
|
||||||
|
|
||||||
|
let opts: Options | undefined;
|
||||||
|
onMount(async () => {
|
||||||
|
opts = await options.getAll();
|
||||||
|
options.addEventListener("changed", async () => {
|
||||||
|
opts = await options.getAll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
isFormValid = formElement?.checkValidity();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Saves options and show indicator. */
|
||||||
|
async function onFormSubmit() {
|
||||||
|
formElement.reportValidity();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!opts) return;
|
||||||
|
|
||||||
|
// Remove unnecessary prop
|
||||||
|
for (const item of opts.siteWhitelist) {
|
||||||
|
if (item.isUserAgentDisabled === false) {
|
||||||
|
delete item.isUserAgentDisabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await options.setAll(opts);
|
||||||
|
|
||||||
|
// 1s long saved indicator
|
||||||
|
showSavedIndicator = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
showSavedIndicator = false;
|
||||||
|
}, 1000);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to save options!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFormInput() {
|
||||||
|
isFormValid = formElement.checkValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
opts = JSON.parse(JSON.stringify(defaultOptions));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if opts}
|
||||||
|
<form
|
||||||
|
id="form"
|
||||||
|
bind:this={formElement}
|
||||||
|
on:input={onFormInput}
|
||||||
|
on:submit|preventDefault={onFormSubmit}
|
||||||
|
>
|
||||||
|
<Bridge bind:opts />
|
||||||
|
|
||||||
|
<fieldset class="category">
|
||||||
|
<legend class="category__name">
|
||||||
|
<h2>{_("optionsMediaCategoryName")}</h2>
|
||||||
|
</legend>
|
||||||
|
<p class="category__description">
|
||||||
|
{_("optionsMediaCategoryDescription")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="mediaEnabled"
|
||||||
|
id="mediaEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.mediaEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="mediaEnabled">
|
||||||
|
{_("optionsMediaEnabled")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="mediaSyncElement"
|
||||||
|
id="mediaSyncElement"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.mediaSyncElement}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="mediaSyncElement">
|
||||||
|
{_("optionsMediaSyncElement")}
|
||||||
|
</label>
|
||||||
|
<div class="option__description">
|
||||||
|
{_("optionsMediaSyncElementDescription")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="mediaStopOnUnload"
|
||||||
|
id="mediaStopOnUnload"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.mediaStopOnUnload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="mediaStopOnUnload">
|
||||||
|
{_("optionsMediaStopOnUnload")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="localMediaEnabled"
|
||||||
|
id="localMediaEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.localMediaEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="localMediaEnabled">
|
||||||
|
{_("optionsLocalMediaEnabled")}
|
||||||
|
</label>
|
||||||
|
<div class="option__description">
|
||||||
|
{_("optionsLocalMediaCategoryDescription")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option">
|
||||||
|
<label class="option__label" for="localMediaServerPort">
|
||||||
|
{_("optionsLocalMediaServerPort")}
|
||||||
|
</label>
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="localMediaServerPort"
|
||||||
|
id="localMediaServerPort"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
min="1025"
|
||||||
|
max="65535"
|
||||||
|
bind:value={opts.localMediaServerPort}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="category">
|
||||||
|
<legend class="category__name">
|
||||||
|
<h2>{_("optionsMirroringCategoryName")}</h2>
|
||||||
|
</legend>
|
||||||
|
<p class="category__description">
|
||||||
|
{_("optionsMirroringCategoryDescription")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="mirroringEnabled"
|
||||||
|
id="mirroringEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.mirroringEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="mirroringEnabled">
|
||||||
|
{_("optionsMirroringEnabled")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option">
|
||||||
|
<label class="option__label" for="mirroringAppId">
|
||||||
|
{_("optionsMirroringAppId")}
|
||||||
|
</label>
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="mirroringAppId"
|
||||||
|
id="mirroringAppId"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
bind:value={opts.mirroringAppId}
|
||||||
|
/>
|
||||||
|
<div class="option__description">
|
||||||
|
{_("optionsMirroringAppIdDescription")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="category">
|
||||||
|
<legend class="category__name">
|
||||||
|
<h2>{_("optionsReceiverSelectorCategoryName")}</h2>
|
||||||
|
</legend>
|
||||||
|
<p class="category__description">
|
||||||
|
{_("optionsReceiverSelectorCategoryDescription")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="receiverSelectorWaitForConnection"
|
||||||
|
id="receiverSelectorWaitForConnection"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.receiverSelectorWaitForConnection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
class="option__label"
|
||||||
|
for="receiverSelectorWaitForConnection"
|
||||||
|
>
|
||||||
|
{_("optionsReceiverSelectorWaitForConnection")}
|
||||||
|
</label>
|
||||||
|
<div class="option__description">
|
||||||
|
{_("optionsReceiverSelectorWaitForConnectionDescription")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="receiverSelectorCloseIfFocusLost"
|
||||||
|
id="receiverSelectorCloseIfFocusLost"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.receiverSelectorCloseIfFocusLost}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
class="option__label"
|
||||||
|
for="receiverSelectorCloseIfFocusLost"
|
||||||
|
>
|
||||||
|
{_("optionsReceiverSelectorCloseIfFocusLost")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="category">
|
||||||
|
<legend class="category__name">
|
||||||
|
<h2>{_("optionsSiteWhitelistCategoryName")}</h2>
|
||||||
|
</legend>
|
||||||
|
<p class="category__description">
|
||||||
|
{_("optionsSiteWhitelistCategoryDescription")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
name="siteWhitelistEnabled"
|
||||||
|
id="siteWhitelistEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.siteWhitelistEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="siteWhitelistEnabled">
|
||||||
|
{_("optionsSiteWhitelistEnabled")}
|
||||||
|
<span class="option__recommended">
|
||||||
|
{_("optionsOptionRecommended")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="option__description">
|
||||||
|
{_("optionsSiteWhitelistEnabledDescription")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option">
|
||||||
|
<div class="option__label">
|
||||||
|
{_("optionsSiteWhitelistContent")}
|
||||||
|
</div>
|
||||||
|
<div class="option__control">
|
||||||
|
<Whitelist bind:items={opts.siteWhitelist} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div id="buttons">
|
||||||
|
{#if showSavedIndicator}
|
||||||
|
<div id="status-line">
|
||||||
|
{_("optionsSaved")}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button on:click={resetForm} type="button">
|
||||||
|
{_("optionsReset")}
|
||||||
|
</button>
|
||||||
|
<button type="submit" default disabled={!isFormValid}>
|
||||||
|
{_("optionsSave")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
145
ext/src/ui/options/Whitelist.svelte
Normal file
145
ext/src/ui/options/Whitelist.svelte
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { tick } from "svelte";
|
||||||
|
|
||||||
|
import { WhitelistItemData } from "../../background/whitelist";
|
||||||
|
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/matchPattern";
|
||||||
|
|
||||||
|
void REMOTE_MATCH_PATTERN_REGEX;
|
||||||
|
|
||||||
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
/** Whitelist items to display. */
|
||||||
|
export let items: WhitelistItemData[];
|
||||||
|
|
||||||
|
let isEditing = false;
|
||||||
|
let editingIndex: number;
|
||||||
|
let editingInput: HTMLInputElement;
|
||||||
|
let editingValue: string;
|
||||||
|
|
||||||
|
$: isEditingValid =
|
||||||
|
isEditing && REMOTE_MATCH_PATTERN_REGEX.test(editingValue);
|
||||||
|
|
||||||
|
async function beginEditing(index: number) {
|
||||||
|
if (isEditing) return;
|
||||||
|
|
||||||
|
editingIndex = index;
|
||||||
|
editingValue = items[index].pattern;
|
||||||
|
isEditing = true;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
isEditingValid = editingInput.validity.valid;
|
||||||
|
editingInput.focus();
|
||||||
|
editingInput.select();
|
||||||
|
}
|
||||||
|
function finishEditing() {
|
||||||
|
if (!isEditing || !editingInput.validity.valid) return;
|
||||||
|
|
||||||
|
isEditing = false;
|
||||||
|
items[editingIndex].pattern = editingValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkEditValidity() {
|
||||||
|
editingInput.setCustomValidity(
|
||||||
|
// Has duplicate pattern
|
||||||
|
items.some(
|
||||||
|
(item, index) =>
|
||||||
|
index !== editingIndex && item.pattern === editingValue
|
||||||
|
)
|
||||||
|
? _("optionsSiteWhitelistInvalidDuplicatePattern")
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
isEditingValid = editingInput.checkValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem() {
|
||||||
|
if (isEditing) return;
|
||||||
|
items = [...items, { pattern: "" }];
|
||||||
|
beginEditing(items.length - 1);
|
||||||
|
}
|
||||||
|
function removeItem(index: number) {
|
||||||
|
if (isEditing) {
|
||||||
|
if (index !== editingIndex) return;
|
||||||
|
isEditing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.splice(index, 1);
|
||||||
|
items = items;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="whitelist">
|
||||||
|
<ul class="whitelist__items">
|
||||||
|
{#each items as item, i}
|
||||||
|
{@const isEditingItem = isEditing && editingIndex === i}
|
||||||
|
|
||||||
|
<li
|
||||||
|
class="whitelist__item"
|
||||||
|
class:whitelist__item--selected={isEditingItem}
|
||||||
|
on:dblclick={() => beginEditing(i)}
|
||||||
|
>
|
||||||
|
<div class="whitelist__title">
|
||||||
|
{#if isEditingItem}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="whitelist__input-pattern"
|
||||||
|
pattern={REMOTE_MATCH_PATTERN_REGEX.source}
|
||||||
|
required
|
||||||
|
bind:this={editingInput}
|
||||||
|
bind:value={editingValue}
|
||||||
|
on:input={checkEditValidity}
|
||||||
|
on:blur={finishEditing}
|
||||||
|
on:keypress={ev =>
|
||||||
|
ev.key === "Enter" && finishEditing()}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
{item.pattern}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isEditingItem}
|
||||||
|
<label class="whitelist__user-agent">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={item.isUserAgentDisabled}
|
||||||
|
/>
|
||||||
|
{_("optionsSiteWhitelistUserAgent")}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="whitelist__edit-button ghost"
|
||||||
|
title={_("optionsSiteWhitelistEditItem")}
|
||||||
|
disabled={isEditing && !isEditingValid}
|
||||||
|
on:click={() => beginEditing(i)}
|
||||||
|
>
|
||||||
|
<img src="assets/photon_edit.svg" alt="icon, edit" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="whitelist__remove-button ghost"
|
||||||
|
title={_("optionsSiteWhitelistRemoveItem")}
|
||||||
|
disabled={isEditing && !isEditingItem && !isEditingValid}
|
||||||
|
on:click={() => removeItem(i)}
|
||||||
|
>
|
||||||
|
<img src="assets/photon_delete.svg" alt="icon, remove" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="whitelist__view-actions">
|
||||||
|
<button
|
||||||
|
class="whitelist__add-button ghost"
|
||||||
|
title={_("optionsSiteWhitelistAddItem")}
|
||||||
|
on:click={addItem}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img src="assets/photon_new.svg" alt="icon, add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
/* eslint-disable max-len */
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
import type { WhitelistItemData } from "../../background/whitelist";
|
|
||||||
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/matchPattern";
|
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
|
||||||
|
|
||||||
interface WhitelistProps {
|
|
||||||
items: WhitelistItemData[];
|
|
||||||
onChange: (items: WhitelistItemData[]) => void;
|
|
||||||
}
|
|
||||||
interface WhitelistState {
|
|
||||||
addingNewItem: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Editable list component for site whitelist. */
|
|
||||||
export default class Whitelist extends Component<
|
|
||||||
WhitelistProps,
|
|
||||||
WhitelistState
|
|
||||||
> {
|
|
||||||
state: WhitelistState = {
|
|
||||||
addingNewItem: false
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="whitelist">
|
|
||||||
<ul className="whitelist__items">
|
|
||||||
{this.props.items.map((item, i) => (
|
|
||||||
<WhitelistItem
|
|
||||||
value={item}
|
|
||||||
onEdit={(oldValue, newValue) => {
|
|
||||||
// Replace item
|
|
||||||
this.props.onChange(
|
|
||||||
this.props.items.map(item =>
|
|
||||||
item.pattern === oldValue?.pattern
|
|
||||||
? newValue
|
|
||||||
: item
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onRemove={value => {
|
|
||||||
// Remove item
|
|
||||||
this.props.onChange(
|
|
||||||
this.props.items.filter(
|
|
||||||
item => item.pattern !== value?.pattern
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
key={i}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{this.state.addingNewItem && (
|
|
||||||
<WhitelistItem
|
|
||||||
isEditing={true}
|
|
||||||
onEdit={(__, newValue) => {
|
|
||||||
// Add new item
|
|
||||||
this.setState({ addingNewItem: false }, () => {
|
|
||||||
this.props.onChange([
|
|
||||||
...this.props.items,
|
|
||||||
newValue
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onRemove={() => {
|
|
||||||
// Cancel adding new item
|
|
||||||
this.setState({ addingNewItem: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div className="whitelist__view-actions">
|
|
||||||
<button
|
|
||||||
className="whitelist__add-button ghost"
|
|
||||||
title={_("optionsSiteWhitelistAddItem")}
|
|
||||||
onClick={() => this.setState({ addingNewItem: true })}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<img src="assets/photon_new.svg" alt="icon, add" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WhitelistItemProps {
|
|
||||||
value?: WhitelistItemData;
|
|
||||||
/** Initial editing state */
|
|
||||||
isEditing?: boolean;
|
|
||||||
onEdit: (
|
|
||||||
oldValue: WhitelistItemData | undefined,
|
|
||||||
newValue: WhitelistItemData
|
|
||||||
) => void;
|
|
||||||
onRemove: (value?: WhitelistItemData) => void;
|
|
||||||
}
|
|
||||||
interface WhitelistItemState {
|
|
||||||
isEditing: boolean;
|
|
||||||
editValue?: WhitelistItemData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Editable item component for whitelist. */
|
|
||||||
class WhitelistItem extends Component<WhitelistItemProps, WhitelistItemState> {
|
|
||||||
private inputElement: HTMLInputElement | null = null;
|
|
||||||
|
|
||||||
constructor(props: WhitelistItemProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = { isEditing: props.isEditing ?? false };
|
|
||||||
|
|
||||||
this.beginEditing = this.beginEditing.bind(this);
|
|
||||||
this.finishEditing = this.finishEditing.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets editing state and focuses input field. */
|
|
||||||
private beginEditing() {
|
|
||||||
if (this.state.isEditing) return;
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
isEditing: true,
|
|
||||||
editValue: this.props.value
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.inputElement?.focus();
|
|
||||||
this.inputElement?.select();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Checks input validity and sends edit update. */
|
|
||||||
private finishEditing() {
|
|
||||||
if (!this.state.isEditing || !this.state.editValue) return;
|
|
||||||
|
|
||||||
if (this.inputElement?.validity.valid) {
|
|
||||||
this.props.onEdit(this.props.value, this.state.editValue);
|
|
||||||
this.setState({
|
|
||||||
isEditing: false,
|
|
||||||
editValue: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const selectedClassName = this.state.isEditing
|
|
||||||
? "whitelist__item--selected"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={`whitelist__item ${selectedClassName}`}>
|
|
||||||
<div
|
|
||||||
className="whitelist__title"
|
|
||||||
onDoubleClick={this.beginEditing}
|
|
||||||
>
|
|
||||||
{this.state.isEditing ? (
|
|
||||||
<input
|
|
||||||
ref={el => (this.inputElement = el)}
|
|
||||||
type="text"
|
|
||||||
className="whitelist__input-pattern"
|
|
||||||
value={this.state.editValue?.pattern}
|
|
||||||
pattern={REMOTE_MATCH_PATTERN_REGEX.source}
|
|
||||||
onChange={ev => {
|
|
||||||
this.setState(prevState => ({
|
|
||||||
editValue: {
|
|
||||||
...prevState.editValue,
|
|
||||||
pattern: ev.target.value
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
onBlur={this.finishEditing}
|
|
||||||
onKeyPress={ev => {
|
|
||||||
if (ev.key === "Enter") {
|
|
||||||
this.finishEditing();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
this.props.value?.pattern
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="whitelist__user-agent">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={!this.props.value?.isUserAgentDisabled}
|
|
||||||
onChange={ev => {
|
|
||||||
if (!this.props.value) return;
|
|
||||||
this.props.onEdit(this.props.value, {
|
|
||||||
...this.props.value,
|
|
||||||
isUserAgentDisabled: !ev.target.checked
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{_("optionsSiteWhitelistUserAgent")}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="whitelist__edit-button ghost"
|
|
||||||
title={_("optionsSiteWhitelistEditItem")}
|
|
||||||
onClick={this.beginEditing}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<img src="assets/photon_edit.svg" alt="icon, edit" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="whitelist__remove-button ghost"
|
|
||||||
title={_("optionsSiteWhitelistRemoveItem")}
|
|
||||||
onClick={() => {
|
|
||||||
this.props.onRemove(this.props.value);
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<img src="assets/photon_delete.svg" alt="icon, remove" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
ext/src/ui/options/index.ts
Normal file
25
ext/src/ui/options/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import Options from "./Options.svelte";
|
||||||
|
|
||||||
|
// macOS styles
|
||||||
|
browser.runtime.getPlatformInfo().then(platformInfo => {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
|
||||||
|
switch (platformInfo.os) {
|
||||||
|
case "mac": {
|
||||||
|
link.href = "styles/mac.css";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link.href) {
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const target = document.getElementById("root");
|
||||||
|
if (target) {
|
||||||
|
new Options({ target });
|
||||||
|
}
|
||||||
@@ -1,462 +0,0 @@
|
|||||||
/* eslint-disable max-len */
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import React, { Component } from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
|
|
||||||
import defaultOptions from "../../defaultOptions";
|
|
||||||
import type { WhitelistItemData } from "../../background/whitelist";
|
|
||||||
|
|
||||||
import Bridge from "./Bridge";
|
|
||||||
import Whitelist from "./Whitelist";
|
|
||||||
|
|
||||||
import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge";
|
|
||||||
import logger from "../../lib/logger";
|
|
||||||
import options, { Options } from "../../lib/options";
|
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
|
||||||
|
|
||||||
// macOS styles
|
|
||||||
browser.runtime.getPlatformInfo().then(platformInfo => {
|
|
||||||
const link = document.createElement("link");
|
|
||||||
link.rel = "stylesheet";
|
|
||||||
|
|
||||||
switch (platformInfo.os) {
|
|
||||||
case "mac": {
|
|
||||||
link.href = "styles/mac.css";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link.href) {
|
|
||||||
document.head.appendChild(link);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getInputValue(input: HTMLInputElement) {
|
|
||||||
switch (input.type) {
|
|
||||||
case "checkbox":
|
|
||||||
return input.checked;
|
|
||||||
case "number":
|
|
||||||
return parseFloat(input.value);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return input.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OptionsAppProps {}
|
|
||||||
interface OptionsAppState {
|
|
||||||
hasLoaded: boolean;
|
|
||||||
bridgeLoading: boolean;
|
|
||||||
bridgeLoadingTimedOut: boolean;
|
|
||||||
isFormValid: boolean;
|
|
||||||
hasSaved: boolean;
|
|
||||||
|
|
||||||
options?: Options;
|
|
||||||
bridgeInfo?: BridgeInfo;
|
|
||||||
platform?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OptionsApp extends Component<OptionsAppProps, OptionsAppState> {
|
|
||||||
private form: HTMLFormElement | null = null;
|
|
||||||
|
|
||||||
state: OptionsAppState = {
|
|
||||||
hasLoaded: false,
|
|
||||||
bridgeLoading: true,
|
|
||||||
bridgeLoadingTimedOut: false,
|
|
||||||
isFormValid: true,
|
|
||||||
hasSaved: false
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: OptionsAppProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.handleReset = this.handleReset.bind(this);
|
|
||||||
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
|
||||||
this.handleFormChange = this.handleFormChange.bind(this);
|
|
||||||
this.handleInputChange = this.handleInputChange.bind(this);
|
|
||||||
this.handleWhitelistChange = this.handleWhitelistChange.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async componentDidMount() {
|
|
||||||
this.setState({
|
|
||||||
hasLoaded: true,
|
|
||||||
options: await options.getAll(),
|
|
||||||
platform: (await browser.runtime.getPlatformInfo()).os
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update options data if changed whilst page is open
|
|
||||||
options.addEventListener("changed", async () => {
|
|
||||||
this.setState({
|
|
||||||
options: await options.getAll()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bridgeInfo = await bridge.getInfo();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
bridgeInfo,
|
|
||||||
bridgeLoading: false
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to fetch bridge/platform info.");
|
|
||||||
|
|
||||||
if (err instanceof BridgeTimedOutError) {
|
|
||||||
this.setState({
|
|
||||||
bridgeLoading: false,
|
|
||||||
bridgeLoadingTimedOut: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
bridgeLoading: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
if (!this.state.hasLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form
|
|
||||||
id="form"
|
|
||||||
ref={form => {
|
|
||||||
this.form = form;
|
|
||||||
}}
|
|
||||||
onSubmit={this.handleFormSubmit}
|
|
||||||
onChange={this.handleFormChange}
|
|
||||||
>
|
|
||||||
<Bridge
|
|
||||||
info={this.state.bridgeInfo}
|
|
||||||
loading={this.state.bridgeLoading}
|
|
||||||
loadingTimedOut={this.state.bridgeLoadingTimedOut}
|
|
||||||
options={this.state.options}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<fieldset className="category">
|
|
||||||
<legend className="category__name">
|
|
||||||
<h2>{_("optionsMediaCategoryName")}</h2>
|
|
||||||
</legend>
|
|
||||||
<p className="category__description">
|
|
||||||
{_("optionsMediaCategoryDescription")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="mediaEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.options?.mediaEnabled}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsMediaEnabled")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="mediaSyncElement"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.state.options?.mediaSyncElement
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsMediaSyncElement")}
|
|
||||||
</div>
|
|
||||||
<div className="option__description">
|
|
||||||
{_("optionsMediaSyncElementDescription")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="mediaStopOnUnload"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.state.options?.mediaStopOnUnload
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsMediaStopOnUnload")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="localMediaEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.state.options?.localMediaEnabled
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsLocalMediaEnabled")}
|
|
||||||
</div>
|
|
||||||
<div className="option__description">
|
|
||||||
{_("optionsLocalMediaCategoryDescription")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="option">
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsLocalMediaServerPort")}
|
|
||||||
</div>
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="localMediaServerPort"
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
min="1025"
|
|
||||||
max="65535"
|
|
||||||
value={
|
|
||||||
this.state.options?.localMediaServerPort
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset className="category">
|
|
||||||
<legend className="category__name">
|
|
||||||
<h2>{_("optionsMirroringCategoryName")}</h2>
|
|
||||||
</legend>
|
|
||||||
<p className="category__description">
|
|
||||||
{_("optionsMirroringCategoryDescription")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="mirroringEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.state.options?.mirroringEnabled
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsMirroringEnabled")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="option">
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsMirroringAppId")}
|
|
||||||
</div>
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="mirroringAppId"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={this.state.options?.mirroringAppId}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
<div className="option__description">
|
|
||||||
{_("optionsMirroringAppIdDescription")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset className="category">
|
|
||||||
<legend className="category__name">
|
|
||||||
<h2>{_("optionsReceiverSelectorCategoryName")}</h2>
|
|
||||||
</legend>
|
|
||||||
<p className="category__description">
|
|
||||||
{_("optionsReceiverSelectorCategoryDescription")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="receiverSelectorWaitForConnection"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.state.options
|
|
||||||
?.receiverSelectorWaitForConnection
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsReceiverSelectorWaitForConnection")}
|
|
||||||
</div>
|
|
||||||
<div className="option__description">
|
|
||||||
{_(
|
|
||||||
"optionsReceiverSelectorWaitForConnectionDescription"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="receiverSelectorCloseIfFocusLost"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.state.options
|
|
||||||
?.receiverSelectorCloseIfFocusLost
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsReceiverSelectorCloseIfFocusLost")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset className="category">
|
|
||||||
<legend className="category__name">
|
|
||||||
<h2>{_("optionsSiteWhitelistCategoryName")}</h2>
|
|
||||||
</legend>
|
|
||||||
<p className="category__description">
|
|
||||||
{_("optionsSiteWhitelistCategoryDescription")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label className="option option--inline">
|
|
||||||
<div className="option__control">
|
|
||||||
<input
|
|
||||||
name="siteWhitelistEnabled"
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
this.state.options?.siteWhitelistEnabled
|
|
||||||
}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsSiteWhitelistEnabled")}
|
|
||||||
<span className="option__recommended">
|
|
||||||
{_("optionsOptionRecommended")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="option__description">
|
|
||||||
{_("optionsSiteWhitelistEnabledDescription")}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="option">
|
|
||||||
<div className="option__label">
|
|
||||||
{_("optionsSiteWhitelistContent")}
|
|
||||||
</div>
|
|
||||||
<div className="option__control">
|
|
||||||
{this.state.options?.siteWhitelist && (
|
|
||||||
<Whitelist
|
|
||||||
items={this.state.options.siteWhitelist}
|
|
||||||
onChange={this.handleWhitelistChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div id="buttons">
|
|
||||||
<div id="status-line">
|
|
||||||
{this.state.hasSaved && _("optionsSaved")}
|
|
||||||
</div>
|
|
||||||
<button onClick={this.handleReset} type="button">
|
|
||||||
{_("optionsReset")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
// @ts-ignore
|
|
||||||
default
|
|
||||||
disabled={!this.state.isFormValid}
|
|
||||||
>
|
|
||||||
{_("optionsSave")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleReset() {
|
|
||||||
this.setState({
|
|
||||||
options: { ...defaultOptions }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleFormSubmit(ev: React.FormEvent<HTMLFormElement>) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
this.form?.reportValidity();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.state.options) {
|
|
||||||
await options.setAll(this.state.options);
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
hasSaved: true
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
this.setState({
|
|
||||||
hasSaved: false
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to save options");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleFormChange(ev: React.FormEvent<HTMLFormElement>) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
const isFormValid = this.form?.checkValidity();
|
|
||||||
if (isFormValid !== undefined) {
|
|
||||||
this.setState({
|
|
||||||
isFormValid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInputChange(ev: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
this.setState(currentState => {
|
|
||||||
if (currentState.options) {
|
|
||||||
currentState.options[ev.target.name] = getInputValue(ev.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentState;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleWhitelistChange(whitelist: WhitelistItemData[]) {
|
|
||||||
this.setState(currentState => {
|
|
||||||
if (currentState.options) {
|
|
||||||
currentState.options.siteWhitelist = whitelist;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentState;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.render(<OptionsApp />, document.querySelector("#root"));
|
|
||||||
@@ -347,8 +347,9 @@ button.ghost:not(:hover) {
|
|||||||
.whitelist__item {
|
.whitelist__item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding: 0 5px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.whitelist__item:nth-child(odd) {
|
.whitelist__item:nth-child(odd) {
|
||||||
@@ -368,23 +369,11 @@ button.ghost:not(:hover) {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.whitelist__item:not(.whitelist__item--selected) > .whitelist__title {
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whitelist__title + button {
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whitelist__input-pattern {
|
.whitelist__input-pattern {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
margin-inline-end: 1em;
|
|
||||||
width: -moz-available;
|
width: -moz-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
.whitelist__user-agent {
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
}
|
|
||||||
.whitelist__add-button {
|
.whitelist__add-button {
|
||||||
margin-inline-end: auto;
|
margin-inline-end: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig"
|
"extends": "../tsconfig",
|
||||||
, "compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react"
|
"jsx": "react",
|
||||||
, "lib": [ "ESNext", "DOM", "DOM.Iterable" ]
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
, "moduleResolution": "node"
|
"moduleResolution": "node",
|
||||||
, "sourceMap": true
|
"sourceMap": true,
|
||||||
|
"module": "esnext"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
940
package-lock.json
generated
940
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
"@typescript-eslint/parser": "^5.14.0",
|
||||||
|
"esbuild-svelte": "^0.7.0",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
@@ -32,6 +33,8 @@
|
|||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"selenium-webdriver": "^4.1.1",
|
"selenium-webdriver": "^4.1.1",
|
||||||
|
"svelte": "^3.48.0",
|
||||||
|
"svelte-preprocess": "^4.10.6",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "^4.6.2",
|
||||||
"ws": "^8.5.0"
|
"ws": "^8.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
, "module": "commonjs"
|
"module": "commonjs",
|
||||||
, "noImplicitAny": true
|
"noImplicitAny": true,
|
||||||
, "noUnusedParameters": true
|
"noUnusedParameters": true,
|
||||||
, "removeComments": true
|
"removeComments": true,
|
||||||
, "resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
, "target": "es6"
|
"target": "es6",
|
||||||
, "strict": true
|
"strict": true
|
||||||
}
|
},
|
||||||
|
"exclude": ["node_modules/**/*", "dist/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user