Simple Preact download (#134)

* Simple Preact download
This commit is contained in:
Will Scullin 2022-06-19 09:01:44 -07:00 committed by GitHub
parent 5b5655b70e
commit c7a7bcd19b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 185 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
.modalContent {
display: flex;
font-size: 1.1em;
justify-content: space-between;
padding: 10px 0;
width: 320px;
}

View File

@ -250,4 +250,8 @@ export class SmartStorageBroker implements MassStorage<unknown> {
}
return true;
}
getBinary(_drive: number) {
return null;
}
}

View File

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

View File

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

View File

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