Add bridge downloads to options page

This commit is contained in:
hensm
2019-02-12 09:06:57 +00:00
parent da17c6df0d
commit d4d55ea59e
7 changed files with 310 additions and 86 deletions

View File

@@ -21,14 +21,17 @@
, "optionsBridgeLoading": { , "optionsBridgeLoading": {
"message": "Loading bridge info..." "message": "Loading bridge info..."
} }
, "optionsBridgeFoundStatusText": { , "optionsBridgeFoundStatusTitle": {
"message": "Bridge found" "message": "Bridge found"
} }
, "optionsBridgeIssueStatusText": { , "optionsBridgeIssueStatusTitle": {
"message": "Bridge issue" "message": "Bridge issue"
} }
, "optionsBridgeNotFoundStatusTitle": {
"message": "Bridge not found"
}
, "optionsBridgeNotFoundStatusText": { , "optionsBridgeNotFoundStatusText": {
"message": "Bridge not found. Try downloading and installing the latest version." "message": "Try downloading and installing the latest version."
} }
, "optionsBridgeInfo": { , "optionsBridgeInfo": {
"message": "Bridge info:" "message": "Bridge info:"
@@ -66,6 +69,18 @@
, "optionsBridgeNoAction": { , "optionsBridgeNoAction": {
"message": "No action needed." "message": "No action needed."
} }
, "optionsBridgeDownloadsGet": {
"message": "Fetch downloads"
}
, "optionsBridgeDownloadsLoading": {
"message": "Fetching downloads$1"
}
, "optionsBridgeDownloadsGetFailed": {
"message": "Failed to fetch downloads"
}
, "optionsBridgeDownloadsTitle": {
"message": "Bridge downloads"
}
, "optionsMediaCategoryName": { , "optionsMediaCategoryName": {
"message": "Media casting" "message": "Media casting"

10
ext/src/lib/utils.js Normal file
View File

@@ -0,0 +1,10 @@
"use strict";
export function getNextEllipsis (ellipsis) {
return do {
if (ellipsis === "") ".";
else if (ellipsis === ".") "..";
else if (ellipsis === "..") "...";
else if (ellipsis === "...") "";
};
}

View File

@@ -31,7 +31,9 @@
"page": "options/index.html" "page": "options/index.html"
} }
, "permissions": [ , "permissions": [
"menus" "downloads"
, "downloads.open"
, "menus"
, "nativeMessaging" , "nativeMessaging"
, "storage" , "storage"
, "webNavigation" , "webNavigation"

View File

@@ -1,7 +1,45 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { getNextEllipsis } from "../lib/utils";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
const ENDPOINT_URL = "https://api.github.com/repos/hensm/fx_cast/releases/14720978";
async function downloadApp (info, platform) {
const download = browser.downloads.download({
filename: info[platform].name
, url: info[platform].url
});
}
const BridgeDownloads = (props) => (
<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>
);
const BridgeStats = (props) => ( const BridgeStats = (props) => (
<table className="bridge__stats"> <table className="bridge__stats">
<tr> <tr>
@@ -49,54 +87,176 @@ const BridgeStats = (props) => (
</table> </table>
); );
export default (props) => ( export default class Bridge extends Component {
<div className="bridge"> constructor (props) {
{ do { super(props);
if (props.loading) {
<div className="bridge__loading">
{ _("optionsBridgeLoading") }
<progress></progress>
</div>
} else {
const infoClasses = `bridge__info ${props.info
? "bridge__info--found"
: "bridge__info--not-found"}`;
const [ statusIcon, statusText ] = do { this.onGetDownloads = this.onGetDownloads.bind(this);
if (!props.info) { this.onGetDownloadsResponse = this.onGetDownloadsResponse.bind(this);
[ "assets/icons8-cancel-120.png" this.onGetDownloadsError = this.onGetDownloadsError.bind(this);
, _("optionsBridgeNotFoundStatusText") ]
} else { this.state = {
if (props.info.isVersionCompatible) { downloads: null
[ "assets/icons8-ok-120.png" , isLoadingDownloads: false
, _("optionsBridgeFoundStatusText") ] , wasErrorLoadingDownloads: false
} else { , downloadsLoadingEllipsis: "..."
[ "assets/icons8-warn-120.png" };
, _("optionsBridgeIssueStatusText") ] }
}
}
onGetDownloads () {
this.setState({
isLoadingDownloads: true
});
const timeout = setInterval(() => {
this.setState(state => ({
downloadsLoadingEllipsis: getNextEllipsis(
state.downloadsLoadingEllipsis)
}));
}, 500);
fetch(ENDPOINT_URL)
.then(res => {
window.clearTimeout(timeout);
return res.json()
})
.then(this.onGetDownloadsResponse)
.catch(this.onGetDownloadsError);
}
async onGetDownloadsResponse (res) {
const platformInfo = await browser.runtime.getPlatformInfo();
const downloads = res.assets
.reduce((acc, asset) => {
const download = {
name: asset.name
, url: asset.browser_download_url
}; };
<div className={infoClasses}> const platformExtensions = {
<div className="bridge__status"> "exe": "win"
<img className="bridge__status-icon" , "pkg": "mac"
width="60" height="60" , "deb": "deb"
src={ statusIcon } /> , "rpm": "rpm"
};
<h2 className="bridge__status-text"> const fileExtension = asset.name.match(/.*\.(.*)$/).pop();
{ statusText }
</h2>
</div>
{ do { if (fileExtension in platformExtensions) {
if (props.info) { const platform = platformExtensions[fileExtension];
<BridgeStats info={ props.info }/> acc[platform] = download;
} else { }
// TODO: Download links
} return acc;
}} }, { platform: platformInfo.os });
</div>
} this.setState({
}} isLoadingDownloads: false
</div> , downloads
); });
}
onGetDownloadsError (err) {
this.setState({
isLoadingDownloads: false
, wasErrorLoadingDownloads: true
});
}
render () {
return (
<div className="bridge">
{ do {
if (this.props.loading) {
<div className="bridge__loading">
{ _("optionsBridgeLoading") }
<progress></progress>
</div>
} else {
const infoClasses = `bridge__info ${this.props.info
? "bridge__info--found"
: "bridge__info--not-found"}`;
const [ statusIcon, statusTitle, statusText ] = do {
if (!this.props.info) {
[ "assets/icons8-cancel-120.png"
, _("optionsBridgeNotFoundStatusTitle")
, _("optionsBridgeNotFoundStatusText") ]
} else {
if (this.props.info.isVersionCompatible) {
[ "assets/icons8-ok-120.png"
, _("optionsBridgeFoundStatusTitle") ]
} else {
[ "assets/icons8-warn-120.png"
, _("optionsBridgeIssueStatusTitle") ]
}
}
};
<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>
{ do {
if (statusText) {
<p className="bridge__status-text">
{ statusText }
</p>
}
}}
</div>
{ do {
if (this.props.info) {
<BridgeStats info={ this.props.info }/>
}
}}
</div>
}
}}
{ do {
if (!this.props.loading
&& (!this.props.info
|| !this.props.info.isVersionCompatible)) {
<div className="bridge__download-info">
<h2 className="bridge__download-info-title">
{ _("optionsBridgeDownloadsTitle") }
</h2>
{ do {
if (this.state.downloads) {
<BridgeDownloads info={ this.state.downloads }/>
} else if (this.state.wasErrorLoadingDownloads) {
<div className="bridge__download-info-get-error">
{ _("optionsBridgeDownloadsGetFailed") }
</div>
} else {
<button className="bridge__download-info-get"
onClick={ this.onGetDownloads }
disabled={ this.state.isLoadingDownloads }>
{ do {
if (this.state.isLoadingDownloads) {
_("optionsBridgeDownloadsLoading"
, getNextEllipsis(this.state.downloadsLoadingEllipsis));
} else {
_("optionsBridgeDownloadsGet");
}
}}
</button>
}
}}
</div>
}
}}
</div>
);
}
}

View File

@@ -1,5 +1,6 @@
:root { :root {
--border-color: rgb(225, 225, 225);; --border-color: rgb(225, 225, 225);
--secondary-color: rgb(125, 125, 125);
} }
*:invalid { *:invalid {
@@ -23,7 +24,7 @@
} }
#status-line { #status-line {
color: graytext; color: var(--secondary-color);
} }
@@ -73,13 +74,21 @@
padding-inline-end: 25px; padding-inline-end: 25px;
} }
.bridge__status-text { .bridge__status-title {
margin: initial; margin: initial;
font-weight: 500; font-weight: 500;
font-size: 1.5em; font-size: 1.5em;
white-space: nowrap; 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 { .bridge__info--not-found .bridge__status {
flex-direction: row; flex-direction: row;
} }
@@ -89,7 +98,7 @@
.bridge__info--not-found .bridge__status-icon { .bridge__info--not-found .bridge__status-icon {
margin-inline-end: 10px; margin-inline-end: 10px;
} }
.bridge__info--not-found .bridge__status-text { .bridge__info--not-found .bridge__status-title {
font-weight: normal; font-weight: normal;
white-space: normal; white-space: normal;
} }
@@ -108,6 +117,37 @@
vertical-align: top; vertical-align: top;
} }
.bridge__download-info {
margin-top: 30px;
display: flex;
flex-direction: column;
width: -moz-available;
}
.bridge__download-info-get {
align-self: flex-start;
justify-content: center;
}
.bridge__download-info-title {
font-weight: 500;
font-size: 1.25em;
}
.bridge-downloads {
display: flex;
}
.bridge-downloads__download {
}
.bridge-downloads__linux {
display: flex;
flex-direction: column;
}
.category { .category {
border: initial; border: initial;
@@ -120,7 +160,7 @@
} }
.category:disabled { .category:disabled {
color: graytext; color: var(--secondary-color);
} }
#form > .category:not(:first-child) { #form > .category:not(:first-child) {
@@ -148,7 +188,7 @@
} }
.category__description { .category__description {
color: graytext; color: var(--secondary-color);
margin-top: initial; margin-top: initial;
max-width: 60ch; max-width: 60ch;
} }

View File

@@ -3,6 +3,8 @@
import React, { Component } from "react"; import React, { Component } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { getNextEllipsis } from "../lib/utils";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
// macOS styles // macOS styles
@@ -168,14 +170,9 @@ class Receiver extends Component {
}); });
setInterval(() => { setInterval(() => {
this.setState({ this.setState(state => ({
ellipsis: do { ellipsis: getNextEllipsis(state.ellipsis)
if (this.state.ellipsis === "") "."; }));
else if (this.state.ellipsis === ".") "..";
else if (this.state.ellipsis === "..") "...";
else if (this.state.ellipsis === "...") "";
}
});
}, 500); }, 500);
} }