jsbeeb 1.22.4 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +16 -11
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +198 -123
- package/src/6847.js +4 -5
- package/src/6847_fontdata.js +0 -2
- package/src/acia.js +2 -3
- package/src/app/app.js +0 -1
- package/src/app/args.js +0 -2
- package/src/app/electron.js +4 -6
- package/src/app-bench.js +1 -3
- package/src/archive.js +243 -0
- package/src/basic-loader.js +0 -7
- package/src/basic-tokenise.js +19 -6
- package/src/bbc-palette.js +0 -2
- package/src/bbcdiscs.js +0 -2
- package/src/bem-snapshot.js +1 -3
- package/src/binary.js +175 -0
- package/src/cmos.js +0 -2
- package/src/ddnoise.js +0 -1
- package/src/debounce.js +7 -0
- package/src/disc-surface.js +0 -2
- package/src/disc.js +10 -9
- package/src/fake6502.js +7 -23
- package/src/fdc.js +9 -51
- package/src/filestore.js +10 -17
- package/src/hex.js +38 -0
- package/src/index.js +11 -0
- package/src/intel-fdc.js +18 -20
- package/src/jsbeeb.css +33 -0
- package/src/{utils_atom.js → keymap-atom.js} +2 -18
- package/src/{utils.js → keymap.js} +47 -610
- package/src/loader.js +63 -0
- package/src/machine-session.js +37 -177
- package/src/machine-spec.js +59 -0
- package/src/main.js +69 -184
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +40 -4
- package/src/music5000.js +0 -2
- package/src/ppia.js +3 -4
- package/src/printer.js +7 -14
- package/src/relaynoise.js +0 -1
- package/src/rewind.js +0 -2
- package/src/sample-player.js +2 -3
- package/src/serial.js +0 -2
- package/src/snapshot-helpers.js +0 -2
- package/src/snapshot.js +0 -2
- package/src/soundchip.js +1 -1
- package/src/state-utils.js +0 -2
- package/src/sth.js +2 -4
- package/src/tapes.js +10 -4
- package/src/teletext.js +2 -3
- package/src/teletext_adaptor.js +2 -4
- package/src/teletext_data.js +0 -2
- package/src/test-machine.js +307 -0
- package/src/touchscreen.js +2 -4
- package/src/tube.js +5 -6
- package/src/typist.js +103 -0
- package/src/uef-snapshot.js +0 -2
- package/src/url-params.js +4 -5
- package/src/vdu-capture.js +113 -0
- package/src/via.js +3 -4
- package/src/video-filters/pal-composite.js +0 -2
- package/src/video-filters/passthrough-filter.js +0 -2
- package/src/video-filters/pixel-grid.js +0 -2
- package/src/video-filters/shader-program.js +0 -2
- package/src/video-filters/xbr-filter.js +0 -2
- package/src/video.js +12 -11
- package/src/wd-fdc.js +20 -20
- package/src/web/analogue-inputs.js +6 -8
- package/src/web/analytics.js +13 -0
- package/src/web/audio-handler.js +2 -2
- package/src/{audio-utils.js → web/audio-utils.js} +0 -2
- package/src/web/autoboot.js +10 -13
- package/src/{canvas.js → web/canvas.js} +4 -5
- package/src/web/config.js +42 -72
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +8 -49
- package/src/web/disc-visualiser.js +0 -2
- package/src/web/display.js +21 -4
- package/src/{dom-utils.js → web/dom-utils.js} +1 -11
- package/src/web/drives.js +1 -1
- package/src/web/emulation-loop.js +54 -33
- package/src/web/front-panel.js +13 -12
- package/src/{gamepads.js → web/gamepads.js} +2 -6
- package/src/web/google-drive-picker.js +14 -9
- package/src/{google-drive.js → web/google-drive.js} +9 -10
- package/src/web/hfe-picker.js +5 -4
- package/src/web/icons.js +42 -0
- package/src/web/keyboard-setup.js +29 -40
- package/src/{keyboard.js → web/keyboard.js} +18 -119
- package/src/web/layout.js +4 -0
- package/src/web/local-disc.js +44 -0
- package/src/web/machine.js +27 -20
- package/src/web/media-loader.js +38 -147
- package/src/{microphone-input.js → web/microphone-input.js} +1 -1
- package/src/web/modals.js +6 -9
- package/src/web/page-actions.js +88 -0
- package/src/web/quick-settings.js +16 -15
- package/src/web/reporting.js +0 -7
- package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
- package/src/web/rewind-ui.js +29 -29
- package/src/web/run-controls.js +39 -0
- package/src/web/settings.js +64 -135
- package/src/web/snapshot-ui.js +10 -16
- package/src/{speech-output.js → web/speech-output.js} +0 -2
- package/src/web/sth-picker.js +9 -7
- package/src/web/url-state.js +20 -6
- package/src/econet/L3FS.dat +0 -0
- package/src/econet/scsi.dat +0 -0
- package/tests/test-machine.js +0 -609
package/src/web/machine.js
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { machineSpec } from "../machine-spec.js";
|
|
2
2
|
import { Cmos, localStoragePersistence } from "../cmos.js";
|
|
3
3
|
import { Econet } from "../econet.js";
|
|
4
4
|
import { LoadSD } from "../mmc.js";
|
|
5
5
|
import { tubeModelFor } from "../models.js";
|
|
6
|
-
import * as utils from "../utils.js";
|
|
7
6
|
import { toast } from "./toast.js";
|
|
8
7
|
import { errorText, reportLoadFailure, showNotice } from "./reporting.js";
|
|
8
|
+
import { loadData } from "../loader.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The machine's fittings as the CPU wants them handed over. Pure, so the bank
|
|
12
12
|
* and flag decisions are testable on their own.
|
|
13
13
|
*/
|
|
14
|
-
export function buildEmulationConfig({
|
|
15
|
-
|
|
14
|
+
export function buildEmulationConfig({
|
|
15
|
+
settings,
|
|
16
|
+
parsedQuery,
|
|
17
|
+
keyLayout,
|
|
18
|
+
cpuMultiplier,
|
|
19
|
+
extraRoms,
|
|
20
|
+
userPort,
|
|
21
|
+
printer,
|
|
22
|
+
}) {
|
|
23
|
+
return machineSpec({
|
|
16
24
|
keyLayout,
|
|
17
25
|
cpuMultiplier,
|
|
18
|
-
tubeCpuMultiplier:
|
|
26
|
+
tubeCpuMultiplier: settings.tubeCpuMultiplier,
|
|
19
27
|
videoCyclesBatch: parsedQuery.videoCyclesBatch,
|
|
20
|
-
tube:
|
|
21
|
-
hasMusic5000:
|
|
22
|
-
hasTeletextAdaptor:
|
|
28
|
+
tube: settings.coProcessor ? tubeModelFor(settings.model) : null,
|
|
29
|
+
hasMusic5000: settings.hasMusic5000,
|
|
30
|
+
hasTeletextAdaptor: settings.hasTeletextAdaptor,
|
|
23
31
|
// ROM order determines sideways bank allocation, and the fittings' ROMs claim banks
|
|
24
32
|
// before any the user asked for with ?rom=.
|
|
25
|
-
extraRoms: [...
|
|
33
|
+
extraRoms: [...settings.extraRoms, ...extraRoms],
|
|
26
34
|
userPort,
|
|
27
35
|
printerPort: printer,
|
|
28
36
|
getGamepads: function () {
|
|
@@ -33,14 +41,14 @@ export function buildEmulationConfig({ config, parsedQuery, keyLayout, cpuMultip
|
|
|
33
41
|
logFdcCommands: parsedQuery.logFdcCommands !== undefined,
|
|
34
42
|
logFdcStateChanges: parsedQuery.logFdcStateChanges !== undefined,
|
|
35
43
|
},
|
|
36
|
-
};
|
|
44
|
+
});
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/** The emulated machine itself: the processor with everything bolted to it, and its start-up. */
|
|
40
48
|
export class Machine {
|
|
41
49
|
constructor({
|
|
42
50
|
model,
|
|
43
|
-
|
|
51
|
+
settings,
|
|
44
52
|
parsedQuery,
|
|
45
53
|
keyLayout,
|
|
46
54
|
cpuMultiplier,
|
|
@@ -52,14 +60,14 @@ export class Machine {
|
|
|
52
60
|
video,
|
|
53
61
|
audioHandler,
|
|
54
62
|
dbgr,
|
|
55
|
-
|
|
63
|
+
build = (cpuModel, options) => new cpuModel.Cpu(cpuModel, options),
|
|
56
64
|
}) {
|
|
57
65
|
this.model = model;
|
|
58
66
|
this.audioHandler = audioHandler;
|
|
59
67
|
this.speechOutput = speechOutput;
|
|
60
68
|
|
|
61
69
|
this.econet = null;
|
|
62
|
-
if (
|
|
70
|
+
if (settings.hasEconet) {
|
|
63
71
|
this.econet = new Econet(stationId, model.cyclesPerSecond);
|
|
64
72
|
} else {
|
|
65
73
|
document.getElementById("fsmenuitem").style.display = "none";
|
|
@@ -78,8 +86,8 @@ export class Machine {
|
|
|
78
86
|
this.econet,
|
|
79
87
|
);
|
|
80
88
|
|
|
81
|
-
this.
|
|
82
|
-
|
|
89
|
+
this.spec = buildEmulationConfig({
|
|
90
|
+
settings,
|
|
83
91
|
parsedQuery,
|
|
84
92
|
keyLayout,
|
|
85
93
|
cpuMultiplier,
|
|
@@ -88,17 +96,16 @@ export class Machine {
|
|
|
88
96
|
printer,
|
|
89
97
|
});
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
this.processor = makeCpu(CpuClass, model, {
|
|
99
|
+
this.processor = build(model, {
|
|
93
100
|
dbgr,
|
|
94
101
|
video,
|
|
95
102
|
soundChip: audioHandler.soundChip,
|
|
96
103
|
ddNoise: audioHandler.ddNoise,
|
|
97
104
|
relayNoise: audioHandler.relayNoise,
|
|
98
|
-
music5000:
|
|
105
|
+
music5000: settings.hasMusic5000 ? audioHandler.music5000 : null,
|
|
99
106
|
cmos: this.cmos,
|
|
100
|
-
config: this.emulationConfig,
|
|
101
107
|
econet: this.econet,
|
|
108
|
+
config: this.spec,
|
|
102
109
|
});
|
|
103
110
|
|
|
104
111
|
printer.attach(this.processor.uservia);
|
|
@@ -187,7 +194,7 @@ export class Machine {
|
|
|
187
194
|
await startImageLoad(`BASIC program ${loadBasic}`, () =>
|
|
188
195
|
autoBoot.insertBasic(
|
|
189
196
|
(async () => {
|
|
190
|
-
const data = await
|
|
197
|
+
const data = await loadData(loadBasic);
|
|
191
198
|
return String.fromCharCode.apply(null, data);
|
|
192
199
|
})(),
|
|
193
200
|
basicNeedsRun,
|
package/src/web/media-loader.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import * as utils from "../utils.js";
|
|
2
1
|
import * as disc from "../fdc.js";
|
|
2
|
+
import { localDisc } from "./local-disc.js";
|
|
3
3
|
import { DiscLayout } from "../disc.js";
|
|
4
4
|
import { loadTapeFromData } from "../tapes.js";
|
|
5
5
|
import { toast } from "./toast.js";
|
|
6
|
-
import { errorText, reportIgnoredFiles, reportLoadFailure
|
|
6
|
+
import { errorText, reportIgnoredFiles, reportLoadFailure } from "./reporting.js";
|
|
7
|
+
import { MediaResolver, openIfZip, splitImage } from "../media-resolver.js";
|
|
8
|
+
import { stringToUint8Array } from "../binary.js";
|
|
9
|
+
import { noteEvent } from "./analytics.js";
|
|
7
10
|
|
|
8
11
|
/** The images offered on the Discs dialog's built-in list. */
|
|
9
12
|
export const BuiltInImages = [
|
|
@@ -24,13 +27,6 @@ export const BuiltInImages = [
|
|
|
24
27
|
},
|
|
25
28
|
];
|
|
26
29
|
|
|
27
|
-
export function splitImage(image) {
|
|
28
|
-
const match = image.match(/(([^:]+):\/?\/?|[!^|])?(.*)/);
|
|
29
|
-
const schema = match[2] || match[1] || "";
|
|
30
|
-
image = match[3];
|
|
31
|
-
return { image: image, schema: schema };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
30
|
function readFileAsBinaryString(file) {
|
|
35
31
|
return new Promise((resolve, reject) => {
|
|
36
32
|
const reader = new FileReader();
|
|
@@ -63,11 +59,12 @@ export class MediaLoader extends EventTarget {
|
|
|
63
59
|
this.drives = drives;
|
|
64
60
|
this.urlState = urlState;
|
|
65
61
|
this.modals = modals;
|
|
66
|
-
this.
|
|
62
|
+
this.resolver = new MediaResolver();
|
|
63
|
+
this.driveSource = null;
|
|
67
64
|
|
|
68
65
|
document.getElementById("disc_load").addEventListener("change", async (evt) => {
|
|
69
66
|
if (evt.target.files.length === 0) return;
|
|
70
|
-
|
|
67
|
+
noteEvent("local", "click"); // NB no filename here
|
|
71
68
|
const file = evt.target.files[0];
|
|
72
69
|
try {
|
|
73
70
|
await this.loadHTMLFile(file);
|
|
@@ -79,7 +76,7 @@ export class MediaLoader extends EventTarget {
|
|
|
79
76
|
|
|
80
77
|
document.getElementById("fs_load").addEventListener("change", async (evt) => {
|
|
81
78
|
if (evt.target.files.length === 0) return;
|
|
82
|
-
|
|
79
|
+
noteEvent("local", "click"); // NB no filename here
|
|
83
80
|
const file = evt.target.files[0];
|
|
84
81
|
try {
|
|
85
82
|
await this.loadSCSIFile(file);
|
|
@@ -92,17 +89,15 @@ export class MediaLoader extends EventTarget {
|
|
|
92
89
|
document.getElementById("tape_load").addEventListener("change", async (evt) => {
|
|
93
90
|
if (evt.target.files.length === 0) return;
|
|
94
91
|
const file = evt.target.files[0];
|
|
95
|
-
|
|
92
|
+
noteEvent("local", "clickTape"); // NB no filename here
|
|
96
93
|
|
|
97
94
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
this.setProcessorTape(await loadTapeFromData(tapeName, tapeData, model));
|
|
95
|
+
const { name, data, ignored } = await openIfZip(
|
|
96
|
+
file.name,
|
|
97
|
+
stringToUint8Array(await readFileAsBinaryString(file)),
|
|
98
|
+
);
|
|
99
|
+
reportIgnoredFiles(name, ignored);
|
|
100
|
+
this.setProcessorTape(await loadTapeFromData(name, data, model));
|
|
106
101
|
urlState.set({ tape: undefined });
|
|
107
102
|
modals.hide("tapes");
|
|
108
103
|
} catch (error) {
|
|
@@ -119,7 +114,7 @@ export class MediaLoader extends EventTarget {
|
|
|
119
114
|
event.dataTransfer.dropEffect = "copy";
|
|
120
115
|
});
|
|
121
116
|
pastetext.addEventListener("drop", async (event) => {
|
|
122
|
-
|
|
117
|
+
noteEvent("local", "drop");
|
|
123
118
|
const file = event.dataTransfer.files[0];
|
|
124
119
|
if (!file) return;
|
|
125
120
|
try {
|
|
@@ -148,7 +143,7 @@ export class MediaLoader extends EventTarget {
|
|
|
148
143
|
elem.querySelector(".name").textContent = image.name;
|
|
149
144
|
elem.querySelector(".description").textContent = image.desc;
|
|
150
145
|
elem.addEventListener("click", async () => {
|
|
151
|
-
|
|
146
|
+
noteEvent("images", "click", image.file);
|
|
152
147
|
this.setDisc1Image(image.file);
|
|
153
148
|
modals.hide("discs");
|
|
154
149
|
try {
|
|
@@ -166,16 +161,12 @@ export class MediaLoader extends EventTarget {
|
|
|
166
161
|
|
|
167
162
|
/** Register the fetcher behind an image schema; each picker calls this as it is constructed. */
|
|
168
163
|
addSource(schema, fetcher) {
|
|
169
|
-
this.
|
|
164
|
+
if (schema === "drive") this.driveSource = fetcher;
|
|
165
|
+
else this.resolver.addSource(schema, fetcher);
|
|
170
166
|
}
|
|
171
167
|
|
|
172
|
-
/** Route tape to the correct interface (ACIA for BBC, PPIA for Atom) */
|
|
173
168
|
setProcessorTape(tape) {
|
|
174
|
-
|
|
175
|
-
this.processor.atomppia.setTape(tape);
|
|
176
|
-
} else {
|
|
177
|
-
this.processor.acia.setTape(tape);
|
|
178
|
-
}
|
|
169
|
+
this.processor.tapeInterface.setTape(tape);
|
|
179
170
|
}
|
|
180
171
|
|
|
181
172
|
setDisc1Image(name) {
|
|
@@ -194,14 +185,8 @@ export class MediaLoader extends EventTarget {
|
|
|
194
185
|
}
|
|
195
186
|
|
|
196
187
|
async loadHTMLFile(file) {
|
|
197
|
-
const imageData =
|
|
198
|
-
const loadedDisc = disc.discFor(
|
|
199
|
-
this.processor.fdc,
|
|
200
|
-
file.name,
|
|
201
|
-
imageData,
|
|
202
|
-
undefined,
|
|
203
|
-
this.drives.layoutForDrive(0),
|
|
204
|
-
);
|
|
188
|
+
const imageData = stringToUint8Array(await readFileAsBinaryString(file));
|
|
189
|
+
const loadedDisc = disc.discFor(file.name, imageData, undefined, this.drives.layoutForDrive(0));
|
|
205
190
|
// Local file: retain the image bytes for embedding in save-to-file snapshots.
|
|
206
191
|
loadedDisc.setOriginalImage(imageData);
|
|
207
192
|
this.drives.putDiscIn(0, loadedDisc);
|
|
@@ -213,7 +198,7 @@ export class MediaLoader extends EventTarget {
|
|
|
213
198
|
const { processor } = this;
|
|
214
199
|
if (!processor.filestore) return;
|
|
215
200
|
const binaryData = await readFileAsBinaryString(file);
|
|
216
|
-
processor.filestore.scsi =
|
|
201
|
+
processor.filestore.scsi = stringToUint8Array(binaryData);
|
|
217
202
|
|
|
218
203
|
processor.filestore.PC = 0x400;
|
|
219
204
|
processor.filestore.SP = 0xff;
|
|
@@ -229,128 +214,34 @@ export class MediaLoader extends EventTarget {
|
|
|
229
214
|
|
|
230
215
|
async loadDiscImage(discImage, layout = DiscLayout.auto) {
|
|
231
216
|
if (!discImage) return null;
|
|
232
|
-
const
|
|
233
|
-
discImage = split.image;
|
|
234
|
-
const schema = split.schema;
|
|
217
|
+
const { schema, image } = splitImage(discImage);
|
|
235
218
|
if (schema[0] === "!" || schema === "local") {
|
|
236
|
-
return
|
|
219
|
+
return localDisc(image, layout, (error) =>
|
|
237
220
|
toast(
|
|
238
|
-
`Browser storage would not take changes to ${
|
|
221
|
+
`Browser storage would not take changes to ${image} (${errorText(error)}). Use Discs, Download to keep a copy.`,
|
|
239
222
|
{ title: "Disc", quietKey: "quietLocalDiscSaveFailed" },
|
|
240
223
|
),
|
|
241
224
|
);
|
|
242
225
|
}
|
|
243
|
-
|
|
226
|
+
if (schema === "gd") {
|
|
227
|
+
const [, id, name = "(unknown)"] = image.match(/([^/]+)\/?(.*)/) ?? [null, image];
|
|
228
|
+
return this.driveSource({ name, id }, layout);
|
|
229
|
+
}
|
|
230
|
+
// TODO(#822) come up with a decent UX for passing an 'onChange' parameter to each of these.
|
|
244
231
|
// Consider:
|
|
245
232
|
// * hashing contents and making a local disc image named by original disc hash, save by that, and offer
|
|
246
233
|
// to load the modified disc on load.
|
|
247
234
|
// * popping up a message that notes the disc has changed, and offers a way to make a local image
|
|
248
235
|
// * Dialog box (ugh) saying "is this ok?"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const { name, data, ignored } = await this.sources.sth(discImage);
|
|
253
|
-
reportIgnoredFiles(name, ignored);
|
|
254
|
-
return disc.discFor(this.processor.fdc, name, data, undefined, layout);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
case "hfe":
|
|
258
|
-
return disc.discFor(
|
|
259
|
-
this.processor.fdc,
|
|
260
|
-
discImage,
|
|
261
|
-
await this.sources.hfe(discImage),
|
|
262
|
-
undefined,
|
|
263
|
-
layout,
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
case "gd": {
|
|
267
|
-
const splat = discImage.match(/([^/]+)\/?(.*)/);
|
|
268
|
-
let name = "(unknown)";
|
|
269
|
-
if (splat) {
|
|
270
|
-
discImage = splat[1];
|
|
271
|
-
name = splat[2];
|
|
272
|
-
}
|
|
273
|
-
return this.sources.drive({ name, id: discImage }, layout);
|
|
274
|
-
}
|
|
275
|
-
case "b64data":
|
|
276
|
-
return disc.discFor(this.processor.fdc, "disk.ssd", atob(discImage), undefined, layout);
|
|
277
|
-
|
|
278
|
-
case "data": {
|
|
279
|
-
const arr = Array.prototype.map.call(atob(discImage), (x) => x.charCodeAt(0));
|
|
280
|
-
const { name, data } = await unzipAndReport(arr);
|
|
281
|
-
return disc.discFor(this.processor.fdc, name, data, undefined, layout);
|
|
282
|
-
}
|
|
283
|
-
case "http":
|
|
284
|
-
case "https":
|
|
285
|
-
case "file": {
|
|
286
|
-
const asUrl = `${schema}://${discImage}`;
|
|
287
|
-
// url may end in query params etc, which can upset the DSD/SSD etc detection on the extension.
|
|
288
|
-
discImage = new URL(asUrl).pathname;
|
|
289
|
-
let discData = await utils.loadData(asUrl);
|
|
290
|
-
if (/\.zip/i.test(discImage)) {
|
|
291
|
-
const unzipped = await unzipAndReport(discData);
|
|
292
|
-
discData = unzipped.data;
|
|
293
|
-
discImage = unzipped.name;
|
|
294
|
-
}
|
|
295
|
-
return disc.discFor(this.processor.fdc, discImage, discData, undefined, layout);
|
|
296
|
-
}
|
|
297
|
-
default:
|
|
298
|
-
return disc.discFor(
|
|
299
|
-
this.processor.fdc,
|
|
300
|
-
discImage,
|
|
301
|
-
await disc.load("discs/" + discImage),
|
|
302
|
-
undefined,
|
|
303
|
-
layout,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
236
|
+
const { name, data, ignored } = await this.resolver.resolve("disc", discImage);
|
|
237
|
+
reportIgnoredFiles(name, ignored);
|
|
238
|
+
return disc.discFor(name, data, undefined, layout);
|
|
306
239
|
}
|
|
307
240
|
|
|
308
241
|
async loadTapeImage(tapeImage) {
|
|
309
242
|
if (!tapeImage) return null;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
switch (schema) {
|
|
315
|
-
case "|":
|
|
316
|
-
case "sth": {
|
|
317
|
-
const { name, data, ignored } = await this.sources.tapeSth(tapeImage);
|
|
318
|
-
reportIgnoredFiles(name, ignored);
|
|
319
|
-
return await loadTapeFromData(name, data, this.model);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
case "data": {
|
|
323
|
-
const arr = Array.prototype.map.call(atob(tapeImage), (x) => x.charCodeAt(0));
|
|
324
|
-
const { name, data } = await unzipAndReport(arr);
|
|
325
|
-
return await loadTapeFromData(name, data, this.model);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
case "http":
|
|
329
|
-
case "https":
|
|
330
|
-
case "file": {
|
|
331
|
-
const asUrl = `${schema}://${tapeImage}`;
|
|
332
|
-
// url may end in query params etc, which can upset file handling
|
|
333
|
-
tapeImage = new URL(asUrl).pathname;
|
|
334
|
-
let tapeData = await utils.loadData(asUrl);
|
|
335
|
-
if (/\.zip/i.test(tapeImage)) {
|
|
336
|
-
const unzipped = await unzipAndReport(tapeData);
|
|
337
|
-
tapeData = unzipped.data;
|
|
338
|
-
tapeImage = unzipped.name;
|
|
339
|
-
}
|
|
340
|
-
return await loadTapeFromData(tapeImage, tapeData, this.model);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
default: {
|
|
344
|
-
const tapePath = "tapes/" + tapeImage;
|
|
345
|
-
let tapeData = await utils.loadData(tapePath);
|
|
346
|
-
let tapeName = tapeImage;
|
|
347
|
-
if (/\.zip/i.test(tapeName)) {
|
|
348
|
-
const unzipped = await unzipAndReport(tapeData);
|
|
349
|
-
tapeData = unzipped.data;
|
|
350
|
-
tapeName = unzipped.name;
|
|
351
|
-
}
|
|
352
|
-
return await loadTapeFromData(tapeName, tapeData, this.model);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
243
|
+
const { name, data, ignored } = await this.resolver.resolve("tape", tapeImage);
|
|
244
|
+
reportIgnoredFiles(name, ignored);
|
|
245
|
+
return loadTapeFromData(name, data, this.model);
|
|
355
246
|
}
|
|
356
247
|
}
|
package/src/web/modals.js
CHANGED
|
@@ -12,17 +12,16 @@ export class Modals {
|
|
|
12
12
|
this.errorModal = new bootstrap.Modal(this.errorDialog);
|
|
13
13
|
this.loadingDialog = document.getElementById("loading-dialog");
|
|
14
14
|
this.loadingModal = new bootstrap.Modal(this.loadingDialog);
|
|
15
|
-
this.googleDriveAuth = document.getElementById("google-drive-auth");
|
|
16
15
|
this.aysEl = document.getElementById("are-you-sure");
|
|
17
16
|
this.aysModal = new bootstrap.Modal(this.aysEl);
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
document.addEventListener("show.bs.modal", () => {
|
|
21
|
-
if (!
|
|
22
|
-
if (loop.isRunning()) loop.stop(false);
|
|
18
|
+
const holds = new WeakMap();
|
|
19
|
+
document.addEventListener("show.bs.modal", (event) => {
|
|
20
|
+
if (!holds.has(event.target)) holds.set(event.target, loop.pause(`the ${event.target.id} dialog`));
|
|
23
21
|
});
|
|
24
|
-
document.addEventListener("hidden.bs.modal", () => {
|
|
25
|
-
|
|
22
|
+
document.addEventListener("hidden.bs.modal", (event) => {
|
|
23
|
+
holds.get(event.target)?.();
|
|
24
|
+
holds.delete(event.target);
|
|
26
25
|
});
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -48,12 +47,10 @@ export class Modals {
|
|
|
48
47
|
|
|
49
48
|
popupLoading(msg) {
|
|
50
49
|
this.loadingDialog.querySelector(".loading").textContent = msg;
|
|
51
|
-
this.googleDriveAuth.style.display = "none";
|
|
52
50
|
this.loadingModal.show();
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
loadingFinished(message) {
|
|
56
|
-
this.googleDriveAuth.style.display = "none";
|
|
57
54
|
this.loadingModal.hide();
|
|
58
55
|
if (message) toast(message);
|
|
59
56
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { downloadDriveData } from "./dom-utils.js";
|
|
2
|
+
|
|
3
|
+
const UnloadWarning =
|
|
4
|
+
"It seems like you're still using the emulator. If you're in Chrome, it's impossible for jsbeeb to prevent some shortcuts (like ctrl-W) from performing their default behaviour (e.g. closing the window).\n" +
|
|
5
|
+
"As a workaround, create an 'Application Shortcut' from the Tools menu. When jsbeeb runs as an application, it *can* prevent ctrl-W from closing the window.";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The page around the emulator: the reset menu items and the filestore
|
|
9
|
+
* download, what focus and unload do to the machine, the drop guard, the
|
|
10
|
+
* dialogs a URL can ask for, and the version stamp.
|
|
11
|
+
*/
|
|
12
|
+
export class PageActions {
|
|
13
|
+
constructor({ loop, processor, keyboard, audioHandler, rewindUI, modals, parsedQuery, version }) {
|
|
14
|
+
this.processor = processor;
|
|
15
|
+
this.rewindUI = rewindUI;
|
|
16
|
+
this.pageListeners = new AbortController();
|
|
17
|
+
const { signal } = this.pageListeners;
|
|
18
|
+
|
|
19
|
+
for (const el of document.querySelectorAll(".initially-hidden")) el.classList.remove("initially-hidden");
|
|
20
|
+
document.getElementById("paste-form").addEventListener("submit", (event) => event.preventDefault());
|
|
21
|
+
|
|
22
|
+
window.addEventListener(
|
|
23
|
+
"blur",
|
|
24
|
+
() => {
|
|
25
|
+
keyboard.clearKeys();
|
|
26
|
+
loop.setEmulationLead(audioHandler.setWindowFocused(false));
|
|
27
|
+
},
|
|
28
|
+
{ signal },
|
|
29
|
+
);
|
|
30
|
+
window.addEventListener("focus", () => loop.setEmulationLead(audioHandler.setWindowFocused(true)), { signal });
|
|
31
|
+
|
|
32
|
+
// To lower the chance of data loss, only the drop zone in the menu bar accepts drops.
|
|
33
|
+
document.addEventListener(
|
|
34
|
+
"dragover",
|
|
35
|
+
(event) => {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
event.dataTransfer.dropEffect = "none";
|
|
38
|
+
},
|
|
39
|
+
{ signal },
|
|
40
|
+
);
|
|
41
|
+
document.addEventListener("drop", (event) => event.preventDefault(), { signal });
|
|
42
|
+
|
|
43
|
+
window.addEventListener(
|
|
44
|
+
"beforeunload",
|
|
45
|
+
(event) => {
|
|
46
|
+
if (!loop.isRunning() || !processor.sysvia.hasAnyKeyDown()) return;
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
event.returnValue = UnloadWarning;
|
|
49
|
+
},
|
|
50
|
+
{ signal },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
document.getElementById("hard-reset").addEventListener("click", (event) => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
this.hardReset();
|
|
56
|
+
});
|
|
57
|
+
document.getElementById("soft-reset").addEventListener("click", (event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
this.softReset();
|
|
60
|
+
});
|
|
61
|
+
document.getElementById("download-filestore-link").addEventListener("click", () => this.downloadFilestore());
|
|
62
|
+
|
|
63
|
+
if (Object.hasOwn(parsedQuery, "about")) modals.show("info");
|
|
64
|
+
if (Object.hasOwn(parsedQuery, "pp-tos")) modals.show("pp-tos");
|
|
65
|
+
|
|
66
|
+
const versionElement = document.getElementById("jsbeeb-version");
|
|
67
|
+
if (versionElement) versionElement.textContent = `Version ${version}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Stops listening to the window and document; the elements go with the page. */
|
|
71
|
+
dispose() {
|
|
72
|
+
this.pageListeners.abort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
hardReset() {
|
|
76
|
+
this.rewindUI.reset();
|
|
77
|
+
this.processor.reset(true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
softReset() {
|
|
81
|
+
this.processor.reset(false);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
downloadFilestore() {
|
|
85
|
+
if (!this.processor.filestore) return;
|
|
86
|
+
downloadDriveData(this.processor.filestore.scsi, "scsi", ".dat");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1,44 +1,45 @@
|
|
|
1
1
|
import { AudioOutputs } from "../audio-output.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The sound output, speaker amount and display mode controls on the top bar
|
|
5
|
-
*
|
|
6
|
-
* bar can be kept in step with the same settings elsewhere.
|
|
4
|
+
* The sound output, speaker amount and display mode controls on the top bar:
|
|
5
|
+
* a view of those three settings, and a way to set them.
|
|
7
6
|
*/
|
|
8
7
|
export class QuickSettings {
|
|
9
|
-
constructor(
|
|
8
|
+
constructor(settings) {
|
|
10
9
|
this.output = document.getElementById("audio-output");
|
|
11
10
|
this.amount = document.getElementById("speaker-amount");
|
|
12
11
|
this.display = document.getElementById("display-mode");
|
|
13
12
|
if (!this.output || !this.amount || !this.display) return;
|
|
14
13
|
|
|
15
|
-
this.showAudioOutput(audioOutput);
|
|
16
|
-
this.showSpeakerAmount(speakerAmount);
|
|
17
|
-
this.showDisplayMode(displayMode);
|
|
14
|
+
this.showAudioOutput(settings.audioOutput);
|
|
15
|
+
this.showSpeakerAmount(settings.speakerAmount);
|
|
16
|
+
this.showDisplayMode(settings.displayMode);
|
|
17
|
+
settings.on("audioOutput", (audioOutput) => this.showAudioOutput(audioOutput));
|
|
18
|
+
settings.on("speakerAmount", (speakerAmount) => this.showSpeakerAmount(speakerAmount));
|
|
19
|
+
settings.on("displayMode", (displayMode) => this.showDisplayMode(displayMode));
|
|
18
20
|
|
|
19
21
|
this.output.addEventListener("click", (e) => {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
+
const audioOutput = e.target.closest("[data-output]")?.dataset.output;
|
|
23
|
+
if (audioOutput) settings.set({ audioOutput });
|
|
22
24
|
});
|
|
23
|
-
this.amount.addEventListener("input", () =>
|
|
25
|
+
this.amount.addEventListener("input", () => settings.set({ speakerAmount: parseFloat(this.amount.value) }));
|
|
24
26
|
this.display.addEventListener("click", (e) => {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
+
const displayMode = e.target.closest("[data-mode]")?.dataset.mode;
|
|
28
|
+
if (displayMode) settings.set({ displayMode });
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
showAudioOutput(audioOutput) {
|
|
31
|
-
if (!this.output) return;
|
|
32
33
|
select(this.output.querySelectorAll("[data-output]"), (b) => b.dataset.output === audioOutput);
|
|
33
34
|
this.amount.disabled = audioOutput !== AudioOutputs.speaker;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
showSpeakerAmount(speakerAmount) {
|
|
37
|
-
|
|
38
|
+
this.amount.value = speakerAmount;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
showDisplayMode(mode) {
|
|
41
|
-
|
|
42
|
+
select(this.display.querySelectorAll("[data-mode]"), (b) => b.dataset.mode === mode);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
package/src/web/reporting.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as utils from "../utils.js";
|
|
2
1
|
import { toast } from "./toast.js";
|
|
3
2
|
|
|
4
3
|
export const errorText = (error) => error?.message ?? `${error}`;
|
|
@@ -15,12 +14,6 @@ export function reportIgnoredFiles(name, ignored) {
|
|
|
15
14
|
});
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export async function unzipAndReport(data) {
|
|
19
|
-
const unzipped = await utils.unzipDiscImage(data);
|
|
20
|
-
reportIgnoredFiles(unzipped.name, unzipped.ignored);
|
|
21
|
-
return unzipped;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
17
|
/** Handles a component's "notice" event by toasting it. */
|
|
25
18
|
export function showNotice(event) {
|
|
26
19
|
const { message, title, quietKey } = event.detail;
|