mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
More WIP
This commit is contained in:
parent
1de57ab75a
commit
d4b20c32c2
|
@ -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';
|
||||
|
||||
|
|
|
@ -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}`}
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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%'}}>
|
||||
|
|
24
js/components/ProgressModal.tsx
Normal file
24
js/components/ProgressModal.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user