mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Break up UI
This commit is contained in:
parent
a9885dbfbd
commit
2c89289ff1
|
@ -34,9 +34,9 @@
|
|||
|
||||
</head>
|
||||
<body class="apple2"
|
||||
ondragover="Apple2.handleDragOver(0, event)"
|
||||
ondrop="Apple2.handleDrop(0, event)"
|
||||
ondragend="Apple2.handleDragEnd(0, event)">
|
||||
ondragover="Disk2UI.handleDragOver(0, event)"
|
||||
ondrop="Disk2UI.handleDrop(0, event)"
|
||||
ondragend="Disk2UI.handleDragEnd(0, event)">
|
||||
<div class="outer">
|
||||
<div id="header">
|
||||
<a href="https://github.com/whscullin/apple2js#readme" target="_blank">
|
||||
|
@ -51,31 +51,31 @@
|
|||
</div>
|
||||
<div class="inset">
|
||||
<div class="disk"
|
||||
ondragover="Apple2.handleDragOver(1, event)"
|
||||
ondrop="Apple2.handleDrop(1, event)"
|
||||
ondragend="Apple2.handleDragEnd(1, event)">
|
||||
ondragover="Disk2UI.handleDragOver(event)"
|
||||
ondrop="Disk2UI.handleDrop(1, event)"
|
||||
ondragend="Disk2UI.handleDragEnd(event)">
|
||||
<div class="disk-light" id="disk1"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="Apple2.openLoad(1, event);">
|
||||
onclick="Disk2UI.openLoad(1, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<button title="Save Disk"
|
||||
onclick="Apple2.openSave(1, event);">
|
||||
onclick="Disk2UI.openSave(1, event);">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<div id="disk-label1" class="disk-label">Disk 1</div>
|
||||
</div>
|
||||
<div class="disk"
|
||||
ondragover="Apple2.handleDragOver(2, event)"
|
||||
ondrop="Apple2.handleDrop(2, event)"
|
||||
ondragend="Apple2.handleDragEnd(2, event)">
|
||||
ondragover="Disk2UI.handleDragOver(event)"
|
||||
ondrop="Disk2UI.handleDrop(2, event)"
|
||||
ondragend="Disk2UI.handleDragEnd(event)">
|
||||
<div class="disk-light" id="disk2"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="Apple2.openLoad(2, event);">
|
||||
onclick="Disk2UI.openLoad(2, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<button title="Save Disk"
|
||||
onclick="Apple2.openSave(2, event);">
|
||||
onclick="Disk2UI.openSave(2, event);">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<div id="disk-label2" class="disk-label">Disk 2</div>
|
||||
|
@ -89,14 +89,14 @@
|
|||
<button id="toggle-sound" onclick="Apple2.toggleSound()" title="Toggle Sound">
|
||||
<i class="fas fa-volume-off"></i>
|
||||
</button>
|
||||
<button id="toggle-printer" onclick="Apple2.openPrinterModal()" title="Toggle Printer">
|
||||
<button id="toggle-printer" onclick="Printer.openPrinterModal()" title="Toggle Printer">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button onclick="window.open('https://github.com/whscullin/apple2js#readme', 'blank')" title="About">
|
||||
<i class="fas fa-info"></i>
|
||||
</button>
|
||||
<button onclick="Apple2.openOptions()" title="Options (F4)">
|
||||
<button onclick="OptionsModal.openModal()" title="Options (F4)">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -165,7 +165,7 @@
|
|||
</div>
|
||||
</main>
|
||||
<footer class="modal__footer">
|
||||
<button class="modal__btn" onclick="Apple2.doSave()" aria-label="Save disk locally">Save</button>
|
||||
<button class="modal__btn" onclick="Disk2UI.doSave()" aria-label="Save disk locally">Save</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -227,13 +227,13 @@
|
|||
<tr>
|
||||
<td>
|
||||
<select id="category_select" multiple="multiple"
|
||||
onchange="Apple2.selectCategory(event)" >
|
||||
onchange="Disk2UI.selectCategory(event)" >
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select id="disk_select" multiple="multiple"
|
||||
onchange="Apple2.selectDisk(event)"
|
||||
ondblclick="Apple2.clickDisk(event)">
|
||||
onchange="Disk2UI.selectDisk(event)"
|
||||
ondblclick="Disk2UI.clickDisk(event)">
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -248,7 +248,7 @@
|
|||
</main>
|
||||
<footer class="modal__footer">
|
||||
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Cancel</button>
|
||||
<button class="modal__btn" onclick="Apple2.doLoad(event)" aria-label="Open the selected disk">Open</button>
|
||||
<button class="modal__btn" onclick="Disk2UI.doLoad(event)" aria-label="Open the selected disk">Open</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -269,7 +269,7 @@
|
|||
</main>
|
||||
<footer class="modal__footer">
|
||||
<a id="raw_printer_output" class="button">Download Raw Output</a>
|
||||
<button class="modal__btn" onclick="Apple2.clearPrinterPaper()" aria-label="Clear the paper">Clear</button>
|
||||
<button class="modal__btn" onclick="Printer.clear()" aria-label="Clear the paper">Clear</button>
|
||||
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
129
apple2jse.html
129
apple2jse.html
|
@ -34,9 +34,9 @@
|
|||
|
||||
</head>
|
||||
<body class="apple2e"
|
||||
ondragover="Apple2.handleDragOver(0, event)"
|
||||
ondrop="Apple2.handleDrop(0, event)"
|
||||
ondragend="Apple2.handleDragEnd(0, event)">
|
||||
ondragover="Disk2UI.handleDragOver(event)"
|
||||
ondrop="Disk2UI.handleDrop(0, event)"
|
||||
ondragend="Disk2UI.handleDragEnd(event)">
|
||||
<div class="outer">
|
||||
<div id="header">
|
||||
<a href="https://github.com/whscullin/apple2js#readme" target="_blank">
|
||||
|
@ -49,36 +49,62 @@
|
|||
<canvas id="screen" width="592" height="416"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inset">
|
||||
<div class="disk"
|
||||
ondragover="Apple2.handleDragOver(1, event)"
|
||||
ondrop="Apple2.handleDrop(1, event)"
|
||||
ondragend="Apple2.handleDragEnd(1, event)">
|
||||
<div class="disk-light" id="disk1"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="Apple2.openLoad(1, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<button title="Save Disk"
|
||||
onclick="Apple2.openSave(1, event);">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<div id="disk-label1" class="disk-label">Disk 1</div>
|
||||
<div class="storage">
|
||||
<div class="inset disk-ii">
|
||||
<div class="disk"
|
||||
ondragover="Disk2UI.handleDragOver( event)"
|
||||
ondrop="Disk2UI.handleDrop(1, event)"
|
||||
ondragend="Disk2UI.handleDragEnd(event)">
|
||||
<div class="disk-light" id="disk1"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="Disk2UI.openLoad(1, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<button title="Save Disk"
|
||||
onclick="Disk2UI.openSave(1, event);">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<div id="disk-label1" class="disk-label">Disk 1</div>
|
||||
</div>
|
||||
<div class="disk"
|
||||
ondragover="Disk2UI.handleDragOver(event)"
|
||||
ondrop="Disk2UI.handleDrop(2, event)"
|
||||
ondragend="Disk2UI.handleDragEnd(event)">
|
||||
<div class="disk-light" id="disk2"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="Disk2UI.openLoad(2, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<button title="Save Disk"
|
||||
onclick="Disk2UI.openSave(2, event);">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<div id="disk-label2" class="disk-label">Disk 2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="disk"
|
||||
ondragover="Apple2.handleDragOver(2, event)"
|
||||
ondrop="Apple2.handleDrop(2, event)"
|
||||
ondragend="Apple2.handleDragEnd(2, event)">
|
||||
<div class="disk-light" id="disk2"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="Apple2.openLoad(2, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<button title="Save Disk"
|
||||
onclick="Apple2.openSave(2, event);">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<div id="disk-label2" class="disk-label">Disk 2</div>
|
||||
<div class="inset block-storage">
|
||||
<div class="disk"
|
||||
ondragover="BlockStorageUI.handleDragOver(event)"
|
||||
ondrop="BlockStorageUI.handleDrop(1, event)"
|
||||
ondragend="BlockStorageUI.handleDragEnd(event)">
|
||||
<div class="disk-light" id="mass-storage1"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="BlockStorageUI.openLoad(1, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<div id="mass-storage-label1" class="disk-label">HD 1</div>
|
||||
</div>
|
||||
<div class="disk"
|
||||
ondragover="BlockStorageUI.handleDragOver(event)"
|
||||
ondrop="BlockStorageUI.handleDrop(2, true, event)"
|
||||
ondragend="BlockStorageUI.handleDragEnd(event)">
|
||||
<div class="disk-light" id="mass-storage2"></div>
|
||||
<button title="Load Disk"
|
||||
onclick="BlockStorageUI.openLoad(2, event);">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<div id="mass-storage-label2" class="disk-label">HD 2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reset-row">
|
||||
|
@ -90,14 +116,14 @@
|
|||
<button id="toggle-sound" onclick="Apple2.toggleSound()" title="Toggle Sound">
|
||||
<i class="fas fa-volume-off"></i>
|
||||
</button>
|
||||
<button id="toggle-printer" onclick="Apple2.openPrinterModal()" title="Toggle Printer">
|
||||
<button id="toggle-printer" onclick="Printer.openPrinterModal()" title="Toggle Printer">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button onclick="window.open('https://github.com/whscullin/apple2js#readme', 'blank')" title="About">
|
||||
<i class="fas fa-info"></i>
|
||||
</button>
|
||||
<button onclick="Apple2.openOptions()" title="Options (F4)">
|
||||
<button onclick="OptionsModal.openModal()" title="Options (F4)">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -170,7 +196,7 @@
|
|||
</div>
|
||||
</main>
|
||||
<footer class="modal__footer">
|
||||
<button class="modal__btn" onclick="Apple2.doSave()" aria-label="Save disk locally">Save</button>
|
||||
<button class="modal__btn" onclick="Disk2UI.doSave()" aria-label="Save disk locally">Save</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -232,13 +258,13 @@
|
|||
<tr>
|
||||
<td>
|
||||
<select id="category_select" multiple="multiple"
|
||||
onchange="Apple2.selectCategory(event)" >
|
||||
onchange="Disk2UI.selectCategory(event)" >
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select id="disk_select" multiple="multiple"
|
||||
onchange="Apple2.selectDisk(event)"
|
||||
ondblclick="Apple2.clickDisk(event)">
|
||||
onchange="Disk2UI.selectDisk(event)"
|
||||
ondblclick="Disk2UI.clickDisk(event)">
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -253,7 +279,30 @@
|
|||
</main>
|
||||
<footer class="modal__footer">
|
||||
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Cancel</button>
|
||||
<button class="modal__btn" onclick="Apple2.doLoad(event)" aria-label="Open the selected disk">Open</button>
|
||||
<button class="modal__btn" onclick="Disk2UI.doLoad(event)" aria-label="Open the selected disk">Open</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="mass-storage-modal" aria-hidden="true">
|
||||
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
|
||||
<div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="Load Disk">
|
||||
<header class="modal__header">
|
||||
<span class="modal__title" id="mass-storage-modal-title">
|
||||
Load Disk
|
||||
</span>
|
||||
<button class="modal__close" aria-label="Close modal" data-micromodal-close>
|
||||
</button>
|
||||
</header>
|
||||
<main class="modal__content" id="mass-storage-modal-content">
|
||||
<form action="#">
|
||||
<input type="file" id="mass-storage-file" />
|
||||
</form>
|
||||
</main>
|
||||
<footer class="modal__footer">
|
||||
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Cancel</button>
|
||||
<button class="modal__btn" onclick="BlockStorageUI.doLoad(event)" aria-label="Open the selected disk">Open</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -274,7 +323,7 @@
|
|||
</main>
|
||||
<footer class="modal__footer">
|
||||
<a id="raw_printer_output" class="button">Download Raw Output</a>
|
||||
<button class="modal__btn" onclick="Apple2.clearPrinterPaper()" aria-label="Clear the paper">Clear</button>
|
||||
<button class="modal__btn" onclick="Printer.clear()" aria-label="Clear the paper">Clear</button>
|
||||
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
@ -107,11 +107,37 @@ body {
|
|||
border: 1px inset #888;
|
||||
}
|
||||
|
||||
.storage {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.apple2e .storage .inset {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.disk {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
max-width: 50%;
|
||||
max-width: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.mass .disk {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.storage .inset.block-storage {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.storage.mass .inset.block-storage {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.disk-light {
|
||||
|
@ -152,9 +178,10 @@ th {
|
|||
|
||||
.inset {
|
||||
border-radius: 6px;
|
||||
background-color: #c4c1a0;
|
||||
border: 3px inset #f0edd0;
|
||||
padding: 6px;
|
||||
margin: 10px 0;
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,12 +4,13 @@ import { rom as readOnlyRom } from '../roms/cards/cffa';
|
|||
import { read2MGHeader } from '../formats/2mg';
|
||||
import { ProDOSVolume } from '../formats/prodos';
|
||||
import createBlockDisk from '../formats/block';
|
||||
import type { DriveNumber } from 'js/formats/types';
|
||||
import { dump } from '../formats/prodos/utils';
|
||||
import {
|
||||
BlockDisk,
|
||||
BlockFormat,
|
||||
ENCODING_BLOCK,
|
||||
MassStorage,
|
||||
BlockStorage,
|
||||
} from 'js/formats/types';
|
||||
|
||||
const rom = new Uint8Array(readOnlyRom);
|
||||
|
@ -85,7 +86,7 @@ export interface CFFAState {
|
|||
disks: Array<BlockDisk | null>
|
||||
}
|
||||
|
||||
export default class CFFA implements Card, MassStorage, Restorable<CFFAState> {
|
||||
export default class CFFA implements Card, BlockStorage, Restorable<CFFAState> {
|
||||
|
||||
// CFFA internal Flags
|
||||
|
||||
|
@ -399,45 +400,47 @@ export default class CFFA implements Card, MassStorage, Restorable<CFFAState> {
|
|||
setState(state: CFFAState) {
|
||||
state.disks.forEach(
|
||||
(disk, idx) => {
|
||||
const drive = idx + 1 as DriveNumber;
|
||||
|
||||
if (disk) {
|
||||
this.setBlockVolume(idx + 1, disk);
|
||||
this.setBlockVolume(drive, disk);
|
||||
} else {
|
||||
this.resetBlockVolume(idx + 1);
|
||||
this.resetBlockVolume(drive);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetBlockVolume(drive: number) {
|
||||
drive = drive - 1;
|
||||
resetBlockVolume(drive: DriveNumber) {
|
||||
const driveIdx = drive - 1;
|
||||
|
||||
this._sectors[drive] = [];
|
||||
this._sectors[driveIdx] = [];
|
||||
|
||||
this._identity[drive][IDENTITY.SectorCountHigh] = 0;
|
||||
this._identity[drive][IDENTITY.SectorCountLow] = 0;
|
||||
this._identity[driveIdx][IDENTITY.SectorCountHigh] = 0;
|
||||
this._identity[driveIdx][IDENTITY.SectorCountLow] = 0;
|
||||
|
||||
if (drive) {
|
||||
if (driveIdx) {
|
||||
rom[SETTINGS.Max32MBPartitionsDev1] = 0x0;
|
||||
} else {
|
||||
rom[SETTINGS.Max32MBPartitionsDev0] = 0x0;
|
||||
}
|
||||
}
|
||||
|
||||
setBlockVolume(drive: number, disk: BlockDisk) {
|
||||
drive = drive - 1;
|
||||
setBlockVolume(drive: DriveNumber, disk: BlockDisk) {
|
||||
const driveIdx = drive - 1;
|
||||
|
||||
// Convert 512 byte blocks into 256 word sectors
|
||||
this._sectors[drive] = disk.blocks.map(function(block) {
|
||||
this._sectors[driveIdx] = disk.blocks.map(function(block) {
|
||||
return new Uint16Array(block.buffer);
|
||||
});
|
||||
|
||||
this._identity[drive][IDENTITY.SectorCountHigh] = this._sectors[0].length & 0xffff;
|
||||
this._identity[drive][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
|
||||
this._identity[driveIdx][IDENTITY.SectorCountHigh] = this._sectors[0].length & 0xffff;
|
||||
this._identity[driveIdx][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
|
||||
|
||||
const prodos = new ProDOSVolume(disk);
|
||||
dump(prodos);
|
||||
|
||||
this._partitions[drive] = prodos;
|
||||
this._partitions[driveIdx] = prodos;
|
||||
|
||||
if (drive) {
|
||||
rom[SETTINGS.Max32MBPartitionsDev1] = 0x1;
|
||||
|
@ -449,7 +452,7 @@ export default class CFFA implements Card, MassStorage, Restorable<CFFAState> {
|
|||
|
||||
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
|
||||
|
||||
setBinary(drive: number, name: string, ext: BlockFormat, rawData: ArrayBuffer) {
|
||||
setBinary(drive: DriveNumber, name: string, ext: BlockFormat, rawData: ArrayBuffer) {
|
||||
const volume = 254;
|
||||
const readOnly = false;
|
||||
|
||||
|
@ -467,4 +470,11 @@ export default class CFFA implements Card, MassStorage, Restorable<CFFAState> {
|
|||
|
||||
return this.setBlockVolume(drive, disk);
|
||||
}
|
||||
|
||||
getMetadata(driveNo: number): Record<string, any> {
|
||||
const drive = this._partitions[driveNo - 1];
|
||||
return {
|
||||
name: drive?.vdh.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
NibbleFormat,
|
||||
DISK_PROCESSED,
|
||||
DRIVE_NUMBERS,
|
||||
DriveCallbacks,
|
||||
DriveNumber,
|
||||
JSONDisk,
|
||||
ENCODING_NIBBLE,
|
||||
|
@ -151,11 +152,6 @@ const PHASE_DELTA = [
|
|||
[-2, -1, 0, 1],
|
||||
[1, -2, -1, 0]
|
||||
] as const;
|
||||
export interface Callbacks {
|
||||
driveLight: (drive: DriveNumber, on: boolean) => void;
|
||||
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
||||
label: (drive: DriveNumber, name?: string, side?: string) => void;
|
||||
}
|
||||
|
||||
/** Common information for Nibble and WOZ disks. */
|
||||
|
||||
|
@ -383,7 +379,7 @@ export default class DiskII implements Card {
|
|||
private worker: Worker;
|
||||
|
||||
/** Builds a new Disk ][ card. */
|
||||
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) {
|
||||
constructor(private io: Apple2IO, private callbacks: DriveCallbacks, private sectors = 16) {
|
||||
this.debug('Disk ][');
|
||||
|
||||
this.lastCycles = this.io.cycles();
|
||||
|
@ -755,11 +751,12 @@ export default class DiskII implements Card {
|
|||
this.cur = this.drives[this.drive - 1];
|
||||
}
|
||||
|
||||
getMetadata(driveNo: DriveNumber) {
|
||||
getMetadata(driveNo: DriveNumber): Record<string, any> {
|
||||
const drive = this.drives[driveNo - 1];
|
||||
return {
|
||||
format: drive.format,
|
||||
volume: drive.volume,
|
||||
name: drive.name,
|
||||
track: drive.track,
|
||||
head: drive.head,
|
||||
phase: drive.phase,
|
||||
|
|
|
@ -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 } from '../formats/types';
|
||||
import { BlockStorage, BlockDisk, ENCODING_BLOCK, DriveCallbacks, DriveNumber } from '../formats/types';
|
||||
import CPU6502, { CpuState, flags } from '../cpu6502';
|
||||
import { read2MGHeader } from '../formats/2mg';
|
||||
import createBlockDisk from '../formats/block';
|
||||
|
@ -15,7 +15,8 @@ export interface SmartPortState {
|
|||
}
|
||||
|
||||
export interface SmartPortOptions {
|
||||
block: boolean;
|
||||
block: boolean
|
||||
callbacks: DriveCallbacks
|
||||
}
|
||||
|
||||
class Address {
|
||||
|
@ -118,10 +119,12 @@ const DEVICE_TYPE_SCSI_HD = 0x07;
|
|||
// $0D: Printer
|
||||
// $0E: Clock
|
||||
// $0F: Modem
|
||||
export default class SmartPort implements Card, MassStorage, Restorable<SmartPortState> {
|
||||
export default class SmartPort implements Card, BlockStorage, Restorable<SmartPortState> {
|
||||
|
||||
private rom: rom;
|
||||
private disks: BlockDisk[] = [];
|
||||
private lightTimers: number[] = [];
|
||||
private callbacks;
|
||||
|
||||
constructor(private cpu: CPU6502, options: SmartPortOptions) {
|
||||
if (options?.block) {
|
||||
|
@ -133,6 +136,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
debug('SmartPort card');
|
||||
this.rom = smartPortRom;
|
||||
}
|
||||
this.callbacks = options.callbacks;
|
||||
}
|
||||
|
||||
private debug(..._args: any[]) {
|
||||
|
@ -143,7 +147,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
* dumpBlock
|
||||
*/
|
||||
|
||||
dumpBlock(drive: number, block: number) {
|
||||
dumpBlock(drive: DriveNumber, block: number) {
|
||||
let result = '';
|
||||
let b;
|
||||
let jdx;
|
||||
|
@ -178,7 +182,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
* getDeviceInfo
|
||||
*/
|
||||
|
||||
getDeviceInfo(state: CpuState, drive: number) {
|
||||
getDeviceInfo(state: CpuState, drive: DriveNumber) {
|
||||
if (this.disks[drive]) {
|
||||
const blocks = this.disks[drive].blocks.length;
|
||||
state.x = blocks & 0xff;
|
||||
|
@ -196,7 +200,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
* readBlock
|
||||
*/
|
||||
|
||||
readBlock(state: CpuState, drive: number, block: number, buffer: Address) {
|
||||
readBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) {
|
||||
this.debug('read drive=' + drive);
|
||||
this.debug('read buffer=' + buffer);
|
||||
this.debug('read block=$' + toHex(block));
|
||||
|
@ -210,6 +214,13 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
|
||||
// debug('read', '\n' + dumpBlock(drive, block));
|
||||
|
||||
this.callbacks.driveLight(drive, true);
|
||||
if (this.lightTimers[drive]) {
|
||||
window.clearTimeout(this.lightTimers[drive]);
|
||||
}
|
||||
this.lightTimers[drive] = window.setTimeout(() => {
|
||||
this.callbacks.driveLight(drive, false);
|
||||
}, 100);
|
||||
for (let idx = 0; idx < 512; idx++) {
|
||||
buffer.writeByte(this.disks[drive].blocks[block][idx]);
|
||||
buffer = buffer.inc(1);
|
||||
|
@ -223,7 +234,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
* writeBlock
|
||||
*/
|
||||
|
||||
writeBlock(state: CpuState, drive: number, block: number, buffer: Address) {
|
||||
writeBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) {
|
||||
this.debug('write drive=' + drive);
|
||||
this.debug('write buffer=' + buffer);
|
||||
this.debug('write block=$' + toHex(block));
|
||||
|
@ -256,7 +267,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
* formatDevice
|
||||
*/
|
||||
|
||||
formatDevice(state: CpuState, drive: number) {
|
||||
formatDevice(state: CpuState, drive: DriveNumber) {
|
||||
if (!this.disks[drive]?.blocks.length) {
|
||||
debug('Drive', drive, 'is empty');
|
||||
state.a = DEVICE_OFFLINE;
|
||||
|
@ -323,10 +334,10 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
if (off === blockOff && this.cpu.getSync()) { // Regular block device entry POINT
|
||||
this.debug('block device entry');
|
||||
cmd = this.cpu.read(0x00, COMMAND);
|
||||
unit = this.cpu.read(0x00, UNIT);
|
||||
unit = this.cpu.read(0x00, UNIT) as DriveNumber;
|
||||
const bufferAddr = new Address(this.cpu, ADDRESS_LO);
|
||||
const blockAddr = new Address(this.cpu, BLOCK_LO);
|
||||
const drive = (unit & 0x80) ? 2 : 1;
|
||||
const drive = ((unit & 0x80) ? 2 : 1) as DriveNumber;
|
||||
const driveSlot = (unit & 0x70) >> 4;
|
||||
|
||||
buffer = bufferAddr.readAddress();
|
||||
|
@ -374,7 +385,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
stackAddr.writeAddress(retVal.inc(3));
|
||||
|
||||
const parameterCount = cmdListAddr.readByte();
|
||||
unit = cmdListAddr.inc(1).readByte();
|
||||
unit = cmdListAddr.inc(1).readByte() as DriveNumber | 0;
|
||||
buffer = cmdListAddr.inc(2).readAddress();
|
||||
let status;
|
||||
|
||||
|
@ -444,16 +455,16 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
|
||||
case 0x01: // READ BLOCK
|
||||
block = cmdListAddr.inc(4).readWord();
|
||||
this.readBlock(state, unit, block, buffer);
|
||||
this.readBlock(state, unit as DriveNumber, block, buffer);
|
||||
break;
|
||||
|
||||
case 0x02: // WRITE BLOCK
|
||||
block = cmdListAddr.inc(4).readWord();
|
||||
this.writeBlock(state, unit, block, buffer);
|
||||
this.writeBlock(state, unit as DriveNumber, block, buffer);
|
||||
break;
|
||||
|
||||
case 0x03: // FORMAT
|
||||
this.formatDevice(state, unit);
|
||||
this.formatDevice(state, unit as DriveNumber);
|
||||
break;
|
||||
|
||||
case 0x04: // CONTROL
|
||||
|
@ -518,7 +529,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
);
|
||||
}
|
||||
|
||||
setBinary(drive: number, name: string, fmt: string, rawData: ArrayBuffer) {
|
||||
setBinary(drive: DriveNumber, name: string, fmt: string, rawData: ArrayBuffer) {
|
||||
const volume = 254;
|
||||
const readOnly = false;
|
||||
if (fmt == '2mg') {
|
||||
|
@ -532,11 +543,21 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
|
|||
volume,
|
||||
};
|
||||
|
||||
this.disks[drive] = createBlockDisk(options);
|
||||
const disk = createBlockDisk(options);
|
||||
this.callbacks.label(drive, disk.name);
|
||||
this.disks[drive] = disk;
|
||||
|
||||
const prodos = new ProDOSVolume(this.disks[drive]);
|
||||
dump(prodos);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getMetadata(driveNo: DriveNumber): Record<string, any> {
|
||||
const drive = this.disks[driveNo - 1];
|
||||
return {
|
||||
name: drive.name,
|
||||
readOnly: drive.readOnly,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,6 +192,26 @@ export type FormatWorkerResponse =
|
|||
/**
|
||||
* Block device common interface
|
||||
*/
|
||||
export interface MassStorage {
|
||||
setBinary(drive: number, name: string, ext: BlockFormat, data: ArrayBuffer): boolean
|
||||
export interface BlockStorage {
|
||||
setBinary(drive: DriveNumber, name: string, ext: BlockFormat, data: ArrayBuffer): boolean
|
||||
getMetadata(drive: DriveNumber): Record<string, any>
|
||||
}
|
||||
|
||||
export interface FloppyStorage {
|
||||
setBinary(drive: DriveNumber, name: string, ext: NibbleFormat, data: ArrayBuffer): boolean
|
||||
getMetadata(drive: DriveNumber): Record<string, any>
|
||||
}
|
||||
|
||||
export interface DiskStorage<T> {
|
||||
setBinary(drive: DriveNumber, name: string, ext: T, data: ArrayBuffer): boolean
|
||||
getMetadata(drive: DriveNumber): Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface DriveCallbacks {
|
||||
driveLight: (drive: DriveNumber, on: boolean) => void;
|
||||
dirty: (drive: DriveNumber, dirty: boolean) => void;
|
||||
label: (drive: DriveNumber, name: string, side?: string) => void;
|
||||
}
|
||||
|
|
14
js/main2.ts
14
js/main2.ts
|
@ -1,6 +1,11 @@
|
|||
import Prefs from './prefs';
|
||||
|
||||
import { driveLights, initUI, updateUI } from './ui/apple2';
|
||||
import {
|
||||
diskLights,
|
||||
initUI,
|
||||
BlockStorageLights,
|
||||
updateUI
|
||||
} from './ui/apple2';
|
||||
import Printer from './ui/printer';
|
||||
|
||||
import DiskII from './cards/disk2';
|
||||
|
@ -71,9 +76,12 @@ apple2.ready.then(() => {
|
|||
const parallel = new Parallel(printer);
|
||||
const videoTerm = new VideoTerm();
|
||||
const slinky = new RAMFactor(1024 * 1024);
|
||||
const disk2 = new DiskII(io, driveLights, sectors);
|
||||
const disk2 = new DiskII(io, diskLights, sectors);
|
||||
const clock = new Thunderclock();
|
||||
const smartport = new SmartPort(cpu, { block: true });
|
||||
const smartport = new SmartPort(cpu, {
|
||||
block: true,
|
||||
callbacks: BlockStorageLights,
|
||||
});
|
||||
|
||||
io.setSlot(0, lc);
|
||||
io.setSlot(1, parallel);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Prefs from './prefs';
|
||||
|
||||
import { driveLights, initUI, updateUI } from './ui/apple2';
|
||||
import { diskLights, BlockStorageLights, initUI, updateUI } from './ui/apple2';
|
||||
import Printer from './ui/printer';
|
||||
import { MouseUI } from './ui/mouse';
|
||||
|
||||
|
@ -60,10 +60,13 @@ apple2.ready.then(() => {
|
|||
|
||||
const parallel = new Parallel(printer);
|
||||
const slinky = new RAMFactor(1024 * 1024);
|
||||
const disk2 = new DiskII(io, driveLights);
|
||||
const disk2 = new DiskII(io, diskLights);
|
||||
const clock = new Thunderclock();
|
||||
const smartport = new SmartPort(cpu, { block: !enhanced });
|
||||
const mouse = new Mouse(cpu, mouseUI);
|
||||
const smartport = new SmartPort(cpu, {
|
||||
block: !enhanced,
|
||||
callbacks: BlockStorageLights,
|
||||
});
|
||||
|
||||
io.setSlot(1, parallel);
|
||||
io.setSlot(2, slinky);
|
||||
|
|
7
js/ui/alert.ts
Normal file
7
js/ui/alert.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import MicroModal from 'micromodal';
|
||||
|
||||
export function openAlert(msg: string) {
|
||||
const el = document.querySelector<HTMLDivElement>('#alert-modal .message')!;
|
||||
el.innerText = msg;
|
||||
MicroModal.show('alert-modal');
|
||||
}
|
732
js/ui/apple2.ts
732
js/ui/apple2.ts
|
@ -3,18 +3,15 @@ import MicroModal from 'micromodal';
|
|||
import { base64_json_parse, base64_json_stringify } from '../base64';
|
||||
import { Audio, SOUND_ENABLED_OPTION } from './audio';
|
||||
import DriveLights from './drive_lights';
|
||||
import { byte, includes, word } from '../types';
|
||||
import { BLOCK_FORMATS, MassStorage, NIBBLE_FORMATS } from '../formats/types';
|
||||
import { includes } from '../types';
|
||||
import { BLOCK_FORMATS, NIBBLE_FORMATS, BlockStorage } from '../formats/types';
|
||||
import {
|
||||
DISK_FORMATS,
|
||||
DriveNumber,
|
||||
DRIVE_NUMBERS,
|
||||
JSONDisk
|
||||
} from '../formats/types';
|
||||
import { initGamepad } from './gamepad';
|
||||
import KeyBoard from './keyboard';
|
||||
import Tape, { TAPE_TYPES } from './tape';
|
||||
import type { GamepadConfiguration } from './types';
|
||||
import Tape from './tape';
|
||||
import { gup, hup } from './url';
|
||||
|
||||
import ApplesoftDump from '../applesoft/decompiler';
|
||||
import ApplesoftCompiler from '../applesoft/compiler';
|
||||
|
@ -32,6 +29,9 @@ import { Screen, SCREEN_FULL_PAGE } from './screen';
|
|||
import { JoyStick } from './joystick';
|
||||
import { System } from './system';
|
||||
|
||||
import { Disk2UI } from './disk2';
|
||||
import { BlockStorageUI } from './block_storage';
|
||||
|
||||
let paused = false;
|
||||
|
||||
let startTime = Date.now();
|
||||
|
@ -43,54 +43,22 @@ let hashtag = document.location.hash;
|
|||
|
||||
const optionsModal = new OptionsModal();
|
||||
|
||||
interface DiskDescriptor {
|
||||
name: string;
|
||||
disk?: number;
|
||||
filename: string;
|
||||
e?: boolean;
|
||||
category: string;
|
||||
}
|
||||
|
||||
type DiskCollection = {
|
||||
[name: string]: DiskDescriptor[]
|
||||
};
|
||||
|
||||
const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
|
||||
const BIN_TYPES = ['bin'];
|
||||
|
||||
const KNOWN_FILE_TYPES = [
|
||||
...DISK_FORMATS,
|
||||
...TAPE_TYPES,
|
||||
...BIN_TYPES,
|
||||
] as readonly string[];
|
||||
|
||||
const disk_categories: DiskCollection = { 'Local Saves': [] };
|
||||
const disk_sets: DiskCollection = {};
|
||||
// Disk names
|
||||
const disk_cur_name: string[] = [];
|
||||
// Disk categories
|
||||
const disk_cur_cat: string[] = [];
|
||||
|
||||
let _apple2: Apple2;
|
||||
let cpu: CPU6502;
|
||||
let stats: Stats;
|
||||
let vm: VideoModes;
|
||||
let tape: Tape;
|
||||
let _disk2: DiskII;
|
||||
let _massStorage: MassStorage;
|
||||
let _printer: Printer;
|
||||
let audio: Audio;
|
||||
let screen: Screen;
|
||||
let joystick: JoyStick;
|
||||
let system: System;
|
||||
let keyboard: KeyBoard;
|
||||
let io: Apple2IO;
|
||||
let _currentDrive: DriveNumber = 1;
|
||||
let _e: boolean;
|
||||
|
||||
let ready: Promise<[void, void]>;
|
||||
|
||||
export const driveLights = new DriveLights();
|
||||
export const diskLights = new DriveLights('disk');
|
||||
export const BlockStorageLights = new DriveLights('mass-storage');
|
||||
|
||||
export function dumpAppleSoftProgram() {
|
||||
const dumper = new ApplesoftDump(cpu);
|
||||
|
@ -103,401 +71,9 @@ export function compileAppleSoftProgram(program: string) {
|
|||
dumpAppleSoftProgram();
|
||||
}
|
||||
|
||||
export function openLoad(driveString: string, event: MouseEvent) {
|
||||
const drive = parseInt(driveString, 10) as DriveNumber;
|
||||
_currentDrive = drive;
|
||||
if (event.metaKey && includes(DRIVE_NUMBERS, drive)) {
|
||||
openLoadHTTP();
|
||||
} else {
|
||||
if (disk_cur_cat[drive]) {
|
||||
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||
element.value = disk_cur_cat[drive];
|
||||
selectCategory();
|
||||
}
|
||||
MicroModal.show('load-modal');
|
||||
}
|
||||
}
|
||||
|
||||
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 a = document.querySelector<HTMLAnchorElement>('#local_save_link')!;
|
||||
|
||||
if (!data) {
|
||||
alert('No data from drive ' + drive);
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([data], { 'type': mimeType });
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = driveLights.label(drive) + '.dsk';
|
||||
|
||||
if (event.metaKey) {
|
||||
dumpDisk(drive);
|
||||
} else {
|
||||
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
||||
saveName.value = driveLights.label(drive);
|
||||
MicroModal.show('save-modal');
|
||||
}
|
||||
}
|
||||
|
||||
export function openAlert(msg: string) {
|
||||
const el = document.querySelector<HTMLDivElement>('#alert-modal .message')!;
|
||||
el.innerText = msg;
|
||||
MicroModal.show('alert-modal');
|
||||
}
|
||||
|
||||
/********************************************************************
|
||||
*
|
||||
* Drag and Drop
|
||||
*/
|
||||
|
||||
export function handleDragOver(_drive: number, event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
export function handleDragEnd(_drive: number, event: DragEvent) {
|
||||
const dt = event.dataTransfer!;
|
||||
if (dt.items) {
|
||||
for (let i = 0; i < dt.items.length; i++) {
|
||||
dt.items.remove(i);
|
||||
}
|
||||
} else {
|
||||
dt.clearData();
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDrop(drive: number, event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (drive < 1) {
|
||||
if (!_disk2.getMetadata(1)) {
|
||||
drive = 1;
|
||||
} else if (!_disk2.getMetadata(2)) {
|
||||
drive = 2;
|
||||
} else {
|
||||
drive = 1;
|
||||
}
|
||||
}
|
||||
const dt = event.dataTransfer!;
|
||||
if (dt.files.length == 1) {
|
||||
const runOnLoad = event.shiftKey;
|
||||
doLoadLocal(drive as DriveNumber, dt.files[0], { runOnLoad });
|
||||
} else if (dt.files.length == 2) {
|
||||
doLoadLocal(1, dt.files[0]);
|
||||
doLoadLocal(2, dt.files[1]);
|
||||
} else {
|
||||
for (let idx = 0; idx < dt.items.length; idx++) {
|
||||
if (dt.items[idx].type === 'text/uri-list') {
|
||||
dt.items[idx].getAsString(function (url) {
|
||||
const parts = hup().split('|');
|
||||
parts[drive - 1] = url;
|
||||
document.location.hash = parts.join('|');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadingStart() {
|
||||
const meter = document.querySelector<HTMLDivElement>('#loading-modal .meter')!;
|
||||
meter.style.display = 'none';
|
||||
MicroModal.show('loading-modal');
|
||||
}
|
||||
|
||||
function loadingProgress(current: number, total: number) {
|
||||
if (total) {
|
||||
const meter = document.querySelector<HTMLDivElement>('#loading-modal .meter')!;
|
||||
const progress = document.querySelector<HTMLDivElement>('#loading-modal .progress')!;
|
||||
meter.style.display = 'block';
|
||||
progress.style.width = current / total * meter.clientWidth + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function loadingStop() {
|
||||
MicroModal.close('loading-modal');
|
||||
|
||||
if (!paused) {
|
||||
ready.then(() => {
|
||||
_apple2.run();
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
interface JSONBinaryImage {
|
||||
type: 'binary',
|
||||
start: word,
|
||||
length: word,
|
||||
data: byte[],
|
||||
gamepad?: GamepadConfiguration,
|
||||
}
|
||||
|
||||
export function loadAjax(drive: DriveNumber, url: string) {
|
||||
loadingStart();
|
||||
|
||||
fetch(url).then(function (response: Response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error('Error loading: ' + response.statusText);
|
||||
}
|
||||
}).then(function (data: JSONDisk | JSONBinaryImage) {
|
||||
if (data.type === 'binary') {
|
||||
loadBinary(data as JSONBinaryImage);
|
||||
} else if (includes(DISK_FORMATS, data.type)) {
|
||||
loadDisk(drive, data);
|
||||
}
|
||||
initGamepad(data.gamepad);
|
||||
loadingStop();
|
||||
}).catch(function (error) {
|
||||
loadingStop();
|
||||
openAlert(error.message);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
export function doLoad(event: MouseEvent|KeyboardEvent) {
|
||||
MicroModal.close('load-modal');
|
||||
const select = document.querySelector<HTMLSelectElement>('#disk_select')!;
|
||||
const urls = select.value;
|
||||
let url;
|
||||
if (urls && urls.length) {
|
||||
if (typeof (urls) == 'string') {
|
||||
url = urls;
|
||||
} else {
|
||||
url = urls[0];
|
||||
}
|
||||
}
|
||||
|
||||
const localFile = document.querySelector<HTMLInputElement>('#local_file')!;
|
||||
const files = localFile.files;
|
||||
if (files && files.length == 1) {
|
||||
const runOnLoad = event.shiftKey;
|
||||
doLoadLocal(_currentDrive, files[0], { runOnLoad });
|
||||
} else if (url) {
|
||||
let filename;
|
||||
MicroModal.close('load-modal');
|
||||
if (url.substr(0, 6) == 'local:') {
|
||||
filename = url.substr(6);
|
||||
if (filename == '__manage') {
|
||||
openManage();
|
||||
} else {
|
||||
loadLocalStorage(_currentDrive, filename);
|
||||
}
|
||||
} else {
|
||||
const r1 = /json\/disks\/(.*).json$/.exec(url);
|
||||
if (r1) {
|
||||
filename = r1[1];
|
||||
} else {
|
||||
filename = url;
|
||||
}
|
||||
const parts = hup().split('|');
|
||||
parts[_currentDrive - 1] = filename;
|
||||
document.location.hash = parts.join('|');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function doSave() {
|
||||
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
||||
const name = saveName.value;
|
||||
saveLocalStorage(_currentDrive, name);
|
||||
MicroModal.close('save-modal');
|
||||
window.setTimeout(() => openAlert('Saved'), 0);
|
||||
}
|
||||
|
||||
export function doDelete(name: string) {
|
||||
if (window.confirm('Delete ' + name + '?')) {
|
||||
deleteLocalStorage(name);
|
||||
}
|
||||
}
|
||||
|
||||
interface LoadOptions {
|
||||
address?: word,
|
||||
runOnLoad?: boolean,
|
||||
}
|
||||
|
||||
function doLoadLocal(drive: DriveNumber, file: File, options: Partial<LoadOptions> = {}) {
|
||||
const parts = file.name.split('.');
|
||||
const ext = parts[parts.length - 1].toLowerCase();
|
||||
const matches = file.name.match(CIDERPRESS_EXTENSION);
|
||||
let type, aux;
|
||||
if (matches && matches.length === 3) {
|
||||
[, type, aux] = matches;
|
||||
}
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
doLoadLocalDisk(drive, file);
|
||||
} else if (includes(TAPE_TYPES, ext)) {
|
||||
tape.doLoadLocalTape(file);
|
||||
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
|
||||
const address = aux !== undefined ? parseInt(aux, 16) : undefined;
|
||||
doLoadBinary(file, { address, ...options });
|
||||
} else {
|
||||
const addressInput = document.querySelector<HTMLInputElement>('#local_file_address');
|
||||
const addressStr = addressInput?.value;
|
||||
if (addressStr) {
|
||||
const address = parseInt(addressStr, 16);
|
||||
if (isNaN(address)) {
|
||||
openAlert('Invalid address: ' + addressStr);
|
||||
return;
|
||||
}
|
||||
doLoadBinary(file, { address, ...options });
|
||||
} else {
|
||||
openAlert('Unknown file type: ' + ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doLoadBinary(file: File, options: LoadOptions) {
|
||||
loadingStart();
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function () {
|
||||
const result = this.result as ArrayBuffer;
|
||||
let { address } = options;
|
||||
address = address ?? 0x2000;
|
||||
const bytes = new Uint8Array(result);
|
||||
for (let idx = 0; idx < result.byteLength; idx++) {
|
||||
cpu.write(address >> 8, address & 0xff, bytes[idx]);
|
||||
address++;
|
||||
}
|
||||
if (options.runOnLoad) {
|
||||
cpu.reset();
|
||||
cpu.setPC(address);
|
||||
}
|
||||
loadingStop();
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
function doLoadLocalDisk(drive: DriveNumber, file: File) {
|
||||
loadingStart();
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function () {
|
||||
const result = this.result as ArrayBuffer;
|
||||
const parts = file.name.split('.');
|
||||
const ext = parts.pop()!.toLowerCase();
|
||||
const name = parts.join('.');
|
||||
|
||||
// Remove any json file reference
|
||||
const files = hup().split('|');
|
||||
files[drive - 1] = '';
|
||||
document.location.hash = files.join('|');
|
||||
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (result.byteLength >= 800 * 1024) {
|
||||
if (
|
||||
includes(BLOCK_FORMATS, ext) &&
|
||||
_massStorage.setBinary(drive, name, ext, result)
|
||||
) {
|
||||
initGamepad();
|
||||
} else {
|
||||
openAlert(`Unable to load ${name}`);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
includes(NIBBLE_FORMATS, ext) &&
|
||||
_disk2.setBinary(drive, name, ext, result)
|
||||
) {
|
||||
initGamepad();
|
||||
} else {
|
||||
openAlert(`Unable to load ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
loadingStop();
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
export function doLoadHTTP(drive: DriveNumber, url?: string) {
|
||||
if (!url) {
|
||||
MicroModal.close('http-modal');
|
||||
}
|
||||
|
||||
loadingStart();
|
||||
const input = document.querySelector<HTMLInputElement>('#http_url')!;
|
||||
url = url || input.value;
|
||||
if (url) {
|
||||
fetch(url).then(function (response) {
|
||||
if (response.ok) {
|
||||
const reader = response!.body!.getReader();
|
||||
let received = 0;
|
||||
const chunks: Uint8Array[] = [];
|
||||
const contentLength = parseInt(response.headers.get('content-length')!, 10);
|
||||
|
||||
return reader.read().then(
|
||||
function readChunk(result): Promise<ArrayBufferLike> {
|
||||
if (result.done) {
|
||||
const data = new Uint8Array(received);
|
||||
let offset = 0;
|
||||
for (let idx = 0; idx < chunks.length; idx++) {
|
||||
data.set(chunks[idx], offset);
|
||||
offset += chunks[idx].length;
|
||||
}
|
||||
return Promise.resolve(data.buffer);
|
||||
}
|
||||
|
||||
received += result.value.length;
|
||||
if (contentLength) {
|
||||
loadingProgress(received, contentLength);
|
||||
}
|
||||
chunks.push(result.value);
|
||||
|
||||
return reader.read().then(readChunk);
|
||||
});
|
||||
} else {
|
||||
throw new Error('Error loading: ' + response.statusText);
|
||||
}
|
||||
}).then(function (data) {
|
||||
const urlParts = url!.split('/');
|
||||
const file = urlParts.pop()!;
|
||||
const fileParts = file.split('.');
|
||||
const ext = fileParts.pop()!.toLowerCase();
|
||||
const name = decodeURIComponent(fileParts.join('.'));
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (data.byteLength >= 800 * 1024) {
|
||||
if (
|
||||
includes(BLOCK_FORMATS, ext) &&
|
||||
_massStorage.setBinary(drive, name, ext, data)
|
||||
) {
|
||||
initGamepad();
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
includes(NIBBLE_FORMATS, ext) &&
|
||||
_disk2.setBinary(drive, name, ext, data)
|
||||
) {
|
||||
initGamepad();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Extension ${ext} not recognized.`);
|
||||
}
|
||||
loadingStop();
|
||||
}).catch(function (error) {
|
||||
loadingStop();
|
||||
openAlert(error.message);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openLoadHTTP() {
|
||||
MicroModal.show('http-modal');
|
||||
}
|
||||
|
||||
function openManage() {
|
||||
MicroModal.show('manage-modal');
|
||||
}
|
||||
|
||||
let showStats = 0;
|
||||
|
||||
export function updateKHz() {
|
||||
function updateKHz() {
|
||||
const now = Date.now();
|
||||
const ms = now - startTime;
|
||||
const cycles = cpu.getCycles();
|
||||
|
@ -557,195 +133,10 @@ function updateSoundButton(on: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
function dumpDisk(drive: DriveNumber) {
|
||||
const wind = window.open('', '_blank')!;
|
||||
wind.document.title = driveLights.label(drive);
|
||||
wind.document.write('<pre>');
|
||||
wind.document.write(_disk2.getJSON(drive, true));
|
||||
wind.document.write('</pre>');
|
||||
wind.document.close();
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
_apple2.reset();
|
||||
}
|
||||
|
||||
function loadBinary(bin: JSONBinaryImage) {
|
||||
const maxLen = Math.min(bin.length, 0x10000 - bin.start);
|
||||
for (let idx = 0; idx < maxLen; idx++) {
|
||||
const pos = bin.start + idx;
|
||||
cpu.write(pos, bin.data[idx]);
|
||||
}
|
||||
cpu.reset();
|
||||
cpu.setPC(bin.start);
|
||||
}
|
||||
|
||||
export function selectCategory() {
|
||||
const diskSelect = document.querySelector<HTMLSelectElement>('#disk_select')!;
|
||||
const categorySelect = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||
diskSelect.innerHTML = '';
|
||||
const cat = disk_categories[categorySelect.value];
|
||||
if (cat) {
|
||||
for (let idx = 0; idx < cat.length; idx++) {
|
||||
const file = cat[idx];
|
||||
let name = file.name;
|
||||
if (file.disk) {
|
||||
name += ' - ' + file.disk;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = file.filename;
|
||||
option.innerText = name;
|
||||
diskSelect.append(option);
|
||||
if (disk_cur_name[_currentDrive] === name) {
|
||||
option.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function selectDisk() {
|
||||
const localFile = document.querySelector<HTMLInputElement>('#local_file')!;
|
||||
localFile.value = '';
|
||||
}
|
||||
|
||||
export function clickDisk(event: MouseEvent|KeyboardEvent) {
|
||||
doLoad(event);
|
||||
}
|
||||
|
||||
/** Called to load disks from the local catalog. */
|
||||
function loadDisk(drive: DriveNumber, disk: JSONDisk) {
|
||||
let name = disk.name;
|
||||
const category = disk.category!; // all disks in the local catalog have a category
|
||||
|
||||
if (disk.disk) {
|
||||
name += ' - ' + disk.disk;
|
||||
}
|
||||
|
||||
disk_cur_cat[drive] = category;
|
||||
disk_cur_name[drive] = name;
|
||||
|
||||
_disk2.setDisk(drive, disk);
|
||||
initGamepad(disk.gamepad);
|
||||
}
|
||||
|
||||
/*
|
||||
* LocalStorage Disk Storage
|
||||
*/
|
||||
|
||||
function updateLocalStorage() {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
|
||||
const names = Object.keys(diskIndex);
|
||||
|
||||
const cat: DiskDescriptor[] = disk_categories['Local Saves'] = [];
|
||||
const contentDiv = document.querySelector<HTMLDivElement>('#manage-modal-content')!;
|
||||
contentDiv.innerHTML = '';
|
||||
|
||||
names.forEach(function (name) {
|
||||
cat.push({
|
||||
'category': 'Local Saves',
|
||||
'name': name,
|
||||
'filename': 'local:' + name
|
||||
});
|
||||
contentDiv.innerHTML =
|
||||
'<span class="local_save">' +
|
||||
name +
|
||||
' <a href="#" onclick="Apple2.doDelete(\'' +
|
||||
name +
|
||||
'\')">Delete</a><br /></span>';
|
||||
});
|
||||
cat.push({
|
||||
'category': 'Local Saves',
|
||||
'name': 'Manage Saves...',
|
||||
'filename': 'local:__manage'
|
||||
});
|
||||
}
|
||||
|
||||
type LocalDiskIndex = {
|
||||
[name: string]: string,
|
||||
}
|
||||
|
||||
function saveLocalStorage(drive: DriveNumber, name: string) {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex;
|
||||
|
||||
const json = _disk2.getJSON(drive);
|
||||
diskIndex[name] = json;
|
||||
|
||||
window.localStorage.diskIndex = JSON.stringify(diskIndex);
|
||||
|
||||
driveLights.label(drive, name);
|
||||
driveLights.dirty(drive, false);
|
||||
updateLocalStorage();
|
||||
}
|
||||
|
||||
function deleteLocalStorage(name: string) {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex;
|
||||
if (diskIndex[name]) {
|
||||
delete diskIndex[name];
|
||||
openAlert('Deleted');
|
||||
}
|
||||
window.localStorage.diskIndex = JSON.stringify(diskIndex);
|
||||
updateLocalStorage();
|
||||
}
|
||||
|
||||
function loadLocalStorage(drive: DriveNumber, name: string) {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex;
|
||||
if (diskIndex[name]) {
|
||||
_disk2.setJSON(drive, diskIndex[name]);
|
||||
driveLights.label(drive, name);
|
||||
driveLights.dirty(drive, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.localStorage !== undefined) {
|
||||
const nodes = document.querySelectorAll<HTMLElement>('.disksave');
|
||||
nodes.forEach(function (el) {
|
||||
el.style.display = 'inline-block';
|
||||
});
|
||||
}
|
||||
|
||||
const categorySelect = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
disk_index: DiskDescriptor[];
|
||||
}
|
||||
}
|
||||
|
||||
function buildDiskIndex() {
|
||||
let oldCat = '';
|
||||
let option;
|
||||
for (let idx = 0; idx < window.disk_index.length; idx++) {
|
||||
const file = window.disk_index[idx];
|
||||
const cat = file.category;
|
||||
const name = file.name;
|
||||
const disk = file.disk;
|
||||
if (file.e && !_e) {
|
||||
continue;
|
||||
}
|
||||
if (cat != oldCat) {
|
||||
option = document.createElement('option');
|
||||
option.value = cat;
|
||||
option.innerText = cat;
|
||||
categorySelect.append(option);
|
||||
|
||||
disk_categories[cat] = [];
|
||||
oldCat = cat;
|
||||
}
|
||||
disk_categories[cat].push(file);
|
||||
if (disk) {
|
||||
if (!disk_sets[name]) {
|
||||
disk_sets[name] = [];
|
||||
}
|
||||
disk_sets[name].push(file);
|
||||
}
|
||||
}
|
||||
option = document.createElement('option');
|
||||
option.innerText = 'Local Saves';
|
||||
categorySelect.append(option);
|
||||
|
||||
updateLocalStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the URL fragment. It is expected to be of the form:
|
||||
* `disk1|disk2` where each disk is the name of a local image OR
|
||||
|
@ -763,12 +154,14 @@ function processHash(hash: string) {
|
|||
const parts = file.split('.');
|
||||
const ext = parts[parts.length - 1].toLowerCase();
|
||||
if (ext == 'json') {
|
||||
loadAjax(drive, file);
|
||||
} else {
|
||||
doLoadHTTP(drive, file);
|
||||
window.Disk2UI.loadAjax(drive, file);
|
||||
} else if (includes(NIBBLE_FORMATS, ext)) {
|
||||
window.Disk2UI.doLoadHTTP(drive, file);
|
||||
} else if (includes(BLOCK_FORMATS, ext)) {
|
||||
window.BlockStorageUI.doLoadHTTP(drive, file);
|
||||
}
|
||||
} else if (file) {
|
||||
loadAjax(drive, 'json/disks/' + file + '.json');
|
||||
window.Disk2UI.loadAjax(drive, 'json/disks/' + file + '.json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -799,25 +192,6 @@ export function pauseRun() {
|
|||
paused = !paused;
|
||||
}
|
||||
|
||||
export function openOptions() {
|
||||
optionsModal.openModal();
|
||||
}
|
||||
|
||||
export function openPrinterModal() {
|
||||
const mimeType = 'application/octet-stream';
|
||||
const data = _printer.getRawOutput();
|
||||
const a = document.querySelector<HTMLAnchorElement>('#raw_printer_output')!;
|
||||
|
||||
const blob = new Blob([data], { 'type': mimeType });
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = 'raw_printer_output.bin';
|
||||
MicroModal.show('printer-modal');
|
||||
}
|
||||
|
||||
export function clearPrinterPaper() {
|
||||
_printer.clear();
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
clipboardData?: DataTransfer;
|
||||
|
@ -830,40 +204,22 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a query parameter or the empty string if it does not
|
||||
* exist.
|
||||
* @param name the parameter name. Note that `name` must not have any RegExp
|
||||
* meta-characters except '[' and ']' or it will fail.
|
||||
*/
|
||||
|
||||
function gup(name: string) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(name);
|
||||
declare global {
|
||||
interface Window {
|
||||
Disk2UI: Disk2UI
|
||||
OptionsModal: OptionsModal
|
||||
BlockStorageUI: BlockStorageUI
|
||||
Printer: Printer
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the URL fragment. */
|
||||
function hup() {
|
||||
const regex = new RegExp('#(.*)');
|
||||
const hash = decodeURIComponent(window.location.hash);
|
||||
const results = regex.exec(hash);
|
||||
if (!results)
|
||||
return '';
|
||||
else
|
||||
return results[1];
|
||||
}
|
||||
|
||||
function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) {
|
||||
function onLoaded(apple2: Apple2, disk2: DiskII, BlockStorage: BlockStorage, printer: Printer, e: boolean) {
|
||||
_apple2 = apple2;
|
||||
cpu = _apple2.getCPU();
|
||||
io = _apple2.getIO();
|
||||
stats = apple2.getStats();
|
||||
vm = apple2.getVideoModes();
|
||||
tape = new Tape(io);
|
||||
_disk2 = disk2;
|
||||
_massStorage = massStorage;
|
||||
_printer = printer;
|
||||
_e = e;
|
||||
|
||||
system = new System(io, e);
|
||||
optionsModal.addOptions(system);
|
||||
|
@ -878,7 +234,15 @@ function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, print
|
|||
optionsModal.addOptions(audio);
|
||||
initSoundToggle();
|
||||
|
||||
ready = Promise.all([audio.ready, apple2.ready]);
|
||||
window.Disk2UI = new Disk2UI(cpu, disk2, tape, e);
|
||||
window.BlockStorageUI = new BlockStorageUI(BlockStorage);
|
||||
window.OptionsModal = optionsModal;
|
||||
window.Printer = printer;
|
||||
|
||||
ready = Promise.all([
|
||||
audio.ready,
|
||||
apple2.ready
|
||||
]);
|
||||
|
||||
MicroModal.init();
|
||||
|
||||
|
@ -906,8 +270,6 @@ function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, print
|
|||
}
|
||||
});
|
||||
|
||||
buildDiskIndex();
|
||||
|
||||
/*
|
||||
* Input Handling
|
||||
*/
|
||||
|
@ -935,33 +297,15 @@ function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, print
|
|||
|
||||
const hash = gup('disk') || hup();
|
||||
if (hash) {
|
||||
_apple2.stop();
|
||||
processHash(hash);
|
||||
} else {
|
||||
ready.then(() => {
|
||||
_apple2.run();
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
document.querySelector<HTMLInputElement>('#local_file')?.addEventListener(
|
||||
'change',
|
||||
(event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const address = document.querySelector<HTMLInputElement>('#local_file_address_input')!;
|
||||
const parts = target.value.split('.');
|
||||
const ext = parts[parts.length - 1];
|
||||
|
||||
if (KNOWN_FILE_TYPES.includes(ext)) {
|
||||
address.style.display = 'none';
|
||||
} else {
|
||||
address.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
);
|
||||
ready.then(() => {
|
||||
_apple2.run();
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
export function initUI(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) {
|
||||
export function initUI(apple2: Apple2, disk2: DiskII, BlockStorage: BlockStorage, printer: Printer, e: boolean) {
|
||||
window.addEventListener('load', () => {
|
||||
onLoaded(apple2, disk2, massStorage, printer, e);
|
||||
onLoaded(apple2, disk2, BlockStorage, printer, e);
|
||||
});
|
||||
}
|
||||
|
|
47
js/ui/block_storage.ts
Normal file
47
js/ui/block_storage.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import MicroModal from 'micromodal';
|
||||
|
||||
import { includes } from '../types';
|
||||
import {
|
||||
BlockFormat,
|
||||
BLOCK_FORMATS,
|
||||
DriveNumber,
|
||||
DiskStorage,
|
||||
} from '../formats/types';
|
||||
import { openAlert } from './alert';
|
||||
import { StorageUI } from './storage';
|
||||
|
||||
export class BlockStorageUI extends StorageUI {
|
||||
constructor(BlockStorage: DiskStorage<BlockFormat>) {
|
||||
super(BlockStorage, BLOCK_FORMATS);
|
||||
}
|
||||
|
||||
doLoadLocal(drive: DriveNumber, file: File) {
|
||||
const parts = file.name.split('.');
|
||||
const ext = parts[parts.length - 1].toLowerCase();
|
||||
if (includes(BLOCK_FORMATS, ext)) {
|
||||
this.doLoadLocalDisk(drive, file);
|
||||
} else {
|
||||
openAlert('Unknown file type: ' + ext);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Mass Storage Load Dialog Methods
|
||||
//
|
||||
|
||||
openLoad(driveString: string) {
|
||||
const drive = parseInt(driveString, 10) as DriveNumber;
|
||||
this.currentDrive = drive;
|
||||
MicroModal.show('mass-storage-modal');
|
||||
}
|
||||
|
||||
doLoad() {
|
||||
MicroModal.close('mass-storage-modal');
|
||||
|
||||
const localFile = document.querySelector<HTMLInputElement>('#mass-storage-file')!;
|
||||
const files = localFile.files;
|
||||
if (files && files.length == 1) {
|
||||
this.doLoadLocal(this.currentDrive, files[0]);
|
||||
}
|
||||
}
|
||||
}
|
438
js/ui/disk2.ts
Normal file
438
js/ui/disk2.ts
Normal file
|
@ -0,0 +1,438 @@
|
|||
import MicroModal from 'micromodal';
|
||||
import { includes, word } from '../types';
|
||||
import {
|
||||
DISK_FORMATS,
|
||||
DriveNumber,
|
||||
DRIVE_NUMBERS,
|
||||
JSONDisk,
|
||||
NIBBLE_FORMATS,
|
||||
} from '../formats/types';
|
||||
import Tape, { TAPE_TYPES } from './tape';
|
||||
import DiskII from 'js/cards/disk2';
|
||||
import CPU6502 from 'js/cpu6502';
|
||||
import { openAlert } from './alert';
|
||||
import { initGamepad } from './gamepad';
|
||||
import {
|
||||
loadingStart,
|
||||
loadingStop
|
||||
} from './loader';
|
||||
import { StorageUI } from './storage';
|
||||
import { hup } from './url';
|
||||
|
||||
const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
|
||||
const BIN_TYPES = ['bin'];
|
||||
|
||||
const KNOWN_FILE_TYPES = [
|
||||
...DISK_FORMATS,
|
||||
...TAPE_TYPES,
|
||||
...BIN_TYPES,
|
||||
] as readonly string[];
|
||||
|
||||
export interface LoadOptions {
|
||||
address?: word,
|
||||
runOnLoad?: boolean,
|
||||
}
|
||||
|
||||
type LocalDiskIndex = {
|
||||
[name: string]: string,
|
||||
}
|
||||
|
||||
interface DiskDescriptor {
|
||||
name: string;
|
||||
disk?: number;
|
||||
filename: string;
|
||||
e?: boolean;
|
||||
category: string;
|
||||
}
|
||||
|
||||
type DiskCollection = {
|
||||
[name: string]: DiskDescriptor[]
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
disk_index: DiskDescriptor[];
|
||||
}
|
||||
}
|
||||
|
||||
export class Disk2UI extends StorageUI {
|
||||
currentDrive: DriveNumber = 1;
|
||||
|
||||
disk_sets: DiskCollection = {};
|
||||
disk_categories: DiskCollection = { 'Local Saves': [] };
|
||||
// Disk names
|
||||
disk_cur_name: string[] = [];
|
||||
// Disk categories
|
||||
disk_cur_cat: string[] = [];
|
||||
|
||||
constructor(
|
||||
private cpu: CPU6502,
|
||||
private disk2: DiskII,
|
||||
private tape: Tape,
|
||||
private e: boolean,
|
||||
) {
|
||||
super(disk2, NIBBLE_FORMATS);
|
||||
this.buildDiskIndex();
|
||||
|
||||
document.querySelector<HTMLInputElement>('#local_file')?.addEventListener(
|
||||
'change',
|
||||
(event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const address = document.querySelector<HTMLInputElement>('#local_file_address_input')!;
|
||||
const parts = target.value.split('.');
|
||||
const ext = parts[parts.length - 1];
|
||||
|
||||
if (KNOWN_FILE_TYPES.includes(ext)) {
|
||||
address.style.display = 'none';
|
||||
} else {
|
||||
address.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
doLoadLocal(drive: DriveNumber, file: File, options: LoadOptions = {}) {
|
||||
const parts = file.name.split('.');
|
||||
const ext = parts[parts.length - 1].toLowerCase();
|
||||
const matches = file.name.match(CIDERPRESS_EXTENSION);
|
||||
let type, aux;
|
||||
if (matches && matches.length === 3) {
|
||||
[, type, aux] = matches;
|
||||
}
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
this.doLoadLocalDisk(drive, file);
|
||||
} else if (includes(TAPE_TYPES, ext)) {
|
||||
this.tape.doLoadLocalTape(file);
|
||||
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
|
||||
const address = aux !== undefined ? parseInt(aux, 16) : undefined;
|
||||
this.doLoadBinary(file, { address, ...options });
|
||||
} else {
|
||||
const addressInput = document.querySelector<HTMLInputElement>('#local_file_address');
|
||||
const addressStr = addressInput?.value;
|
||||
if (addressStr) {
|
||||
const address = parseInt(addressStr, 16);
|
||||
if (isNaN(address)) {
|
||||
openAlert('Invalid address: ' + addressStr);
|
||||
return;
|
||||
}
|
||||
this.doLoadBinary(file, { address, ...options });
|
||||
} else {
|
||||
openAlert('Unknown file type: ' + ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doLoadBinary(file: File, options: LoadOptions) {
|
||||
loadingStart();
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
const result = fileReader.result as ArrayBuffer;
|
||||
let { address } = options;
|
||||
address = address ?? 0x2000;
|
||||
const bytes = new Uint8Array(result);
|
||||
for (let idx = 0; idx < result.byteLength; idx++) {
|
||||
this.cpu.write(address >> 8, address & 0xff, bytes[idx]);
|
||||
address++;
|
||||
}
|
||||
if (options.runOnLoad) {
|
||||
this.cpu.reset();
|
||||
this.cpu.setPC(address);
|
||||
}
|
||||
loadingStop();
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
//
|
||||
// Disk II Save Methods
|
||||
//
|
||||
|
||||
openSave(drive: DriveNumber, event: MouseEvent) {
|
||||
const mimeType = 'application/octet-stream';
|
||||
const data = this.disk2.getBinary(drive);
|
||||
const { name } = this.disk2.getMetadata(drive);
|
||||
const a = document.querySelector<HTMLAnchorElement>('#local_save_link')!;
|
||||
|
||||
if (!data) {
|
||||
alert('No data from drive ' + drive);
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([data], { 'type': mimeType });
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = name + '.dsk';
|
||||
|
||||
if (event.metaKey) {
|
||||
this.dumpDisk(drive);
|
||||
} else {
|
||||
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
||||
saveName.value = name;
|
||||
MicroModal.show('save-modal');
|
||||
}
|
||||
}
|
||||
|
||||
private dumpDisk(drive: DriveNumber) {
|
||||
const { name } = this.disk2.getMetadata(drive);
|
||||
const wind = window.open('', '_blank')!;
|
||||
wind.document.title = name;
|
||||
wind.document.write('<pre>');
|
||||
wind.document.write(this.disk2.getJSON(drive, true));
|
||||
wind.document.write('</pre>');
|
||||
wind.document.close();
|
||||
}
|
||||
|
||||
//
|
||||
// Disk II Load Dialog Methods
|
||||
//
|
||||
|
||||
openLoad(drive: DriveNumber, event: MouseEvent) {
|
||||
this.currentDrive = drive;
|
||||
if (event.metaKey && includes(DRIVE_NUMBERS, drive)) {
|
||||
this.openLoadHTTP();
|
||||
} else {
|
||||
if (this.disk_cur_cat[drive]) {
|
||||
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||
element.value = this.disk_cur_cat[drive];
|
||||
this.selectCategory();
|
||||
}
|
||||
MicroModal.show('load-modal');
|
||||
}
|
||||
}
|
||||
|
||||
selectCategory() {
|
||||
const diskSelect = document.querySelector<HTMLSelectElement>('#disk_select')!;
|
||||
const categorySelect = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||
diskSelect.innerHTML = '';
|
||||
const cat = this.disk_categories[categorySelect.value];
|
||||
if (cat) {
|
||||
for (let idx = 0; idx < cat.length; idx++) {
|
||||
const file = cat[idx];
|
||||
let name = file.name;
|
||||
if (file.disk) {
|
||||
name += ' - ' + file.disk;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = file.filename;
|
||||
option.innerText = name;
|
||||
diskSelect.append(option);
|
||||
if (this.disk_cur_name[this.currentDrive] === name) {
|
||||
option.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called to load disks from the local catalog. */
|
||||
loadDisk(drive: DriveNumber, disk: JSONDisk) {
|
||||
let name = disk.name;
|
||||
const category = disk.category!; // all disks in the local catalog have a category
|
||||
|
||||
if (disk.disk) {
|
||||
name += ' - ' + disk.disk;
|
||||
}
|
||||
|
||||
this.disk_cur_cat[drive] = category;
|
||||
this.disk_cur_name[drive] = name;
|
||||
|
||||
this.disk2.setDisk(drive, disk);
|
||||
initGamepad(disk.gamepad);
|
||||
}
|
||||
|
||||
clickDisk(event: MouseEvent|KeyboardEvent) {
|
||||
this.doLoad(event);
|
||||
}
|
||||
|
||||
doLoad(event: MouseEvent|KeyboardEvent) {
|
||||
MicroModal.close('load-modal');
|
||||
const select = document.querySelector<HTMLSelectElement>('#disk_select')!;
|
||||
const urls = select.value;
|
||||
let url;
|
||||
if (urls && urls.length) {
|
||||
if (typeof (urls) == 'string') {
|
||||
url = urls;
|
||||
} else {
|
||||
url = urls[0];
|
||||
}
|
||||
}
|
||||
|
||||
const localFile = document.querySelector<HTMLInputElement>('#local_file')!;
|
||||
const files = localFile.files;
|
||||
if (files && files.length == 1) {
|
||||
const runOnLoad = event.shiftKey;
|
||||
this.doLoadLocal(this.currentDrive, files[0], { runOnLoad });
|
||||
} else if (url) {
|
||||
let filename;
|
||||
MicroModal.close('load-modal');
|
||||
if (url.substr(0, 6) == 'local:') {
|
||||
filename = url.substr(6);
|
||||
if (filename == '__manage') {
|
||||
this.openManage();
|
||||
} else {
|
||||
this.loadLocalStorage(this.currentDrive, filename);
|
||||
}
|
||||
} else {
|
||||
const r1 = /json\/disks\/(.*).json$/.exec(url);
|
||||
if (r1) {
|
||||
filename = r1[1];
|
||||
} else {
|
||||
filename = url;
|
||||
}
|
||||
const parts = hup().split('|');
|
||||
parts[this.currentDrive - 1] = filename;
|
||||
document.location.hash = parts.join('|');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Local Storage methods
|
||||
//
|
||||
|
||||
doSave() {
|
||||
const saveName = document.querySelector<HTMLInputElement>('#save_name')!;
|
||||
const name = saveName.value;
|
||||
this.saveLocalStorage(this.currentDrive, name);
|
||||
MicroModal.close('save-modal');
|
||||
window.setTimeout(() => openAlert('Saved'), 0);
|
||||
}
|
||||
|
||||
doDelete(name: string) {
|
||||
if (window.confirm('Delete ' + name + '?')) {
|
||||
this.deleteLocalStorage(name);
|
||||
}
|
||||
}
|
||||
|
||||
openManage() {
|
||||
MicroModal.show('manage-modal');
|
||||
}
|
||||
|
||||
private saveLocalStorage(drive: DriveNumber, name: string) {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex;
|
||||
|
||||
const json = this.disk2.getJSON(drive);
|
||||
diskIndex[name] = json;
|
||||
|
||||
window.localStorage.diskIndex = JSON.stringify(diskIndex);
|
||||
|
||||
this.updateLocalStorage();
|
||||
}
|
||||
|
||||
private deleteLocalStorage(name: string) {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex;
|
||||
if (diskIndex[name]) {
|
||||
delete diskIndex[name];
|
||||
openAlert('Deleted');
|
||||
}
|
||||
window.localStorage.diskIndex = JSON.stringify(diskIndex);
|
||||
this.updateLocalStorage();
|
||||
}
|
||||
|
||||
private loadLocalStorage(drive: DriveNumber, name: string) {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex;
|
||||
if (diskIndex[name]) {
|
||||
this.disk2.setJSON(drive, diskIndex[name]);
|
||||
}
|
||||
}
|
||||
|
||||
private updateLocalStorage() {
|
||||
const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
|
||||
const names = Object.keys(diskIndex);
|
||||
|
||||
const cat: DiskDescriptor[] = this.disk_categories['Local Saves'] = [];
|
||||
const contentDiv = document.querySelector<HTMLDivElement>('#manage-modal-content')!;
|
||||
contentDiv.innerHTML = '';
|
||||
|
||||
names.forEach(function (name) {
|
||||
cat.push({
|
||||
'category': 'Local Saves',
|
||||
'name': name,
|
||||
'filename': 'local:' + name
|
||||
});
|
||||
contentDiv.innerHTML =
|
||||
'<span class="local_save">' +
|
||||
name +
|
||||
' <a href="#" onclick="Apple2.doDelete(\'' +
|
||||
name +
|
||||
'\')">Delete</a><br /></span>';
|
||||
});
|
||||
cat.push({
|
||||
'category': 'Local Saves',
|
||||
'name': 'Manage Saves...',
|
||||
'filename': 'local:__manage'
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Disk II Modal
|
||||
//
|
||||
|
||||
private buildDiskIndex() {
|
||||
const categorySelect = document.querySelector<HTMLSelectElement>('#category_select')!;
|
||||
|
||||
let oldCat = '';
|
||||
let option;
|
||||
for (let idx = 0; idx < window.disk_index.length; idx++) {
|
||||
const file = window.disk_index[idx];
|
||||
const cat = file.category;
|
||||
const name = file.name;
|
||||
const disk = file.disk;
|
||||
if (file.e && !this.e) {
|
||||
continue;
|
||||
}
|
||||
if (cat != oldCat) {
|
||||
option = document.createElement('option');
|
||||
option.value = cat;
|
||||
option.innerText = cat;
|
||||
categorySelect.append(option);
|
||||
|
||||
this.disk_categories[cat] = [];
|
||||
oldCat = cat;
|
||||
}
|
||||
this.disk_categories[cat].push(file);
|
||||
if (disk) {
|
||||
if (!this.disk_sets[name]) {
|
||||
this.disk_sets[name] = [];
|
||||
}
|
||||
this.disk_sets[name].push(file);
|
||||
}
|
||||
}
|
||||
option = document.createElement('option');
|
||||
option.innerText = 'Local Saves';
|
||||
categorySelect.append(option);
|
||||
|
||||
this.updateLocalStorage();
|
||||
}
|
||||
|
||||
//
|
||||
// JSON loading
|
||||
//
|
||||
|
||||
loadAjax(drive: DriveNumber, url: string) {
|
||||
loadingStart();
|
||||
|
||||
fetch(url).then(function (response: Response) {
|
||||
loadingStop();
|
||||
if (response.ok && response.status == 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
openAlert('Error loading: ' + response.statusText);
|
||||
}
|
||||
}).then((data: JSONDisk | undefined) => {
|
||||
if (data) {
|
||||
if (includes(DISK_FORMATS, data.type)) {
|
||||
this.loadDisk(drive, data);
|
||||
}
|
||||
initGamepad(data.gamepad);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
loadingStop();
|
||||
openAlert(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
selectDisk() {
|
||||
const localFile = document.querySelector<HTMLInputElement>('#local_file')!;
|
||||
localFile.value = '';
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
import { Callbacks } from '../cards/disk2';
|
||||
import type { DriveNumber } from '../formats/types';
|
||||
import type { DriveCallbacks, DriveNumber } from '../formats/types';
|
||||
|
||||
export default class DriveLights implements DriveCallbacks {
|
||||
constructor(private prefix: string) {}
|
||||
|
||||
export default class DriveLights implements Callbacks {
|
||||
public driveLight(drive: DriveNumber, on: boolean) {
|
||||
const disk =
|
||||
document.querySelector('#disk' + drive)! as HTMLElement;
|
||||
disk.style.backgroundImage =
|
||||
on ? 'url(css/red-on-16.png)' :
|
||||
'url(css/red-off-16.png)';
|
||||
document.querySelector<HTMLElement>(`#${this.prefix}${drive}`);
|
||||
if (disk) {
|
||||
disk.style.backgroundImage =
|
||||
on ? 'url(css/red-on-16.png)' :
|
||||
'url(css/red-off-16.png)';
|
||||
}
|
||||
}
|
||||
|
||||
public dirty(_drive: DriveNumber, _dirty: boolean) {
|
||||
|
@ -16,10 +19,14 @@ export default class DriveLights implements Callbacks {
|
|||
|
||||
public label(drive: DriveNumber, label?: string, side?: string) {
|
||||
const labelElement =
|
||||
document.querySelector('#disk-label' + drive)! as HTMLElement;
|
||||
if (label) {
|
||||
labelElement.innerText = label + (side ? ` - ${side}` : '');
|
||||
document.querySelector<HTMLElement>(`#${this.prefix}-label${drive}`);
|
||||
if (labelElement) {
|
||||
if (label) {
|
||||
labelElement.innerText = label + (side ? ` - ${side}` : '');
|
||||
}
|
||||
return labelElement.innerText;
|
||||
} else {
|
||||
return `Disk ${drive}`;
|
||||
}
|
||||
return labelElement.innerText;
|
||||
}
|
||||
}
|
||||
|
|
20
js/ui/loader.ts
Normal file
20
js/ui/loader.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import MicroModal from 'micromodal';
|
||||
|
||||
export function loadingStart() {
|
||||
const meter = document.querySelector<HTMLDivElement>('#loading-modal .meter')!;
|
||||
meter.style.display = 'none';
|
||||
MicroModal.show('loading-modal');
|
||||
}
|
||||
|
||||
export function loadingProgress(current: number, total: number) {
|
||||
if (total) {
|
||||
const meter = document.querySelector<HTMLDivElement>('#loading-modal .meter')!;
|
||||
const progress = document.querySelector<HTMLDivElement>('#loading-modal .progress')!;
|
||||
meter.style.display = 'block';
|
||||
progress.style.width = current / total * meter.clientWidth + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
export function loadingStop() {
|
||||
MicroModal.close('loading-modal');
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { byte } from '../types';
|
||||
import MicroModal from 'micromodal';
|
||||
|
||||
/**
|
||||
* Printer UI. The "paper" is bound to the element selected by the input.
|
||||
|
@ -78,4 +79,15 @@ export default class Printer {
|
|||
getRawOutput() {
|
||||
return this._raw.slice(0, this._rawLen);
|
||||
}
|
||||
|
||||
openPrinterModal() {
|
||||
const mimeType = 'application/octet-stream';
|
||||
const data = this.getRawOutput();
|
||||
const a = document.querySelector<HTMLAnchorElement>('#raw_printer_output')!;
|
||||
|
||||
const blob = new Blob([data], { 'type': mimeType });
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = 'raw_printer_output.bin';
|
||||
MicroModal.show('printer-modal');
|
||||
}
|
||||
}
|
||||
|
|
165
js/ui/storage.ts
Normal file
165
js/ui/storage.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
import MicroModal from 'micromodal';
|
||||
import { includes } from '../types';
|
||||
import {
|
||||
BlockFormat,
|
||||
BLOCK_FORMATS,
|
||||
DriveNumber,
|
||||
NibbleFormat,
|
||||
NIBBLE_FORMATS,
|
||||
DiskStorage
|
||||
} from '../formats/types';
|
||||
import { openAlert } from './alert';
|
||||
import { initGamepad } from './gamepad';
|
||||
import {
|
||||
loadingProgress,
|
||||
loadingStart,
|
||||
loadingStop
|
||||
} from './loader';
|
||||
import { hup } from './url';
|
||||
import type { LoadOptions } from './disk2';
|
||||
|
||||
export class StorageUI {
|
||||
protected currentDrive: DriveNumber = 1;
|
||||
|
||||
constructor(
|
||||
private storage: DiskStorage<BlockFormat|NibbleFormat>,
|
||||
private formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS) {
|
||||
}
|
||||
|
||||
handleDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
handleDragEnd(event: DragEvent) {
|
||||
const dt = event.dataTransfer!;
|
||||
if (dt.items) {
|
||||
for (let i = 0; i < dt.items.length; i++) {
|
||||
dt.items.remove(i);
|
||||
}
|
||||
} else {
|
||||
dt.clearData();
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(drive: DriveNumber, event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (drive < 1) {
|
||||
if (!this.storage.getMetadata(1)) {
|
||||
drive = 1;
|
||||
} else if (!this.storage.getMetadata(2)) {
|
||||
drive = 2;
|
||||
} else {
|
||||
drive = 1;
|
||||
}
|
||||
}
|
||||
const dt = event.dataTransfer!;
|
||||
if (dt.files.length == 1) {
|
||||
const runOnLoad = event.shiftKey;
|
||||
this.doLoadLocal(drive, dt.files[0], { runOnLoad });
|
||||
} else if (dt.files.length == 2) {
|
||||
this.doLoadLocal(1, dt.files[0]);
|
||||
this.doLoadLocal(2, dt.files[1]);
|
||||
}
|
||||
}
|
||||
|
||||
doLoadLocalDisk(drive: DriveNumber, file: File) {
|
||||
loadingStart();
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
const result = fileReader.result as ArrayBuffer;
|
||||
const parts = file.name.split('.');
|
||||
const ext = parts.pop()!.toLowerCase();
|
||||
const name = parts.join('.');
|
||||
|
||||
// Remove any json file reference
|
||||
const files = hup().split('|');
|
||||
files[drive - 1] = '';
|
||||
document.location.hash = files.join('|');
|
||||
|
||||
if (includes(this.formats, ext)) {
|
||||
if (
|
||||
this.storage.setBinary(drive, name, ext, result)
|
||||
) {
|
||||
initGamepad();
|
||||
} else {
|
||||
openAlert(`Unable to load ${name}`);
|
||||
}
|
||||
}
|
||||
loadingStop();
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
doLoadHTTP(drive: DriveNumber, url?: string) {
|
||||
if (!url) {
|
||||
MicroModal.close('http-modal');
|
||||
}
|
||||
|
||||
loadingStart();
|
||||
const input = document.querySelector<HTMLInputElement>('#http_url')!;
|
||||
url = url || input.value;
|
||||
if (url) {
|
||||
fetch(url).then((response) =>{
|
||||
if (response.ok) {
|
||||
const reader = response!.body!.getReader();
|
||||
let received = 0;
|
||||
const chunks: Uint8Array[] = [];
|
||||
const contentLength = parseInt(response.headers.get('content-length')!, 10);
|
||||
|
||||
return reader.read().then(
|
||||
function readChunk(result): Promise<ArrayBufferLike> {
|
||||
if (result.done) {
|
||||
const data = new Uint8Array(received);
|
||||
let offset = 0;
|
||||
for (let idx = 0; idx < chunks.length; idx++) {
|
||||
data.set(chunks[idx], offset);
|
||||
offset += chunks[idx].length;
|
||||
}
|
||||
return Promise.resolve(data.buffer);
|
||||
}
|
||||
|
||||
received += result.value.length;
|
||||
if (contentLength) {
|
||||
loadingProgress(received, contentLength);
|
||||
}
|
||||
chunks.push(result.value);
|
||||
|
||||
return reader.read().then(readChunk);
|
||||
});
|
||||
} else {
|
||||
openAlert('Error loading: ' + response.statusText);
|
||||
}
|
||||
}).then((data) => {
|
||||
const urlParts = url!.split('/');
|
||||
const file = urlParts.pop()!;
|
||||
const fileParts = file.split('.');
|
||||
const ext = fileParts.pop()!.toLowerCase();
|
||||
const name = decodeURIComponent(fileParts.join('.'));
|
||||
if (includes(this.formats, ext)) {
|
||||
if (data && this.storage.setBinary(drive, name, ext, data)) {
|
||||
initGamepad();
|
||||
} else {
|
||||
openAlert(`Unable to load ${name}`);
|
||||
}
|
||||
} else {
|
||||
openAlert('Unknown extension: ' + ext);
|
||||
}
|
||||
loadingStop();
|
||||
}).catch(function (error) {
|
||||
loadingStop();
|
||||
openAlert(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected doLoadLocal(_drive: DriveNumber, _file: File, _options?: LoadOptions) {
|
||||
throw new Error('Unimplemented');
|
||||
}
|
||||
|
||||
openLoadHTTP() {
|
||||
MicroModal.show('http-modal');
|
||||
}
|
||||
}
|
25
js/ui/url.ts
Normal file
25
js/ui/url.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Returns the value of a query parameter or the empty string if it does not
|
||||
* exist.
|
||||
* @param name the parameter name.
|
||||
*/
|
||||
|
||||
export function gup(name: string) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(name) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL hash fragment minus the hash symbol or the empty
|
||||
* string if it does not exist.
|
||||
*/
|
||||
|
||||
export function hup() {
|
||||
const regex = new RegExp('#(.*)');
|
||||
const hash = decodeURIComponent(window.location.hash);
|
||||
const results = regex.exec(hash);
|
||||
if (!results)
|
||||
return '';
|
||||
else
|
||||
return results[1];
|
||||
}
|
|
@ -71,14 +71,6 @@ describe('toBinary', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('gup', () => {
|
||||
// untestable due to direct reference to window.location
|
||||
});
|
||||
|
||||
describe('hup', () => {
|
||||
// untestable due to direct reference to window.location
|
||||
});
|
||||
|
||||
describe('numToString', () => {
|
||||
it('packs a zero byte into a string of all zeros', () => {
|
||||
expect(numToString(0x00)).toEqual('\0\0\0\0');
|
||||
|
|
Loading…
Reference in New Issue
Block a user