@bloopjs/web 0.0.90 → 0.0.92
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/dist/App.d.ts.map +1 -1
- package/dist/debugui/components/BottomBar.d.ts.map +1 -1
- package/dist/debugui/components/LoadTapeDialog.d.ts +2 -0
- package/dist/debugui/components/LoadTapeDialog.d.ts.map +1 -0
- package/dist/debugui/state.d.ts +9 -0
- package/dist/debugui/state.d.ts.map +1 -1
- package/dist/debugui/styles.d.ts +1 -1
- package/dist/debugui/styles.d.ts.map +1 -1
- package/dist/mod.js +560 -64
- package/dist/mod.js.map +10 -9
- package/package.json +3 -3
- package/src/App.ts +3 -1
- package/src/debugui/components/BottomBar.tsx +25 -5
- package/src/debugui/components/LoadTapeDialog.tsx +112 -0
- package/src/debugui/state.ts +105 -0
- package/src/debugui/styles.ts +256 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloopjs/web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.92",
|
|
4
4
|
"author": "Neil Sarkar",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"typescript": "^5"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bloopjs/bloop": "0.0.
|
|
37
|
-
"@bloopjs/engine": "0.0.
|
|
36
|
+
"@bloopjs/bloop": "0.0.92",
|
|
37
|
+
"@bloopjs/engine": "0.0.92",
|
|
38
38
|
"@preact/signals": "^1.3.1",
|
|
39
39
|
"partysocket": "^1.1.6",
|
|
40
40
|
"preact": "^10.25.4"
|
package/src/App.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
triggerHmrFlash,
|
|
14
14
|
wirePlaybarHandlers,
|
|
15
15
|
wireTapeDragDrop,
|
|
16
|
+
wireTapeLoadHandlers,
|
|
16
17
|
} from "./debugui/state.ts";
|
|
17
18
|
import {
|
|
18
19
|
joinRoom as joinRoomInternal,
|
|
@@ -129,9 +130,10 @@ export class App {
|
|
|
129
130
|
if (this.#debugUi) return this.#debugUi;
|
|
130
131
|
this.#debugUi = new DebugUi(opts);
|
|
131
132
|
|
|
132
|
-
// Wire up playbar handlers
|
|
133
|
+
// Wire up playbar handlers, drag-drop, and tape loading
|
|
133
134
|
wirePlaybarHandlers(this);
|
|
134
135
|
wireTapeDragDrop(this.#debugUi.canvas, this);
|
|
136
|
+
wireTapeLoadHandlers(this);
|
|
135
137
|
|
|
136
138
|
return this.#debugUi;
|
|
137
139
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useCallback } from "preact/hooks";
|
|
2
2
|
import { debugState } from "../state.ts";
|
|
3
|
+
import { LoadTapeDialog } from "./LoadTapeDialog.tsx";
|
|
3
4
|
|
|
4
5
|
/** Hook that returns handlers for repeat-on-hold behavior with initial debounce */
|
|
5
6
|
function useRepeatOnHold(action: () => void) {
|
|
@@ -114,39 +115,57 @@ export function BottomBar() {
|
|
|
114
115
|
|
|
115
116
|
const seekDrag = useSeekDrag(handleSeek);
|
|
116
117
|
|
|
118
|
+
const handleLoadTapeClick = useCallback(() => {
|
|
119
|
+
debugState.isLoadDialogOpen.value = true;
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
const handleSaveTapeClick = useCallback(() => {
|
|
123
|
+
debugState.onSaveTape.value?.();
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
117
126
|
return (
|
|
118
127
|
<div className="bottom-bar">
|
|
119
128
|
<div className="playbar-controls">
|
|
120
|
-
<button className="playbar-btn" {...jumpBackRepeat}>
|
|
129
|
+
<button className="playbar-btn jump-back" {...jumpBackRepeat}>
|
|
121
130
|
{"<<"}
|
|
122
131
|
<span className="tooltip tooltip-left">
|
|
123
132
|
Jump back <kbd>4</kbd>
|
|
124
133
|
</span>
|
|
125
134
|
</button>
|
|
126
|
-
<button className="playbar-btn" {...stepBackRepeat}>
|
|
135
|
+
<button className="playbar-btn step-back" {...stepBackRepeat}>
|
|
127
136
|
{"<"}
|
|
128
137
|
<span className="tooltip">
|
|
129
138
|
Step back <kbd>5</kbd>
|
|
130
139
|
</span>
|
|
131
140
|
</button>
|
|
132
|
-
<button className="playbar-btn" onClick={handlePlayPause}>
|
|
141
|
+
<button className="playbar-btn play-pause" onClick={handlePlayPause}>
|
|
133
142
|
{isPlaying ? "||" : ">"}
|
|
134
143
|
<span className="tooltip">
|
|
135
144
|
{isPlaying ? "Pause" : "Play"} <kbd>6</kbd>
|
|
136
145
|
</span>
|
|
137
146
|
</button>
|
|
138
|
-
<button className="playbar-btn" {...stepForwardRepeat}>
|
|
147
|
+
<button className="playbar-btn step-forward" {...stepForwardRepeat}>
|
|
139
148
|
{">"}
|
|
140
149
|
<span className="tooltip">
|
|
141
150
|
Step forward <kbd>7</kbd>
|
|
142
151
|
</span>
|
|
143
152
|
</button>
|
|
144
|
-
<button className="playbar-btn" {...jumpForwardRepeat}>
|
|
153
|
+
<button className="playbar-btn jump-forward" {...jumpForwardRepeat}>
|
|
145
154
|
{">>"}
|
|
146
155
|
<span className="tooltip">
|
|
147
156
|
Jump forward <kbd>8</kbd>
|
|
148
157
|
</span>
|
|
149
158
|
</button>
|
|
159
|
+
<button className="playbar-btn save-tape-btn" onClick={handleSaveTapeClick}>
|
|
160
|
+
Save
|
|
161
|
+
<span className="tooltip">
|
|
162
|
+
Save tape <kbd>Cmd+S</kbd>
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
<button className="playbar-btn load-tape-btn" onClick={handleLoadTapeClick}>
|
|
166
|
+
Load
|
|
167
|
+
<span className="tooltip">Load tape</span>
|
|
168
|
+
</button>
|
|
150
169
|
</div>
|
|
151
170
|
<div className="seek-bar" {...seekDrag}>
|
|
152
171
|
<div
|
|
@@ -160,6 +179,7 @@ export function BottomBar() {
|
|
|
160
179
|
/>
|
|
161
180
|
)}
|
|
162
181
|
</div>
|
|
182
|
+
<LoadTapeDialog />
|
|
163
183
|
</div>
|
|
164
184
|
);
|
|
165
185
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback, useState } from "preact/hooks";
|
|
2
|
+
import { debugState } from "../state.ts";
|
|
3
|
+
|
|
4
|
+
export function LoadTapeDialog() {
|
|
5
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
6
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
7
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
8
|
+
|
|
9
|
+
const isOpen = debugState.isLoadDialogOpen.value;
|
|
10
|
+
const lastTapeName = debugState.lastTapeName.value;
|
|
11
|
+
|
|
12
|
+
// Sync dialog open/close with signal
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const dialog = dialogRef.current;
|
|
15
|
+
if (!dialog) return;
|
|
16
|
+
|
|
17
|
+
if (isOpen && !dialog.open) {
|
|
18
|
+
dialog.showModal();
|
|
19
|
+
} else if (!isOpen && dialog.open) {
|
|
20
|
+
dialog.close();
|
|
21
|
+
}
|
|
22
|
+
}, [isOpen]);
|
|
23
|
+
|
|
24
|
+
// Handle escape key closing dialog
|
|
25
|
+
const handleClose = useCallback(() => {
|
|
26
|
+
debugState.isLoadDialogOpen.value = false;
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const handleFileSelect = useCallback(async (file: File) => {
|
|
30
|
+
if (!file.name.endsWith(".bloop")) return;
|
|
31
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
32
|
+
debugState.onLoadTape.value?.(bytes, file.name);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const handleDropZoneClick = useCallback(() => {
|
|
36
|
+
fileInputRef.current?.click();
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const handleFileInputChange = useCallback(
|
|
40
|
+
(e: { currentTarget: HTMLInputElement }) => {
|
|
41
|
+
const input = e.currentTarget;
|
|
42
|
+
const file = input.files?.[0];
|
|
43
|
+
if (file) {
|
|
44
|
+
handleFileSelect(file);
|
|
45
|
+
input.value = ""; // Reset for next selection
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[handleFileSelect],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const handleDragOver = useCallback(
|
|
52
|
+
(e: { preventDefault: () => void; dataTransfer: DataTransfer | null }) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
55
|
+
setIsDragOver(true);
|
|
56
|
+
},
|
|
57
|
+
[],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleDragLeave = useCallback(() => {
|
|
61
|
+
setIsDragOver(false);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleDrop = useCallback(
|
|
65
|
+
(e: { preventDefault: () => void; dataTransfer: DataTransfer | null }) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
setIsDragOver(false);
|
|
68
|
+
const file = e.dataTransfer?.files[0];
|
|
69
|
+
if (file) {
|
|
70
|
+
handleFileSelect(file);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[handleFileSelect],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const handleReplayLast = useCallback(() => {
|
|
77
|
+
debugState.onReplayLastTape.value?.();
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<dialog ref={dialogRef} className="load-tape-dialog" onClose={handleClose}>
|
|
82
|
+
<div className="load-tape-dialog-content">
|
|
83
|
+
<h3>Load Tape</h3>
|
|
84
|
+
<div
|
|
85
|
+
className={`drop-zone ${isDragOver ? "drag-over" : ""}`}
|
|
86
|
+
onClick={handleDropZoneClick}
|
|
87
|
+
onDragOver={handleDragOver}
|
|
88
|
+
onDragLeave={handleDragLeave}
|
|
89
|
+
onDrop={handleDrop}
|
|
90
|
+
>
|
|
91
|
+
<span className="drop-zone-text">
|
|
92
|
+
Drop .bloop file here
|
|
93
|
+
<br />
|
|
94
|
+
or click to browse
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
<input
|
|
98
|
+
ref={fileInputRef}
|
|
99
|
+
type="file"
|
|
100
|
+
accept=".bloop"
|
|
101
|
+
className="hidden-file-input"
|
|
102
|
+
onChange={handleFileInputChange}
|
|
103
|
+
/>
|
|
104
|
+
{lastTapeName && (
|
|
105
|
+
<button className="replay-last-btn" onClick={handleReplayLast}>
|
|
106
|
+
Replay last: {lastTapeName}
|
|
107
|
+
</button>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</dialog>
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/debugui/state.ts
CHANGED
|
@@ -53,6 +53,12 @@ export type DebugState = {
|
|
|
53
53
|
onStepForward: Signal<(() => void) | null>;
|
|
54
54
|
onJumpForward: Signal<(() => void) | null>;
|
|
55
55
|
onSeek: Signal<((position: number) => void) | null>;
|
|
56
|
+
// Tape loading/saving
|
|
57
|
+
onLoadTape: Signal<((bytes: Uint8Array, fileName: string) => void) | null>;
|
|
58
|
+
onReplayLastTape: Signal<(() => void) | null>;
|
|
59
|
+
onSaveTape: Signal<(() => void) | null>;
|
|
60
|
+
lastTapeName: Signal<string | null>;
|
|
61
|
+
isLoadDialogOpen: Signal<boolean>;
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
const layoutMode = signal<LayoutMode>("off");
|
|
@@ -84,6 +90,13 @@ const onStepForward = signal<(() => void) | null>(null);
|
|
|
84
90
|
const onJumpForward = signal<(() => void) | null>(null);
|
|
85
91
|
const onSeek = signal<((position: number) => void) | null>(null);
|
|
86
92
|
|
|
93
|
+
// Tape loading/saving
|
|
94
|
+
const onLoadTape = signal<((bytes: Uint8Array, fileName: string) => void) | null>(null);
|
|
95
|
+
const onReplayLastTape = signal<(() => void) | null>(null);
|
|
96
|
+
const onSaveTape = signal<(() => void) | null>(null);
|
|
97
|
+
const lastTapeName = signal<string | null>(null);
|
|
98
|
+
const isLoadDialogOpen = signal(false);
|
|
99
|
+
|
|
87
100
|
export const debugState: DebugState = {
|
|
88
101
|
/** Layout mode: off, letterboxed, or full */
|
|
89
102
|
layoutMode,
|
|
@@ -129,6 +142,13 @@ export const debugState: DebugState = {
|
|
|
129
142
|
onStepForward,
|
|
130
143
|
onJumpForward,
|
|
131
144
|
onSeek,
|
|
145
|
+
|
|
146
|
+
/** Tape loading/saving */
|
|
147
|
+
onLoadTape,
|
|
148
|
+
onReplayLastTape,
|
|
149
|
+
onSaveTape,
|
|
150
|
+
lastTapeName,
|
|
151
|
+
isLoadDialogOpen,
|
|
132
152
|
};
|
|
133
153
|
|
|
134
154
|
/** Cycle through layout modes: off -> letterboxed -> full -> off */
|
|
@@ -310,3 +330,88 @@ export function wireTapeDragDrop(canvas: HTMLCanvasElement, app: App): void {
|
|
|
310
330
|
app.loadTape(bytes);
|
|
311
331
|
});
|
|
312
332
|
}
|
|
333
|
+
|
|
334
|
+
// IndexedDB helpers for tape persistence
|
|
335
|
+
const TAPE_DB_NAME = "bloop-debug";
|
|
336
|
+
const TAPE_STORE_NAME = "tapes";
|
|
337
|
+
const TAPE_KEY = "last";
|
|
338
|
+
|
|
339
|
+
function openTapeDB(): Promise<IDBDatabase> {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const request = indexedDB.open(TAPE_DB_NAME, 1);
|
|
342
|
+
request.onerror = () => reject(request.error);
|
|
343
|
+
request.onsuccess = () => resolve(request.result);
|
|
344
|
+
request.onupgradeneeded = () => {
|
|
345
|
+
request.result.createObjectStore(TAPE_STORE_NAME);
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function saveTapeToStorage(
|
|
351
|
+
bytes: Uint8Array,
|
|
352
|
+
fileName: string,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
const db = await openTapeDB();
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
const tx = db.transaction(TAPE_STORE_NAME, "readwrite");
|
|
357
|
+
tx.objectStore(TAPE_STORE_NAME).put({ bytes, fileName }, TAPE_KEY);
|
|
358
|
+
tx.oncomplete = () => resolve();
|
|
359
|
+
tx.onerror = () => reject(tx.error);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function loadTapeFromStorage(): Promise<{
|
|
364
|
+
bytes: Uint8Array;
|
|
365
|
+
fileName: string;
|
|
366
|
+
} | null> {
|
|
367
|
+
try {
|
|
368
|
+
const db = await openTapeDB();
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
const tx = db.transaction(TAPE_STORE_NAME, "readonly");
|
|
371
|
+
const request = tx.objectStore(TAPE_STORE_NAME).get(TAPE_KEY);
|
|
372
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
373
|
+
request.onerror = () => reject(request.error);
|
|
374
|
+
});
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Check for saved tape and update lastTapeName signal */
|
|
381
|
+
export async function checkForSavedTape(): Promise<void> {
|
|
382
|
+
const saved = await loadTapeFromStorage();
|
|
383
|
+
debugState.lastTapeName.value = saved?.fileName ?? null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Wire up tape loading handlers */
|
|
387
|
+
export function wireTapeLoadHandlers(app: App): void {
|
|
388
|
+
debugState.onLoadTape.value = async (bytes: Uint8Array, fileName: string) => {
|
|
389
|
+
app.loadTape(bytes);
|
|
390
|
+
await saveTapeToStorage(bytes, fileName);
|
|
391
|
+
debugState.lastTapeName.value = fileName;
|
|
392
|
+
debugState.isLoadDialogOpen.value = false;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
debugState.onReplayLastTape.value = async () => {
|
|
396
|
+
const saved = await loadTapeFromStorage();
|
|
397
|
+
if (saved) {
|
|
398
|
+
app.loadTape(saved.bytes);
|
|
399
|
+
debugState.isLoadDialogOpen.value = false;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
debugState.onSaveTape.value = () => {
|
|
404
|
+
if (!app.sim.hasHistory) return;
|
|
405
|
+
const tape = app.sim.saveTape();
|
|
406
|
+
const blob = new Blob([tape], { type: "application/octet-stream" });
|
|
407
|
+
const url = URL.createObjectURL(blob);
|
|
408
|
+
const a = document.createElement("a");
|
|
409
|
+
a.href = url;
|
|
410
|
+
a.download = `tape-${Date.now()}.bloop`;
|
|
411
|
+
a.click();
|
|
412
|
+
URL.revokeObjectURL(url);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Check for saved tape on init
|
|
416
|
+
checkForSavedTape();
|
|
417
|
+
}
|