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.
Files changed (112) hide show
  1. package/README.md +2 -2
  2. package/package.json +16 -11
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +198 -123
  5. package/src/6847.js +4 -5
  6. package/src/6847_fontdata.js +0 -2
  7. package/src/acia.js +2 -3
  8. package/src/app/app.js +0 -1
  9. package/src/app/args.js +0 -2
  10. package/src/app/electron.js +4 -6
  11. package/src/app-bench.js +1 -3
  12. package/src/archive.js +243 -0
  13. package/src/basic-loader.js +0 -7
  14. package/src/basic-tokenise.js +19 -6
  15. package/src/bbc-palette.js +0 -2
  16. package/src/bbcdiscs.js +0 -2
  17. package/src/bem-snapshot.js +1 -3
  18. package/src/binary.js +175 -0
  19. package/src/cmos.js +0 -2
  20. package/src/ddnoise.js +0 -1
  21. package/src/debounce.js +7 -0
  22. package/src/disc-surface.js +0 -2
  23. package/src/disc.js +10 -9
  24. package/src/fake6502.js +7 -23
  25. package/src/fdc.js +9 -51
  26. package/src/filestore.js +10 -17
  27. package/src/hex.js +38 -0
  28. package/src/index.js +11 -0
  29. package/src/intel-fdc.js +18 -20
  30. package/src/jsbeeb.css +33 -0
  31. package/src/{utils_atom.js → keymap-atom.js} +2 -18
  32. package/src/{utils.js → keymap.js} +47 -610
  33. package/src/loader.js +63 -0
  34. package/src/machine-session.js +37 -177
  35. package/src/machine-spec.js +59 -0
  36. package/src/main.js +69 -184
  37. package/src/media-resolver.js +68 -0
  38. package/src/mmc.js +3 -2
  39. package/src/models.js +40 -4
  40. package/src/music5000.js +0 -2
  41. package/src/ppia.js +3 -4
  42. package/src/printer.js +7 -14
  43. package/src/relaynoise.js +0 -1
  44. package/src/rewind.js +0 -2
  45. package/src/sample-player.js +2 -3
  46. package/src/serial.js +0 -2
  47. package/src/snapshot-helpers.js +0 -2
  48. package/src/snapshot.js +0 -2
  49. package/src/soundchip.js +1 -1
  50. package/src/state-utils.js +0 -2
  51. package/src/sth.js +2 -4
  52. package/src/tapes.js +10 -4
  53. package/src/teletext.js +2 -3
  54. package/src/teletext_adaptor.js +2 -4
  55. package/src/teletext_data.js +0 -2
  56. package/src/test-machine.js +307 -0
  57. package/src/touchscreen.js +2 -4
  58. package/src/tube.js +5 -6
  59. package/src/typist.js +103 -0
  60. package/src/uef-snapshot.js +0 -2
  61. package/src/url-params.js +4 -5
  62. package/src/vdu-capture.js +113 -0
  63. package/src/via.js +3 -4
  64. package/src/video-filters/pal-composite.js +0 -2
  65. package/src/video-filters/passthrough-filter.js +0 -2
  66. package/src/video-filters/pixel-grid.js +0 -2
  67. package/src/video-filters/shader-program.js +0 -2
  68. package/src/video-filters/xbr-filter.js +0 -2
  69. package/src/video.js +12 -11
  70. package/src/wd-fdc.js +20 -20
  71. package/src/web/analogue-inputs.js +6 -8
  72. package/src/web/analytics.js +13 -0
  73. package/src/web/audio-handler.js +2 -2
  74. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  75. package/src/web/autoboot.js +10 -13
  76. package/src/{canvas.js → web/canvas.js} +4 -5
  77. package/src/web/config.js +42 -72
  78. package/src/web/console-surface.js +25 -0
  79. package/src/web/debug.js +8 -49
  80. package/src/web/disc-visualiser.js +0 -2
  81. package/src/web/display.js +21 -4
  82. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  83. package/src/web/drives.js +1 -1
  84. package/src/web/emulation-loop.js +54 -33
  85. package/src/web/front-panel.js +13 -12
  86. package/src/{gamepads.js → web/gamepads.js} +2 -6
  87. package/src/web/google-drive-picker.js +14 -9
  88. package/src/{google-drive.js → web/google-drive.js} +9 -10
  89. package/src/web/hfe-picker.js +5 -4
  90. package/src/web/icons.js +42 -0
  91. package/src/web/keyboard-setup.js +29 -40
  92. package/src/{keyboard.js → web/keyboard.js} +18 -119
  93. package/src/web/layout.js +4 -0
  94. package/src/web/local-disc.js +44 -0
  95. package/src/web/machine.js +27 -20
  96. package/src/web/media-loader.js +38 -147
  97. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  98. package/src/web/modals.js +6 -9
  99. package/src/web/page-actions.js +88 -0
  100. package/src/web/quick-settings.js +16 -15
  101. package/src/web/reporting.js +0 -7
  102. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  103. package/src/web/rewind-ui.js +29 -29
  104. package/src/web/run-controls.js +39 -0
  105. package/src/web/settings.js +64 -135
  106. package/src/web/snapshot-ui.js +10 -16
  107. package/src/{speech-output.js → web/speech-output.js} +0 -2
  108. package/src/web/sth-picker.js +9 -7
  109. package/src/web/url-state.js +20 -6
  110. package/src/econet/L3FS.dat +0 -0
  111. package/src/econet/scsi.dat +0 -0
  112. package/tests/test-machine.js +0 -609
