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/README.md
CHANGED
|
@@ -177,7 +177,7 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
177
177
|
Doesn't support the sth: pseudo URL unlike `disc` and `tape`, but if given a ZIP file will attempt to use the `.rom`
|
|
178
178
|
file assumed to be within.
|
|
179
179
|
- (mostly internal use) `logFdcCommands`, `logFdcStateChanges` - turn on logging in the disc controller.
|
|
180
|
-
- `audioDebug
|
|
180
|
+
- `audioDebug` - show audio queue stats chart.
|
|
181
181
|
|
|
182
182
|
## Patches
|
|
183
183
|
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.12.0",
|
|
11
11
|
"//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=22.12.0"
|
|
@@ -30,13 +30,8 @@
|
|
|
30
30
|
"argparse": "^2.0.1",
|
|
31
31
|
"bootstrap": "^5.3.8",
|
|
32
32
|
"bootswatch": "^5.3.8",
|
|
33
|
-
"event-emitter-es6": "^1.1.5",
|
|
34
|
-
"fflate": "^0.8.2",
|
|
35
|
-
"jquery": "^4.0.0",
|
|
36
|
-
"pako": "^2.1.0",
|
|
37
33
|
"sharp": "^0.34.5",
|
|
38
|
-
"smoothie": "^1.36.1"
|
|
39
|
-
"underscore": "^1.13.7"
|
|
34
|
+
"smoothie": "^1.36.1"
|
|
40
35
|
},
|
|
41
36
|
"devDependencies": {
|
|
42
37
|
"@eslint/js": "^10.0.1",
|
package/src/6502.js
CHANGED
|
@@ -1106,7 +1106,7 @@ export class Cpu6502 extends Base6502 {
|
|
|
1106
1106
|
const ramRomOs = this.ramRomOs;
|
|
1107
1107
|
let data = await utils.loadData(name);
|
|
1108
1108
|
if (/\.zip/i.test(name)) {
|
|
1109
|
-
data = utils.unzipRomImage(data).data;
|
|
1109
|
+
data = (await utils.unzipRomImage(data)).data;
|
|
1110
1110
|
}
|
|
1111
1111
|
ramRomOs.set(data, offset);
|
|
1112
1112
|
}
|
|
@@ -1208,6 +1208,13 @@ export class Cpu6502 extends Base6502 {
|
|
|
1208
1208
|
if (state.roms) {
|
|
1209
1209
|
this.ramRomOs.set(state.roms.slice(0, 16 * 16384), this.romOffset);
|
|
1210
1210
|
}
|
|
1211
|
+
// Selectively overwrite individual sideways RAM banks (e.g. from BeebEm UEF import)
|
|
1212
|
+
// without touching ROM banks that jsbeeb has already loaded.
|
|
1213
|
+
if (state.swRamBanks) {
|
|
1214
|
+
for (const [bank, data] of Object.entries(state.swRamBanks)) {
|
|
1215
|
+
this.ramRomOs.set(data.slice(0, 16384), this.romOffset + Number(bank) * 16384);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1211
1218
|
this.videoDisplayPage = state.videoDisplayPage;
|
|
1212
1219
|
this.music5000PageSel = state.music5000PageSel;
|
|
1213
1220
|
|
package/src/app/app.js
CHANGED
|
@@ -242,7 +242,7 @@ const template = [
|
|
|
242
242
|
const result = await dialog.showOpenDialog(browserWindow, {
|
|
243
243
|
title: "Load emulator state",
|
|
244
244
|
filters: [
|
|
245
|
-
{ name: "Snapshot files", extensions: ["gz", "json", "snp"] },
|
|
245
|
+
{ name: "Snapshot files", extensions: ["gz", "json", "snp", "uef"] },
|
|
246
246
|
{ name: "All files", extensions: ["*"] },
|
|
247
247
|
],
|
|
248
248
|
properties: ["openFile"],
|
package/src/app/electron.js
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
// Electron integration for jsbeeb desktop application.
|
|
4
4
|
// Handles IPC communication for loading disc/tape images and showing modals from Electron's main process.
|
|
5
5
|
|
|
6
|
-
export let initialise = function () {};
|
|
7
|
-
|
|
8
6
|
function init(args) {
|
|
9
7
|
const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions, config } = args;
|
|
10
8
|
const api = window.electronAPI;
|
|
@@ -57,15 +55,17 @@ function init(args) {
|
|
|
57
55
|
|
|
58
56
|
// Save settings when they change
|
|
59
57
|
if (config) {
|
|
60
|
-
config.
|
|
61
|
-
api.saveSettings(
|
|
58
|
+
config.addEventListener("settings-changed", (e) => {
|
|
59
|
+
api.saveSettings(e.detail);
|
|
62
60
|
});
|
|
63
|
-
config.
|
|
64
|
-
api.saveSettings(
|
|
61
|
+
config.addEventListener("media-changed", (e) => {
|
|
62
|
+
api.saveSettings(e.detail);
|
|
65
63
|
});
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
export function initialise(args) {
|
|
68
|
+
if (typeof window.electronAPI !== "undefined") {
|
|
69
|
+
init(args);
|
|
70
|
+
}
|
|
71
71
|
}
|
package/src/bem-snapshot.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
import { decompress } from "./utils.js";
|
|
2
3
|
|
|
3
4
|
// B-em snapshot format parser (versions 1 and 3).
|
|
4
5
|
// v1 (BEMSNAP1): Fixed-size 327,885 byte packed struct. Reference: beebjit state.c
|
|
5
6
|
// v3 (BEMSNAP3): Section-based with key+size headers, zlib-compressed memory. Reference: b-em savestate.c
|
|
6
7
|
|
|
8
|
+
import { volumeTable, buildVideoState, buildSnapshot } from "./snapshot-helpers.js";
|
|
9
|
+
|
|
7
10
|
const BemV1Size = 327885;
|
|
8
11
|
|
|
9
12
|
// v1 struct offsets
|
|
@@ -113,7 +116,7 @@ function convertViaState(bemVia, ic32) {
|
|
|
113
116
|
t2l: bemVia.t2l,
|
|
114
117
|
t1c: bemVia.t1c,
|
|
115
118
|
t2c: bemVia.t2c,
|
|
116
|
-
t1hit: !!bemVia.t1hit,
|
|
119
|
+
t1hit: bemVia.acr & 0x40 ? false : !!bemVia.t1hit,
|
|
117
120
|
t2hit: !!bemVia.t2hit,
|
|
118
121
|
portapins: 0xff,
|
|
119
122
|
portbpins: 0xff,
|
|
@@ -134,17 +137,6 @@ function convertViaState(bemVia, ic32) {
|
|
|
134
137
|
return result;
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
// Volume lookup table matching soundchip.js
|
|
138
|
-
const volumeTable = new Float32Array(16);
|
|
139
|
-
(() => {
|
|
140
|
-
let f = 1.0;
|
|
141
|
-
for (let i = 0; i < 15; ++i) {
|
|
142
|
-
volumeTable[i] = f / 4;
|
|
143
|
-
f *= Math.pow(10, -0.1);
|
|
144
|
-
}
|
|
145
|
-
volumeTable[15] = 0;
|
|
146
|
-
})();
|
|
147
|
-
|
|
148
140
|
function convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift) {
|
|
149
141
|
const registers = new Uint16Array(4);
|
|
150
142
|
const counter = new Float32Array(4);
|
|
@@ -180,178 +172,6 @@ function convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift) {
|
|
|
180
172
|
};
|
|
181
173
|
}
|
|
182
174
|
|
|
183
|
-
function buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters) {
|
|
184
|
-
const regs = new Uint8Array(32);
|
|
185
|
-
regs.set(crtcRegs.slice(0, 18));
|
|
186
|
-
const actualPal = new Uint8Array(16);
|
|
187
|
-
for (let i = 0; i < 16; i++) actualPal[i] = ulaPalette[i] & 0x0f;
|
|
188
|
-
|
|
189
|
-
// Use NULA collook if provided, otherwise use default BBC palette
|
|
190
|
-
const collook =
|
|
191
|
-
nulaCollook ||
|
|
192
|
-
new Int32Array([
|
|
193
|
-
0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff, 0xff000000,
|
|
194
|
-
0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff,
|
|
195
|
-
]);
|
|
196
|
-
|
|
197
|
-
// Compute ulaPal from actualPal + collook, matching jsbeeb's Ula._recomputeUlaPal
|
|
198
|
-
const flashEnabled = !!(ulaControl & 1);
|
|
199
|
-
const defaultFlash = new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]);
|
|
200
|
-
const flash = defaultFlash;
|
|
201
|
-
const ulaPal = new Int32Array(16);
|
|
202
|
-
for (let i = 0; i < 16; i++) {
|
|
203
|
-
const palVal = actualPal[i];
|
|
204
|
-
let colour = collook[(palVal & 0xf) ^ 7];
|
|
205
|
-
if (palVal & 8 && flashEnabled && flash[(palVal & 7) ^ 7]) {
|
|
206
|
-
colour = collook[palVal & 0xf];
|
|
207
|
-
}
|
|
208
|
-
ulaPal[i] = colour;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
regs,
|
|
213
|
-
bitmapX: 0,
|
|
214
|
-
bitmapY: 0,
|
|
215
|
-
oddClock: false,
|
|
216
|
-
frameCount: 0,
|
|
217
|
-
doEvenFrameLogic: false,
|
|
218
|
-
isEvenRender: true,
|
|
219
|
-
lastRenderWasEven: false,
|
|
220
|
-
firstScanline: true,
|
|
221
|
-
inHSync: false,
|
|
222
|
-
inVSync: false,
|
|
223
|
-
hadVSyncThisRow: false,
|
|
224
|
-
checkVertAdjust: false,
|
|
225
|
-
endOfMainLatched: false,
|
|
226
|
-
endOfVertAdjustLatched: false,
|
|
227
|
-
endOfFrameLatched: false,
|
|
228
|
-
inVertAdjust: false,
|
|
229
|
-
inDummyRaster: false,
|
|
230
|
-
hpulseWidth: regs[3] & 0x0f,
|
|
231
|
-
vpulseWidth: (regs[3] & 0xf0) >>> 4,
|
|
232
|
-
hpulseCounter: 0,
|
|
233
|
-
vpulseCounter: 0,
|
|
234
|
-
dispEnabled: 0x3f,
|
|
235
|
-
horizCounter: crtcCounters ? crtcCounters.hc : 0,
|
|
236
|
-
vertCounter: crtcCounters ? crtcCounters.vc : 0,
|
|
237
|
-
scanlineCounter: crtcCounters ? crtcCounters.sc : 0,
|
|
238
|
-
vertAdjustCounter: 0,
|
|
239
|
-
addr: crtcCounters ? crtcCounters.ma : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
240
|
-
lineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
241
|
-
nextLineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
242
|
-
ulactrl: ulaControl,
|
|
243
|
-
pixelsPerChar: ulaControl & 0x10 ? 8 : 16,
|
|
244
|
-
halfClock: !(ulaControl & 0x10),
|
|
245
|
-
ulaMode: (ulaControl >>> 2) & 3,
|
|
246
|
-
teletextMode: !!(ulaControl & 2),
|
|
247
|
-
displayEnableSkew: Math.min((regs[8] & 0x30) >>> 4, 2),
|
|
248
|
-
ulaPal,
|
|
249
|
-
actualPal,
|
|
250
|
-
cursorOn: false,
|
|
251
|
-
cursorOff: false,
|
|
252
|
-
cursorOnThisFrame: false,
|
|
253
|
-
cursorDrawIndex: 0,
|
|
254
|
-
cursorPos: (regs[15] | (regs[14] << 8)) & 0x3fff,
|
|
255
|
-
interlacedSyncAndVideo: (regs[8] & 3) === 3,
|
|
256
|
-
screenSubtract: 0,
|
|
257
|
-
ula: {
|
|
258
|
-
collook: collook.slice(),
|
|
259
|
-
flash: new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]),
|
|
260
|
-
paletteWriteFlag: false,
|
|
261
|
-
paletteFirstByte: 0,
|
|
262
|
-
paletteMode: 0,
|
|
263
|
-
horizontalOffset: 0,
|
|
264
|
-
leftBlank: 0,
|
|
265
|
-
disabled: false,
|
|
266
|
-
attributeMode: 0,
|
|
267
|
-
attributeText: 0,
|
|
268
|
-
},
|
|
269
|
-
crtc: { curReg: 0 },
|
|
270
|
-
teletext: {
|
|
271
|
-
prevCol: 0,
|
|
272
|
-
col: 7,
|
|
273
|
-
bg: 0,
|
|
274
|
-
sep: false,
|
|
275
|
-
dbl: false,
|
|
276
|
-
oldDbl: false,
|
|
277
|
-
secondHalfOfDouble: false,
|
|
278
|
-
wasDbl: false,
|
|
279
|
-
gfx: false,
|
|
280
|
-
flash: false,
|
|
281
|
-
flashOn: false,
|
|
282
|
-
flashTime: 0,
|
|
283
|
-
heldChar: 0,
|
|
284
|
-
holdChar: false,
|
|
285
|
-
dataQueue: [0, 0, 0, 0],
|
|
286
|
-
scanlineCounter: 0,
|
|
287
|
-
levelDEW: false,
|
|
288
|
-
levelDISPTMG: false,
|
|
289
|
-
levelRA0: false,
|
|
290
|
-
nextGlyphs: "normal",
|
|
291
|
-
curGlyphs: "normal",
|
|
292
|
-
heldGlyphs: "normal",
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const DefaultAcia = {
|
|
298
|
-
sr: 0x02,
|
|
299
|
-
cr: 0x00,
|
|
300
|
-
dr: 0x00,
|
|
301
|
-
rs423Selected: false,
|
|
302
|
-
motorOn: false,
|
|
303
|
-
tapeCarrierCount: 0,
|
|
304
|
-
tapeDcdLineLevel: false,
|
|
305
|
-
hadDcdHigh: false,
|
|
306
|
-
serialReceiveRate: 19200,
|
|
307
|
-
serialReceiveCyclesPerByte: 0,
|
|
308
|
-
txCompleteTaskOffset: null,
|
|
309
|
-
runTapeTaskOffset: null,
|
|
310
|
-
runRs423TaskOffset: null,
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
const DefaultAdc = { status: 0x40, low: 0x00, high: 0x00, taskOffset: null };
|
|
314
|
-
|
|
315
|
-
function buildSnapshot(modelName, cpuState, ram, roms, sysvia, uservia, video, soundChip) {
|
|
316
|
-
return {
|
|
317
|
-
format: "jsbeeb-snapshot",
|
|
318
|
-
version: 1,
|
|
319
|
-
model: modelName,
|
|
320
|
-
timestamp: new Date().toISOString(),
|
|
321
|
-
importedFrom: "b-em",
|
|
322
|
-
state: {
|
|
323
|
-
a: cpuState.a,
|
|
324
|
-
x: cpuState.x,
|
|
325
|
-
y: cpuState.y,
|
|
326
|
-
s: cpuState.s,
|
|
327
|
-
pc: cpuState.pc,
|
|
328
|
-
p: cpuState.flags | 0x30,
|
|
329
|
-
nmiLevel: !!cpuState.nmi,
|
|
330
|
-
nmiEdge: false,
|
|
331
|
-
halted: false,
|
|
332
|
-
takeInt: false,
|
|
333
|
-
romsel: cpuState.fe30 ?? 0,
|
|
334
|
-
acccon: cpuState.fe34 ?? 0,
|
|
335
|
-
videoDisplayPage: 0,
|
|
336
|
-
currentCycles: 0,
|
|
337
|
-
targetCycles: 0,
|
|
338
|
-
cycleSeconds: 0,
|
|
339
|
-
peripheralCycles: 0,
|
|
340
|
-
videoCycles: 0,
|
|
341
|
-
music5000PageSel: 0,
|
|
342
|
-
ram,
|
|
343
|
-
roms,
|
|
344
|
-
scheduler: { epoch: 0 },
|
|
345
|
-
sysvia,
|
|
346
|
-
uservia,
|
|
347
|
-
video,
|
|
348
|
-
soundChip,
|
|
349
|
-
acia: { ...DefaultAcia },
|
|
350
|
-
adc: { ...DefaultAdc },
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
175
|
// ── V1 parser (BEMSNAP1, fixed struct) ──────────────────────────────
|
|
356
176
|
|
|
357
177
|
function parseBemV1(buffer) {
|
|
@@ -401,6 +221,7 @@ function parseBemV1(buffer) {
|
|
|
401
221
|
const snShift = view.getUint16(soundOff + 53, true);
|
|
402
222
|
|
|
403
223
|
return buildSnapshot(
|
|
224
|
+
"b-em",
|
|
404
225
|
"B",
|
|
405
226
|
cpuState,
|
|
406
227
|
ram,
|
|
@@ -434,39 +255,6 @@ function readString(data, pos) {
|
|
|
434
255
|
return str;
|
|
435
256
|
}
|
|
436
257
|
|
|
437
|
-
async function inflateRaw(compressedData) {
|
|
438
|
-
// Use DecompressionStream (available in modern browsers and Node 18+)
|
|
439
|
-
if (typeof DecompressionStream !== "undefined") {
|
|
440
|
-
const ds = new DecompressionStream("deflate");
|
|
441
|
-
const writer = ds.writable.getWriter();
|
|
442
|
-
const reader = ds.readable.getReader();
|
|
443
|
-
const chunks = [];
|
|
444
|
-
|
|
445
|
-
// Start reading before writing to avoid backpressure deadlock
|
|
446
|
-
const readPromise = (async () => {
|
|
447
|
-
for (;;) {
|
|
448
|
-
const { done, value } = await reader.read();
|
|
449
|
-
if (done) break;
|
|
450
|
-
chunks.push(value);
|
|
451
|
-
}
|
|
452
|
-
})();
|
|
453
|
-
|
|
454
|
-
await writer.write(compressedData);
|
|
455
|
-
await writer.close();
|
|
456
|
-
await readPromise;
|
|
457
|
-
|
|
458
|
-
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
459
|
-
const result = new Uint8Array(totalLength);
|
|
460
|
-
let offset = 0;
|
|
461
|
-
for (const chunk of chunks) {
|
|
462
|
-
result.set(chunk, offset);
|
|
463
|
-
offset += chunk.length;
|
|
464
|
-
}
|
|
465
|
-
return result;
|
|
466
|
-
}
|
|
467
|
-
throw new Error("Zlib decompression not available (need DecompressionStream API)");
|
|
468
|
-
}
|
|
469
|
-
|
|
470
258
|
function parseBemV3(buffer) {
|
|
471
259
|
const bytes = new Uint8Array(buffer);
|
|
472
260
|
let offset = 8; // Skip "BEMSNAP3" signature
|
|
@@ -523,7 +311,7 @@ function parseBemV3(buffer) {
|
|
|
523
311
|
if (memSection) {
|
|
524
312
|
// Memory is zlib-compressed; decompression is async.
|
|
525
313
|
// Decompressed layout: 2 bytes (fe30, fe34) + 64KB RAM + 256KB ROM
|
|
526
|
-
return
|
|
314
|
+
return decompress(memSection.data, "deflate").then((memData) => {
|
|
527
315
|
cpuState.fe30 = memData[0];
|
|
528
316
|
cpuState.fe34 = memData[1];
|
|
529
317
|
const ramStart = 2;
|
|
@@ -669,6 +457,7 @@ function finishV3Parse(modelName, cpuState, ram, roms, sections) {
|
|
|
669
457
|
}
|
|
670
458
|
|
|
671
459
|
return buildSnapshot(
|
|
460
|
+
"b-em",
|
|
672
461
|
modelName,
|
|
673
462
|
cpuState,
|
|
674
463
|
ram,
|
package/src/config.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
import
|
|
3
|
-
import EventEmitter from "event-emitter-es6";
|
|
4
|
-
import { findModel } from "./models.js";
|
|
2
|
+
import { allModels, findModel } from "./models.js";
|
|
5
3
|
import { getFilterForMode } from "./canvas.js";
|
|
6
4
|
|
|
7
|
-
export class Config extends
|
|
5
|
+
export class Config extends EventTarget {
|
|
8
6
|
constructor(onChange, onClose) {
|
|
9
7
|
super();
|
|
10
8
|
this.onChange = onChange;
|
|
@@ -12,8 +10,8 @@ export class Config extends EventEmitter {
|
|
|
12
10
|
this.changed = {};
|
|
13
11
|
this.model = null;
|
|
14
12
|
this.coProcessor = null;
|
|
15
|
-
const
|
|
16
|
-
|
|
13
|
+
const configuration = document.getElementById("configuration");
|
|
14
|
+
configuration.addEventListener("show.bs.modal", () => {
|
|
17
15
|
this.changed = {};
|
|
18
16
|
this.setDropdownText(this.model.name);
|
|
19
17
|
this.set65c02(this.model.tube);
|
|
@@ -23,116 +21,134 @@ export class Config extends EventEmitter {
|
|
|
23
21
|
this.setEconet(this.model.hasEconet);
|
|
24
22
|
});
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
configuration.addEventListener("hide.bs.modal", () => {
|
|
27
25
|
this.onClose(this.changed);
|
|
28
26
|
if (Object.keys(this.changed).length > 0) {
|
|
29
|
-
this.
|
|
27
|
+
this.dispatchEvent(new CustomEvent("settings-changed", { detail: this.changed }));
|
|
30
28
|
}
|
|
31
29
|
});
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const modelMenu = document.querySelector(".model-menu");
|
|
32
|
+
for (const model of allModels) {
|
|
33
|
+
if (model.synonyms.length === 0) continue; // skip non-selectable models (e.g. Tube65C02)
|
|
34
|
+
const li = document.createElement("li");
|
|
35
|
+
const a = document.createElement("a");
|
|
36
|
+
a.href = "#";
|
|
37
|
+
a.className = "dropdown-item";
|
|
38
|
+
a.dataset.target = model.synonyms[0];
|
|
39
|
+
a.textContent = model.name;
|
|
40
|
+
li.appendChild(a);
|
|
41
|
+
modelMenu.appendChild(li);
|
|
42
|
+
}
|
|
43
|
+
modelMenu.addEventListener("click", (e) => {
|
|
44
|
+
const link = e.target.closest("a[data-target]");
|
|
45
|
+
if (!link) return;
|
|
46
|
+
this.changed.model = link.dataset.target;
|
|
47
|
+
this.setDropdownText(link.textContent);
|
|
36
48
|
});
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
this.changed.coProcessor =
|
|
40
|
-
|
|
50
|
+
document.getElementById("65c02").addEventListener("click", () => {
|
|
51
|
+
this.changed.coProcessor = document.getElementById("65c02").checked;
|
|
52
|
+
document.getElementById("tubeCpuMultiplier").disabled = !document.getElementById("65c02").checked;
|
|
41
53
|
});
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
const val = parseInt(
|
|
45
|
-
|
|
55
|
+
document.getElementById("tubeCpuMultiplier").addEventListener("input", () => {
|
|
56
|
+
const val = parseInt(document.getElementById("tubeCpuMultiplier").value, 10);
|
|
57
|
+
document.getElementById("tubeCpuMultiplierValue").textContent = val;
|
|
46
58
|
this.changed.tubeCpuMultiplier = val;
|
|
47
59
|
});
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
this.changed.hasTeletextAdaptor =
|
|
61
|
+
document.getElementById("hasTeletextAdaptor").addEventListener("click", () => {
|
|
62
|
+
this.changed.hasTeletextAdaptor = document.getElementById("hasTeletextAdaptor").checked;
|
|
51
63
|
});
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
this.changed.hasEconet =
|
|
65
|
+
document.getElementById("hasEconet").addEventListener("click", () => {
|
|
66
|
+
this.changed.hasEconet = document.getElementById("hasEconet").checked;
|
|
55
67
|
});
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
this.changed.hasMusic5000 =
|
|
69
|
+
document.getElementById("hasMusic5000").addEventListener("click", () => {
|
|
70
|
+
this.changed.hasMusic5000 = document.getElementById("hasMusic5000").checked;
|
|
59
71
|
});
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
for (const link of document.querySelectorAll(".keyboard-menu a")) {
|
|
74
|
+
link.addEventListener("click", (e) => {
|
|
75
|
+
const keyLayout = e.target.dataset.target;
|
|
76
|
+
this.changed.keyLayout = keyLayout;
|
|
77
|
+
this.setKeyLayout(keyLayout);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
for (const option of document.querySelectorAll(".mic-channel-option")) {
|
|
82
|
+
option.addEventListener("click", (e) => {
|
|
83
|
+
const channelString = e.target.dataset.channel;
|
|
84
|
+
const channel = channelString === "" ? undefined : parseInt(channelString, 10);
|
|
85
|
+
this.changed.microphoneChannel = channel;
|
|
86
|
+
this.setMicrophoneChannel(channel);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
this.changed.mouseJoystickEnabled =
|
|
90
|
+
document.getElementById("mouseJoystickEnabled").addEventListener("click", () => {
|
|
91
|
+
this.changed.mouseJoystickEnabled = document.getElementById("mouseJoystickEnabled").checked;
|
|
76
92
|
});
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
this.changed.speechOutput =
|
|
94
|
+
document.getElementById("speechOutput").addEventListener("click", () => {
|
|
95
|
+
this.changed.speechOutput = document.getElementById("speechOutput").checked;
|
|
80
96
|
});
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
for (const option of document.querySelectorAll(".display-mode-option")) {
|
|
99
|
+
option.addEventListener("click", (e) => {
|
|
100
|
+
const mode = e.target.dataset.mode;
|
|
101
|
+
this.changed.displayMode = mode;
|
|
102
|
+
this.setDisplayMode(mode);
|
|
103
|
+
this.onChange({ displayMode: mode });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
setMicrophoneChannel(channel) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} else {
|
|
94
|
-
$(".mic-channel-text").text("Disabled");
|
|
95
|
-
}
|
|
109
|
+
const text = channel !== undefined ? `Channel ${channel}` : "Disabled";
|
|
110
|
+
for (const el of document.querySelectorAll(".mic-channel-text")) el.textContent = text;
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
setMouseJoystickEnabled(enabled) {
|
|
99
|
-
|
|
114
|
+
document.getElementById("mouseJoystickEnabled").checked = !!enabled;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
setSpeechOutput(enabled) {
|
|
103
|
-
|
|
118
|
+
document.getElementById("speechOutput").checked = !!enabled;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
setDisplayMode(mode) {
|
|
107
122
|
const config = getFilterForMode(mode).getDisplayConfig();
|
|
108
|
-
|
|
123
|
+
for (const el of document.querySelectorAll(".display-mode-text")) el.textContent = config.name;
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
setModel(modelName) {
|
|
112
127
|
this.model = findModel(modelName);
|
|
113
|
-
|
|
128
|
+
for (const el of document.querySelectorAll(".bbc-model")) el.textContent = this.model.name;
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
setKeyLayout(keyLayout) {
|
|
117
|
-
|
|
132
|
+
const text = keyLayout[0].toUpperCase() + keyLayout.substring(1);
|
|
133
|
+
for (const el of document.querySelectorAll(".keyboard-layout")) el.textContent = text;
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
set65c02(enabled) {
|
|
121
137
|
enabled = !!enabled;
|
|
122
|
-
|
|
138
|
+
document.getElementById("65c02").checked = enabled;
|
|
123
139
|
this.model.tube = enabled ? findModel("Tube65c02") : null;
|
|
124
|
-
|
|
140
|
+
document.getElementById("tubeCpuMultiplier").disabled = !enabled;
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
setTubeCpuMultiplier(value) {
|
|
128
144
|
this.tubeCpuMultiplier = value;
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
document.getElementById("tubeCpuMultiplier").value = value;
|
|
146
|
+
document.getElementById("tubeCpuMultiplierValue").textContent = value;
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
setEconet(enabled) {
|
|
134
150
|
enabled = !!enabled;
|
|
135
|
-
|
|
151
|
+
document.getElementById("hasEconet").checked = enabled;
|
|
136
152
|
this.model.hasEconet = enabled;
|
|
137
153
|
|
|
138
154
|
if (enabled && this.model.isMaster) {
|
|
@@ -142,20 +158,21 @@ export class Config extends EventEmitter {
|
|
|
142
158
|
|
|
143
159
|
setMusic5000(enabled) {
|
|
144
160
|
enabled = !!enabled;
|
|
145
|
-
|
|
161
|
+
document.getElementById("hasMusic5000").checked = enabled;
|
|
146
162
|
this.model.hasMusic5000 = enabled;
|
|
147
163
|
this.addRemoveROM("ample.rom", enabled);
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
setTeletext(enabled) {
|
|
151
167
|
enabled = !!enabled;
|
|
152
|
-
|
|
168
|
+
document.getElementById("hasTeletextAdaptor").checked = enabled;
|
|
153
169
|
this.model.hasTeletextAdaptor = enabled;
|
|
154
170
|
this.addRemoveROM("ats-3.0.rom", enabled);
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
setDropdownText(modelName) {
|
|
158
|
-
|
|
174
|
+
const el = document.querySelector("#bbc-model-dropdown .bbc-model");
|
|
175
|
+
if (el) el.textContent = modelName;
|
|
159
176
|
}
|
|
160
177
|
|
|
161
178
|
addRemoveROM(romName, required) {
|
package/src/dom-utils.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Minimal DOM helpers to replace jQuery usage.
|
|
4
|
+
|
|
5
|
+
export function show(el) {
|
|
6
|
+
el.style.display = "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hide(el) {
|
|
10
|
+
el.style.display = "none";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toggle(el, visible) {
|
|
14
|
+
el.style.display = visible ? "" : "none";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function fadeIn(el, duration = 400) {
|
|
18
|
+
el.style.display = "";
|
|
19
|
+
el.style.transition = `opacity ${duration}ms`;
|
|
20
|
+
el.style.opacity = "0";
|
|
21
|
+
// Force reflow so the transition triggers.
|
|
22
|
+
void el.offsetHeight;
|
|
23
|
+
el.style.opacity = "1";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function fadeOut(el, duration = 400) {
|
|
27
|
+
el.style.transition = `opacity ${duration}ms`;
|
|
28
|
+
el.style.opacity = "0";
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
if (el.style.opacity === "0") el.style.display = "none";
|
|
31
|
+
}, duration);
|
|
32
|
+
}
|