mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-09 17:19:59 +00:00
Convert ext options to typescript
This commit is contained in:
13
ext/src/global.d.ts
vendored
13
ext/src/global.d.ts
vendored
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/*"
|
||||
]
|
||||
}
|
||||
28
ext/src/options/defaultOptions.ts
Normal file
28
ext/src/options/defaultOptions.ts
Normal 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;
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user