@@ -1,28 +1,36 @@
1
- import { AtomCpu6502, Cpu6502 } from "../6502.js";
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({ config, parsedQuery, keyLayout, cpuMultiplier, extraRoms, userPort, printer }) {
15
- return {
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: config.tubeCpuMultiplier,
26
+ tubeCpuMultiplier: settings.tubeCpuMultiplier,
19
27
  videoCyclesBatch: parsedQuery.videoCyclesBatch,
20
- tube: config.coProcessor ? tubeModelFor(config.model) : null,
21
- hasMusic5000: config.hasMusic5000,
22
- hasTeletextAdaptor: config.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: [...config.extraRoms, ...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
- config,
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
- makeCpu = (CpuClass, ...args) => new CpuClass(...args),
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 (config.hasEconet) {
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.emulationConfig = buildEmulationConfig({
82
- config,
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
- const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
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: config.hasMusic5000 ? audioHandler.music5000 : null,
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 utils.loadData(loadBasic);
197
+ const data = await loadData(loadBasic);
191
198
  return String.fromCharCode.apply(null, data);
192
199
  })(),
193
200
  basicNeedsRun,
@@ -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, unzipAndReport } from "./reporting.js";
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.sources = {};
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
- utils.noteEvent("local", "click"); // NB no filename here
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
- utils.noteEvent("local", "click"); // NB no filename here
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
- utils.noteEvent("local", "clickTape"); // NB no filename here
92
+ noteEvent("local", "clickTape"); // NB no filename here
96
93
 
97
94
  try {
98
- let tapeData = await readFileAsBinaryString(file);
99
- let tapeName = file.name;
100
- if (/\.zip/i.test(tapeName)) {
101
- const unzipped = await unzipAndReport(utils.stringToUint8Array(tapeData));
102
- tapeData = unzipped.data;
103
- tapeName = unzipped.name;
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
- utils.noteEvent("local", "drop");
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
- utils.noteEvent("images", "click", image.file);
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.sources[schema] = fetcher;
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
- if (this.model.isAtom) {
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 = utils.stringToUint8Array(await readFileAsBinaryString(file));
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 = utils.stringToUint8Array(binaryData);
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 split = splitImage(discImage);
233
- discImage = split.image;
234
- const schema = split.schema;
217
+ const { schema, image } = splitImage(discImage);
235
218
  if (schema[0] === "!" || schema === "local") {
236
- return disc.localDisc(this.processor.fdc, discImage, layout, (error) =>
219
+ return localDisc(image, layout, (error) =>
237
220
  toast(
238
- `Browser storage would not take changes to ${discImage} (${errorText(error)}). Use Discs, Download to keep a copy.`,
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
- // TODO: come up with a decent UX for passing an 'onChange' parameter to each of these.
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
- switch (schema) {
250
- case "|":
251
- case "sth": {
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 split = splitImage(tapeImage);
311
- tapeImage = split.image;
312
- const schema = split.schema;
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
  }
@@ -1,4 +1,4 @@
1
- import { AnalogueSource } from "./analogue-source.js";
1
+ import { AnalogueSource } from "../analogue-source.js";
2
2
  import { createAudioContext } from "./audio-utils.js";
3
3
 
4
4
  /**
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
- let savedRunning = false;
20
- document.addEventListener("show.bs.modal", () => {
21
- if (!this.anyVisible()) savedRunning = loop.isRunning();
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
- if (!this.anyVisible() && savedRunning) loop.go();
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
- * Choices go to the callbacks; what is shown follows the show methods, so the
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({ onAudioOutput, onSpeakerAmount, onDisplayMode }, { audioOutput, speakerAmount, displayMode }) {
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 value = e.target.closest("[data-output]")?.dataset.output;
21
- if (value) onAudioOutput(value);
22
+ const audioOutput = e.target.closest("[data-output]")?.dataset.output;
23
+ if (audioOutput) settings.set({ audioOutput });
22
24
  });
23
- this.amount.addEventListener("input", () => onSpeakerAmount(parseFloat(this.amount.value)));
25
+ this.amount.addEventListener("input", () => settings.set({ speakerAmount: parseFloat(this.amount.value) }));
24
26
  this.display.addEventListener("click", (e) => {
25
- const mode = e.target.closest("[data-mode]")?.dataset.mode;
26
- if (mode) onDisplayMode(mode);
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
- if (this.amount) this.amount.value = speakerAmount;
38
+ this.amount.value = speakerAmount;
38
39
  }
39
40
 
40
41
  showDisplayMode(mode) {
41
- if (this.display) select(this.display.querySelectorAll("[data-mode]"), (b) => b.dataset.mode === mode);
42
+ select(this.display.querySelectorAll("[data-mode]"), (b) => b.dataset.mode === mode);
42
43
  }
43
44
  }
44
45
 
@@ -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;
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  const ThumbnailWidth = 160;
4
2
  const ThumbnailHeight = 128;
5
3
  const FramebufferWidth = 1024;