mirror of
				https://github.com/balena-io/etcher.git
				synced 2025-10-30 05:28:32 +00:00 
			
		
		
		
	Compare commits
	
		
			23 Commits
		
	
	
		
			v1.5.107
			...
			save-url-i
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f6ce9a217d | ||
|   | fce2d94df7 | ||
|   | 3feb22ee66 | ||
|   | b80a6b2feb | ||
|   | b4e6970119 | ||
|   | 2e3978b3c9 | ||
|   | c6cd421f17 | ||
|   | c3296eed54 | ||
|   | 153e37b9dc | ||
|   | 78aca6a19f | ||
|   | 27695babfd | ||
|   | 06a96db72d | ||
|   | 6584cef774 | ||
|   | 3c77800b1d | ||
|   | 74a78076cf | ||
|   | 8ff8b02f37 | ||
|   | e9603505d2 | ||
| ![bulldozer-balena[bot]](/assets/img/avatar_default.png)  | 0f45f6aca1 | ||
|   | 0a28a7794d | ||
|   | 7c2644ec51 | ||
|   | ae62812c61 | ||
| ![bulldozer-balena[bot]](/assets/img/avatar_default.png)  | 68e24df52b | ||
|   | b9076d01af | 
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -3,6 +3,16 @@ | ||||
| All notable changes to this project will be documented in this file. | ||||
| This project adheres to [Semantic Versioning](http://semver.org/). | ||||
|  | ||||
| # v1.5.109 | ||||
| ## (2020-09-14) | ||||
|  | ||||
| * Workaround elevation bug on Windows when the username contains an ampersand [Alexis Svinartchouk] | ||||
|  | ||||
| # v1.5.108 | ||||
| ## (2020-09-10) | ||||
|  | ||||
| * Fix content not loading when the app path contains special characters [Alexis Svinartchouk] | ||||
|  | ||||
| # v1.5.107 | ||||
| ## (2020-09-04) | ||||
|  | ||||
|   | ||||
| @@ -356,6 +356,16 @@ async function main() { | ||||
| 	ReactDOM.render( | ||||
| 		React.createElement(MainPage), | ||||
| 		document.getElementById('main'), | ||||
| 		// callback to set the correct zoomFactor for webviews as well | ||||
| 		async () => { | ||||
| 			const fullscreen = await settings.get('fullscreen'); | ||||
| 			const width = fullscreen ? window.screen.width : window.outerWidth; | ||||
| 			try { | ||||
| 				electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH); | ||||
| 			} catch (err) { | ||||
| 				// noop | ||||
| 			} | ||||
| 		}, | ||||
| 	); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -18,15 +18,7 @@ import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exc | ||||
| import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; | ||||
| import * as sourceDestination from 'etcher-sdk/build/source-destination/'; | ||||
| import * as React from 'react'; | ||||
| import { | ||||
| 	Flex, | ||||
| 	ModalProps, | ||||
| 	Txt, | ||||
| 	Badge, | ||||
| 	Link, | ||||
| 	Table, | ||||
| 	TableColumn, | ||||
| } from 'rendition'; | ||||
| import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition'; | ||||
| import styled from 'styled-components'; | ||||
|  | ||||
| import { | ||||
| @@ -43,7 +35,12 @@ import { getImage, isDriveSelected } from '../../models/selection-state'; | ||||
| import { store } from '../../models/store'; | ||||
| import { logEvent, logException } from '../../modules/analytics'; | ||||
| import { open as openExternal } from '../../os/open-external/services/open-external'; | ||||
| import { Alert, Modal, ScrollableFlex } from '../../styled-components'; | ||||
| import { | ||||
| 	Alert, | ||||
| 	GenericTableProps, | ||||
| 	Modal, | ||||
| 	Table, | ||||
| } from '../../styled-components'; | ||||
|  | ||||
| import DriveSVGIcon from '../../../assets/tgt.svg'; | ||||
| import { SourceMetadata } from '../source-selector/source-selector'; | ||||
| @@ -75,74 +72,29 @@ function isDrivelistDrive(drive: Drive): drive is DrivelistDrive { | ||||
| 	return typeof (drive as DrivelistDrive).size === 'number'; | ||||
| } | ||||
|  | ||||
| const DrivesTable = styled(({ refFn, ...props }) => ( | ||||
| 	<div> | ||||
| 		<Table<Drive> ref={refFn} {...props} /> | ||||
| 	</div> | ||||
| const DrivesTable = styled((props: GenericTableProps<Drive>) => ( | ||||
| 	<Table<Drive> {...props} /> | ||||
| ))` | ||||
| 	[data-display='table-head'] | ||||
| 		> [data-display='table-row'] | ||||
| 		> [data-display='table-cell'] { | ||||
| 		position: sticky; | ||||
| 		top: 0; | ||||
| 		background-color: ${(props) => props.theme.colors.quartenary.light}; | ||||
|  | ||||
| 		input[type='checkbox'] + div { | ||||
| 			display: ${({ multipleSelection }) => | ||||
| 				multipleSelection ? 'flex' : 'none'}; | ||||
| 		} | ||||
|  | ||||
| 		&:first-child { | ||||
| 			padding-left: 15px; | ||||
| 		} | ||||
|  | ||||
| 		&:nth-child(2) { | ||||
| 			width: 38%; | ||||
| 		} | ||||
|  | ||||
| 		&:nth-child(3) { | ||||
| 			width: 15%; | ||||
| 		} | ||||
|  | ||||
| 		&:nth-child(4) { | ||||
| 			width: 15%; | ||||
| 		} | ||||
|  | ||||
| 		&:nth-child(5) { | ||||
| 			width: 32%; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	[data-display='table-body'] > [data-display='table-row'] { | ||||
| 		> [data-display='table-cell']:first-child { | ||||
| 			padding-left: 15px; | ||||
| 		} | ||||
|  | ||||
| 		> [data-display='table-cell']:last-child { | ||||
| 			padding-right: 0; | ||||
| 		} | ||||
|  | ||||
| 		&[data-highlight='true'] { | ||||
| 			&.system { | ||||
| 				background-color: ${(props) => | ||||
| 					props.showWarnings ? '#fff5e6' : '#e8f5fc'}; | ||||
| 	[data-display='table-head'], | ||||
| 	[data-display='table-body'] { | ||||
| 		> [data-display='table-row'] > [data-display='table-cell'] { | ||||
| 			&:nth-child(2) { | ||||
| 				width: 32%; | ||||
| 			} | ||||
|  | ||||
| 			> [data-display='table-cell']:first-child { | ||||
| 				box-shadow: none; | ||||
| 			&:nth-child(3) { | ||||
| 				width: 15%; | ||||
| 			} | ||||
|  | ||||
| 			&:nth-child(4) { | ||||
| 				width: 15%; | ||||
| 			} | ||||
|  | ||||
| 			&:nth-child(5) { | ||||
| 				width: 32%; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	&& [data-display='table-row'] > [data-display='table-cell'] { | ||||
| 		padding: 6px 8px; | ||||
| 		color: #2a506f; | ||||
| 	} | ||||
|  | ||||
| 	input[type='checkbox'] + div { | ||||
| 		border-radius: ${({ multipleSelection }) => | ||||
| 			multipleSelection ? '4px' : '50%'}; | ||||
| 	} | ||||
| `; | ||||
|  | ||||
| function badgeShadeFromStatus(status: string) { | ||||
| @@ -393,6 +345,16 @@ export class DriveSelector extends React.Component< | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private deselectingAll(rows: DrivelistDrive[]) { | ||||
| 		return ( | ||||
| 			rows.length > 0 && | ||||
| 			rows.length === this.state.selectedList.length && | ||||
| 			this.state.selectedList.every( | ||||
| 				(d) => rows.findIndex((r) => d.device === r.device) > -1, | ||||
| 			) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	componentDidMount() { | ||||
| 		this.unsubscribe = store.subscribe(() => { | ||||
| 			const drives = getDrives(); | ||||
| @@ -453,95 +415,92 @@ export class DriveSelector extends React.Component< | ||||
| 				}} | ||||
| 				{...props} | ||||
| 			> | ||||
| 				<Flex width="100%" height="90%"> | ||||
| 					{!hasAvailableDrives() ? ( | ||||
| 						<Flex | ||||
| 							flexDirection="column" | ||||
| 							justifyContent="center" | ||||
| 							alignItems="center" | ||||
| 							width="100%" | ||||
| 						> | ||||
| 							<DriveSVGIcon width="40px" height="90px" /> | ||||
| 							<b>{this.props.emptyListLabel}</b> | ||||
| 						</Flex> | ||||
| 					) : ( | ||||
| 						<ScrollableFlex flexDirection="column" width="100%"> | ||||
| 							<DrivesTable | ||||
| 								refFn={(t: Table<Drive>) => { | ||||
| 									if (t !== null) { | ||||
| 										t.setRowSelection(selectedList); | ||||
| 									} | ||||
| 								}} | ||||
| 								multipleSelection={this.props.multipleSelection} | ||||
| 								columns={this.tableColumns} | ||||
| 								data={displayedDrives} | ||||
| 								disabledRows={disabledDrives} | ||||
| 								getRowClass={(row: Drive) => | ||||
| 									isDrivelistDrive(row) && row.isSystem ? ['system'] : [] | ||||
| 				{!hasAvailableDrives() ? ( | ||||
| 					<Flex | ||||
| 						flexDirection="column" | ||||
| 						justifyContent="center" | ||||
| 						alignItems="center" | ||||
| 						width="100%" | ||||
| 					> | ||||
| 						<DriveSVGIcon width="40px" height="90px" /> | ||||
| 						<b>{this.props.emptyListLabel}</b> | ||||
| 					</Flex> | ||||
| 				) : ( | ||||
| 					<> | ||||
| 						<DrivesTable | ||||
| 							refFn={(t) => { | ||||
| 								if (t !== null) { | ||||
| 									t.setRowSelection(selectedList); | ||||
| 								} | ||||
| 								rowKey="displayName" | ||||
| 								onCheck={(rows: Drive[]) => { | ||||
| 									const newSelection = rows.filter(isDrivelistDrive); | ||||
| 									if (this.props.multipleSelection) { | ||||
| 										this.setState({ | ||||
| 											selectedList: newSelection, | ||||
| 										}); | ||||
| 										return; | ||||
| 							}} | ||||
| 							checkedRowsNumber={selectedList.length} | ||||
| 							multipleSelection={this.props.multipleSelection} | ||||
| 							columns={this.tableColumns} | ||||
| 							data={displayedDrives} | ||||
| 							disabledRows={disabledDrives} | ||||
| 							getRowClass={(row: Drive) => | ||||
| 								isDrivelistDrive(row) && row.isSystem ? ['system'] : [] | ||||
| 							} | ||||
| 							rowKey="displayName" | ||||
| 							onCheck={(rows: Drive[]) => { | ||||
| 								let newSelection = rows.filter(isDrivelistDrive); | ||||
| 								if (this.props.multipleSelection) { | ||||
| 									if (this.deselectingAll(newSelection)) { | ||||
| 										newSelection = []; | ||||
| 									} | ||||
| 									this.setState({ | ||||
| 										selectedList: newSelection.slice(newSelection.length - 1), | ||||
| 										selectedList: newSelection, | ||||
| 									}); | ||||
| 								}} | ||||
| 								onRowClick={(row: Drive) => { | ||||
| 									if ( | ||||
| 										!isDrivelistDrive(row) || | ||||
| 										this.driveShouldBeDisabled(row, image) | ||||
| 									) { | ||||
| 										return; | ||||
| 									} | ||||
| 									if (this.props.multipleSelection) { | ||||
| 										const newList = [...selectedList]; | ||||
| 										const selectedIndex = selectedList.findIndex( | ||||
| 											(drive) => drive.device === row.device, | ||||
| 										); | ||||
| 										if (selectedIndex === -1) { | ||||
| 											newList.push(row); | ||||
| 										} else { | ||||
| 											// Deselect if selected | ||||
| 											newList.splice(selectedIndex, 1); | ||||
| 										} | ||||
| 										this.setState({ | ||||
| 											selectedList: newList, | ||||
| 										}); | ||||
| 										return; | ||||
| 									} | ||||
| 									this.setState({ | ||||
| 										selectedList: [row], | ||||
| 									}); | ||||
| 								}} | ||||
| 							/> | ||||
| 							{numberOfHiddenSystemDrives > 0 && ( | ||||
| 								<Link | ||||
| 									mt={15} | ||||
| 									mb={15} | ||||
| 									fontSize="14px" | ||||
| 									onClick={() => this.setState({ showSystemDrives: true })} | ||||
| 								> | ||||
| 									<Flex alignItems="center"> | ||||
| 										<ChevronDownSvg height="1em" fill="currentColor" /> | ||||
| 										<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt> | ||||
| 									</Flex> | ||||
| 								</Link> | ||||
| 							)} | ||||
| 						</ScrollableFlex> | ||||
| 					)} | ||||
| 					{this.props.showWarnings && hasSystemDrives ? ( | ||||
| 						<Alert className="system-drive-alert" style={{ width: '67%' }}> | ||||
| 							Selecting your system drive is dangerous and will erase your | ||||
| 							drive! | ||||
| 						</Alert> | ||||
| 					) : null} | ||||
| 				</Flex> | ||||
| 									return; | ||||
| 								} | ||||
| 								this.setState({ | ||||
| 									selectedList: newSelection.slice(newSelection.length - 1), | ||||
| 								}); | ||||
| 							}} | ||||
| 							onRowClick={(row: Drive) => { | ||||
| 								if ( | ||||
| 									!isDrivelistDrive(row) || | ||||
| 									this.driveShouldBeDisabled(row, image) | ||||
| 								) { | ||||
| 									return; | ||||
| 								} | ||||
| 								const index = selectedList.findIndex( | ||||
| 									(d) => d.device === row.device, | ||||
| 								); | ||||
| 								const newList = this.props.multipleSelection | ||||
| 									? [...selectedList] | ||||
| 									: []; | ||||
| 								if (index === -1) { | ||||
| 									newList.push(row); | ||||
| 								} else { | ||||
| 									// Deselect if selected | ||||
| 									newList.splice(index, 1); | ||||
| 								} | ||||
| 								this.setState({ | ||||
| 									selectedList: newList, | ||||
| 								}); | ||||
| 							}} | ||||
| 						/> | ||||
| 						{numberOfHiddenSystemDrives > 0 && ( | ||||
| 							<Link | ||||
| 								mt={15} | ||||
| 								mb={15} | ||||
| 								fontSize="14px" | ||||
| 								onClick={() => this.setState({ showSystemDrives: true })} | ||||
| 							> | ||||
| 								<Flex alignItems="center"> | ||||
| 									<ChevronDownSvg height="1em" fill="currentColor" /> | ||||
| 									<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt> | ||||
| 								</Flex> | ||||
| 							</Link> | ||||
| 						)} | ||||
| 					</> | ||||
| 				)} | ||||
| 				{this.props.showWarnings && hasSystemDrives ? ( | ||||
| 					<Alert className="system-drive-alert" style={{ width: '67%' }}> | ||||
| 						Selecting your system drive is dangerous and will erase your drive! | ||||
| 					</Alert> | ||||
| 				) : null} | ||||
|  | ||||
| 				{missingDriversModal.drive !== undefined && ( | ||||
| 					<Modal | ||||
|   | ||||
| @@ -14,7 +14,6 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| import * as _ from 'lodash'; | ||||
| import * as React from 'react'; | ||||
| import { Flex } from 'rendition'; | ||||
| import { v4 as uuidV4 } from 'uuid'; | ||||
| @@ -23,13 +22,9 @@ import * as flashState from '../../models/flash-state'; | ||||
| import * as selectionState from '../../models/selection-state'; | ||||
| import { Actions, store } from '../../models/store'; | ||||
| import * as analytics from '../../modules/analytics'; | ||||
| import { open as openExternal } from '../../os/open-external/services/open-external'; | ||||
| import { FlashAnother } from '../flash-another/flash-another'; | ||||
| import { FlashResults } from '../flash-results/flash-results'; | ||||
|  | ||||
| import EtcherSvg from '../../../assets/etcher.svg'; | ||||
| import LoveSvg from '../../../assets/love.svg'; | ||||
| import BalenaSvg from '../../../assets/balena.svg'; | ||||
| import { FlashResults, FlashError } from '../flash-results/flash-results'; | ||||
| import { SafeWebview } from '../safe-webview/safe-webview'; | ||||
|  | ||||
| function restart(goToMain: () => void) { | ||||
| 	selectionState.deselectAllDrives(); | ||||
| @@ -44,22 +39,59 @@ function restart(goToMain: () => void) { | ||||
| 	goToMain(); | ||||
| } | ||||
|  | ||||
| function formattedErrors() { | ||||
| 	const errors = _.map( | ||||
| 		_.get(flashState.getFlashResults(), ['results', 'errors']), | ||||
| 		(error) => { | ||||
| 			return `${error.device}: ${error.message || error.code}`; | ||||
| 		}, | ||||
| 	); | ||||
| 	return errors.join('\n'); | ||||
| } | ||||
|  | ||||
| function FinishPage({ goToMain }: { goToMain: () => void }) { | ||||
| 	const results = flashState.getFlashResults().results || {}; | ||||
| 	const [webviewShowing, setWebviewShowing] = React.useState(false); | ||||
| 	const flashResults = flashState.getFlashResults(); | ||||
| 	let errors: FlashError[] = flashResults.results?.errors; | ||||
| 	if (errors === undefined) { | ||||
| 		errors = (store.getState().toJS().failedDevicePaths || []).map( | ||||
| 			([, error]: [string, FlashError]) => ({ | ||||
| 				...error, | ||||
| 			}), | ||||
| 		); | ||||
| 	} | ||||
| 	const { | ||||
| 		averageSpeed, | ||||
| 		blockmappedSize, | ||||
| 		bytesWritten, | ||||
| 		failed, | ||||
| 		size, | ||||
| 	} = flashState.getFlashState(); | ||||
| 	const { | ||||
| 		skip, | ||||
| 		results = { | ||||
| 			bytesWritten, | ||||
| 			sourceMetadata: { | ||||
| 				size, | ||||
| 				blockmappedSize, | ||||
| 			}, | ||||
| 			averageFlashingSpeed: averageSpeed, | ||||
| 			devices: { failed, successful: 0 }, | ||||
| 		}, | ||||
| 	} = flashResults; | ||||
| 	return ( | ||||
| 		<Flex flexDirection="column" width="100%" color="#fff"> | ||||
| 			<Flex height="160px" alignItems="center" justifyContent="center"> | ||||
| 				<FlashResults results={results} errors={formattedErrors()} /> | ||||
| 		<Flex height="100%" justifyContent="space-between"> | ||||
| 			<Flex | ||||
| 				width={webviewShowing ? '36.2vw' : '100vw'} | ||||
| 				height="100vh" | ||||
| 				alignItems="center" | ||||
| 				justifyContent="center" | ||||
| 				flexDirection="column" | ||||
| 				style={{ | ||||
| 					position: 'absolute', | ||||
| 					top: 0, | ||||
| 					zIndex: 1, | ||||
| 					boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)', | ||||
| 				}} | ||||
| 			> | ||||
| 				<FlashResults | ||||
| 					image={selectionState.getImageName()} | ||||
| 					results={results} | ||||
| 					skip={skip} | ||||
| 					errors={errors} | ||||
| 					mb="32px" | ||||
| 					goToMain={goToMain} | ||||
| 				/> | ||||
|  | ||||
| 				<FlashAnother | ||||
| 					onClick={() => { | ||||
| @@ -67,34 +99,18 @@ function FinishPage({ goToMain }: { goToMain: () => void }) { | ||||
| 					}} | ||||
| 				/> | ||||
| 			</Flex> | ||||
|  | ||||
| 			<Flex | ||||
| 				flexDirection="column" | ||||
| 				height="320px" | ||||
| 				justifyContent="space-between" | ||||
| 				alignItems="center" | ||||
| 			> | ||||
| 				<Flex fontSize="28px" mt="40px"> | ||||
| 					Thanks for using | ||||
| 					<EtcherSvg | ||||
| 						width="165px" | ||||
| 						style={{ margin: '0 10px', cursor: 'pointer' }} | ||||
| 						onClick={() => | ||||
| 							openExternal('https://balena.io/etcher?ref=etcher_offline_banner') | ||||
| 						} | ||||
| 					/> | ||||
| 				</Flex> | ||||
| 				<Flex mb="10px"> | ||||
| 					made with | ||||
| 					<LoveSvg height="20px" style={{ margin: '0 10px' }} /> | ||||
| 					by | ||||
| 					<BalenaSvg | ||||
| 						height="20px" | ||||
| 						style={{ margin: '0 10px', cursor: 'pointer' }} | ||||
| 						onClick={() => openExternal('https://balena.io?ref=etcher_success')} | ||||
| 					/> | ||||
| 				</Flex> | ||||
| 			</Flex> | ||||
| 			<SafeWebview | ||||
| 				src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true" | ||||
| 				onWebviewShow={setWebviewShowing} | ||||
| 				style={{ | ||||
| 					display: webviewShowing ? 'flex' : 'none', | ||||
| 					position: 'absolute', | ||||
| 					right: 0, | ||||
| 					bottom: 0, | ||||
| 					width: '63.8vw', | ||||
| 					height: '100vh', | ||||
| 				}} | ||||
| 			/> | ||||
| 		</Flex> | ||||
| 	); | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ export interface FlashAnotherProps { | ||||
| export const FlashAnother = (props: FlashAnotherProps) => { | ||||
| 	return ( | ||||
| 		<BaseButton primary onClick={props.onClick}> | ||||
| 			Flash Another | ||||
| 			Flash another | ||||
| 		</BaseButton> | ||||
| 	); | ||||
| }; | ||||
|   | ||||
| @@ -16,19 +16,108 @@ | ||||
|  | ||||
| import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg'; | ||||
| import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg'; | ||||
| import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg'; | ||||
| import * as _ from 'lodash'; | ||||
| import outdent from 'outdent'; | ||||
| import * as React from 'react'; | ||||
| import { Flex, Txt } from 'rendition'; | ||||
| import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition'; | ||||
| import styled from 'styled-components'; | ||||
|  | ||||
| import { progress } from '../../../../shared/messages'; | ||||
| import { bytesToMegabytes } from '../../../../shared/units'; | ||||
|  | ||||
| import FlashSvg from '../../../assets/flash.svg'; | ||||
| import { resetState } from '../../models/flash-state'; | ||||
| import * as selection from '../../models/selection-state'; | ||||
| import { middleEllipsis } from '../../utils/middle-ellipsis'; | ||||
| import { Modal, Table } from '../../styled-components'; | ||||
|  | ||||
| const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)` | ||||
| 	[data-display='table-head'], | ||||
| 	[data-display='table-body'] { | ||||
| 		[data-display='table-cell'] { | ||||
| 			&:first-child { | ||||
| 				width: 30%; | ||||
| 			} | ||||
|  | ||||
| 			&:nth-child(2) { | ||||
| 				width: 20%; | ||||
| 			} | ||||
|  | ||||
| 			&:last-child { | ||||
| 				width: 50%; | ||||
| 			} | ||||
| 		} | ||||
| `; | ||||
|  | ||||
| const DoneIcon = (props: { | ||||
| 	skipped: boolean; | ||||
| 	allFailed: boolean; | ||||
| 	someFailed: boolean; | ||||
| }) => { | ||||
| 	const { allFailed, someFailed } = props; | ||||
| 	const someOrAllFailed = allFailed || someFailed; | ||||
| 	const svgProps = { | ||||
| 		width: '24px', | ||||
| 		fill: someOrAllFailed ? '#c6c8c9' : '#1ac135', | ||||
| 		style: { | ||||
| 			width: '28px', | ||||
| 			height: '28px', | ||||
| 			marginTop: '-25px', | ||||
| 			marginLeft: '13px', | ||||
| 			zIndex: 1, | ||||
| 			color: someOrAllFailed ? '#c6c8c9' : '#1ac135', | ||||
| 		}, | ||||
| 	}; | ||||
| 	return allFailed && !props.skipped ? ( | ||||
| 		<TimesCircleSvg {...svgProps} /> | ||||
| 	) : ( | ||||
| 		<CheckCircleSvg {...svgProps} /> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| export interface FlashError extends Error { | ||||
| 	description: string; | ||||
| 	device: string; | ||||
| 	code: string; | ||||
| } | ||||
|  | ||||
| function formattedErrors(errors: FlashError[]) { | ||||
| 	return errors | ||||
| 		.map((error) => `${error.device}: ${error.message || error.code}`) | ||||
| 		.join('\n'); | ||||
| } | ||||
|  | ||||
| const columns: Array<TableColumn<FlashError>> = [ | ||||
| 	{ | ||||
| 		field: 'description', | ||||
| 		label: 'Target', | ||||
| 	}, | ||||
| 	{ | ||||
| 		field: 'device', | ||||
| 		label: 'Location', | ||||
| 	}, | ||||
| 	{ | ||||
| 		field: 'message', | ||||
| 		label: 'Error', | ||||
| 		render: (message: string, { code }: FlashError) => { | ||||
| 			return message ?? code; | ||||
| 		}, | ||||
| 	}, | ||||
| ]; | ||||
|  | ||||
| export function FlashResults({ | ||||
| 	goToMain, | ||||
| 	image = '', | ||||
| 	errors, | ||||
| 	results, | ||||
| 	skip, | ||||
| 	...props | ||||
| }: { | ||||
| 	errors: string; | ||||
| 	goToMain: () => void; | ||||
| 	image?: string; | ||||
| 	errors: FlashError[]; | ||||
| 	skip: boolean; | ||||
| 	results: { | ||||
| 		bytesWritten: number; | ||||
| 		sourceMetadata: { | ||||
| @@ -38,8 +127,9 @@ export function FlashResults({ | ||||
| 		averageFlashingSpeed: number; | ||||
| 		devices: { failed: number; successful: number }; | ||||
| 	}; | ||||
| }) { | ||||
| 	const allDevicesFailed = results.devices.successful === 0; | ||||
| } & FlexProps) { | ||||
| 	const [showErrorsInfo, setShowErrorsInfo] = React.useState(false); | ||||
| 	const allFailed = results.devices.successful === 0; | ||||
| 	const effectiveSpeed = _.round( | ||||
| 		bytesToMegabytes( | ||||
| 			results.sourceMetadata.size / | ||||
| @@ -48,44 +138,56 @@ export function FlashResults({ | ||||
| 		1, | ||||
| 	); | ||||
| 	return ( | ||||
| 		<Flex | ||||
| 			flexDirection="column" | ||||
| 			mr="80px" | ||||
| 			height="90px" | ||||
| 			style={{ | ||||
| 				position: 'relative', | ||||
| 				top: '25px', | ||||
| 			}} | ||||
| 		> | ||||
| 			<Flex alignItems="center"> | ||||
| 				<CheckCircleSvg | ||||
| 					width="24px" | ||||
| 					fill={allDevicesFailed ? '#c6c8c9' : '#1ac135'} | ||||
| 					style={{ | ||||
| 						margin: '0 15px 0 0', | ||||
| 					}} | ||||
| 				/> | ||||
| 				<Txt fontSize={24} color="#fff"> | ||||
| 		<Flex flexDirection="column" {...props}> | ||||
| 			<Flex alignItems="center" flexDirection="column"> | ||||
| 				<Flex | ||||
| 					alignItems="center" | ||||
| 					mt="50px" | ||||
| 					mb="32px" | ||||
| 					color="#7e8085" | ||||
| 					flexDirection="column" | ||||
| 				> | ||||
| 					<FlashSvg width="40px" height="40px" className="disabled" /> | ||||
| 					<DoneIcon | ||||
| 						skipped={skip} | ||||
| 						allFailed={allFailed} | ||||
| 						someFailed={results.devices.failed !== 0} | ||||
| 					/> | ||||
| 					<Txt>{middleEllipsis(image, 24)}</Txt> | ||||
| 				</Flex> | ||||
| 				<Txt fontSize={24} color="#fff" mb="17px"> | ||||
| 					Flash Complete! | ||||
| 				</Txt> | ||||
| 				{skip ? <Flex color="#7e8085">Validation has been skipped</Flex> : null} | ||||
| 			</Flex> | ||||
| 			<Flex flexDirection="column" mr="0" mb="0" ml="40px" color="#7e8085"> | ||||
| 			<Flex flexDirection="column" color="#7e8085"> | ||||
| 				{Object.entries(results.devices).map(([type, quantity]) => { | ||||
| 					const failedTargets = type === 'failed'; | ||||
| 					return quantity ? ( | ||||
| 						<Flex | ||||
| 							alignItems="center" | ||||
| 							tooltip={type === 'failed' ? errors : undefined} | ||||
| 						> | ||||
| 						<Flex alignItems="center"> | ||||
| 							<CircleSvg | ||||
| 								width="14px" | ||||
| 								fill={type === 'failed' ? '#ff4444' : '#1ac135'} | ||||
| 								color={failedTargets ? '#ff4444' : '#1ac135'} | ||||
| 							/> | ||||
| 							<Txt ml={10}>{quantity}</Txt> | ||||
| 							<Txt ml={10}>{progress[type](quantity)}</Txt> | ||||
| 							<Txt ml="10px" color="#fff"> | ||||
| 								{quantity} | ||||
| 							</Txt> | ||||
| 							<Txt | ||||
| 								ml="10px" | ||||
| 								tooltip={failedTargets ? formattedErrors(errors) : undefined} | ||||
| 							> | ||||
| 								{progress[type](quantity)} | ||||
| 							</Txt> | ||||
| 							{failedTargets && ( | ||||
| 								<Link ml="10px" onClick={() => setShowErrorsInfo(true)}> | ||||
| 									more info | ||||
| 								</Link> | ||||
| 							)} | ||||
| 						</Flex> | ||||
| 					) : null; | ||||
| 				})} | ||||
| 				{!allDevicesFailed && ( | ||||
| 				{!allFailed && ( | ||||
| 					<Txt | ||||
| 						fontSize="10px" | ||||
| 						style={{ | ||||
| @@ -101,6 +203,33 @@ export function FlashResults({ | ||||
| 					</Txt> | ||||
| 				)} | ||||
| 			</Flex> | ||||
|  | ||||
| 			{showErrorsInfo && ( | ||||
| 				<Modal | ||||
| 					titleElement={ | ||||
| 						<Flex alignItems="baseline" mb={18}> | ||||
| 							<Txt fontSize={24} align="left"> | ||||
| 								Failed targets | ||||
| 							</Txt> | ||||
| 						</Flex> | ||||
| 					} | ||||
| 					action="Retry failed targets" | ||||
| 					cancel={() => setShowErrorsInfo(false)} | ||||
| 					done={() => { | ||||
| 						setShowErrorsInfo(false); | ||||
| 						resetState(); | ||||
| 						selection | ||||
| 							.getSelectedDrives() | ||||
| 							.filter((drive) => | ||||
| 								errors.every((error) => error.device !== drive.device), | ||||
| 							) | ||||
| 							.forEach((drive) => selection.deselectDrive(drive.device)); | ||||
| 						goToMain(); | ||||
| 					}} | ||||
| 				> | ||||
| 					<ErrorsTable columns={columns} data={errors} /> | ||||
| 				</Modal> | ||||
| 			)} | ||||
| 		</Flex> | ||||
| 	); | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import * as React from 'react'; | ||||
| import { Flex, Button, ProgressBar, Txt } from 'rendition'; | ||||
| import { default as styled } from 'styled-components'; | ||||
|  | ||||
| import { fromFlashState } from '../../modules/progress-status'; | ||||
| import { fromFlashState, FlashState } from '../../modules/progress-status'; | ||||
| import { StepButton } from '../../styled-components'; | ||||
|  | ||||
| const FlashProgressBar = styled(ProgressBar)` | ||||
| @@ -44,12 +44,12 @@ const FlashProgressBar = styled(ProgressBar)` | ||||
| `; | ||||
|  | ||||
| interface ProgressButtonProps { | ||||
| 	type: 'decompressing' | 'flashing' | 'verifying'; | ||||
| 	type: FlashState['type']; | ||||
| 	active: boolean; | ||||
| 	percentage: number; | ||||
| 	position: number; | ||||
| 	disabled: boolean; | ||||
| 	cancel: () => void; | ||||
| 	cancel: (type: string) => void; | ||||
| 	callback: () => void; | ||||
| 	warning?: boolean; | ||||
| } | ||||
| @@ -58,13 +58,18 @@ const colors = { | ||||
| 	decompressing: '#00aeef', | ||||
| 	flashing: '#da60ff', | ||||
| 	verifying: '#1ac135', | ||||
| 	downloading: '#00aeef', | ||||
| 	default: '#00aeef', | ||||
| } as const; | ||||
|  | ||||
| const CancelButton = styled((props) => ( | ||||
| 	<Button plain {...props}> | ||||
| 		Cancel | ||||
| 	</Button> | ||||
| ))` | ||||
| const CancelButton = styled(({ type, onClick, ...props }) => { | ||||
| 	const status = type === 'verifying' ? 'Skip' : 'Cancel'; | ||||
| 	return ( | ||||
| 		<Button plain onClick={() => onClick(status)} {...props}> | ||||
| 			{status} | ||||
| 		</Button> | ||||
| 	); | ||||
| })` | ||||
| 	font-weight: 600; | ||||
| 	&&& { | ||||
| 		width: auto; | ||||
| @@ -75,10 +80,13 @@ const CancelButton = styled((props) => ( | ||||
|  | ||||
| export class ProgressButton extends React.PureComponent<ProgressButtonProps> { | ||||
| 	public render() { | ||||
| 		const type = this.props.type || 'default'; | ||||
| 		const percentage = this.props.percentage; | ||||
| 		const warning = this.props.warning; | ||||
| 		const { status, position } = fromFlashState({ | ||||
| 			type: this.props.type, | ||||
| 			percentage, | ||||
| 			position: this.props.position, | ||||
| 			percentage: this.props.percentage, | ||||
| 		}); | ||||
| 		if (this.props.active) { | ||||
| 			return ( | ||||
| @@ -96,21 +104,24 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> { | ||||
| 					> | ||||
| 						<Flex> | ||||
| 							<Txt color="#fff">{status} </Txt> | ||||
| 							<Txt color={colors[this.props.type]}>{position}</Txt> | ||||
| 							<Txt color={colors[type]}>{position}</Txt> | ||||
| 						</Flex> | ||||
| 						<CancelButton onClick={this.props.cancel} color="#00aeef" /> | ||||
| 						{type && ( | ||||
| 							<CancelButton | ||||
| 								type={type} | ||||
| 								onClick={this.props.cancel} | ||||
| 								color="#00aeef" | ||||
| 							/> | ||||
| 						)} | ||||
| 					</Flex> | ||||
| 					<FlashProgressBar | ||||
| 						background={colors[this.props.type]} | ||||
| 						value={this.props.percentage} | ||||
| 					/> | ||||
| 					<FlashProgressBar background={colors[type]} value={percentage} /> | ||||
| 				</> | ||||
| 			); | ||||
| 		} | ||||
| 		return ( | ||||
| 			<StepButton | ||||
| 				primary={!this.props.warning} | ||||
| 				warning={this.props.warning} | ||||
| 				primary={!warning} | ||||
| 				warning={warning} | ||||
| 				onClick={this.props.callback} | ||||
| 				disabled={this.props.disabled} | ||||
| 				style={{ | ||||
|   | ||||
| @@ -15,7 +15,6 @@ | ||||
|  */ | ||||
|  | ||||
| import * as electron from 'electron'; | ||||
| import * as _ from 'lodash'; | ||||
| import * as React from 'react'; | ||||
|  | ||||
| import * as packageJSON from '../../../../../package.json'; | ||||
| @@ -94,8 +93,8 @@ export class SafeWebview extends React.PureComponent< | ||||
| 		); | ||||
| 		this.entryHref = url.href; | ||||
| 		// Events steal 'this' | ||||
| 		this.didFailLoad = _.bind(this.didFailLoad, this); | ||||
| 		this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this); | ||||
| 		this.didFailLoad = this.didFailLoad.bind(this); | ||||
| 		this.didGetResponseDetails = this.didGetResponseDetails.bind(this); | ||||
| 		// Make a persistent electron session for the webview | ||||
| 		this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, { | ||||
| 			// Disable the cache for the session such that new content shows up when refreshing | ||||
|   | ||||
| @@ -61,7 +61,7 @@ async function getSettingsList(): Promise<Setting[]> { | ||||
| 		{ | ||||
| 			name: 'updatesEnabled', | ||||
| 			label: 'Auto-updates enabled', | ||||
| 			hide: _.includes(['rpm', 'deb'], packageType), | ||||
| 			hide: ['rpm', 'deb'].includes(packageType), | ||||
| 		}, | ||||
| 	]; | ||||
| } | ||||
| @@ -121,9 +121,9 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) { | ||||
| 			done={() => toggleModal(false)} | ||||
| 		> | ||||
| 			<Flex flexDirection="column"> | ||||
| 				{_.map(settingsList, (setting: Setting, i: number) => { | ||||
| 				{settingsList.map((setting: Setting, i: number) => { | ||||
| 					return setting.hide ? null : ( | ||||
| 						<Flex key={setting.name}> | ||||
| 						<Flex key={setting.name} mb={14}> | ||||
| 							<Checkbox | ||||
| 								toggle | ||||
| 								tabIndex={6 + i} | ||||
| @@ -135,12 +135,13 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) { | ||||
| 					); | ||||
| 				})} | ||||
| 				<Flex | ||||
| 					mt={28} | ||||
| 					mt={18} | ||||
| 					alignItems="center" | ||||
| 					color="#00aeef" | ||||
| 					style={{ | ||||
| 						width: 'fit-content', | ||||
| 						cursor: 'pointer', | ||||
| 						fontSize: 14, | ||||
| 					}} | ||||
| 					onClick={() => | ||||
| 						openExternal( | ||||
|   | ||||
| @@ -25,15 +25,7 @@ import { GPTPartition, MBRPartition } from 'partitioninfo'; | ||||
| import * as path from 'path'; | ||||
| import * as prettyBytes from 'pretty-bytes'; | ||||
| import * as React from 'react'; | ||||
| import { | ||||
| 	Flex, | ||||
| 	ButtonProps, | ||||
| 	Modal as SmallModal, | ||||
| 	Txt, | ||||
| 	Card as BaseCard, | ||||
| 	Input, | ||||
| 	Spinner, | ||||
| } from 'rendition'; | ||||
| import { Flex, ButtonProps, Modal as SmallModal, Txt } from 'rendition'; | ||||
| import styled from 'styled-components'; | ||||
|  | ||||
| import * as errors from '../../../../shared/errors'; | ||||
| @@ -48,62 +40,21 @@ import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drive | ||||
| import { | ||||
| 	ChangeButton, | ||||
| 	DetailsText, | ||||
| 	Modal, | ||||
| 	StepButton, | ||||
| 	StepNameButton, | ||||
| 	ScrollableFlex, | ||||
| } from '../../styled-components'; | ||||
| import { colors } from '../../theme'; | ||||
| import { middleEllipsis } from '../../utils/middle-ellipsis'; | ||||
| import URLSelector from '../url-selector/url-selector'; | ||||
| import { SVGIcon } from '../svg-icon/svg-icon'; | ||||
|  | ||||
| import ImageSvg from '../../../assets/image.svg'; | ||||
| import { DriveSelector } from '../drive-selector/drive-selector'; | ||||
| import { DrivelistDrive } from '../../../../shared/drive-constraints'; | ||||
|  | ||||
| const recentUrlImagesKey = 'recentUrlImages'; | ||||
|  | ||||
| function normalizeRecentUrlImages(urls: any[]): URL[] { | ||||
| 	if (!Array.isArray(urls)) { | ||||
| 		urls = []; | ||||
| 	} | ||||
| 	urls = urls | ||||
| 		.map((url) => { | ||||
| 			try { | ||||
| 				return new URL(url); | ||||
| 			} catch (error) { | ||||
| 				// Invalid URL, skip | ||||
| 			} | ||||
| 		}) | ||||
| 		.filter((url) => url !== undefined); | ||||
| 	urls = _.uniqBy(urls, (url) => url.href); | ||||
| 	return urls.slice(urls.length - 5); | ||||
| } | ||||
|  | ||||
| function getRecentUrlImages(): URL[] { | ||||
| 	let urls = []; | ||||
| 	try { | ||||
| 		urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]'); | ||||
| 	} catch { | ||||
| 		// noop | ||||
| 	} | ||||
| 	return normalizeRecentUrlImages(urls); | ||||
| } | ||||
|  | ||||
| function setRecentUrlImages(urls: URL[]) { | ||||
| 	const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href)); | ||||
| 	localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized)); | ||||
| } | ||||
|  | ||||
| const isURL = (imagePath: string) => | ||||
| 	imagePath.startsWith('https://') || imagePath.startsWith('http://'); | ||||
|  | ||||
| const Card = styled(BaseCard)` | ||||
| 	hr { | ||||
| 		margin: 5px 0; | ||||
| 	} | ||||
| `; | ||||
|  | ||||
| // TODO move these styles to rendition | ||||
| const ModalText = styled.p` | ||||
| 	a { | ||||
| @@ -127,85 +78,6 @@ function isString(value: any): value is string { | ||||
| 	return typeof value === 'string'; | ||||
| } | ||||
|  | ||||
| const URLSelector = ({ | ||||
| 	done, | ||||
| 	cancel, | ||||
| }: { | ||||
| 	done: (imageURL: string) => void; | ||||
| 	cancel: () => void; | ||||
| }) => { | ||||
| 	const [imageURL, setImageURL] = React.useState(''); | ||||
| 	const [recentImages, setRecentImages] = React.useState<URL[]>([]); | ||||
| 	const [loading, setLoading] = React.useState(false); | ||||
| 	React.useEffect(() => { | ||||
| 		const fetchRecentUrlImages = async () => { | ||||
| 			const recentUrlImages: URL[] = await getRecentUrlImages(); | ||||
| 			setRecentImages(recentUrlImages); | ||||
| 		}; | ||||
| 		fetchRecentUrlImages(); | ||||
| 	}, []); | ||||
| 	return ( | ||||
| 		<Modal | ||||
| 			cancel={cancel} | ||||
| 			primaryButtonProps={{ | ||||
| 				disabled: loading || !imageURL, | ||||
| 			}} | ||||
| 			action={loading ? <Spinner /> : 'OK'} | ||||
| 			done={async () => { | ||||
| 				setLoading(true); | ||||
| 				const urlStrings = recentImages.map((url: URL) => url.href); | ||||
| 				const normalizedRecentUrls = normalizeRecentUrlImages([ | ||||
| 					...urlStrings, | ||||
| 					imageURL, | ||||
| 				]); | ||||
| 				setRecentUrlImages(normalizedRecentUrls); | ||||
| 				await done(imageURL); | ||||
| 			}} | ||||
| 		> | ||||
| 			<Flex flexDirection="column"> | ||||
| 				<Flex style={{ width: '100%' }} flexDirection="column"> | ||||
| 					<Txt mb="10px" fontSize="24px"> | ||||
| 						Use Image URL | ||||
| 					</Txt> | ||||
| 					<Input | ||||
| 						value={imageURL} | ||||
| 						placeholder="Enter a valid URL" | ||||
| 						type="text" | ||||
| 						onChange={(evt: React.ChangeEvent<HTMLInputElement>) => | ||||
| 							setImageURL(evt.target.value) | ||||
| 						} | ||||
| 					/> | ||||
| 				</Flex> | ||||
| 				{recentImages.length > 0 && ( | ||||
| 					<Flex flexDirection="column" height="78.6%"> | ||||
| 						<Txt fontSize={18}>Recent</Txt> | ||||
| 						<ScrollableFlex flexDirection="column"> | ||||
| 							<Card | ||||
| 								p="10px 15px" | ||||
| 								rows={recentImages | ||||
| 									.map((recent) => ( | ||||
| 										<Txt | ||||
| 											key={recent.href} | ||||
| 											onClick={() => { | ||||
| 												setImageURL(recent.href); | ||||
| 											}} | ||||
| 											style={{ | ||||
| 												overflowWrap: 'break-word', | ||||
| 											}} | ||||
| 										> | ||||
| 											{recent.pathname.split('/').pop()} - {recent.href} | ||||
| 										</Txt> | ||||
| 									)) | ||||
| 									.reverse()} | ||||
| 							/> | ||||
| 						</ScrollableFlex> | ||||
| 					</Flex> | ||||
| 				)} | ||||
| 			</Flex> | ||||
| 		</Modal> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| interface Flow { | ||||
| 	icon?: JSX.Element; | ||||
| 	onClick: (evt: React.MouseEvent) => void; | ||||
| @@ -213,22 +85,28 @@ interface Flow { | ||||
| } | ||||
|  | ||||
| const FlowSelector = styled( | ||||
| 	({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => { | ||||
| 		return ( | ||||
| 			<StepButton | ||||
| 				plain | ||||
| 				onClick={(evt) => flow.onClick(evt)} | ||||
| 				icon={flow.icon} | ||||
| 				{...props} | ||||
| 			> | ||||
| 				{flow.label} | ||||
| 			</StepButton> | ||||
| 		); | ||||
| 	}, | ||||
| 	({ flow, ...props }: { flow: Flow } & ButtonProps) => ( | ||||
| 		<StepButton | ||||
| 			plain={!props.primary} | ||||
| 			primary={props.primary} | ||||
| 			onClick={(evt: React.MouseEvent<Element, MouseEvent>) => | ||||
| 				flow.onClick(evt) | ||||
| 			} | ||||
| 			icon={flow.icon} | ||||
| 			{...props} | ||||
| 		> | ||||
| 			{flow.label} | ||||
| 		</StepButton> | ||||
| 	), | ||||
| )` | ||||
| 	border-radius: 24px; | ||||
| 	color: rgba(255, 255, 255, 0.7); | ||||
|  | ||||
| 	:enabled:focus, | ||||
| 	:enabled:focus svg { | ||||
| 		color: ${colors.primary.foreground} !important; | ||||
| 	} | ||||
|  | ||||
| 	:enabled:hover { | ||||
| 		background-color: ${colors.primary.background}; | ||||
| 		color: ${colors.primary.foreground}; | ||||
| @@ -269,6 +147,7 @@ interface SourceSelectorState { | ||||
| 	showImageDetails: boolean; | ||||
| 	showURLSelector: boolean; | ||||
| 	showDriveSelector: boolean; | ||||
| 	defaultFlowActive: boolean; | ||||
| } | ||||
|  | ||||
| export class SourceSelector extends React.Component< | ||||
| @@ -285,7 +164,11 @@ export class SourceSelector extends React.Component< | ||||
| 			showImageDetails: false, | ||||
| 			showURLSelector: false, | ||||
| 			showDriveSelector: false, | ||||
| 			defaultFlowActive: true, | ||||
| 		}; | ||||
|  | ||||
| 		// Bind `this` since it's used in an event's callback | ||||
| 		this.onSelectImage = this.onSelectImage.bind(this); | ||||
| 	} | ||||
|  | ||||
| 	public componentDidMount() { | ||||
| @@ -527,6 +410,10 @@ export class SourceSelector extends React.Component< | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	private setDefaultFlowActive(defaultFlowActive: boolean) { | ||||
| 		this.setState({ defaultFlowActive }); | ||||
| 	} | ||||
|  | ||||
| 	// TODO add a visual change when dragging a file over the selector | ||||
| 	public render() { | ||||
| 		const { flashing } = this.props; | ||||
| @@ -593,12 +480,15 @@ export class SourceSelector extends React.Component< | ||||
| 					) : ( | ||||
| 						<> | ||||
| 							<FlowSelector | ||||
| 								primary={this.state.defaultFlowActive} | ||||
| 								key="Flash from file" | ||||
| 								flow={{ | ||||
| 									onClick: () => this.openImageSelector(), | ||||
| 									label: 'Flash from file', | ||||
| 									icon: <FileSvg height="1em" fill="currentColor" />, | ||||
| 								}} | ||||
| 								onMouseEnter={() => this.setDefaultFlowActive(false)} | ||||
| 								onMouseLeave={() => this.setDefaultFlowActive(true)} | ||||
| 							/> | ||||
| 							<FlowSelector | ||||
| 								key="Flash from URL" | ||||
| @@ -607,6 +497,8 @@ export class SourceSelector extends React.Component< | ||||
| 									label: 'Flash from URL', | ||||
| 									icon: <LinkSvg height="1em" fill="currentColor" />, | ||||
| 								}} | ||||
| 								onMouseEnter={() => this.setDefaultFlowActive(false)} | ||||
| 								onMouseLeave={() => this.setDefaultFlowActive(true)} | ||||
| 							/> | ||||
| 							<FlowSelector | ||||
| 								key="Clone drive" | ||||
| @@ -615,6 +507,8 @@ export class SourceSelector extends React.Component< | ||||
| 									label: 'Clone drive', | ||||
| 									icon: <CopySvg height="1em" fill="currentColor" />, | ||||
| 								}} | ||||
| 								onMouseEnter={() => this.setDefaultFlowActive(false)} | ||||
| 								onMouseLeave={() => this.setDefaultFlowActive(true)} | ||||
| 							/> | ||||
| 						</> | ||||
| 					)} | ||||
|   | ||||
							
								
								
									
										167
									
								
								lib/gui/app/components/url-selector/url-selector.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								lib/gui/app/components/url-selector/url-selector.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| import { uniqBy } from 'lodash'; | ||||
| import * as React from 'react'; | ||||
| import Checkbox from 'rendition/dist_esm5/components/Checkbox'; | ||||
| import { Flex } from 'rendition/dist_esm5/components/Flex'; | ||||
| import Input from 'rendition/dist_esm5/components/Input'; | ||||
| import Link from 'rendition/dist_esm5/components/Link'; | ||||
| import RadioButton from 'rendition/dist_esm5/components/RadioButton'; | ||||
| import Txt from 'rendition/dist_esm5/components/Txt'; | ||||
|  | ||||
| import * as settings from '../../models/settings'; | ||||
| import { Modal, ScrollableFlex } from '../../styled-components'; | ||||
| import { openDialog } from '../../os/dialog'; | ||||
| import { startEllipsis } from '../../utils/start-ellipsis'; | ||||
|  | ||||
| const RECENT_URL_IMAGES_KEY = 'recentUrlImages'; | ||||
| const SAVE_IMAGE_AFTER_FLASH_KEY = 'saveUrlImage'; | ||||
| const SAVE_IMAGE_AFTER_FLASH_PATH_KEY = 'saveUrlImageTo'; | ||||
|  | ||||
| function normalizeRecentUrlImages(urls: any[]): URL[] { | ||||
| 	if (!Array.isArray(urls)) { | ||||
| 		urls = []; | ||||
| 	} | ||||
| 	urls = urls | ||||
| 		.map((url) => { | ||||
| 			try { | ||||
| 				return new URL(url); | ||||
| 			} catch (error) { | ||||
| 				// Invalid URL, skip | ||||
| 			} | ||||
| 		}) | ||||
| 		.filter((url) => url !== undefined); | ||||
| 	urls = uniqBy(urls, (url) => url.href); | ||||
| 	return urls.slice(-5); | ||||
| } | ||||
|  | ||||
| function getRecentUrlImages(): URL[] { | ||||
| 	let urls = []; | ||||
| 	try { | ||||
| 		urls = JSON.parse(localStorage.getItem(RECENT_URL_IMAGES_KEY) || '[]'); | ||||
| 	} catch { | ||||
| 		// noop | ||||
| 	} | ||||
| 	return normalizeRecentUrlImages(urls); | ||||
| } | ||||
|  | ||||
| function setRecentUrlImages(urls: string[]) { | ||||
| 	localStorage.setItem(RECENT_URL_IMAGES_KEY, JSON.stringify(urls)); | ||||
| } | ||||
|  | ||||
| export const URLSelector = ({ | ||||
| 	done, | ||||
| 	cancel, | ||||
| }: { | ||||
| 	done: (imageURL: string) => void; | ||||
| 	cancel: () => void; | ||||
| }) => { | ||||
| 	const [imageURL, setImageURL] = React.useState(''); | ||||
| 	const [recentImages, setRecentImages] = React.useState<URL[]>([]); | ||||
| 	const [loading, setLoading] = React.useState(false); | ||||
| 	const [saveImage, setSaveImage] = React.useState(false); | ||||
| 	const [saveImagePath, setSaveImagePath] = React.useState(''); | ||||
| 	React.useEffect(() => { | ||||
| 		const fetchRecentUrlImages = async () => { | ||||
| 			const recentUrlImages: URL[] = await getRecentUrlImages(); | ||||
| 			setRecentImages(recentUrlImages); | ||||
| 		}; | ||||
| 		const getSaveImageSettings = async () => { | ||||
| 			const saveUrlImage: boolean = await settings.get( | ||||
| 				SAVE_IMAGE_AFTER_FLASH_KEY, | ||||
| 			); | ||||
| 			const saveUrlImageToPath: string = await settings.get( | ||||
| 				SAVE_IMAGE_AFTER_FLASH_PATH_KEY, | ||||
| 			); | ||||
| 			setSaveImage(saveUrlImage); | ||||
| 			setSaveImagePath(saveUrlImageToPath); | ||||
| 		}; | ||||
| 		fetchRecentUrlImages(); | ||||
| 		getSaveImageSettings(); | ||||
| 	}, []); | ||||
| 	return ( | ||||
| 		<Modal | ||||
| 			title="Use Image URL" | ||||
| 			cancel={cancel} | ||||
| 			primaryButtonProps={{ | ||||
| 				className: loading || !imageURL ? 'disabled' : '', | ||||
| 			}} | ||||
| 			done={async () => { | ||||
| 				setLoading(true); | ||||
| 				const urlStrings = recentImages | ||||
| 					.map((url: URL) => url.href) | ||||
| 					.concat(imageURL); | ||||
| 				setRecentUrlImages(urlStrings); | ||||
| 				await done(imageURL); | ||||
| 			}} | ||||
| 		> | ||||
| 			<Flex flexDirection="column"> | ||||
| 				<Flex mb="16px" width="100%" height="auto" flexDirection="column"> | ||||
| 					<Input | ||||
| 						value={imageURL} | ||||
| 						placeholder="Enter a valid URL" | ||||
| 						type="text" | ||||
| 						onChange={(evt: React.ChangeEvent<HTMLInputElement>) => | ||||
| 							setImageURL(evt.target.value) | ||||
| 						} | ||||
| 					/> | ||||
| 					<Flex alignItems="flex-end"> | ||||
| 						<Checkbox | ||||
| 							mt="16px" | ||||
| 							checked={saveImage} | ||||
| 							onChange={(evt) => { | ||||
| 								const value = evt.target.checked; | ||||
| 								setSaveImage(value); | ||||
| 								settings | ||||
| 									.set(SAVE_IMAGE_AFTER_FLASH_KEY, value) | ||||
| 									.then(() => setSaveImage(value)); | ||||
| 							}} | ||||
| 							label={<>Save file to: </>} | ||||
| 						/> | ||||
| 						<Link | ||||
| 							disabled={!saveImage} | ||||
| 							onClick={async () => { | ||||
| 								if (saveImage) { | ||||
| 									const folder = await openDialog('openDirectory'); | ||||
| 									if (folder) { | ||||
| 										await settings.set(SAVE_IMAGE_AFTER_FLASH_PATH_KEY, folder); | ||||
| 										setSaveImagePath(folder); | ||||
| 									} | ||||
| 								} | ||||
| 							}} | ||||
| 						> | ||||
| 							{startEllipsis(saveImagePath, 20)} | ||||
| 						</Link> | ||||
| 					</Flex> | ||||
| 				</Flex> | ||||
| 				{recentImages.length > 0 && ( | ||||
| 					<Flex flexDirection="column" height="58%"> | ||||
| 						<Txt fontSize={18} mb="10px"> | ||||
| 							Recent | ||||
| 						</Txt> | ||||
| 						<ScrollableFlex flexDirection="column" p="0"> | ||||
| 							{recentImages | ||||
| 								.map((recent, i) => ( | ||||
| 									<RadioButton | ||||
| 										mb={i !== 0 ? '6px' : '0'} | ||||
| 										key={recent.href} | ||||
| 										checked={imageURL === recent.href} | ||||
| 										label={`${recent.pathname.split('/').pop()} - ${ | ||||
| 											recent.href | ||||
| 										}`} | ||||
| 										onChange={() => { | ||||
| 											setImageURL(recent.href); | ||||
| 										}} | ||||
| 										style={{ | ||||
| 											overflowWrap: 'break-word', | ||||
| 										}} | ||||
| 									/> | ||||
| 								)) | ||||
| 								.reverse()} | ||||
| 						</ScrollableFlex> | ||||
| 					</Flex> | ||||
| 				)} | ||||
| 			</Flex> | ||||
| 		</Modal> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| export default URLSelector; | ||||
| @@ -75,14 +75,25 @@ export function setDevicePaths(devicePaths: string[]) { | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| export function addFailedDevicePath(devicePath: string) { | ||||
| 	const failedDevicePathsSet = new Set( | ||||
| export function addFailedDevicePath({ | ||||
| 	device, | ||||
| 	error, | ||||
| }: { | ||||
| 	device: sdk.scanner.adapters.DrivelistDrive; | ||||
| 	error: Error; | ||||
| }) { | ||||
| 	const failedDevicePathsMap = new Map( | ||||
| 		store.getState().toJS().failedDevicePaths, | ||||
| 	); | ||||
| 	failedDevicePathsSet.add(devicePath); | ||||
| 	failedDevicePathsMap.set(device.device, { | ||||
| 		description: device.description, | ||||
| 		device: device.device, | ||||
| 		devicePath: device.devicePath, | ||||
| 		...error, | ||||
| 	}); | ||||
| 	store.dispatch({ | ||||
| 		type: Actions.SET_FAILED_DEVICE_PATHS, | ||||
| 		data: Array.from(failedDevicePathsSet), | ||||
| 		data: Array.from(failedDevicePathsMap), | ||||
| 	}); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -188,12 +188,15 @@ function stateObserver(state: typeof DEFAULT_STATE) { | ||||
| 	} else { | ||||
| 		selectedDrivesPaths = s.devicePaths; | ||||
| 	} | ||||
| 	const failedDevicePaths = s.failedDevicePaths.map( | ||||
| 		([devicePath]: [string]) => devicePath, | ||||
| 	); | ||||
| 	const newLedsState = { | ||||
| 		step, | ||||
| 		sourceDrive: sourceDrivePath, | ||||
| 		availableDrives: availableDrivesPaths, | ||||
| 		selectedDrives: selectedDrivesPaths, | ||||
| 		failedDrives: s.failedDevicePaths, | ||||
| 		failedDrives: failedDevicePaths, | ||||
| 	}; | ||||
| 	if (!_.isEqual(newLedsState, ledsState)) { | ||||
| 		updateLeds(newLedsState); | ||||
|   | ||||
| @@ -72,24 +72,24 @@ export function getImage(): SourceMetadata | undefined { | ||||
| 	return store.getState().toJS().selection.image; | ||||
| } | ||||
|  | ||||
| export function getImagePath() { | ||||
| 	return getImage()?.path; | ||||
| export function getImagePath(): string | undefined { | ||||
| 	return store.getState().toJS().selection.image?.path; | ||||
| } | ||||
|  | ||||
| export function getImageSize() { | ||||
| 	return getImage()?.size; | ||||
| export function getImageSize(): number | undefined { | ||||
| 	return store.getState().toJS().selection.image?.size; | ||||
| } | ||||
|  | ||||
| export function getImageName() { | ||||
| 	return getImage()?.name; | ||||
| export function getImageName(): string | undefined { | ||||
| 	return store.getState().toJS().selection.image?.name; | ||||
| } | ||||
|  | ||||
| export function getImageLogo() { | ||||
| 	return getImage()?.logo; | ||||
| export function getImageLogo(): string | undefined { | ||||
| 	return store.getState().toJS().selection.image?.logo; | ||||
| } | ||||
|  | ||||
| export function getImageSupportUrl() { | ||||
| 	return getImage()?.supportUrl; | ||||
| export function getImageSupportUrl(): string | undefined { | ||||
| 	return store.getState().toJS().selection.image?.supportUrl; | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings'); | ||||
|  | ||||
| const JSON_INDENT = 2; | ||||
|  | ||||
| export const DEFAULT_WIDTH = 800; | ||||
| export const DEFAULT_HEIGHT = 480; | ||||
|  | ||||
| /** | ||||
|  * @summary Userdata directory path | ||||
|  * @description | ||||
| @@ -38,12 +41,15 @@ const JSON_INDENT = 2; | ||||
|  * NOTE: The ternary is due to this module being loaded both, | ||||
|  * Electron's main process and renderer process | ||||
|  */ | ||||
| const USER_DATA_DIR = electron.app | ||||
| 	? electron.app.getPath('userData') | ||||
| 	: electron.remote.app.getPath('userData'); | ||||
|  | ||||
| const app = electron.app || electron.remote.app; | ||||
|  | ||||
| const USER_DATA_DIR = app.getPath('userData'); | ||||
|  | ||||
| const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); | ||||
|  | ||||
| const DOWNLOADS_DIR = app.getPath('downloads'); | ||||
|  | ||||
| async function readConfigFile(filename: string): Promise<_.Dictionary<any>> { | ||||
| 	let contents = '{}'; | ||||
| 	try { | ||||
| @@ -80,6 +86,8 @@ const DEFAULT_SETTINGS: _.Dictionary<any> = { | ||||
| 	desktopNotifications: true, | ||||
| 	autoBlockmapping: true, | ||||
| 	decompressFirst: true, | ||||
| 	saveUrlImage: false, | ||||
| 	saveUrlImageTo: DOWNLOADS_DIR, | ||||
| }; | ||||
|  | ||||
| const settings = _.cloneDeep(DEFAULT_SETTINGS); | ||||
|   | ||||
| @@ -295,6 +295,7 @@ function storeReducer( | ||||
|  | ||||
| 			_.defaults(action.data, { | ||||
| 				cancelled: false, | ||||
| 				skip: false, | ||||
| 			}); | ||||
|  | ||||
| 			if (!_.isBoolean(action.data.cancelled)) { | ||||
| @@ -335,6 +336,12 @@ function storeReducer( | ||||
| 				); | ||||
| 			} | ||||
|  | ||||
| 			if (action.data.skip) { | ||||
| 				return state | ||||
| 					.set('isFlashing', false) | ||||
| 					.set('flashResults', Immutable.fromJS(action.data)); | ||||
| 			} | ||||
|  | ||||
| 			return state | ||||
| 				.set('isFlashing', false) | ||||
| 				.set('flashResults', Immutable.fromJS(action.data)) | ||||
|   | ||||
| @@ -131,6 +131,7 @@ function writerEnv() { | ||||
| } | ||||
|  | ||||
| interface FlashResults { | ||||
| 	skip?: boolean; | ||||
| 	cancelled?: boolean; | ||||
| } | ||||
|  | ||||
| @@ -140,12 +141,15 @@ async function performWrite( | ||||
| 	onProgress: sdk.multiWrite.OnProgressFunction, | ||||
| ): Promise<{ cancelled?: boolean }> { | ||||
| 	let cancelled = false; | ||||
| 	let skip = false; | ||||
| 	ipc.serve(); | ||||
| 	const { | ||||
| 		unmountOnSuccess, | ||||
| 		validateWriteOnSuccess, | ||||
| 		autoBlockmapping, | ||||
| 		decompressFirst, | ||||
| 		saveUrlImage, | ||||
| 		saveUrlImageTo, | ||||
| 	} = await settings.getAll(); | ||||
| 	return await new Promise((resolve, reject) => { | ||||
| 		ipc.server.on('error', (error) => { | ||||
| @@ -171,7 +175,7 @@ async function performWrite( | ||||
|  | ||||
| 		ipc.server.on('fail', ({ device, error }) => { | ||||
| 			if (device.devicePath) { | ||||
| 				flashState.addFailedDevicePath(device.devicePath); | ||||
| 				flashState.addFailedDevicePath({ device, error }); | ||||
| 			} | ||||
| 			handleErrorLogging(error, analyticsData); | ||||
| 		}); | ||||
| @@ -188,6 +192,11 @@ async function performWrite( | ||||
| 			cancelled = true; | ||||
| 		}); | ||||
|  | ||||
| 		ipc.server.on('skip', () => { | ||||
| 			terminateServer(); | ||||
| 			skip = true; | ||||
| 		}); | ||||
|  | ||||
| 		ipc.server.on('state', onProgress); | ||||
|  | ||||
| 		ipc.server.on('ready', (_data, socket) => { | ||||
| @@ -199,6 +208,8 @@ async function performWrite( | ||||
| 				autoBlockmapping, | ||||
| 				unmountOnSuccess, | ||||
| 				decompressFirst, | ||||
| 				saveUrlImage, | ||||
| 				saveUrlImageTo, | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| @@ -213,6 +224,7 @@ async function performWrite( | ||||
| 					environment: env, | ||||
| 				}); | ||||
| 				flashResults.cancelled = cancelled || results.cancelled; | ||||
| 				flashResults.skip = skip; | ||||
| 			} catch (error) { | ||||
| 				// This happens when the child is killed using SIGKILL | ||||
| 				const SIGKILL_EXIT_CODE = 137; | ||||
| @@ -229,6 +241,7 @@ async function performWrite( | ||||
| 			// This likely means the child died halfway through | ||||
| 			if ( | ||||
| 				!flashResults.cancelled && | ||||
| 				!flashResults.skip && | ||||
| 				!_.get(flashResults, ['results', 'bytesWritten']) | ||||
| 			) { | ||||
| 				reject( | ||||
| @@ -286,8 +299,7 @@ export async function flash( | ||||
| 	} catch (error) { | ||||
| 		flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); | ||||
| 		windowProgress.clear(); | ||||
| 		let { results } = flashState.getFlashResults(); | ||||
| 		results = results || {}; | ||||
| 		const { results = {} } = flashState.getFlashResults(); | ||||
| 		const eventData = { | ||||
| 			...analyticsData, | ||||
| 			errors: results.errors, | ||||
| @@ -306,7 +318,7 @@ export async function flash( | ||||
| 		}; | ||||
| 		analytics.logEvent('Elevation cancelled', eventData); | ||||
| 	} else { | ||||
| 		const { results } = flashState.getFlashResults(); | ||||
| 		const { results = {} } = flashState.getFlashResults(); | ||||
| 		const eventData = { | ||||
| 			...analyticsData, | ||||
| 			errors: results.errors, | ||||
| @@ -322,7 +334,8 @@ export async function flash( | ||||
| /** | ||||
|  * @summary Cancel write operation | ||||
|  */ | ||||
| export async function cancel() { | ||||
| export async function cancel(type: string) { | ||||
| 	const status = type.toLowerCase(); | ||||
| 	const drives = selectionState.getSelectedDevices(); | ||||
| 	const analyticsData = { | ||||
| 		image: selectionState.getImagePath(), | ||||
| @@ -332,7 +345,7 @@ export async function cancel() { | ||||
| 		flashInstanceUuid: flashState.getFlashUuid(), | ||||
| 		unmountOnSuccess: await settings.get('unmountOnSuccess'), | ||||
| 		validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), | ||||
| 		status: 'cancel', | ||||
| 		status, | ||||
| 	}; | ||||
| 	analytics.logEvent('Cancel', analyticsData); | ||||
|  | ||||
| @@ -342,7 +355,7 @@ export async function cancel() { | ||||
| 		// @ts-ignore (no Server.sockets in @types/node-ipc) | ||||
| 		const [socket] = ipc.server.sockets; | ||||
| 		if (socket !== undefined) { | ||||
| 			ipc.server.emit(socket, 'cancel'); | ||||
| 			ipc.server.emit(socket, status); | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		analytics.logException(error); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ export interface FlashState { | ||||
| 	percentage?: number; | ||||
| 	speed: number; | ||||
| 	position: number; | ||||
| 	type?: 'decompressing' | 'flashing' | 'verifying'; | ||||
| 	type?: 'decompressing' | 'flashing' | 'verifying' | 'downloading'; | ||||
| } | ||||
|  | ||||
| export function fromFlashState({ | ||||
| @@ -62,6 +62,12 @@ export function fromFlashState({ | ||||
| 		} else { | ||||
| 			return { status: 'Finishing...' }; | ||||
| 		} | ||||
| 	} else if (type === 'downloading') { | ||||
| 		if (percentage == null) { | ||||
| 			return { status: 'Downloading...' }; | ||||
| 		} else if (percentage < 100) { | ||||
| 			return { position: `${percentage}%`, status: 'Downloading...' }; | ||||
| 		} | ||||
| 	} | ||||
| 	return { status: 'Failed' }; | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,12 @@ async function mountSourceDrive() { | ||||
|  * Notice that by image, we mean *.img/*.iso/*.zip/etc files. | ||||
|  */ | ||||
| export async function selectImage(): Promise<string | undefined> { | ||||
| 	return await openDialog(); | ||||
| } | ||||
|  | ||||
| export async function openDialog( | ||||
| 	type: 'openFile' | 'openDirectory' = 'openFile', | ||||
| ) { | ||||
| 	await mountSourceDrive(); | ||||
| 	const options: electron.OpenDialogOptions = { | ||||
| 		// This variable is set when running in GNU/Linux from | ||||
| @@ -50,23 +56,26 @@ export async function selectImage(): Promise<string | undefined> { | ||||
| 		// | ||||
| 		// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 | ||||
| 		defaultPath: process.env.OWD, | ||||
| 		properties: ['openFile', 'treatPackageAsDirectory'], | ||||
| 		filters: [ | ||||
| 			{ | ||||
| 				name: 'OS Images', | ||||
| 				extensions: SUPPORTED_EXTENSIONS, | ||||
| 			}, | ||||
| 			{ | ||||
| 				name: 'All', | ||||
| 				extensions: ['*'], | ||||
| 			}, | ||||
| 		], | ||||
| 		properties: [type, 'treatPackageAsDirectory'], | ||||
| 		filters: | ||||
| 			type === 'openFile' | ||||
| 				? [ | ||||
| 						{ | ||||
| 							name: 'OS Images', | ||||
| 							extensions: SUPPORTED_EXTENSIONS, | ||||
| 						}, | ||||
| 						{ | ||||
| 							name: 'All', | ||||
| 							extensions: ['*'], | ||||
| 						}, | ||||
| 				  ] | ||||
| 				: undefined, | ||||
| 	}; | ||||
| 	const currentWindow = electron.remote.getCurrentWindow(); | ||||
| 	const [file] = ( | ||||
| 	const [path] = ( | ||||
| 		await electron.remote.dialog.showOpenDialog(currentWindow, options) | ||||
| 	).filePaths; | ||||
| 	return file; | ||||
| 	return path; | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -82,14 +82,12 @@ async function flashImageToDrive( | ||||
| 	try { | ||||
| 		await imageWriter.flash(image, drives); | ||||
| 		if (!flashState.wasLastFlashCancelled()) { | ||||
| 			const flashResults: any = flashState.getFlashResults(); | ||||
| 			const { | ||||
| 				results = { devices: { successful: 0, failed: 0 } }, | ||||
| 			} = flashState.getFlashResults(); | ||||
| 			notification.send( | ||||
| 				'Flash complete!', | ||||
| 				messages.info.flashComplete( | ||||
| 					basename, | ||||
| 					drives as any, | ||||
| 					flashResults.results.devices, | ||||
| 				), | ||||
| 				messages.info.flashComplete(basename, drives as any, results.devices), | ||||
| 				iconPath, | ||||
| 			); | ||||
| 			goToSuccess(); | ||||
|   | ||||
| @@ -25,7 +25,6 @@ import styled from 'styled-components'; | ||||
|  | ||||
| import FinishPage from '../../components/finish/finish'; | ||||
| import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; | ||||
| import { SafeWebview } from '../../components/safe-webview/safe-webview'; | ||||
| import { SettingsModal } from '../../components/settings/settings'; | ||||
| import { | ||||
| 	SourceMetadata, | ||||
| @@ -48,6 +47,8 @@ import { | ||||
| import { FlashStep } from './Flash'; | ||||
|  | ||||
| import EtcherSvg from '../../../assets/etcher.svg'; | ||||
| import { SafeWebview } from '../../components/safe-webview/safe-webview'; | ||||
| import { colors } from '../../theme'; | ||||
|  | ||||
| const Icon = styled(BaseIcon)` | ||||
| 	margin-right: 20px; | ||||
| @@ -87,9 +88,7 @@ const StepBorder = styled.div<{ | ||||
| 	position: relative; | ||||
| 	height: 2px; | ||||
| 	background-color: ${(props) => | ||||
| 		props.disabled | ||||
| 			? props.theme.colors.dark.disabled.foreground | ||||
| 			: props.theme.colors.dark.foreground}; | ||||
| 		props.disabled ? colors.dark.disabled.foreground : colors.dark.foreground}; | ||||
| 	width: 120px; | ||||
| 	top: 19px; | ||||
|  | ||||
| @@ -169,7 +168,104 @@ export class MainPage extends React.Component< | ||||
| 		const notFlashingOrSplitView = | ||||
| 			!this.state.isFlashing || !this.state.isWebviewShowing; | ||||
| 		return ( | ||||
| 			<> | ||||
| 			<Flex | ||||
| 				m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`} | ||||
| 				justifyContent="space-between" | ||||
| 			> | ||||
| 				{notFlashingOrSplitView && ( | ||||
| 					<> | ||||
| 						<SourceSelector flashing={this.state.isFlashing} /> | ||||
| 						<Flex> | ||||
| 							<StepBorder disabled={shouldDriveStepBeDisabled} left /> | ||||
| 						</Flex> | ||||
| 						<TargetSelector | ||||
| 							disabled={shouldDriveStepBeDisabled} | ||||
| 							hasDrive={this.state.hasDrive} | ||||
| 							flashing={this.state.isFlashing} | ||||
| 						/> | ||||
| 						<Flex> | ||||
| 							<StepBorder disabled={shouldFlashStepBeDisabled} right /> | ||||
| 						</Flex> | ||||
| 					</> | ||||
| 				)} | ||||
|  | ||||
| 				{this.state.isFlashing && this.state.isWebviewShowing && ( | ||||
| 					<Flex | ||||
| 						style={{ | ||||
| 							position: 'absolute', | ||||
| 							top: 0, | ||||
| 							left: 0, | ||||
| 							width: '36.2vw', | ||||
| 							height: '100vh', | ||||
| 							zIndex: 1, | ||||
| 							boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)', | ||||
| 						}} | ||||
| 					> | ||||
| 						<ReducedFlashingInfos | ||||
| 							imageLogo={this.state.imageLogo} | ||||
| 							imageName={this.state.imageName} | ||||
| 							imageSize={ | ||||
| 								typeof this.state.imageSize === 'number' | ||||
| 									? prettyBytes(this.state.imageSize) | ||||
| 									: '' | ||||
| 							} | ||||
| 							driveTitle={this.state.driveTitle} | ||||
| 							driveLabel={this.state.driveLabel} | ||||
| 							style={{ | ||||
| 								position: 'absolute', | ||||
| 								color: '#fff', | ||||
| 								left: 35, | ||||
| 								top: 72, | ||||
| 							}} | ||||
| 						/> | ||||
| 					</Flex> | ||||
| 				)} | ||||
| 				{this.state.isFlashing && this.state.featuredProjectURL && ( | ||||
| 					<SafeWebview | ||||
| 						src={this.state.featuredProjectURL} | ||||
| 						onWebviewShow={(isWebviewShowing: boolean) => { | ||||
| 							this.setState({ isWebviewShowing }); | ||||
| 						}} | ||||
| 						style={{ | ||||
| 							position: 'absolute', | ||||
| 							right: 0, | ||||
| 							bottom: 0, | ||||
| 							width: '63.8vw', | ||||
| 							height: '100vh', | ||||
| 						}} | ||||
| 					/> | ||||
| 				)} | ||||
|  | ||||
| 				<FlashStep | ||||
| 					goToSuccess={() => this.setState({ current: 'success' })} | ||||
| 					shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} | ||||
| 					isFlashing={this.state.isFlashing} | ||||
| 					step={state.type} | ||||
| 					percentage={state.percentage} | ||||
| 					position={state.position} | ||||
| 					failed={state.failed} | ||||
| 					speed={state.speed} | ||||
| 					eta={state.eta} | ||||
| 					style={{ zIndex: 1 }} | ||||
| 				/> | ||||
| 			</Flex> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private renderSuccess() { | ||||
| 		return ( | ||||
| 			<FinishPage | ||||
| 				goToMain={() => { | ||||
| 					flashState.resetState(); | ||||
| 					this.setState({ current: 'main' }); | ||||
| 				}} | ||||
| 			/> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	public render() { | ||||
| 		return ( | ||||
| 			<ThemedProvider style={{ height: '100%', width: '100%' }}> | ||||
| 				<Flex | ||||
| 					justifyContent="space-between" | ||||
| 					alignItems="center" | ||||
| @@ -233,117 +329,6 @@ export class MainPage extends React.Component< | ||||
| 						}} | ||||
| 					/> | ||||
| 				)} | ||||
|  | ||||
| 				<Flex | ||||
| 					m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`} | ||||
| 					justifyContent="space-between" | ||||
| 				> | ||||
| 					{notFlashingOrSplitView && ( | ||||
| 						<> | ||||
| 							<SourceSelector flashing={this.state.isFlashing} /> | ||||
| 							<Flex> | ||||
| 								<StepBorder disabled={shouldDriveStepBeDisabled} left /> | ||||
| 							</Flex> | ||||
| 							<TargetSelector | ||||
| 								disabled={shouldDriveStepBeDisabled} | ||||
| 								hasDrive={this.state.hasDrive} | ||||
| 								flashing={this.state.isFlashing} | ||||
| 							/> | ||||
| 							<Flex> | ||||
| 								<StepBorder disabled={shouldFlashStepBeDisabled} right /> | ||||
| 							</Flex> | ||||
| 						</> | ||||
| 					)} | ||||
|  | ||||
| 					{this.state.isFlashing && this.state.isWebviewShowing && ( | ||||
| 						<Flex | ||||
| 							style={{ | ||||
| 								position: 'absolute', | ||||
| 								top: 0, | ||||
| 								left: 0, | ||||
| 								width: '36.2vw', | ||||
| 								height: '100vh', | ||||
| 								zIndex: 1, | ||||
| 								boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)', | ||||
| 							}} | ||||
| 						> | ||||
| 							<ReducedFlashingInfos | ||||
| 								imageLogo={this.state.imageLogo} | ||||
| 								imageName={this.state.imageName} | ||||
| 								imageSize={ | ||||
| 									typeof this.state.imageSize === 'number' | ||||
| 										? prettyBytes(this.state.imageSize) | ||||
| 										: '' | ||||
| 								} | ||||
| 								driveTitle={this.state.driveTitle} | ||||
| 								driveLabel={this.state.driveLabel} | ||||
| 								style={{ | ||||
| 									position: 'absolute', | ||||
| 									color: '#fff', | ||||
| 									left: 35, | ||||
| 									top: 72, | ||||
| 								}} | ||||
| 							/> | ||||
| 						</Flex> | ||||
| 					)} | ||||
| 					{this.state.isFlashing && this.state.featuredProjectURL && ( | ||||
| 						<SafeWebview | ||||
| 							src={this.state.featuredProjectURL} | ||||
| 							onWebviewShow={(isWebviewShowing: boolean) => { | ||||
| 								this.setState({ isWebviewShowing }); | ||||
| 							}} | ||||
| 							style={{ | ||||
| 								position: 'absolute', | ||||
| 								right: 0, | ||||
| 								bottom: 0, | ||||
| 								width: '63.8vw', | ||||
| 								height: '100vh', | ||||
| 							}} | ||||
| 						/> | ||||
| 					)} | ||||
|  | ||||
| 					<FlashStep | ||||
| 						goToSuccess={() => this.setState({ current: 'success' })} | ||||
| 						shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} | ||||
| 						isFlashing={this.state.isFlashing} | ||||
| 						step={state.type} | ||||
| 						percentage={state.percentage} | ||||
| 						position={state.position} | ||||
| 						failed={state.failed} | ||||
| 						speed={state.speed} | ||||
| 						eta={state.eta} | ||||
| 						style={{ zIndex: 1 }} | ||||
| 					/> | ||||
| 				</Flex> | ||||
| 			</> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private renderSuccess() { | ||||
| 		return ( | ||||
| 			<Flex flexDirection="column" alignItems="center" height="100%"> | ||||
| 				<FinishPage | ||||
| 					goToMain={() => { | ||||
| 						flashState.resetState(); | ||||
| 						this.setState({ current: 'main' }); | ||||
| 					}} | ||||
| 				/> | ||||
| 				<SafeWebview | ||||
| 					src="https://www.balena.io/etcher/success-banner/" | ||||
| 					style={{ | ||||
| 						width: '100%', | ||||
| 						height: '320px', | ||||
| 						position: 'absolute', | ||||
| 						bottom: 0, | ||||
| 					}} | ||||
| 				/> | ||||
| 			</Flex> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	public render() { | ||||
| 		return ( | ||||
| 			<ThemedProvider style={{ height: '100%', width: '100%' }}> | ||||
| 				{this.state.current === 'main' | ||||
| 					? this.renderMain() | ||||
| 					: this.renderSuccess()} | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| import * as _ from 'lodash'; | ||||
| import * as React from 'react'; | ||||
| import { | ||||
| 	Alert as AlertBase, | ||||
| @@ -23,27 +24,16 @@ import { | ||||
| 	ButtonProps, | ||||
| 	Modal as ModalBase, | ||||
| 	Provider, | ||||
| 	Table as BaseTable, | ||||
| 	TableProps as BaseTableProps, | ||||
| 	Txt, | ||||
| 	Theme as renditionTheme, | ||||
| } from 'rendition'; | ||||
| import styled, { css } from 'styled-components'; | ||||
|  | ||||
| import { colors, theme } from './theme'; | ||||
|  | ||||
| const defaultTheme = { | ||||
| 	...renditionTheme, | ||||
| 	...theme, | ||||
| 	layer: { | ||||
| 		extend: () => ` | ||||
| 			> div:first-child { | ||||
| 				background-color: transparent; | ||||
| 			} | ||||
| 		`, | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| export const ThemedProvider = (props: any) => ( | ||||
| 	<Provider theme={defaultTheme} {...props}></Provider> | ||||
| 	<Provider theme={theme} {...props}></Provider> | ||||
| ); | ||||
|  | ||||
| export const BaseButton = styled(Button)` | ||||
| @@ -134,24 +124,23 @@ const modalFooterShadowCss = css` | ||||
| 	background-attachment: local, local, scroll, scroll; | ||||
| `; | ||||
|  | ||||
| export const Modal = styled(({ style, ...props }) => { | ||||
| export const Modal = styled(({ style, children, ...props }) => { | ||||
| 	return ( | ||||
| 		<Provider | ||||
| 			theme={{ | ||||
| 				...defaultTheme, | ||||
| 			theme={_.merge({}, theme, { | ||||
| 				header: { | ||||
| 					height: '50px', | ||||
| 				}, | ||||
| 				layer: { | ||||
| 					extend: () => ` | ||||
| 					${defaultTheme.layer.extend()} | ||||
| 						${theme.layer.extend()} | ||||
|  | ||||
| 					> div:last-child { | ||||
| 						top: 0; | ||||
| 					} | ||||
| 				`, | ||||
| 						> div:last-child { | ||||
| 							top: 0; | ||||
| 						} | ||||
| 					`, | ||||
| 				}, | ||||
| 			}} | ||||
| 			})} | ||||
| 		> | ||||
| 			<ModalBase | ||||
| 				position="top" | ||||
| @@ -167,7 +156,11 @@ export const Modal = styled(({ style, ...props }) => { | ||||
| 					...style, | ||||
| 				}} | ||||
| 				{...props} | ||||
| 			/> | ||||
| 			> | ||||
| 				<ScrollableFlex flexDirection="column" width="100%" height="90%"> | ||||
| 					{...children} | ||||
| 				</ScrollableFlex> | ||||
| 			</ModalBase> | ||||
| 		</Provider> | ||||
| 	); | ||||
| })` | ||||
| @@ -175,6 +168,11 @@ export const Modal = styled(({ style, ...props }) => { | ||||
| 		padding: 0; | ||||
| 		height: 100%; | ||||
|  | ||||
| 		> div:first-child { | ||||
| 			height: 81%; | ||||
| 			padding: 24px 30px 0; | ||||
| 		} | ||||
|  | ||||
| 		> h3 { | ||||
| 			margin: 0; | ||||
| 			padding: 24px 30px 0; | ||||
| @@ -188,11 +186,8 @@ export const Modal = styled(({ style, ...props }) => { | ||||
|  | ||||
| 		> div:nth-child(2) { | ||||
| 			height: 61%; | ||||
|  | ||||
| 			> div:not(.system-drive-alert) { | ||||
| 				padding: 0 30px; | ||||
| 				${modalFooterShadowCss} | ||||
| 			} | ||||
| 			padding: 0 30px; | ||||
| 			${modalFooterShadowCss} | ||||
| 		} | ||||
|  | ||||
| 		> div:last-child { | ||||
| @@ -249,3 +244,99 @@ export const Alert = styled((props) => ( | ||||
| 		display: none; | ||||
| 	} | ||||
| `; | ||||
|  | ||||
| export interface GenericTableProps<T> extends BaseTableProps<T> { | ||||
| 	refFn: (t: BaseTable<T>) => void; | ||||
| 	data: T[]; | ||||
| 	checkedRowsNumber?: number; | ||||
| 	multipleSelection: boolean; | ||||
| 	showWarnings?: boolean; | ||||
| } | ||||
|  | ||||
| const GenericTable: <T>( | ||||
| 	props: GenericTableProps<T>, | ||||
| ) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({ | ||||
| 	refFn, | ||||
| 	...props | ||||
| }: GenericTableProps<T>) => ( | ||||
| 	<div> | ||||
| 		<BaseTable<T> ref={refFn} {...props} /> | ||||
| 	</div> | ||||
| ); | ||||
|  | ||||
| function StyledTable<T>() { | ||||
| 	return styled((props: GenericTableProps<T>) => ( | ||||
| 		<GenericTable<T> {...props} /> | ||||
| 	))` | ||||
| 		[data-display='table-head'] | ||||
| 			> [data-display='table-row'] | ||||
| 			> [data-display='table-cell'] { | ||||
| 			position: sticky; | ||||
| 			background-color: #f8f9fd; | ||||
| 			top: 0; | ||||
| 			z-index: 1; | ||||
|  | ||||
| 			input[type='checkbox'] + div { | ||||
| 				display: ${(props) => (props.multipleSelection ? 'flex' : 'none')}; | ||||
|  | ||||
| 				${(props) => | ||||
| 					props.multipleSelection && | ||||
| 					props.checkedRowsNumber !== 0 && | ||||
| 					props.checkedRowsNumber !== props.data.length | ||||
| 						? ` | ||||
| 						font-weight: 600; | ||||
| 						color: ${colors.primary.foreground}; | ||||
| 						background: ${colors.primary.background}; | ||||
|  | ||||
| 						::after { | ||||
| 							content: '–'; | ||||
| 						} | ||||
| 						` | ||||
| 						: ''} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		[data-display='table-head'] > [data-display='table-row'], | ||||
| 		[data-display='table-body'] > [data-display='table-row'] { | ||||
| 			> [data-display='table-cell']:first-child { | ||||
| 				padding-left: 15px; | ||||
| 				width: 6%; | ||||
| 			} | ||||
|  | ||||
| 			> [data-display='table-cell']:last-child { | ||||
| 				padding-right: 0; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		[data-display='table-body'] > [data-display='table-row'] { | ||||
| 			&:nth-of-type(2n) { | ||||
| 				background: transparent; | ||||
| 			} | ||||
|  | ||||
| 			&[data-highlight='true'] { | ||||
| 				&.system { | ||||
| 					background-color: ${(props) => (props.showWarnings ? '#fff5e6' : '#e8f5fc')}; | ||||
| 				} | ||||
|  | ||||
| 				> [data-display='table-cell']:first-child { | ||||
| 					box-shadow: none; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		&& [data-display='table-row'] > [data-display='table-cell'] { | ||||
| 			padding: 6px 8px; | ||||
| 			color: #2a506f; | ||||
| 		} | ||||
|  | ||||
| 		input[type='checkbox'] + div { | ||||
| 			border-radius: ${(props) => (props.multipleSelection ? '4px' : '50%')}; | ||||
| 		} | ||||
| 	`; | ||||
| } | ||||
|  | ||||
| export const Table = <T extends {}>(props: GenericTableProps<T>) => { | ||||
| 	const TypedStyledFunctional = StyledTable<T>(); | ||||
| 	return <TypedStyledFunctional {...props} />; | ||||
| }; | ||||
|   | ||||
| @@ -14,6 +14,9 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| import * as _ from 'lodash'; | ||||
| import { Theme } from 'rendition'; | ||||
|  | ||||
| export const colors = { | ||||
| 	dark: { | ||||
| 		foreground: '#fff', | ||||
| @@ -67,8 +70,7 @@ export const colors = { | ||||
|  | ||||
| const font = 'SourceSansPro'; | ||||
|  | ||||
| export const theme = { | ||||
| 	colors, | ||||
| export const theme = _.merge({}, Theme, { | ||||
| 	font, | ||||
| 	global: { | ||||
| 		font: { | ||||
| @@ -109,4 +111,11 @@ export const theme = { | ||||
| 			} | ||||
| 		`, | ||||
| 	}, | ||||
| }; | ||||
| 	layer: { | ||||
| 		extend: () => ` | ||||
| 			> div:first-child { | ||||
| 				background-color: transparent; | ||||
| 			} | ||||
| 		`, | ||||
| 	}, | ||||
| }); | ||||
|   | ||||
							
								
								
									
										28
									
								
								lib/gui/app/utils/start-ellipsis.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								lib/gui/app/utils/start-ellipsis.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| /* | ||||
|  * Copyright 2020 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. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * @summary Truncate text from the start with an ellipsis | ||||
|  */ | ||||
| export function startEllipsis(input: string, limit: number): string { | ||||
| 	// Do nothing, the string doesn't need truncation. | ||||
| 	if (input.length <= limit) { | ||||
| 		return input; | ||||
| 	} | ||||
|  | ||||
| 	const lastPart = input.slice(input.length - limit, input.length); | ||||
| 	return `…${lastPart}`; | ||||
| } | ||||
| @@ -122,8 +122,8 @@ interface AutoUpdaterConfig { | ||||
|  | ||||
| async function createMainWindow() { | ||||
| 	const fullscreen = Boolean(await settings.get('fullscreen')); | ||||
| 	const defaultWidth = 800; | ||||
| 	const defaultHeight = 480; | ||||
| 	const defaultWidth = settings.DEFAULT_WIDTH; | ||||
| 	const defaultHeight = settings.DEFAULT_HEIGHT; | ||||
| 	let width = defaultWidth; | ||||
| 	let height = defaultHeight; | ||||
| 	if (fullscreen) { | ||||
| @@ -174,7 +174,13 @@ async function createMainWindow() { | ||||
| 		event.preventDefault(); | ||||
| 	}); | ||||
|  | ||||
| 	mainWindow.loadURL(`file://${path.join(__dirname, 'index.html')}`); | ||||
| 	mainWindow.loadURL( | ||||
| 		`file://${path.join( | ||||
| 			'/', | ||||
| 			...__dirname.split(path.sep).map(encodeURIComponent), | ||||
| 			'index.html', | ||||
| 		)}`, | ||||
| 	); | ||||
|  | ||||
| 	const page = mainWindow.webContents; | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,8 @@ | ||||
| import { Drive as DrivelistDrive } from 'drivelist'; | ||||
| import * as sdk from 'etcher-sdk'; | ||||
| import { cleanupTmpFiles } from 'etcher-sdk/build/tmp'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import * as _ from 'lodash'; | ||||
| import * as ipc from 'node-ipc'; | ||||
| import { totalmem } from 'os'; | ||||
|  | ||||
| @@ -55,8 +57,9 @@ function log(message: string) { | ||||
| /** | ||||
|  * @summary Terminate the child writer process | ||||
|  */ | ||||
| function terminate(exitCode: number) { | ||||
| async function terminate(exitCode: number) { | ||||
| 	ipc.disconnect(IPC_SERVER_ID); | ||||
| 	await cleanupTmpFiles(Date.now()); | ||||
| 	process.nextTick(() => { | ||||
| 		process.exit(exitCode || SUCCESS); | ||||
| 	}); | ||||
| @@ -68,7 +71,7 @@ function terminate(exitCode: number) { | ||||
| async function handleError(error: Error) { | ||||
| 	ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); | ||||
| 	await delay(DISCONNECT_DELAY); | ||||
| 	terminate(GENERAL_ERROR); | ||||
| 	await terminate(GENERAL_ERROR); | ||||
| } | ||||
|  | ||||
| interface WriteResult { | ||||
| @@ -136,8 +139,10 @@ async function writeAndValidate({ | ||||
| 		sourceMetadata, | ||||
| 	}; | ||||
| 	for (const [destination, error] of failures) { | ||||
| 		const err = error as Error & { device: string }; | ||||
| 		err.device = (destination as sdk.sourceDestination.BlockDevice).device; | ||||
| 		const err = error as Error & { device: string; description: string }; | ||||
| 		const drive = destination as sdk.sourceDestination.BlockDevice; | ||||
| 		err.device = drive.device; | ||||
| 		err.description = drive.description; | ||||
| 		result.errors.push(err); | ||||
| 	} | ||||
| 	return result; | ||||
| @@ -151,6 +156,13 @@ interface WriteOptions { | ||||
| 	autoBlockmapping: boolean; | ||||
| 	decompressFirst: boolean; | ||||
| 	SourceType: string; | ||||
| 	saveUrlImage: boolean; | ||||
| 	saveUrlImageTo: string; | ||||
| } | ||||
|  | ||||
| interface ProgressState | ||||
| 	extends Omit<sdk.multiWrite.MultiDestinationProgress, 'type'> { | ||||
| 	type: sdk.multiWrite.MultiDestinationProgress['type'] | 'downloading'; | ||||
| } | ||||
|  | ||||
| ipc.connectTo(IPC_SERVER_ID, () => { | ||||
| @@ -163,22 +175,22 @@ ipc.connectTo(IPC_SERVER_ID, () => { | ||||
| 	// no flashing information is available, then it will | ||||
| 	// assume that the child died halfway through. | ||||
|  | ||||
| 	process.once('SIGINT', () => { | ||||
| 		terminate(SUCCESS); | ||||
| 	process.once('SIGINT', async () => { | ||||
| 		await terminate(SUCCESS); | ||||
| 	}); | ||||
|  | ||||
| 	process.once('SIGTERM', () => { | ||||
| 		terminate(SUCCESS); | ||||
| 	process.once('SIGTERM', async () => { | ||||
| 		await terminate(SUCCESS); | ||||
| 	}); | ||||
|  | ||||
| 	// The IPC server failed. Abort. | ||||
| 	ipc.of[IPC_SERVER_ID].on('error', () => { | ||||
| 		terminate(SUCCESS); | ||||
| 	ipc.of[IPC_SERVER_ID].on('error', async () => { | ||||
| 		await terminate(SUCCESS); | ||||
| 	}); | ||||
|  | ||||
| 	// The IPC server was disconnected. Abort. | ||||
| 	ipc.of[IPC_SERVER_ID].on('disconnect', () => { | ||||
| 		terminate(SUCCESS); | ||||
| 	ipc.of[IPC_SERVER_ID].on('disconnect', async () => { | ||||
| 		await terminate(SUCCESS); | ||||
| 	}); | ||||
|  | ||||
| 	ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => { | ||||
| @@ -188,7 +200,7 @@ ipc.connectTo(IPC_SERVER_ID, () => { | ||||
| 		 * @example | ||||
| 		 * writer.on('progress', onProgress) | ||||
| 		 */ | ||||
| 		const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => { | ||||
| 		const onProgress = (state: ProgressState) => { | ||||
| 			ipc.of[IPC_SERVER_ID].emit('state', state); | ||||
| 		}; | ||||
|  | ||||
| @@ -203,11 +215,20 @@ ipc.connectTo(IPC_SERVER_ID, () => { | ||||
| 			log('Abort'); | ||||
| 			ipc.of[IPC_SERVER_ID].emit('abort'); | ||||
| 			await delay(DISCONNECT_DELAY); | ||||
| 			terminate(exitCode); | ||||
| 			await terminate(exitCode); | ||||
| 		}; | ||||
|  | ||||
| 		const onSkip = async () => { | ||||
| 			log('Skip validation'); | ||||
| 			ipc.of[IPC_SERVER_ID].emit('skip'); | ||||
| 			await delay(DISCONNECT_DELAY); | ||||
| 			await terminate(exitCode); | ||||
| 		}; | ||||
|  | ||||
| 		ipc.of[IPC_SERVER_ID].on('cancel', onAbort); | ||||
|  | ||||
| 		ipc.of[IPC_SERVER_ID].on('skip', onSkip); | ||||
|  | ||||
| 		/** | ||||
| 		 * @summary Failure handler (non-fatal errors) | ||||
| 		 * @param {SourceDestination} destination - destination | ||||
| @@ -257,7 +278,16 @@ ipc.connectTo(IPC_SERVER_ID, () => { | ||||
| 						path: imagePath, | ||||
| 					}); | ||||
| 				} else { | ||||
| 					source = new Http({ url: imagePath, avoidRandomAccess: true }); | ||||
| 					if (options.saveUrlImage) { | ||||
| 						source = await saveFileBeforeFlash( | ||||
| 							imagePath, | ||||
| 							options.saveUrlImageTo, | ||||
| 							onProgress, | ||||
| 							onFail, | ||||
| 						); | ||||
| 					} else { | ||||
| 						source = new Http({ url: imagePath, avoidRandomAccess: true }); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			const results = await writeAndValidate({ | ||||
| @@ -275,7 +305,7 @@ ipc.connectTo(IPC_SERVER_ID, () => { | ||||
| 			}); | ||||
| 			ipc.of[IPC_SERVER_ID].emit('done', { results }); | ||||
| 			await delay(DISCONNECT_DELAY); | ||||
| 			terminate(exitCode); | ||||
| 			await terminate(exitCode); | ||||
| 		} catch (error) { | ||||
| 			log(`Error: ${error.message}`); | ||||
| 			exitCode = GENERAL_ERROR; | ||||
| @@ -290,3 +320,43 @@ ipc.connectTo(IPC_SERVER_ID, () => { | ||||
| 		ipc.of[IPC_SERVER_ID].emit('ready', {}); | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| async function saveFileBeforeFlash( | ||||
| 	imagePath: string, | ||||
| 	saveUrlImageTo: string, | ||||
| 	onProgress: (state: ProgressState) => void, | ||||
| 	onFail: ( | ||||
| 		destination: sdk.sourceDestination.SourceDestination, | ||||
| 		error: Error, | ||||
| 	) => void, | ||||
| ) { | ||||
| 	const urlImage = new Http({ url: imagePath, avoidRandomAccess: true }); | ||||
| 	const source = await urlImage.getInnerSource(); | ||||
| 	const metadata = await source.getMetadata(); | ||||
| 	const fileName = `${saveUrlImageTo}/${metadata.name}`; | ||||
| 	let alreadyDownloaded = false; | ||||
| 	try { | ||||
| 		alreadyDownloaded = (await fs.stat(fileName)).isFile(); | ||||
| 	} catch (error) { | ||||
| 		if (error.code !== 'ENOENT') { | ||||
| 			throw error; | ||||
| 		} | ||||
| 	} | ||||
| 	if (!alreadyDownloaded) { | ||||
| 		await sdk.multiWrite.decompressThenFlash({ | ||||
| 			source, | ||||
| 			destinations: [new File({ path: fileName, write: true })], | ||||
| 			onProgress: (progress) => { | ||||
| 				onProgress({ | ||||
| 					...progress, | ||||
| 					type: 'downloading', | ||||
| 				}); | ||||
| 			}, | ||||
| 			onFail: (...args) => { | ||||
| 				onFail(...args); | ||||
| 			}, | ||||
| 			verify: true, | ||||
| 		}); | ||||
| 	} | ||||
| 	return new File({ path: fileName }); | ||||
| } | ||||
|   | ||||
							
								
								
									
										170
									
								
								npm-shrinkwrap.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										170
									
								
								npm-shrinkwrap.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "balena-etcher", | ||||
|   "version": "1.5.107", | ||||
|   "version": "1.5.109", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
| @@ -1570,34 +1570,32 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@react-google-maps/api": { | ||||
|       "version": "1.9.12", | ||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.9.12.tgz", | ||||
|       "integrity": "sha512-YpYZOMduxiQIt8+njdffoqD4fYdOugudoafnAD1N+mEUrVnFlslUPMQ+gOJwuYdlkTAR5NZUbCt80LJWEN+ZnA==", | ||||
|       "version": "1.10.1", | ||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.10.1.tgz", | ||||
|       "integrity": "sha512-hb8urUcwZw99Cu3yQnZWUbXjR1Ym/8C21kSX6B02I29l6DXNxDbJ5Jo/T5swhnizPKY7TNhR1oTctC/HY7SQWA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@react-google-maps/infobox": "1.9.11", | ||||
|         "@react-google-maps/marker-clusterer": "1.9.11", | ||||
|         "acorn": "7.4.0", | ||||
|         "acorn-jsx": "^5.2.0", | ||||
|         "@react-google-maps/infobox": "1.10.0", | ||||
|         "@react-google-maps/marker-clusterer": "1.10.0", | ||||
|         "invariant": "2.2.4" | ||||
|       } | ||||
|     }, | ||||
|     "@react-google-maps/infobox": { | ||||
|       "version": "1.9.11", | ||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.9.11.tgz", | ||||
|       "integrity": "sha512-22ewm+OpOh69ikypG29idsdRz2OWeFsN+8zvYBzSETxKP782rmUGqhSIvXXmHa8TOcktm7EaEqOWWvZwaxymag==", | ||||
|       "version": "1.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.10.0.tgz", | ||||
|       "integrity": "sha512-MhT2nMmjeG7TCxRv/JdylDyNd/n66ggSQQhTWVjJJTtdB/xqd0T8BHCkBWDN9uF0i0yCZzMFl2P2Y1zJ+xppBg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@react-google-maps/marker-clusterer": { | ||||
|       "version": "1.9.11", | ||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.9.11.tgz", | ||||
|       "integrity": "sha512-yIABKlkORju131efXUZs/tL7FCK9IXtvy2M9SQRZy/mwgoOIYeoJlPPaBjn81DQqZLRj6AdAocydk+MnjWqFiQ==", | ||||
|       "version": "1.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.10.0.tgz", | ||||
|       "integrity": "sha512-3GLVgeXNStVcdiLMxzi3cBjr32ctlexLPPGQguwcYd6yPLaCcnVCwyzhV68KvL00xqOAD1c3aABV9EGgY8u6Qw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@rjsf/core": { | ||||
|       "version": "2.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.3.0.tgz", | ||||
|       "integrity": "sha512-OZKYHt9tjKhzOH4CvsPiCwepuIacqI++cNmnL2fsxh1IF+uEWGlo3NLDWhhSaBbOv9jps6a5YQcLbLtjNuSwug==", | ||||
|       "version": "2.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.4.0.tgz", | ||||
|       "integrity": "sha512-8zlydBkGldOxGXFEwNGFa1gzTxpcxaYn7ofegcu8XHJ7IKMCfpnU3ABg+H3eml1KZCX3FODmj1tHFJKuTmfynw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@babel/runtime-corejs2": "^7.8.7", | ||||
| @@ -2180,9 +2178,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@types/react-native": { | ||||
|       "version": "0.63.9", | ||||
|       "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.9.tgz", | ||||
|       "integrity": "sha512-6ec/z9zjAkFH3rD1RYqbrA/Lj+jux6bumWCte4yRy3leyelTdqtmOd2Ph+86IXQQzsIArEMBwmraAbNQ0J3UAA==", | ||||
|       "version": "0.63.18", | ||||
|       "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.18.tgz", | ||||
|       "integrity": "sha512-WwEWqmHiqFn61M1FZR/+frj+E8e2o8i5cPqu9mjbjtZS/gBfCKVESF2ai/KAlaQECkkWkx/nMJeCc5eHMmLQgw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@types/react": "*" | ||||
| @@ -2237,9 +2235,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@types/styled-components": { | ||||
|       "version": "5.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz", | ||||
|       "integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==", | ||||
|       "version": "5.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.3.tgz", | ||||
|       "integrity": "sha512-HGpirof3WOhiX17lb61Q/tpgqn48jxO8EfZkdJ8ueYqwLbK2AHQe/G08DasdA2IdKnmwOIP1s9X2bopxKXgjRw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@types/hoist-non-react-statics": "*", | ||||
| @@ -2692,18 +2690,6 @@ | ||||
|       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "acorn": { | ||||
|       "version": "7.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", | ||||
|       "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "acorn-jsx": { | ||||
|       "version": "5.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", | ||||
|       "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "agent-base": { | ||||
|       "version": "4.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", | ||||
| @@ -5281,6 +5267,12 @@ | ||||
|         "assert-plus": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "date-fns": { | ||||
|       "version": "2.16.1", | ||||
|       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", | ||||
|       "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "de-indent": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", | ||||
| @@ -5496,15 +5488,6 @@ | ||||
|         "minimalistic-assert": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "detab": { | ||||
|       "version": "2.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.3.tgz", | ||||
|       "integrity": "sha512-Up8P0clUVwq0FnFjDclzZsy9PadzRn5FFxrr47tQQvMHqyiFYVbpH8oXDzWtF0Q7pYy3l+RPmtBl+BsFF6wH0A==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "repeat-string": "^1.5.4" | ||||
|       } | ||||
|     }, | ||||
|     "detect-file": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", | ||||
| @@ -6871,9 +6854,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "ext2fs": { | ||||
|       "version": "2.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/ext2fs/-/ext2fs-2.0.4.tgz", | ||||
|       "integrity": "sha512-7ILtkKb6j9L+nR1qO4zCiy6aZulzKu7dO82na+qXwc6KEoEr23u/u476/thebbPcvYJMv71I7FebJv8P4MNjHw==", | ||||
|       "version": "2.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/ext2fs/-/ext2fs-2.0.5.tgz", | ||||
|       "integrity": "sha512-qNv+XrXrauspqoUYRgcKV7HNkoDAAY/KU6nZHGB8Y2tT553fiMtiZd4VYOdxd+0zrNZozi+0fJjLbiGBnEGJUw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "bindings": "^1.3.0", | ||||
| @@ -8939,9 +8922,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "json-e": { | ||||
|       "version": "4.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.1.0.tgz", | ||||
|       "integrity": "sha512-Jb8kMB1lICgjAAppv+q0EFFovOPdjE3htb7pt9+uE2j3J1W5ZCuBOmAdGi0OUetCZ4wqSO6qT/Np36XDRjHH7w==", | ||||
|       "version": "4.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.3.0.tgz", | ||||
|       "integrity": "sha512-E3zcmx6pHsBgQ4ZztQNG4OAZHreBZfGBrg68kv9nGOkRqAdKfs792asP/wp9Fayfx1THDiHKYStqWJj/N7Bb9A==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "json-stable-stringify-without-jsonify": "^1.0.1" | ||||
| @@ -9749,18 +9732,15 @@ | ||||
|       } | ||||
|     }, | ||||
|     "mdast-util-to-hast": { | ||||
|       "version": "9.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.0.tgz", | ||||
|       "integrity": "sha512-Akl2Vi9y9cSdr19/Dfu58PVwifPXuFt1IrHe7l+Crme1KvgUT+5z+cHLVcQVGCiNTZZcdqjnuv9vPkGsqWytWA==", | ||||
|       "version": "9.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.1.tgz", | ||||
|       "integrity": "sha512-vpMWKFKM2mnle+YbNgDXxx95vv0CoLU0v/l3F5oFAG5DV7qwkZVWA206LsAdOnEVyf5vQcLnb3cWJywu7mUxsQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@types/mdast": "^3.0.0", | ||||
|         "@types/unist": "^2.0.3", | ||||
|         "collapse-white-space": "^1.0.0", | ||||
|         "detab": "^2.0.0", | ||||
|         "mdast-util-definitions": "^3.0.0", | ||||
|         "mdurl": "^1.0.0", | ||||
|         "trim-lines": "^1.0.0", | ||||
|         "unist-builder": "^2.0.0", | ||||
|         "unist-util-generated": "^1.0.0", | ||||
|         "unist-util-position": "^3.0.0", | ||||
| @@ -9842,9 +9822,9 @@ | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "crypto-random-string": { | ||||
|           "version": "3.2.0", | ||||
|           "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.2.0.tgz", | ||||
|           "integrity": "sha512-8vPu5bsKaq2uKRy3OL7h1Oo7RayAWB8sYexLKAqvCXVib8SxgbmoF1IN4QMKjBv8uI8mp5gPPMbiRah25GMrVQ==", | ||||
|           "version": "3.3.0", | ||||
|           "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.0.tgz", | ||||
|           "integrity": "sha512-teWAwfMb1d6brahYyKqcBEb5Yp8PJPvPOdOonXDnvaKOTmKDFNVE8E3Y2XQuzjNV/3XMwHbrX9fHWvrhRKt4Gg==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "type-fest": "^0.8.1" | ||||
| @@ -11897,9 +11877,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "polished": { | ||||
|       "version": "3.6.5", | ||||
|       "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.5.tgz", | ||||
|       "integrity": "sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ==", | ||||
|       "version": "3.6.6", | ||||
|       "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.6.tgz", | ||||
|       "integrity": "sha512-yiB2ims2DZPem0kCD6V0wnhcVGFEhNh0Iw0axNpKU+oSAgFt6yx6HxIT23Qg0WWvgS379cS35zT4AOyZZRzpQQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.9.2" | ||||
| @@ -12511,9 +12491,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "react-notifications-component": { | ||||
|       "version": "2.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.0.tgz", | ||||
|       "integrity": "sha512-0IhtgqAmsKSyjY1wBUxciUVXiYGRr5BRdn67pYDlkqq9ORF98NZekpG7/MNX0BzzfGvt9Wg7rFhT1BtwOvvLLg==", | ||||
|       "version": "2.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.1.tgz", | ||||
|       "integrity": "sha512-RloHzm15egnuPihf8PvldIEvPQoT9+5BE9UxCNTt+GfsWeI3SEZKyaX9mq90v899boqteLiOI736Zd4tXtl7Tg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "prop-types": "^15.6.2" | ||||
| @@ -12660,6 +12640,21 @@ | ||||
|       "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "regexp-match-indices": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", | ||||
|       "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "regexp-tree": "^0.1.11" | ||||
|       } | ||||
|     }, | ||||
|     "regexp-tree": { | ||||
|       "version": "0.1.21", | ||||
|       "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.21.tgz", | ||||
|       "integrity": "sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "regexpu-core": { | ||||
|       "version": "4.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", | ||||
| @@ -12827,9 +12822,9 @@ | ||||
|       "optional": true | ||||
|     }, | ||||
|     "rendition": { | ||||
|       "version": "18.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.4.1.tgz", | ||||
|       "integrity": "sha512-mV/0p+M8XR/Xa/ZFzgflZPHelpuONiTSa/CMMuHkmXR7vhF7tB2ORxLRc/DbymmdN6cWQwXAyA81t9TDAOhgVQ==", | ||||
|       "version": "18.8.3", | ||||
|       "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.8.3.tgz", | ||||
|       "integrity": "sha512-kDuXFheXY9KlSvIMdB4Er2OeAnwgj9aya5Xu43hwpXxC4KlFlNKqQNmcOvKLc/Fk9dyw04TKOr1SbXyM148yRg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@fortawesome/fontawesome-svg-core": "^1.2.25", | ||||
| @@ -12855,6 +12850,7 @@ | ||||
|         "color": "^3.1.2", | ||||
|         "color-hash": "^1.0.3", | ||||
|         "copy-to-clipboard": "^3.0.8", | ||||
|         "date-fns": "^2.16.1", | ||||
|         "grommet": "^2.14.0", | ||||
|         "hast-util-sanitize": "^3.0.0", | ||||
|         "json-e": "^4.1.0", | ||||
| @@ -12869,6 +12865,7 @@ | ||||
|         "react-simplemde-editor": "^4.1.1", | ||||
|         "recompose": "0.26.0", | ||||
|         "regex-parser": "^2.2.7", | ||||
|         "regexp-match-indices": "^1.0.2", | ||||
|         "rehype-raw": "^4.0.2", | ||||
|         "rehype-react": "^6.1.0", | ||||
|         "rehype-sanitize": "^3.0.1", | ||||
| @@ -12885,9 +12882,9 @@ | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/node": { | ||||
|           "version": "13.13.15", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz", | ||||
|           "integrity": "sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw==", | ||||
|           "version": "13.13.20", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.20.tgz", | ||||
|           "integrity": "sha512-1kx55tU3AvGX2Cjk2W4GMBxbgIz892V+X10S2gUreIAq8qCWgaQH+tZBOWc0bi2BKFhQt+CX0BTx28V9QPNa+A==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "uuid": { | ||||
| @@ -14257,9 +14254,8 @@ | ||||
|       } | ||||
|     }, | ||||
|     "sudo-prompt": { | ||||
|       "version": "9.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", | ||||
|       "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", | ||||
|       "version": "github:zvin/sudo-prompt#81cab70c1f3f816b71539c4c5d7ecf1309094f8c", | ||||
|       "from": "github:zvin/sudo-prompt#workaround-windows-amperstand-in-username", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "sumchecker": { | ||||
| @@ -14746,12 +14742,6 @@ | ||||
|       "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "trim-lines": { | ||||
|       "version": "1.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-1.1.3.tgz", | ||||
|       "integrity": "sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "trim-trailing-lines": { | ||||
|       "version": "1.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", | ||||
| @@ -15036,9 +15026,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "uglify-js": { | ||||
|       "version": "3.10.2", | ||||
|       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.2.tgz", | ||||
|       "integrity": "sha512-GXCYNwqoo0MbLARghYjxVBxDCnU0tLqN7IPLdHHbibCb1NI5zBkU2EPcy/GaVxc0BtTjqyGXJCINe6JMR2Dpow==", | ||||
|       "version": "3.10.4", | ||||
|       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz", | ||||
|       "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "unbzip2-stream": { | ||||
| @@ -16467,9 +16457,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "whatwg-fetch": { | ||||
|       "version": "3.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", | ||||
|       "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==", | ||||
|       "version": "3.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", | ||||
|       "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "which": { | ||||
| @@ -16661,9 +16651,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "xterm": { | ||||
|       "version": "4.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.8.1.tgz", | ||||
|       "integrity": "sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ==", | ||||
|       "version": "4.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz", | ||||
|       "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "xterm-addon-fit": { | ||||
| @@ -16775,4 +16765,4 @@ | ||||
|       "dev": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ | ||||
|   "name": "balena-etcher", | ||||
|   "private": true, | ||||
|   "displayName": "balenaEtcher", | ||||
|   "version": "1.5.107", | ||||
|   "version": "1.5.109", | ||||
|   "packageType": "local", | ||||
|   "main": "generated/etcher.js", | ||||
|   "description": "Flash OS images to SD cards and USB drives, safely and easily.", | ||||
| @@ -94,7 +94,7 @@ | ||||
|     "react": "^16.8.5", | ||||
|     "react-dom": "^16.8.5", | ||||
|     "redux": "^4.0.5", | ||||
|     "rendition": "^18.4.1", | ||||
|     "rendition": "^18.8.3", | ||||
|     "resin-corvus": "^2.0.5", | ||||
|     "semver": "^7.3.2", | ||||
|     "simple-progress-webpack-plugin": "^1.1.2", | ||||
| @@ -102,7 +102,7 @@ | ||||
|     "spectron": "^11.0.0", | ||||
|     "string-replace-loader": "^2.3.0", | ||||
|     "styled-components": "^5.1.0", | ||||
|     "sudo-prompt": "^9.0.0", | ||||
|     "sudo-prompt": "github:zvin/sudo-prompt#workaround-windows-amperstand-in-username", | ||||
|     "sys-class-rgb-led": "^2.1.0", | ||||
|     "tmp": "^0.2.1", | ||||
|     "ts-loader": "^8.0.0", | ||||
|   | ||||
| @@ -393,6 +393,7 @@ describe('Model: flashState', function () { | ||||
|  | ||||
| 				expect(flashResults).to.deep.equal({ | ||||
| 					cancelled: false, | ||||
| 					skip: false, | ||||
| 					sourceChecksum: '1234', | ||||
| 				}); | ||||
| 			}); | ||||
|   | ||||
| @@ -700,11 +700,6 @@ describe('Shared: DriveConstraints', function () { | ||||
| 				}); | ||||
|  | ||||
| 				it('should return false if the drive is not large enough and is a source drive', function () { | ||||
| 					console.log('YAYYY', { | ||||
| 						...image, | ||||
| 						path: path.join(this.mountpoint, 'rpi.img'), | ||||
| 						size: 5000000000, | ||||
| 					}); | ||||
| 					expect( | ||||
| 						constraints.isDriveValid(this.drive, { | ||||
| 							...image, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user