Move UI components to ui/ directory

This commit is contained in:
hensm
2019-04-09 01:11:24 +01:00
parent a6ebb4fd22
commit 56ec766d86
22 changed files with 19 additions and 15 deletions

View File

@@ -0,0 +1,357 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
import semver from "semver";
import { getNextEllipsis
, getWindowCenteredProps } from "../../lib/utils";
import { BridgeInfo } from "../../lib/getBridgeInfo";
const _ = browser.i18n.getMessage;
const ENDPOINT_URL = "https://api.github.com/repos/hensm/fx_cast/releases/latest";
async function downloadApp (info: any, platform: string) {
const download = browser.downloads.download({
filename: info[platform].name
, url: info[platform].url
});
}
interface BridgeDownloadsProps {
info: any;
}
const BridgeDownloads = (props: BridgeDownloadsProps) => (
<div className="bridge-downloads">
<button className="bridge-downloads__download
bridge-downloads__win"
disabled
onClick={ () => downloadApp(props.info, "win") }>
Windows
</button>
<button className="bridge-downloads__download
bridge-downloads__mac"
onClick={ () => downloadApp(props.info, "mac") }>
macOS
</button>
<div className="bridge-downloads__linux">
<button className="bridge-downloads__download"
onClick={ () => downloadApp(props.info, "deb") }>
Linux (deb)
</button>
<button className="bridge-downloads__download"
onClick={ () => downloadApp(props.info, "rpm") }>
Linux (rpm)
</button>
</div>
</div>
);
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>
{ // If older
props.info.isVersionOlder
? _("optionsBridgeOlderAction")
// else if newer
: props.info.isVersionNewer
? _("optionsBridgeNewerAction")
// else
: _("optionsBridgeNoAction")
}
</td>
</tr>
</table>
);
interface BridgeProps {
info: BridgeInfo;
platform: string;
loading: boolean;
}
interface BridgeState {
isCheckingUpdates: boolean;
isUpdateAvailable: boolean;
wasErrorCheckingUpdates: boolean;
checkUpdatesEllipsis: string;
updateStatus: string;
packageType: string;
}
export default class Bridge extends Component<BridgeProps, BridgeState> {
private updateData: any;
private updateStatusTimeout: number;
constructor (props: BridgeProps) {
super(props);
this.state = {
isCheckingUpdates: false
, isUpdateAvailable: false
, wasErrorCheckingUpdates: false
, checkUpdatesEllipsis: "..."
, updateStatus: null
, packageType: null
};
this.onCheckUpdates = this.onCheckUpdates.bind(this);
this.onCheckUpdatesResponse = this.onCheckUpdatesResponse.bind(this);
this.onCheckUpdatesError = this.onCheckUpdatesError.bind(this);
this.onUpdate = this.onUpdate.bind(this);
this.onPackageTypeChange = this.onPackageTypeChange.bind(this);
}
public render () {
return (
<div className="bridge">
{ this.props.loading
? ( <div className="bridge__loading">
{ _("optionsBridgeLoading") }
<progress></progress>
</div> )
: this.renderStatus() }
{ !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">
{ this.props.platform === "linux" &&
<select className="bridge__update-package-type"
onChange={ this.onPackageTypeChange }
value={ this.state.packageType }>
<option value="" disabled selected>
{ _("optionsBridgeUpdatePackageTypeSelect") }
</option>
<option value="deb">
{ _("optionsBridgeUpdatePackageTypeDeb") }
</option>
<option value="rpm">
{ _("optionsBridgeUpdatePackageTypeRpm") }
</option>
</select> }
<button className="bridge__update-start"
onClick={ this.onUpdate }
disabled={ this.props.platform === "linux"
&& !this.state.packageType }>
{ _("optionsBridgeUpdate") }
</button>
</div>
</div> )
: ( <button className="bridge__update-check"
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
? "bridge__info--found"
: "bridge__info--not-found"}`;
let statusIcon: string;
let statusTitle: string;
let statusText: string;
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(ENDPOINT_URL)
.then(res => {
window.clearTimeout(timeout);
return res.json();
})
.then(this.onCheckUpdatesResponse)
.catch(this.onCheckUpdatesError);
}
private showUpdateStatus () {
if (this.updateStatusTimeout) {
window.clearTimeout(this.updateStatusTimeout);
}
this.updateStatusTimeout = window.setTimeout(() => {
this.setState({
updateStatus: null
});
}, 1500);
}
private async onUpdate () {
// Current window to base centered position on
const win = await browser.windows.getCurrent();
const centeredProps = getWindowCenteredProps(win, 400, 150);
const updaterPopup = await browser.windows.create({
url: "../updater/index.html"
, type: "popup"
, ...centeredProps
});
// Size/position not set correctly on creation (bug?)
await browser.windows.update(updaterPopup.id, {
...centeredProps
});
browser.runtime.onConnect.addListener(port => {
if (port.name === "updater") {
const asset = this.updateData.assets.find((currentAsset: any) => {
const fileExtension = currentAsset.name.match(/.*\.(.*)$/).pop();
const currentPlatform = (this.props.platform === "linux")
? this.state.packageType
: this.props.platform;
switch (fileExtension) {
case "exe": return "win" === currentPlatform;
case "pkg": return "mac" === currentPlatform;
case "deb": return "deb" === currentPlatform;
case "rpm": return "rpm" === currentPlatform;
}
});
port.postMessage({
subject: "updater:/updateData"
, data: asset
});
port.onDisconnect.addListener(() => {
browser.windows.remove(updaterPopup.id);
});
}
});
}
private async onCheckUpdatesResponse (res: any) {
const isUpdateAvailable = !this.props.info || semver.lt(
this.props.info.version, res.tag_name);
if (isUpdateAvailable) {
this.updateData = res;
}
this.setState({
isCheckingUpdates: false
, isUpdateAvailable
, updateStatus: !isUpdateAvailable
? _("optionsBridgeUpdateStatusNoUpdates")
: null
});
this.showUpdateStatus();
}
private onCheckUpdatesError () {
this.setState({
isCheckingUpdates: false
, wasErrorCheckingUpdates: true
, updateStatus: _("optionsBridgeUpdateStatusError")
});
this.showUpdateStatus();
}
private onPackageTypeChange (ev: React.ChangeEvent<HTMLSelectElement>) {
this.setState({
packageType: ev.target.value
});
}
}

View File

@@ -0,0 +1,184 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
import EditableListItem from "./EditableListItem";
const _ = browser.i18n.getMessage;
interface EditableListProps {
data: string[];
itemPattern: RegExp;
onChange (data: string[]): void;
itemPatternError (err?: string): string;
}
interface EditableListState {
addingNewItem: boolean;
rawView: boolean;
rawViewValue: string;
}
export default class EditableList extends Component<
EditableListProps, EditableListState> {
private rawViewTextArea: HTMLTextAreaElement;
constructor (props: EditableListProps) {
super(props);
this.state = {
addingNewItem: false
, rawView: false
, rawViewValue: ""
};
this.handleItemRemove = this.handleItemRemove.bind(this);
this.handleItemEdit = this.handleItemEdit.bind(this);
this.handleSwitchView = this.handleSwitchView.bind(this);
this.handleSaveRaw = this.handleSaveRaw.bind(this);
this.handleRawViewTextAreaChange = this.handleRawViewTextAreaChange.bind(this);
this.handleAddItem = this.handleAddItem.bind(this);
this.handleNewItemRemove = this.handleNewItemRemove.bind(this);
this.handleNewItemEdit = this.handleNewItemEdit.bind(this);
}
public render () {
return (
<div className="editable-list">
<div className="editable-list__view-actions">
{ this.state.rawView &&
<button className="editable-list__save-raw-button"
onClick={ this.handleSaveRaw }
type="button">
{ _("optionsUserAgentWhitelistSaveRaw") }
</button> }
<button className="editable-list__view-button"
onClick={ this.handleSwitchView }
type="button">
{ this.state.rawView
? _("optionsUserAgentWhitelistBasicView")
: _("optionsUserAgentWhitelistRawView") }
</button>
</div>
<hr />
{ this.state.rawView
? (
<textarea className="editable-list__raw-view"
rows={ this.props.data.length}
value={ this.state.rawViewValue}
onChange={ this.handleRawViewTextAreaChange }
ref={ el => { this.rawViewTextArea = el; }}>
</textarea>
) : (
<ul className="editable-list__items">
{ this.props.data.map((item, i) =>
<EditableListItem text={ item }
itemPattern={ this.props.itemPattern }
itemPatternError={ this.props.itemPatternError }
onRemove={ this.handleItemRemove }
onEdit={ this.handleItemEdit }
key={ i } /> )}
{ this.state.addingNewItem &&
<EditableListItem text=""
itemPattern={ this.props.itemPattern }
itemPatternError={ this.props.itemPatternError }
onRemove={ this.handleNewItemRemove }
onEdit={ this.handleNewItemEdit }
editing={ true } /> }
<div className="editable-list__item editable-list__item-actions">
<button className="editable-list__add-button"
onClick={ this.handleAddItem }
type="button">
{ _("optionsUserAgentWhitelistAddItem") }
</button>
</div>
</ul>
)}
</div>
);
}
private handleItemRemove (item: string) {
const newItems = new Set(this.props.data);
newItems.delete(item);
this.props.onChange([...newItems]);
}
private handleItemEdit (item: string, newValue: string) {
this.props.onChange(this.props.data.map(
currentItem => currentItem === item
? newValue
: currentItem));
}
private handleSwitchView () {
this.setState(currentState => {
if (currentState.rawView) {
return {
rawView: false
, rawViewValue: ""
};
}
return {
rawView: true
, rawViewValue: this.props.data.join("\n")
};
});
}
private handleSaveRaw () {
this.setState(currentState => {
const newItems = currentState.rawViewValue.split("\n")
.filter(item => item !== "");
if ("itemPattern" in this.props) {
for (const item of newItems) {
if (!this.props.itemPattern.test(item)) {
this.rawViewTextArea.setCustomValidity(
this.props.itemPatternError(item));
return;
}
}
this.rawViewTextArea.setCustomValidity("");
}
this.props.onChange(newItems);
});
}
private handleRawViewTextAreaChange (ev: React.ChangeEvent<HTMLTextAreaElement>) {
if (this.rawViewTextArea.scrollHeight > this.rawViewTextArea.clientHeight) {
this.rawViewTextArea.style.height = `${this.rawViewTextArea.scrollHeight}px`;
}
this.setState({
rawViewValue: ev.target.value
});
}
private handleAddItem () {
this.setState({
addingNewItem: true
});
}
private handleNewItemRemove () {
this.setState({
addingNewItem: false
});
}
private handleNewItemEdit (item: string, newItem: string) {
this.setState({
addingNewItem: false
}, () => {
this.props.onChange([ ...this.props.data, newItem ]);
});
}
}

View File

@@ -0,0 +1,124 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
const _ = browser.i18n.getMessage;
interface EditableListItemProps {
text: string;
itemPattern: RegExp;
editing?: boolean;
itemPatternError (err?: string): string;
onRemove (item: string): void;
onEdit (item: string, newValue: string): void;
}
interface EditableListItemState {
editing: boolean;
editValue: string;
}
export default class EditableListItem extends Component<
EditableListItemProps, EditableListItemState> {
private input: HTMLInputElement;
constructor (props: EditableListItemProps) {
super(props);
this.state = {
editing: this.props.editing || false
, editValue: ""
};
this.handleRemove = this.handleRemove.bind(this);
this.handleEditBegin = this.handleEditBegin.bind(this);
this.handleEditEnd = this.handleEditEnd.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleInputKeyPress = this.handleInputKeyPress.bind(this);
}
public render () {
const selected = this.state.editing
? "editable-list__item--selected" : "";
return (
<li className={`editable-list__item ${selected}`}>
<div className="editable-list__title"
onDoubleClick={ this.handleEditBegin }>
{ this.state.editing
? <input className="editable-list__edit-field"
type="text"
ref={ input => this.input = input }
value={ this.state.editValue }
onBlur={ this.handleEditEnd }
onChange={ this.handleInputChange }
onKeyPress={ this.handleInputKeyPress }/>
: this.props.text }
</div>
<button onClick={ this.handleEditBegin }
type="button">
{ _("optionsUserAgentWhitelistEditItem") }
</button>
<button onClick={ this.handleRemove }
type="button">
{ _("optionsUserAgentWhitelistRemoveItem") }
</button>
</li>
);
}
private stopEditing (input: HTMLInputElement) {
if (this.props.editing
&& !this.props.itemPattern.test(this.state.editValue)) {
input.setCustomValidity(this.props.itemPatternError());
}
if (!input.validity.valid) {
return;
}
this.props.onEdit(this.props.text, this.state.editValue);
this.setState({
editing: false
, editValue: ""
});
}
private handleRemove () {
this.props.onRemove(this.props.text);
}
private handleEditBegin () {
this.setState({
editing: true
, editValue: this.props.text
}, () => {
this.input.focus();
});
}
private handleEditEnd (ev: React.FocusEvent<HTMLInputElement>) {
this.stopEditing(ev.target);
}
private handleInputChange (ev: React.ChangeEvent<HTMLInputElement>) {
this.setState({
editValue: ev.target.value
});
if (!this.props.itemPattern.test(ev.target.value)) {
ev.target.setCustomValidity(this.props.itemPatternError());
} else {
ev.target.setCustomValidity("");
}
}
private handleInputKeyPress (ev: React.KeyboardEvent<HTMLInputElement>) {
if (ev.key === "Enter") {
this.stopEditing(ev.target as HTMLInputElement);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles/index.css">
<script src="bundle.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,362 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
import ReactDOM from "react-dom";
import defaultOptions, { Options } from "../../defaultOptions";
import Bridge from "./Bridge";
import EditableList from "./EditableList";
import getBridgeInfo, { BridgeInfo } from "../../lib/getBridgeInfo";
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/utils";
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);
}
});
function getInputValue (input: HTMLInputElement) {
switch (input.type) {
case "checkbox":
return input.checked;
case "number":
return parseFloat(input.value);
default:
return input.value;
}
}
interface OptionsAppState {
hasLoaded: boolean;
options: Options;
bridgeInfo: BridgeInfo;
platform: string;
bridgeLoading: boolean;
isFormValid: boolean;
hasSaved: boolean;
}
class OptionsApp extends Component<{}, OptionsAppState> {
private form: HTMLFormElement;
constructor (props: {}) {
super(props);
this.state = {
hasLoaded: false
, options: null
, bridgeInfo: null
, platform: null
, bridgeLoading: true
, isFormValid: true
, hasSaved: false
};
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);
this.getWhitelistItemPatternError
= this.getWhitelistItemPatternError.bind(this);
}
public async componentDidMount () {
const { options } = await browser.storage.sync.get("options");
this.setState({
hasLoaded: true
, options
});
const bridgeInfo = await getBridgeInfo();
const { os } = await browser.runtime.getPlatformInfo();
this.setState({
bridgeInfo
, platform: os
, bridgeLoading: false
});
}
public render () {
if (!this.state.hasLoaded) {
return;
}
return (
<div>
<Bridge info={ this.state.bridgeInfo }
platform={ this.state.platform }
loading={ this.state.bridgeLoading } />
<form id="form" ref={ form => { this.form = form; }}
onSubmit={ this.handleFormSubmit }
onChange={ this.handleFormChange }>
<fieldset className="category">
<legend className="category__name">
<h2>{ _("optionsMediaCategoryName") }</h2>
</legend>
<p className="category__description">
{ _("optionsMediaCategoryDescription") }
</p>
<label className="option option--inline">
<input name="mediaEnabled"
type="checkbox"
checked={ this.state.options.mediaEnabled }
onChange={ this.handleInputChange } />
<div className="option__label">
{ _("optionsMediaEnabled") }
</div>
</label>
<label className="option option--inline">
<input name="mediaSyncElement"
type="checkbox"
checked={ this.state.options.mediaSyncElement }
onChange={ this.handleInputChange } />
<div className="option__label">
{ _("optionsMediaSyncElement") }
</div>
</label>
<label className="option option--inline">
<input name="mediaStopOnUnload"
type="checkbox"
checked={ this.state.options.mediaStopOnUnload }
onChange={ this.handleInputChange } />
<div className="option__label">
{ _("optionsMediaStopOnUnload") }
</div>
</label>
<fieldset className="category"
disabled={ !this.state.options.mediaEnabled }>
<legend className="category__name">
<h2>{ _("optionsLocalMediaCategoryName") }</h2>
</legend>
<p className="category__description">
{ _("optionsLocalMediaCategoryDescription") }
</p>
<label className="option option--inline">
<input name="localMediaEnabled"
type="checkbox"
checked={ this.state.options.localMediaEnabled }
onChange={ this.handleInputChange } />
<div className="option__label">
{ _("optionsLocalMediaEnabled") }
</div>
</label>
<label className="option">
<div className="option__label">
{ _("optionsLocalMediaServerPort") }
</div>
<input name="localMediaServerPort"
type="number"
required
min="1025"
max="65535"
value={ this.state.options.localMediaServerPort }
onChange={ this.handleInputChange } />
</label>
</fieldset>
</fieldset>
<fieldset className="category">
<legend className="category__name">
<h2>{ _("optionsMirroringCategoryName") }</h2>
</legend>
<p className="category__description">
{ _("optionsMirroringCategoryDescription") }
</p>
<label className="option option--inline">
<input name="mirroringEnabled"
type="checkbox"
checked={ this.state.options.mirroringEnabled }
onChange={ this.handleInputChange } />
<div className="option__label">
{ _("optionsMirroringEnabled") }
</div>
</label>
<label className="option">
<div className="option__label">
{ _("optionsMirroringAppId") }
</div>
<input name="mirroringAppId"
type="text"
required
value={ this.state.options.mirroringAppId }
onChange={ this.handleInputChange } />
</label>
</fieldset>
<fieldset className="category">
<legend className="category__name">
<h2>{ _("optionsUserAgentWhitelistCategoryName") }</h2>
</legend>
<p className="category__description">
{ _("optionsUserAgentWhitelistCategoryDescription") }
</p>
<label className="option option--inline">
<input name="userAgentWhitelistEnabled"
type="checkbox"
checked={ this.state.options.userAgentWhitelistEnabled }
onChange={ this.handleInputChange } />
<div className="option__label">
{ _("optionsUserAgentWhitelistEnabled") }
</div>
</label>
<div className="option">
<div className="option__label">
{ _("optionsUserAgentWhitelistContent") }
</div>
<EditableList data={ this.state.options.userAgentWhitelist }
onChange={ this.handleWhitelistChange }
itemPattern={ REMOTE_MATCH_PATTERN_REGEX }
itemPatternError={ this.getWhitelistItemPatternError } />
</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"
default
disabled={ !this.state.isFormValid }>
{ _("optionsSave") }
</button>
</div>
</form>
</div>
);
}
/**
* Set stored option values to current state
*/
private setStorage () {
return browser.storage.sync.set({
options: this.state.options
});
}
private handleReset () {
this.setState({
options: { ...defaultOptions }
});
}
private async handleFormSubmit (ev: React.FormEvent<HTMLFormElement>) {
ev.preventDefault();
this.form.reportValidity();
try {
const { options: oldOptions }
= await browser.storage.sync.get("options");
await this.setStorage();
const { options } = await browser.storage.sync.get("options");
const alteredOptions = [];
for (const [ key, val ] of Object.entries(options)) {
const oldVal = oldOptions[key];
if (oldVal !== val) {
alteredOptions.push(key);
}
}
this.setState({
hasSaved: true
}, () => {
window.setTimeout(() => {
this.setState({
hasSaved: false
});
}, 1000);
});
// Send update message / event
browser.runtime.sendMessage({
subject: "optionsUpdated"
, data: { alteredOptions }
});
} catch (err) {
console.error("Failed to save options");
}
}
private handleFormChange (ev: React.FormEvent<HTMLFormElement>) {
ev.preventDefault();
this.setState({
isFormValid: this.form.checkValidity()
});
}
private handleInputChange (ev: React.ChangeEvent<HTMLInputElement>) {
const { target } = ev;
this.setState(({ options }) => {
options[target.name as keyof Options] = getInputValue(target);
return { options };
});
}
private handleWhitelistChange (whitelist: string[]) {
this.setState(({ options }) => {
options.userAgentWhitelist = whitelist;
return { options };
});
}
private getWhitelistItemPatternError (info: string): string {
return _("optionsUserAgentWhitelistInvalidMatchPattern", info);
}
private async updateBridgeInfo () {
this.setState({
bridgeLoading: true
});
const bridgeInfo = await getBridgeInfo();
this.setState({
bridgeInfo
, bridgeLoading: false
});
}
}
ReactDOM.render(
<OptionsApp />
, document.querySelector("#root"));

View File

@@ -0,0 +1,299 @@
:root {
--border-color: rgb(225, 225, 225);
--secondary-color: rgb(125, 125, 125);
}
*:invalid {
box-shadow: 0 0 1.5px 1px red;
}
#form {
display: flex;
flex-direction: column;
}
#buttons {
display: flex;
align-items: center;
align-self: flex-end;
margin-block-start: 20px;
}
#buttons > :not(:last-child) {
margin-inline-end: 5px;
}
#status-line {
color: var(--secondary-color);
}
.bridge {
border-bottom: 1px solid var(--border-color);
margin-bottom: 10px;
padding-bottom: 20px;
}
.bridge,
.bridge__loading {
display: flex;
flex-direction: column;
}
.bridge__loading {
align-items: center;
align-self: center;
font-size: 1.25em;
font-weight: 300;
width: 30%;
}
.bridge__loading progress {
margin-top: 5px;
width: 100%;
}
.bridge__info {
display: flex;
padding-inline-start: 25px;
}
.bridge__status {
align-items: center;
display: flex;
flex-basis: min-content;
flex-direction: column;
margin-inline-end: 25px;
}
.bridge__info--not-found {
padding-inline-end: 25px;
}
.bridge__info--found .bridge__status {
border-inline-end: 1px solid var(--border-color);
padding-inline-end: 25px;
}
.bridge__status-title {
margin: initial;
font-weight: 500;
font-size: 1.5em;
white-space: nowrap;
}
.bridge__status-text {
margin: initial;
margin-top: 5px;
font-size: 1.15em;
font-weight: 300;
text-align: center;
}
.bridge__info--not-found .bridge__status {
display: grid;
grid-template-columns: min-content 1fr;
grid-template-rows: min-content min-content;
grid-template-areas:
"status-icon status-title"
"status-icon status-text";
}
.bridge__info--found .bridge__status-icon {
margin-block-end: 5px;
}
.bridge__info--not-found .bridge__status-icon {
grid-area: status-icon;
margin-inline-end: 10px;
}
.bridge__info--not-found .bridge__status-title {
grid-area: status-title;
font-weight: normal;
white-space: normal;
}
.bridge__info--not-found .bridge__status-text {
grid-area: status-text;
margin-top: initial;
}
.bridge__stats {
border-collapse: collapse;
border-spacing: 0;
}
.bridge__stats th {
font-weight: 500;
padding-inline-end: 10px;
text-align: end;
vertical-align: top;
white-space: nowrap;
}
.bridge__update-info {
align-items: center;
display: flex;
margin-top: 30px;
}
.bridge__update-label {
display: inline-block;
margin: initial;
}
.bridge__update-options {
display: inline-flex;
flex-direction: column;
margin-left: 10px;
}
.bridge__update-start {
align-self: flex-end;
margin-top: 5px;
}
.bridge--update-status {
margin-left: 10px;
}
.category {
border: initial;
display: grid;
grid-template-columns: 150px 1fr;
grid-column-gap: 10px;
grid-row-gap: 5px;
margin: initial;
padding: 10px 0;
}
.category:disabled {
color: var(--secondary-color);
}
#form > .category:not(:first-child) {
border-top: 1px solid var(--border-color);
}
.category > .category {
padding: 5px 0;
box-shadow: inset 2px 0 0 0 var(--border-color);
}
.category > .category > .category__name,
.category > .category > .category__description {
margin-inline-start: 16px;
}
.category__name {
float: left;
}
.category__name > h2 {
font-size: 1.15em;
font-weight: 500;
margin: initial;
}
.category__description {
color: var(--secondary-color);
margin-top: initial;
max-width: 60ch;
}
.category__name,
.category__description,
.category .category {
grid-column: span 2;
}
.option {
display: contents;
}
.option--inline {
display: block;
grid-column-start: 2;
}
.option__label {
text-align: right;
display: inline-block;
}
.option > input {
align-self: center;
justify-self: flex-start;
margin-inline-start: initial;
}
.editable-list {
background-color: -moz-field;
border: 1px solid var(--border-color);
color: -moz-fieldtext;
justify-content: end;
padding: 5px;
}
.editable-list__view-actions {
display: flex;
justify-content: end;
}
.editable-list__view-button,
.editable-list__save-raw-button {
}
.editable-list hr {
border: initial;
border-top: 1px solid var(--border-color);
margin: 5px 0;
}
.editable-list__items {
display: flex;
flex-direction: column;
margin: initial;
margin-inline-start: -5px;
padding: initial;
width: calc(100% + 10px);
}
.editable-list__item {
align-items: center;
display: flex;
height: 2em;
padding: 0 5px;
}
.editable-list__item:nth-child(even):not(:last-child) {
background-color: -moz-eventreerow;
}
.editable-list__item:nth-child(odd):not(:last-child) {
background-color: -moz-oddtreerow;
}
.editable-list__item--selected {
background-color: -moz-cellhighlight !important;
color: -moz-cellhighlighttext !important;
}
.editable-list__title {
flex: 1;
}
.editable-list__edit-field {
width: -moz-available;
margin-inline-end: 1em;
}
.editable-list__raw-view {
max-height: 300px;
resize: vertical;
width: 100%;
}
.editable-list__add-button {
align-self: end;
margin-top: 5px;
}

View File

@@ -0,0 +1,24 @@
body {
font: menu;
}
button,
select,
input {
font: inherit;
}
button:not([disabled]):hover:active {
color: -moz-mac-buttonactivetext;
}
button[default]:not([disabled]):not(:-moz-window-inactive) {
color: -moz-mac-defaultbuttontext;
}
button[default]:not(:hover):active {
color: ButtonText;
}
button,
select {
height: 22px;
}