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/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, modelsCompatible } from "./snapshot.js";
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
- $(".embed-hide").hide();
166
- $("body").css("background-color", "transparent");
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
- $(window).trigger("resize");
233
+ window.dispatchEvent(new Event("resize"));
234
234
  }
235
235
  },
236
236
  function onClose(changed) {
237
- parsedQuery = _.extend(parsedQuery, changed);
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().then(() => {});
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.find("img");
301
- img.hide();
300
+ const img = div.querySelector("img");
301
+ img.style.display = "none";
302
302
  if (!url) return;
303
- img.attr("src", url).on("load", function () {
303
+ img.addEventListener("load", function () {
304
304
  onload(div, img);
305
- img.show();
305
+ img.style.display = "";
306
306
  });
307
+ img.src = url;
307
308
  }
308
309
 
309
- sbBind($(".sidebar.left"), parsedQuery.sbLeft, function (div, img) {
310
- div.css({ left: -img.width() - 5 });
310
+ sbBind(document.querySelector(".sidebar.left"), parsedQuery.sbLeft, function (div, img) {
311
+ div.style.left = -img.naturalWidth - 5 + "px";
311
312
  });
312
- sbBind($(".sidebar.right"), parsedQuery.sbRight, function (div, img) {
313
- div.css({ right: -img.width() - 5 });
313
+ sbBind(document.querySelector(".sidebar.right"), parsedQuery.sbRight, function (div, img) {
314
+ div.style.right = -img.naturalWidth - 5 + "px";
314
315
  });
315
- sbBind($(".sidebar.bottom"), parsedQuery.sbBottom, function (div, img) {
316
- div.css({ bottom: -img.height() });
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 $screen = $("#screen");
331
+ const screenCanvas = document.getElementById("screen");
331
332
 
332
- const $errorDialog = $("#error-dialog");
333
- const $errorDialogModal = new bootstrap.Modal($errorDialog[0]);
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
- $errorDialog.find(".context").text(context);
347
- $errorDialog.find(".error").text(error);
348
- $errorDialogModal.show();
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($screen[0], filterClass) : new canvasLib.Canvas($screen[0]);
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 audioStatsNode = document.getElementById("audio-stats");
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: $("#audio-warning"),
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
- $(".initially-hidden").removeClass("initially-hidden");
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 $pastetext = $("#paste-text");
483
- $pastetext.closest("form").on("submit", (event) => event.preventDefault());
484
- $pastetext.on("paste", function (event) {
485
- const text = event.originalEvent.clipboardData.getData("text/plain");
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
- $pastetext.on("dragover", function (event) {
490
+ pastetext.addEventListener("dragover", function (event) {
489
491
  event.preventDefault();
490
492
  event.stopPropagation();
491
- event.originalEvent.dataTransfer.dropEffect = "copy";
493
+ event.dataTransfer.dropEffect = "copy";
492
494
  });
493
- $pastetext.on("drop", async function (event) {
495
+ pastetext.addEventListener("drop", async function (event) {
494
496
  utils.noteEvent("local", "drop");
495
- const file = event.originalEvent.dataTransfer.files[0];
496
- if (isSnapshotFile(file.name)) {
497
- await loadStateFromFile(file);
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 $cub = $("#cub-monitor");
504
- $cub.on("mousemove mousedown mouseup", function (evt) {
505
- audioHandler.tryResume().then(() => {});
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 cubOffset = $cub.offset();
508
- const screenOffset = $screen.offset();
509
- const x = (evt.offsetX - cubOffset.left + screenOffset.left) / $screen.width();
510
- const y = (evt.offsetY - cubOffset.top + screenOffset.top) / $screen.height();
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 $monitorPic = $("#cub-monitor-pic");
534
- $monitorPic.attr("src", config.image);
535
- $monitorPic.attr("alt", config.imageAlt);
536
- $monitorPic.attr("width", config.imageWidth);
537
- $monitorPic.attr("height", config.imageHeight);
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
- $(window).blur(function () {
550
+ window.addEventListener("blur", function () {
542
551
  keyboard.clearKeys();
543
552
  });
544
553
 
545
- $("#fs").click(function (event) {
546
- $screen[0].requestFullscreen();
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 $debugPause = $("#debug-pause");
553
- const $debugPlay = $("#debug-play");
554
- $debugPause.click(() => stop(true));
555
- $debugPlay.click(() => {
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
- $("#fsmenuitem").hide();
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 $micPermissionStatus = $("#micPermissionStatus");
698
- $micPermissionStatus.text("Requesting microphone access...");
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
- $micPermissionStatus.text("Microphone connected successfully");
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
- $micPermissionStatus.text(`Error: ${microphoneInput.getErrorMessage() || "Unknown error"}`);
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.on("showError", ({ context, error }) => showError(context, error));
740
- keyboard.on("pause", () => stop(false));
741
- keyboard.on("resume", () => go());
742
- keyboard.on("break", (pressed) => {
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 (pressed) utils.noteEvent("keyboard", "press", "break");
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().then(() => {});
849
- ensureMicrophoneRunning().then(() => {});
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.emit("media-changed", { disc1: name });
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.emit("media-changed", { disc2: name });
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.emit("media-changed", { tape: name });
879
+ config.dispatchEvent(new CustomEvent("media-changed", { detail: { tape: name } }));
872
880
  }
873
881
 
874
882
  function sthClearList() {
875
- $("#sth-list li:not(.template)").remove();
883
+ for (const el of document.querySelectorAll("#sth-list li:not(.template)")) el.remove();
876
884
  }
877
885
 
878
886
  function sthStartLoad() {
879
- const $sth = $("#sth .loading");
880
- $sth.text("Loading catalog from STH archive");
881
- $sth.show();
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 = $("#sth-list");
932
- $("#sth .loading").hide();
933
- const template = sthList.find(".template");
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 cat = all.slice(0, MaxAtATime);
946
+ const batch = all.slice(0, MaxAtATime);
939
947
  const remaining = all.slice(MaxAtATime);
940
- const filter = $("#sth-filter").val();
941
- $.each(cat, function (_, cat) {
942
- const row = template.clone().removeClass("template").appendTo(sthList);
943
- row.find(".name").text(cat);
944
- $(row).on("click", function () {
945
- onClick(cat);
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.toggle(cat.toLowerCase().indexOf(filter) >= 0);
949
- });
950
- if (all.length) _.delay(doSome, Delay, remaining);
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 $sthLoading = $("#sth .loading");
959
- $sthLoading.text("There was an error accessing the STH archive");
960
- $sthLoading.show();
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 $sthAutoboot = $("#sth .autoboot");
968
- $sthAutoboot.click(function () {
969
- if ($sthAutoboot.prop("checked")) {
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
- $(document).on("click", "a.sth", function () {
978
- const type = $(this).data("id");
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
- $("#sth-list li:not(.template)").each(function () {
991
- const el = $(this);
992
- el.toggle(el.text().toLowerCase().indexOf(filter) >= 0);
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
- $("#sth-filter").on("change keyup", function () {
997
- setSthFilter($("#sth-filter").val());
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
- $("#disc_load").on("change", async function (evt) {
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
- $("#fs_load").on("change", async function (evt) {
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
- $("#tape_load").on("change", async function (evt) {
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
- $("#tapes").modal("hide");
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 $(".modal:visible").length !== 0;
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 $loadingDialog = $("#loading-dialog");
1245
- const $loadingDialogModal = new bootstrap.Modal($loadingDialog[0]);
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
- $loadingDialog.find(".loading").text(msg);
1249
- $("#google-drive-auth").hide();
1250
- $loadingDialogModal.show();
1260
+ loadingDialog.querySelector(".loading").textContent = msg;
1261
+ googleDriveAuth.style.display = "none";
1262
+ loadingDialogModal.show();
1251
1263
  }
1252
1264
 
1253
1265
  function loadingFinished(error) {
1254
- $("#google-drive-auth").hide();
1266
+ googleDriveAuth.style.display = "none";
1255
1267
  if (error) {
1256
- $loadingDialogModal.show();
1257
- $loadingDialog.find(".loading").text("Error: " + error);
1268
+ loadingDialogModal.show();
1269
+ loadingDialog.querySelector(".loading").textContent = "Error: " + error;
1258
1270
  setTimeout(function () {
1259
- $loadingDialogModal.hide();
1271
+ loadingDialogModal.hide();
1260
1272
  }, 5000);
1261
1273
  } else {
1262
- $loadingDialogModal.hide();
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
- $googleDrive.find(".loading").text("There was an error accessing your Google Drive account: " + err);
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
- $("#google-drive-auth form").on("submit", async function (e) {
1279
- $("#google-drive-auth").hide();
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
- $("#google-drive-auth").show();
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
- $(".if-drive-available").hide();
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
- $(".if-drive-available").show();
1338
+ for (const el of document.querySelectorAll(".if-drive-available")) el.style.display = "";
1325
1339
  await gdAuth(true);
1326
1340
  }
1327
1341
  })();
1328
- const $googleDrive = $("#google-drive");
1329
- const $googleDriveModal = new bootstrap.Modal($googleDrive[0]);
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
- $googleDriveModal.show();
1346
+ googleDriveModal.show();
1334
1347
  }
1335
1348
  return false;
1336
1349
  });
1337
- $googleDrive[0].addEventListener("show.bs.modal", async function () {
1338
- $googleDrive.find(".loading").text("Loading...").show();
1339
- $googleDrive.find("li").not(".template").remove();
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 = $googleDrive.find(".list");
1342
- $googleDrive.find(".loading").hide();
1343
- const template = dbList.find(".template");
1344
- $.each(cat, function (_, cat) {
1345
- const row = template.clone().removeClass("template").appendTo(dbList);
1346
- row.find(".name").text(cat.name);
1347
- $(row).on("click", function () {
1348
- utils.noteEvent("google-drive", "click", cat.name);
1349
- setDisc1Image(`gd:${cat.id}/${cat.name}`);
1350
- gdLoad(cat).then(function (ssd) {
1351
- processor.fdc.loadDisc(0, ssd);
1352
- });
1353
- $googleDriveModal.hide();
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 = $("#disc-list");
1358
- const template = discList.find(".template");
1359
- $.each(availableImages, function (i, image) {
1360
- const elem = template.clone().removeClass("template").appendTo(discList);
1361
- elem.find(".name").text(image.name);
1362
- elem.find(".description").text(image.desc);
1363
- $(elem).on("click", function () {
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
- $("#google-drive form").on("submit", async function (e) {
1389
+ document.querySelector("#google-drive form").addEventListener("submit", async function (e) {
1374
1390
  e.preventDefault();
1375
- let name = $("#google-drive .disc-name").val();
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
- $googleDriveModal.hide();
1395
+ googleDriveModal.hide();
1380
1396
  popupLoading("Creating '" + name + "' on Google Drive");
1381
1397
 
1382
1398
  let data;
1383
- if ($("#google-drive .create-from-existing").prop("checked")) {
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
- $("#download-drive-link").on("click", function () {
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
- $("#download-drive-hfe-link").on("click", function () {
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
- $("#download-filestore-link").on("click", function () {
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
- $("#hard-reset").click(function (event) {
1458
+ document.getElementById("hard-reset").addEventListener("click", function (event) {
1443
1459
  hardReset();
1444
1460
  event.preventDefault();
1445
1461
  });
1446
1462
 
1447
- $("#soft-reset").click(function (event) {
1463
+ document.getElementById("soft-reset").addEventListener("click", function (event) {
1448
1464
  processor.reset(false);
1449
1465
  event.preventDefault();
1450
1466
  });
1451
1467
 
1452
- $("#save-state").click(async function (event) {
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 (!modelsCompatible(snapshot.model, model.name)) {
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
- return lower.endsWith(".snp") || lower.endsWith(".json") || lower.endsWith(".json.gz") || lower.endsWith(".gz");
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
- $("#load-state").on("change", async function (event) {
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
- $("#tape-menu a").on("click", function (e) {
1545
- const type = $(e.target).attr("data-id");
1546
- if (type === undefined) return;
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
- processor.acia.rewindTape();
1552
- } else {
1553
- console.log("unknown type", type);
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 = $("#" + name);
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.toggleClass("on", on);
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
- startPromise
1675
- .then(async () => {
1697
+ (async () => {
1698
+ try {
1699
+ await startPromise;
1700
+
1676
1701
  switch (needsAutoboot) {
1677
1702
  case "boot":
1678
- $sthAutoboot.prop("checked", true);
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
- $sthAutoboot.prop("checked", false);
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 $ays = $("#are-you-sure");
1723
- const $aysModal = new bootstrap.Modal($ays[0]);
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
- $ays.find(".context").text(message);
1727
- $ays.find(".ays-yes").text(yesText);
1728
- $ays.find(".ays-no").text(noText);
1729
- $ays.find(".ays-yes").one("click", function () {
1730
- $aysModal.hide();
1731
- yesFunc();
1732
- });
1733
- $aysModal.show();
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 = $(".virtualMHz");
1782
- this.header = $("#virtual-mhz-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.text(thisMHz.toFixed(1));
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.css("color", this.speedy ? "red" : "white");
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
- $debugPlay.attr("disabled", running);
1919
- $debugPause.attr("disabled", !running);
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 $cubMonitor = $("#cub-monitor");
1941
- const $cubMonitorPic = $("#cub-monitor-pic");
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 = $screen.attr("width");
1957
- const canvasNativeHeight = $screen.attr("height");
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 = $("#header-bar").outerHeight() || 0;
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
- $cubMonitor.height(height).width(width);
1988
- $cubMonitorPic.height(height).width(width);
1989
- $screen.width(finalCanvasWidth).height(finalCanvasHeight);
1990
- $screen.css("left", canvasOrigLeft * containerScale + "px");
1991
- $screen.css("top", canvasOrigTop * containerScale + "px");
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 = _.debounce(benchmarkCpu, 1);
2012
- window.profileCpu = _.debounce(profileCpu, 1);
2013
- window.benchmarkVideo = _.debounce(benchmarkVideo, 1);
2014
- window.profileVideo = _.debounce(profileVideo, 1);
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": () => $("#save-state").trigger("click"),
2099
+ "save-state": () => document.getElementById("save-state").click(),
2068
2100
  rewind: () => rewindUI.open(),
2069
2101
  },
2070
2102
  });