diff --git a/lib/gui/app/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx index ffa08aff..ee8a60f4 100644 --- a/lib/gui/app/components/drive-selector/drive-selector.tsx +++ b/lib/gui/app/components/drive-selector/drive-selector.tsx @@ -43,6 +43,7 @@ import { } from '../../styled-components'; import { SourceMetadata } from '../source-selector/source-selector'; +import { middleEllipsis } from '../../utils/middle-ellipsis'; interface UsbbootDrive extends sourceDestination.UsbbootDrive { progress: number; @@ -136,17 +137,18 @@ const InitProgress = styled( `; export interface DriveSelectorProps - extends Omit { + extends Omit { write: boolean; multipleSelection: boolean; showWarnings?: boolean; - cancel: () => void; + cancel: (drives: DrivelistDrive[]) => void; done: (drives: DrivelistDrive[]) => void; titleLabel: string; emptyListLabel: string; emptyListIcon: JSX.Element; selectedList?: DrivelistDrive[]; updateSelectedList?: () => DrivelistDrive[]; + onSelect?: (drive: DrivelistDrive) => void; } interface DriveSelectorState { @@ -167,12 +169,14 @@ export class DriveSelector extends React.Component< > { private unsubscribe: (() => void) | undefined; tableColumns: Array>; + originalList: DrivelistDrive[]; constructor(props: DriveSelectorProps) { super(props); const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; const selectedList = this.props.selectedList || []; + this.originalList = [...(this.props.selectedList || [])]; this.state = { drives: getDrives(), @@ -199,7 +203,9 @@ export class DriveSelector extends React.Component< fill={drive.isSystem ? '#fca321' : '#8f9297'} /> )} - {description} + + {middleEllipsis(description, 32)} + ); } @@ -259,7 +265,7 @@ export class DriveSelector extends React.Component< return ( isUsbbootDrive(drive) || isDriverlessDrive(drive) || - !isDriveValid(drive, image) || + !isDriveValid(drive, image, this.props.write) || (this.props.write && drive.isReadOnly) ); } @@ -348,16 +354,6 @@ export class DriveSelector extends React.Component< } } - private deselectingAll(rows: DrivelistDrive[]) { - return ( - rows.length > 0 && - rows.length === this.state.selectedList.length && - this.state.selectedList.every( - (d) => rows.findIndex((r) => d.device === r.device) > -1, - ) - ); - } - componentDidMount() { this.unsubscribe = store.subscribe(() => { const drives = getDrives(); @@ -408,7 +404,7 @@ export class DriveSelector extends React.Component< } titleDetails={{getDrives().length} found} - cancel={cancel} + cancel={() => cancel(this.originalList)} done={() => done(selectedList)} action={`Select (${selectedList.length})`} primaryButtonProps={{ @@ -448,14 +444,34 @@ export class DriveSelector extends React.Component< onCheck={(rows: Drive[]) => { let newSelection = rows.filter(isDrivelistDrive); if (this.props.multipleSelection) { - if (this.deselectingAll(newSelection)) { + if (rows.length === 0) { newSelection = []; } + const deselecting = selectedList.filter( + (selected) => + newSelection.filter( + (row) => row.device === selected.device, + ).length === 0, + ); + const selecting = newSelection.filter( + (row) => + selectedList.filter( + (selected) => row.device === selected.device, + ).length === 0, + ); + deselecting.concat(selecting).forEach((row) => { + if (this.props.onSelect) { + this.props.onSelect(row); + } + }); this.setState({ selectedList: newSelection, }); return; } + if (this.props.onSelect) { + this.props.onSelect(newSelection[newSelection.length - 1]); + } this.setState({ selectedList: newSelection.slice(newSelection.length - 1), }); @@ -467,6 +483,9 @@ export class DriveSelector extends React.Component< ) { return; } + if (this.props.onSelect) { + this.props.onSelect(row); + } const index = selectedList.findIndex( (d) => d.device === row.device, ); diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 6f73c112..f669b891 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -558,6 +558,12 @@ export class SourceSelector extends React.Component< this.setState({ defaultFlowActive }); } + private closeModal() { + this.setState({ + showDriveSelector: false, + }); + } + // TODO add a visual change when dragging a file over the selector public render() { const { flashing } = this.props; @@ -661,6 +667,9 @@ export class SourceSelector extends React.Component< {this.state.warning != null && ( {' '} @@ -736,21 +745,30 @@ export class SourceSelector extends React.Component< titleLabel="Select source" emptyListLabel="Plug a source drive" emptyListIcon={} - cancel={() => { - this.setState({ - showDriveSelector: false, - }); - }} - done={async (drives: DrivelistDrive[]) => { - if (drives.length) { - await this.selectSource( - drives[0], - sourceDestination.BlockDevice, - ); + cancel={(originalList) => { + if (originalList.length) { + const originalSource = originalList[0]; + if (selectionImage?.drive?.device !== originalSource.device) { + this.selectSource( + originalSource, + sourceDestination.BlockDevice, + ); + } + } else { + selectionState.deselectImage(); + } + this.closeModal(); + }} + done={() => this.closeModal()} + onSelect={(drive) => { + if (drive) { + if ( + selectionState.getImage()?.drive?.device === drive?.device + ) { + return selectionState.deselectImage(); + } + this.selectSource(drive, sourceDestination.BlockDevice); } - this.setState({ - showDriveSelector: false, - }); }} /> )} diff --git a/lib/gui/app/components/target-selector/target-selector.tsx b/lib/gui/app/components/target-selector/target-selector.tsx index 1ce57dc1..7be61e8d 100644 --- a/lib/gui/app/components/target-selector/target-selector.tsx +++ b/lib/gui/app/components/target-selector/target-selector.tsx @@ -28,6 +28,7 @@ import { getSelectedDrives, deselectDrive, selectDrive, + deselectAllDrives, } from '../../models/selection-state'; import { observe } from '../../models/store'; import * as analytics from '../../modules/analytics'; @@ -164,11 +165,30 @@ export const TargetSelector = ({ {showTargetSelectorModal && ( setShowTargetSelectorModal(false)} - done={(modalTargets) => { - selectAllTargets(modalTargets); + cancel={(originalList) => { + if (originalList.length) { + selectAllTargets(originalList); + } else { + deselectAllDrives(); + } setShowTargetSelectorModal(false); }} + done={(modalTargets) => { + if (modalTargets.length === 0) { + deselectAllDrives(); + } + setShowTargetSelectorModal(false); + }} + onSelect={(drive) => { + if ( + getSelectedDrives().find( + (selectedDrive) => selectedDrive.device === drive.device, + ) + ) { + return deselectDrive(drive.device); + } + selectDrive(drive.device); + }} /> )} diff --git a/lib/gui/app/models/leds.ts b/lib/gui/app/models/leds.ts index a9aa7716..d9bce75f 100644 --- a/lib/gui/app/models/leds.ts +++ b/lib/gui/app/models/leds.ts @@ -17,12 +17,11 @@ import * as _ from 'lodash'; import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led'; -import { - isSourceDrive, - DrivelistDrive, -} from '../../../shared/drive-constraints'; +import { DrivelistDrive } from '../../../shared/drive-constraints'; +import { getDrives } from './available-drives'; +import { getImage, getSelectedDrives } from './selection-state'; import * as settings from './settings'; -import { DEFAULT_STATE, observe } from './store'; +import { observe, store } from './store'; const leds: Map = new Map(); const animator = new Animator([], 10); @@ -40,7 +39,7 @@ function createAnimationFunction( ): AnimationFunction { return (t: number): Color => { const intensity = intensityFunction(t); - return color.map((v) => v * intensity) as Color; + return color.map((v: number) => v * intensity) as Color; }; } @@ -160,35 +159,28 @@ export function updateLeds({ animator.mapping = mapping; } -interface DeviceFromState { - devicePath?: string; - device: string; -} - let ledsState: LedsState | undefined; -function stateObserver(state: typeof DEFAULT_STATE) { - const s = state.toJS(); +function stateObserver() { + const s = store.getState().toJS(); let step: 'main' | 'flashing' | 'verifying' | 'finish'; if (s.isFlashing) { step = s.flashState.type; } else { step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish'; } - const availableDrives = s.availableDrives.filter( - (d: DeviceFromState) => d.devicePath, + const availableDrives = getDrives().filter( + (d: DrivelistDrive) => d.devicePath, ); - const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) => - isSourceDrive(d, s.selection.image), - )[0]?.devicePath; + const sourceDrivePath = getImage()?.drive?.devicePath; const availableDrivesPaths = availableDrives.map( - (d: DeviceFromState) => d.devicePath, + (d: DrivelistDrive) => d.devicePath, ); let selectedDrivesPaths: string[]; if (step === 'main') { - selectedDrivesPaths = availableDrives - .filter((d: DrivelistDrive) => s.selection.devices.includes(d.device)) - .map((d: DrivelistDrive) => d.devicePath); + selectedDrivesPaths = getSelectedDrives() + .filter((drive) => drive.devicePath !== null) + .map((drive) => drive.devicePath) as string[]; } else { selectedDrivesPaths = s.devicePaths; } @@ -201,7 +193,7 @@ function stateObserver(state: typeof DEFAULT_STATE) { availableDrives: availableDrivesPaths, selectedDrives: selectedDrivesPaths, failedDrives: failedDevicePaths, - }; + } as LedsState; if (!_.isEqual(newLedsState, ledsState)) { updateLeds(newLedsState); ledsState = newLedsState; diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 8944b1e6..253d2597 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -276,7 +276,7 @@ export class MainPage extends React.Component< style={{ // Allow window to be dragged from header // @ts-ignore - '-webkit-app-region': 'drag', + WebkitAppRegion: 'drag', position: 'relative', zIndex: 2, }} @@ -304,7 +304,7 @@ export class MainPage extends React.Component< onClick={() => this.setState({ hideSettings: false })} style={{ // Make touch events click instead of dragging - '-webkit-app-region': 'no-drag', + WebkitAppRegion: 'no-drag', }} /> {!settings.getSync('disableExternalLinks') && ( @@ -319,7 +319,7 @@ export class MainPage extends React.Component< tabIndex={6} style={{ // Make touch events click instead of dragging - '-webkit-app-region': 'no-drag', + WebkitAppRegion: 'no-drag', }} /> )} diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts index 34e4241c..182d93ab 100644 --- a/lib/shared/drive-constraints.ts +++ b/lib/shared/drive-constraints.ts @@ -104,24 +104,19 @@ export function isDriveLargeEnough( return driveSize >= (image.size || UNKNOWN_SIZE); } -/** - * @summary Check if a drive is disabled (i.e. not ready for selection) - */ -export function isDriveDisabled(drive: DrivelistDrive): boolean { - return drive.disabled || false; -} - /** * @summary Check if a drive is valid, i.e. large enough for an image */ export function isDriveValid( drive: DrivelistDrive, image?: SourceMetadata, + write: boolean = true, ): boolean { return ( - isDriveLargeEnough(drive, image) && - !isSourceDrive(drive, image as SourceMetadata) && - !isDriveDisabled(drive) + !write || + (!drive.disabled && + isDriveLargeEnough(drive, image) && + !isSourceDrive(drive, image as SourceMetadata)) ); } diff --git a/tests/shared/drive-constraints.spec.ts b/tests/shared/drive-constraints.spec.ts index cda427f7..e6738ae6 100644 --- a/tests/shared/drive-constraints.spec.ts +++ b/tests/shared/drive-constraints.spec.ts @@ -514,40 +514,6 @@ describe('Shared: DriveConstraints', function () { }); }); - describe('.isDriveDisabled()', function () { - it('should return true if the drive is disabled', function () { - const result = constraints.isDriveDisabled(({ - device: '/dev/disk1', - size: 1000000000, - isReadOnly: false, - disabled: true, - } as unknown) as constraints.DrivelistDrive); - - expect(result).to.be.true; - }); - - it('should return false if the drive is not disabled', function () { - const result = constraints.isDriveDisabled(({ - device: '/dev/disk1', - size: 1000000000, - isReadOnly: false, - disabled: false, - } as unknown) as constraints.DrivelistDrive); - - expect(result).to.be.false; - }); - - it('should return false if "disabled" is undefined', function () { - const result = constraints.isDriveDisabled({ - device: '/dev/disk1', - size: 1000000000, - isReadOnly: false, - } as constraints.DrivelistDrive); - - expect(result).to.be.false; - }); - }); - describe('.isDriveSizeRecommended()', function () { const image: SourceMetadata = { description: 'rpi.img',