Replace downloads list with updater tool

This commit is contained in:
hensm
2019-02-14 05:00:03 +00:00
parent d4d55ea59e
commit 98bd2f2ab3
11 changed files with 524 additions and 94 deletions

View File

@@ -69,17 +69,32 @@
, "optionsBridgeNoAction": { , "optionsBridgeNoAction": {
"message": "No action needed." "message": "No action needed."
} }
, "optionsBridgeDownloadsGet": { , "optionsBridgeUpdateCheck": {
"message": "Fetch downloads" "message": "Check for Updates"
} }
, "optionsBridgeDownloadsLoading": { , "optionsBridgeUpdateChecking": {
"message": "Fetching downloads$1" "message": "Checking for Updates$1"
} }
, "optionsBridgeDownloadsGetFailed": { , "optionsBridgeUpdateStatusNoUpdates": {
"message": "Failed to fetch downloads" "message": "No updates available"
}
, "optionsBridgeUpdateStatusError": {
"message": "Error checking for updates"
}
, "optionsBridgeUpdateAvailable": {
"message": "An update is available:"
}
, "optionsBridgeUpdatePackageTypeSelect": {
"message": "Select package type"
} }
, "optionsBridgeDownloadsTitle": { , "optionsBridgeUpdatePackageTypeDeb": {
"message": "Bridge downloads" "message": ".deb (Debian/Ubuntu)"
}
, "optionsBridgeUpdatePackageTypeRpm": {
"message": ".rpm (Fedora)"
}
, "optionsBridgeUpdate": {
"message": "Update Now..."
} }
, "optionsMediaCategoryName": { , "optionsMediaCategoryName": {
@@ -167,4 +182,24 @@
, "optionsSaved": { , "optionsSaved": {
"message": "Saved!" "message": "Saved!"
} }
, "updaterCancel": {
"message": "Cancel"
}
, "updaterDescriptionDownloading": {
"message": "Downloading..."
}
, "updaterAdditionalDescriptionDownloading": {
"message": "Fetching update package"
}
, "updaterDescriptionInstallReady": {
"message": "Ready to install"
}
, "updaterAdditionalDescriptionInstallReady": {
"message": "The install package has been downloaded to your default downloads location."
}
, "updaterInstall": {
"message": "Install..."
}
} }

View File

@@ -10,7 +10,6 @@ export default async function getBridgeInfo () {
applicationVersion = response.data; applicationVersion = response.data;
} catch (err) { } catch (err) {
console.error(err)
return null; return null;
} }

View File

