apple2js/js/components/FileModal.tsx

159 lines
5.2 KiB
TypeScript
Raw Normal View History

import { h, Fragment, JSX } from 'preact';
2022-07-24 20:53:06 +00:00
import { useCallback, useEffect, useState } from 'preact/hooks';
Split disk data out into its own record (#158) * Harmonize drive and disk type hierarchies Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar, but not exactly the same. For example, `encoding` and `format` were missing on some `XXXDisk` types where they existed on the `XXXDrive` type. This change attempts to bring the hierarchies closer together. However, the biggest visible consequence is the introduction of the `FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This replaces `NIBBLE_FORMATS` in most places. A couple of new type guards for disk formats and disks have been added as well. All tests pass, everything compiles with no errors, and both WOZ and nibble format disks load in the emulator. * Move disk data to a `disk` field in the drive Before, disk data was mixed in with state about the drive itself (like track, motor phase, etc.). This made it hard to know exactly what data was necessary for different image formats. Now, the disk data is in a `disk` field whose type depends on the drive type. This makes responisbility a bit easier. One oddity, though, is that the `Drive` has metadata _and_ the `Disk` has metadata. When a disk is in the drive, these should be `===`, but when there is no disk in the drive, obviously only the drive metadata is set. All tests pass, everything compiles, and both WOZ and nibble disks work in the emulator (both preact and classic). * Squash the `Drive` type hierarchy Before, the type of the drive depended on the type of the disk in the drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive` contained a `WozDisk`. With the extraction of the disk data to a single field, this type hierarchy makes no sense. Instead, it suffices to check the type of the disk. This change removes the `NibbleDrive` and `WozDrive` types and type guards, checking the disk type where necessary. This change also introduces the `NoFloppyDisk` type to represent the lack of a disk. This allows the drive to have metadata, for one. All tests pass, everything compiles, and both WOZ and nibble disks work locally. * Use more destructuring assignment Now, more places use constructs like: ```TypeScript const { metadata, readOnly, track, head, phase, dirty } = drive; return { disk: getDiskState(drive.disk), metadata: {...metadata}, readOnly, track, head, phase, dirty, }; ``` * Remove the `Disk` object from the `Drive` object This change splits out the disk objects into a record parallel to the drive objects. The idea is that the `Drive` structure becomes a representation of the state of the drive that is separate from the disk image actually in the drive. This helps in an upcoming refactoring. This also changes the default empty disks to be writable. While odd, the write protect switch should be in the "off" position since there is no disk pressing on it. Finally, `insertDisk` now resets the head position to 0 since there is no way of preserving the head position across disks. (Even in the real world, the motor-off delay plus spindle spin-down would make it impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
import { DiskDescriptor, DriveNumber, FLOPPY_FORMATS, NibbleFormat } from '../formats/types';
import { Modal, ModalContent, ModalFooter } from './Modal';
import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files';
import DiskII from '../cards/disk2';
import { ErrorModal } from './ErrorModal';
2022-07-24 20:53:06 +00:00
import { noAwait, spawn } from './util/promises';
import { useHash } from './hooks/useHash';
import { FileChooser, FilePickerAcceptType } from './FileChooser';
import styles from './css/FileModal.module.scss';
const DISK_TYPES: FilePickerAcceptType[] = [
{
description: 'Disk Images',
Split disk data out into its own record (#158) * Harmonize drive and disk type hierarchies Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar, but not exactly the same. For example, `encoding` and `format` were missing on some `XXXDisk` types where they existed on the `XXXDrive` type. This change attempts to bring the hierarchies closer together. However, the biggest visible consequence is the introduction of the `FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This replaces `NIBBLE_FORMATS` in most places. A couple of new type guards for disk formats and disks have been added as well. All tests pass, everything compiles with no errors, and both WOZ and nibble format disks load in the emulator. * Move disk data to a `disk` field in the drive Before, disk data was mixed in with state about the drive itself (like track, motor phase, etc.). This made it hard to know exactly what data was necessary for different image formats. Now, the disk data is in a `disk` field whose type depends on the drive type. This makes responisbility a bit easier. One oddity, though, is that the `Drive` has metadata _and_ the `Disk` has metadata. When a disk is in the drive, these should be `===`, but when there is no disk in the drive, obviously only the drive metadata is set. All tests pass, everything compiles, and both WOZ and nibble disks work in the emulator (both preact and classic). * Squash the `Drive` type hierarchy Before, the type of the drive depended on the type of the disk in the drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive` contained a `WozDisk`. With the extraction of the disk data to a single field, this type hierarchy makes no sense. Instead, it suffices to check the type of the disk. This change removes the `NibbleDrive` and `WozDrive` types and type guards, checking the disk type where necessary. This change also introduces the `NoFloppyDisk` type to represent the lack of a disk. This allows the drive to have metadata, for one. All tests pass, everything compiles, and both WOZ and nibble disks work locally. * Use more destructuring assignment Now, more places use constructs like: ```TypeScript const { metadata, readOnly, track, head, phase, dirty } = drive; return { disk: getDiskState(drive.disk), metadata: {...metadata}, readOnly, track, head, phase, dirty, }; ``` * Remove the `Disk` object from the `Drive` object This change splits out the disk objects into a record parallel to the drive objects. The idea is that the `Drive` structure becomes a representation of the state of the drive that is separate from the disk image actually in the drive. This helps in an upcoming refactoring. This also changes the default empty disks to be writable. While odd, the write protect switch should be in the "off" position since there is no disk pressing on it. Finally, `insertDisk` now resets the head position to 0 since there is no way of preserving the head position across disks. (Even in the real world, the motor-off delay plus spindle spin-down would make it impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
accept: { 'application/octet-stream': FLOPPY_FORMATS.map(x => '.' + x) },
}
];
export type NibbleFileCallback = (
name: string,
fmt: NibbleFormat,
rawData: ArrayBuffer
) => boolean;
interface FileModalProps {
isOpen: boolean;
disk2: DiskII;
driveNo: DriveNumber;
onClose: (closeBox?: boolean) => void;
}
2022-07-24 20:53:06 +00:00
interface IndexEntry {
filename: string;
name: string;
category: string;
}
export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) => {
const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true);
const [category, setCategory] = useState<string>();
const [handles, setHandles] = useState<FileSystemFileHandle[]>();
const [filename, setFilename] = useState<string>();
const [error, setError] = useState<unknown>();
2022-07-24 20:53:06 +00:00
const [index, setIndex] = useState<IndexEntry[]>();
const hash = useHash();
2022-07-24 20:53:06 +00:00
useEffect(() => {
spawn(async () => {
2022-07-26 02:27:55 +00:00
try {
2022-07-27 01:05:33 +00:00
const indexResponse = await fetch('json/disks/index.json');
2022-07-26 02:27:55 +00:00
const index = await indexResponse.json() as IndexEntry[];
setIndex(index);
} catch (error) {
setIndex([]);
setError(error);
}
2022-07-24 20:53:06 +00:00
});
}, []);
const doCancel = useCallback(() => onClose(true), [onClose]);
const doOpen = useCallback(async () => {
const hashParts = getHashParts(hash);
setBusy(true);
try {
if (handles?.length === 1) {
hashParts[driveNo] = '';
await loadLocalNibbleFile(disk2, driveNo, await handles[0].getFile());
}
if (filename) {
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
hashParts[driveNo] = name[1];
await loadJSON(disk2, driveNo, filename);
}
} catch (e) {
setError(e);
} finally {
setHashParts(hashParts);
setBusy(false);
onClose();
}
setHashParts(hashParts);
}, [disk2, filename, driveNo, onClose, handles, hash]);
const onChange = useCallback((handles: FileSystemFileHandle[]) => {
setEmpty(handles.length === 0);
setHandles(handles);
}, []);
const doSelectCategory = useCallback(
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) =>
setCategory(event.currentTarget.value)
, []
);
const doSelectFilename = useCallback(
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) => {
setEmpty(!event.currentTarget.value);
setFilename(event.currentTarget.value);
}, []
);
2022-07-24 20:53:06 +00:00
if (!index) {
return null;
}
const categories = index.reduce<Record<string, DiskDescriptor[]>>(
(
acc: Record<string, DiskDescriptor[]>,
disk: DiskDescriptor
) => {
const category = disk.category || 'Misc';
acc[category] = [disk, ...(acc[category] || [])];
return acc;
},
{}
);
const categoryNames = Object.keys(categories).sort();
2022-07-27 01:05:33 +00:00
const disks = category ? categories[category].sort() : [];
return (
<>
2022-06-04 21:31:57 +00:00
<Modal title="Open File" isOpen={isOpen} onClose={onClose}>
<ModalContent>
<div className={styles.loadModal}>
<select multiple onChange={doSelectCategory}>
{categoryNames.map((name) => (
<option key={name}>{name}</option>
))}
</select>
<select multiple onChange={doSelectFilename}>
{disks.map((disk) => (
<option key={disk.filename} value={disk.filename}>
{disk.name}
{disk.disk ? ` - ${disk.disk}` : ''}
</option>
))}
</select>
</div>
<FileChooser onChange={onChange} accept={DISK_TYPES} />
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
</ModalFooter>
</Modal>
<ErrorModal error={error} setError={setError} />
</>
);
};