diff --git a/lib/gui/app/components/featured-project/featured-project.tsx b/lib/gui/app/components/featured-project/featured-project.tsx deleted file mode 100644 index 9ed36346..00000000 --- a/lib/gui/app/components/featured-project/featured-project.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as React from 'react'; - -import * as settings from '../../models/settings'; -import * as analytics from '../../modules/analytics'; -import { SafeWebview } from '../safe-webview/safe-webview'; - -interface FeaturedProjectProps { - shouldShow: boolean; - onWebviewShow: (isWebviewShowing: boolean) => void; - style?: React.CSSProperties; -} - -interface FeaturedProjectState { - endpoint: string | null; - show: boolean; -} - -export class FeaturedProject extends React.Component< - FeaturedProjectProps, - FeaturedProjectState -> { - constructor(props: FeaturedProjectProps) { - super(props); - this.state = { - endpoint: null, - show: false, - }; - } - - public async componentDidMount() { - try { - const url = new URL( - (await settings.get('featuredProjectEndpoint')) || - 'https://assets.balena.io/etcher-featured/index.html', - ); - url.searchParams.append('borderRight', 'false'); - url.searchParams.append('darkBackground', 'true'); - this.setState({ endpoint: url.toString() }); - } catch (error) { - analytics.logException(error); - } - } - - public render() { - const { style = {} } = this.props; - return this.state.endpoint ? ( - - ) : null; - } -} diff --git a/lib/gui/app/components/safe-webview/safe-webview.tsx b/lib/gui/app/components/safe-webview/safe-webview.tsx index 961acc00..614ae11d 100644 --- a/lib/gui/app/components/safe-webview/safe-webview.tsx +++ b/lib/gui/app/components/safe-webview/safe-webview.tsx @@ -58,8 +58,6 @@ const API_VERSION = '2'; interface SafeWebviewProps { // The website source URL src: string; - // @summary Refresh the webview - refreshNow?: boolean; // Webview lifecycle event onWebviewShow?: (isWebviewShowing: boolean) => void; style?: React.CSSProperties; diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 2e15443e..7d8fa78f 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -30,6 +30,7 @@ import { Txt, Card as BaseCard, Input, + Spinner, } from 'rendition'; import styled from 'styled-components'; @@ -137,8 +138,9 @@ const URLSelector = ({ : 'OK'} done={async () => { setLoading(true); const urlStrings = recentImages.map((url: URL) => url.href); @@ -288,7 +290,7 @@ export class SourceSelector extends React.Component< await this.selectImageByPath({ imagePath, SourceType: isURL ? sourceDestination.Http : sourceDestination.File, - }); + }).promise; } private reselectImage() { @@ -344,74 +346,97 @@ export class SourceSelector extends React.Component< } } - private async selectImageByPath({ imagePath, SourceType }: SourceOptions) { - try { - imagePath = await replaceWindowsNetworkDriveLetter(imagePath); - } catch (error) { - analytics.logException(error); - } + private selectImageByPath({ + imagePath, + SourceType, + }: SourceOptions): { promise: Promise; cancel: () => void } { + let cancelled = false; + return { + cancel: () => { + cancelled = true; + }, + promise: (async () => { + try { + imagePath = await replaceWindowsNetworkDriveLetter(imagePath); + } catch (error) { + analytics.logException(error); + } + if (cancelled) { + return; + } - let source; - if (SourceType === sourceDestination.File) { - source = new sourceDestination.File({ - path: imagePath, - }); - } else { - if ( - !imagePath.startsWith('https://') && - !imagePath.startsWith('http://') - ) { - const invalidImageError = errors.createUserError({ - title: 'Unsupported protocol', - description: messages.error.unsupportedProtocol(), - }); + let source; + if (SourceType === sourceDestination.File) { + source = new sourceDestination.File({ + path: imagePath, + }); + } else { + if ( + !imagePath.startsWith('https://') && + !imagePath.startsWith('http://') + ) { + const invalidImageError = errors.createUserError({ + title: 'Unsupported protocol', + description: messages.error.unsupportedProtocol(), + }); - osDialog.showError(invalidImageError); - analytics.logEvent('Unsupported protocol', { path: imagePath }); - return; - } - source = new sourceDestination.Http({ url: imagePath }); - } + osDialog.showError(invalidImageError); + analytics.logEvent('Unsupported protocol', { path: imagePath }); + return; + } + source = new sourceDestination.Http({ url: imagePath }); + } - try { - const innerSource = await source.getInnerSource(); - const metadata = (await innerSource.getMetadata()) as sourceDestination.Metadata & { - hasMBR: boolean; - partitions: MBRPartition[] | GPTPartition[]; - path: string; - extension: string; - }; - const partitionTable = await innerSource.getPartitionTable(); - if (partitionTable) { - metadata.hasMBR = true; - metadata.partitions = partitionTable.partitions; - } else { - metadata.hasMBR = false; - } - metadata.path = imagePath; - metadata.extension = path.extname(imagePath).slice(1); - this.selectImage(metadata); - this.afterSelected({ - imagePath, - SourceType, - }); - } catch (error) { - const imageError = errors.createUserError({ - title: 'Error opening image', - description: messages.error.openImage( - path.basename(imagePath), - error.message, - ), - }); - osDialog.showError(imageError); - analytics.logException(error); - } finally { - try { - await source.close(); - } catch (error) { - // Noop - } - } + try { + const innerSource = await source.getInnerSource(); + if (cancelled) { + return; + } + const metadata = (await innerSource.getMetadata()) as sourceDestination.Metadata & { + hasMBR: boolean; + partitions: MBRPartition[] | GPTPartition[]; + path: string; + extension: string; + }; + if (cancelled) { + return; + } + const partitionTable = await innerSource.getPartitionTable(); + if (cancelled) { + return; + } + if (partitionTable) { + metadata.hasMBR = true; + metadata.partitions = partitionTable.partitions; + } else { + metadata.hasMBR = false; + } + metadata.path = imagePath; + metadata.extension = path.extname(imagePath).slice(1); + this.selectImage(metadata); + this.afterSelected({ + imagePath, + SourceType, + }); + } catch (error) { + const imageError = errors.createUserError({ + title: 'Error opening image', + description: messages.error.openImage( + path.basename(imagePath), + error.message, + ), + }); + osDialog.showError(imageError); + analytics.logException(error); + } finally { + try { + await source.close(); + } catch (error) { + // Noop + } + } + })(), + }; } private async openImageSelector() { @@ -425,22 +450,22 @@ export class SourceSelector extends React.Component< analytics.logEvent('Image selector closed'); return; } - this.selectImageByPath({ + await this.selectImageByPath({ imagePath, SourceType: sourceDestination.File, - }); + }).promise; } catch (error) { exceptionReporter.report(error); } } - private onDrop(event: React.DragEvent) { + private async onDrop(event: React.DragEvent) { const [file] = event.dataTransfer.files; if (file) { - this.selectImageByPath({ + await this.selectImageByPath({ imagePath: file.path, SourceType: sourceDestination.File, - }); + }).promise; } } @@ -484,6 +509,9 @@ export class SourceSelector extends React.Component< const imageName = selectionState.getImageName(); const imageSize = selectionState.getImageSize(); const imageLogo = selectionState.getImageLogo(); + let cancelURLSelection = () => { + // noop + }; return ( <> @@ -585,6 +613,7 @@ export class SourceSelector extends React.Component< {showURLSelector && ( { + cancelURLSelection(); this.setState({ showURLSelector: false, }); @@ -594,16 +623,17 @@ export class SourceSelector extends React.Component< // if no file was resolved from the dialog. if (!imageURL) { analytics.logEvent('URL selector closed'); - this.setState({ - showURLSelector: false, - }); - return; + } else { + let promise; + ({ + promise, + cancel: cancelURLSelection, + } = this.selectImageByPath({ + imagePath: imageURL, + SourceType: sourceDestination.Http, + })); + await promise; } - - await this.selectImageByPath({ - imagePath: imageURL, - SourceType: sourceDestination.Http, - }); this.setState({ showURLSelector: false, }); diff --git a/lib/gui/app/css/main.css b/lib/gui/app/css/main.css index 03ab634d..b5350722 100644 --- a/lib/gui/app/css/main.css +++ b/lib/gui/app/css/main.css @@ -49,11 +49,6 @@ body { -webkit-overflow-scrolling: touch; } -/* Allow window to be dragged from header */ -#app-header { - -webkit-app-region: drag; -} - /* Prevent blue outline */ a:focus, input:focus, diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 34f89ccb..24c6e9c6 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -146,7 +146,6 @@ interface FlashStepProps { goToSuccess: () => void; source: SourceOptions; isFlashing: boolean; - isWebviewShowing: boolean; style?: React.CSSProperties; // TODO: factorize step: 'decompressing' | 'flashing' | 'verifying'; diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index efecb712..d4577175 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -24,7 +24,6 @@ import * as React from 'react'; import { Flex } from 'rendition'; import styled from 'styled-components'; -import { FeaturedProject } from '../../components/featured-project/featured-project'; import FinishPage from '../../components/finish/finish'; import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; import { SafeWebview } from '../../components/safe-webview/safe-webview'; @@ -114,6 +113,7 @@ interface MainPageState { isWebviewShowing: boolean; hideSettings: boolean; source: SourceOptions; + featuredProjectURL?: string; } export class MainPage extends React.Component< @@ -147,10 +147,21 @@ export class MainPage extends React.Component< }; } - public componentDidMount() { + private async getFeaturedProjectURL() { + const url = new URL( + (await settings.get('featuredProjectEndpoint')) || + 'https://assets.balena.io/etcher-featured/index.html', + ); + url.searchParams.append('borderRight', 'false'); + url.searchParams.append('darkBackground', 'true'); + return url.toString(); + } + + public async componentDidMount() { observe(() => { this.setState(this.stateHelper()); }); + this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() }); } private renderMain() { @@ -163,41 +174,42 @@ export class MainPage extends React.Component< return ( <> - - openExternal('https://www.balena.io/etcher?ref=etcher_footer') - } - tabIndex={100} - /> + + + + openExternal('https://www.balena.io/etcher?ref=etcher_footer') + } + tabIndex={100} + /> + - + } plain tabIndex={5} onClick={() => this.setState({ hideSettings: false })} + style={{ + // Make touch events click instead of dragging + '-webkit-app-region': 'no-drag', + }} /> {!settings.getSync('disableExternalLinks') && ( )} @@ -226,80 +242,72 @@ export class MainPage extends React.Component< justifyContent="space-between" > {notFlashingOrSplitView && ( - - this.setState({ source }) - } - /> - )} - - {notFlashingOrSplitView && ( - - - - )} - - {notFlashingOrSplitView && ( - - )} - - {notFlashingOrSplitView && ( - - - - )} - - {this.state.isFlashing && ( <> - - + + this.setState({ source }) + } + /> + + - { - this.setState({ isWebviewShowing }); - }} + + + + + + )} + + {this.state.isFlashing && this.state.isWebviewShowing && ( + + - + + )} + {this.state.isFlashing && this.state.featuredProjectURL && ( + { + this.setState({ isWebviewShowing }); + }} + style={{ + position: 'absolute', + right: 0, + bottom: 0, + width: '63.8vw', + height: '100vh', + }} + /> )}