mirror of
				https://github.com/balena-io/etcher.git
				synced 2025-10-31 14:08:31 +00:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
			v1.7.0
			...
			save-url-i
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f6ce9a217d | ||
|   | fce2d94df7 | ||
|   | 3feb22ee66 | ||
|   | b80a6b2feb | ||
|   | b4e6970119 | ||
|   | 2e3978b3c9 | ||
|   | c6cd421f17 | ||
|   | c3296eed54 | ||
|   | 153e37b9dc | ||
|   | 78aca6a19f | ||
|   | 27695babfd | ||
|   | 06a96db72d | ||
|   | 6584cef774 | ||
|   | 3c77800b1d | ||
|   | 74a78076cf | ||
|   | 8ff8b02f37 | 
| @@ -356,6 +356,16 @@ async function main() { | |||||||
| 	ReactDOM.render( | 	ReactDOM.render( | ||||||
| 		React.createElement(MainPage), | 		React.createElement(MainPage), | ||||||
| 		document.getElementById('main'), | 		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 ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; | ||||||
| import * as sourceDestination from 'etcher-sdk/build/source-destination/'; | import * as sourceDestination from 'etcher-sdk/build/source-destination/'; | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition'; | ||||||
| 	Flex, |  | ||||||
| 	ModalProps, |  | ||||||
| 	Txt, |  | ||||||
| 	Badge, |  | ||||||
| 	Link, |  | ||||||
| 	Table, |  | ||||||
| 	TableColumn, |  | ||||||
| } from 'rendition'; |  | ||||||
| import styled from 'styled-components'; | import styled from 'styled-components'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
| @@ -43,7 +35,12 @@ import { getImage, isDriveSelected } from '../../models/selection-state'; | |||||||
| import { store } from '../../models/store'; | import { store } from '../../models/store'; | ||||||
| import { logEvent, logException } from '../../modules/analytics'; | import { logEvent, logException } from '../../modules/analytics'; | ||||||
| import { open as openExternal } from '../../os/open-external/services/open-external'; | 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 DriveSVGIcon from '../../../assets/tgt.svg'; | ||||||
| import { SourceMetadata } from '../source-selector/source-selector'; | import { SourceMetadata } from '../source-selector/source-selector'; | ||||||
| @@ -75,29 +72,14 @@ function isDrivelistDrive(drive: Drive): drive is DrivelistDrive { | |||||||
| 	return typeof (drive as DrivelistDrive).size === 'number'; | 	return typeof (drive as DrivelistDrive).size === 'number'; | ||||||
| } | } | ||||||
|  |  | ||||||
| const DrivesTable = styled(({ refFn, ...props }) => ( | const DrivesTable = styled((props: GenericTableProps<Drive>) => ( | ||||||
| 	<div> | 	<Table<Drive> {...props} /> | ||||||
| 		<Table<Drive> ref={refFn} {...props} /> |  | ||||||
| 	</div> |  | ||||||
| ))` | ))` | ||||||
| 	[data-display='table-head'] | 	[data-display='table-head'], | ||||||
| 		> [data-display='table-row'] | 	[data-display='table-body'] { | ||||||
| 		> [data-display='table-cell'] { | 		> [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) { | 			&:nth-child(2) { | ||||||
| 			width: 38%; | 				width: 32%; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			&:nth-child(3) { | 			&:nth-child(3) { | ||||||
| @@ -112,36 +94,6 @@ const DrivesTable = styled(({ refFn, ...props }) => ( | |||||||
| 				width: 32%; | 				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-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: ${({ multipleSelection }) => |  | ||||||
| 			multipleSelection ? '4px' : '50%'}; |  | ||||||
| 	} | 	} | ||||||
| `; | `; | ||||||
|  |  | ||||||
| @@ -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() { | 	componentDidMount() { | ||||||
| 		this.unsubscribe = store.subscribe(() => { | 		this.unsubscribe = store.subscribe(() => { | ||||||
| 			const drives = getDrives(); | 			const drives = getDrives(); | ||||||
| @@ -453,7 +415,6 @@ export class DriveSelector extends React.Component< | |||||||
| 				}} | 				}} | ||||||
| 				{...props} | 				{...props} | ||||||
| 			> | 			> | ||||||
| 				<Flex width="100%" height="90%"> |  | ||||||
| 				{!hasAvailableDrives() ? ( | 				{!hasAvailableDrives() ? ( | ||||||
| 					<Flex | 					<Flex | ||||||
| 						flexDirection="column" | 						flexDirection="column" | ||||||
| @@ -465,13 +426,14 @@ export class DriveSelector extends React.Component< | |||||||
| 						<b>{this.props.emptyListLabel}</b> | 						<b>{this.props.emptyListLabel}</b> | ||||||
| 					</Flex> | 					</Flex> | ||||||
| 				) : ( | 				) : ( | ||||||
| 						<ScrollableFlex flexDirection="column" width="100%"> | 					<> | ||||||
| 						<DrivesTable | 						<DrivesTable | ||||||
| 								refFn={(t: Table<Drive>) => { | 							refFn={(t) => { | ||||||
| 								if (t !== null) { | 								if (t !== null) { | ||||||
| 									t.setRowSelection(selectedList); | 									t.setRowSelection(selectedList); | ||||||
| 								} | 								} | ||||||
| 							}} | 							}} | ||||||
|  | 							checkedRowsNumber={selectedList.length} | ||||||
| 							multipleSelection={this.props.multipleSelection} | 							multipleSelection={this.props.multipleSelection} | ||||||
| 							columns={this.tableColumns} | 							columns={this.tableColumns} | ||||||
| 							data={displayedDrives} | 							data={displayedDrives} | ||||||
| @@ -481,8 +443,11 @@ export class DriveSelector extends React.Component< | |||||||
| 							} | 							} | ||||||
| 							rowKey="displayName" | 							rowKey="displayName" | ||||||
| 							onCheck={(rows: Drive[]) => { | 							onCheck={(rows: Drive[]) => { | ||||||
| 									const newSelection = rows.filter(isDrivelistDrive); | 								let newSelection = rows.filter(isDrivelistDrive); | ||||||
| 								if (this.props.multipleSelection) { | 								if (this.props.multipleSelection) { | ||||||
|  | 									if (this.deselectingAll(newSelection)) { | ||||||
|  | 										newSelection = []; | ||||||
|  | 									} | ||||||
| 									this.setState({ | 									this.setState({ | ||||||
| 										selectedList: newSelection, | 										selectedList: newSelection, | ||||||
| 									}); | 									}); | ||||||
| @@ -499,25 +464,21 @@ export class DriveSelector extends React.Component< | |||||||
| 								) { | 								) { | ||||||
| 									return; | 									return; | ||||||
| 								} | 								} | ||||||
| 									if (this.props.multipleSelection) { | 								const index = selectedList.findIndex( | ||||||
| 										const newList = [...selectedList]; | 									(d) => d.device === row.device, | ||||||
| 										const selectedIndex = selectedList.findIndex( |  | ||||||
| 											(drive) => drive.device === row.device, |  | ||||||
| 								); | 								); | ||||||
| 										if (selectedIndex === -1) { | 								const newList = this.props.multipleSelection | ||||||
|  | 									? [...selectedList] | ||||||
|  | 									: []; | ||||||
|  | 								if (index === -1) { | ||||||
| 									newList.push(row); | 									newList.push(row); | ||||||
| 								} else { | 								} else { | ||||||
| 									// Deselect if selected | 									// Deselect if selected | ||||||
| 											newList.splice(selectedIndex, 1); | 									newList.splice(index, 1); | ||||||
| 								} | 								} | ||||||
| 								this.setState({ | 								this.setState({ | ||||||
| 									selectedList: newList, | 									selectedList: newList, | ||||||
| 								}); | 								}); | ||||||
| 										return; |  | ||||||
| 									} |  | ||||||
| 									this.setState({ |  | ||||||
| 										selectedList: [row], |  | ||||||
| 									}); |  | ||||||
| 							}} | 							}} | ||||||
| 						/> | 						/> | ||||||
| 						{numberOfHiddenSystemDrives > 0 && ( | 						{numberOfHiddenSystemDrives > 0 && ( | ||||||
| @@ -533,15 +494,13 @@ export class DriveSelector extends React.Component< | |||||||
| 								</Flex> | 								</Flex> | ||||||
| 							</Link> | 							</Link> | ||||||
| 						)} | 						)} | ||||||
| 						</ScrollableFlex> | 					</> | ||||||
| 				)} | 				)} | ||||||
| 				{this.props.showWarnings && hasSystemDrives ? ( | 				{this.props.showWarnings && hasSystemDrives ? ( | ||||||
| 					<Alert className="system-drive-alert" style={{ width: '67%' }}> | 					<Alert className="system-drive-alert" style={{ width: '67%' }}> | ||||||
| 							Selecting your system drive is dangerous and will erase your | 						Selecting your system drive is dangerous and will erase your drive! | ||||||
| 							drive! |  | ||||||
| 					</Alert> | 					</Alert> | ||||||
| 				) : null} | 				) : null} | ||||||
| 				</Flex> |  | ||||||
|  |  | ||||||
| 				{missingDriversModal.drive !== undefined && ( | 				{missingDriversModal.drive !== undefined && ( | ||||||
| 					<Modal | 					<Modal | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| import * as _ from 'lodash'; |  | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Flex } from 'rendition'; | import { Flex } from 'rendition'; | ||||||
| import { v4 as uuidV4 } from 'uuid'; | 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 * as selectionState from '../../models/selection-state'; | ||||||
| import { Actions, store } from '../../models/store'; | import { Actions, store } from '../../models/store'; | ||||||
| import * as analytics from '../../modules/analytics'; | 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 { FlashAnother } from '../flash-another/flash-another'; | ||||||
| import { FlashResults } from '../flash-results/flash-results'; | import { FlashResults, FlashError } from '../flash-results/flash-results'; | ||||||
|  | import { SafeWebview } from '../safe-webview/safe-webview'; | ||||||
| import EtcherSvg from '../../../assets/etcher.svg'; |  | ||||||
| import LoveSvg from '../../../assets/love.svg'; |  | ||||||
| import BalenaSvg from '../../../assets/balena.svg'; |  | ||||||
|  |  | ||||||
| function restart(goToMain: () => void) { | function restart(goToMain: () => void) { | ||||||
| 	selectionState.deselectAllDrives(); | 	selectionState.deselectAllDrives(); | ||||||
| @@ -44,22 +39,59 @@ function restart(goToMain: () => void) { | |||||||
| 	goToMain(); | 	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 }) { | 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 ( | 	return ( | ||||||
| 		<Flex flexDirection="column" width="100%" color="#fff"> | 		<Flex height="100%" justifyContent="space-between"> | ||||||
| 			<Flex height="160px" alignItems="center" justifyContent="center"> | 			<Flex | ||||||
| 				<FlashResults results={results} errors={formattedErrors()} /> | 				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 | 				<FlashAnother | ||||||
| 					onClick={() => { | 					onClick={() => { | ||||||
| @@ -67,35 +99,19 @@ function FinishPage({ goToMain }: { goToMain: () => void }) { | |||||||
| 					}} | 					}} | ||||||
| 				/> | 				/> | ||||||
| 			</Flex> | 			</Flex> | ||||||
|  | 			<SafeWebview | ||||||
| 			<Flex | 				src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true" | ||||||
| 				flexDirection="column" | 				onWebviewShow={setWebviewShowing} | ||||||
| 				height="320px" | 				style={{ | ||||||
| 				justifyContent="space-between" | 					display: webviewShowing ? 'flex' : 'none', | ||||||
| 				alignItems="center" | 					position: 'absolute', | ||||||
| 			> | 					right: 0, | ||||||
| 				<Flex fontSize="28px" mt="40px"> | 					bottom: 0, | ||||||
| 					Thanks for using | 					width: '63.8vw', | ||||||
| 					<EtcherSvg | 					height: '100vh', | ||||||
| 						width="165px" | 				}} | ||||||
| 						style={{ margin: '0 10px', cursor: 'pointer' }} |  | ||||||
| 						onClick={() => |  | ||||||
| 							openExternal('https://balena.io/etcher?ref=etcher_offline_banner') |  | ||||||
| 						} |  | ||||||
| 			/> | 			/> | ||||||
| 		</Flex> | 		</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> |  | ||||||
| 		</Flex> |  | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ export interface FlashAnotherProps { | |||||||
| export const FlashAnother = (props: FlashAnotherProps) => { | export const FlashAnother = (props: FlashAnotherProps) => { | ||||||
| 	return ( | 	return ( | ||||||
| 		<BaseButton primary onClick={props.onClick}> | 		<BaseButton primary onClick={props.onClick}> | ||||||
| 			Flash Another | 			Flash another | ||||||
| 		</BaseButton> | 		</BaseButton> | ||||||
| 	); | 	); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -16,19 +16,108 @@ | |||||||
|  |  | ||||||
| import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg'; | import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg'; | ||||||
| import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-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 * as _ from 'lodash'; | ||||||
| import outdent from 'outdent'; | import outdent from 'outdent'; | ||||||
| import * as React from 'react'; | 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 { progress } from '../../../../shared/messages'; | ||||||
| import { bytesToMegabytes } from '../../../../shared/units'; | 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({ | export function FlashResults({ | ||||||
|  | 	goToMain, | ||||||
|  | 	image = '', | ||||||
| 	errors, | 	errors, | ||||||
| 	results, | 	results, | ||||||
|  | 	skip, | ||||||
|  | 	...props | ||||||
| }: { | }: { | ||||||
| 	errors: string; | 	goToMain: () => void; | ||||||
|  | 	image?: string; | ||||||
|  | 	errors: FlashError[]; | ||||||
|  | 	skip: boolean; | ||||||
| 	results: { | 	results: { | ||||||
| 		bytesWritten: number; | 		bytesWritten: number; | ||||||
| 		sourceMetadata: { | 		sourceMetadata: { | ||||||
| @@ -38,8 +127,9 @@ export function FlashResults({ | |||||||
| 		averageFlashingSpeed: number; | 		averageFlashingSpeed: number; | ||||||
| 		devices: { failed: number; successful: number }; | 		devices: { failed: number; successful: number }; | ||||||
| 	}; | 	}; | ||||||
| }) { | } & FlexProps) { | ||||||
| 	const allDevicesFailed = results.devices.successful === 0; | 	const [showErrorsInfo, setShowErrorsInfo] = React.useState(false); | ||||||
|  | 	const allFailed = results.devices.successful === 0; | ||||||
| 	const effectiveSpeed = _.round( | 	const effectiveSpeed = _.round( | ||||||
| 		bytesToMegabytes( | 		bytesToMegabytes( | ||||||
| 			results.sourceMetadata.size / | 			results.sourceMetadata.size / | ||||||
| @@ -48,44 +138,56 @@ export function FlashResults({ | |||||||
| 		1, | 		1, | ||||||
| 	); | 	); | ||||||
| 	return ( | 	return ( | ||||||
| 		<Flex | 		<Flex flexDirection="column" {...props}> | ||||||
| 			flexDirection="column" | 			<Flex alignItems="center" 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"> |  | ||||||
| 					Flash Complete! |  | ||||||
| 				</Txt> |  | ||||||
| 			</Flex> |  | ||||||
| 			<Flex flexDirection="column" mr="0" mb="0" ml="40px" color="#7e8085"> |  | ||||||
| 				{Object.entries(results.devices).map(([type, quantity]) => { |  | ||||||
| 					return quantity ? ( |  | ||||||
| 				<Flex | 				<Flex | ||||||
| 					alignItems="center" | 					alignItems="center" | ||||||
| 							tooltip={type === 'failed' ? errors : undefined} | 					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" color="#7e8085"> | ||||||
|  | 				{Object.entries(results.devices).map(([type, quantity]) => { | ||||||
|  | 					const failedTargets = type === 'failed'; | ||||||
|  | 					return quantity ? ( | ||||||
|  | 						<Flex alignItems="center"> | ||||||
| 							<CircleSvg | 							<CircleSvg | ||||||
| 								width="14px" | 								width="14px" | ||||||
| 								fill={type === 'failed' ? '#ff4444' : '#1ac135'} | 								fill={type === 'failed' ? '#ff4444' : '#1ac135'} | ||||||
|  | 								color={failedTargets ? '#ff4444' : '#1ac135'} | ||||||
| 							/> | 							/> | ||||||
| 							<Txt ml={10}>{quantity}</Txt> | 							<Txt ml="10px" color="#fff"> | ||||||
| 							<Txt ml={10}>{progress[type](quantity)}</Txt> | 								{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> | 						</Flex> | ||||||
| 					) : null; | 					) : null; | ||||||
| 				})} | 				})} | ||||||
| 				{!allDevicesFailed && ( | 				{!allFailed && ( | ||||||
| 					<Txt | 					<Txt | ||||||
| 						fontSize="10px" | 						fontSize="10px" | ||||||
| 						style={{ | 						style={{ | ||||||
| @@ -101,6 +203,33 @@ export function FlashResults({ | |||||||
| 					</Txt> | 					</Txt> | ||||||
| 				)} | 				)} | ||||||
| 			</Flex> | 			</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> | 		</Flex> | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ import * as React from 'react'; | |||||||
| import { Flex, Button, ProgressBar, Txt } from 'rendition'; | import { Flex, Button, ProgressBar, Txt } from 'rendition'; | ||||||
| import { default as styled } from 'styled-components'; | 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'; | import { StepButton } from '../../styled-components'; | ||||||
|  |  | ||||||
| const FlashProgressBar = styled(ProgressBar)` | const FlashProgressBar = styled(ProgressBar)` | ||||||
| @@ -44,12 +44,12 @@ const FlashProgressBar = styled(ProgressBar)` | |||||||
| `; | `; | ||||||
|  |  | ||||||
| interface ProgressButtonProps { | interface ProgressButtonProps { | ||||||
| 	type: 'decompressing' | 'flashing' | 'verifying'; | 	type: FlashState['type']; | ||||||
| 	active: boolean; | 	active: boolean; | ||||||
| 	percentage: number; | 	percentage: number; | ||||||
| 	position: number; | 	position: number; | ||||||
| 	disabled: boolean; | 	disabled: boolean; | ||||||
| 	cancel: () => void; | 	cancel: (type: string) => void; | ||||||
| 	callback: () => void; | 	callback: () => void; | ||||||
| 	warning?: boolean; | 	warning?: boolean; | ||||||
| } | } | ||||||
| @@ -58,13 +58,18 @@ const colors = { | |||||||
| 	decompressing: '#00aeef', | 	decompressing: '#00aeef', | ||||||
| 	flashing: '#da60ff', | 	flashing: '#da60ff', | ||||||
| 	verifying: '#1ac135', | 	verifying: '#1ac135', | ||||||
|  | 	downloading: '#00aeef', | ||||||
|  | 	default: '#00aeef', | ||||||
| } as const; | } as const; | ||||||
|  |  | ||||||
| const CancelButton = styled((props) => ( | const CancelButton = styled(({ type, onClick, ...props }) => { | ||||||
| 	<Button plain {...props}> | 	const status = type === 'verifying' ? 'Skip' : 'Cancel'; | ||||||
| 		Cancel | 	return ( | ||||||
|  | 		<Button plain onClick={() => onClick(status)} {...props}> | ||||||
|  | 			{status} | ||||||
| 		</Button> | 		</Button> | ||||||
| ))` | 	); | ||||||
|  | })` | ||||||
| 	font-weight: 600; | 	font-weight: 600; | ||||||
| 	&&& { | 	&&& { | ||||||
| 		width: auto; | 		width: auto; | ||||||
| @@ -75,10 +80,13 @@ const CancelButton = styled((props) => ( | |||||||
|  |  | ||||||
| export class ProgressButton extends React.PureComponent<ProgressButtonProps> { | export class ProgressButton extends React.PureComponent<ProgressButtonProps> { | ||||||
| 	public render() { | 	public render() { | ||||||
|  | 		const type = this.props.type || 'default'; | ||||||
|  | 		const percentage = this.props.percentage; | ||||||
|  | 		const warning = this.props.warning; | ||||||
| 		const { status, position } = fromFlashState({ | 		const { status, position } = fromFlashState({ | ||||||
| 			type: this.props.type, | 			type: this.props.type, | ||||||
|  | 			percentage, | ||||||
| 			position: this.props.position, | 			position: this.props.position, | ||||||
| 			percentage: this.props.percentage, |  | ||||||
| 		}); | 		}); | ||||||
| 		if (this.props.active) { | 		if (this.props.active) { | ||||||
| 			return ( | 			return ( | ||||||
| @@ -96,21 +104,24 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> { | |||||||
| 					> | 					> | ||||||
| 						<Flex> | 						<Flex> | ||||||
| 							<Txt color="#fff">{status} </Txt> | 							<Txt color="#fff">{status} </Txt> | ||||||
| 							<Txt color={colors[this.props.type]}>{position}</Txt> | 							<Txt color={colors[type]}>{position}</Txt> | ||||||
| 						</Flex> | 						</Flex> | ||||||
| 						<CancelButton onClick={this.props.cancel} color="#00aeef" /> | 						{type && ( | ||||||
| 					</Flex> | 							<CancelButton | ||||||
| 					<FlashProgressBar | 								type={type} | ||||||
| 						background={colors[this.props.type]} | 								onClick={this.props.cancel} | ||||||
| 						value={this.props.percentage} | 								color="#00aeef" | ||||||
| 							/> | 							/> | ||||||
|  | 						)} | ||||||
|  | 					</Flex> | ||||||
|  | 					<FlashProgressBar background={colors[type]} value={percentage} /> | ||||||
| 				</> | 				</> | ||||||
| 			); | 			); | ||||||
| 		} | 		} | ||||||
| 		return ( | 		return ( | ||||||
| 			<StepButton | 			<StepButton | ||||||
| 				primary={!this.props.warning} | 				primary={!warning} | ||||||
| 				warning={this.props.warning} | 				warning={warning} | ||||||
| 				onClick={this.props.callback} | 				onClick={this.props.callback} | ||||||
| 				disabled={this.props.disabled} | 				disabled={this.props.disabled} | ||||||
| 				style={{ | 				style={{ | ||||||
|   | |||||||
| @@ -15,7 +15,6 @@ | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| import * as electron from 'electron'; | import * as electron from 'electron'; | ||||||
| import * as _ from 'lodash'; |  | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
|  |  | ||||||
| import * as packageJSON from '../../../../../package.json'; | import * as packageJSON from '../../../../../package.json'; | ||||||
| @@ -94,8 +93,8 @@ export class SafeWebview extends React.PureComponent< | |||||||
| 		); | 		); | ||||||
| 		this.entryHref = url.href; | 		this.entryHref = url.href; | ||||||
| 		// Events steal 'this' | 		// Events steal 'this' | ||||||
| 		this.didFailLoad = _.bind(this.didFailLoad, this); | 		this.didFailLoad = this.didFailLoad.bind(this); | ||||||
| 		this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this); | 		this.didGetResponseDetails = this.didGetResponseDetails.bind(this); | ||||||
| 		// Make a persistent electron session for the webview | 		// Make a persistent electron session for the webview | ||||||
| 		this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, { | 		this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, { | ||||||
| 			// Disable the cache for the session such that new content shows up when refreshing | 			// 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', | 			name: 'updatesEnabled', | ||||||
| 			label: 'Auto-updates enabled', | 			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)} | 			done={() => toggleModal(false)} | ||||||
| 		> | 		> | ||||||
| 			<Flex flexDirection="column"> | 			<Flex flexDirection="column"> | ||||||
| 				{_.map(settingsList, (setting: Setting, i: number) => { | 				{settingsList.map((setting: Setting, i: number) => { | ||||||
| 					return setting.hide ? null : ( | 					return setting.hide ? null : ( | ||||||
| 						<Flex key={setting.name}> | 						<Flex key={setting.name} mb={14}> | ||||||
| 							<Checkbox | 							<Checkbox | ||||||
| 								toggle | 								toggle | ||||||
| 								tabIndex={6 + i} | 								tabIndex={6 + i} | ||||||
| @@ -135,12 +135,13 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) { | |||||||
| 					); | 					); | ||||||
| 				})} | 				})} | ||||||
| 				<Flex | 				<Flex | ||||||
| 					mt={28} | 					mt={18} | ||||||
| 					alignItems="center" | 					alignItems="center" | ||||||
| 					color="#00aeef" | 					color="#00aeef" | ||||||
| 					style={{ | 					style={{ | ||||||
| 						width: 'fit-content', | 						width: 'fit-content', | ||||||
| 						cursor: 'pointer', | 						cursor: 'pointer', | ||||||
|  | 						fontSize: 14, | ||||||
| 					}} | 					}} | ||||||
| 					onClick={() => | 					onClick={() => | ||||||
| 						openExternal( | 						openExternal( | ||||||
|   | |||||||
| @@ -25,15 +25,7 @@ import { GPTPartition, MBRPartition } from 'partitioninfo'; | |||||||
| import * as path from 'path'; | import * as path from 'path'; | ||||||
| import * as prettyBytes from 'pretty-bytes'; | import * as prettyBytes from 'pretty-bytes'; | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { Flex, ButtonProps, Modal as SmallModal, Txt } from 'rendition'; | ||||||
| 	Flex, |  | ||||||
| 	ButtonProps, |  | ||||||
| 	Modal as SmallModal, |  | ||||||
| 	Txt, |  | ||||||
| 	Card as BaseCard, |  | ||||||
| 	Input, |  | ||||||
| 	Spinner, |  | ||||||
| } from 'rendition'; |  | ||||||
| import styled from 'styled-components'; | import styled from 'styled-components'; | ||||||
|  |  | ||||||
| import * as errors from '../../../../shared/errors'; | import * as errors from '../../../../shared/errors'; | ||||||
| @@ -48,62 +40,21 @@ import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drive | |||||||
| import { | import { | ||||||
| 	ChangeButton, | 	ChangeButton, | ||||||
| 	DetailsText, | 	DetailsText, | ||||||
| 	Modal, |  | ||||||
| 	StepButton, | 	StepButton, | ||||||
| 	StepNameButton, | 	StepNameButton, | ||||||
| 	ScrollableFlex, |  | ||||||
| } from '../../styled-components'; | } from '../../styled-components'; | ||||||
| import { colors } from '../../theme'; | import { colors } from '../../theme'; | ||||||
| import { middleEllipsis } from '../../utils/middle-ellipsis'; | import { middleEllipsis } from '../../utils/middle-ellipsis'; | ||||||
|  | import URLSelector from '../url-selector/url-selector'; | ||||||
| import { SVGIcon } from '../svg-icon/svg-icon'; | import { SVGIcon } from '../svg-icon/svg-icon'; | ||||||
|  |  | ||||||
| import ImageSvg from '../../../assets/image.svg'; | import ImageSvg from '../../../assets/image.svg'; | ||||||
| import { DriveSelector } from '../drive-selector/drive-selector'; | import { DriveSelector } from '../drive-selector/drive-selector'; | ||||||
| import { DrivelistDrive } from '../../../../shared/drive-constraints'; | 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) => | const isURL = (imagePath: string) => | ||||||
| 	imagePath.startsWith('https://') || imagePath.startsWith('http://'); | 	imagePath.startsWith('https://') || imagePath.startsWith('http://'); | ||||||
|  |  | ||||||
| const Card = styled(BaseCard)` |  | ||||||
| 	hr { |  | ||||||
| 		margin: 5px 0; |  | ||||||
| 	} |  | ||||||
| `; |  | ||||||
|  |  | ||||||
| // TODO move these styles to rendition | // TODO move these styles to rendition | ||||||
| const ModalText = styled.p` | const ModalText = styled.p` | ||||||
| 	a { | 	a { | ||||||
| @@ -127,85 +78,6 @@ function isString(value: any): value is string { | |||||||
| 	return typeof value === '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 { | interface Flow { | ||||||
| 	icon?: JSX.Element; | 	icon?: JSX.Element; | ||||||
| 	onClick: (evt: React.MouseEvent) => void; | 	onClick: (evt: React.MouseEvent) => void; | ||||||
| @@ -213,22 +85,28 @@ interface Flow { | |||||||
| } | } | ||||||
|  |  | ||||||
| const FlowSelector = styled( | const FlowSelector = styled( | ||||||
| 	({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => { | 	({ flow, ...props }: { flow: Flow } & ButtonProps) => ( | ||||||
| 		return ( |  | ||||||
| 		<StepButton | 		<StepButton | ||||||
| 				plain | 			plain={!props.primary} | ||||||
| 				onClick={(evt) => flow.onClick(evt)} | 			primary={props.primary} | ||||||
|  | 			onClick={(evt: React.MouseEvent<Element, MouseEvent>) => | ||||||
|  | 				flow.onClick(evt) | ||||||
|  | 			} | ||||||
| 			icon={flow.icon} | 			icon={flow.icon} | ||||||
| 			{...props} | 			{...props} | ||||||
| 		> | 		> | ||||||
| 			{flow.label} | 			{flow.label} | ||||||
| 		</StepButton> | 		</StepButton> | ||||||
| 		); | 	), | ||||||
| 	}, |  | ||||||
| )` | )` | ||||||
| 	border-radius: 24px; | 	border-radius: 24px; | ||||||
| 	color: rgba(255, 255, 255, 0.7); | 	color: rgba(255, 255, 255, 0.7); | ||||||
|  |  | ||||||
|  | 	:enabled:focus, | ||||||
|  | 	:enabled:focus svg { | ||||||
|  | 		color: ${colors.primary.foreground} !important; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	:enabled:hover { | 	:enabled:hover { | ||||||
| 		background-color: ${colors.primary.background}; | 		background-color: ${colors.primary.background}; | ||||||
| 		color: ${colors.primary.foreground}; | 		color: ${colors.primary.foreground}; | ||||||
| @@ -269,6 +147,7 @@ interface SourceSelectorState { | |||||||
| 	showImageDetails: boolean; | 	showImageDetails: boolean; | ||||||
| 	showURLSelector: boolean; | 	showURLSelector: boolean; | ||||||
| 	showDriveSelector: boolean; | 	showDriveSelector: boolean; | ||||||
|  | 	defaultFlowActive: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class SourceSelector extends React.Component< | export class SourceSelector extends React.Component< | ||||||
| @@ -285,7 +164,11 @@ export class SourceSelector extends React.Component< | |||||||
| 			showImageDetails: false, | 			showImageDetails: false, | ||||||
| 			showURLSelector: false, | 			showURLSelector: false, | ||||||
| 			showDriveSelector: false, | 			showDriveSelector: false, | ||||||
|  | 			defaultFlowActive: true, | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
|  | 		// Bind `this` since it's used in an event's callback | ||||||
|  | 		this.onSelectImage = this.onSelectImage.bind(this); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public componentDidMount() { | 	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 | 	// TODO add a visual change when dragging a file over the selector | ||||||
| 	public render() { | 	public render() { | ||||||
| 		const { flashing } = this.props; | 		const { flashing } = this.props; | ||||||
| @@ -593,12 +480,15 @@ export class SourceSelector extends React.Component< | |||||||
| 					) : ( | 					) : ( | ||||||
| 						<> | 						<> | ||||||
| 							<FlowSelector | 							<FlowSelector | ||||||
|  | 								primary={this.state.defaultFlowActive} | ||||||
| 								key="Flash from file" | 								key="Flash from file" | ||||||
| 								flow={{ | 								flow={{ | ||||||
| 									onClick: () => this.openImageSelector(), | 									onClick: () => this.openImageSelector(), | ||||||
| 									label: 'Flash from file', | 									label: 'Flash from file', | ||||||
| 									icon: <FileSvg height="1em" fill="currentColor" />, | 									icon: <FileSvg height="1em" fill="currentColor" />, | ||||||
| 								}} | 								}} | ||||||
|  | 								onMouseEnter={() => this.setDefaultFlowActive(false)} | ||||||
|  | 								onMouseLeave={() => this.setDefaultFlowActive(true)} | ||||||
| 							/> | 							/> | ||||||
| 							<FlowSelector | 							<FlowSelector | ||||||
| 								key="Flash from URL" | 								key="Flash from URL" | ||||||
| @@ -607,6 +497,8 @@ export class SourceSelector extends React.Component< | |||||||
| 									label: 'Flash from URL', | 									label: 'Flash from URL', | ||||||
| 									icon: <LinkSvg height="1em" fill="currentColor" />, | 									icon: <LinkSvg height="1em" fill="currentColor" />, | ||||||
| 								}} | 								}} | ||||||
|  | 								onMouseEnter={() => this.setDefaultFlowActive(false)} | ||||||
|  | 								onMouseLeave={() => this.setDefaultFlowActive(true)} | ||||||
| 							/> | 							/> | ||||||
| 							<FlowSelector | 							<FlowSelector | ||||||
| 								key="Clone drive" | 								key="Clone drive" | ||||||
| @@ -615,6 +507,8 @@ export class SourceSelector extends React.Component< | |||||||
| 									label: 'Clone drive', | 									label: 'Clone drive', | ||||||
| 									icon: <CopySvg height="1em" fill="currentColor" />, | 									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) { | export function addFailedDevicePath({ | ||||||
| 	const failedDevicePathsSet = new Set( | 	device, | ||||||
|  | 	error, | ||||||
|  | }: { | ||||||
|  | 	device: sdk.scanner.adapters.DrivelistDrive; | ||||||
|  | 	error: Error; | ||||||
|  | }) { | ||||||
|  | 	const failedDevicePathsMap = new Map( | ||||||
| 		store.getState().toJS().failedDevicePaths, | 		store.getState().toJS().failedDevicePaths, | ||||||
| 	); | 	); | ||||||
| 	failedDevicePathsSet.add(devicePath); | 	failedDevicePathsMap.set(device.device, { | ||||||
|  | 		description: device.description, | ||||||
|  | 		device: device.device, | ||||||
|  | 		devicePath: device.devicePath, | ||||||
|  | 		...error, | ||||||
|  | 	}); | ||||||
| 	store.dispatch({ | 	store.dispatch({ | ||||||
| 		type: Actions.SET_FAILED_DEVICE_PATHS, | 		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 { | 	} else { | ||||||
| 		selectedDrivesPaths = s.devicePaths; | 		selectedDrivesPaths = s.devicePaths; | ||||||
| 	} | 	} | ||||||
|  | 	const failedDevicePaths = s.failedDevicePaths.map( | ||||||
|  | 		([devicePath]: [string]) => devicePath, | ||||||
|  | 	); | ||||||
| 	const newLedsState = { | 	const newLedsState = { | ||||||
| 		step, | 		step, | ||||||
| 		sourceDrive: sourceDrivePath, | 		sourceDrive: sourceDrivePath, | ||||||
| 		availableDrives: availableDrivesPaths, | 		availableDrives: availableDrivesPaths, | ||||||
| 		selectedDrives: selectedDrivesPaths, | 		selectedDrives: selectedDrivesPaths, | ||||||
| 		failedDrives: s.failedDevicePaths, | 		failedDrives: failedDevicePaths, | ||||||
| 	}; | 	}; | ||||||
| 	if (!_.isEqual(newLedsState, ledsState)) { | 	if (!_.isEqual(newLedsState, ledsState)) { | ||||||
| 		updateLeds(newLedsState); | 		updateLeds(newLedsState); | ||||||
|   | |||||||
| @@ -72,24 +72,24 @@ export function getImage(): SourceMetadata | undefined { | |||||||
| 	return store.getState().toJS().selection.image; | 	return store.getState().toJS().selection.image; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getImagePath() { | export function getImagePath(): string | undefined { | ||||||
| 	return getImage()?.path; | 	return store.getState().toJS().selection.image?.path; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getImageSize() { | export function getImageSize(): number | undefined { | ||||||
| 	return getImage()?.size; | 	return store.getState().toJS().selection.image?.size; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getImageName() { | export function getImageName(): string | undefined { | ||||||
| 	return getImage()?.name; | 	return store.getState().toJS().selection.image?.name; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getImageLogo() { | export function getImageLogo(): string | undefined { | ||||||
| 	return getImage()?.logo; | 	return store.getState().toJS().selection.image?.logo; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getImageSupportUrl() { | export function getImageSupportUrl(): string | undefined { | ||||||
| 	return getImage()?.supportUrl; | 	return store.getState().toJS().selection.image?.supportUrl; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings'); | |||||||
|  |  | ||||||
| const JSON_INDENT = 2; | const JSON_INDENT = 2; | ||||||
|  |  | ||||||
|  | export const DEFAULT_WIDTH = 800; | ||||||
|  | export const DEFAULT_HEIGHT = 480; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @summary Userdata directory path |  * @summary Userdata directory path | ||||||
|  * @description |  * @description | ||||||
| @@ -38,12 +41,15 @@ const JSON_INDENT = 2; | |||||||
|  * NOTE: The ternary is due to this module being loaded both, |  * NOTE: The ternary is due to this module being loaded both, | ||||||
|  * Electron's main process and renderer process |  * Electron's main process and renderer process | ||||||
|  */ |  */ | ||||||
| const USER_DATA_DIR = electron.app |  | ||||||
| 	? electron.app.getPath('userData') | const app = electron.app || electron.remote.app; | ||||||
| 	: electron.remote.app.getPath('userData'); |  | ||||||
|  | const USER_DATA_DIR = app.getPath('userData'); | ||||||
|  |  | ||||||
| const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); | const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); | ||||||
|  |  | ||||||
|  | const DOWNLOADS_DIR = app.getPath('downloads'); | ||||||
|  |  | ||||||
| async function readConfigFile(filename: string): Promise<_.Dictionary<any>> { | async function readConfigFile(filename: string): Promise<_.Dictionary<any>> { | ||||||
| 	let contents = '{}'; | 	let contents = '{}'; | ||||||
| 	try { | 	try { | ||||||
| @@ -80,6 +86,8 @@ const DEFAULT_SETTINGS: _.Dictionary<any> = { | |||||||
| 	desktopNotifications: true, | 	desktopNotifications: true, | ||||||
| 	autoBlockmapping: true, | 	autoBlockmapping: true, | ||||||
| 	decompressFirst: true, | 	decompressFirst: true, | ||||||
|  | 	saveUrlImage: false, | ||||||
|  | 	saveUrlImageTo: DOWNLOADS_DIR, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const settings = _.cloneDeep(DEFAULT_SETTINGS); | const settings = _.cloneDeep(DEFAULT_SETTINGS); | ||||||
|   | |||||||
| @@ -295,6 +295,7 @@ function storeReducer( | |||||||
|  |  | ||||||
| 			_.defaults(action.data, { | 			_.defaults(action.data, { | ||||||
| 				cancelled: false, | 				cancelled: false, | ||||||
|  | 				skip: false, | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| 			if (!_.isBoolean(action.data.cancelled)) { | 			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 | 			return state | ||||||
| 				.set('isFlashing', false) | 				.set('isFlashing', false) | ||||||
| 				.set('flashResults', Immutable.fromJS(action.data)) | 				.set('flashResults', Immutable.fromJS(action.data)) | ||||||
|   | |||||||
| @@ -131,6 +131,7 @@ function writerEnv() { | |||||||
| } | } | ||||||
|  |  | ||||||
| interface FlashResults { | interface FlashResults { | ||||||
|  | 	skip?: boolean; | ||||||
| 	cancelled?: boolean; | 	cancelled?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -140,12 +141,15 @@ async function performWrite( | |||||||
| 	onProgress: sdk.multiWrite.OnProgressFunction, | 	onProgress: sdk.multiWrite.OnProgressFunction, | ||||||
| ): Promise<{ cancelled?: boolean }> { | ): Promise<{ cancelled?: boolean }> { | ||||||
| 	let cancelled = false; | 	let cancelled = false; | ||||||
|  | 	let skip = false; | ||||||
| 	ipc.serve(); | 	ipc.serve(); | ||||||
| 	const { | 	const { | ||||||
| 		unmountOnSuccess, | 		unmountOnSuccess, | ||||||
| 		validateWriteOnSuccess, | 		validateWriteOnSuccess, | ||||||
| 		autoBlockmapping, | 		autoBlockmapping, | ||||||
| 		decompressFirst, | 		decompressFirst, | ||||||
|  | 		saveUrlImage, | ||||||
|  | 		saveUrlImageTo, | ||||||
| 	} = await settings.getAll(); | 	} = await settings.getAll(); | ||||||
| 	return await new Promise((resolve, reject) => { | 	return await new Promise((resolve, reject) => { | ||||||
| 		ipc.server.on('error', (error) => { | 		ipc.server.on('error', (error) => { | ||||||
| @@ -171,7 +175,7 @@ async function performWrite( | |||||||
|  |  | ||||||
| 		ipc.server.on('fail', ({ device, error }) => { | 		ipc.server.on('fail', ({ device, error }) => { | ||||||
| 			if (device.devicePath) { | 			if (device.devicePath) { | ||||||
| 				flashState.addFailedDevicePath(device.devicePath); | 				flashState.addFailedDevicePath({ device, error }); | ||||||
| 			} | 			} | ||||||
| 			handleErrorLogging(error, analyticsData); | 			handleErrorLogging(error, analyticsData); | ||||||
| 		}); | 		}); | ||||||
| @@ -188,6 +192,11 @@ async function performWrite( | |||||||
| 			cancelled = true; | 			cancelled = true; | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		ipc.server.on('skip', () => { | ||||||
|  | 			terminateServer(); | ||||||
|  | 			skip = true; | ||||||
|  | 		}); | ||||||
|  |  | ||||||
| 		ipc.server.on('state', onProgress); | 		ipc.server.on('state', onProgress); | ||||||
|  |  | ||||||
| 		ipc.server.on('ready', (_data, socket) => { | 		ipc.server.on('ready', (_data, socket) => { | ||||||
| @@ -199,6 +208,8 @@ async function performWrite( | |||||||
| 				autoBlockmapping, | 				autoBlockmapping, | ||||||
| 				unmountOnSuccess, | 				unmountOnSuccess, | ||||||
| 				decompressFirst, | 				decompressFirst, | ||||||
|  | 				saveUrlImage, | ||||||
|  | 				saveUrlImageTo, | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| @@ -213,6 +224,7 @@ async function performWrite( | |||||||
| 					environment: env, | 					environment: env, | ||||||
| 				}); | 				}); | ||||||
| 				flashResults.cancelled = cancelled || results.cancelled; | 				flashResults.cancelled = cancelled || results.cancelled; | ||||||
|  | 				flashResults.skip = skip; | ||||||
| 			} catch (error) { | 			} catch (error) { | ||||||
| 				// This happens when the child is killed using SIGKILL | 				// This happens when the child is killed using SIGKILL | ||||||
| 				const SIGKILL_EXIT_CODE = 137; | 				const SIGKILL_EXIT_CODE = 137; | ||||||
| @@ -229,6 +241,7 @@ async function performWrite( | |||||||
| 			// This likely means the child died halfway through | 			// This likely means the child died halfway through | ||||||
| 			if ( | 			if ( | ||||||
| 				!flashResults.cancelled && | 				!flashResults.cancelled && | ||||||
|  | 				!flashResults.skip && | ||||||
| 				!_.get(flashResults, ['results', 'bytesWritten']) | 				!_.get(flashResults, ['results', 'bytesWritten']) | ||||||
| 			) { | 			) { | ||||||
| 				reject( | 				reject( | ||||||
| @@ -286,8 +299,7 @@ export async function flash( | |||||||
| 	} catch (error) { | 	} catch (error) { | ||||||
| 		flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); | 		flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); | ||||||
| 		windowProgress.clear(); | 		windowProgress.clear(); | ||||||
| 		let { results } = flashState.getFlashResults(); | 		const { results = {} } = flashState.getFlashResults(); | ||||||
| 		results = results || {}; |  | ||||||
| 		const eventData = { | 		const eventData = { | ||||||
| 			...analyticsData, | 			...analyticsData, | ||||||
| 			errors: results.errors, | 			errors: results.errors, | ||||||
| @@ -306,7 +318,7 @@ export async function flash( | |||||||
| 		}; | 		}; | ||||||
| 		analytics.logEvent('Elevation cancelled', eventData); | 		analytics.logEvent('Elevation cancelled', eventData); | ||||||
| 	} else { | 	} else { | ||||||
| 		const { results } = flashState.getFlashResults(); | 		const { results = {} } = flashState.getFlashResults(); | ||||||
| 		const eventData = { | 		const eventData = { | ||||||
| 			...analyticsData, | 			...analyticsData, | ||||||
| 			errors: results.errors, | 			errors: results.errors, | ||||||
| @@ -322,7 +334,8 @@ export async function flash( | |||||||
| /** | /** | ||||||
|  * @summary Cancel write operation |  * @summary Cancel write operation | ||||||
|  */ |  */ | ||||||
| export async function cancel() { | export async function cancel(type: string) { | ||||||
|  | 	const status = type.toLowerCase(); | ||||||
| 	const drives = selectionState.getSelectedDevices(); | 	const drives = selectionState.getSelectedDevices(); | ||||||
| 	const analyticsData = { | 	const analyticsData = { | ||||||
| 		image: selectionState.getImagePath(), | 		image: selectionState.getImagePath(), | ||||||
| @@ -332,7 +345,7 @@ export async function cancel() { | |||||||
| 		flashInstanceUuid: flashState.getFlashUuid(), | 		flashInstanceUuid: flashState.getFlashUuid(), | ||||||
| 		unmountOnSuccess: await settings.get('unmountOnSuccess'), | 		unmountOnSuccess: await settings.get('unmountOnSuccess'), | ||||||
| 		validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), | 		validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), | ||||||
| 		status: 'cancel', | 		status, | ||||||
| 	}; | 	}; | ||||||
| 	analytics.logEvent('Cancel', analyticsData); | 	analytics.logEvent('Cancel', analyticsData); | ||||||
|  |  | ||||||
| @@ -342,7 +355,7 @@ export async function cancel() { | |||||||
| 		// @ts-ignore (no Server.sockets in @types/node-ipc) | 		// @ts-ignore (no Server.sockets in @types/node-ipc) | ||||||
| 		const [socket] = ipc.server.sockets; | 		const [socket] = ipc.server.sockets; | ||||||
| 		if (socket !== undefined) { | 		if (socket !== undefined) { | ||||||
| 			ipc.server.emit(socket, 'cancel'); | 			ipc.server.emit(socket, status); | ||||||
| 		} | 		} | ||||||
| 	} catch (error) { | 	} catch (error) { | ||||||
| 		analytics.logException(error); | 		analytics.logException(error); | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ export interface FlashState { | |||||||
| 	percentage?: number; | 	percentage?: number; | ||||||
| 	speed: number; | 	speed: number; | ||||||
| 	position: number; | 	position: number; | ||||||
| 	type?: 'decompressing' | 'flashing' | 'verifying'; | 	type?: 'decompressing' | 'flashing' | 'verifying' | 'downloading'; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function fromFlashState({ | export function fromFlashState({ | ||||||
| @@ -62,6 +62,12 @@ export function fromFlashState({ | |||||||
| 		} else { | 		} else { | ||||||
| 			return { status: 'Finishing...' }; | 			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' }; | 	return { status: 'Failed' }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,6 +40,12 @@ async function mountSourceDrive() { | |||||||
|  * Notice that by image, we mean *.img/*.iso/*.zip/etc files. |  * Notice that by image, we mean *.img/*.iso/*.zip/etc files. | ||||||
|  */ |  */ | ||||||
| export async function selectImage(): Promise<string | undefined> { | export async function selectImage(): Promise<string | undefined> { | ||||||
|  | 	return await openDialog(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function openDialog( | ||||||
|  | 	type: 'openFile' | 'openDirectory' = 'openFile', | ||||||
|  | ) { | ||||||
| 	await mountSourceDrive(); | 	await mountSourceDrive(); | ||||||
| 	const options: electron.OpenDialogOptions = { | 	const options: electron.OpenDialogOptions = { | ||||||
| 		// This variable is set when running in GNU/Linux from | 		// This variable is set when running in GNU/Linux from | ||||||
| @@ -50,8 +56,10 @@ export async function selectImage(): Promise<string | undefined> { | |||||||
| 		// | 		// | ||||||
| 		// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 | 		// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 | ||||||
| 		defaultPath: process.env.OWD, | 		defaultPath: process.env.OWD, | ||||||
| 		properties: ['openFile', 'treatPackageAsDirectory'], | 		properties: [type, 'treatPackageAsDirectory'], | ||||||
| 		filters: [ | 		filters: | ||||||
|  | 			type === 'openFile' | ||||||
|  | 				? [ | ||||||
| 						{ | 						{ | ||||||
| 							name: 'OS Images', | 							name: 'OS Images', | ||||||
| 							extensions: SUPPORTED_EXTENSIONS, | 							extensions: SUPPORTED_EXTENSIONS, | ||||||
| @@ -60,13 +68,14 @@ export async function selectImage(): Promise<string | undefined> { | |||||||
| 							name: 'All', | 							name: 'All', | ||||||
| 							extensions: ['*'], | 							extensions: ['*'], | ||||||
| 						}, | 						}, | ||||||
| 		], | 				  ] | ||||||
|  | 				: undefined, | ||||||
| 	}; | 	}; | ||||||
| 	const currentWindow = electron.remote.getCurrentWindow(); | 	const currentWindow = electron.remote.getCurrentWindow(); | ||||||
| 	const [file] = ( | 	const [path] = ( | ||||||
| 		await electron.remote.dialog.showOpenDialog(currentWindow, options) | 		await electron.remote.dialog.showOpenDialog(currentWindow, options) | ||||||
| 	).filePaths; | 	).filePaths; | ||||||
| 	return file; | 	return path; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -82,14 +82,12 @@ async function flashImageToDrive( | |||||||
| 	try { | 	try { | ||||||
| 		await imageWriter.flash(image, drives); | 		await imageWriter.flash(image, drives); | ||||||
| 		if (!flashState.wasLastFlashCancelled()) { | 		if (!flashState.wasLastFlashCancelled()) { | ||||||
| 			const flashResults: any = flashState.getFlashResults(); | 			const { | ||||||
|  | 				results = { devices: { successful: 0, failed: 0 } }, | ||||||
|  | 			} = flashState.getFlashResults(); | ||||||
| 			notification.send( | 			notification.send( | ||||||
| 				'Flash complete!', | 				'Flash complete!', | ||||||
| 				messages.info.flashComplete( | 				messages.info.flashComplete(basename, drives as any, results.devices), | ||||||
| 					basename, |  | ||||||
| 					drives as any, |  | ||||||
| 					flashResults.results.devices, |  | ||||||
| 				), |  | ||||||
| 				iconPath, | 				iconPath, | ||||||
| 			); | 			); | ||||||
| 			goToSuccess(); | 			goToSuccess(); | ||||||
|   | |||||||
| @@ -25,7 +25,6 @@ import styled from 'styled-components'; | |||||||
|  |  | ||||||
| import FinishPage from '../../components/finish/finish'; | import FinishPage from '../../components/finish/finish'; | ||||||
| import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; | 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 { SettingsModal } from '../../components/settings/settings'; | ||||||
| import { | import { | ||||||
| 	SourceMetadata, | 	SourceMetadata, | ||||||
| @@ -48,6 +47,8 @@ import { | |||||||
| import { FlashStep } from './Flash'; | import { FlashStep } from './Flash'; | ||||||
|  |  | ||||||
| import EtcherSvg from '../../../assets/etcher.svg'; | import EtcherSvg from '../../../assets/etcher.svg'; | ||||||
|  | import { SafeWebview } from '../../components/safe-webview/safe-webview'; | ||||||
|  | import { colors } from '../../theme'; | ||||||
|  |  | ||||||
| const Icon = styled(BaseIcon)` | const Icon = styled(BaseIcon)` | ||||||
| 	margin-right: 20px; | 	margin-right: 20px; | ||||||
| @@ -87,9 +88,7 @@ const StepBorder = styled.div<{ | |||||||
| 	position: relative; | 	position: relative; | ||||||
| 	height: 2px; | 	height: 2px; | ||||||
| 	background-color: ${(props) => | 	background-color: ${(props) => | ||||||
| 		props.disabled | 		props.disabled ? colors.dark.disabled.foreground : colors.dark.foreground}; | ||||||
| 			? props.theme.colors.dark.disabled.foreground |  | ||||||
| 			: props.theme.colors.dark.foreground}; |  | ||||||
| 	width: 120px; | 	width: 120px; | ||||||
| 	top: 19px; | 	top: 19px; | ||||||
|  |  | ||||||
| @@ -169,71 +168,6 @@ export class MainPage extends React.Component< | |||||||
| 		const notFlashingOrSplitView = | 		const notFlashingOrSplitView = | ||||||
| 			!this.state.isFlashing || !this.state.isWebviewShowing; | 			!this.state.isFlashing || !this.state.isWebviewShowing; | ||||||
| 		return ( | 		return ( | ||||||
| 			<> |  | ||||||
| 				<Flex |  | ||||||
| 					justifyContent="space-between" |  | ||||||
| 					alignItems="center" |  | ||||||
| 					paddingTop="14px" |  | ||||||
| 					style={{ |  | ||||||
| 						// Allow window to be dragged from header |  | ||||||
| 						// @ts-ignore |  | ||||||
| 						'-webkit-app-region': 'drag', |  | ||||||
| 						position: 'relative', |  | ||||||
| 						zIndex: 1, |  | ||||||
| 					}} |  | ||||||
| 				> |  | ||||||
| 					<Flex width="100%" /> |  | ||||||
| 					<Flex width="100%" alignItems="center" justifyContent="center"> |  | ||||||
| 						<EtcherSvg |  | ||||||
| 							width="123px" |  | ||||||
| 							height="22px" |  | ||||||
| 							style={{ |  | ||||||
| 								cursor: 'pointer', |  | ||||||
| 							}} |  | ||||||
| 							onClick={() => |  | ||||||
| 								openExternal('https://www.balena.io/etcher?ref=etcher_footer') |  | ||||||
| 							} |  | ||||||
| 							tabIndex={100} |  | ||||||
| 						/> |  | ||||||
| 					</Flex> |  | ||||||
|  |  | ||||||
| 					<Flex width="100%" alignItems="center" justifyContent="flex-end"> |  | ||||||
| 						<Icon |  | ||||||
| 							icon={<CogSvg height="1em" fill="currentColor" />} |  | ||||||
| 							plain |  | ||||||
| 							tabIndex={5} |  | ||||||
| 							onClick={() => this.setState({ hideSettings: false })} |  | ||||||
| 							style={{ |  | ||||||
| 								// Make touch events click instead of dragging |  | ||||||
| 								'-webkit-app-region': 'no-drag', |  | ||||||
| 							}} |  | ||||||
| 						/> |  | ||||||
| 						{!settings.getSync('disableExternalLinks') && ( |  | ||||||
| 							<Icon |  | ||||||
| 								icon={<QuestionCircleSvg height="1em" fill="currentColor" />} |  | ||||||
| 								onClick={() => |  | ||||||
| 									openExternal( |  | ||||||
| 										selectionState.getImageSupportUrl() || |  | ||||||
| 											'https://github.com/balena-io/etcher/blob/master/SUPPORT.md', |  | ||||||
| 									) |  | ||||||
| 								} |  | ||||||
| 								tabIndex={6} |  | ||||||
| 								style={{ |  | ||||||
| 									// Make touch events click instead of dragging |  | ||||||
| 									'-webkit-app-region': 'no-drag', |  | ||||||
| 								}} |  | ||||||
| 							/> |  | ||||||
| 						)} |  | ||||||
| 					</Flex> |  | ||||||
| 				</Flex> |  | ||||||
| 				{this.state.hideSettings ? null : ( |  | ||||||
| 					<SettingsModal |  | ||||||
| 						toggleModal={(value: boolean) => { |  | ||||||
| 							this.setState({ hideSettings: !value }); |  | ||||||
| 						}} |  | ||||||
| 					/> |  | ||||||
| 				)} |  | ||||||
|  |  | ||||||
| 			<Flex | 			<Flex | ||||||
| 				m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`} | 				m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`} | ||||||
| 				justifyContent="space-between" | 				justifyContent="space-between" | ||||||
| @@ -315,35 +249,86 @@ export class MainPage extends React.Component< | |||||||
| 					style={{ zIndex: 1 }} | 					style={{ zIndex: 1 }} | ||||||
| 				/> | 				/> | ||||||
| 			</Flex> | 			</Flex> | ||||||
| 			</> |  | ||||||
| 		); | 		); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	private renderSuccess() { | 	private renderSuccess() { | ||||||
| 		return ( | 		return ( | ||||||
| 			<Flex flexDirection="column" alignItems="center" height="100%"> |  | ||||||
| 			<FinishPage | 			<FinishPage | ||||||
| 				goToMain={() => { | 				goToMain={() => { | ||||||
| 					flashState.resetState(); | 					flashState.resetState(); | ||||||
| 					this.setState({ current: 'main' }); | 					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() { | 	public render() { | ||||||
| 		return ( | 		return ( | ||||||
| 			<ThemedProvider style={{ height: '100%', width: '100%' }}> | 			<ThemedProvider style={{ height: '100%', width: '100%' }}> | ||||||
|  | 				<Flex | ||||||
|  | 					justifyContent="space-between" | ||||||
|  | 					alignItems="center" | ||||||
|  | 					paddingTop="14px" | ||||||
|  | 					style={{ | ||||||
|  | 						// Allow window to be dragged from header | ||||||
|  | 						// @ts-ignore | ||||||
|  | 						'-webkit-app-region': 'drag', | ||||||
|  | 						position: 'relative', | ||||||
|  | 						zIndex: 1, | ||||||
|  | 					}} | ||||||
|  | 				> | ||||||
|  | 					<Flex width="100%" /> | ||||||
|  | 					<Flex width="100%" alignItems="center" justifyContent="center"> | ||||||
|  | 						<EtcherSvg | ||||||
|  | 							width="123px" | ||||||
|  | 							height="22px" | ||||||
|  | 							style={{ | ||||||
|  | 								cursor: 'pointer', | ||||||
|  | 							}} | ||||||
|  | 							onClick={() => | ||||||
|  | 								openExternal('https://www.balena.io/etcher?ref=etcher_footer') | ||||||
|  | 							} | ||||||
|  | 							tabIndex={100} | ||||||
|  | 						/> | ||||||
|  | 					</Flex> | ||||||
|  |  | ||||||
|  | 					<Flex width="100%" alignItems="center" justifyContent="flex-end"> | ||||||
|  | 						<Icon | ||||||
|  | 							icon={<CogSvg height="1em" fill="currentColor" />} | ||||||
|  | 							plain | ||||||
|  | 							tabIndex={5} | ||||||
|  | 							onClick={() => this.setState({ hideSettings: false })} | ||||||
|  | 							style={{ | ||||||
|  | 								// Make touch events click instead of dragging | ||||||
|  | 								'-webkit-app-region': 'no-drag', | ||||||
|  | 							}} | ||||||
|  | 						/> | ||||||
|  | 						{!settings.getSync('disableExternalLinks') && ( | ||||||
|  | 							<Icon | ||||||
|  | 								icon={<QuestionCircleSvg height="1em" fill="currentColor" />} | ||||||
|  | 								onClick={() => | ||||||
|  | 									openExternal( | ||||||
|  | 										selectionState.getImageSupportUrl() || | ||||||
|  | 											'https://github.com/balena-io/etcher/blob/master/SUPPORT.md', | ||||||
|  | 									) | ||||||
|  | 								} | ||||||
|  | 								tabIndex={6} | ||||||
|  | 								style={{ | ||||||
|  | 									// Make touch events click instead of dragging | ||||||
|  | 									'-webkit-app-region': 'no-drag', | ||||||
|  | 								}} | ||||||
|  | 							/> | ||||||
|  | 						)} | ||||||
|  | 					</Flex> | ||||||
|  | 				</Flex> | ||||||
|  | 				{this.state.hideSettings ? null : ( | ||||||
|  | 					<SettingsModal | ||||||
|  | 						toggleModal={(value: boolean) => { | ||||||
|  | 							this.setState({ hideSettings: !value }); | ||||||
|  | 						}} | ||||||
|  | 					/> | ||||||
|  | 				)} | ||||||
| 				{this.state.current === 'main' | 				{this.state.current === 'main' | ||||||
| 					? this.renderMain() | 					? this.renderMain() | ||||||
| 					: this.renderSuccess()} | 					: this.renderSuccess()} | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | import * as _ from 'lodash'; | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { | ||||||
| 	Alert as AlertBase, | 	Alert as AlertBase, | ||||||
| @@ -23,27 +24,16 @@ import { | |||||||
| 	ButtonProps, | 	ButtonProps, | ||||||
| 	Modal as ModalBase, | 	Modal as ModalBase, | ||||||
| 	Provider, | 	Provider, | ||||||
|  | 	Table as BaseTable, | ||||||
|  | 	TableProps as BaseTableProps, | ||||||
| 	Txt, | 	Txt, | ||||||
| 	Theme as renditionTheme, |  | ||||||
| } from 'rendition'; | } from 'rendition'; | ||||||
| import styled, { css } from 'styled-components'; | import styled, { css } from 'styled-components'; | ||||||
|  |  | ||||||
| import { colors, theme } from './theme'; | import { colors, theme } from './theme'; | ||||||
|  |  | ||||||
| const defaultTheme = { |  | ||||||
| 	...renditionTheme, |  | ||||||
| 	...theme, |  | ||||||
| 	layer: { |  | ||||||
| 		extend: () => ` |  | ||||||
| 			> div:first-child { |  | ||||||
| 				background-color: transparent; |  | ||||||
| 			} |  | ||||||
| 		`, |  | ||||||
| 	}, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const ThemedProvider = (props: any) => ( | export const ThemedProvider = (props: any) => ( | ||||||
| 	<Provider theme={defaultTheme} {...props}></Provider> | 	<Provider theme={theme} {...props}></Provider> | ||||||
| ); | ); | ||||||
|  |  | ||||||
| export const BaseButton = styled(Button)` | export const BaseButton = styled(Button)` | ||||||
| @@ -134,24 +124,23 @@ const modalFooterShadowCss = css` | |||||||
| 	background-attachment: local, local, scroll, scroll; | 	background-attachment: local, local, scroll, scroll; | ||||||
| `; | `; | ||||||
|  |  | ||||||
| export const Modal = styled(({ style, ...props }) => { | export const Modal = styled(({ style, children, ...props }) => { | ||||||
| 	return ( | 	return ( | ||||||
| 		<Provider | 		<Provider | ||||||
| 			theme={{ | 			theme={_.merge({}, theme, { | ||||||
| 				...defaultTheme, |  | ||||||
| 				header: { | 				header: { | ||||||
| 					height: '50px', | 					height: '50px', | ||||||
| 				}, | 				}, | ||||||
| 				layer: { | 				layer: { | ||||||
| 					extend: () => ` | 					extend: () => ` | ||||||
| 					${defaultTheme.layer.extend()} | 						${theme.layer.extend()} | ||||||
|  |  | ||||||
| 						> div:last-child { | 						> div:last-child { | ||||||
| 							top: 0; | 							top: 0; | ||||||
| 						} | 						} | ||||||
| 					`, | 					`, | ||||||
| 				}, | 				}, | ||||||
| 			}} | 			})} | ||||||
| 		> | 		> | ||||||
| 			<ModalBase | 			<ModalBase | ||||||
| 				position="top" | 				position="top" | ||||||
| @@ -167,7 +156,11 @@ export const Modal = styled(({ style, ...props }) => { | |||||||
| 					...style, | 					...style, | ||||||
| 				}} | 				}} | ||||||
| 				{...props} | 				{...props} | ||||||
| 			/> | 			> | ||||||
|  | 				<ScrollableFlex flexDirection="column" width="100%" height="90%"> | ||||||
|  | 					{...children} | ||||||
|  | 				</ScrollableFlex> | ||||||
|  | 			</ModalBase> | ||||||
| 		</Provider> | 		</Provider> | ||||||
| 	); | 	); | ||||||
| })` | })` | ||||||
| @@ -175,6 +168,11 @@ export const Modal = styled(({ style, ...props }) => { | |||||||
| 		padding: 0; | 		padding: 0; | ||||||
| 		height: 100%; | 		height: 100%; | ||||||
|  |  | ||||||
|  | 		> div:first-child { | ||||||
|  | 			height: 81%; | ||||||
|  | 			padding: 24px 30px 0; | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		> h3 { | 		> h3 { | ||||||
| 			margin: 0; | 			margin: 0; | ||||||
| 			padding: 24px 30px 0; | 			padding: 24px 30px 0; | ||||||
| @@ -188,12 +186,9 @@ export const Modal = styled(({ style, ...props }) => { | |||||||
|  |  | ||||||
| 		> div:nth-child(2) { | 		> div:nth-child(2) { | ||||||
| 			height: 61%; | 			height: 61%; | ||||||
|  |  | ||||||
| 			> div:not(.system-drive-alert) { |  | ||||||
| 			padding: 0 30px; | 			padding: 0 30px; | ||||||
| 			${modalFooterShadowCss} | 			${modalFooterShadowCss} | ||||||
| 		} | 		} | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		> div:last-child { | 		> div:last-child { | ||||||
| 			margin: 0; | 			margin: 0; | ||||||
| @@ -249,3 +244,99 @@ export const Alert = styled((props) => ( | |||||||
| 		display: none; | 		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. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | import * as _ from 'lodash'; | ||||||
|  | import { Theme } from 'rendition'; | ||||||
|  |  | ||||||
| export const colors = { | export const colors = { | ||||||
| 	dark: { | 	dark: { | ||||||
| 		foreground: '#fff', | 		foreground: '#fff', | ||||||
| @@ -67,8 +70,7 @@ export const colors = { | |||||||
|  |  | ||||||
| const font = 'SourceSansPro'; | const font = 'SourceSansPro'; | ||||||
|  |  | ||||||
| export const theme = { | export const theme = _.merge({}, Theme, { | ||||||
| 	colors, |  | ||||||
| 	font, | 	font, | ||||||
| 	global: { | 	global: { | ||||||
| 		font: { | 		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() { | async function createMainWindow() { | ||||||
| 	const fullscreen = Boolean(await settings.get('fullscreen')); | 	const fullscreen = Boolean(await settings.get('fullscreen')); | ||||||
| 	const defaultWidth = 800; | 	const defaultWidth = settings.DEFAULT_WIDTH; | ||||||
| 	const defaultHeight = 480; | 	const defaultHeight = settings.DEFAULT_HEIGHT; | ||||||
| 	let width = defaultWidth; | 	let width = defaultWidth; | ||||||
| 	let height = defaultHeight; | 	let height = defaultHeight; | ||||||
| 	if (fullscreen) { | 	if (fullscreen) { | ||||||
|   | |||||||
| @@ -17,6 +17,8 @@ | |||||||
| import { Drive as DrivelistDrive } from 'drivelist'; | import { Drive as DrivelistDrive } from 'drivelist'; | ||||||
| import * as sdk from 'etcher-sdk'; | import * as sdk from 'etcher-sdk'; | ||||||
| import { cleanupTmpFiles } from 'etcher-sdk/build/tmp'; | import { cleanupTmpFiles } from 'etcher-sdk/build/tmp'; | ||||||
|  | import { promises as fs } from 'fs'; | ||||||
|  | import * as _ from 'lodash'; | ||||||
| import * as ipc from 'node-ipc'; | import * as ipc from 'node-ipc'; | ||||||
| import { totalmem } from 'os'; | import { totalmem } from 'os'; | ||||||
|  |  | ||||||
| @@ -55,8 +57,9 @@ function log(message: string) { | |||||||
| /** | /** | ||||||
|  * @summary Terminate the child writer process |  * @summary Terminate the child writer process | ||||||
|  */ |  */ | ||||||
| function terminate(exitCode: number) { | async function terminate(exitCode: number) { | ||||||
| 	ipc.disconnect(IPC_SERVER_ID); | 	ipc.disconnect(IPC_SERVER_ID); | ||||||
|  | 	await cleanupTmpFiles(Date.now()); | ||||||
| 	process.nextTick(() => { | 	process.nextTick(() => { | ||||||
| 		process.exit(exitCode || SUCCESS); | 		process.exit(exitCode || SUCCESS); | ||||||
| 	}); | 	}); | ||||||
| @@ -68,7 +71,7 @@ function terminate(exitCode: number) { | |||||||
| async function handleError(error: Error) { | async function handleError(error: Error) { | ||||||
| 	ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); | 	ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); | ||||||
| 	await delay(DISCONNECT_DELAY); | 	await delay(DISCONNECT_DELAY); | ||||||
| 	terminate(GENERAL_ERROR); | 	await terminate(GENERAL_ERROR); | ||||||
| } | } | ||||||
|  |  | ||||||
| interface WriteResult { | interface WriteResult { | ||||||
| @@ -136,8 +139,10 @@ async function writeAndValidate({ | |||||||
| 		sourceMetadata, | 		sourceMetadata, | ||||||
| 	}; | 	}; | ||||||
| 	for (const [destination, error] of failures) { | 	for (const [destination, error] of failures) { | ||||||
| 		const err = error as Error & { device: string }; | 		const err = error as Error & { device: string; description: string }; | ||||||
| 		err.device = (destination as sdk.sourceDestination.BlockDevice).device; | 		const drive = destination as sdk.sourceDestination.BlockDevice; | ||||||
|  | 		err.device = drive.device; | ||||||
|  | 		err.description = drive.description; | ||||||
| 		result.errors.push(err); | 		result.errors.push(err); | ||||||
| 	} | 	} | ||||||
| 	return result; | 	return result; | ||||||
| @@ -151,6 +156,13 @@ interface WriteOptions { | |||||||
| 	autoBlockmapping: boolean; | 	autoBlockmapping: boolean; | ||||||
| 	decompressFirst: boolean; | 	decompressFirst: boolean; | ||||||
| 	SourceType: string; | 	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, () => { | ipc.connectTo(IPC_SERVER_ID, () => { | ||||||
| @@ -163,22 +175,22 @@ ipc.connectTo(IPC_SERVER_ID, () => { | |||||||
| 	// no flashing information is available, then it will | 	// no flashing information is available, then it will | ||||||
| 	// assume that the child died halfway through. | 	// assume that the child died halfway through. | ||||||
|  |  | ||||||
| 	process.once('SIGINT', () => { | 	process.once('SIGINT', async () => { | ||||||
| 		terminate(SUCCESS); | 		await terminate(SUCCESS); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	process.once('SIGTERM', () => { | 	process.once('SIGTERM', async () => { | ||||||
| 		terminate(SUCCESS); | 		await terminate(SUCCESS); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	// The IPC server failed. Abort. | 	// The IPC server failed. Abort. | ||||||
| 	ipc.of[IPC_SERVER_ID].on('error', () => { | 	ipc.of[IPC_SERVER_ID].on('error', async () => { | ||||||
| 		terminate(SUCCESS); | 		await terminate(SUCCESS); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	// The IPC server was disconnected. Abort. | 	// The IPC server was disconnected. Abort. | ||||||
| 	ipc.of[IPC_SERVER_ID].on('disconnect', () => { | 	ipc.of[IPC_SERVER_ID].on('disconnect', async () => { | ||||||
| 		terminate(SUCCESS); | 		await terminate(SUCCESS); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => { | 	ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => { | ||||||
| @@ -188,7 +200,7 @@ ipc.connectTo(IPC_SERVER_ID, () => { | |||||||
| 		 * @example | 		 * @example | ||||||
| 		 * writer.on('progress', onProgress) | 		 * writer.on('progress', onProgress) | ||||||
| 		 */ | 		 */ | ||||||
| 		const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => { | 		const onProgress = (state: ProgressState) => { | ||||||
| 			ipc.of[IPC_SERVER_ID].emit('state', state); | 			ipc.of[IPC_SERVER_ID].emit('state', state); | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| @@ -203,11 +215,20 @@ ipc.connectTo(IPC_SERVER_ID, () => { | |||||||
| 			log('Abort'); | 			log('Abort'); | ||||||
| 			ipc.of[IPC_SERVER_ID].emit('abort'); | 			ipc.of[IPC_SERVER_ID].emit('abort'); | ||||||
| 			await delay(DISCONNECT_DELAY); | 			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('cancel', onAbort); | ||||||
|  |  | ||||||
|  | 		ipc.of[IPC_SERVER_ID].on('skip', onSkip); | ||||||
|  |  | ||||||
| 		/** | 		/** | ||||||
| 		 * @summary Failure handler (non-fatal errors) | 		 * @summary Failure handler (non-fatal errors) | ||||||
| 		 * @param {SourceDestination} destination - destination | 		 * @param {SourceDestination} destination - destination | ||||||
| @@ -256,10 +277,19 @@ ipc.connectTo(IPC_SERVER_ID, () => { | |||||||
| 					source = new File({ | 					source = new File({ | ||||||
| 						path: imagePath, | 						path: imagePath, | ||||||
| 					}); | 					}); | ||||||
|  | 				} else { | ||||||
|  | 					if (options.saveUrlImage) { | ||||||
|  | 						source = await saveFileBeforeFlash( | ||||||
|  | 							imagePath, | ||||||
|  | 							options.saveUrlImageTo, | ||||||
|  | 							onProgress, | ||||||
|  | 							onFail, | ||||||
|  | 						); | ||||||
| 					} else { | 					} else { | ||||||
| 						source = new Http({ url: imagePath, avoidRandomAccess: true }); | 						source = new Http({ url: imagePath, avoidRandomAccess: true }); | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  | 			} | ||||||
| 			const results = await writeAndValidate({ | 			const results = await writeAndValidate({ | ||||||
| 				source, | 				source, | ||||||
| 				destinations: dests, | 				destinations: dests, | ||||||
| @@ -275,7 +305,7 @@ ipc.connectTo(IPC_SERVER_ID, () => { | |||||||
| 			}); | 			}); | ||||||
| 			ipc.of[IPC_SERVER_ID].emit('done', { results }); | 			ipc.of[IPC_SERVER_ID].emit('done', { results }); | ||||||
| 			await delay(DISCONNECT_DELAY); | 			await delay(DISCONNECT_DELAY); | ||||||
| 			terminate(exitCode); | 			await terminate(exitCode); | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			log(`Error: ${error.message}`); | 			log(`Error: ${error.message}`); | ||||||
| 			exitCode = GENERAL_ERROR; | 			exitCode = GENERAL_ERROR; | ||||||
| @@ -290,3 +320,43 @@ ipc.connectTo(IPC_SERVER_ID, () => { | |||||||
| 		ipc.of[IPC_SERVER_ID].emit('ready', {}); | 		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 }); | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										155
									
								
								npm-shrinkwrap.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										155
									
								
								npm-shrinkwrap.json
									
									
									
										generated
									
									
									
								
							| @@ -1570,34 +1570,32 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "@react-google-maps/api": { |     "@react-google-maps/api": { | ||||||
|       "version": "1.9.12", |       "version": "1.10.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.9.12.tgz", |       "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.10.1.tgz", | ||||||
|       "integrity": "sha512-YpYZOMduxiQIt8+njdffoqD4fYdOugudoafnAD1N+mEUrVnFlslUPMQ+gOJwuYdlkTAR5NZUbCt80LJWEN+ZnA==", |       "integrity": "sha512-hb8urUcwZw99Cu3yQnZWUbXjR1Ym/8C21kSX6B02I29l6DXNxDbJ5Jo/T5swhnizPKY7TNhR1oTctC/HY7SQWA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@react-google-maps/infobox": "1.9.11", |         "@react-google-maps/infobox": "1.10.0", | ||||||
|         "@react-google-maps/marker-clusterer": "1.9.11", |         "@react-google-maps/marker-clusterer": "1.10.0", | ||||||
|         "acorn": "7.4.0", |  | ||||||
|         "acorn-jsx": "^5.2.0", |  | ||||||
|         "invariant": "2.2.4" |         "invariant": "2.2.4" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "@react-google-maps/infobox": { |     "@react-google-maps/infobox": { | ||||||
|       "version": "1.9.11", |       "version": "1.10.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.9.11.tgz", |       "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.10.0.tgz", | ||||||
|       "integrity": "sha512-22ewm+OpOh69ikypG29idsdRz2OWeFsN+8zvYBzSETxKP782rmUGqhSIvXXmHa8TOcktm7EaEqOWWvZwaxymag==", |       "integrity": "sha512-MhT2nMmjeG7TCxRv/JdylDyNd/n66ggSQQhTWVjJJTtdB/xqd0T8BHCkBWDN9uF0i0yCZzMFl2P2Y1zJ+xppBg==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "@react-google-maps/marker-clusterer": { |     "@react-google-maps/marker-clusterer": { | ||||||
|       "version": "1.9.11", |       "version": "1.10.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.9.11.tgz", |       "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.10.0.tgz", | ||||||
|       "integrity": "sha512-yIABKlkORju131efXUZs/tL7FCK9IXtvy2M9SQRZy/mwgoOIYeoJlPPaBjn81DQqZLRj6AdAocydk+MnjWqFiQ==", |       "integrity": "sha512-3GLVgeXNStVcdiLMxzi3cBjr32ctlexLPPGQguwcYd6yPLaCcnVCwyzhV68KvL00xqOAD1c3aABV9EGgY8u6Qw==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "@rjsf/core": { |     "@rjsf/core": { | ||||||
|       "version": "2.3.0", |       "version": "2.4.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.3.0.tgz", |       "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.4.0.tgz", | ||||||
|       "integrity": "sha512-OZKYHt9tjKhzOH4CvsPiCwepuIacqI++cNmnL2fsxh1IF+uEWGlo3NLDWhhSaBbOv9jps6a5YQcLbLtjNuSwug==", |       "integrity": "sha512-8zlydBkGldOxGXFEwNGFa1gzTxpcxaYn7ofegcu8XHJ7IKMCfpnU3ABg+H3eml1KZCX3FODmj1tHFJKuTmfynw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@babel/runtime-corejs2": "^7.8.7", |         "@babel/runtime-corejs2": "^7.8.7", | ||||||
| @@ -2180,9 +2178,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "@types/react-native": { |     "@types/react-native": { | ||||||
|       "version": "0.63.9", |       "version": "0.63.18", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.9.tgz", |       "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.18.tgz", | ||||||
|       "integrity": "sha512-6ec/z9zjAkFH3rD1RYqbrA/Lj+jux6bumWCte4yRy3leyelTdqtmOd2Ph+86IXQQzsIArEMBwmraAbNQ0J3UAA==", |       "integrity": "sha512-WwEWqmHiqFn61M1FZR/+frj+E8e2o8i5cPqu9mjbjtZS/gBfCKVESF2ai/KAlaQECkkWkx/nMJeCc5eHMmLQgw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@types/react": "*" |         "@types/react": "*" | ||||||
| @@ -2237,9 +2235,9 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "@types/styled-components": { |     "@types/styled-components": { | ||||||
|       "version": "5.1.2", |       "version": "5.1.3", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz", |       "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.3.tgz", | ||||||
|       "integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==", |       "integrity": "sha512-HGpirof3WOhiX17lb61Q/tpgqn48jxO8EfZkdJ8ueYqwLbK2AHQe/G08DasdA2IdKnmwOIP1s9X2bopxKXgjRw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@types/hoist-non-react-statics": "*", |         "@types/hoist-non-react-statics": "*", | ||||||
| @@ -2692,18 +2690,6 @@ | |||||||
|       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", |       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", | ||||||
|       "dev": true |       "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": { |     "agent-base": { | ||||||
|       "version": "4.3.0", |       "version": "4.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", |       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", | ||||||
| @@ -5281,6 +5267,12 @@ | |||||||
|         "assert-plus": "^1.0.0" |         "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": { |     "de-indent": { | ||||||
|       "version": "1.0.2", |       "version": "1.0.2", | ||||||
|       "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", |       "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", | ||||||
| @@ -5496,15 +5488,6 @@ | |||||||
|         "minimalistic-assert": "^1.0.0" |         "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": { |     "detect-file": { | ||||||
|       "version": "1.0.0", |       "version": "1.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", | ||||||
| @@ -8939,9 +8922,9 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "json-e": { |     "json-e": { | ||||||
|       "version": "4.1.0", |       "version": "4.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.1.0.tgz", |       "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.3.0.tgz", | ||||||
|       "integrity": "sha512-Jb8kMB1lICgjAAppv+q0EFFovOPdjE3htb7pt9+uE2j3J1W5ZCuBOmAdGi0OUetCZ4wqSO6qT/Np36XDRjHH7w==", |       "integrity": "sha512-E3zcmx6pHsBgQ4ZztQNG4OAZHreBZfGBrg68kv9nGOkRqAdKfs792asP/wp9Fayfx1THDiHKYStqWJj/N7Bb9A==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "json-stable-stringify-without-jsonify": "^1.0.1" |         "json-stable-stringify-without-jsonify": "^1.0.1" | ||||||
| @@ -9749,18 +9732,15 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "mdast-util-to-hast": { |     "mdast-util-to-hast": { | ||||||
|       "version": "9.1.0", |       "version": "9.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.0.tgz", |       "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.1.tgz", | ||||||
|       "integrity": "sha512-Akl2Vi9y9cSdr19/Dfu58PVwifPXuFt1IrHe7l+Crme1KvgUT+5z+cHLVcQVGCiNTZZcdqjnuv9vPkGsqWytWA==", |       "integrity": "sha512-vpMWKFKM2mnle+YbNgDXxx95vv0CoLU0v/l3F5oFAG5DV7qwkZVWA206LsAdOnEVyf5vQcLnb3cWJywu7mUxsQ==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@types/mdast": "^3.0.0", |         "@types/mdast": "^3.0.0", | ||||||
|         "@types/unist": "^2.0.3", |         "@types/unist": "^2.0.3", | ||||||
|         "collapse-white-space": "^1.0.0", |  | ||||||
|         "detab": "^2.0.0", |  | ||||||
|         "mdast-util-definitions": "^3.0.0", |         "mdast-util-definitions": "^3.0.0", | ||||||
|         "mdurl": "^1.0.0", |         "mdurl": "^1.0.0", | ||||||
|         "trim-lines": "^1.0.0", |  | ||||||
|         "unist-builder": "^2.0.0", |         "unist-builder": "^2.0.0", | ||||||
|         "unist-util-generated": "^1.0.0", |         "unist-util-generated": "^1.0.0", | ||||||
|         "unist-util-position": "^3.0.0", |         "unist-util-position": "^3.0.0", | ||||||
| @@ -9842,9 +9822,9 @@ | |||||||
|       }, |       }, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "crypto-random-string": { |         "crypto-random-string": { | ||||||
|           "version": "3.2.0", |           "version": "3.3.0", | ||||||
|           "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.2.0.tgz", |           "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.0.tgz", | ||||||
|           "integrity": "sha512-8vPu5bsKaq2uKRy3OL7h1Oo7RayAWB8sYexLKAqvCXVib8SxgbmoF1IN4QMKjBv8uI8mp5gPPMbiRah25GMrVQ==", |           "integrity": "sha512-teWAwfMb1d6brahYyKqcBEb5Yp8PJPvPOdOonXDnvaKOTmKDFNVE8E3Y2XQuzjNV/3XMwHbrX9fHWvrhRKt4Gg==", | ||||||
|           "dev": true, |           "dev": true, | ||||||
|           "requires": { |           "requires": { | ||||||
|             "type-fest": "^0.8.1" |             "type-fest": "^0.8.1" | ||||||
| @@ -11897,9 +11877,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "polished": { |     "polished": { | ||||||
|       "version": "3.6.5", |       "version": "3.6.6", | ||||||
|       "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.5.tgz", |       "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.6.tgz", | ||||||
|       "integrity": "sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ==", |       "integrity": "sha512-yiB2ims2DZPem0kCD6V0wnhcVGFEhNh0Iw0axNpKU+oSAgFt6yx6HxIT23Qg0WWvgS379cS35zT4AOyZZRzpQQ==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@babel/runtime": "^7.9.2" |         "@babel/runtime": "^7.9.2" | ||||||
| @@ -12511,9 +12491,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "react-notifications-component": { |     "react-notifications-component": { | ||||||
|       "version": "2.4.0", |       "version": "2.4.1", | ||||||
|       "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.0.tgz", |       "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.1.tgz", | ||||||
|       "integrity": "sha512-0IhtgqAmsKSyjY1wBUxciUVXiYGRr5BRdn67pYDlkqq9ORF98NZekpG7/MNX0BzzfGvt9Wg7rFhT1BtwOvvLLg==", |       "integrity": "sha512-RloHzm15egnuPihf8PvldIEvPQoT9+5BE9UxCNTt+GfsWeI3SEZKyaX9mq90v899boqteLiOI736Zd4tXtl7Tg==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "prop-types": "^15.6.2" |         "prop-types": "^15.6.2" | ||||||
| @@ -12660,6 +12640,21 @@ | |||||||
|       "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", |       "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", | ||||||
|       "dev": true |       "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": { |     "regexpu-core": { | ||||||
|       "version": "4.7.0", |       "version": "4.7.0", | ||||||
|       "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", |       "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", | ||||||
| @@ -12827,9 +12822,9 @@ | |||||||
|       "optional": true |       "optional": true | ||||||
|     }, |     }, | ||||||
|     "rendition": { |     "rendition": { | ||||||
|       "version": "18.4.1", |       "version": "18.8.3", | ||||||
|       "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.4.1.tgz", |       "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.8.3.tgz", | ||||||
|       "integrity": "sha512-mV/0p+M8XR/Xa/ZFzgflZPHelpuONiTSa/CMMuHkmXR7vhF7tB2ORxLRc/DbymmdN6cWQwXAyA81t9TDAOhgVQ==", |       "integrity": "sha512-kDuXFheXY9KlSvIMdB4Er2OeAnwgj9aya5Xu43hwpXxC4KlFlNKqQNmcOvKLc/Fk9dyw04TKOr1SbXyM148yRg==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@fortawesome/fontawesome-svg-core": "^1.2.25", |         "@fortawesome/fontawesome-svg-core": "^1.2.25", | ||||||
| @@ -12855,6 +12850,7 @@ | |||||||
|         "color": "^3.1.2", |         "color": "^3.1.2", | ||||||
|         "color-hash": "^1.0.3", |         "color-hash": "^1.0.3", | ||||||
|         "copy-to-clipboard": "^3.0.8", |         "copy-to-clipboard": "^3.0.8", | ||||||
|  |         "date-fns": "^2.16.1", | ||||||
|         "grommet": "^2.14.0", |         "grommet": "^2.14.0", | ||||||
|         "hast-util-sanitize": "^3.0.0", |         "hast-util-sanitize": "^3.0.0", | ||||||
|         "json-e": "^4.1.0", |         "json-e": "^4.1.0", | ||||||
| @@ -12869,6 +12865,7 @@ | |||||||
|         "react-simplemde-editor": "^4.1.1", |         "react-simplemde-editor": "^4.1.1", | ||||||
|         "recompose": "0.26.0", |         "recompose": "0.26.0", | ||||||
|         "regex-parser": "^2.2.7", |         "regex-parser": "^2.2.7", | ||||||
|  |         "regexp-match-indices": "^1.0.2", | ||||||
|         "rehype-raw": "^4.0.2", |         "rehype-raw": "^4.0.2", | ||||||
|         "rehype-react": "^6.1.0", |         "rehype-react": "^6.1.0", | ||||||
|         "rehype-sanitize": "^3.0.1", |         "rehype-sanitize": "^3.0.1", | ||||||
| @@ -12885,9 +12882,9 @@ | |||||||
|       }, |       }, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@types/node": { |         "@types/node": { | ||||||
|           "version": "13.13.15", |           "version": "13.13.20", | ||||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz", |           "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.20.tgz", | ||||||
|           "integrity": "sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw==", |           "integrity": "sha512-1kx55tU3AvGX2Cjk2W4GMBxbgIz892V+X10S2gUreIAq8qCWgaQH+tZBOWc0bi2BKFhQt+CX0BTx28V9QPNa+A==", | ||||||
|           "dev": true |           "dev": true | ||||||
|         }, |         }, | ||||||
|         "uuid": { |         "uuid": { | ||||||
| @@ -14745,12 +14742,6 @@ | |||||||
|       "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", |       "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", | ||||||
|       "dev": true |       "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": { |     "trim-trailing-lines": { | ||||||
|       "version": "1.1.3", |       "version": "1.1.3", | ||||||
|       "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", |       "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", | ||||||
| @@ -15035,9 +15026,9 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "uglify-js": { |     "uglify-js": { | ||||||
|       "version": "3.10.2", |       "version": "3.10.4", | ||||||
|       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.2.tgz", |       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz", | ||||||
|       "integrity": "sha512-GXCYNwqoo0MbLARghYjxVBxDCnU0tLqN7IPLdHHbibCb1NI5zBkU2EPcy/GaVxc0BtTjqyGXJCINe6JMR2Dpow==", |       "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "unbzip2-stream": { |     "unbzip2-stream": { | ||||||
| @@ -16466,9 +16457,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "whatwg-fetch": { |     "whatwg-fetch": { | ||||||
|       "version": "3.4.0", |       "version": "3.4.1", | ||||||
|       "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", |       "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", | ||||||
|       "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==", |       "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "which": { |     "which": { | ||||||
| @@ -16660,9 +16651,9 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "xterm": { |     "xterm": { | ||||||
|       "version": "4.8.1", |       "version": "4.9.0", | ||||||
|       "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.8.1.tgz", |       "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz", | ||||||
|       "integrity": "sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ==", |       "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==", | ||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "xterm-addon-fit": { |     "xterm-addon-fit": { | ||||||
|   | |||||||
| @@ -94,7 +94,7 @@ | |||||||
|     "react": "^16.8.5", |     "react": "^16.8.5", | ||||||
|     "react-dom": "^16.8.5", |     "react-dom": "^16.8.5", | ||||||
|     "redux": "^4.0.5", |     "redux": "^4.0.5", | ||||||
|     "rendition": "^18.4.1", |     "rendition": "^18.8.3", | ||||||
|     "resin-corvus": "^2.0.5", |     "resin-corvus": "^2.0.5", | ||||||
|     "semver": "^7.3.2", |     "semver": "^7.3.2", | ||||||
|     "simple-progress-webpack-plugin": "^1.1.2", |     "simple-progress-webpack-plugin": "^1.1.2", | ||||||
|   | |||||||
| @@ -393,6 +393,7 @@ describe('Model: flashState', function () { | |||||||
|  |  | ||||||
| 				expect(flashResults).to.deep.equal({ | 				expect(flashResults).to.deep.equal({ | ||||||
| 					cancelled: false, | 					cancelled: false, | ||||||
|  | 					skip: false, | ||||||
| 					sourceChecksum: '1234', | 					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 () { | 				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( | 					expect( | ||||||
| 						constraints.isDriveValid(this.drive, { | 						constraints.isDriveValid(this.drive, { | ||||||
| 							...image, | 							...image, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user