web-a2e 1.0.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/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- package/wiki/Video-Rendering.md +658 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* index.js - Disk manager subsystem initialization and exports
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DriveSounds } from "./drive-sounds.js";
|
|
9
|
+
import { showToast } from "../ui/toast.js";
|
|
10
|
+
import { DiskSurfaceRenderer } from "./disk-surface-renderer.js";
|
|
11
|
+
import {
|
|
12
|
+
loadDisk,
|
|
13
|
+
loadDiskFromData,
|
|
14
|
+
insertBlankDisk,
|
|
15
|
+
ejectDisk,
|
|
16
|
+
performEject,
|
|
17
|
+
saveDiskWithPicker,
|
|
18
|
+
} from "./disk-operations.js";
|
|
19
|
+
import {
|
|
20
|
+
loadDiskFromStorage,
|
|
21
|
+
saveDiskToStorage,
|
|
22
|
+
getRecentDisks,
|
|
23
|
+
loadRecentDisk,
|
|
24
|
+
addToRecentDisks,
|
|
25
|
+
clearRecentDisks,
|
|
26
|
+
} from "./disk-persistence.js";
|
|
27
|
+
import { createDatabaseManager } from "../utils/indexeddb-helper.js";
|
|
28
|
+
|
|
29
|
+
const CACHE_STORE = "images";
|
|
30
|
+
|
|
31
|
+
const libraryCacheDb = createDatabaseManager({
|
|
32
|
+
dbName: "a2e-disk-library-cache",
|
|
33
|
+
version: 1,
|
|
34
|
+
onUpgrade: (event) => {
|
|
35
|
+
const database = event.target.result;
|
|
36
|
+
if (!database.objectStoreNames.contains(CACHE_STORE)) {
|
|
37
|
+
database.createObjectStore(CACHE_STORE, { keyPath: "id" });
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new drive state object with default values
|
|
44
|
+
* @returns {Object} Drive state object
|
|
45
|
+
*/
|
|
46
|
+
function createDriveState() {
|
|
47
|
+
return {
|
|
48
|
+
input: null,
|
|
49
|
+
insertBtn: null,
|
|
50
|
+
blankBtn: null,
|
|
51
|
+
ejectBtn: null,
|
|
52
|
+
browseBtn: null,
|
|
53
|
+
recentBtn: null,
|
|
54
|
+
recentDropdown: null,
|
|
55
|
+
nameLabel: null,
|
|
56
|
+
trackLabel: null,
|
|
57
|
+
filename: null,
|
|
58
|
+
lastTrack: -1,
|
|
59
|
+
surfaceRenderer: null,
|
|
60
|
+
trackAccessCounts: null,
|
|
61
|
+
lastHeadPosition: -1,
|
|
62
|
+
maxAccessCount: 0,
|
|
63
|
+
lastDecayTime: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class DiskManager {
|
|
68
|
+
constructor(wasmModule) {
|
|
69
|
+
this.wasmModule = wasmModule;
|
|
70
|
+
this.drives = [createDriveState(), createDriveState()];
|
|
71
|
+
|
|
72
|
+
// Save modal state
|
|
73
|
+
this.pendingEjectDrive = null;
|
|
74
|
+
this.saveModal = null;
|
|
75
|
+
this.saveFilenameInput = null;
|
|
76
|
+
|
|
77
|
+
// Canvas for focus management
|
|
78
|
+
this.canvas = null;
|
|
79
|
+
|
|
80
|
+
// Active recent disks dropdown
|
|
81
|
+
this.activeDropdown = null;
|
|
82
|
+
|
|
83
|
+
// Drive sounds
|
|
84
|
+
this.sounds = new DriveSounds();
|
|
85
|
+
|
|
86
|
+
// Callback for when a disk is loaded
|
|
87
|
+
this.onDiskLoaded = null;
|
|
88
|
+
|
|
89
|
+
// File explorer reference (set by main.js)
|
|
90
|
+
this.fileExplorer = null;
|
|
91
|
+
|
|
92
|
+
// Set by main.js so surface rendering can be skipped when hidden
|
|
93
|
+
this.drivesWindowVisible = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
init() {
|
|
97
|
+
// Get canvas for focus management
|
|
98
|
+
this.canvas = document.getElementById("screen");
|
|
99
|
+
|
|
100
|
+
// Set up drive 1
|
|
101
|
+
this.setupDrive(0, "disk1");
|
|
102
|
+
|
|
103
|
+
// Set up drive 2
|
|
104
|
+
this.setupDrive(1, "disk2");
|
|
105
|
+
|
|
106
|
+
// Set up drag and drop on the display
|
|
107
|
+
this.setupDragDrop();
|
|
108
|
+
|
|
109
|
+
// Set up save modal
|
|
110
|
+
this.setupSaveModal();
|
|
111
|
+
|
|
112
|
+
// Close dropdown when clicking outside
|
|
113
|
+
document.addEventListener("click", (e) => {
|
|
114
|
+
if (this.activeDropdown && !e.target.closest(".recent-container")) {
|
|
115
|
+
this.closeRecentDropdown();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Restore any persisted disks from previous session
|
|
120
|
+
this.restoreDisks();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Restore disks from IndexedDB that were inserted in a previous session
|
|
125
|
+
*/
|
|
126
|
+
async restoreDisks() {
|
|
127
|
+
for (let driveNum = 0; driveNum < 2; driveNum++) {
|
|
128
|
+
try {
|
|
129
|
+
const diskData = await loadDiskFromStorage(driveNum);
|
|
130
|
+
if (diskData) {
|
|
131
|
+
const drive = this.drives[driveNum];
|
|
132
|
+
loadDiskFromData({
|
|
133
|
+
wasmModule: this.wasmModule,
|
|
134
|
+
drive,
|
|
135
|
+
driveNum,
|
|
136
|
+
filename: diskData.filename,
|
|
137
|
+
data: diskData.data,
|
|
138
|
+
onSuccess: (filename) => {
|
|
139
|
+
this.setDiskName(driveNum, filename);
|
|
140
|
+
if (this.onDiskLoaded) this.onDiskLoaded(driveNum, filename);
|
|
141
|
+
},
|
|
142
|
+
onError: (error) =>
|
|
143
|
+
console.error(
|
|
144
|
+
`Failed to restore disk in drive ${driveNum + 1}:`,
|
|
145
|
+
error,
|
|
146
|
+
),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`Error restoring disk for drive ${driveNum + 1}:`, error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
refocusCanvas() {
|
|
156
|
+
if (this.canvas) {
|
|
157
|
+
setTimeout(() => this.canvas.focus(), 0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setupDrive(driveNum, elementId) {
|
|
162
|
+
const container = document.getElementById(elementId);
|
|
163
|
+
if (!container) return;
|
|
164
|
+
|
|
165
|
+
const drive = this.drives[driveNum];
|
|
166
|
+
drive.input = container.querySelector(`#${elementId}-input`);
|
|
167
|
+
drive.insertBtn = container.querySelector(".disk-insert");
|
|
168
|
+
drive.blankBtn = container.querySelector(".disk-blank");
|
|
169
|
+
drive.ejectBtn = container.querySelector(".disk-eject");
|
|
170
|
+
drive.browseBtn = container.querySelector(".disk-browse");
|
|
171
|
+
drive.recentBtn = container.querySelector(".disk-recent");
|
|
172
|
+
drive.recentDropdown = container.querySelector(".recent-dropdown");
|
|
173
|
+
drive.nameLabel = container.querySelector(".disk-name");
|
|
174
|
+
drive.trackLabel = container.querySelector(".disk-track");
|
|
175
|
+
const surfaceCanvas = container.querySelector(".disk-surface");
|
|
176
|
+
if (surfaceCanvas) {
|
|
177
|
+
drive.surfaceRenderer = new DiskSurfaceRenderer(surfaceCanvas);
|
|
178
|
+
drive.trackAccessCounts = new Uint32Array(35);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Insert button click
|
|
182
|
+
if (drive.insertBtn) {
|
|
183
|
+
drive.insertBtn.addEventListener("click", () => {
|
|
184
|
+
drive.input.click();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Blank disk button click
|
|
189
|
+
if (drive.blankBtn) {
|
|
190
|
+
drive.blankBtn.addEventListener("click", () => {
|
|
191
|
+
this.insertBlankDisk(driveNum);
|
|
192
|
+
this.refocusCanvas();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Recent button click
|
|
197
|
+
if (drive.recentBtn) {
|
|
198
|
+
drive.recentBtn.addEventListener("click", (e) => {
|
|
199
|
+
e.stopPropagation();
|
|
200
|
+
this.toggleRecentDropdown(driveNum);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// File input change
|
|
205
|
+
if (drive.input) {
|
|
206
|
+
drive.input.addEventListener("change", (e) => {
|
|
207
|
+
if (e.target.files.length > 0) {
|
|
208
|
+
this.loadDisk(driveNum, e.target.files[0]);
|
|
209
|
+
}
|
|
210
|
+
this.refocusCanvas();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Eject button click
|
|
215
|
+
if (drive.ejectBtn) {
|
|
216
|
+
drive.ejectBtn.addEventListener("click", () => {
|
|
217
|
+
this.ejectDisk(driveNum);
|
|
218
|
+
this.refocusCanvas();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Browse button click
|
|
223
|
+
if (drive.browseBtn) {
|
|
224
|
+
drive.browseBtn.addEventListener("click", () => {
|
|
225
|
+
if (this.fileExplorer) {
|
|
226
|
+
this.fileExplorer.showFloppyDisk(driveNum);
|
|
227
|
+
}
|
|
228
|
+
this.refocusCanvas();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setupDragDrop() {
|
|
234
|
+
const displayContainer = document.getElementById("monitor-frame");
|
|
235
|
+
if (!displayContainer) return;
|
|
236
|
+
|
|
237
|
+
displayContainer.addEventListener("dragover", (e) => {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
e.stopPropagation();
|
|
240
|
+
displayContainer.classList.add("drag-over");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
displayContainer.addEventListener("dragleave", (e) => {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
e.stopPropagation();
|
|
246
|
+
displayContainer.classList.remove("drag-over");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
displayContainer.addEventListener("drop", (e) => {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
e.stopPropagation();
|
|
252
|
+
displayContainer.classList.remove("drag-over");
|
|
253
|
+
|
|
254
|
+
if (e.dataTransfer.files.length > 0) {
|
|
255
|
+
// Load into first empty drive, or drive 1 if both full
|
|
256
|
+
const driveNum = !this.drives[0].filename
|
|
257
|
+
? 0
|
|
258
|
+
: !this.drives[1].filename
|
|
259
|
+
? 1
|
|
260
|
+
: 0;
|
|
261
|
+
this.loadDisk(driveNum, e.dataTransfer.files[0]);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setupSaveModal() {
|
|
267
|
+
this.saveModal = document.getElementById("save-disk-modal");
|
|
268
|
+
this.saveFilenameInput = document.getElementById("save-disk-filename");
|
|
269
|
+
const confirmBtn = document.getElementById("save-disk-confirm");
|
|
270
|
+
const cancelBtn = document.getElementById("save-disk-cancel");
|
|
271
|
+
|
|
272
|
+
if (confirmBtn) {
|
|
273
|
+
confirmBtn.addEventListener("click", () => {
|
|
274
|
+
this.handleSaveConfirm();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (cancelBtn) {
|
|
279
|
+
cancelBtn.addEventListener("click", () => {
|
|
280
|
+
this.handleSaveCancel();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle click on backdrop (::backdrop) to close
|
|
285
|
+
if (this.saveModal) {
|
|
286
|
+
this.saveModal.addEventListener("click", (e) => {
|
|
287
|
+
// Close if clicking on the dialog itself (backdrop area)
|
|
288
|
+
if (e.target === this.saveModal) {
|
|
289
|
+
this.handleSaveCancel();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Handle native cancel event (Escape key)
|
|
294
|
+
this.saveModal.addEventListener("cancel", (e) => {
|
|
295
|
+
e.preventDefault(); // Prevent default close to handle our own logic
|
|
296
|
+
this.handleSaveCancel();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle Enter key in filename input
|
|
301
|
+
if (this.saveFilenameInput) {
|
|
302
|
+
this.saveFilenameInput.addEventListener("keydown", (e) => {
|
|
303
|
+
if (e.key === "Enter") {
|
|
304
|
+
this.handleSaveConfirm();
|
|
305
|
+
}
|
|
306
|
+
// Escape is now handled by native dialog cancel event
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
showSaveModal(driveNum) {
|
|
312
|
+
const drive = this.drives[driveNum];
|
|
313
|
+
this.pendingEjectDrive = driveNum;
|
|
314
|
+
|
|
315
|
+
// Set default filename based on current disk name
|
|
316
|
+
let defaultName = drive.filename || `disk${driveNum + 1}.dsk`;
|
|
317
|
+
// Ensure it has an extension
|
|
318
|
+
if (!defaultName.includes(".")) {
|
|
319
|
+
defaultName += ".dsk";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (this.saveFilenameInput) {
|
|
323
|
+
this.saveFilenameInput.value = defaultName;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (this.saveModal) {
|
|
327
|
+
this.saveModal.showModal();
|
|
328
|
+
// Focus the input and select the filename (without extension)
|
|
329
|
+
if (this.saveFilenameInput) {
|
|
330
|
+
this.saveFilenameInput.focus();
|
|
331
|
+
const dotIndex = defaultName.lastIndexOf(".");
|
|
332
|
+
if (dotIndex > 0) {
|
|
333
|
+
this.saveFilenameInput.setSelectionRange(0, dotIndex);
|
|
334
|
+
} else {
|
|
335
|
+
this.saveFilenameInput.select();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
hideSaveModal() {
|
|
342
|
+
if (this.saveModal && this.saveModal.open) {
|
|
343
|
+
this.saveModal.close();
|
|
344
|
+
}
|
|
345
|
+
this.pendingEjectDrive = null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async handleSaveConfirm() {
|
|
349
|
+
if (this.pendingEjectDrive === null) return;
|
|
350
|
+
|
|
351
|
+
const driveNum = this.pendingEjectDrive;
|
|
352
|
+
const defaultFilename =
|
|
353
|
+
this.saveFilenameInput?.value || `disk${driveNum + 1}.dsk`;
|
|
354
|
+
|
|
355
|
+
this.hideSaveModal();
|
|
356
|
+
|
|
357
|
+
// Try to save with file picker
|
|
358
|
+
const saved = await saveDiskWithPicker(
|
|
359
|
+
this.wasmModule,
|
|
360
|
+
driveNum,
|
|
361
|
+
defaultFilename,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Always eject after save attempt (user may have cancelled picker)
|
|
365
|
+
this.performEject(driveNum);
|
|
366
|
+
|
|
367
|
+
if (saved) {
|
|
368
|
+
console.log(`Disk saved successfully`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
handleSaveCancel() {
|
|
373
|
+
if (this.pendingEjectDrive === null) return;
|
|
374
|
+
|
|
375
|
+
const driveNum = this.pendingEjectDrive;
|
|
376
|
+
this.hideSaveModal();
|
|
377
|
+
this.performEject(driveNum);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Set the disk name label with scrolling animation if the name is too long
|
|
382
|
+
*/
|
|
383
|
+
setDiskName(driveNum, name) {
|
|
384
|
+
const drive = this.drives[driveNum];
|
|
385
|
+
if (!drive.nameLabel) return;
|
|
386
|
+
|
|
387
|
+
// Create inner span for the text if it doesn't exist
|
|
388
|
+
let textSpan = drive.nameLabel.querySelector(".disk-name-text");
|
|
389
|
+
if (!textSpan) {
|
|
390
|
+
textSpan = document.createElement("span");
|
|
391
|
+
textSpan.className = "disk-name-text";
|
|
392
|
+
drive.nameLabel.innerHTML = "";
|
|
393
|
+
drive.nameLabel.appendChild(textSpan);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Set the text
|
|
397
|
+
textSpan.textContent = name;
|
|
398
|
+
|
|
399
|
+
// Remove scrolling class initially
|
|
400
|
+
drive.nameLabel.classList.remove("scrolling");
|
|
401
|
+
|
|
402
|
+
// Check if text overflows after a brief delay to allow rendering
|
|
403
|
+
requestAnimationFrame(() => {
|
|
404
|
+
const containerWidth = drive.nameLabel.offsetWidth;
|
|
405
|
+
const textWidth = textSpan.offsetWidth;
|
|
406
|
+
|
|
407
|
+
if (textWidth > containerWidth) {
|
|
408
|
+
// Calculate scroll distance (negative to scroll left)
|
|
409
|
+
const scrollDistance = containerWidth - textWidth;
|
|
410
|
+
|
|
411
|
+
// Calculate duration based on text length (roughly 40px per second)
|
|
412
|
+
const duration = Math.max(3, Math.abs(scrollDistance) / 40);
|
|
413
|
+
|
|
414
|
+
// Set CSS custom properties for the animation
|
|
415
|
+
drive.nameLabel.style.setProperty(
|
|
416
|
+
"--scroll-distance",
|
|
417
|
+
`${scrollDistance}px`,
|
|
418
|
+
);
|
|
419
|
+
drive.nameLabel.style.setProperty("--scroll-duration", `${duration}s`);
|
|
420
|
+
|
|
421
|
+
// Enable scrolling
|
|
422
|
+
drive.nameLabel.classList.add("scrolling");
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Disk operations - delegate to disk-operations module
|
|
428
|
+
|
|
429
|
+
async loadDisk(driveNum, file) {
|
|
430
|
+
const drive = this.drives[driveNum];
|
|
431
|
+
await loadDisk({
|
|
432
|
+
wasmModule: this.wasmModule,
|
|
433
|
+
drive,
|
|
434
|
+
driveNum,
|
|
435
|
+
file,
|
|
436
|
+
onSuccess: (filename) => {
|
|
437
|
+
this.setDiskName(driveNum, filename);
|
|
438
|
+
if (this.onDiskLoaded) this.onDiskLoaded(driveNum, filename);
|
|
439
|
+
},
|
|
440
|
+
onError: (error) => showToast(error, "error"),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
insertBlankDisk(driveNum) {
|
|
445
|
+
const drive = this.drives[driveNum];
|
|
446
|
+
insertBlankDisk({
|
|
447
|
+
wasmModule: this.wasmModule,
|
|
448
|
+
drive,
|
|
449
|
+
driveNum,
|
|
450
|
+
onSuccess: (filename) => this.setDiskName(driveNum, filename),
|
|
451
|
+
onError: (error) => showToast(error, "error"),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async ejectDisk(driveNum) {
|
|
456
|
+
const drive = this.drives[driveNum];
|
|
457
|
+
await ejectDisk({
|
|
458
|
+
wasmModule: this.wasmModule,
|
|
459
|
+
drive,
|
|
460
|
+
driveNum,
|
|
461
|
+
onEject: () => {
|
|
462
|
+
this.setDiskName(driveNum, "No Disk");
|
|
463
|
+
this._resetDriveVisuals(driveNum);
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
performEject(driveNum) {
|
|
469
|
+
const drive = this.drives[driveNum];
|
|
470
|
+
performEject({
|
|
471
|
+
wasmModule: this.wasmModule,
|
|
472
|
+
drive,
|
|
473
|
+
driveNum,
|
|
474
|
+
onEject: () => {
|
|
475
|
+
this.setDiskName(driveNum, "No Disk");
|
|
476
|
+
this._resetDriveVisuals(driveNum);
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Drive state and LED updates
|
|
482
|
+
|
|
483
|
+
updateLEDs() {
|
|
484
|
+
// Update drive images and track display based on motor state
|
|
485
|
+
if (!this.wasmModule._getDiskMotorOn) return;
|
|
486
|
+
|
|
487
|
+
const selectedDrive = this.wasmModule._getSelectedDrive
|
|
488
|
+
? this.wasmModule._getSelectedDrive()
|
|
489
|
+
: 0;
|
|
490
|
+
|
|
491
|
+
// Check if any motor is running for motor sound
|
|
492
|
+
const anyMotorOn =
|
|
493
|
+
this.wasmModule._getDiskMotorOn(0) || this.wasmModule._getDiskMotorOn(1);
|
|
494
|
+
if (anyMotorOn && !this.sounds.motorRunning) {
|
|
495
|
+
this.sounds.startMotorSound();
|
|
496
|
+
} else if (!anyMotorOn && this.sounds.motorRunning) {
|
|
497
|
+
this.sounds.stopMotorSound();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
for (let driveNum = 0; driveNum < 2; driveNum++) {
|
|
501
|
+
const drive = this.drives[driveNum];
|
|
502
|
+
const motorOn = this.wasmModule._getDiskMotorOn(driveNum);
|
|
503
|
+
const isActive = motorOn && driveNum === selectedDrive;
|
|
504
|
+
const hasDisk = drive.filename !== null;
|
|
505
|
+
|
|
506
|
+
// Update track display and check for seek
|
|
507
|
+
let track = 0;
|
|
508
|
+
let quarterTrack = 0;
|
|
509
|
+
let isWriteMode = false;
|
|
510
|
+
if (drive.trackLabel) {
|
|
511
|
+
if (hasDisk && this.wasmModule._getDiskTrack) {
|
|
512
|
+
track = this.wasmModule._getDiskTrack(driveNum);
|
|
513
|
+
drive.trackLabel.textContent = `T${track.toString().padStart(2, "0")}`;
|
|
514
|
+
|
|
515
|
+
// Check for track change and play seek sound (only on whole track changes)
|
|
516
|
+
if (isActive && drive.lastTrack >= 0 && track !== drive.lastTrack) {
|
|
517
|
+
this.sounds.playSeekSound();
|
|
518
|
+
}
|
|
519
|
+
drive.lastTrack = track;
|
|
520
|
+
|
|
521
|
+
// Highlight when motor is on and this drive is selected
|
|
522
|
+
if (isActive) {
|
|
523
|
+
drive.trackLabel.classList.add("active");
|
|
524
|
+
} else {
|
|
525
|
+
drive.trackLabel.classList.remove("active");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Update heatmap tracking
|
|
529
|
+
if (isActive && drive.trackAccessCounts) {
|
|
530
|
+
const clampedTrack = Math.min(track, 34);
|
|
531
|
+
drive.trackAccessCounts[clampedTrack]++;
|
|
532
|
+
if (drive.trackAccessCounts[clampedTrack] > drive.maxAccessCount) {
|
|
533
|
+
drive.maxAccessCount = drive.trackAccessCounts[clampedTrack];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Decay track highlights every 200ms
|
|
538
|
+
const now = performance.now();
|
|
539
|
+
if (drive.trackAccessCounts && now - drive.lastDecayTime > 100) {
|
|
540
|
+
drive.lastDecayTime = now;
|
|
541
|
+
let newMax = 0;
|
|
542
|
+
for (let t = 0; t < 35; t++) {
|
|
543
|
+
if (drive.trackAccessCounts[t] > 0) {
|
|
544
|
+
drive.trackAccessCounts[t] = Math.floor(
|
|
545
|
+
drive.trackAccessCounts[t] * 0.8,
|
|
546
|
+
);
|
|
547
|
+
if (drive.trackAccessCounts[t] > newMax) {
|
|
548
|
+
newMax = drive.trackAccessCounts[t];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
drive.maxAccessCount = newMax;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Get head position
|
|
556
|
+
if (this.wasmModule._getDiskHeadPosition) {
|
|
557
|
+
quarterTrack = this.wasmModule._getDiskHeadPosition(driveNum);
|
|
558
|
+
drive.lastHeadPosition = quarterTrack;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Get write mode
|
|
562
|
+
if (this.wasmModule._getDiskWriteMode) {
|
|
563
|
+
isWriteMode = this.wasmModule._getDiskWriteMode(driveNum);
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
drive.trackLabel.textContent = "T--";
|
|
567
|
+
drive.trackLabel.classList.remove("active");
|
|
568
|
+
drive.lastTrack = -1;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Update surface renderer (skip when window is hidden)
|
|
573
|
+
if (drive.surfaceRenderer && this.drivesWindowVisible) {
|
|
574
|
+
const diskColor =
|
|
575
|
+
hasDisk && drive.filename
|
|
576
|
+
? this._getStickerColor(drive.filename)
|
|
577
|
+
: null;
|
|
578
|
+
|
|
579
|
+
drive.surfaceRenderer.update({
|
|
580
|
+
hasDisk,
|
|
581
|
+
isActive,
|
|
582
|
+
isWriteMode,
|
|
583
|
+
quarterTrack,
|
|
584
|
+
track,
|
|
585
|
+
trackAccessCounts: drive.trackAccessCounts,
|
|
586
|
+
maxAccessCount: drive.maxAccessCount,
|
|
587
|
+
diskColor,
|
|
588
|
+
timestamp: performance.now(),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Sound control - delegate to DriveSounds
|
|
595
|
+
|
|
596
|
+
setSeekSoundEnabled(enabled) {
|
|
597
|
+
this.sounds.setSeekSoundEnabled(enabled);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
setSeekVolume(volume) {
|
|
601
|
+
this.sounds.setSeekVolume(volume);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
setMotorSoundEnabled(enabled) {
|
|
605
|
+
this.sounds.setMotorSoundEnabled(enabled);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
setMotorVolume(volume) {
|
|
609
|
+
this.sounds.setMotorVolume(volume);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
setMasterVolume(volume) {
|
|
613
|
+
this.sounds.setMasterVolume(volume);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Recent disks dropdown
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Toggle the recent disks dropdown for a drive
|
|
620
|
+
*/
|
|
621
|
+
async toggleRecentDropdown(driveNum) {
|
|
622
|
+
const drive = this.drives[driveNum];
|
|
623
|
+
if (!drive.recentDropdown) return;
|
|
624
|
+
|
|
625
|
+
// Close any other open dropdown
|
|
626
|
+
if (this.activeDropdown && this.activeDropdown !== drive.recentDropdown) {
|
|
627
|
+
this.closeRecentDropdown();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (drive.recentDropdown.classList.contains("open")) {
|
|
631
|
+
this.closeRecentDropdown();
|
|
632
|
+
} else {
|
|
633
|
+
await this.populateRecentDropdown(driveNum);
|
|
634
|
+
drive.recentDropdown.classList.add("open");
|
|
635
|
+
this.activeDropdown = drive.recentDropdown;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Close the currently open dropdown
|
|
641
|
+
*/
|
|
642
|
+
closeRecentDropdown() {
|
|
643
|
+
if (this.activeDropdown) {
|
|
644
|
+
this.activeDropdown.classList.remove("open");
|
|
645
|
+
this.activeDropdown = null;
|
|
646
|
+
}
|
|
647
|
+
this.refocusCanvas();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Populate the recent disks dropdown
|
|
652
|
+
*/
|
|
653
|
+
async populateRecentDropdown(driveNum) {
|
|
654
|
+
const drive = this.drives[driveNum];
|
|
655
|
+
if (!drive.recentDropdown) return;
|
|
656
|
+
|
|
657
|
+
const recentDisks = await getRecentDisks(driveNum);
|
|
658
|
+
|
|
659
|
+
drive.recentDropdown.innerHTML = "";
|
|
660
|
+
|
|
661
|
+
if (recentDisks.length === 0) {
|
|
662
|
+
const emptyItem = document.createElement("div");
|
|
663
|
+
emptyItem.className = "recent-item empty";
|
|
664
|
+
emptyItem.textContent = "No recent disks";
|
|
665
|
+
drive.recentDropdown.appendChild(emptyItem);
|
|
666
|
+
} else {
|
|
667
|
+
for (const disk of recentDisks) {
|
|
668
|
+
const item = document.createElement("div");
|
|
669
|
+
item.className = "recent-item";
|
|
670
|
+
item.textContent = disk.filename;
|
|
671
|
+
item.title = disk.filename;
|
|
672
|
+
item.addEventListener("click", (e) => {
|
|
673
|
+
e.stopPropagation();
|
|
674
|
+
this.loadRecentDiskInDrive(driveNum, disk.id);
|
|
675
|
+
});
|
|
676
|
+
drive.recentDropdown.appendChild(item);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Add separator and clear option
|
|
680
|
+
const separator = document.createElement("div");
|
|
681
|
+
separator.className = "recent-separator";
|
|
682
|
+
drive.recentDropdown.appendChild(separator);
|
|
683
|
+
|
|
684
|
+
const clearItem = document.createElement("div");
|
|
685
|
+
clearItem.className = "recent-item recent-clear";
|
|
686
|
+
clearItem.textContent = "Clear Recent";
|
|
687
|
+
clearItem.addEventListener("click", async (e) => {
|
|
688
|
+
e.stopPropagation();
|
|
689
|
+
await clearRecentDisks(driveNum);
|
|
690
|
+
this.closeRecentDropdown();
|
|
691
|
+
});
|
|
692
|
+
drive.recentDropdown.appendChild(clearItem);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Append library section
|
|
696
|
+
await this._appendLibrarySection(drive.recentDropdown, driveNum, "floppy");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Fetch a library image with IndexedDB caching
|
|
701
|
+
*/
|
|
702
|
+
async _getLibraryImageData(entry) {
|
|
703
|
+
try {
|
|
704
|
+
const cached = await libraryCacheDb.get(CACHE_STORE, entry.id);
|
|
705
|
+
if (cached) return new Uint8Array(cached.data);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
console.warn("Library cache read failed:", err);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const resp = await fetch(`/disks/${entry.file}`);
|
|
711
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
712
|
+
const arrayBuffer = await resp.arrayBuffer();
|
|
713
|
+
const data = new Uint8Array(arrayBuffer);
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
await libraryCacheDb.put(CACHE_STORE, {
|
|
717
|
+
id: entry.id,
|
|
718
|
+
file: entry.file,
|
|
719
|
+
data: arrayBuffer,
|
|
720
|
+
});
|
|
721
|
+
} catch (err) {
|
|
722
|
+
console.warn("Library cache write failed:", err);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return data;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Append library entries to a recent dropdown
|
|
730
|
+
*/
|
|
731
|
+
async _appendLibrarySection(dropdown, driveNum, type) {
|
|
732
|
+
try {
|
|
733
|
+
const resp = await fetch("/disks/library.json");
|
|
734
|
+
if (!resp.ok) return;
|
|
735
|
+
const library = await resp.json();
|
|
736
|
+
const entries = library.filter((e) => e.type === type);
|
|
737
|
+
if (entries.length === 0) return;
|
|
738
|
+
|
|
739
|
+
const separator = document.createElement("div");
|
|
740
|
+
separator.className = "recent-separator";
|
|
741
|
+
dropdown.appendChild(separator);
|
|
742
|
+
|
|
743
|
+
const label = document.createElement("div");
|
|
744
|
+
label.className = "recent-section-label";
|
|
745
|
+
label.textContent = "Library";
|
|
746
|
+
dropdown.appendChild(label);
|
|
747
|
+
|
|
748
|
+
for (const entry of entries) {
|
|
749
|
+
const item = document.createElement("div");
|
|
750
|
+
item.className = "recent-item";
|
|
751
|
+
item.textContent = entry.name;
|
|
752
|
+
item.title = entry.description || entry.name;
|
|
753
|
+
item.addEventListener("click", async (e) => {
|
|
754
|
+
e.stopPropagation();
|
|
755
|
+
this.closeRecentDropdown();
|
|
756
|
+
try {
|
|
757
|
+
const data = await this._getLibraryImageData(entry);
|
|
758
|
+
const drive = this.drives[driveNum];
|
|
759
|
+
loadDiskFromData({
|
|
760
|
+
wasmModule: this.wasmModule,
|
|
761
|
+
drive,
|
|
762
|
+
driveNum,
|
|
763
|
+
filename: entry.file,
|
|
764
|
+
data,
|
|
765
|
+
onSuccess: (filename) => {
|
|
766
|
+
this.setDiskName(driveNum, filename);
|
|
767
|
+
if (this.onDiskLoaded) this.onDiskLoaded(driveNum, filename);
|
|
768
|
+
},
|
|
769
|
+
onError: (error) => showToast(error, "error"),
|
|
770
|
+
});
|
|
771
|
+
saveDiskToStorage(driveNum, entry.file, data);
|
|
772
|
+
addToRecentDisks(driveNum, entry.file, data);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
console.error(`Failed to load ${entry.file}:`, err);
|
|
775
|
+
showToast(`Failed to load ${entry.name}`, "error");
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
dropdown.appendChild(item);
|
|
779
|
+
}
|
|
780
|
+
} catch (err) {
|
|
781
|
+
console.error("Failed to load library:", err);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Load a recent disk into a drive
|
|
787
|
+
*/
|
|
788
|
+
async loadRecentDiskInDrive(driveNum, diskId) {
|
|
789
|
+
this.closeRecentDropdown();
|
|
790
|
+
|
|
791
|
+
const diskData = await loadRecentDisk(diskId);
|
|
792
|
+
if (!diskData) {
|
|
793
|
+
console.error("Failed to load recent disk");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const drive = this.drives[driveNum];
|
|
798
|
+
loadDiskFromData({
|
|
799
|
+
wasmModule: this.wasmModule,
|
|
800
|
+
drive,
|
|
801
|
+
driveNum,
|
|
802
|
+
filename: diskData.filename,
|
|
803
|
+
data: diskData.data,
|
|
804
|
+
onSuccess: (filename) => {
|
|
805
|
+
this.setDiskName(driveNum, filename);
|
|
806
|
+
if (this.onDiskLoaded) this.onDiskLoaded(driveNum, filename);
|
|
807
|
+
},
|
|
808
|
+
onError: (error) => showToast(error, "error"),
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// Update access time by re-adding to recent list
|
|
812
|
+
addToRecentDisks(driveNum, diskData.filename, diskData.data);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Sync UI with emulator state after a state restore
|
|
817
|
+
* This updates the drive displays to show any disks loaded from the state snapshot
|
|
818
|
+
*/
|
|
819
|
+
syncWithEmulatorState() {
|
|
820
|
+
for (let driveNum = 0; driveNum < 2; driveNum++) {
|
|
821
|
+
const drive = this.drives[driveNum];
|
|
822
|
+
const hasDisk = this.wasmModule._isDiskInserted(driveNum);
|
|
823
|
+
|
|
824
|
+
if (hasDisk) {
|
|
825
|
+
// Get filename from emulator
|
|
826
|
+
const filenamePtr = this.wasmModule._getDiskFilename(driveNum);
|
|
827
|
+
let filename = "Restored Disk";
|
|
828
|
+
if (filenamePtr) {
|
|
829
|
+
filename = this.wasmModule.UTF8ToString(filenamePtr);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
drive.filename = filename;
|
|
833
|
+
if (drive.ejectBtn) drive.ejectBtn.disabled = false;
|
|
834
|
+
if (drive.browseBtn) drive.browseBtn.disabled = false;
|
|
835
|
+
this.setDiskName(driveNum, filename);
|
|
836
|
+
console.log(`Synced drive ${driveNum + 1}: ${filename}`);
|
|
837
|
+
} else {
|
|
838
|
+
// No disk in drive
|
|
839
|
+
drive.filename = null;
|
|
840
|
+
if (drive.ejectBtn) drive.ejectBtn.disabled = true;
|
|
841
|
+
if (drive.browseBtn) drive.browseBtn.disabled = true;
|
|
842
|
+
if (drive.input) drive.input.value = "";
|
|
843
|
+
this.setDiskName(driveNum, "No Disk");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Visual enhancement methods
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Reset visual enhancements for a drive on eject
|
|
852
|
+
*/
|
|
853
|
+
_resetDriveVisuals(driveNum) {
|
|
854
|
+
const drive = this.drives[driveNum];
|
|
855
|
+
|
|
856
|
+
// Reset track access data
|
|
857
|
+
if (drive.trackAccessCounts) {
|
|
858
|
+
drive.trackAccessCounts.fill(0);
|
|
859
|
+
drive.maxAccessCount = 0;
|
|
860
|
+
drive.lastHeadPosition = -1;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Reset surface renderer
|
|
864
|
+
if (drive.surfaceRenderer) {
|
|
865
|
+
drive.surfaceRenderer.reset();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Derive a sticker color from a filename hash
|
|
871
|
+
* Returns one of 8 vintage label colors
|
|
872
|
+
*/
|
|
873
|
+
_getStickerColor(filename) {
|
|
874
|
+
const colors = [
|
|
875
|
+
"#f5f0d0", // cream
|
|
876
|
+
"#e8d8a0", // manila
|
|
877
|
+
"#c8e6c0", // pale green
|
|
878
|
+
"#b8d4e8", // pale blue
|
|
879
|
+
"#f0c0c8", // pink
|
|
880
|
+
"#f0e8a0", // yellow
|
|
881
|
+
"#d0c8e8", // lavender
|
|
882
|
+
"#f0ece8", // white
|
|
883
|
+
];
|
|
884
|
+
let hash = 0;
|
|
885
|
+
for (let i = 0; i < filename.length; i++) {
|
|
886
|
+
hash = ((hash << 5) - hash + filename.charCodeAt(i)) | 0;
|
|
887
|
+
}
|
|
888
|
+
return colors[Math.abs(hash) % colors.length];
|
|
889
|
+
}
|
|
890
|
+
}
|