diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index 7937a35..c12ebae 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -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, Restorable< [] ]; + private _name: string[] = []; + constructor() { debug('CFFA'); @@ -412,6 +415,7 @@ export default class CFFA implements Card, MassStorage, 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, 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, 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, + }; + } } diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 5264b56..d17a19b 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -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 { } // 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 { } } - return data; + return { + ext: 'dsk', + name: cur.name, + data: data.buffer + }; } // TODO(flan): Does not work with WOZ disks diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 98db6ae..e3bdd60 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -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, Restor private disks: BlockDisk[] = []; private busy: boolean[] = []; private busyTimeout: ReturnType[] = []; + private ext: string[] = []; constructor( private cpu: CPU6502, @@ -561,6 +562,7 @@ export default class SmartPort implements Card, MassStorage, 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, 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, + }; + } } diff --git a/js/components/BlockDisk.tsx b/js/components/BlockDisk.tsx index 56fa726..18053d9 100644 --- a/js/components/BlockDisk.tsx +++ b/js/components/BlockDisk.tsx @@ -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(); 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 ( { onClose={doClose} isOpen={modalOpen} /> +
{ +
{ const label = side ? `${name} - ${side}` : name; const [modalOpen, setModalOpen] = useState(false); + const [downloadModalOpen, setDownloadModalOpen] = useState(false); const [error, setError] = useState(); 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 ( { onError={setError} > +
+
{label}
diff --git a/js/components/DownloadModal.tsx b/js/components/DownloadModal.tsx new file mode 100644 index 0000000..f628c62 --- /dev/null +++ b/js/components/DownloadModal.tsx @@ -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; + 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 ( + <> + + +
+ { href + ? ( + <> + Disk Name: {downloadName} + + Download + + + ) : ( + No Download Available + ) + } +
+
+ + + +
+ + ); +}; diff --git a/js/components/css/DownloadModal.module.css b/js/components/css/DownloadModal.module.css new file mode 100644 index 0000000..2f972f8 --- /dev/null +++ b/js/components/css/DownloadModal.module.css @@ -0,0 +1,7 @@ +.modalContent { + display: flex; + font-size: 1.1em; + justify-content: space-between; + padding: 10px 0; + width: 320px; +} diff --git a/js/components/util/files.ts b/js/components/util/files.ts index 1ac9bd9..3a3dbd3 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -250,4 +250,8 @@ export class SmartStorageBroker implements MassStorage { } return true; } + + getBinary(_drive: number) { + return null; + } } diff --git a/js/formats/types.ts b/js/formats/types.ts index 33bca9f..c52d484 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -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 { setBinary(drive: number, name: string, ext: T, data: ArrayBuffer): boolean; + getBinary(drive: number): MassStorageData | null; } diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 99102d3..e278b2d 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -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('#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'; diff --git a/js/ui/drive_lights.ts b/js/ui/drive_lights.ts index a33f2cc..04399a9 100644 --- a/js/ui/drive_lights.ts +++ b/js/ui/drive_lights.ts @@ -17,9 +17,13 @@ export default class DriveLights implements Callbacks { public label(drive: DriveNumber, label?: string, side?: string) { const labelElement = document.querySelector(`#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; }