From 66f3e04d8ef6a485f7a0864218dca8a4e821d8f6 Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Sun, 5 Jun 2022 10:57:04 -0700 Subject: [PATCH] Preact mass storage (#125) The major impetus for rewriting in UI, at least. Still some ironing to do, but much nicer than my attempt to do this using the old UI "framework". --- .eslintrc.json | 3 +- css/green-off-16.png | Bin 0 -> 784 bytes css/green-off-32.png | Bin 0 -> 1985 bytes css/green-on-16.png | Bin 0 -> 919 bytes css/green-on-32.png | Bin 0 -> 2535 bytes css/red-off-16.png | Bin css/red-off-32.png | Bin 0 -> 1967 bytes css/red-on-16.png | Bin css/red-on-32.png | Bin 0 -> 2382 bytes js/cards/cffa.ts | 2 +- js/cards/smartport.ts | 55 ++++-- js/components/Apple2.tsx | 36 ++-- js/components/BlockDisk.tsx | 74 ++++++++ js/components/BlockFileModal.tsx | 77 ++++++++ js/components/DiskII.tsx | 29 +-- js/components/Drives.tsx | 119 +++++++++++-- js/components/FileModal.tsx | 4 +- js/components/ProgressModal.tsx | 29 +++ js/components/css/BlockDisk.module.css | 38 ++++ js/components/css/BlockFileModal.module.css | 3 + js/components/css/DiskII.module.css | 11 +- js/components/css/Drives.module.css | 10 ++ js/components/css/Inset.module.css | 1 - js/components/css/ProgressModal.module.css | 10 ++ js/components/util/files.ts | 186 +++++++++++++++----- js/components/util/keyboard.ts | 1 - js/components/util/promises.ts | 19 ++ js/formats/types.ts | 4 +- js/main2.ts | 2 +- js/main2e.ts | 2 +- js/ui/apple2.ts | 28 +-- js/ui/keyboard.ts | 2 +- 32 files changed, 612 insertions(+), 133 deletions(-) create mode 100644 css/green-off-16.png create mode 100644 css/green-off-32.png create mode 100644 css/green-on-16.png create mode 100644 css/green-on-32.png mode change 100755 => 100644 css/red-off-16.png create mode 100644 css/red-off-32.png mode change 100755 => 100644 css/red-on-16.png create mode 100644 css/red-on-32.png create mode 100644 js/components/BlockDisk.tsx create mode 100644 js/components/BlockFileModal.tsx create mode 100644 js/components/ProgressModal.tsx create mode 100644 js/components/css/BlockDisk.module.css create mode 100644 js/components/css/BlockFileModal.module.css create mode 100644 js/components/css/Drives.module.css create mode 100644 js/components/css/ProgressModal.module.css diff --git a/.eslintrc.json b/.eslintrc.json index a5e6161..dca0508 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,7 +31,6 @@ ], "no-var": "error", "no-use-before-define": "off", - "no-dupe-class-members": "off", "no-console": [ "error", { @@ -213,4 +212,4 @@ "version": "16" } } -} \ No newline at end of file +} diff --git a/css/green-off-16.png b/css/green-off-16.png new file mode 100644 index 0000000000000000000000000000000000000000..a237a208f7557ed0a153cac39f78354927ca568f GIT binary patch literal 784 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 literal 0 HcmV?d00001 diff --git a/css/red-off-16.png b/css/red-off-16.png old mode 100755 new mode 100644 diff --git a/css/red-off-32.png b/css/red-off-32.png new file mode 100644 index 0000000000000000000000000000000000000000..861d3b934d16d6c7b2e4868c2c26434dae364435 GIT binary patch literal 1967 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 AcK`qY literal 0 HcmV?d00001 diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index 058a9ca..7937a35 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -85,7 +85,7 @@ export interface CFFAState { disks: Array; } -export default class CFFA implements Card, MassStorage, Restorable { +export default class CFFA implements Card, MassStorage, Restorable { // CFFA internal Flags diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 8e3c4aa..7568e5b 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -1,12 +1,13 @@ import { debug, toHex } from '../util'; import { rom as smartPortRom } from '../roms/cards/smartport'; import { Card, Restorable, byte, word, rom } from '../types'; -import { MassStorage, BlockDisk, ENCODING_BLOCK } from '../formats/types'; +import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat } from '../formats/types'; import CPU6502, { CpuState, flags } from '../cpu6502'; import { read2MGHeader } from '../formats/2mg'; import createBlockDisk from '../formats/block'; import { ProDOSVolume } from '../formats/prodos'; import { dump } from '../formats/prodos/utils'; +import { DriveNumber } from '../formats/types'; const ID = 'SMARTPORT.J.S'; @@ -18,6 +19,12 @@ export interface SmartPortOptions { block: boolean; } +export interface Callbacks { + driveLight: (drive: DriveNumber, on: boolean) => void; + dirty: (drive: DriveNumber, dirty: boolean) => void; + label: (drive: DriveNumber, name?: string, side?: string) => void; +} + class Address { lo: byte; hi: byte; @@ -118,12 +125,18 @@ const DEVICE_TYPE_SCSI_HD = 0x07; // $0D: Printer // $0E: Clock // $0F: Modem -export default class SmartPort implements Card, MassStorage, Restorable { +export default class SmartPort implements Card, MassStorage, Restorable { private rom: rom; private disks: BlockDisk[] = []; + private busy: boolean[] = []; + private busyTimeout: ReturnType[] = []; - constructor(private cpu: CPU6502, options: SmartPortOptions) { + constructor( + private cpu: CPU6502, + private callbacks: Callbacks | null, + options: SmartPortOptions + ) { if (options?.block) { const dumbPortRom = new Uint8Array(smartPortRom); dumbPortRom[0x07] = 0x3C; @@ -139,11 +152,23 @@ export default class SmartPort implements Card, MassStorage, Restorable { + this.busy[drive] = false; + this.callbacks?.driveLight(drive, false); + }, 100); + } + /* * dumpBlock */ - dumpBlock(drive: number, block: number) { + dumpBlock(drive: DriveNumber, block: number) { let result = ''; let b; let jdx; @@ -178,7 +203,7 @@ export default class SmartPort implements Card, MassStorage, Restorable { - const { e, sectors } = props; + const { e, enhanced, sectors } = props; const screen = useRef(null); const [apple2, setApple2] = useState(); const [io, setIO] = useState(); const [cpu, setCPU] = useState(); const [error, setError] = useState(); + const drivesReady = useMemo(() => new Ready(setError), []); useEffect(() => { if (screen.current) { @@ -53,26 +55,30 @@ export const Apple2 = (props: Apple2Props) => { ...props, }; const apple2 = new Apple2Impl(options); - apple2.ready.then(() => { - setApple2(apple2); - const io = apple2.getIO(); - const cpu = apple2.getCPU(); - setIO(io); - setCPU(cpu); - apple2.reset(); - apple2.run(); - }).catch((e) => setError(e)); + noAwait((async () => { + try { + await apple2.ready; + setApple2(apple2); + setIO(apple2.getIO()); + setCPU(apple2.getCPU()); + await drivesReady.ready; + apple2.reset(); + apple2.run(); + } catch (e) { + setError(e); + } + }))(); } - }, [props]); + }, [props, drivesReady]); return (
+ - - + diff --git a/js/components/BlockDisk.tsx b/js/components/BlockDisk.tsx new file mode 100644 index 0000000..4b1f478 --- /dev/null +++ b/js/components/BlockDisk.tsx @@ -0,0 +1,74 @@ +import { h } from 'preact'; +import { useCallback, useState } from 'preact/hooks'; +import cs from 'classnames'; +import SmartPort from '../cards/smartport'; +import { BlockFileModal } from './BlockFileModal'; +import { ErrorModal } from './ErrorModal'; + +import styles from './css/BlockDisk.module.css'; + +/** + * Storage structure for drive state returned via callbacks. + */ +export interface BlockDiskData { + number: 1 | 2; + on: boolean; + name?: string; +} + +/** + * Interface for BlockDisk. + */ +export interface BlockDiskProps extends BlockDiskData { + smartPort: SmartPort | undefined; +} + +/** + * BlockDisk component + * + * Includes drive light, disk name and side, and UI for loading disks. + * + * @param smartPort SmartPort object + * @param number Drive 1 or 2 + * @param on Active state + * @param name Disk name identifier + * @param side Disk side identifier + * @returns BlockDisk component + */ +export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => { + const [modalOpen, setModalOpen] = useState(false); + const [error, setError] = useState(); + + const doClose = useCallback(() => { + setModalOpen(false); + }, []); + + const onOpenModal = useCallback(() => { + setModalOpen(true); + }, []); + + return ( +
+ + +
+ +
+ {name} +
+
+ ); +}; diff --git a/js/components/BlockFileModal.tsx b/js/components/BlockFileModal.tsx new file mode 100644 index 0000000..6b76bb0 --- /dev/null +++ b/js/components/BlockFileModal.tsx @@ -0,0 +1,77 @@ +import { h, Fragment } from 'preact'; +import { useCallback, useState } from 'preact/hooks'; +import { DriveNumber, BLOCK_FORMATS } from '../formats/types'; +import { ErrorModal } from './ErrorModal'; +import { FileChooser } from './FileChooser'; +import { Modal, ModalContent, ModalFooter } from './Modal'; +import { loadLocalBlockFile, getHashParts, setHashParts } from './util/files'; +import SmartPort from 'js/cards/smartport'; +import { useHash } from './hooks/useHash'; +import { noAwait } from './util/promises'; + +import styles from './css/BlockFileModal.module.css'; + +const DISK_TYPES: FilePickerAcceptType[] = [ + { + description: 'Disk Images', + accept: { 'application/octet-stream': BLOCK_FORMATS.map(x => '.' + x) }, + } +]; + +interface BlockFileModalProps { + isOpen: boolean; + smartPort: SmartPort | undefined; + number: DriveNumber; + onClose: (closeBox?: boolean) => void; +} + +export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFileModalProps) => { + const [handles, setHandles] = useState(); + const [busy, setBusy] = useState(false); + const [empty, setEmpty] = useState(true); + const [error, setError] = useState(); + const hash = useHash(); + + const doCancel = useCallback(() => onClose(true), [onClose]); + + const doOpen = useCallback(async () => { + const hashParts = getHashParts(hash); + + if (smartPort && handles?.length === 1) { + hashParts[number] = ''; + setBusy(true); + try { + await loadLocalBlockFile(smartPort, number, await handles[0].getFile()); + } catch (error) { + setError(error); + } finally { + setBusy(false); + onClose(); + } + } + + setHashParts(hashParts); + }, [handles, hash, smartPort, number, onClose]); + + const onChange = useCallback((handles: FileSystemFileHandle[]) => { + setEmpty(handles.length === 0); + setHandles(handles); + }, []); + + return ( + <> + + +
+ +
+
+ + + + +
+ + + ); +}; diff --git a/js/components/DiskII.tsx b/js/components/DiskII.tsx index fb97717..b1c5758 100644 --- a/js/components/DiskII.tsx +++ b/js/components/DiskII.tsx @@ -1,11 +1,9 @@ import { h } from 'preact'; -import { useCallback, useEffect, useState } from 'preact/hooks'; +import { useCallback, useState } from 'preact/hooks'; import cs from 'classnames'; import Disk2 from '../cards/disk2'; -import { FileModal } from './FileModal'; -import { loadJSON, loadHttpFile, getHashParts } from './util/files'; import { ErrorModal } from './ErrorModal'; -import { useHash } from './hooks/useHash'; +import { FileModal } from './FileModal'; import styles from './css/DiskII.module.css'; @@ -30,7 +28,6 @@ export interface DiskIIProps extends DiskIIData { * Disk II component * * Includes drive light, disk name and side, and UI for loading disks. - * Handles initial loading of disks specified in the hash. * * @param disk2 Disk2 object * @param number Drive 1 or 2 @@ -43,28 +40,6 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => { const label = side ? `${name} - ${side}` : name; const [modalOpen, setModalOpen] = useState(false); const [error, setError] = useState(); - const [currentHash, setCurrentHash] = useState(); - - const hash = useHash(); - - useEffect(() => { - const hashParts = getHashParts(hash); - const newHash = hashParts[number]; - if (disk2 && newHash) { - const hashPart = decodeURIComponent(newHash); - if (hashPart !== currentHash) { - if (hashPart.match(/^https?:/)) { - loadHttpFile(disk2, number, hashPart) - .catch((e) => setError(e)); - } else { - const filename = `/json/disks/${hashPart}.json`; - loadJSON(disk2, number, filename) - .catch((e) => setError(e)); - } - setCurrentHash(hashPart); - } - } - }, [currentHash, disk2, hash, number]); const doClose = useCallback(() => { setModalOpen(false); diff --git a/js/components/Drives.tsx b/js/components/Drives.tsx index 1952c15..22fd5c9 100644 --- a/js/components/Drives.tsx +++ b/js/components/Drives.tsx @@ -1,26 +1,53 @@ -import { h, Fragment } from 'preact'; -import {useEffect, useState } from 'preact/hooks'; +import { h } from 'preact'; +import { useCallback, useEffect, useState } from 'preact/hooks'; import Disk2, { Callbacks } from '../cards/disk2'; import Apple2IO from '../apple2io'; import { DiskII, DiskIIData } from './DiskII'; +import SmartPort from 'js/cards/smartport'; +import CPU6502 from 'js/cpu6502'; +import { BlockDisk } from './BlockDisk'; +import { ErrorModal } from './ErrorModal'; +import { ProgressModal } from './ProgressModal'; +import { loadHttpUnknownFile, getHashParts, loadJSON } from './util/files'; +import { useHash } from './hooks/useHash'; +import { DriveNumber } from 'js/formats/types'; +import { Ready } from './util/promises'; + +import styles from './css/Drives.module.css'; /** * Interface for Drives component. */ export interface DrivesProps { + cpu: CPU6502 | undefined; io: Apple2IO | undefined; + enhanced: boolean; sectors: number; + ready: Ready; } /** * Drive interface component. Presents the interface to load disks. - * Provides the callback to the Disk2 object to update the DiskII - * components. + * Provides the callback to the Disk2 and SmartPort objects to update + * the DiskII and BlockDisk components. + * Handles initial loading of disks specified in the hash. * + * @cpu CPU object * @param io Apple I/O object + * @param sectors 13 or 16 sector rom mode + * @enhanced Whether to create a SmartPort ROM device + * @ready Signal disk availability * @returns Drives component */ -export const Drives = ({ io, sectors }: DrivesProps) => { +export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => { + const [current, setCurrent] = useState(0); + const [error, setError] = useState(); + const [total, setTotal] = useState(0); + const onProgress = useCallback((current: number, total: number) => { + setCurrent(current); + setTotal(total); + }, []); + const [disk2, setDisk2] = useState(); const [data1, setData1] = useState({ on: false, @@ -32,9 +59,54 @@ export const Drives = ({ io, sectors }: DrivesProps) => { number: 2, name: 'Disk 2', }); + const [smartData1, setSmartData1] = useState({ + on: false, + number: 1, + name: 'HD 1' + }); + const [smartData2, setSmartData2] = useState({ + on: false, + number: 2, + name: 'HD 2' + }); + + const [smartPort, setSmartPort] = useState(); + + const hash = useHash(); + + useEffect(() => { + if (smartPort && disk2) { + const hashParts = getHashParts(hash); + let loading = 0; + for (const drive of [1, 2] as DriveNumber[]) { + if (hashParts && hashParts[drive]) { + const hashPart = decodeURIComponent(hashParts[drive]); + if (hashPart.match(/^https?:/)) { + loading++; + loadHttpUnknownFile(disk2, smartPort, drive, hashPart, onProgress) + .catch((e) => setError(e)) + .finally(() => { + if (--loading === 0) { + ready.onReady(); + } + setCurrent(0); + setTotal(0); + }); + } else { + const url = `/json/disks/${hashPart}.json`; + loadJSON(disk2, drive, url).catch((e) => setError(e)); + } + } + } + if (!loading) { + ready.onReady(); + } + } + }, [hash, onProgress, disk2, ready, smartPort]); useEffect(() => { const setData = [setData1, setData2]; + const setSmartData = [setSmartData1, setSmartData2]; const callbacks: Callbacks = { driveLight: (drive, on) => { setData[drive - 1]?.(data => ({...data, on })); @@ -51,17 +123,42 @@ export const Drives = ({ io, sectors }: DrivesProps) => { } }; - if (io) { + const smartPortCallbacks: Callbacks = { + driveLight: (drive, on) => { + setSmartData[drive - 1]?.(data => ({...data, on })); + }, + label: (drive, name, side) => { + setSmartData[drive - 1]?.(data => ({ + ...data, + name: name ?? `HD ${drive}`, + side, + })); + }, + dirty: () => {/* Unused */} + }; + + if (cpu && io) { const disk2 = new Disk2(io, callbacks, sectors); io.setSlot(6, disk2); setDisk2(disk2); + const smartPort = new SmartPort(cpu, smartPortCallbacks, { block: !enhanced }); + io.setSlot(7, smartPort); + setSmartPort(smartPort); } - }, [io, sectors]); + }, [cpu, enhanced, io, sectors]); return ( - <> - - - +
+ + +
+ + +
+
+ + +
+
); }; diff --git a/js/components/FileModal.tsx b/js/components/FileModal.tsx index 4ba5594..d7c95b8 100644 --- a/js/components/FileModal.tsx +++ b/js/components/FileModal.tsx @@ -2,7 +2,7 @@ import { h, Fragment, 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 { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files'; import DiskII from '../cards/disk2'; import { ErrorModal } from './ErrorModal'; @@ -66,7 +66,7 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => try { if (disk2 && handles?.length === 1) { hashParts[number] = ''; - await loadLocalFile(disk2, number, await handles[0].getFile()); + await loadLocalNibbleFile(disk2, number, await handles[0].getFile()); } if (disk2 && filename) { const name = filename.match(/\/([^/]+).json$/) || ['', '']; diff --git a/js/components/ProgressModal.tsx b/js/components/ProgressModal.tsx new file mode 100644 index 0000000..d16629a --- /dev/null +++ b/js/components/ProgressModal.tsx @@ -0,0 +1,29 @@ +import { h } from 'preact'; +import { Modal, ModalContent } from './Modal'; + +import styles from './css/ProgressModal.module.css'; + +export interface ErrorProps { + title: string; + current: number | undefined; + total: number | undefined; +} + +export const ProgressModal = ({ title, current, total } : ErrorProps) => { + if (current && total) { + return ( + + +
+
+
+ + + ); + } else { + return null; + } +}; diff --git a/js/components/css/BlockDisk.module.css b/js/components/css/BlockDisk.module.css new file mode 100644 index 0000000..c91527d --- /dev/null +++ b/js/components/css/BlockDisk.module.css @@ -0,0 +1,38 @@ +.disk { + align-items: center; + display: flex; + flex-grow: 1; +} + +.diskLight { + margin: 5px; + background-image: url(../../../css/green-off-16.png); + background-size: 16px 16px; + flex-shrink: 0; + width: 16px; + height: 16px; +} + +.diskLight.on { + background-image: url(../../../css/green-on-16.png); +} + +.diskLabel { + color: #000; + font-family: sans-serif; + font-weight: bold; + margin-right: 0.5em; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex-grow: 1; +} + +@media only screen and (min-resolution: 1.25dppx) { + .diskLight { + background-image: url(../../../css/green-off-32.png); + } + .diskLight.on { + background-image: url(../../../css/green-on-32.png); + } +} diff --git a/js/components/css/BlockFileModal.module.css b/js/components/css/BlockFileModal.module.css new file mode 100644 index 0000000..f238b28 --- /dev/null +++ b/js/components/css/BlockFileModal.module.css @@ -0,0 +1,3 @@ +.modalContent { + width: 320px; +} diff --git a/js/components/css/DiskII.module.css b/js/components/css/DiskII.module.css index f10501e..0e3ac2b 100644 --- a/js/components/css/DiskII.module.css +++ b/js/components/css/DiskII.module.css @@ -2,12 +2,12 @@ align-items: center; display: flex; flex-grow: 1; - max-width: 50%; } .diskLight { margin: 5px; background-image: url(../../../css/red-off-16.png); + background-size: 16px 16px; flex-shrink: 0; width: 16px; height: 16px; @@ -27,3 +27,12 @@ white-space: nowrap; flex-grow: 1; } + +@media only screen and (min-resolution: 1.25dppx) { + .diskLight { + background-image: url(../../../css/red-off-32.png); + } + .diskLight.on { + background-image: url(../../../css/red-on-32.png); + } +} diff --git a/js/components/css/Drives.module.css b/js/components/css/Drives.module.css new file mode 100644 index 0000000..37ab8d8 --- /dev/null +++ b/js/components/css/Drives.module.css @@ -0,0 +1,10 @@ +.drives { + display: flex; + width: 100%; +} + +.driveBay { + display: flex; + flex-direction: column; + flex: 1 1 50%; +} diff --git a/js/components/css/Inset.module.css b/js/components/css/Inset.module.css index 2b0c332..1adea2f 100644 --- a/js/components/css/Inset.module.css +++ b/js/components/css/Inset.module.css @@ -11,7 +11,6 @@ display: none; } - .inset button { min-width: 36px; margin: 0 2px; diff --git a/js/components/css/ProgressModal.module.css b/js/components/css/ProgressModal.module.css new file mode 100644 index 0000000..f95a9c2 --- /dev/null +++ b/js/components/css/ProgressModal.module.css @@ -0,0 +1,10 @@ +.progressContainer { + width: 320px; + height: 20px; + background: #000 +} + +.progressBar { + height: 20px; + background: #0f0; +} diff --git a/js/components/util/files.ts b/js/components/util/files.ts index 6d5f52e..a23fee6 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -1,11 +1,19 @@ import { includes } from 'js/types'; import { initGamepad } from 'js/ui/gamepad'; import { + BlockFormat, + BLOCK_FORMATS, + DISK_FORMATS, DriveNumber, JSONDisk, - NIBBLE_FORMATS + MassStorage, + NibbleFormat, + NIBBLE_FORMATS, } from 'js/formats/types'; -import DiskII from 'js/cards/disk2'; +import Disk2 from 'js/cards/disk2'; +import SmartPort from 'js/cards/smartport'; + +type ProgressCallback = (current: number, total: number) => void; /** * Routine to split a legacy hash into parts for disk loading @@ -26,17 +34,19 @@ export const setHashParts = (parts: string[]) => { window.location.hash = `#${parts[1]}` + (parts[2] ? `|${parts[2]}` : ''); }; -/** - * Local file loading routine. Allows a File object from a file - * selection form element to be loaded. - * - * @param disk2 Disk2 object - * @param number Drive number - * @param file Browser File object to load - * @returns true if successful - */ +export const getNameAndExtension = (url: string) => { + const urlParts = url.split('/'); + const file = urlParts.pop() || url; + const fileParts = file.split('.'); + const ext = fileParts.pop()?.toLowerCase() || '[none]'; + const name = decodeURIComponent(fileParts.join('.')); + + return { name, ext }; +}; + export const loadLocalFile = ( - disk2: DiskII, + storage: MassStorage, + formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS, number: DriveNumber, file: File, ) => { @@ -48,16 +58,12 @@ export const loadLocalFile = ( const ext = parts.pop()?.toLowerCase() || '[none]'; const name = parts.join('.'); - if (includes(NIBBLE_FORMATS, ext)) { - if (result.byteLength >= 800 * 1024) { - reject(`Unable to load ${name}`); + if (includes(formats, ext)) { + initGamepad(); + if (storage.setBinary(number, name, ext, result)) { + resolve(true); } else { - initGamepad(); - if (disk2.setBinary(number, name, ext, result)) { - resolve(true); - } else { - reject(`Unable to load ${name}`); - } + reject(`Unable to load ${name}`); } } else { reject(`Extension "${ext}" not recognized.`); @@ -67,6 +73,32 @@ export const loadLocalFile = ( }); }; +/** + * Local file loading routine. Allows a File object from a file + * selection form element to be loaded. + * + * @param smartPort SmartPort object + * @param number Drive number + * @param file Browser File object to load + * @returns true if successful + */ +export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, file: File) => { + return loadLocalFile(smartPort, BLOCK_FORMATS, number, file); +}; + +/** + * Local file loading routine. Allows a File object from a file + * selection form element to be loaded. + * + * @param disk2 Disk2 object + * @param number Drive number + * @param file Browser File object to load + * @returns true if successful + */ +export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => { + return loadLocalFile(disk2, NIBBLE_FORMATS, number, file); +}; + /** * JSON loading routine, loads a JSON file at the given URL. Requires * proper cross domain loading headers if the URL is not on the same server @@ -77,37 +109,27 @@ export const loadLocalFile = ( * @param url URL, relative or absolute to JSON file * @returns true if successful */ -export const loadJSON = async (disk2: DiskII, number: DriveNumber, url: string) => { +export const loadJSON = async ( + disk2: Disk2, + number: DriveNumber, + url: string, +) => { const response = await fetch(url); if (!response.ok) { throw new Error(`Error loading: ${response.statusText}`); } const data = await response.json() as JSONDisk; if (!includes(NIBBLE_FORMATS, data.type)) { - throw new Error(`Type ${data.type} not recognized.`); + throw new Error(`Type "${data.type}" not recognized.`); } disk2.setDisk(number, data); initGamepad(data.gamepad); }; -/** - * HTTP loading routine, loads a file at the given URL. Requires - * proper cross domain loading headers if the URL is not on the same server - * as the emulator. Only supports nibble based formats at the moment. - * - * @param disk2 Disk2 object - * @param number Drive number - * @param url URL, relative or absolute to JSON file - * @returns true if successful - */ export const loadHttpFile = async ( - disk2: DiskII, - number: DriveNumber, url: string, -) => { - if (url.endsWith('.json')) { - return loadJSON(disk2, number, url); - } + onProgress?: ProgressCallback +): Promise => { const response = await fetch(url); if (!response.ok) { throw new Error(`Error loading: ${response.statusText}`); @@ -116,13 +138,16 @@ export const loadHttpFile = async ( throw new Error('Error loading: no body'); } const reader = response.body.getReader(); + const contentLength = parseInt(response.headers.get('content-length') || '0', 10); let received = 0; const chunks: Uint8Array[] = []; let result = await reader.read(); + onProgress?.(1, contentLength); while (!result.done) { chunks.push(result.value); received += result.value.length; + onProgress?.(received, contentLength); result = await reader.read(); } @@ -133,14 +158,87 @@ export const loadHttpFile = async ( offset += chunk.length; } - const urlParts = url.split('/'); - const file = urlParts.pop() || url; - const fileParts = file.split('.'); - const ext = fileParts.pop()?.toLowerCase() || '[none]'; - const name = decodeURIComponent(fileParts.join('.')); + return data.buffer; +}; + +/** + * HTTP loading routine, loads a file at the given URL. Requires + * proper cross domain loading headers if the URL is not on the same server + * as the emulator. + * + * @param smartPort SmartPort object + * @param number Drive number + * @param url URL, relative or absolute to JSON file + * @returns true if successful + */ +export const loadHttpBlockFile = async ( + smartPort: SmartPort, + number: DriveNumber, + url: string, + onProgress?: ProgressCallback +): Promise => { + const { name, ext } = getNameAndExtension(url); + if (!includes(BLOCK_FORMATS, ext)) { + throw new Error(`Extension "${ext}" not recognized.`); + } + const data = await loadHttpFile(url, onProgress); + smartPort.setBinary(number, name, ext, data); + initGamepad(); + + return true; +}; + +/** + * HTTP loading routine, loads a file at the given URL. Requires + * proper cross domain loading headers if the URL is not on the same server + * as the emulator. + * + * @param disk2 Disk2 object + * @param number Drive number + * @param url URL, relative or absolute to JSON file + * @returns true if successful + */ +export const loadHttpNibbleFile = async ( + disk2: Disk2, + number: DriveNumber, + url: string, + onProgress?: ProgressCallback +) => { + if (url.endsWith('.json')) { + return loadJSON(disk2, number, url); + } + const { name, ext } = getNameAndExtension(url); if (!includes(NIBBLE_FORMATS, ext)) { throw new Error(`Extension "${ext}" not recognized.`); } + const data = await loadHttpFile(url, onProgress); disk2.setBinary(number, name, ext, data); initGamepad(); + return loadHttpFile(url, onProgress); +}; + +export const loadHttpUnknownFile = async ( + disk2: Disk2, + smartPort: SmartPort, + number: DriveNumber, + url: string, + onProgress?: ProgressCallback, +) => { + const data = await loadHttpFile(url, onProgress); + const { name, ext } = getNameAndExtension(url); + if (includes(DISK_FORMATS, ext)) { + if (data.byteLength >= 800 * 1024) { + if (includes(BLOCK_FORMATS, ext)) { + smartPort.setBinary(number, name, ext, data); + } else { + throw new Error(`Unable to load "${name}"`); + } + } else if (includes(NIBBLE_FORMATS, ext)) { + disk2.setBinary(number, name, ext, data); + } else { + throw new Error(`Unable to load "${name}"`); + } + } else { + throw new Error(`Extension "${ext}" not recognized.`); + } }; diff --git a/js/components/util/keyboard.ts b/js/components/util/keyboard.ts index 13f374a..48d75cc 100644 --- a/js/components/util/keyboard.ts +++ b/js/components/util/keyboard.ts @@ -161,7 +161,6 @@ const uiKitMap = { 'UIKeyInputEscape': 0x1B } as const; - export const isUiKitKey = (k: string): k is KnownKeys => { return k in uiKitMap; }; diff --git a/js/components/util/promises.ts b/js/components/util/promises.ts index d60a46a..ee8e872 100644 --- a/js/components/util/promises.ts +++ b/js/components/util/promises.ts @@ -10,3 +10,22 @@ export type NoAwait Promise> = export function noAwait Promise>(f: F): NoAwait { return f as NoAwait; } + +/** + * Utility class that allows a promise to be passed to a + * service to be resolved. + */ + +export class Ready { + onError: (value?: unknown) => void; + onReady: (value?: unknown) => void; + + ready: Promise; + + constructor(private errorHandler = console.error) { + this.ready = new Promise((resolve, reject) => { + this.onReady = resolve; + this.onError = reject; + }).catch(this.errorHandler); + } +} diff --git a/js/formats/types.ts b/js/formats/types.ts index 104106f..33bca9f 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -215,6 +215,6 @@ export type FormatWorkerResponse = /** * Block device common interface */ -export interface MassStorage { - setBinary(drive: number, name: string, ext: BlockFormat, data: ArrayBuffer): boolean; +export interface MassStorage { + setBinary(drive: number, name: string, ext: T, data: ArrayBuffer): boolean; } diff --git a/js/main2.ts b/js/main2.ts index 5aeddd6..9eaba1f 100644 --- a/js/main2.ts +++ b/js/main2.ts @@ -74,7 +74,7 @@ apple2.ready.then(() => { const slinky = new RAMFactor(1024 * 1024); const disk2 = new DiskII(io, driveLights, sectors); const clock = new Thunderclock(); - const smartport = new SmartPort(cpu, { block: true }); + const smartport = new SmartPort(cpu, null, { block: true }); io.setSlot(0, lc); io.setSlot(1, parallel); diff --git a/js/main2e.ts b/js/main2e.ts index 5fd4b93..2280d58 100644 --- a/js/main2e.ts +++ b/js/main2e.ts @@ -63,7 +63,7 @@ apple2.ready.then(() => { const slinky = new RAMFactor(1024 * 1024); const disk2 = new DiskII(io, driveLights); const clock = new Thunderclock(); - const smartport = new SmartPort(cpu, { block: !enhanced }); + const smartport = new SmartPort(cpu, null, { block: !enhanced }); const mouse = new Mouse(cpu, mouseUI); io.setSlot(1, parallel); diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 91050ef..99102d3 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -13,7 +13,8 @@ import { MassStorage, NIBBLE_FORMATS, JSONBinaryImage, - JSONDisk + JSONDisk, + BlockFormat } from '../formats/types'; import { initGamepad } from './gamepad'; import KeyBoard from './keyboard'; @@ -74,7 +75,7 @@ let stats: Stats; let vm: VideoModes; let tape: Tape; let _disk2: DiskII; -let _massStorage: MassStorage; +let _massStorage: MassStorage; let _printer: Printer; let audio: Audio; let screen: Screen; @@ -270,8 +271,8 @@ export function doLoad(event: MouseEvent|KeyboardEvent) { } else if (url) { let filename; MicroModal.close('load-modal'); - if (url.substr(0, 6) === 'local:') { - filename = url.substr(6); + if (url.slice(0, 6) === 'local:') { + filename = url.slice(6); if (filename === '__manage') { openManage(); } else { @@ -450,10 +451,8 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { const name = decodeURIComponent(fileParts.join('.')); if (includes(DISK_FORMATS, ext)) { if (data.byteLength >= 800 * 1024) { - if ( - includes(BLOCK_FORMATS, ext) && - _massStorage.setBinary(drive, name, ext, data) - ) { + if (includes(BLOCK_FORMATS, ext)) { + _massStorage.setBinary(drive, name, ext, data); initGamepad(); } } else { @@ -842,7 +841,12 @@ function hup() { return results[1]; } -function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) { +function onLoaded( + apple2: Apple2, + disk2: DiskII, + massStorage: MassStorage, + printer: Printer, e: boolean +) { _apple2 = apple2; cpu = _apple2.getCPU(); io = _apple2.getIO(); @@ -950,7 +954,11 @@ function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, print ); } -export function initUI(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) { +export function initUI( + apple2: Apple2, + disk2: DiskII, + massStorage: MassStorage, + printer: Printer, e: boolean) { window.addEventListener('load', () => { onLoaded(apple2, disk2, massStorage, printer, e); }); diff --git a/js/ui/keyboard.ts b/js/ui/keyboard.ts index efd1010..b715060 100644 --- a/js/ui/keyboard.ts +++ b/js/ui/keyboard.ts @@ -354,7 +354,7 @@ export default class KeyBoard { const buildLabel = (k: string) => { const span = document.createElement('span'); span.innerHTML = k; - if (k.length > 1 && k.substr(0, 1) !== '&') + if (k.length > 1 && k.slice(0, 1) !== '&') span.classList.add('small'); return span; };