mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
52a1c65fe4
* Create a FileChooser component using showOpenFilePicker Before, `FileModal` always used a file input control for selecting local files. This allowed the emulator to read from the file, but precluded writing back to the file. With this change, the `FileModal` delegates to the new `FileChooser` component. The `FileChooser` will use `showOpenFilePicker` if it is available and a regular file input if it's not. Using `showOpenFilePicker` has the advantage of allowing the emulator to write back to the file (if the user grants permission). While the emulator does not yet take advantage of this write capability, that will come. * Addressed comments * useState() instead of direct DOM manipulation * backed out eslint changes in favor of suppressing the warning
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { h, JSX } from 'preact';
|
|
import { useCallback, useState } from 'preact/hooks';
|
|
import { DiskDescriptor, DriveNumber, NibbleFormat, NIBBLE_FORMATS } from '../formats/types';
|
|
import { Modal, ModalContent, ModalFooter } from './Modal';
|
|
import { loadLocalFile, loadJSON, getHashParts, setHashParts } from './util/files';
|
|
import DiskII from '../cards/disk2';
|
|
|
|
import index from 'json/disks/index.json';
|
|
import { FileChooser, FilePickerAcceptType, FileSystemFileHandleLike } from './FileChooser';
|
|
|
|
const DISK_TYPES: FilePickerAcceptType[] = [
|
|
{
|
|
description: 'Disk Images',
|
|
accept: { 'application/octet-stream': NIBBLE_FORMATS.map(x => '.' + x) },
|
|
}
|
|
];
|
|
|
|
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();
|
|
|
|
export type NibbleFileCallback = (
|
|
name: string,
|
|
fmt: NibbleFormat,
|
|
rawData: ArrayBuffer
|
|
) => boolean;
|
|
|
|
interface FileModalProps {
|
|
isOpen: boolean;
|
|
disk2: DiskII | undefined;
|
|
number: DriveNumber;
|
|
onClose: (closeBox?: boolean) => void;
|
|
}
|
|
|
|
export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => {
|
|
const [busy, setBusy] = useState<boolean>(false);
|
|
const [empty, setEmpty] = useState<boolean>(true);
|
|
const [category, setCategory] = useState<string>();
|
|
const [handles, setHandles] = useState<FileSystemFileHandleLike[]>();
|
|
const [filename, setFilename] = useState<string>();
|
|
|
|
const doCancel = useCallback(() => onClose(true), []);
|
|
|
|
const doOpen = useCallback(async () => {
|
|
const hashParts = getHashParts();
|
|
|
|
if (disk2 && handles && handles.length === 1) {
|
|
hashParts[number] = '';
|
|
setBusy(true);
|
|
try {
|
|
await loadLocalFile(disk2, number, await handles[0].getFile());
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
setBusy(false);
|
|
onClose();
|
|
}
|
|
}
|
|
|
|
if (disk2 && filename) {
|
|
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
|
|
hashParts[number] = name[1];
|
|
setBusy(true);
|
|
loadJSON(disk2, number, filename)
|
|
.catch(console.error)
|
|
.finally(() => {
|
|
setBusy(false);
|
|
onClose();
|
|
});
|
|
}
|
|
|
|
setHashParts(hashParts);
|
|
}, [disk2, filename, number, onClose, handles]);
|
|
|
|
const onChange = useCallback((handles: FileSystemFileHandleLike[]) => {
|
|
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);
|
|
}, []
|
|
);
|
|
|
|
const disks = category ? categories[category] : [];
|
|
|
|
return (
|
|
<Modal title="Open File" isOpen={isOpen}>
|
|
<ModalContent>
|
|
<div id="load-modal">
|
|
<select multiple onChange={doSelectCategory}>
|
|
{categoryNames.map((name) => (
|
|
<option>{name}</option>
|
|
))}
|
|
</select>
|
|
<select multiple onChange={doSelectFilename}>
|
|
{disks.map((disk) => (
|
|
<option 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={doOpen} disabled={busy || empty}>Open</button>
|
|
</ModalFooter>
|
|
</Modal>
|
|
);
|
|
};
|