jsbeeb 1.10.0 → 1.12.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 +1 -1
- package/package.json +2 -7
- package/src/6502.js +8 -1
- package/src/app/app.js +1 -1
- package/src/app/electron.js +8 -8
- package/src/bem-snapshot.js +7 -218
- package/src/config.js +79 -62
- package/src/dom-utils.js +32 -0
- package/src/google-drive.js +3 -4
- package/src/keyboard.js +17 -14
- package/src/main.js +267 -235
- package/src/models.js +4 -4
- package/src/snapshot-helpers.js +208 -0
- package/src/snapshot.js +9 -18
- package/src/sth.js +1 -1
- package/src/tapes.js +2 -2
- package/src/uef-snapshot.js +402 -0
- package/src/utils.js +146 -15
- package/src/web/audio-handler.js +41 -25
- package/src/web/debug.js +100 -71
package/src/main.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import $ from "jquery";
|
|
2
|
-
import _ from "underscore";
|
|
3
1
|
import * as bootstrap from "bootstrap";
|
|
4
2
|
import { version } from "../package.json";
|
|
5
3
|
|
|
@@ -30,8 +28,9 @@ import { MicrophoneInput } from "./microphone-input.js";
|
|
|
30
28
|
import { SpeechOutput } from "./speech-output.js";
|
|
31
29
|
import { MouseJoystickSource } from "./mouse-joystick-source.js";
|
|
32
30
|
import { getFilterForMode } from "./canvas.js";
|
|
33
|
-
import { createSnapshot, restoreSnapshot, snapshotToJSON, snapshotFromJSON,
|
|
31
|
+
import { createSnapshot, restoreSnapshot, snapshotToJSON, snapshotFromJSON, isSameModel } from "./snapshot.js";
|
|
34
32
|
import { isBemSnapshot, parseBemSnapshot } from "./bem-snapshot.js";
|
|
33
|
+
import { isUefSnapshot, parseUefSnapshot } from "./uef-snapshot.js";
|
|
35
34
|
import { RewindBuffer } from "./rewind.js";
|
|
36
35
|
import { RewindUI } from "./rewind-ui.js";
|
|
37
36
|
import {
|
|
@@ -104,6 +103,7 @@ const paramTypes = {
|
|
|
104
103
|
coProcessor: ParamTypes.BOOL,
|
|
105
104
|
mouseJoystickEnabled: ParamTypes.BOOL,
|
|
106
105
|
speechOutput: ParamTypes.BOOL,
|
|
106
|
+
audioDebug: ParamTypes.BOOL,
|
|
107
107
|
|
|
108
108
|
// Numeric parameters
|
|
109
109
|
speed: ParamTypes.INT,
|
|
@@ -162,8 +162,8 @@ if (parsedQuery.keyLayout) {
|
|
|
162
162
|
keyLayout = (parsedQuery.keyLayout + "").toLowerCase();
|
|
163
163
|
}
|
|
164
164
|
if (parsedQuery.embed) {
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
|
|
166
|
+
document.body.style.backgroundColor = "transparent";
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
fastTape = !!parsedQuery.fasttape;
|
|
@@ -230,11 +230,11 @@ const config = new Config(
|
|
|
230
230
|
setCrtPic(displayModeFilter);
|
|
231
231
|
swapCanvas(displayModeFilter);
|
|
232
232
|
// Trigger window resize to recalculate layout with new dimensions
|
|
233
|
-
|
|
233
|
+
window.dispatchEvent(new Event("resize"));
|
|
234
234
|
}
|
|
235
235
|
},
|
|
236
236
|
function onClose(changed) {
|
|
237
|
-
parsedQuery =
|
|
237
|
+
parsedQuery = Object.assign(parsedQuery, changed);
|
|
238
238
|
if (
|
|
239
239
|
changed.model ||
|
|
240
240
|
changed.coProcessor !== undefined ||
|
|
@@ -261,7 +261,7 @@ const config = new Config(
|
|
|
261
261
|
updateAdcSources(parsedQuery.mouseJoystickEnabled, parsedQuery.microphoneChannel);
|
|
262
262
|
|
|
263
263
|
if (changed.microphoneChannel !== undefined) {
|
|
264
|
-
setupMicrophone()
|
|
264
|
+
setupMicrophone();
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
if (changed.speechOutput !== undefined) {
|
|
@@ -297,23 +297,24 @@ config.setDisplayMode(displayMode);
|
|
|
297
297
|
model = config.model;
|
|
298
298
|
|
|
299
299
|
function sbBind(div, url, onload) {
|
|
300
|
-
const img = div.
|
|
301
|
-
img.
|
|
300
|
+
const img = div.querySelector("img");
|
|
301
|
+
img.style.display = "none";
|
|
302
302
|
if (!url) return;
|
|
303
|
-
img.
|
|
303
|
+
img.addEventListener("load", function () {
|
|
304
304
|
onload(div, img);
|
|
305
|
-
img.
|
|
305
|
+
img.style.display = "";
|
|
306
306
|
});
|
|
307
|
+
img.src = url;
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
sbBind(
|
|
310
|
-
div.
|
|
310
|
+
sbBind(document.querySelector(".sidebar.left"), parsedQuery.sbLeft, function (div, img) {
|
|
311
|
+
div.style.left = -img.naturalWidth - 5 + "px";
|
|
311
312
|
});
|
|
312
|
-
sbBind(
|
|
313
|
-
div.
|
|
313
|
+
sbBind(document.querySelector(".sidebar.right"), parsedQuery.sbRight, function (div, img) {
|
|
314
|
+
div.style.right = -img.naturalWidth - 5 + "px";
|
|
314
315
|
});
|
|
315
|
-
sbBind(
|
|
316
|
-
div.
|
|
316
|
+
sbBind(document.querySelector(".sidebar.bottom"), parsedQuery.sbBottom, function (div, img) {
|
|
317
|
+
div.style.bottom = -img.naturalHeight + "px";
|
|
317
318
|
});
|
|
318
319
|
|
|
319
320
|
if (parsedQuery.cpuMultiplier !== undefined) {
|
|
@@ -327,10 +328,10 @@ let tryGl = true;
|
|
|
327
328
|
if (parsedQuery.glEnabled !== undefined) {
|
|
328
329
|
tryGl = parsedQuery.glEnabled === "true";
|
|
329
330
|
}
|
|
330
|
-
const
|
|
331
|
+
const screenCanvas = document.getElementById("screen");
|
|
331
332
|
|
|
332
|
-
const
|
|
333
|
-
const
|
|
333
|
+
const errorDialog = document.getElementById("error-dialog");
|
|
334
|
+
const errorDialogModal = new bootstrap.Modal(errorDialog);
|
|
334
335
|
|
|
335
336
|
async function compressBlob(blob) {
|
|
336
337
|
const stream = blob.stream().pipeThrough(new CompressionStream("gzip"));
|
|
@@ -343,13 +344,13 @@ async function decompressBlob(blob) {
|
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
function showError(context, error) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
347
|
+
errorDialog.querySelector(".context").textContent = context;
|
|
348
|
+
errorDialog.querySelector(".error").textContent = error;
|
|
349
|
+
errorDialogModal.show();
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
function createCanvasForFilter(filterClass) {
|
|
352
|
-
const newCanvas = tryGl ? canvasLib.bestCanvas(
|
|
353
|
+
const newCanvas = tryGl ? canvasLib.bestCanvas(screenCanvas, filterClass) : new canvasLib.Canvas(screenCanvas);
|
|
353
354
|
|
|
354
355
|
if (filterClass.requiresGl() && !newCanvas.isWebGl()) {
|
|
355
356
|
const config = filterClass.getDisplayConfig();
|
|
@@ -384,21 +385,22 @@ video = new Video(model.isMaster, canvas.fb32, function paint(minx, miny, maxx,
|
|
|
384
385
|
});
|
|
385
386
|
if (parsedQuery.fakeVideo !== undefined) video = new FakeVideo();
|
|
386
387
|
|
|
387
|
-
const
|
|
388
|
+
const audioStatsEl = document.getElementById("audio-stats");
|
|
389
|
+
if (audioStatsEl) audioStatsEl.hidden = !parsedQuery.audioDebug;
|
|
390
|
+
const audioStatsNode = parsedQuery.audioDebug ? audioStatsEl : null;
|
|
388
391
|
const audioHandler = new AudioHandler({
|
|
389
|
-
warningNode:
|
|
392
|
+
warningNode: document.getElementById("audio-warning"),
|
|
390
393
|
statsNode: audioStatsNode,
|
|
391
394
|
audioFilterFreq,
|
|
392
395
|
audioFilterQ,
|
|
393
396
|
noSeek,
|
|
394
397
|
});
|
|
395
|
-
if (!parsedQuery.audioDebug) audioStatsNode.style.display = "none";
|
|
396
398
|
// Firefox will report that audio is suspended even when it will
|
|
397
399
|
// start playing without user interaction, so we need to delay a
|
|
398
400
|
// little to get a reliable indication.
|
|
399
401
|
window.setTimeout(() => audioHandler.checkStatus(), 1000);
|
|
400
402
|
|
|
401
|
-
|
|
403
|
+
for (const el of document.querySelectorAll(".initially-hidden")) el.classList.remove("initially-hidden");
|
|
402
404
|
|
|
403
405
|
const $discsModal = new bootstrap.Modal(document.getElementById("discs"));
|
|
404
406
|
const $fsModal = new bootstrap.Modal(document.getElementById("econetfs"));
|
|
@@ -479,35 +481,39 @@ async function loadSCSIFile(file) {
|
|
|
479
481
|
$fsModal.hide();
|
|
480
482
|
}
|
|
481
483
|
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const text = event.
|
|
484
|
+
const pastetext = document.getElementById("paste-text");
|
|
485
|
+
pastetext.closest("form").addEventListener("submit", (event) => event.preventDefault());
|
|
486
|
+
pastetext.addEventListener("paste", function (event) {
|
|
487
|
+
const text = event.clipboardData.getData("text/plain");
|
|
486
488
|
sendRawKeyboardToBBC(utils.stringToBBCKeys(text), true);
|
|
487
489
|
});
|
|
488
|
-
|
|
490
|
+
pastetext.addEventListener("dragover", function (event) {
|
|
489
491
|
event.preventDefault();
|
|
490
492
|
event.stopPropagation();
|
|
491
|
-
event.
|
|
493
|
+
event.dataTransfer.dropEffect = "copy";
|
|
492
494
|
});
|
|
493
|
-
|
|
495
|
+
pastetext.addEventListener("drop", async function (event) {
|
|
494
496
|
utils.noteEvent("local", "drop");
|
|
495
|
-
const file = event.
|
|
496
|
-
|
|
497
|
-
|
|
497
|
+
const file = event.dataTransfer.files[0];
|
|
498
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
499
|
+
if (isSnapshotFile(file.name, arrayBuffer)) {
|
|
500
|
+
await loadStateFromFile(file, arrayBuffer);
|
|
501
|
+
} else if (file.name.toLowerCase().endsWith(".uef")) {
|
|
502
|
+
// Regular UEF tape image (not a BeebEm save state)
|
|
503
|
+
processor.acia.setTape(loadTapeFromData(file.name, new Uint8Array(arrayBuffer)));
|
|
498
504
|
} else {
|
|
499
505
|
await loadHTMLFile(file);
|
|
500
506
|
}
|
|
501
507
|
});
|
|
502
508
|
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
audioHandler.tryResume()
|
|
509
|
+
const cubMonitor = document.getElementById("cub-monitor");
|
|
510
|
+
function onCubMouseEvent(evt) {
|
|
511
|
+
audioHandler.tryResume();
|
|
506
512
|
if (document.activeElement !== document.body) document.activeElement.blur();
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
const x = (evt.offsetX -
|
|
510
|
-
const y = (evt.offsetY -
|
|
513
|
+
const cubRect = cubMonitor.getBoundingClientRect();
|
|
514
|
+
const screenRect = screenCanvas.getBoundingClientRect();
|
|
515
|
+
const x = (evt.offsetX - cubRect.left + screenRect.left) / screenCanvas.offsetWidth;
|
|
516
|
+
const y = (evt.offsetY - cubRect.top + screenRect.top) / screenCanvas.offsetHeight;
|
|
511
517
|
|
|
512
518
|
// Handle touchscreen
|
|
513
519
|
if (processor.touchScreen) processor.touchScreen.onMouse(x, y, evt.buttons);
|
|
@@ -526,33 +532,36 @@ $cub.on("mousemove mousedown mouseup", function (evt) {
|
|
|
526
532
|
}
|
|
527
533
|
|
|
528
534
|
evt.preventDefault();
|
|
529
|
-
}
|
|
535
|
+
}
|
|
536
|
+
for (const eventType of ["mousemove", "mousedown", "mouseup"]) {
|
|
537
|
+
cubMonitor.addEventListener(eventType, onCubMouseEvent);
|
|
538
|
+
}
|
|
530
539
|
|
|
531
540
|
function setCrtPic(filterMode) {
|
|
532
541
|
const config = filterMode.getDisplayConfig();
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
542
|
+
const monitorPic = document.getElementById("cub-monitor-pic");
|
|
543
|
+
monitorPic.src = config.image;
|
|
544
|
+
monitorPic.alt = config.imageAlt;
|
|
545
|
+
monitorPic.width = config.imageWidth;
|
|
546
|
+
monitorPic.height = config.imageHeight;
|
|
538
547
|
}
|
|
539
548
|
setCrtPic(displayModeFilter);
|
|
540
549
|
|
|
541
|
-
|
|
550
|
+
window.addEventListener("blur", function () {
|
|
542
551
|
keyboard.clearKeys();
|
|
543
552
|
});
|
|
544
553
|
|
|
545
|
-
|
|
546
|
-
|
|
554
|
+
document.getElementById("fs").addEventListener("click", function (event) {
|
|
555
|
+
screenCanvas.requestFullscreen();
|
|
547
556
|
event.preventDefault();
|
|
548
557
|
});
|
|
549
558
|
|
|
550
559
|
let keyboard; // This will be initialised after the processor is created
|
|
551
560
|
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
561
|
+
const debugPause = document.getElementById("debug-pause");
|
|
562
|
+
const debugPlay = document.getElementById("debug-play");
|
|
563
|
+
debugPause.addEventListener("click", () => stop(true));
|
|
564
|
+
debugPlay.addEventListener("click", () => {
|
|
556
565
|
dbgr.hide();
|
|
557
566
|
go();
|
|
558
567
|
});
|
|
@@ -581,7 +590,7 @@ window.addEventListener("beforeunload", function (event) {
|
|
|
581
590
|
if (model.hasEconet) {
|
|
582
591
|
econet = new Econet(stationId);
|
|
583
592
|
} else {
|
|
584
|
-
|
|
593
|
+
document.getElementById("fsmenuitem").style.display = "none";
|
|
585
594
|
}
|
|
586
595
|
|
|
587
596
|
const cmos = new Cmos(
|
|
@@ -636,7 +645,6 @@ microphoneInput.setErrorCallback((message) => {
|
|
|
636
645
|
});
|
|
637
646
|
|
|
638
647
|
// Create MouseJoystickSource but don't enable by default
|
|
639
|
-
const screenCanvas = document.getElementById("screen");
|
|
640
648
|
const mouseJoystickSource = new MouseJoystickSource(screenCanvas);
|
|
641
649
|
|
|
642
650
|
/**
|
|
@@ -694,14 +702,14 @@ async function ensureMicrophoneRunning() {
|
|
|
694
702
|
}
|
|
695
703
|
|
|
696
704
|
async function setupMicrophone() {
|
|
697
|
-
const
|
|
698
|
-
|
|
705
|
+
const micPermissionStatus = document.getElementById("micPermissionStatus");
|
|
706
|
+
micPermissionStatus.textContent = "Requesting microphone access...";
|
|
699
707
|
|
|
700
708
|
// Try to initialise the microphone
|
|
701
709
|
const success = await microphoneInput.initialise();
|
|
702
710
|
if (success) {
|
|
703
711
|
// Note: Channel assignment is handled by updateAdcSources()
|
|
704
|
-
|
|
712
|
+
micPermissionStatus.textContent = "Microphone connected successfully";
|
|
705
713
|
await ensureMicrophoneRunning();
|
|
706
714
|
|
|
707
715
|
// Try starting audio context from user gesture
|
|
@@ -710,7 +718,7 @@ async function setupMicrophone() {
|
|
|
710
718
|
};
|
|
711
719
|
document.addEventListener("click", tryAgain);
|
|
712
720
|
} else {
|
|
713
|
-
|
|
721
|
+
micPermissionStatus.textContent = `Error: ${microphoneInput.getErrorMessage() || "Unknown error"}`;
|
|
714
722
|
config.setMicrophoneChannel(undefined);
|
|
715
723
|
// Update URL to remove the parameter
|
|
716
724
|
delete parsedQuery.microphoneChannel;
|
|
@@ -736,12 +744,12 @@ keyboard = new Keyboard({
|
|
|
736
744
|
keyLayout,
|
|
737
745
|
dbgr,
|
|
738
746
|
});
|
|
739
|
-
keyboard.
|
|
740
|
-
keyboard.
|
|
741
|
-
keyboard.
|
|
742
|
-
keyboard.
|
|
747
|
+
keyboard.addEventListener("showError", (e) => showError(e.detail.context, e.detail.error));
|
|
748
|
+
keyboard.addEventListener("pause", () => stop(false));
|
|
749
|
+
keyboard.addEventListener("resume", () => go());
|
|
750
|
+
keyboard.addEventListener("break", (e) => {
|
|
743
751
|
// F12/Break: Reset processor
|
|
744
|
-
if (
|
|
752
|
+
if (e.detail) utils.noteEvent("keyboard", "press", "break");
|
|
745
753
|
});
|
|
746
754
|
|
|
747
755
|
// Register default key handlers
|
|
@@ -845,8 +853,8 @@ keyboard.registerKeyHandler(
|
|
|
845
853
|
|
|
846
854
|
// Setup key handlers
|
|
847
855
|
document.addEventListener("keydown", (evt) => {
|
|
848
|
-
audioHandler.tryResume()
|
|
849
|
-
ensureMicrophoneRunning()
|
|
856
|
+
audioHandler.tryResume();
|
|
857
|
+
ensureMicrophoneRunning();
|
|
850
858
|
keyboard.keyDown(evt);
|
|
851
859
|
});
|
|
852
860
|
document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
|
|
@@ -856,29 +864,29 @@ function setDisc1Image(name) {
|
|
|
856
864
|
delete parsedQuery.disc;
|
|
857
865
|
parsedQuery.disc1 = name;
|
|
858
866
|
updateUrl();
|
|
859
|
-
config.
|
|
867
|
+
config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc1: name } }));
|
|
860
868
|
}
|
|
861
869
|
|
|
862
870
|
function setDisc2Image(name) {
|
|
863
871
|
parsedQuery.disc2 = name;
|
|
864
872
|
updateUrl();
|
|
865
|
-
config.
|
|
873
|
+
config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc2: name } }));
|
|
866
874
|
}
|
|
867
875
|
|
|
868
876
|
function setTapeImage(name) {
|
|
869
877
|
parsedQuery.tape = name;
|
|
870
878
|
updateUrl();
|
|
871
|
-
config.
|
|
879
|
+
config.dispatchEvent(new CustomEvent("media-changed", { detail: { tape: name } }));
|
|
872
880
|
}
|
|
873
881
|
|
|
874
882
|
function sthClearList() {
|
|
875
|
-
|
|
883
|
+
for (const el of document.querySelectorAll("#sth-list li:not(.template)")) el.remove();
|
|
876
884
|
}
|
|
877
885
|
|
|
878
886
|
function sthStartLoad() {
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
887
|
+
const sthLoading = document.querySelector("#sth .loading");
|
|
888
|
+
sthLoading.textContent = "Loading catalog from STH archive";
|
|
889
|
+
sthLoading.style.display = "";
|
|
882
890
|
sthClearList();
|
|
883
891
|
}
|
|
884
892
|
|
|
@@ -928,26 +936,28 @@ document.getElementById("sth").addEventListener("shown.bs.modal", () => {
|
|
|
928
936
|
function makeOnCat(onClick) {
|
|
929
937
|
return function (cat) {
|
|
930
938
|
sthClearList();
|
|
931
|
-
const sthList =
|
|
932
|
-
|
|
933
|
-
const template = sthList.
|
|
939
|
+
const sthList = document.getElementById("sth-list");
|
|
940
|
+
document.querySelector("#sth .loading").style.display = "none";
|
|
941
|
+
const template = sthList.querySelector(".template");
|
|
934
942
|
|
|
935
943
|
function doSome(all) {
|
|
936
944
|
const MaxAtATime = 100;
|
|
937
945
|
const Delay = 30;
|
|
938
|
-
const
|
|
946
|
+
const batch = all.slice(0, MaxAtATime);
|
|
939
947
|
const remaining = all.slice(MaxAtATime);
|
|
940
|
-
const filter =
|
|
941
|
-
|
|
942
|
-
const row = template.
|
|
943
|
-
row.
|
|
944
|
-
|
|
945
|
-
|
|
948
|
+
const filter = document.getElementById("sth-filter").value;
|
|
949
|
+
for (const name of batch) {
|
|
950
|
+
const row = template.cloneNode(true);
|
|
951
|
+
row.classList.remove("template");
|
|
952
|
+
sthList.appendChild(row);
|
|
953
|
+
row.querySelector(".name").textContent = name;
|
|
954
|
+
row.addEventListener("click", function () {
|
|
955
|
+
onClick(name);
|
|
946
956
|
$sthModal.hide();
|
|
947
957
|
});
|
|
948
|
-
row.
|
|
949
|
-
}
|
|
950
|
-
if (all.length)
|
|
958
|
+
row.style.display = name.toLowerCase().indexOf(filter) >= 0 ? "" : "none";
|
|
959
|
+
}
|
|
960
|
+
if (all.length) setTimeout(() => doSome(remaining), Delay);
|
|
951
961
|
}
|
|
952
962
|
|
|
953
963
|
doSome(cat);
|
|
@@ -955,18 +965,18 @@ function makeOnCat(onClick) {
|
|
|
955
965
|
}
|
|
956
966
|
|
|
957
967
|
function sthOnError() {
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
968
|
+
const sthLoading = document.querySelector("#sth .loading");
|
|
969
|
+
sthLoading.textContent = "There was an error accessing the STH archive";
|
|
970
|
+
sthLoading.style.display = "";
|
|
961
971
|
sthClearList();
|
|
962
972
|
}
|
|
963
973
|
|
|
964
974
|
discSth = new StairwayToHell(sthStartLoad, makeOnCat(discSthClick), sthOnError, false);
|
|
965
975
|
tapeSth = new StairwayToHell(sthStartLoad, makeOnCat(tapeSthClick), sthOnError, true);
|
|
966
976
|
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
if (
|
|
977
|
+
const sthAutoboot = document.querySelector("#sth .autoboot");
|
|
978
|
+
sthAutoboot.addEventListener("click", function () {
|
|
979
|
+
if (sthAutoboot.checked) {
|
|
970
980
|
parsedQuery.autoboot = "";
|
|
971
981
|
} else {
|
|
972
982
|
delete parsedQuery.autoboot;
|
|
@@ -974,8 +984,10 @@ $sthAutoboot.click(function () {
|
|
|
974
984
|
updateUrl();
|
|
975
985
|
});
|
|
976
986
|
|
|
977
|
-
|
|
978
|
-
const
|
|
987
|
+
document.addEventListener("click", function (e) {
|
|
988
|
+
const target = e.target.closest("a.sth");
|
|
989
|
+
if (!target) return;
|
|
990
|
+
const type = target.dataset.id;
|
|
979
991
|
if (type === "discs") {
|
|
980
992
|
discSth.populate();
|
|
981
993
|
} else if (type === "tapes") {
|
|
@@ -987,15 +999,14 @@ $(document).on("click", "a.sth", function () {
|
|
|
987
999
|
|
|
988
1000
|
function setSthFilter(filter) {
|
|
989
1001
|
filter = filter.toLowerCase();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
});
|
|
1002
|
+
for (const el of document.querySelectorAll("#sth-list li:not(.template)")) {
|
|
1003
|
+
el.style.display = el.textContent.toLowerCase().indexOf(filter) >= 0 ? "" : "none";
|
|
1004
|
+
}
|
|
994
1005
|
}
|
|
995
1006
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1007
|
+
const sthFilter = document.getElementById("sth-filter");
|
|
1008
|
+
sthFilter.addEventListener("change", () => setSthFilter(sthFilter.value));
|
|
1009
|
+
sthFilter.addEventListener("keyup", () => setSthFilter(sthFilter.value));
|
|
999
1010
|
|
|
1000
1011
|
function sendRawKeyboardToBBC(keysToSend, checkCapsAndShiftLocks) {
|
|
1001
1012
|
if (keyboard) {
|
|
@@ -1138,7 +1149,7 @@ async function loadDiscImage(discImage) {
|
|
|
1138
1149
|
|
|
1139
1150
|
case "data": {
|
|
1140
1151
|
const arr = Array.prototype.map.call(atob(discImage), (x) => x.charCodeAt(0));
|
|
1141
|
-
const { name, data } = utils.unzipDiscImage(arr);
|
|
1152
|
+
const { name, data } = await utils.unzipDiscImage(arr);
|
|
1142
1153
|
return disc.discFor(processor.fdc, name, data);
|
|
1143
1154
|
}
|
|
1144
1155
|
case "http":
|
|
@@ -1149,7 +1160,7 @@ async function loadDiscImage(discImage) {
|
|
|
1149
1160
|
discImage = new URL(asUrl).pathname;
|
|
1150
1161
|
let discData = await utils.loadData(asUrl);
|
|
1151
1162
|
if (/\.zip/i.test(discImage)) {
|
|
1152
|
-
const unzipped = utils.unzipDiscImage(discData);
|
|
1163
|
+
const unzipped = await utils.unzipDiscImage(discData);
|
|
1153
1164
|
discData = unzipped.data;
|
|
1154
1165
|
discImage = unzipped.name;
|
|
1155
1166
|
}
|
|
@@ -1168,12 +1179,12 @@ async function loadTapeImage(tapeImage) {
|
|
|
1168
1179
|
switch (schema) {
|
|
1169
1180
|
case "|":
|
|
1170
1181
|
case "sth":
|
|
1171
|
-
return loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage));
|
|
1182
|
+
return await loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage));
|
|
1172
1183
|
|
|
1173
1184
|
case "data": {
|
|
1174
1185
|
const arr = Array.prototype.map.call(atob(tapeImage), (x) => x.charCodeAt(0));
|
|
1175
|
-
const { name, data } = utils.unzipDiscImage(arr);
|
|
1176
|
-
return loadTapeFromData(name, data);
|
|
1186
|
+
const { name, data } = await utils.unzipDiscImage(arr);
|
|
1187
|
+
return await loadTapeFromData(name, data);
|
|
1177
1188
|
}
|
|
1178
1189
|
|
|
1179
1190
|
case "http":
|
|
@@ -1184,11 +1195,11 @@ async function loadTapeImage(tapeImage) {
|
|
|
1184
1195
|
tapeImage = new URL(asUrl).pathname;
|
|
1185
1196
|
let tapeData = await utils.loadData(asUrl);
|
|
1186
1197
|
if (/\.zip/i.test(tapeImage)) {
|
|
1187
|
-
const unzipped = utils.unzipDiscImage(tapeData);
|
|
1198
|
+
const unzipped = await utils.unzipDiscImage(tapeData);
|
|
1188
1199
|
tapeData = unzipped.data;
|
|
1189
1200
|
tapeImage = unzipped.name;
|
|
1190
1201
|
}
|
|
1191
|
-
return loadTapeFromData(tapeImage, tapeData);
|
|
1202
|
+
return await loadTapeFromData(tapeImage, tapeData);
|
|
1192
1203
|
}
|
|
1193
1204
|
|
|
1194
1205
|
default:
|
|
@@ -1196,7 +1207,7 @@ async function loadTapeImage(tapeImage) {
|
|
|
1196
1207
|
}
|
|
1197
1208
|
}
|
|
1198
1209
|
|
|
1199
|
-
|
|
1210
|
+
document.getElementById("disc_load").addEventListener("change", async function (evt) {
|
|
1200
1211
|
if (evt.target.files.length === 0) return;
|
|
1201
1212
|
utils.noteEvent("local", "click"); // NB no filename here
|
|
1202
1213
|
const file = evt.target.files[0];
|
|
@@ -1204,7 +1215,7 @@ $("#disc_load").on("change", async function (evt) {
|
|
|
1204
1215
|
evt.target.value = ""; // clear so if the user picks the same file again after a reset we get a "change"
|
|
1205
1216
|
});
|
|
1206
1217
|
|
|
1207
|
-
|
|
1218
|
+
document.getElementById("fs_load").addEventListener("change", async function (evt) {
|
|
1208
1219
|
if (evt.target.files.length === 0) return;
|
|
1209
1220
|
utils.noteEvent("local", "click"); // NB no filename here
|
|
1210
1221
|
const file = evt.target.files[0];
|
|
@@ -1212,22 +1223,22 @@ $("#fs_load").on("change", async function (evt) {
|
|
|
1212
1223
|
evt.target.value = ""; // clear so if the user picks the same file again after a reset we get a "change"
|
|
1213
1224
|
});
|
|
1214
1225
|
|
|
1215
|
-
|
|
1226
|
+
document.getElementById("tape_load").addEventListener("change", async function (evt) {
|
|
1216
1227
|
if (evt.target.files.length === 0) return;
|
|
1217
1228
|
const file = evt.target.files[0];
|
|
1218
1229
|
utils.noteEvent("local", "clickTape"); // NB no filename here
|
|
1219
1230
|
|
|
1220
1231
|
const binaryData = await readFileAsBinaryString(file);
|
|
1221
|
-
processor.acia.setTape(loadTapeFromData("local file", binaryData));
|
|
1232
|
+
processor.acia.setTape(await loadTapeFromData("local file", binaryData));
|
|
1222
1233
|
delete parsedQuery.tape;
|
|
1223
1234
|
updateUrl();
|
|
1224
|
-
|
|
1235
|
+
bootstrap.Modal.getInstance(document.getElementById("tapes"))?.hide();
|
|
1225
1236
|
|
|
1226
1237
|
evt.target.value = ""; // clear so if the user picks the same file again after a reset we get a "change"
|
|
1227
1238
|
});
|
|
1228
1239
|
|
|
1229
1240
|
function anyModalsVisible() {
|
|
1230
|
-
return
|
|
1241
|
+
return document.querySelectorAll(".modal.show").length !== 0;
|
|
1231
1242
|
}
|
|
1232
1243
|
|
|
1233
1244
|
let modalSavedRunning = false;
|
|
@@ -1241,42 +1252,45 @@ document.addEventListener("hidden.bs.modal", function () {
|
|
|
1241
1252
|
}
|
|
1242
1253
|
});
|
|
1243
1254
|
|
|
1244
|
-
const
|
|
1245
|
-
const
|
|
1255
|
+
const loadingDialog = document.getElementById("loading-dialog");
|
|
1256
|
+
const loadingDialogModal = new bootstrap.Modal(loadingDialog);
|
|
1257
|
+
const googleDriveAuth = document.getElementById("google-drive-auth");
|
|
1246
1258
|
|
|
1247
1259
|
function popupLoading(msg) {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1260
|
+
loadingDialog.querySelector(".loading").textContent = msg;
|
|
1261
|
+
googleDriveAuth.style.display = "none";
|
|
1262
|
+
loadingDialogModal.show();
|
|
1251
1263
|
}
|
|
1252
1264
|
|
|
1253
1265
|
function loadingFinished(error) {
|
|
1254
|
-
|
|
1266
|
+
googleDriveAuth.style.display = "none";
|
|
1255
1267
|
if (error) {
|
|
1256
|
-
|
|
1257
|
-
|
|
1268
|
+
loadingDialogModal.show();
|
|
1269
|
+
loadingDialog.querySelector(".loading").textContent = "Error: " + error;
|
|
1258
1270
|
setTimeout(function () {
|
|
1259
|
-
|
|
1271
|
+
loadingDialogModal.hide();
|
|
1260
1272
|
}, 5000);
|
|
1261
1273
|
} else {
|
|
1262
|
-
|
|
1274
|
+
loadingDialogModal.hide();
|
|
1263
1275
|
}
|
|
1264
1276
|
}
|
|
1265
1277
|
|
|
1266
1278
|
const googleDrive = new GoogleDriveLoader();
|
|
1279
|
+
const googleDriveEl = document.getElementById("google-drive");
|
|
1267
1280
|
|
|
1268
1281
|
async function gdAuth(imm) {
|
|
1269
1282
|
try {
|
|
1270
1283
|
return await googleDrive.authorize(imm);
|
|
1271
1284
|
} catch (err) {
|
|
1272
1285
|
console.log("Error handling google auth: " + err);
|
|
1273
|
-
|
|
1286
|
+
googleDriveEl.querySelector(".loading").textContent =
|
|
1287
|
+
"There was an error accessing your Google Drive account: " + err;
|
|
1274
1288
|
}
|
|
1275
1289
|
}
|
|
1276
1290
|
|
|
1277
1291
|
let googleDriveLoadingResolve, googleDriveLoadingReject;
|
|
1278
|
-
|
|
1279
|
-
|
|
1292
|
+
document.querySelector("#google-drive-auth form").addEventListener("submit", async function (e) {
|
|
1293
|
+
googleDriveAuth.style.display = "none";
|
|
1280
1294
|
e.preventDefault();
|
|
1281
1295
|
const authed = await gdAuth(false);
|
|
1282
1296
|
if (authed) googleDriveLoadingResolve();
|
|
@@ -1303,7 +1317,7 @@ async function gdLoad(cat) {
|
|
|
1303
1317
|
await new Promise(function (resolve, reject) {
|
|
1304
1318
|
googleDriveLoadingResolve = resolve;
|
|
1305
1319
|
googleDriveLoadingReject = reject;
|
|
1306
|
-
|
|
1320
|
+
googleDriveAuth.style.display = "";
|
|
1307
1321
|
});
|
|
1308
1322
|
}
|
|
1309
1323
|
|
|
@@ -1317,70 +1331,72 @@ async function gdLoad(cat) {
|
|
|
1317
1331
|
}
|
|
1318
1332
|
}
|
|
1319
1333
|
|
|
1320
|
-
|
|
1334
|
+
for (const el of document.querySelectorAll(".if-drive-available")) el.style.display = "none";
|
|
1321
1335
|
(async () => {
|
|
1322
1336
|
const available = await googleDrive.initialise();
|
|
1323
1337
|
if (available) {
|
|
1324
|
-
|
|
1338
|
+
for (const el of document.querySelectorAll(".if-drive-available")) el.style.display = "";
|
|
1325
1339
|
await gdAuth(true);
|
|
1326
1340
|
}
|
|
1327
1341
|
})();
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
$("#open-drive-link").on("click", async function () {
|
|
1342
|
+
const googleDriveModal = new bootstrap.Modal(googleDriveEl);
|
|
1343
|
+
document.getElementById("open-drive-link").addEventListener("click", async function () {
|
|
1331
1344
|
const authed = await gdAuth(false);
|
|
1332
1345
|
if (authed) {
|
|
1333
|
-
|
|
1346
|
+
googleDriveModal.show();
|
|
1334
1347
|
}
|
|
1335
1348
|
return false;
|
|
1336
1349
|
});
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1350
|
+
googleDriveEl.addEventListener("show.bs.modal", async function () {
|
|
1351
|
+
const gdLoading = googleDriveEl.querySelector(".loading");
|
|
1352
|
+
gdLoading.textContent = "Loading...";
|
|
1353
|
+
gdLoading.style.display = "";
|
|
1354
|
+
for (const el of googleDriveEl.querySelectorAll("li:not(.template)")) el.remove();
|
|
1340
1355
|
const cat = await googleDrive.listFiles();
|
|
1341
|
-
const dbList =
|
|
1342
|
-
|
|
1343
|
-
const template = dbList.
|
|
1344
|
-
|
|
1345
|
-
const row = template.
|
|
1346
|
-
row.
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1356
|
+
const dbList = googleDriveEl.querySelector(".list");
|
|
1357
|
+
gdLoading.style.display = "none";
|
|
1358
|
+
const template = dbList.querySelector(".template");
|
|
1359
|
+
for (const item of cat) {
|
|
1360
|
+
const row = template.cloneNode(true);
|
|
1361
|
+
row.classList.remove("template");
|
|
1362
|
+
dbList.appendChild(row);
|
|
1363
|
+
row.querySelector(".name").textContent = item.name;
|
|
1364
|
+
row.addEventListener("click", async function () {
|
|
1365
|
+
utils.noteEvent("google-drive", "click", item.name);
|
|
1366
|
+
setDisc1Image(`gd:${item.id}/${item.name}`);
|
|
1367
|
+
googleDriveModal.hide();
|
|
1368
|
+
const ssd = await gdLoad(item);
|
|
1369
|
+
if (ssd) processor.fdc.loadDisc(0, ssd);
|
|
1354
1370
|
});
|
|
1355
|
-
}
|
|
1371
|
+
}
|
|
1356
1372
|
});
|
|
1357
|
-
const discList =
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
const elem =
|
|
1361
|
-
elem.
|
|
1362
|
-
|
|
1363
|
-
|
|
1373
|
+
const discList = document.getElementById("disc-list");
|
|
1374
|
+
const discTemplate = discList.querySelector(".template");
|
|
1375
|
+
for (const image of availableImages) {
|
|
1376
|
+
const elem = discTemplate.cloneNode(true);
|
|
1377
|
+
elem.classList.remove("template");
|
|
1378
|
+
discList.appendChild(elem);
|
|
1379
|
+
elem.querySelector(".name").textContent = image.name;
|
|
1380
|
+
elem.querySelector(".description").textContent = image.desc;
|
|
1381
|
+
elem.addEventListener("click", async function () {
|
|
1364
1382
|
utils.noteEvent("images", "click", image.file);
|
|
1365
1383
|
setDisc1Image(image.file);
|
|
1366
|
-
loadDiscImage(parsedQuery.disc1).then(function (disc) {
|
|
1367
|
-
processor.fdc.loadDisc(0, disc);
|
|
1368
|
-
});
|
|
1369
1384
|
$discsModal.hide();
|
|
1385
|
+
processor.fdc.loadDisc(0, await loadDiscImage(parsedQuery.disc1));
|
|
1370
1386
|
});
|
|
1371
|
-
}
|
|
1387
|
+
}
|
|
1372
1388
|
|
|
1373
|
-
|
|
1389
|
+
document.querySelector("#google-drive form").addEventListener("submit", async function (e) {
|
|
1374
1390
|
e.preventDefault();
|
|
1375
|
-
let name =
|
|
1391
|
+
let name = document.querySelector("#google-drive .disc-name").value;
|
|
1376
1392
|
if (!name) return;
|
|
1377
1393
|
|
|
1378
1394
|
popupLoading("Connecting to Google Drive");
|
|
1379
|
-
|
|
1395
|
+
googleDriveModal.hide();
|
|
1380
1396
|
popupLoading("Creating '" + name + "' on Google Drive");
|
|
1381
1397
|
|
|
1382
1398
|
let data;
|
|
1383
|
-
if (
|
|
1399
|
+
if (document.querySelector("#google-drive .create-from-existing").checked) {
|
|
1384
1400
|
const discType = disc.guessDiscTypeFromName(name);
|
|
1385
1401
|
data = discType.saver(processor.fdc.drives[0].disc);
|
|
1386
1402
|
name = replaceOrAddExtension(name, discType.extension);
|
|
@@ -1409,7 +1425,7 @@ $("#google-drive form").on("submit", async function (e) {
|
|
|
1409
1425
|
}
|
|
1410
1426
|
});
|
|
1411
1427
|
|
|
1412
|
-
|
|
1428
|
+
document.getElementById("download-drive-link").addEventListener("click", function () {
|
|
1413
1429
|
const disc = processor.fdc.drives[0].disc;
|
|
1414
1430
|
const data = toSsdOrDsd(disc);
|
|
1415
1431
|
const name = disc.name;
|
|
@@ -1418,7 +1434,7 @@ $("#download-drive-link").on("click", function () {
|
|
|
1418
1434
|
downloadDriveData(data, name, extension);
|
|
1419
1435
|
});
|
|
1420
1436
|
|
|
1421
|
-
|
|
1437
|
+
document.getElementById("download-drive-hfe-link").addEventListener("click", function () {
|
|
1422
1438
|
const disc = processor.fdc.drives[0].disc;
|
|
1423
1439
|
const data = toHfe(disc);
|
|
1424
1440
|
const name = disc.name;
|
|
@@ -1426,7 +1442,7 @@ $("#download-drive-hfe-link").on("click", function () {
|
|
|
1426
1442
|
downloadDriveData(data, name, ".hfe");
|
|
1427
1443
|
});
|
|
1428
1444
|
|
|
1429
|
-
|
|
1445
|
+
document.getElementById("download-filestore-link").addEventListener("click", function () {
|
|
1430
1446
|
downloadDriveData(processor.filestore.scsi, "scsi", ".dat");
|
|
1431
1447
|
});
|
|
1432
1448
|
|
|
@@ -1439,17 +1455,17 @@ function hardReset() {
|
|
|
1439
1455
|
processor.reset(true);
|
|
1440
1456
|
}
|
|
1441
1457
|
|
|
1442
|
-
|
|
1458
|
+
document.getElementById("hard-reset").addEventListener("click", function (event) {
|
|
1443
1459
|
hardReset();
|
|
1444
1460
|
event.preventDefault();
|
|
1445
1461
|
});
|
|
1446
1462
|
|
|
1447
|
-
|
|
1463
|
+
document.getElementById("soft-reset").addEventListener("click", function (event) {
|
|
1448
1464
|
processor.reset(false);
|
|
1449
1465
|
event.preventDefault();
|
|
1450
1466
|
});
|
|
1451
1467
|
|
|
1452
|
-
|
|
1468
|
+
document.getElementById("save-state").addEventListener("click", async function (event) {
|
|
1453
1469
|
event.preventDefault();
|
|
1454
1470
|
const wasRunning = running;
|
|
1455
1471
|
if (running) stop(false);
|
|
@@ -1489,14 +1505,16 @@ $("#save-state").click(async function (event) {
|
|
|
1489
1505
|
if (wasRunning) go();
|
|
1490
1506
|
});
|
|
1491
1507
|
|
|
1492
|
-
async function loadStateFromFile(file) {
|
|
1508
|
+
async function loadStateFromFile(file, preReadBuffer) {
|
|
1493
1509
|
const wasRunning = running;
|
|
1494
1510
|
if (running) stop(false);
|
|
1495
1511
|
try {
|
|
1496
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
1512
|
+
const arrayBuffer = preReadBuffer || (await file.arrayBuffer());
|
|
1497
1513
|
let snapshot;
|
|
1498
1514
|
if (isBemSnapshot(arrayBuffer)) {
|
|
1499
1515
|
snapshot = await parseBemSnapshot(arrayBuffer);
|
|
1516
|
+
} else if (isUefSnapshot(arrayBuffer)) {
|
|
1517
|
+
snapshot = parseUefSnapshot(arrayBuffer);
|
|
1500
1518
|
} else {
|
|
1501
1519
|
// Detect gzip (magic bytes 0x1f 0x8b) or plain JSON
|
|
1502
1520
|
const bytes = new Uint8Array(arrayBuffer);
|
|
@@ -1509,7 +1527,7 @@ async function loadStateFromFile(file) {
|
|
|
1509
1527
|
}
|
|
1510
1528
|
snapshot = snapshotFromJSON(text);
|
|
1511
1529
|
}
|
|
1512
|
-
if (!
|
|
1530
|
+
if (!isSameModel(snapshot.model, model.name)) {
|
|
1513
1531
|
// Model mismatch: stash state and reload with correct model
|
|
1514
1532
|
sessionStorage.setItem("jsbeeb-pending-state", snapshotToJSON(snapshot));
|
|
1515
1533
|
const newQuery = { ...parsedQuery, model: snapshot.model };
|
|
@@ -1529,38 +1547,43 @@ async function loadStateFromFile(file) {
|
|
|
1529
1547
|
if (wasRunning) go();
|
|
1530
1548
|
}
|
|
1531
1549
|
|
|
1532
|
-
function isSnapshotFile(filename) {
|
|
1550
|
+
function isSnapshotFile(filename, arrayBuffer) {
|
|
1533
1551
|
const lower = filename.toLowerCase();
|
|
1534
|
-
|
|
1552
|
+
if (lower.endsWith(".snp") || lower.endsWith(".json") || lower.endsWith(".json.gz") || lower.endsWith(".gz"))
|
|
1553
|
+
return true;
|
|
1554
|
+
// .uef can be either a BeebEm save state or a regular tape image - check content
|
|
1555
|
+
if (lower.endsWith(".uef") && arrayBuffer) return isUefSnapshot(arrayBuffer);
|
|
1556
|
+
return false;
|
|
1535
1557
|
}
|
|
1536
1558
|
|
|
1537
|
-
|
|
1559
|
+
document.getElementById("load-state").addEventListener("change", async function (event) {
|
|
1538
1560
|
const file = event.target.files[0];
|
|
1539
1561
|
if (!file) return;
|
|
1540
1562
|
event.target.value = "";
|
|
1541
1563
|
await loadStateFromFile(file);
|
|
1542
1564
|
});
|
|
1543
1565
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
if (type === "rewind") {
|
|
1549
|
-
console.log("Rewinding tape to the start");
|
|
1566
|
+
for (const link of document.querySelectorAll("#tape-menu a")) {
|
|
1567
|
+
link.addEventListener("click", function (e) {
|
|
1568
|
+
const type = e.target.dataset.id;
|
|
1569
|
+
if (type === undefined) return;
|
|
1550
1570
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1571
|
+
if (type === "rewind") {
|
|
1572
|
+
console.log("Rewinding tape to the start");
|
|
1573
|
+
processor.acia.rewindTape();
|
|
1574
|
+
} else {
|
|
1575
|
+
console.log("unknown type", type);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1556
1579
|
|
|
1557
1580
|
function Light(name) {
|
|
1558
|
-
const dom =
|
|
1581
|
+
const dom = document.getElementById(name);
|
|
1559
1582
|
let on = false;
|
|
1560
1583
|
this.update = function (val) {
|
|
1561
1584
|
if (val === on) return;
|
|
1562
1585
|
on = val;
|
|
1563
|
-
dom.
|
|
1586
|
+
dom.classList.toggle("on", on);
|
|
1564
1587
|
};
|
|
1565
1588
|
}
|
|
1566
1589
|
|
|
@@ -1671,11 +1694,13 @@ const startPromise = (async () => {
|
|
|
1671
1694
|
return Promise.all(imageLoads);
|
|
1672
1695
|
})();
|
|
1673
1696
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1697
|
+
(async () => {
|
|
1698
|
+
try {
|
|
1699
|
+
await startPromise;
|
|
1700
|
+
|
|
1676
1701
|
switch (needsAutoboot) {
|
|
1677
1702
|
case "boot":
|
|
1678
|
-
|
|
1703
|
+
sthAutoboot.checked = true;
|
|
1679
1704
|
autoboot(discImage);
|
|
1680
1705
|
break;
|
|
1681
1706
|
case "type":
|
|
@@ -1688,7 +1713,7 @@ startPromise
|
|
|
1688
1713
|
autoRunTape();
|
|
1689
1714
|
break;
|
|
1690
1715
|
default:
|
|
1691
|
-
|
|
1716
|
+
sthAutoboot.checked = false;
|
|
1692
1717
|
break;
|
|
1693
1718
|
}
|
|
1694
1719
|
|
|
@@ -1713,24 +1738,28 @@ startPromise
|
|
|
1713
1738
|
}
|
|
1714
1739
|
|
|
1715
1740
|
go();
|
|
1716
|
-
})
|
|
1717
|
-
.catch((error) => {
|
|
1741
|
+
} catch (error) {
|
|
1718
1742
|
console.error("Error initialising emulator:", error);
|
|
1719
1743
|
showError("initialising", error);
|
|
1720
|
-
}
|
|
1744
|
+
}
|
|
1745
|
+
})();
|
|
1721
1746
|
|
|
1722
|
-
const
|
|
1723
|
-
const
|
|
1747
|
+
const aysEl = document.getElementById("are-you-sure");
|
|
1748
|
+
const aysModal = new bootstrap.Modal(aysEl);
|
|
1724
1749
|
|
|
1725
1750
|
function areYouSure(message, yesText, noText, yesFunc) {
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1751
|
+
aysEl.querySelector(".context").textContent = message;
|
|
1752
|
+
aysEl.querySelector(".ays-yes").textContent = yesText;
|
|
1753
|
+
aysEl.querySelector(".ays-no").textContent = noText;
|
|
1754
|
+
aysEl.querySelector(".ays-yes").addEventListener(
|
|
1755
|
+
"click",
|
|
1756
|
+
function () {
|
|
1757
|
+
aysModal.hide();
|
|
1758
|
+
yesFunc();
|
|
1759
|
+
},
|
|
1760
|
+
{ once: true },
|
|
1761
|
+
);
|
|
1762
|
+
aysModal.show();
|
|
1734
1763
|
}
|
|
1735
1764
|
|
|
1736
1765
|
function benchmarkCpu(numCycles) {
|
|
@@ -1778,8 +1807,8 @@ let last = 0;
|
|
|
1778
1807
|
function VirtualSpeedUpdater() {
|
|
1779
1808
|
this.cycles = 0;
|
|
1780
1809
|
this.time = 0;
|
|
1781
|
-
this.v =
|
|
1782
|
-
this.header =
|
|
1810
|
+
this.v = document.querySelector(".virtualMHz");
|
|
1811
|
+
this.header = document.getElementById("virtual-mhz-header");
|
|
1783
1812
|
this.speedy = false;
|
|
1784
1813
|
|
|
1785
1814
|
this.update = function (cycles, time, speedy) {
|
|
@@ -1792,11 +1821,11 @@ function VirtualSpeedUpdater() {
|
|
|
1792
1821
|
// MRG would be nice to graph instantaneous speed to get some idea where the time goes.
|
|
1793
1822
|
if (this.cycles) {
|
|
1794
1823
|
const thisMHz = this.cycles / this.time / 1000;
|
|
1795
|
-
this.v.
|
|
1824
|
+
this.v.textContent = thisMHz.toFixed(1);
|
|
1796
1825
|
if (this.cycles >= 10 * 2 * 1000 * 1000) {
|
|
1797
1826
|
this.cycles = this.time = 0;
|
|
1798
1827
|
}
|
|
1799
|
-
this.header.
|
|
1828
|
+
this.header.style.color = this.speedy ? "red" : "white";
|
|
1800
1829
|
}
|
|
1801
1830
|
setTimeout(this.display.bind(this), 3333);
|
|
1802
1831
|
};
|
|
@@ -1915,8 +1944,8 @@ function handleVisibilityChange() {
|
|
|
1915
1944
|
document.addEventListener("visibilitychange", handleVisibilityChange, false);
|
|
1916
1945
|
|
|
1917
1946
|
function updateDebugButtons() {
|
|
1918
|
-
|
|
1919
|
-
|
|
1947
|
+
debugPlay.disabled = running;
|
|
1948
|
+
debugPause.disabled = !running;
|
|
1920
1949
|
}
|
|
1921
1950
|
|
|
1922
1951
|
function go() {
|
|
@@ -1937,8 +1966,8 @@ function stop(debug) {
|
|
|
1937
1966
|
}
|
|
1938
1967
|
|
|
1939
1968
|
(function () {
|
|
1940
|
-
const
|
|
1941
|
-
const
|
|
1969
|
+
const resizeCubMonitor = document.getElementById("cub-monitor");
|
|
1970
|
+
const resizeCubMonitorPic = document.getElementById("cub-monitor-pic");
|
|
1942
1971
|
const borderReservedSize = parsedQuery.embed !== undefined ? 0 : 100;
|
|
1943
1972
|
const bottomReservedSize = parsedQuery.embed !== undefined ? 0 : 68;
|
|
1944
1973
|
|
|
@@ -1953,13 +1982,13 @@ function stop(debug) {
|
|
|
1953
1982
|
const visibleWidth = displayConfig.visibleWidth;
|
|
1954
1983
|
const visibleHeight = displayConfig.visibleHeight;
|
|
1955
1984
|
|
|
1956
|
-
const canvasNativeWidth =
|
|
1957
|
-
const canvasNativeHeight =
|
|
1985
|
+
const canvasNativeWidth = screenCanvas.getAttribute("width");
|
|
1986
|
+
const canvasNativeHeight = screenCanvas.getAttribute("height");
|
|
1958
1987
|
const desiredAspectRatio = imageOrigWidth / imageOrigHeight;
|
|
1959
1988
|
const minWidth = imageOrigWidth / 4;
|
|
1960
1989
|
const minHeight = imageOrigHeight / 4;
|
|
1961
1990
|
|
|
1962
|
-
let navbarHeight =
|
|
1991
|
+
let navbarHeight = document.getElementById("header-bar")?.offsetHeight || 0;
|
|
1963
1992
|
let width = Math.max(minWidth, window.innerWidth - borderReservedSize * 2);
|
|
1964
1993
|
let height = Math.max(minHeight, window.innerHeight - navbarHeight - bottomReservedSize);
|
|
1965
1994
|
if (width / height <= desiredAspectRatio) {
|
|
@@ -1984,11 +2013,14 @@ function stop(debug) {
|
|
|
1984
2013
|
finalCanvasWidth = scaledVisibleHeight * canvasAspect;
|
|
1985
2014
|
}
|
|
1986
2015
|
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2016
|
+
resizeCubMonitor.style.height = height + "px";
|
|
2017
|
+
resizeCubMonitor.style.width = width + "px";
|
|
2018
|
+
resizeCubMonitorPic.style.height = height + "px";
|
|
2019
|
+
resizeCubMonitorPic.style.width = width + "px";
|
|
2020
|
+
screenCanvas.style.width = finalCanvasWidth + "px";
|
|
2021
|
+
screenCanvas.style.height = finalCanvasHeight + "px";
|
|
2022
|
+
screenCanvas.style.left = canvasOrigLeft * containerScale + "px";
|
|
2023
|
+
screenCanvas.style.top = canvasOrigTop * containerScale + "px";
|
|
1992
2024
|
}
|
|
1993
2025
|
|
|
1994
2026
|
window.addEventListener("resize", resizeTv);
|
|
@@ -2008,10 +2040,10 @@ if (Object.hasOwn(parsedQuery, "pp-tos")) {
|
|
|
2008
2040
|
|
|
2009
2041
|
// Handy shortcuts. bench/profile stuff is delayed so that they can be
|
|
2010
2042
|
// safely run from the JS console in firefox.
|
|
2011
|
-
window.benchmarkCpu =
|
|
2012
|
-
window.profileCpu =
|
|
2013
|
-
window.benchmarkVideo =
|
|
2014
|
-
window.profileVideo =
|
|
2043
|
+
window.benchmarkCpu = utils.debounce(benchmarkCpu, 1);
|
|
2044
|
+
window.profileCpu = utils.debounce(profileCpu, 1);
|
|
2045
|
+
window.benchmarkVideo = utils.debounce(benchmarkVideo, 1);
|
|
2046
|
+
window.profileVideo = utils.debounce(profileVideo, 1);
|
|
2015
2047
|
window.go = go;
|
|
2016
2048
|
window.stop = stop;
|
|
2017
2049
|
window.soundChip = audioHandler.soundChip;
|
|
@@ -2064,7 +2096,7 @@ electron({
|
|
|
2064
2096
|
actions: {
|
|
2065
2097
|
"soft-reset": () => processor.reset(false),
|
|
2066
2098
|
"hard-reset": hardReset,
|
|
2067
|
-
"save-state": () =>
|
|
2099
|
+
"save-state": () => document.getElementById("save-state").click(),
|
|
2068
2100
|
rewind: () => rewindUI.open(),
|
|
2069
2101
|
},
|
|
2070
2102
|
});
|