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.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,393 @@
1
+ /*
2
+ * save-states-window.js - Save states window UI
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * SaveStatesWindow - Window for managing save state slots
10
+ * Auto-save row at top (load/download only), plus 5 numbered manual slots
11
+ * with save, load, clear, download. Load-from-file at bottom.
12
+ */
13
+
14
+ import { BaseWindow } from "../windows/base-window.js";
15
+ import {
16
+ getAutosaveInfo,
17
+ loadStateFromStorage,
18
+ getAllSlotInfo,
19
+ clearSlot,
20
+ loadStateFromSlot,
21
+ } from "./state-persistence.js";
22
+
23
+ // Magic bytes: "A2ES" in little-endian = 0x53324541
24
+ const STATE_MAGIC = 0x53324541;
25
+ const SLOT_COUNT = 5;
26
+
27
+ export class SaveStatesWindow extends BaseWindow {
28
+ constructor(stateManager, uiController) {
29
+ super({
30
+ id: "save-states",
31
+ title: "Save States",
32
+ defaultWidth: 480,
33
+ defaultHeight: 560,
34
+ minWidth: 400,
35
+ minHeight: 380,
36
+ });
37
+ this.stateManager = stateManager;
38
+ this.uiController = uiController;
39
+ this.autosaveElement = null;
40
+ this.slotElements = [];
41
+ this.hoverPreview = null;
42
+ }
43
+
44
+ renderContent() {
45
+ // Autosave row (no Save or Clear buttons)
46
+ const autosaveHtml = `
47
+ <div class="save-slot save-slot-auto" data-slot="auto">
48
+ <div class="slot-number auto">A</div>
49
+ <div class="slot-thumbnail">
50
+ <span class="slot-empty-icon">--</span>
51
+ </div>
52
+ <div class="slot-info">
53
+ <div class="slot-status empty">No autosave</div>
54
+ <div class="slot-timestamp"></div>
55
+ </div>
56
+ <div class="slot-actions">
57
+ <button class="slot-btn load-btn" data-action="load-auto" disabled>Load</button>
58
+ <button class="slot-btn download-btn" data-action="download-auto" disabled>DL</button>
59
+ </div>
60
+ </div>`;
61
+
62
+ let slotsHtml = "";
63
+ for (let i = 1; i <= SLOT_COUNT; i++) {
64
+ slotsHtml += `
65
+ <div class="save-slot" data-slot="${i}">
66
+ <div class="slot-number">${i}</div>
67
+ <div class="slot-thumbnail">
68
+ <span class="slot-empty-icon">--</span>
69
+ </div>
70
+ <div class="slot-info">
71
+ <div class="slot-status empty">Empty</div>
72
+ <div class="slot-timestamp"></div>
73
+ </div>
74
+ <div class="slot-actions">
75
+ <button class="slot-btn save-btn" data-action="save" data-slot="${i}">Save</button>
76
+ <button class="slot-btn load-btn" data-action="load" data-slot="${i}" disabled>Load</button>
77
+ <button class="slot-btn clear-btn" data-action="clear" data-slot="${i}" disabled>Clear</button>
78
+ <button class="slot-btn download-btn" data-action="download" data-slot="${i}" disabled>DL</button>
79
+ </div>
80
+ </div>`;
81
+ }
82
+
83
+ return `
84
+ <div class="save-states-container">
85
+ ${autosaveHtml}
86
+ <div class="save-states-divider"></div>
87
+ ${slotsHtml}
88
+ <div class="save-states-toolbar">
89
+ <input type="file" accept=".a2state" style="display:none" />
90
+ <button class="slot-btn load-file-btn">Load from File...</button>
91
+ </div>
92
+ </div>`;
93
+ }
94
+
95
+ onContentRendered() {
96
+ // Cache autosave row
97
+ this.autosaveElement = this.contentElement.querySelector('.save-slot-auto');
98
+
99
+ // Cache slot row elements
100
+ for (let i = 1; i <= SLOT_COUNT; i++) {
101
+ const row = this.contentElement.querySelector(`.save-slot[data-slot="${i}"]`);
102
+ this.slotElements.push(row);
103
+ }
104
+
105
+ // Button click delegation
106
+ this.contentElement.addEventListener("click", (e) => {
107
+ const btn = e.target.closest(".slot-btn");
108
+ if (!btn || btn.disabled) return;
109
+
110
+ const action = btn.dataset.action;
111
+ const slot = parseInt(btn.dataset.slot, 10);
112
+
113
+ if (action === "save") this.handleSave(slot);
114
+ else if (action === "load") this.handleLoad(slot);
115
+ else if (action === "clear") this.handleClear(slot);
116
+ else if (action === "download") this.handleDownload(slot);
117
+ else if (action === "load-auto") this.handleLoadAutosave();
118
+ else if (action === "download-auto") this.handleDownloadAutosave();
119
+ });
120
+
121
+ // Hover preview tooltip
122
+ this.hoverPreview = document.createElement("div");
123
+ this.hoverPreview.className = "slot-thumbnail-preview";
124
+ document.body.appendChild(this.hoverPreview);
125
+
126
+ this.contentElement.addEventListener("mouseenter", (e) => {
127
+ const thumb = e.target.closest(".slot-thumbnail");
128
+ if (!thumb) return;
129
+ const previewSrc = thumb.dataset.preview;
130
+ if (!previewSrc) return;
131
+ this.hoverPreview.innerHTML = `<img src="${previewSrc}" />`;
132
+ this.hoverPreview.classList.add("visible");
133
+ }, true);
134
+
135
+ this.contentElement.addEventListener("mouseleave", (e) => {
136
+ const thumb = e.target.closest(".slot-thumbnail");
137
+ if (!thumb) return;
138
+ this.hoverPreview.classList.remove("visible");
139
+ }, true);
140
+
141
+ this.contentElement.addEventListener("mousemove", (e) => {
142
+ if (!this.hoverPreview.classList.contains("visible")) return;
143
+ const previewW = 280;
144
+ const previewH = 192;
145
+ const pad = 12;
146
+ let x = e.clientX + pad;
147
+ let y = e.clientY + pad;
148
+ if (x + previewW > window.innerWidth) x = e.clientX - previewW - pad;
149
+ if (y + previewH > window.innerHeight) y = e.clientY - previewH - pad;
150
+ this.hoverPreview.style.left = `${x}px`;
151
+ this.hoverPreview.style.top = `${y}px`;
152
+ });
153
+
154
+ // Load from file
155
+ const fileInput = this.contentElement.querySelector('input[type="file"]');
156
+ const loadFileBtn = this.contentElement.querySelector(".load-file-btn");
157
+
158
+ if (loadFileBtn && fileInput) {
159
+ loadFileBtn.addEventListener("click", () => fileInput.click());
160
+ fileInput.addEventListener("change", (e) => {
161
+ const file = e.target.files[0];
162
+ if (file) this.handleLoadFromFile(file);
163
+ fileInput.value = "";
164
+ });
165
+ }
166
+ }
167
+
168
+ show() {
169
+ super.show();
170
+ this.refreshSlots();
171
+ }
172
+
173
+ async refreshSlots() {
174
+ // Refresh autosave row
175
+ await this.refreshAutosaveRow();
176
+
177
+ // Refresh manual slots
178
+ const slots = await getAllSlotInfo();
179
+
180
+ for (let i = 0; i < SLOT_COUNT; i++) {
181
+ const row = this.slotElements[i];
182
+ if (!row) continue;
183
+
184
+ const info = slots[i];
185
+ const thumbEl = row.querySelector(".slot-thumbnail");
186
+ const statusEl = row.querySelector(".slot-status");
187
+ const timestampEl = row.querySelector(".slot-timestamp");
188
+ const loadBtn = row.querySelector('[data-action="load"]');
189
+ const clearBtn = row.querySelector('[data-action="clear"]');
190
+ const downloadBtn = row.querySelector('[data-action="download"]');
191
+
192
+ if (info) {
193
+ if (info.thumbnail) {
194
+ thumbEl.innerHTML = `<img src="${info.thumbnail}" alt="Slot ${i + 1}" />`;
195
+ } else {
196
+ thumbEl.innerHTML = '<span class="slot-empty-icon">--</span>';
197
+ }
198
+ thumbEl.dataset.preview = info.preview || info.thumbnail || "";
199
+ statusEl.textContent = "Saved";
200
+ statusEl.classList.remove("empty");
201
+ timestampEl.textContent = this.formatTimestamp(info.savedAt);
202
+ loadBtn.disabled = false;
203
+ clearBtn.disabled = false;
204
+ downloadBtn.disabled = false;
205
+ } else {
206
+ thumbEl.innerHTML = '<span class="slot-empty-icon">--</span>';
207
+ delete thumbEl.dataset.preview;
208
+ statusEl.textContent = "Empty";
209
+ statusEl.classList.add("empty");
210
+ timestampEl.textContent = "";
211
+ loadBtn.disabled = true;
212
+ clearBtn.disabled = true;
213
+ downloadBtn.disabled = true;
214
+ }
215
+ }
216
+ }
217
+
218
+ async refreshAutosaveRow() {
219
+ const row = this.autosaveElement;
220
+ if (!row) return;
221
+
222
+ const info = await getAutosaveInfo();
223
+ const thumbEl = row.querySelector(".slot-thumbnail");
224
+ const statusEl = row.querySelector(".slot-status");
225
+ const timestampEl = row.querySelector(".slot-timestamp");
226
+ const loadBtn = row.querySelector('[data-action="load-auto"]');
227
+ const downloadBtn = row.querySelector('[data-action="download-auto"]');
228
+
229
+ if (info) {
230
+ if (info.thumbnail) {
231
+ thumbEl.innerHTML = `<img src="${info.thumbnail}" alt="Autosave" />`;
232
+ } else {
233
+ thumbEl.innerHTML = '<span class="slot-empty-icon">--</span>';
234
+ }
235
+ thumbEl.dataset.preview = info.preview || info.thumbnail || "";
236
+ statusEl.textContent = "Autosave";
237
+ statusEl.classList.remove("empty");
238
+ timestampEl.textContent = this.formatTimestamp(info.savedAt);
239
+ loadBtn.disabled = false;
240
+ downloadBtn.disabled = false;
241
+ } else {
242
+ thumbEl.innerHTML = '<span class="slot-empty-icon">--</span>';
243
+ delete thumbEl.dataset.preview;
244
+ statusEl.textContent = "No autosave";
245
+ statusEl.classList.add("empty");
246
+ timestampEl.textContent = "";
247
+ loadBtn.disabled = true;
248
+ downloadBtn.disabled = true;
249
+ }
250
+ }
251
+
252
+ formatTimestamp(ts) {
253
+ const date = new Date(ts);
254
+ const now = new Date();
255
+ const diffMs = now - date;
256
+ const diffMins = Math.floor(diffMs / 60000);
257
+ const diffHours = Math.floor(diffMs / 3600000);
258
+
259
+ if (diffMins < 1) return "just now";
260
+ if (diffMins < 60) return `${diffMins}m ago`;
261
+ if (diffHours < 24) return `${diffHours}h ago`;
262
+ return date.toLocaleDateString();
263
+ }
264
+
265
+ async handleSave(slot) {
266
+ if (!this.stateManager.emulator.isRunning()) {
267
+ this.uiController.showNotification("Power on the emulator first");
268
+ return;
269
+ }
270
+ const ok = await this.stateManager.saveToSlot(slot);
271
+ if (ok) {
272
+ this.uiController.showNotification(`Saved to slot ${slot}`);
273
+ } else {
274
+ this.uiController.showNotification("Save failed");
275
+ }
276
+ this.refreshSlots();
277
+ }
278
+
279
+ async handleLoad(slot) {
280
+ const ok = await this.stateManager.restoreFromSlot(slot);
281
+ if (ok) {
282
+ this.uiController.showNotification(`Loaded slot ${slot}`);
283
+ this.uiController.refocusCanvas();
284
+ } else {
285
+ this.uiController.showNotification("Load failed");
286
+ }
287
+ }
288
+
289
+ async handleClear(slot) {
290
+ await clearSlot(slot);
291
+ this.uiController.showNotification(`Cleared slot ${slot}`);
292
+ this.refreshSlots();
293
+ }
294
+
295
+ async handleDownload(slot) {
296
+ const slotData = await loadStateFromSlot(slot);
297
+ if (!slotData) {
298
+ this.uiController.showNotification("No data in slot");
299
+ return;
300
+ }
301
+
302
+ this.downloadBlob(slotData.data, `apple2e-slot-${slot}.a2state`);
303
+ }
304
+
305
+ async handleLoadAutosave() {
306
+ const ok = await this.stateManager.restoreState();
307
+ if (ok) {
308
+ this.uiController.showNotification("Loaded autosave");
309
+ this.uiController.refocusCanvas();
310
+ } else {
311
+ this.uiController.showNotification("Load failed");
312
+ }
313
+ }
314
+
315
+ async handleDownloadAutosave() {
316
+ const data = await loadStateFromStorage();
317
+ if (!data) {
318
+ this.uiController.showNotification("No autosave data");
319
+ return;
320
+ }
321
+
322
+ this.downloadBlob(data, "apple2e-autosave.a2state");
323
+ }
324
+
325
+ async downloadBlob(data, filename) {
326
+ const blob = new Blob([data], { type: "application/octet-stream" });
327
+
328
+ // Try File System Access API, fall back to blob download
329
+ if (window.showSaveFilePicker) {
330
+ try {
331
+ const handle = await window.showSaveFilePicker({
332
+ suggestedName: filename,
333
+ types: [{ description: "Apple //e State", accept: { "application/octet-stream": [".a2state"] } }],
334
+ });
335
+ const writable = await handle.createWritable();
336
+ await writable.write(blob);
337
+ await writable.close();
338
+ this.uiController.showNotification(`Downloaded ${filename}`);
339
+ return;
340
+ } catch (err) {
341
+ if (err.name === "AbortError") return;
342
+ // Fall through to blob download
343
+ }
344
+ }
345
+
346
+ const url = URL.createObjectURL(blob);
347
+ const a = document.createElement("a");
348
+ a.href = url;
349
+ a.download = filename;
350
+ document.body.appendChild(a);
351
+ a.click();
352
+ document.body.removeChild(a);
353
+ URL.revokeObjectURL(url);
354
+ this.uiController.showNotification(`Downloaded ${filename}`);
355
+ }
356
+
357
+ handleLoadFromFile(file) {
358
+ const reader = new FileReader();
359
+ reader.onload = () => {
360
+ const data = new Uint8Array(reader.result);
361
+
362
+ // Validate magic bytes
363
+ if (data.length < 8) {
364
+ this.uiController.showNotification("Invalid state file");
365
+ return;
366
+ }
367
+ const magic = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
368
+ if ((magic >>> 0) !== STATE_MAGIC) {
369
+ this.uiController.showNotification("Invalid state file");
370
+ return;
371
+ }
372
+
373
+ const ok = this.stateManager.restoreFromFileData(data);
374
+ if (ok) {
375
+ this.uiController.showNotification("State loaded from file");
376
+ this.uiController.refocusCanvas();
377
+ } else {
378
+ this.uiController.showNotification("Failed to load state file");
379
+ }
380
+ };
381
+ reader.onerror = () => {
382
+ this.uiController.showNotification("Failed to read file");
383
+ };
384
+ reader.readAsArrayBuffer(file);
385
+ }
386
+
387
+ destroy() {
388
+ if (this.hoverPreview && this.hoverPreview.parentNode) {
389
+ this.hoverPreview.parentNode.removeChild(this.hoverPreview);
390
+ }
391
+ super.destroy();
392
+ }
393
+ }