This commit is contained in:
Will Scullin 2022-05-28 09:32:01 -07:00
parent 1de57ab75a
commit d4b20c32c2
No known key found for this signature in database
GPG Key ID: 26DCD1042C6638CD
9 changed files with 108 additions and 37 deletions

View File

@ -5,6 +5,7 @@ import { Apple2 as Apple2Impl } from '../apple2';
import Apple2IO from '../apple2io';
import CPU6502 from '../cpu6502';
import { ControlStrip } from './ControlStrip';
import { ErrorModal } from './ErrorModal';
import { Inset } from './Inset';
import { Keyboard } from './Keyboard';
import { Mouse } from './Mouse';
@ -12,7 +13,6 @@ import { Screen } from './Screen';
import { Drives } from './Drives';
import { Slinky } from './Slinky';
import { ThunderClock } from './ThunderClock';
import { ErrorModal } from './ErrorModal';
import styles from './css/Apple2.module.css';

View File

@ -3,7 +3,12 @@ import { useCallback, useEffect, useState } from 'preact/hooks';
import cs from 'classnames';
import SmartPort from '../cards/smartport';
import { BlockFileModal } from './BlockFileModal';
import { ErrorModal } from './ErrorModal';
import { ProgressModal } from './ProgressModal';
import { loadHttpBlockFile, getHashParts } from './util/files';
import { useHash } from './hooks/useHash';
import { includes } from 'js/types';
import { BLOCK_FORMATS } from 'js/formats/types';
/**
* Storage structure for Disk II state returned via callbacks.
@ -36,19 +41,34 @@ export interface BlockDiskProps extends DiskIIData {
*/
export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
const [modalOpen, setModalOpen] = useState(false);
const [current, setCurrent] = useState(0);
const [error, setError] = useState<unknown>();
const [total, setTotal] = useState(0);
const onProgress = useCallback((current: number, total: number) => {
setCurrent(current);
setTotal(total);
}, []);
const hash = useHash();
useEffect(() => {
const hashParts = getHashParts();
const hashParts = getHashParts(hash);
if (smartPort && hashParts && hashParts[number]) {
const hashPart = decodeURIComponent(hashParts[number]);
if (hashPart.match(/^https?:/)) {
loadHttpBlockFile(smartPort, number, hashPart)
.catch((error) =>
console.error(error)
);
const fileParts = hashPart.split('.');
const ext = fileParts[fileParts.length - 1];
if (includes(BLOCK_FORMATS, ext)) {
loadHttpBlockFile(smartPort, number, hashPart, onProgress)
.catch((e) => setError(e))
.finally(() => {
setCurrent(0);
setTotal(0);
});
}
}
}
}, [smartPort]);
}, [hash, number, onProgress, smartPort]);
const doClose = useCallback(() => {
setModalOpen(false);
@ -60,6 +80,8 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
return (
<div className="disk">
<ProgressModal current={current} total={total} title="Loading..." />
<ErrorModal error={error} setError={setError} />
<BlockFileModal
smartPort={smartPort}
number={number}
@ -71,7 +93,7 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
className={cs('disk-light', { on })}
/>
<button title="Load Disk">
<i class="fas fa-folder-open" onClick={onOpenModal} />
<i className="fas fa-folder-open" onClick={onOpenModal} />
</button>
<div
id={`disk-label${number}`}

View File

@ -1,9 +1,11 @@
import { h } from 'preact';
import { h, Fragment } from 'preact';
import { useCallback, useRef, useState } from 'preact/hooks';
import { DriveNumber, NibbleFormat } from '../formats/types';
import { ErrorModal } from './ErrorModal';
import { Modal, ModalContent, ModalFooter } from './Modal';
import { loadLocalBlockFile, getHashParts, setHashParts } from './util/files';
import SmartPort from 'js/cards/smartport';
import { useHash } from './hooks/useHash';
export type NibbleFileCallback = (
name: string,
@ -22,17 +24,19 @@ export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFil
const inputRef = useRef<HTMLInputElement>(null);
const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true);
const [error, setError] = useState<unknown>();
const hash = useHash();
const doCancel = useCallback(() => onClose(true), []);
const doCancel = useCallback(() => onClose(true), [onClose]);
const doOpen = useCallback(() => {
const hashParts = getHashParts();
const hashParts = getHashParts(hash);
if (smartPort && inputRef.current && inputRef.current.files?.length === 1) {
hashParts[number] = '';
setBusy(true);
loadLocalBlockFile(smartPort, number, inputRef.current.files[0])
.catch(console.error)
.catch((error) => setError(error))
.finally(() => {
setBusy(false);
onClose();
@ -40,7 +44,7 @@ export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFil
}
setHashParts(hashParts);
}, [ smartPort, number, onClose ]);
}, [hash, smartPort, number, onClose]);
const onChange = useCallback(() => {
if (inputRef) {
@ -49,14 +53,17 @@ export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFil
}, [ inputRef ]);
return (
<Modal title="Open File" isOpen={isOpen}>
<ModalContent>
<input type="file" ref={inputRef} onChange={onChange} />
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={doOpen} disabled={busy || empty}>Open</button>
</ModalFooter>
</Modal>
<>
<Modal title="Open File" isOpen={isOpen}>
<ModalContent>
<input type="file" ref={inputRef} onChange={onChange} />
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={doOpen} disabled={busy || empty}>Open</button>
</ModalFooter>
</Modal>
<ErrorModal error={error} setError={setError} />
</>
);
};

View File

@ -2,10 +2,12 @@ import { h } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
import cs from 'classnames';
import Disk2 from '../cards/disk2';
import { ErrorModal } from './ErrorModal';
import { FileModal } from './FileModal';
import { loadJSON, loadHttpNibbleFile, getHashParts } from './util/files';
import { ErrorModal } from './ErrorModal';
import { useHash } from './hooks/useHash';
import { includes } from 'js/types';
import { NIBBLE_FORMATS } from 'js/formats/types';
import styles from './css/DiskII.module.css';
@ -54,8 +56,12 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
const hashPart = decodeURIComponent(newHash);
if (hashPart !== currentHash) {
if (hashPart.match(/^https?:/)) {
loadHttpNibbleFile(disk2, number, hashPart)
.catch((e) => setError(e));
const fileParts = hashPart.split('.');
const ext = fileParts[fileParts.length - 1];
if (includes(NIBBLE_FORMATS, ext)) {
loadHttpNibbleFile(disk2, number, hashPart)
.catch((e) => setError(e));
}
} else {
const filename = `/json/disks/${hashPart}.json`;
loadJSON(disk2, number, filename)

View File

@ -40,12 +40,12 @@ export const Drives = ({ cpu, io, sectors, e }: DrivesProps) => {
const [smartData1, setSmartData1] = useState<DiskIIData>({
on: false,
number: 1,
name: 'Disk 1'
name: 'HD 1'
});
const [smartData2, setSmartData2] = useState<DiskIIData>({
on: false,
number: 2,
name: 'Disk 2'
name: 'HD 2'
});
const [smartPort, setSmartPort] = useState<SmartPort>();
@ -76,11 +76,11 @@ export const Drives = ({ cpu, io, sectors, e }: DrivesProps) => {
label: (drive, name, side) => {
setSmartData[drive - 1]?.(data => ({
...data,
name: name ?? `Disk ${drive}`,
name: name ?? `HD ${drive}`,
side,
}));
},
dirty: () => {}
dirty: () => {/* Unused */}
};
if (cpu && io) {
@ -91,7 +91,7 @@ export const Drives = ({ cpu, io, sectors, e }: DrivesProps) => {
io.setSlot(7, smartPort);
setSmartPort(smartPort);
}
}, [io, sectors]);
}, [cpu, e, io, sectors]);
return (
<div style={{display: 'flex', width: '100%'}}>

View File

@ -0,0 +1,24 @@
import { h, Fragment } from 'preact';
import { Modal, ModalContent } from './Modal';
export interface ErrorProps {
title: string;
current: number | undefined;
total: number | undefined;
}
export const ProgressModal = ({ title, current, total } : ErrorProps) => {
return (
<>
{ current && total ? (
<Modal title={title} isOpen={true}>
<ModalContent>
<div style={{ width: 320, height: 20, background: '#000'}}>
<div style={{ width: 320 * (current / total), background: '#0f0' }} />
</div>
</ModalContent>
</Modal>
) : null}
</>
);
};

View File

@ -12,6 +12,8 @@ import {
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
*
@ -96,14 +98,18 @@ export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: Fil
* @param url URL, relative or absolute to JSON file
* @returns true if successful
*/
export const loadJSON = async (disk2: Disk2, 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);
@ -114,6 +120,7 @@ const loadHttpFile = async (
formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS,
number: DriveNumber,
url: string,
onProgress?: ProgressCallback
) => {
const response = await fetch(url);
if (!response.ok) {
@ -123,13 +130,16 @@ 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();
}
@ -166,8 +176,9 @@ export const loadHttpBlockFile = (
smartPort: SmartPort,
number: DriveNumber,
url: string,
onProgress?: ProgressCallback
) => {
return loadHttpFile(smartPort, BLOCK_FORMATS, number, url);
return loadHttpFile(smartPort, BLOCK_FORMATS, number, url, onProgress);
};
/**
@ -184,9 +195,10 @@ export const loadHttpNibbleFile = (
disk2: Disk2,
number: DriveNumber,
url: string,
onProgress?: ProgressCallback
) => {
if (url.endsWith('.json')) {
return loadJSON(disk2, number, url);
}
return loadHttpFile(disk2, NIBBLE_FORMATS, number, url);
return loadHttpFile(disk2, NIBBLE_FORMATS, number, url, onProgress);
};

View File

@ -271,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 {

View File

@ -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;
};