Convert ext options to typescript

This commit is contained in:
hensm
2019-02-27 16:32:04 +00:00
parent 3ea943c509
commit 7eaa97a556
9 changed files with 566 additions and 447 deletions

13
ext/src/global.d.ts vendored
View File

@@ -16,3 +16,16 @@ declare namespace browser.runtime {
error: { message: string };
}
}
// Allow default attribute on <button>
declare namespace React {
interface ButtonHTMLAttributes<T> {
default?: boolean;
}
}
declare namespace JSX {
interface IntrinsicElements {
button: React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
}
}

View File

@@ -1,3 +1,6 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
import semver from "semver";
@@ -9,14 +12,19 @@ const _ = browser.i18n.getMessage;
const ENDPOINT_URL = "https://api.github.com/repos/hensm/fx_cast/releases/14720978";
async function downloadApp (info, platform) {
async function downloadApp (info: any, platform: string) {
const download = browser.downloads.download({
filename: info[platform].name
, url: info[platform].url
});
}
const BridgeDownloads = (props) => (
interface BridgeDownloadsProps {
info: any;
}
const BridgeDownloads = (props: BridgeDownloadsProps) => (
<div className="bridge-downloads">
<button className="bridge-downloads__download
bridge-downloads__win"
@@ -43,7 +51,12 @@ const BridgeDownloads = (props) => (
</div>
);
const BridgeStats = (props) => (
interface BridgeStatsProps {
info: any;
}
const BridgeStats = (props: BridgeStatsProps) => (
<table className="bridge__stats">
<tr>
<th>{ _("optionsBridgeStatsName") }</th>
@@ -84,19 +97,29 @@ const BridgeStats = (props) => (
</table>
);
export default class Bridge extends Component {
constructor (props) {
interface BridgeProps {
info: any;
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.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);
this.updateData = null;
this.updateStatusTimeout = null;
this.state = {
isCheckingUpdates: false
, isUpdateAvailable: false
@@ -105,130 +128,81 @@ export default class Bridge extends Component {
, 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);
}
render () {
return (
<div className="bridge">
{ this.props.loading
? ( <div className="bridge__loading">
{ _("optionsBridgeLoading") }
<progress></progress>
</div> )
: this.renderStatus() }
onCheckUpdates () {
this.setState({
isCheckingUpdates: true
});
{ !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 }>
const timeout = setInterval(() => {
this.setState(state => ({
checkUpdatesEllipsis: getNextEllipsis(
state.checkUpdatesEllipsis)
}));
}, 500);
{ this.state.isCheckingUpdates
? _("optionsBridgeUpdateChecking"
, getNextEllipsis(this.state.checkUpdatesEllipsis))
: _("optionsBridgeUpdateCheck") }
</button> )}
fetch(ENDPOINT_URL)
.then(res => {
window.clearTimeout(timeout);
return res.json()
})
.then(this.onCheckUpdatesResponse)
.catch(this.onCheckUpdatesError);
<div className="bridge--update-status">
{ this.state.updateStatus && !this.state.isUpdateAvailable
&& this.state.updateStatus }
</div>
</div> }
</div>
);
}
showUpdateStatus () {
if (this.updateStatusTimeout) {
window.clearTimeout(this.updateStatusTimeout);
}
this.updateStatusTimeout = window.setTimeout(() => {
this.setState({
updateStatus: null
});
}, 1500);
}
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(asset => {
const fileExtension = asset.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);
});
}
});
}
async onCheckUpdatesResponse (res) {
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();
}
onCheckUpdatesError (err) {
this.setState({
isCheckingUpdates: false
, wasErrorCheckingUpdates: true
, updateStatus: _("optionsBridgeUpdateStatusError")
});
this.showUpdateStatus();
}
onPackageTypeChange (ev) {
this.setState({
packageType: ev.target.value
});
}
renderStatus () {
private renderStatus () {
const infoClasses = `bridge__info ${this.props.info
? "bridge__info--found"
: "bridge__info--not-found"}`;
let statusIcon;
let statusTitle;
let statusText;
let statusIcon: string;
let statusTitle: string;
let statusText: string;
if (!this.props.info) {
statusIcon = "assets/icons8-cancel-120.png";
@@ -237,7 +211,7 @@ export default class Bridge extends Component {
} else {
if (this.props.info.isVersionCompatible) {
statusIcon = "assets/icons8-ok-120.png";
statusTitle = _("optionsBridgeFoundStatusTitle";
statusTitle = _("optionsBridgeFoundStatusTitle");
} else {
statusIcon = "assets/icons8-warn-120.png";
statusTitle = _("optionsBridgeIssueStatusTitle");
@@ -267,65 +241,115 @@ export default class Bridge extends Component {
);
}
render () {
return (
<div className="bridge">
{ this.props.loading
? ( <div className="bridge__loading">
{ _("optionsBridgeLoading") }
<progress></progress>
</div>
: this.renderStatus() }
private onCheckUpdates () {
this.setState({
isCheckingUpdates: true
});
{ !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 }>
const timeout = window.setInterval(() => {
this.setState(state => ({
checkUpdatesEllipsis: getNextEllipsis(
state.checkUpdatesEllipsis)
}));
}, 500);
{ this.state.isCheckingUpdates
? _("optionsBridgeUpdateChecking"
, getNextEllipsis(this.state.checkUpdatesEllipsis))
: _("optionsBridgeUpdateCheck") }
</button>
)}
fetch(ENDPOINT_URL)
.then(res => {
window.clearTimeout(timeout);
return res.json();
})
.then(this.onCheckUpdatesResponse)
.catch(this.onCheckUpdatesError);
}
<div className="bridge--update-status">
{ this.state.updateStatus && !this.state.isUpdateAvailable
&& this.state.updateStatus }
</div>
</div> }
</div>
);
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

@@ -1,12 +1,34 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
import EditableListItem from "./EditableListItem";
const _ = browser.i18n.getMessage;
export default class EditableList extends Component {
constructor (props) {
interface EditableListProps {
data: string[];
itemPattern: RegExp;
onChange (data: string[]): void;
itemPatternError (err?: string): string;
}
interface EditableListState {
items: Set<string>;
addingNewItem: boolean;
rawView: boolean;
rawViewValue: string;
}
export default class EditableList extends Component<
EditableListProps, EditableListState> {
private rawViewTextArea: HTMLTextAreaElement;
constructor (props: EditableListProps) {
super(props);
this.state = {
items: new Set(this.props.data)
, addingNewItem: false
@@ -24,99 +46,6 @@ export default class EditableList extends Component {
this.handleNewItemEdit = this.handleNewItemEdit.bind(this);
}
handleItemRemove (item) {
this.setState(currentState => {
const newItems = new Set(currentState.items);
newItems.delete(item);
return {
items: newItems
};
}, () => {
this.props.onChange(Array.from(this.state.items));
});
}
handleItemEdit (item, newValue) {
this.setState(currentState => ({
items: new Set([...currentState.items]
.map(item_ => item_ === item ? newValue : item_))
}), () => {
this.props.onChange(Array.from(this.state.items));
});
}
handleSwitchView () {
this.setState(currentState => {
if (currentState.rawView) {
return {
rawView: false
, rawViewValue: ""
};
}
return {
rawView: true
, rawViewValue: Array.from(currentState.items.values()).join("\n")
};
});
}
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("");
}
return {
items: new Set(newItems)
};
}, () => {
this.props.onChange(Array.from(this.state.items));
});
}
handleRawViewTextAreaChange (ev) {
if (this.rawViewTextArea.scrollHeight > this.rawViewTextArea.clientHeight) {
this.rawViewTextArea.style.height = `${this.rawViewTextArea.scrollHeight}px`;
}
this.setState({
rawViewValue: ev.target.value
});
}
handleAddItem () {
this.setState({
addingNewItem: true
});
}
handleNewItemRemove () {
this.setState({
addingNewItem: false
});
}
handleNewItemEdit (item, newItem) {
this.setState(currentState => ({
items: [ ...currentState.items, newItem ]
, addingNewItem: false
}), () => {
this.props.onChange(Array.from(this.state.items));
});
}
render () {
const items = Array.from(this.state.items.values());
@@ -142,11 +71,11 @@ export default class EditableList extends Component {
rows={ items.length}
value={ this.state.rawViewValue}
onChange={ this.handleRawViewTextAreaChange }
ref={ el => { this.rawViewTextArea = el }}>
ref={ el => { this.rawViewTextArea = el; }}>
</textarea>
) : (
<ul className="editable-list__items">
{ items.map((item, i) =>
{ items.map((item, i) =>
<EditableListItem text={ item }
itemPattern={ this.props.itemPattern }
itemPatternError={ this.props.itemPatternError }
@@ -172,4 +101,99 @@ export default class EditableList extends Component {
</div>
);
}
private handleItemRemove (item: string) {
this.setState(currentState => {
const newItems = new Set(currentState.items);
newItems.delete(item);
return {
items: newItems
};
}, () => {
this.props.onChange(Array.from(this.state.items));
});
}
private handleItemEdit (item: string, newValue: string) {
this.setState(currentState => ({
items: new Set([...currentState.items]
.map(currentItem => currentItem === item
? newValue
: currentItem))
}), () => {
this.props.onChange(Array.from(this.state.items));
});
}
private handleSwitchView () {
this.setState(currentState => {
if (currentState.rawView) {
return {
rawView: false
, rawViewValue: ""
};
}
return {
rawView: true
, rawViewValue: Array.from(currentState.items.values()).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("");
}
return {
items: new Set(newItems)
};
}, () => {
this.props.onChange(Array.from(this.state.items));
});
}
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(currentState => ({
items: new Set([ ...currentState.items, newItem ])
, addingNewItem: false
}), () => {
this.props.onChange(Array.from(this.state.items));
});
}
}

View File

@@ -1,11 +1,33 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
const _ = browser.i18n.getMessage;
export default class EditableListItem extends Component {
constructor (props) {
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: ""
@@ -18,54 +40,6 @@ export default class EditableListItem extends Component {
this.handleInputKeyPress = this.handleInputKeyPress.bind(this);
}
handleRemove () {
this.props.onRemove(this.props.text);
}
handleEditBegin () {
this.setState({
editing: true
, editValue: this.props.text
}, () => {
this.input.focus();
});
}
handleEditEnd (ev) {
if (this.props.editing
&& !this.props.itemPattern.test(this.state.editValue)) {
ev.target.setCustomValidity(this.props.itemPatternError());
}
if (!ev.target.validity.valid) {
return;
}
this.props.onEdit(this.props.text, this.state.editValue);
this.setState({
editing: false
, editValue: ""
});
}
handleInputChange (ev) {
this.setState({
editValue: ev.target.value
});
if (!this.props.itemPattern.test(ev.target.value)) {
ev.target.setCustomValidity(this.props.itemPatternError());
} else {
ev.target.setCustomValidity("");
}
}
handleInputKeyPress (ev) {
if (ev.key === "Enter") {
this.handleEditEnd({ target: ev.target });
}
}
render () {
const selected = this.state.editing
? "editable-list__item--selected" : "";
@@ -93,4 +67,56 @@ export default class EditableListItem extends Component {
</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);
}
}
}

View File

@@ -1,13 +0,0 @@
export default {
mediaEnabled: true
, mediaSyncElement: false
, mediaStopOnUnload: false
, localMediaEnabled: true
, localMediaServerPort: 9555
, mirroringEnabled: false
, mirroringAppId: MIRRORING_APP_ID
, userAgentWhitelistEnabled: true
, userAgentWhitelist: [
"https://www.netflix.com/*"
]
}

View File

@@ -0,0 +1,28 @@
"use strict";
export interface Options {
[ key: string ]: any;
mediaEnabled: boolean;
mediaSyncElement: boolean;
mediaStopOnUnload: boolean;
localMediaEnabled: boolean;
localMediaServerPort: number;
mirroringEnabled: boolean;
mirroringAppId: string;
userAgentWhitelistEnabled: boolean;
userAgentWhitelist: string[];
}
export default {
mediaEnabled: true
, mediaSyncElement: false
, mediaStopOnUnload: false
, localMediaEnabled: true
, localMediaServerPort: 9555
, mirroringEnabled: false
, mirroringAppId: MIRRORING_APP_ID
, userAgentWhitelistEnabled: true
, userAgentWhitelist: [
"https://www.netflix.com/*"
]
} as Options;

View File

@@ -1,12 +1,13 @@
/* tslint:disable:max-line-length */
"use strict";
import React, { Component } from "react";
import ReactDOM from "react-dom";
import defaultOptions from "./defaultOptions";
import defaultOptions, { Options } from "./defaultOptions";
import EditableList from "./EditableList";
import Bridge from "./Bridge";
import EditableList from "./EditableList";
import getBridgeInfo from "../lib/getBridgeInfo";
@@ -27,7 +28,7 @@ browser.runtime.getPlatformInfo()
const MATCH_PATTERN_REGEX = /^(?:(?:(\*|https?|ftp):\/\/((?:\*\.|[^\/\*])+)|(file):\/\/\/?(?:\*\.|[^\/\*])+)(\/.*)|<all_urls>)$/;
function getInputValue (input) {
function getInputValue (input: HTMLInputElement) {
switch (input.type) {
case "checkbox":
return input.checked;
@@ -39,8 +40,21 @@ function getInputValue (input) {
}
}
class App extends Component {
constructor (props) {
interface OptionsAppState {
hasLoaded: boolean;
options: Options;
bridgeInfo: any;
platform: string;
bridgeLoading: boolean;
isFormValid: boolean;
hasSaved: boolean;
}
class App extends Component<{}, OptionsAppState> {
private form: HTMLFormElement;
constructor (props: {}) {
super(props);
this.state = {
@@ -72,105 +86,11 @@ class App extends Component {
});
const bridgeInfo = await getBridgeInfo();
const platform = await browser.runtime.getPlatformInfo();
this.setState({
bridgeInfo
, platform: platform.os
, bridgeLoading: false
})
}
/**
* Set stored option values to current state
*/
setStorage () {
return browser.storage.sync.set({
options: this.state.options
});
}
handleReset () {
this.setState({
options: { ...defaultOptions }
});
}
async handleFormSubmit (ev) {
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) {}
}
handleFormChange (ev) {
ev.preventDefault();
this.setState({
isFormValid: this.form.checkValidity()
});
}
handleInputChange (ev) {
const { target } = ev;
this.setState(({ options }) => {
options[target.name] = getInputValue(target);
return { options };
});
}
handleWhitelistChange (whitelist) {
this.setState(({ options }) => {
options.userAgentWhitelist = whitelist;
return { options };
});
}
getWhitelistItemPatternError (info) {
return _("optionsUserAgentWhitelistInvalidMatchPattern", info);
}
async updateBridgeInfo () {
this.setState({
bridgeLoading: true
});
const bridgeInfo = await getBridgeInfo();
const { os } = await browser.runtime.getPlatformInfo();
this.setState({
bridgeInfo
, platform: os
, bridgeLoading: false
});
}
@@ -338,6 +258,102 @@ class App extends Component {
</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] = 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
});
}
}

View File

@@ -8,7 +8,7 @@ module.exports = (env) => ({
entry: {
"main" : `${env.includePath}/main.ts`
, "popup/bundle" : `${env.includePath}/popup/index.jsx`
, "options/bundle" : `${env.includePath}/options/index.jsx`
, "options/bundle" : `${env.includePath}/options/index.tsx`
, "updater/bundle" : `${env.includePath}/updater/index.jsx`
, "mediaCast" : `${env.includePath}/mediaCast.js`
, "mirroringCast" : `${env.includePath}/mirroringCast.js`

View File

@@ -13,6 +13,7 @@
"limit": 80
, "ignore-pattern": "//|.*\";$"
}]
, "member-access": [ true, "no-public" ]
, "no-console": [ true, "log" ]
, "no-namespace": [ true, "allow-declarations" ]
, "object-literal-sort-keys": false