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