diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index a776b10..f8ea43f 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -69,17 +69,32 @@ , "optionsBridgeNoAction": { "message": "No action needed." } - , "optionsBridgeDownloadsGet": { - "message": "Fetch downloads" + , "optionsBridgeUpdateCheck": { + "message": "Check for Updates" } - , "optionsBridgeDownloadsLoading": { - "message": "Fetching downloads$1" + , "optionsBridgeUpdateChecking": { + "message": "Checking for Updates$1" } - , "optionsBridgeDownloadsGetFailed": { - "message": "Failed to fetch downloads" + , "optionsBridgeUpdateStatusNoUpdates": { + "message": "No updates available" + } + , "optionsBridgeUpdateStatusError": { + "message": "Error checking for updates" + } + , "optionsBridgeUpdateAvailable": { + "message": "An update is available:" + } + , "optionsBridgeUpdatePackageTypeSelect": { + "message": "Select package type" } - , "optionsBridgeDownloadsTitle": { - "message": "Bridge downloads" + , "optionsBridgeUpdatePackageTypeDeb": { + "message": ".deb (Debian/Ubuntu)" + } + , "optionsBridgeUpdatePackageTypeRpm": { + "message": ".rpm (Fedora)" + } + , "optionsBridgeUpdate": { + "message": "Update Now..." } , "optionsMediaCategoryName": { @@ -167,4 +182,24 @@ , "optionsSaved": { "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..." + } } diff --git a/ext/src/lib/getBridgeInfo.js b/ext/src/lib/getBridgeInfo.js index 4f37fad..8f6b31b 100644 --- a/ext/src/lib/getBridgeInfo.js +++ b/ext/src/lib/getBridgeInfo.js @@ -10,7 +10,6 @@ export default async function getBridgeInfo () { applicationVersion = response.data; } catch (err) { - console.error(err) return null; } diff --git a/ext/src/options/Bridge.jsx b/ext/src/options/Bridge.jsx index 47d88ad..51f7ee5 100644 --- a/ext/src/options/Bridge.jsx +++ b/ext/src/options/Bridge.jsx @@ -1,4 +1,6 @@ import React, { Component } from "react"; +import semver from "semver"; + import { getNextEllipsis } from "../lib/utils"; const _ = browser.i18n.getMessage; @@ -91,28 +93,35 @@ export default class Bridge extends Component { constructor (props) { super(props); - this.onGetDownloads = this.onGetDownloads.bind(this); - this.onGetDownloadsResponse = this.onGetDownloadsResponse.bind(this); - this.onGetDownloadsError = this.onGetDownloadsError.bind(this); + 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 = { - downloads: null - , isLoadingDownloads: false - , wasErrorLoadingDownloads: false - , downloadsLoadingEllipsis: "..." + isCheckingUpdates: false + , isUpdateAvailable: false + , wasErrorCheckingUpdates: false + , checkUpdatesEllipsis: "..." + , updateStatus: null + , packageType: null }; } - onGetDownloads () { + onCheckUpdates () { this.setState({ - isLoadingDownloads: true + isCheckingUpdates: true }); const timeout = setInterval(() => { this.setState(state => ({ - downloadsLoadingEllipsis: getNextEllipsis( - state.downloadsLoadingEllipsis) + checkUpdatesEllipsis: getNextEllipsis( + state.checkUpdatesEllipsis) })); }, 500); @@ -121,46 +130,113 @@ export default class Bridge extends Component { window.clearTimeout(timeout); return res.json() }) - .then(this.onGetDownloadsResponse) - .catch(this.onGetDownloadsError); + .then(this.onCheckUpdatesResponse) + .catch(this.onCheckUpdatesError); } - 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 - }; + showUpdateStatus () { + if (this.updateStatusTimeout) { + window.clearTimeout(this.updateStatusTimeout); + } + this.updateStatusTimeout = window.setTimeout(() => { + this.setState({ + updateStatus: null + }); + }, 1500); + } - const platformExtensions = { - "exe": "win" - , "pkg": "mac" - , "deb": "deb" - , "rpm": "rpm" - }; + async onUpdate () { + const width = 400; + const height = 150; - const fileExtension = asset.name.match(/.*\.(.*)$/).pop(); + // Current window to base centered position on + const win = await browser.windows.getCurrent(); - if (fileExtension in platformExtensions) { - const platform = platformExtensions[fileExtension]; - acc[platform] = download; - } + // Top(mid)-center position + const centerX = win.left + (win.width / 2); + const centerY = win.top + (win.height / 3); - return acc; - }, { platform: platformInfo.os }); + const left = Math.floor(centerX - (width / 2)); + const top = Math.floor(centerY - (height / 2)); - this.setState({ - isLoadingDownloads: false - , downloads + const updaterPopup = await browser.windows.create({ + url: "../updater/index.html" + , 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({ - isLoadingDownloads: false - , wasErrorLoadingDownloads: true + 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 }); } @@ -224,35 +300,65 @@ export default class Bridge extends Component { }} { do { - if (!this.props.loading - && (!this.props.info - || !this.props.info.isVersionCompatible)) { -
-

- { _("optionsBridgeDownloadsTitle") } -

+ if (!this.props.loading) { +
{ do { - if (this.state.downloads) { - - } else if (this.state.wasErrorLoadingDownloads) { -
- { _("optionsBridgeDownloadsGetFailed") } + if (this.state.isUpdateAvailable) { +
+

+ { _("optionsBridgeUpdateAvailable") } +

+
+ { do { + if (this.props.platform === "linux") { + + } + }} + +
} else { - } }} + +
+ { do { + if (this.state.updateStatus + && !this.state.isUpdateAvailable) { + this.state.updateStatus; + } + }} +
} }} diff --git a/ext/src/options/index.jsx b/ext/src/options/index.jsx index eafb571..23f6b7c 100644 --- a/ext/src/options/index.jsx +++ b/ext/src/options/index.jsx @@ -46,6 +46,7 @@ class App extends Component { this.state = { options: props.options , bridgeInfo: null + , platform: null , bridgeLoading: true , isFormValid: true , hasSaved: false @@ -63,8 +64,10 @@ class App extends Component { async componentDidMount () { const bridgeInfo = await getBridgeInfo(); + const platform = await browser.runtime.getPlatformInfo(); this.setState({ bridgeInfo + , platform: platform.os , bridgeLoading: false }); } @@ -168,6 +171,7 @@ class App extends Component { return (
{ this.form = form; }} diff --git a/ext/src/options/styles/index.css b/ext/src/options/styles/index.css index 846de40..b13df43 100644 --- a/ext/src/options/styles/index.css +++ b/ext/src/options/styles/index.css @@ -90,18 +90,29 @@ } .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 { 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; @@ -113,41 +124,36 @@ font-weight: 500; padding-inline-end: 10px; text-align: end; - white-space: nowrap; vertical-align: top; + white-space: nowrap; } -.bridge__download-info { +.bridge__update-info { + align-items: center; + display: flex; margin-top: 30px; - display: flex; +} + +.bridge__update-label { + display: inline-block; + margin: initial; +} + +.bridge__update-options { + display: inline-flex; flex-direction: column; - width: -moz-available; + margin-left: 10px; } -.bridge__download-info-get { - align-self: flex-start; - justify-content: center; +.bridge__update-start { + align-self: flex-end; + margin-top: 5px; } -.bridge__download-info-title { - font-weight: 500; - font-size: 1.25em; +.bridge--update-status { + margin-left: 10px; } -.bridge-downloads { - display: flex; -} - -.bridge-downloads__download { - -} - -.bridge-downloads__linux { - display: flex; - flex-direction: column; -} - - .category { border: initial; diff --git a/ext/src/options/styles/mac.css b/ext/src/options/styles/mac.css index c28f803..e04b317 100644 --- a/ext/src/options/styles/mac.css +++ b/ext/src/options/styles/mac.css @@ -18,6 +18,7 @@ button[default]:not(:hover):active { color: ButtonText; } -button { +button, +select { height: 22px; } diff --git a/ext/src/updater/index.html b/ext/src/updater/index.html new file mode 100644 index 0000000..5e9b10e --- /dev/null +++ b/ext/src/updater/index.html @@ -0,0 +1,11 @@ + + + + + + + + +
+ + diff --git a/ext/src/updater/index.jsx b/ext/src/updater/index.jsx new file mode 100644 index 0000000..037061a --- /dev/null +++ b/ext/src/updater/index.jsx @@ -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) => ( +
+
+ { props.description } +
+
+ { props.additionalDescription } +
+ + + + +
+); + +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) { + + } + } + } +} + +ReactDOM.render( + + , document.querySelector("#root")); diff --git a/ext/src/updater/styles/index.css b/ext/src/updater/styles/index.css new file mode 100755 index 0000000..0595c86 --- /dev/null +++ b/ext/src/updater/styles/index.css @@ -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; +} diff --git a/ext/src/updater/styles/mac.css b/ext/src/updater/styles/mac.css new file mode 100755 index 0000000..e146c75 --- /dev/null +++ b/ext/src/updater/styles/mac.css @@ -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; +} diff --git a/ext/webpack.config.js b/ext/webpack.config.js index 55a6ec0..84582ea 100755 --- a/ext/webpack.config.js +++ b/ext/webpack.config.js @@ -10,6 +10,7 @@ module.exports = (env) => ({ , "popup/bundle" : `${env.includePath}/popup/index.jsx` , "options/bundle" : `${env.includePath}/options/index.jsx` , "shim/bundle" : `${env.includePath}/shim/index.js` + , "updater/bundle" : `${env.includePath}/updater/index.jsx` , "content" : `${env.includePath}/content.js` , "contentSetup" : `${env.includePath}/contentSetup.js` , "mediaCast" : `${env.includePath}/mediaCast.js`