apple2js/js/components/FileModal.tsx

159 lines
5.2 KiB
TypeScript

import { h, Fragment, JSX } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
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';
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',
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;
}
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>();
const [index, setIndex] = useState<IndexEntry[]>();
const hash = useHash();
useEffect(() => {
spawn(async () => {
try {
const indexResponse = await fetch('json/disks/index.json');
const index = await indexResponse.json() as IndexEntry[];
setIndex(index);
} catch (error) {
setIndex([]);
setError(error);
}
});
}, []);
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);
}, []
);
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();
const disks = category ? categories[category].sort() : [];
return (
<>
<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} />
</>
);
};