@@ -1,4 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import semver from "semver";
import { getNextEllipsis } from "../lib/utils"; import { getNextEllipsis } from "../lib/utils";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
@@ -91,28 +93,35 @@ export default class Bridge extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.onGetDownloads = this.onGetDownloads.bind(this); this.onCheckUpdates = this.onCheckUpdates.bind(this);
this.onGetDownloadsResponse = this.onGetDownloadsResponse.bind(this); this.onCheckUpdatesResponse = this.onCheckUpdatesResponse.bind(this);
this.onGetDownloadsError = this.onGetDownloadsError.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 = { this.state = {
downloads: null isCheckingUpdates: false
, isLoadingDownloads: false , isUpdateAvailable: false
, wasErrorLoadingDownloads: false , wasErrorCheckingUpdates: false
, downloadsLoadingEllipsis: "..." , checkUpdatesEllipsis: "..."
, updateStatus: null
, packageType: null
}; };
} }
onGetDownloads () { onCheckUpdates () {
this.setState({ this.setState({
isLoadingDownloads: true isCheckingUpdates: true
}); });
const timeout = setInterval(() => { const timeout = setInterval(() => {
this.setState(state => ({ this.setState(state => ({
downloadsLoadingEllipsis: getNextEllipsis( checkUpdatesEllipsis: getNextEllipsis(
state.downloadsLoadingEllipsis) state.checkUpdatesEllipsis)
})); }));
}, 500); }, 500);
@@ -121,46 +130,113 @@ export default class Bridge extends Component {
window.clearTimeout(timeout); window.clearTimeout(timeout);
return res.json() return res.json()
}) })
.then(this.onGetDownloadsResponse) .then(this.onCheckUpdatesResponse)
.catch(this.onGetDownloadsError); .catch(this.onCheckUpdatesError);
} }
async onGetDownloadsResponse (res) { showUpdateStatus () {
const platformInfo = await browser.runtime.getPlatformInfo(); if (this.updateStatusTimeout) {
const downloads = res.assets window.clearTimeout(this.updateStatusTimeout);
.reduce((acc, asset) => { }
const download = { this.updateStatusTimeout = window.setTimeout(() => {
name: asset.name this.setState({
, url: asset.browser_download_url updateStatus: null
}; });
}, 1500);
}
const platformExtensions = { async onUpdate () {
"exe": "win" const width = 400;
, "pkg": "mac" const height = 150;
, "deb": "deb"
, "rpm": "rpm"
};
const fileExtension = asset.name.match(/.*\.(.*)$/).pop(); // Current window to base centered position on
const win = await browser.windows.getCurrent();
if (fileExtension in platformExtensions) { // Top(mid)-center position
const platform = platformExtensions[fileExtension]; const centerX = win.left + (win.width / 2);
acc[platform] = download; const centerY = win.top + (win.height / 3);
}
return acc; const left = Math.floor(centerX - (width / 2));
}, { platform: platformInfo.os }); const top = Math.floor(centerY - (height / 2));
this.setState({ const updaterPopup = await browser.windows.create({
isLoadingDownloads: false url: "../updater/index.html"
, downloads , type: "popup"
, width
, height
, left
, top
});
// Size/position not set correctly on creation (bug?)
await browser.windows.update(updaterPopup.id, {
width
, height
, left
, top
});
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);
});
}
}); });
} }
onGetDownloadsError (err) {
async onCheckUpdatesResponse (res) {
const isUpdateAvailable = !this.props.info || semver.lt(
this.props.info.version, res.tag_name);
if (isUpdateAvailable) {
this.updateData = res;
}
this.setState({ this.setState({
isLoadingDownloads: false isCheckingUpdates: false
, wasErrorLoadingDownloads: true , 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
}); });
} }
@@ -224,35 +300,65 @@ export default class Bridge extends Component {
}} }}
{ do { { do {
if (!this.props.loading if (!this.props.loading) {
&& (!this.props.info <div className="bridge__update-info">
|| !this.props.info.isVersionCompatible)) {
<div className="bridge__download-info">
<h2 className="bridge__download-info-title">
{ _("optionsBridgeDownloadsTitle") }
</h2>
{ do { { do {
if (this.state.downloads) { if (this.state.isUpdateAvailable) {
<BridgeDownloads info={ this.state.downloads }/> <div className="bridge__update">
} else if (this.state.wasErrorLoadingDownloads) { <p className="bridge__update-label">
<div className="bridge__download-info-get-error"> { _("optionsBridgeUpdateAvailable") }
{ _("optionsBridgeDownloadsGetFailed") } </p>
<div className="bridge__update-options">
{ do {
if (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> </div>
} else { } else {
<button className="bridge__download-info-get" <button className="bridge__update-check"
onClick={ this.onGetDownloads } disabled={ this.state.isCheckingUpdates }
disabled={ this.state.isLoadingDownloads }> onClick={ this.onCheckUpdates }>
{ do { { do {
if (this.state.isLoadingDownloads) { if (this.state.isCheckingUpdates) {
_("optionsBridgeDownloadsLoading" _("optionsBridgeUpdateChecking"
, getNextEllipsis(this.state.downloadsLoadingEllipsis)); , getNextEllipsis(this.state.checkUpdatesEllipsis));
} else { } else {
_("optionsBridgeDownloadsGet"); _("optionsBridgeUpdateCheck");
} }
}} }}
</button> </button>
} }
}} }}
<div className="bridge--update-status">
{ do {
if (this.state.updateStatus
&& !this.state.isUpdateAvailable) {
this.state.updateStatus;
}
}}
</div>
</div> </div>
} }
}} }}

View File

@@ -46,6 +46,7 @@ class App extends Component {
this.state = { this.state = {
options: props.options options: props.options
, bridgeInfo: null , bridgeInfo: null
, platform: null
, bridgeLoading: true , bridgeLoading: true
, isFormValid: true , isFormValid: true
, hasSaved: false , hasSaved: false
@@ -63,8 +64,10 @@ class App extends Component {
async componentDidMount () { async componentDidMount () {
const bridgeInfo = await getBridgeInfo(); const bridgeInfo = await getBridgeInfo();
const platform = await browser.runtime.getPlatformInfo();
this.setState({ this.setState({
bridgeInfo bridgeInfo
, platform: platform.os
, bridgeLoading: false , bridgeLoading: false
}); });
} }
@@ -168,6 +171,7 @@ class App extends Component {
return ( return (
<div> <div>
<Bridge info={ this.state.bridgeInfo } <Bridge info={ this.state.bridgeInfo }
platform={ this.state.platform }
loading={ this.state.bridgeLoading } /> loading={ this.state.bridgeLoading } />
<form id="form" ref={ form => { this.form = form; }} <form id="form" ref={ form => { this.form = form; }}

View File

@@ -90,18 +90,29 @@
} }
.bridge__info--not-found .bridge__status { .bridge__info--not-found .bridge__status {
flex-direction: row; 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 { .bridge__info--found .bridge__status-icon {
margin-block-end: 5px; margin-block-end: 5px;
} }
.bridge__info--not-found .bridge__status-icon { .bridge__info--not-found .bridge__status-icon {
grid-area: status-icon;
margin-inline-end: 10px; margin-inline-end: 10px;
} }
.bridge__info--not-found .bridge__status-title { .bridge__info--not-found .bridge__status-title {
grid-area: status-title;
font-weight: normal; font-weight: normal;
white-space: normal; white-space: normal;
} }
.bridge__info--not-found .bridge__status-text {
grid-area: status-text;
margin-top: initial;
}
.bridge__stats { .bridge__stats {
border-collapse: collapse; border-collapse: collapse;
@@ -113,41 +124,36 @@
font-weight: 500; font-weight: 500;
padding-inline-end: 10px; padding-inline-end: 10px;
text-align: end; text-align: end;
white-space: nowrap;
vertical-align: top; vertical-align: top;
white-space: nowrap;
} }
.bridge__download-info { .bridge__update-info {
align-items: center;
display: flex;
margin-top: 30px; margin-top: 30px;
display: flex; }
.bridge__update-label {
display: inline-block;
margin: initial;
}
.bridge__update-options {
display: inline-flex;
flex-direction: column; flex-direction: column;
width: -moz-available; margin-left: 10px;
} }
.bridge__download-info-get { .bridge__update-start {
align-self: flex-start; align-self: flex-end;
justify-content: center; margin-top: 5px;
} }
.bridge__download-info-title { .bridge--update-status {
font-weight: 500; margin-left: 10px;
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;

View File

@@ -18,6 +18,7 @@ button[default]:not(:hover):active {
color: ButtonText; color: ButtonText;
} }
button { button,
select {
height: 22px; height: 22px;
} }

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>

216
ext/src/updater/index.jsx Normal file
View File

@@ -0,0 +1,216 @@
"use strict";
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { getNextEllipsis } 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);
}
});
const Updater = (props) => (
<div className="updater">
<div className="updater__description">
{ props.description }
</div>
<div className="updater__additional-description">
{ props.additionalDescription }
</div>
<progress className="updater__progress"
max={ props.downloadTotal }
value={ props.downloadCurrent }>
</progress>
<button className="updater__install"
onClick={ props.onInstall }
disabled={ props.isDownloading }>
{ _("updaterInstall") }
</button>
<button className="updater__cancel"
onClick={ props.onCancel }
disabled={ !props.isDownloading }>
{ _("updaterCancel") }
</button>
</div>
);
class App extends Component {
constructor (props) {
super(props);
this.downloadId = null;
this.downloadProgressInterval = null;
this.onMessage = this.onMessage.bind(this);
this.onDownloadChanged = this.onDownloadChanged.bind(this);
this.updateDownloadProgress = this.updateDownloadProgress.bind(this);
this.onCancel = this.onCancel.bind(this);
this.onInstall = this.onInstall.bind(this);
this.state = {
hasLoaded: false
, isDownloading: true
, description: _("updaterDescriptionDownloading")
, additionalDescription: _("updaterAdditionalDescriptionDownloading")
, downloadTotal: 0
, downloadCurrent: 0
};
}
async componentDidMount () {
this.port = browser.runtime.connect({
name: "updater"
});
this.port.onMessage.addListener(this.onMessage);
browser.downloads.onChanged.addListener(this.onDownloadChanged);
}
componentDidUpdate () {
// Size window to content
browser.windows.update(this.win.id, {
width: document.body.clientWidth + this.frameWidth
, height: document.body.clientHeight + this.frameHeight
});
}
closeWindow () {
window.clearInterval(this.downloadProgressInterval);
browser.downloads.onChanged.removeListener(this.onDownloadChanged);
this.port.onMessage.removeListener(this.onMessage);
this.port.disconnect();
}
async onMessage (message) {
switch (message.subject) {
case "updater:/updateData": {
// Only run once
if (this.downloadId) {
return;
}
this.win = await browser.windows.getCurrent();
this.frameWidth = this.win.width - window.innerWidth;
this.frameHeight = this.win.height - window.innerHeight;
// Cleanup
if (this.downloadId) {
browser.downloads.cancel(this.downloadId);
}
if (this.downloadProgressInterval) {
window.clearInterval(this.downloadProgressInterval);
}
this.downloadId = await browser.downloads.download({
url: message.data.browser_download_url
, filename: message.data.name
});
this.updateDownloadProgress();
this.downloadProgressInterval = window.setInterval(
this.updateDownloadProgress, 500);
this.setState({
hasLoaded: true
, isDownloading: true
});
break;
};
}
}
onDownloadChanged (downloadItem) {
if (downloadItem.id !== this.downloadId) {
return;
}
if (downloadItem.canResume) {
// Paused
if (downloadItem.canResume.current) {
window.clearInterval(this.downloadProgressInterval);
this.setState({
isDownloading: false
});
// Cancelled
} else {
window.clearInterval(this.downloadProgressInterval);
this.setState({
isDownloading: false
});
}
// Download finished
} else if (downloadItem.state
&& downloadItem.state.current === "complete") {
window.clearInterval(this.downloadProgressInterval);
this.setState({
isDownloading: false
, downloadTotal: 1
, downloadCurrent: 1
, description: _("updaterDescriptionInstallReady")
, additionalDescription: _("updaterAdditionalDescriptionInstallReady")
});
}
}
async updateDownloadProgress () {
const [ download ] = await browser.downloads.search({
id: this.downloadId
});
this.setState({
downloadTotal: download.totalBytes
, downloadCurrent: download.bytesReceived
});
}
async onCancel () {
try {
await browser.downloads.cancel(this.downloadId);
this.closeWindow();
} catch (err) {
// Already cancelled or finished
}
}
async onInstall () {
try {
await browser.downloads.open(this.downloadId);
this.closeWindow();
} catch (err) {
// Cancelled or not finished
}
}
render () {
return do {
if (this.state.hasLoaded) {
<Updater description={ this.state.description }
additionalDescription={ this.state.additionalDescription }
downloadTotal={ this.state.downloadTotal }
downloadCurrent={ this.state.downloadCurrent }
isDownloading={ this.state.isDownloading }
onCancel={ this.onCancel }
onInstall={ this.onInstall } />
}
}
}
}
ReactDOM.render(
<App />
, document.querySelector("#root"));

View File

@@ -0,0 +1,38 @@
body {
background: -moz-dialog;
color: -moz-dialogtext;
margin: initial;
font: message-box;
}
.updater {
align-items: center;
display: grid;
gap: 0.75em;
grid-template-rows: min-content min-content 1fr min-content;
grid-template-columns: 1fr min-content min-content;
grid-template-areas:
"description description description"
"additional-description additional-description additional-description"
"progress progress progress"
". cancel install";
padding: 0.75em;
}
.updater__description {
grid-area: description;
}
.updater__additional-description {
font-size: 0.9em;
grid-area: additional-description;
margin-top: -0.75em;
}
.updater__progress {
grid-area: progress;
}
.updater__install {
grid-area: install;
}
.updater__cancel {
grid-area: cancel;
}

13
ext/src/updater/styles/mac.css Executable file
View File

@@ -0,0 +1,13 @@
body {
background: rgb(236, 236, 236);
font: menu;
}
button,
select {
font: inherit;
}
button:not([disabled]):hover:active {
color: -moz-mac-buttonactivetext;
}

View File

@@ -10,6 +10,7 @@ module.exports = (env) => ({
, "popup/bundle" : `${env.includePath}/popup/index.jsx` , "popup/bundle" : `${env.includePath}/popup/index.jsx`
, "options/bundle" : `${env.includePath}/options/index.jsx` , "options/bundle" : `${env.includePath}/options/index.jsx`
, "shim/bundle" : `${env.includePath}/shim/index.js` , "shim/bundle" : `${env.includePath}/shim/index.js`
, "updater/bundle" : `${env.includePath}/updater/index.jsx`
, "content" : `${env.includePath}/content.js` , "content" : `${env.includePath}/content.js`
, "contentSetup" : `${env.includePath}/contentSetup.js` , "contentSetup" : `${env.includePath}/contentSetup.js`
, "mediaCast" : `${env.includePath}/mediaCast.js` , "mediaCast" : `${env.includePath}/mediaCast.js`