parent
5b5655b70e
commit
c7a7bcd19b
|
@ -10,6 +10,7 @@ import {
|
|||
BlockFormat,
|
||||
ENCODING_BLOCK,
|
||||
MassStorage,
|
||||
MassStorageData,
|
||||
} from 'js/formats/types';
|
||||
|
||||
const rom = new Uint8Array(readOnlyRom);
|
||||
|
@ -136,6 +137,8 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
|||
[]
|
||||
];
|
||||
|
||||
private _name: string[] = [];
|
||||
|
||||
constructor() {
|
||||
debug('CFFA');
|
||||
|
||||
|
@ -412,6 +415,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
|||
drive = drive - 1;
|
||||
|
||||
this._sectors[drive] = [];
|
||||
this._name[drive] = '';
|
||||
|
||||
this._identity[drive][IDENTITY.SectorCountHigh] = 0;
|
||||
this._identity[drive][IDENTITY.SectorCountLow] = 0;
|
||||
|
@ -437,6 +441,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
|||
const prodos = new ProDOSVolume(disk);
|
||||
dump(prodos);
|
||||
|
||||
this._name[drive] = disk.name;
|
||||
this._partitions[drive] = prodos;
|
||||
|
||||
if (drive) {
|
||||
|
@ -467,4 +472,21 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
|||
|
||||
return this.setBlockVolume(drive, disk);
|
||||
}
|
||||
|
||||
getBinary(drive: number): MassStorageData | null {
|
||||
drive = drive - 1;
|
||||
if (!this._sectors[drive]?.length) {
|
||||
return null;
|
||||
}
|
||||
const blocks = this._sectors[drive];
|
||||
const data = new Uint8Array(blocks.length * 512);
|
||||
for (let idx = 0; idx < blocks.length; idx++) {
|
||||
data.set(blocks[idx], idx * 512);
|
||||
}
|
||||
return {
|
||||
name: this._name[drive],
|
||||
ext: 'po',
|
||||
data: data.buffer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
PROCESS_JSON_DISK,
|
||||
PROCESS_JSON,
|
||||
ENCODING_BITSTREAM,
|
||||
MassStorageData,
|
||||
} from '../formats/types';
|
||||
|
||||
import {
|
||||
|
@ -914,7 +915,7 @@ export default class DiskII implements Card<State> {
|
|||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ disks
|
||||
getBinary(drive: DriveNumber) {
|
||||
getBinary(drive: DriveNumber): MassStorageData | null {
|
||||
const cur = this.drives[drive - 1];
|
||||
if (!isNibbleDrive(cur)) {
|
||||
return null;
|
||||
|
@ -937,7 +938,11 @@ export default class DiskII implements Card<State> {
|
|||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
return {
|
||||
ext: 'dsk',
|
||||
name: cur.name,
|
||||
data: data.buffer
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ disks
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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, BlockFormat } from '../formats/types';
|
||||
import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData } from '../formats/types';
|
||||
import CPU6502, { CpuState, flags } from '../cpu6502';
|
||||
import { read2MGHeader } from '../formats/2mg';
|
||||
import createBlockDisk from '../formats/block';
|
||||
|
@ -131,6 +131,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
private disks: BlockDisk[] = [];
|
||||
private busy: boolean[] = [];
|
||||
private busyTimeout: ReturnType<typeof setTimeout>[] = [];
|
||||
private ext: string[] = [];
|
||||
|
||||
constructor(
|
||||
private cpu: CPU6502,
|
||||
|
@ -561,6 +562,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
volume,
|
||||
};
|
||||
|
||||
this.ext[drive] = fmt;
|
||||
this.disks[drive] = createBlockDisk(options);
|
||||
this.callbacks?.label(drive, name);
|
||||
|
||||
|
@ -569,4 +571,20 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
getBinary(drive: number): MassStorageData | null {
|
||||
if (!this.disks[drive]) {
|
||||
return null;
|
||||
}
|
||||
const blocks = this.disks[drive].blocks;
|
||||
const data = new Uint8Array(blocks.length * 512);
|
||||
for (let idx = 0; idx < blocks.length; idx++) {
|
||||
data.set(blocks[idx], idx * 512);
|
||||
}
|
||||
return {
|
||||
name: this.disks[drive].name,
|
||||
ext: 'po',
|
||||
data: data.buffer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { h } from 'preact';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import cs from 'classnames';
|
||||
import { BLOCK_FORMATS } from 'js/formats/types';
|
||||
import SmartPort from '../cards/smartport';
|
||||
import { BlockFileModal } from './BlockFileModal';
|
||||
import { DiskDragTarget } from './DiskDragTarget';
|
||||
import { DownloadModal } from './DownloadModal';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
|
||||
import styles from './css/BlockDisk.module.css';
|
||||
import { BLOCK_FORMATS } from 'js/formats/types';
|
||||
|
||||
/**
|
||||
* Storage structure for drive state returned via callbacks.
|
||||
|
@ -39,6 +40,7 @@ export interface BlockDiskProps extends BlockDiskData {
|
|||
*/
|
||||
export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [downloadModalOpen, setDownloadModalOpen] = useState(false);
|
||||
const [error, setError] = useState<unknown>();
|
||||
|
||||
const doClose = useCallback(() => {
|
||||
|
@ -49,6 +51,14 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
|||
setModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const doCloseDownload = useCallback(() => {
|
||||
setDownloadModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const onOpenDownloadModal = useCallback(() => {
|
||||
setDownloadModalOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DiskDragTarget
|
||||
className={styles.disk}
|
||||
|
@ -64,6 +74,12 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
|||
onClose={doClose}
|
||||
isOpen={modalOpen}
|
||||
/>
|
||||
<DownloadModal
|
||||
number={number}
|
||||
massStorage={smartPort}
|
||||
isOpen={downloadModalOpen}
|
||||
onClose={doCloseDownload}
|
||||
/>
|
||||
<div
|
||||
id={`disk${number}`}
|
||||
className={cs(styles.diskLight, { [styles.on]: on })}
|
||||
|
@ -71,6 +87,9 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
|||
<button title="Load Disk" onClick={onOpenModal}>
|
||||
<i className="fas fa-folder-open" />
|
||||
</button>
|
||||
<button title="Save Disk" onClick={onOpenDownloadModal}>
|
||||
<i className="fas fa-save" />
|
||||
</button>
|
||||
<div
|
||||
id={`disk-label${number}`}
|
||||
className={styles.diskLabel}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { FileModal } from './FileModal';
|
|||
import styles from './css/DiskII.module.css';
|
||||
import { DiskDragTarget } from './DiskDragTarget';
|
||||
import { NIBBLE_FORMATS } from 'js/formats/types';
|
||||
import { DownloadModal } from './DownloadModal';
|
||||
|
||||
/**
|
||||
* Storage structure for Disk II state returned via callbacks.
|
||||
|
@ -41,6 +42,7 @@ export interface DiskIIProps extends DiskIIData {
|
|||
export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
||||
const label = side ? `${name} - ${side}` : name;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [downloadModalOpen, setDownloadModalOpen] = useState(false);
|
||||
const [error, setError] = useState<unknown>();
|
||||
|
||||
const doClose = useCallback(() => {
|
||||
|
@ -51,6 +53,14 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
|||
setModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const doCloseDownload = useCallback(() => {
|
||||
setDownloadModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const onOpenDownloadModal = useCallback(() => {
|
||||
setDownloadModalOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DiskDragTarget
|
||||
className={styles.disk}
|
||||
|
@ -60,11 +70,20 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
|||
onError={setError}
|
||||
>
|
||||
<FileModal disk2={disk2} number={number} onClose={doClose} isOpen={modalOpen} />
|
||||
<DownloadModal
|
||||
number={number}
|
||||
massStorage={disk2}
|
||||
isOpen={downloadModalOpen}
|
||||
onClose={doCloseDownload}
|
||||
/>
|
||||
<ErrorModal error={error} setError={setError} />
|
||||
<div className={cs(styles.diskLight, { [styles.on]: on })} />
|
||||
<button title="Load Disk" onClick={onOpenModal}>
|
||||
<i className="fas fa-folder-open" />
|
||||
</button>
|
||||
<button title="Save Disk" onClick={onOpenDownloadModal}>
|
||||
<i className="fas fa-save" />
|
||||
</button>
|
||||
<div className={styles.diskLabel}>
|
||||
{label}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { h, Fragment } from 'preact';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { DriveNumber, MassStorage } from '../formats/types';
|
||||
import { Modal, ModalContent, ModalFooter } from './Modal';
|
||||
|
||||
import styles from './css/DownloadModal.module.css';
|
||||
|
||||
interface DownloadModalProps {
|
||||
isOpen: boolean;
|
||||
massStorage: MassStorage<unknown>;
|
||||
number: DriveNumber;
|
||||
onClose: (closeBox?: boolean) => void;
|
||||
}
|
||||
|
||||
export const DownloadModal = ({ massStorage, number, onClose, isOpen } : DownloadModalProps) => {
|
||||
const [href, setHref] = useState('');
|
||||
const [downloadName, setDownloadName] = useState('');
|
||||
const doCancel = useCallback(() => onClose(true), [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const storageData = massStorage.getBinary(number);
|
||||
if (storageData) {
|
||||
const { name, ext, data } = storageData;
|
||||
if (data.byteLength) {
|
||||
const blob = new Blob(
|
||||
[data],
|
||||
{ type: 'application/octet-stream' }
|
||||
);
|
||||
const href = window.URL.createObjectURL(blob);
|
||||
setHref(href);
|
||||
setDownloadName(`${name}.${ext}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setHref('');
|
||||
setDownloadName('');
|
||||
}
|
||||
}, [isOpen, number, massStorage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title="Save File" isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<div className={styles.modalContent}>
|
||||
{ href
|
||||
? (
|
||||
<>
|
||||
<span>Disk Name: {downloadName}</span>
|
||||
<a
|
||||
role="button"
|
||||
href={href}
|
||||
download={downloadName}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<span>No Download Available</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button onClick={doCancel}>Close</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
.modalContent {
|
||||
display: flex;
|
||||
font-size: 1.1em;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
width: 320px;
|
||||
}
|
|
@ -250,4 +250,8 @@ export class SmartStorageBroker implements MassStorage<unknown> {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getBinary(_drive: number) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,9 +212,16 @@ export interface DiskProcessedResponse {
|
|||
export type FormatWorkerResponse =
|
||||
DiskProcessedResponse;
|
||||
|
||||
export interface MassStorageData {
|
||||
name: string;
|
||||
ext: string;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block device common interface
|
||||
*/
|
||||
export interface MassStorage<T> {
|
||||
setBinary(drive: number, name: string, ext: T, data: ArrayBuffer): boolean;
|
||||
getBinary(drive: number): MassStorageData | null;
|
||||
}
|
||||
|
|
|
@ -120,14 +120,15 @@ export function openSave(driveString: string, event: MouseEvent) {
|
|||
const drive = parseInt(driveString, 10) as DriveNumber;
|
||||
|
||||
const mimeType = 'application/octet-stream';
|
||||
const data = _disk2.getBinary(drive);
|
||||
const storageData = _disk2.getBinary(drive);
|
||||
const a = document.querySelector<HTMLAnchorElement>('#local_save_link')!;
|
||||
|
||||
if (!data) {
|
||||
if (!storageData) {
|
||||
alert(`No data from drive ${drive}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = storageData;
|
||||
const blob = new Blob([data], { 'type': mimeType });
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = driveLights.label(drive) + '.dsk';
|
||||
|
|
|
@ -17,9 +17,13 @@ export default class DriveLights implements Callbacks {
|
|||
|
||||
public label(drive: DriveNumber, label?: string, side?: string) {
|
||||
const labelElement = document.querySelector<HTMLElement>(`#disk-label${drive}`);
|
||||
const labelText = `${label || ''} ${(side ? `- ${side}` : '')}`;
|
||||
if (label && labelElement) {
|
||||
labelElement.innerText = labelText;
|
||||
let labelText = '';
|
||||
if (labelElement) {
|
||||
labelText = labelElement.innerText;
|
||||
if (label) {
|
||||
labelText = `${label || ''} ${(side ? `- ${side}` : '')}`;
|
||||
labelElement.innerText = labelText;
|
||||
}
|
||||
}
|
||||
return labelText;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue