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