pinokiod 3.86.0 → 3.87.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/Dockerfile +61 -0
- package/docker-entrypoint.sh +75 -0
- package/kernel/api/hf/index.js +1 -1
- package/kernel/api/index.js +1 -1
- package/kernel/api/shell/index.js +6 -0
- package/kernel/api/terminal/index.js +166 -0
- package/kernel/bin/conda.js +3 -2
- package/kernel/bin/index.js +53 -2
- package/kernel/bin/setup.js +32 -0
- package/kernel/bin/vs.js +11 -2
- package/kernel/index.js +42 -2
- package/kernel/info.js +36 -0
- package/kernel/peer.js +42 -15
- package/kernel/router/index.js +23 -15
- package/kernel/router/localhost_static_router.js +0 -3
- package/kernel/router/pinokio_domain_router.js +333 -0
- package/kernel/shells.js +21 -1
- package/kernel/util.js +2 -2
- package/package.json +2 -1
- package/script/install-mode.js +33 -0
- package/script/pinokio.json +7 -0
- package/server/index.js +513 -173
- package/server/public/Socket.js +48 -0
- package/server/public/common.js +1441 -276
- package/server/public/fseditor.js +71 -12
- package/server/public/install.js +1 -1
- package/server/public/layout.js +740 -0
- package/server/public/modalinput.js +0 -1
- package/server/public/style.css +97 -105
- package/server/public/tab-idle-notifier.js +629 -0
- package/server/public/terminal_input_tracker.js +63 -0
- package/server/public/urldropdown.css +319 -53
- package/server/public/urldropdown.js +615 -159
- package/server/public/window_storage.js +97 -28
- package/server/socket.js +40 -9
- package/server/views/500.ejs +2 -2
- package/server/views/app.ejs +3136 -1367
- package/server/views/bookmarklet.ejs +1 -1
- package/server/views/bootstrap.ejs +1 -1
- package/server/views/columns.ejs +2 -13
- package/server/views/connect.ejs +3 -4
- package/server/views/container.ejs +1 -2
- package/server/views/d.ejs +223 -53
- package/server/views/editor.ejs +1 -1
- package/server/views/file_explorer.ejs +1 -1
- package/server/views/index.ejs +12 -11
- package/server/views/index2.ejs +4 -4
- package/server/views/init/index.ejs +4 -5
- package/server/views/install.ejs +1 -1
- package/server/views/layout.ejs +105 -0
- package/server/views/net.ejs +39 -7
- package/server/views/network.ejs +20 -6
- package/server/views/network2.ejs +1 -1
- package/server/views/old_network.ejs +2 -2
- package/server/views/partials/dynamic.ejs +3 -5
- package/server/views/partials/menu.ejs +3 -5
- package/server/views/partials/running.ejs +1 -1
- package/server/views/pro.ejs +1 -1
- package/server/views/prototype/index.ejs +1 -1
- package/server/views/review.ejs +11 -23
- package/server/views/rows.ejs +2 -13
- package/server/views/screenshots.ejs +293 -138
- package/server/views/settings.ejs +3 -4
- package/server/views/setup.ejs +1 -2
- package/server/views/shell.ejs +277 -26
- package/server/views/terminal.ejs +322 -49
- package/server/views/tools.ejs +448 -4
package/server/public/common.js
CHANGED
|
@@ -1,271 +1,1229 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
a.href = url;
|
|
5
|
-
a.download = filename;
|
|
6
|
-
document.body.appendChild(a);
|
|
7
|
-
a.click();
|
|
8
|
-
document.body.removeChild(a);
|
|
9
|
-
URL.revokeObjectURL(url);
|
|
10
|
-
}
|
|
11
|
-
async function uploadBlob(blob, filename = 'screenshot.png') {
|
|
1
|
+
const CAPTURE_MIN_SIZE = 32;
|
|
2
|
+
|
|
3
|
+
async function uploadCapture(blob, filename) {
|
|
12
4
|
const fd = new FormData();
|
|
13
5
|
fd.append('file', blob, filename);
|
|
14
6
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
const endpoints = ['/capture', '/screenshot'];
|
|
8
|
+
let lastError;
|
|
9
|
+
|
|
10
|
+
for (const endpoint of endpoints) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(endpoint, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
body: fd,
|
|
15
|
+
credentials: 'include'
|
|
16
|
+
});
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const text = await res.text();
|
|
20
|
+
throw new Error(`Upload failed: ${res.status} ${text}`);
|
|
21
|
+
}
|
|
22
|
+
return res.json();
|
|
23
|
+
} catch (err) {
|
|
24
|
+
lastError = err;
|
|
25
|
+
}
|
|
25
26
|
}
|
|
26
|
-
const json = await res.json();
|
|
27
|
-
return json
|
|
28
27
|
|
|
28
|
+
throw lastError || new Error('Upload failed');
|
|
29
29
|
}
|
|
30
|
-
async function screenshot(opts = {}) {
|
|
31
|
-
const {
|
|
32
|
-
container = document.body,
|
|
33
|
-
mimeType = 'image/png',
|
|
34
|
-
autoDownload = false,
|
|
35
|
-
filename = 'screenshot.png'
|
|
36
|
-
} = opts;
|
|
37
|
-
|
|
38
|
-
// 1) Capture one frame (no modal visible yet)
|
|
39
|
-
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
40
|
-
video: { cursor: 'always' },
|
|
41
|
-
audio: false
|
|
42
|
-
});
|
|
43
30
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
class ScreenCaptureModal {
|
|
32
|
+
constructor(stream = null, opts = {}) {
|
|
33
|
+
this.stream = stream;
|
|
34
|
+
this.opts = opts;
|
|
35
|
+
this.root = null;
|
|
36
|
+
this.stage = null;
|
|
37
|
+
this.overlay = null;
|
|
38
|
+
this.ctx = null;
|
|
39
|
+
this.statusLabel = null;
|
|
40
|
+
this.btnShot = null;
|
|
41
|
+
this.btnRecord = null;
|
|
42
|
+
this.btnCancel = null;
|
|
43
|
+
this.btnReset = null;
|
|
44
|
+
this.audioToggle = null;
|
|
45
|
+
this.audioCheckbox = null;
|
|
46
|
+
this.rect = null;
|
|
47
|
+
this.dragMode = null;
|
|
48
|
+
this.dragState = null;
|
|
49
|
+
this.dpr = window.devicePixelRatio || 1;
|
|
50
|
+
this.stageSize = null;
|
|
51
|
+
this.selectionLocked = false;
|
|
52
|
+
this.busy = false;
|
|
53
|
+
this.recordingState = 'idle';
|
|
54
|
+
this.mediaRecorder = null;
|
|
55
|
+
this.recordChunks = [];
|
|
56
|
+
this.renderCanvas = null;
|
|
57
|
+
this.renderCtx = null;
|
|
58
|
+
this.renderStream = null;
|
|
59
|
+
this.renderRaf = 0;
|
|
60
|
+
this.timerRaf = 0;
|
|
61
|
+
this.recordingStart = 0;
|
|
62
|
+
this.pendingStopOptions = null;
|
|
63
|
+
this.addedAudioTracks = [];
|
|
64
|
+
this.captureVideo = null;
|
|
65
|
+
this.snapshotCanvas = null;
|
|
66
|
+
this.snapshotUrl = null;
|
|
67
|
+
this.snapshotImg = null;
|
|
68
|
+
this.overlayHidden = false;
|
|
69
|
+
this.floatingControls = null;
|
|
70
|
+
this.floatingStatus = null;
|
|
71
|
+
this.resolveFn = null;
|
|
72
|
+
this.rejectFn = null;
|
|
73
|
+
this.colorDefault = '#ddd';
|
|
74
|
+
this.colorError = '#ff6666';
|
|
75
|
+
this.keydownHandler = this.onKeyDown.bind(this);
|
|
76
|
+
this.resizeHandler = this.fit.bind(this);
|
|
77
|
+
this.beforeUnloadHandler = this.handleBeforeUnload.bind(this);
|
|
78
|
+
this.navigationGuardHandler = this.handleNavigationGuard.bind(this);
|
|
79
|
+
this.navWarningEl = null;
|
|
80
|
+
this.navWarningTimer = 0;
|
|
81
|
+
}
|
|
52
82
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
83
|
+
async open() {
|
|
84
|
+
try {
|
|
85
|
+
await this.initStream();
|
|
86
|
+
await this.captureSnapshot();
|
|
87
|
+
this.buildDom();
|
|
88
|
+
await this.waitSnapshotReady();
|
|
89
|
+
this.fit();
|
|
90
|
+
this.updateStatus('Drag to select the capture area. Press Esc to cancel or stop.');
|
|
91
|
+
this.syncAudioToggleState();
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
this.resolveFn = resolve;
|
|
95
|
+
this.rejectFn = reject;
|
|
96
|
+
});
|
|
97
|
+
} catch (err) {
|
|
98
|
+
this.cleanup();
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async initStream() {
|
|
104
|
+
if (!this.stream) {
|
|
105
|
+
this.stream = await navigator.mediaDevices.getDisplayMedia({
|
|
106
|
+
video: { cursor: 'always' },
|
|
107
|
+
audio: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.captureVideo = document.createElement('video');
|
|
112
|
+
this.captureVideo.srcObject = this.stream;
|
|
113
|
+
this.captureVideo.playsInline = true;
|
|
114
|
+
this.captureVideo.muted = true;
|
|
115
|
+
|
|
116
|
+
await this.waitForVideo(this.captureVideo);
|
|
117
|
+
await this.captureVideo.play().catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
waitForVideo(video) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const onReady = () => {
|
|
123
|
+
cleanup();
|
|
124
|
+
resolve();
|
|
125
|
+
};
|
|
126
|
+
const onError = (err) => {
|
|
127
|
+
cleanup();
|
|
128
|
+
reject(err);
|
|
129
|
+
};
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
video.removeEventListener('loadedmetadata', onReady);
|
|
132
|
+
video.removeEventListener('error', onError);
|
|
133
|
+
};
|
|
134
|
+
if (video.readyState >= 1) {
|
|
135
|
+
resolve();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
video.addEventListener('loadedmetadata', onReady, { once: true });
|
|
139
|
+
video.addEventListener('error', onError, { once: true });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async captureSnapshot() {
|
|
144
|
+
const width = this.captureVideo.videoWidth || window.innerWidth || 1920;
|
|
145
|
+
const height = this.captureVideo.videoHeight || window.innerHeight || 1080;
|
|
146
|
+
this.snapshotCanvas = document.createElement('canvas');
|
|
147
|
+
this.snapshotCanvas.width = width;
|
|
148
|
+
this.snapshotCanvas.height = height;
|
|
149
|
+
const ctx = this.snapshotCanvas.getContext('2d');
|
|
150
|
+
ctx.drawImage(this.captureVideo, 0, 0, width, height);
|
|
151
|
+
this.snapshotUrl = this.snapshotCanvas.toDataURL('image/png');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
waitSnapshotReady() {
|
|
155
|
+
if (!this.snapshotImg) return Promise.resolve();
|
|
156
|
+
if (this.snapshotImg.complete) {
|
|
157
|
+
return Promise.resolve();
|
|
158
|
+
}
|
|
159
|
+
return this.snapshotImg.decode().catch(() => {});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
buildDom() {
|
|
163
|
+
this.root = document.createElement('div');
|
|
164
|
+
this.root.style.cssText = `
|
|
165
|
+
position:fixed; inset:0; z-index:2147483647;
|
|
166
|
+
background:rgba(0,0,0,.75);
|
|
167
|
+
display:grid; place-items:center;
|
|
168
|
+
font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
|
110
169
|
`;
|
|
111
|
-
b.onpointerdown = e => e.preventDefault();
|
|
112
|
-
b.onmouseenter = () => (b.style.background = '#222');
|
|
113
|
-
b.onmouseleave = () => (b.style.background = '#1a1a1a');
|
|
114
|
-
});
|
|
115
170
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
171
|
+
const frame = document.createElement('div');
|
|
172
|
+
frame.style.cssText = `
|
|
173
|
+
width:min(92vw,1200px);
|
|
174
|
+
height:min(80vh,720px);
|
|
175
|
+
display:grid;
|
|
176
|
+
grid-template-rows:auto 1fr auto;
|
|
177
|
+
background:#111;
|
|
178
|
+
border-radius:14px;
|
|
179
|
+
overflow:hidden;
|
|
180
|
+
box-shadow:0 20px 60px rgba(0,0,0,0.6);
|
|
181
|
+
`;
|
|
122
182
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
183
|
+
const header = document.createElement('div');
|
|
184
|
+
header.style.cssText = `
|
|
185
|
+
padding:14px 18px;
|
|
186
|
+
display:flex;
|
|
187
|
+
align-items:center;
|
|
188
|
+
justify-content:space-between;
|
|
189
|
+
background:#181818;
|
|
190
|
+
border-bottom:1px solid rgba(255,255,255,0.05);
|
|
191
|
+
color:#eee;
|
|
192
|
+
`;
|
|
193
|
+
const title = document.createElement('div');
|
|
194
|
+
title.textContent = 'Screen Capture';
|
|
195
|
+
title.style.fontWeight = '600';
|
|
196
|
+
header.appendChild(title);
|
|
197
|
+
|
|
198
|
+
const headerActions = document.createElement('div');
|
|
199
|
+
headerActions.style.cssText = 'display:flex; gap:10px; align-items:center;';
|
|
200
|
+
|
|
201
|
+
this.audioToggle = document.createElement('label');
|
|
202
|
+
this.audioToggle.style.cssText = `
|
|
203
|
+
display:flex; align-items:center; gap:6px;
|
|
204
|
+
font-size:13px; color:#bbb; cursor:pointer;
|
|
205
|
+
`;
|
|
206
|
+
const audioCheckbox = document.createElement('input');
|
|
207
|
+
audioCheckbox.type = 'checkbox';
|
|
208
|
+
audioCheckbox.checked = true;
|
|
209
|
+
audioCheckbox.style.cursor = 'pointer';
|
|
210
|
+
const audioText = document.createElement('span');
|
|
211
|
+
audioText.textContent = 'Include audio when recording';
|
|
212
|
+
this.audioToggle.append(audioCheckbox, audioText);
|
|
213
|
+
this.audioCheckbox = audioCheckbox;
|
|
214
|
+
|
|
215
|
+
this.btnReset = document.createElement('button');
|
|
216
|
+
this.btnReset.textContent = 'Reset selection';
|
|
217
|
+
this.btnReset.style.cssText = this.buttonStyle({
|
|
218
|
+
background: '#222',
|
|
219
|
+
color: '#ccc'
|
|
220
|
+
});
|
|
221
|
+
this.btnReset.addEventListener('click', () => {
|
|
222
|
+
if (this.busy || this.recordingState === 'recording') return;
|
|
223
|
+
this.rect = null;
|
|
224
|
+
this.drawOverlay();
|
|
225
|
+
this.updateButtons();
|
|
226
|
+
this.updateStatus('Drag to select the capture area. Press Esc to cancel or stop.');
|
|
227
|
+
});
|
|
126
228
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let rect = null; // {x,y,w,h} in CSS px
|
|
229
|
+
headerActions.append(this.btnReset, this.audioToggle);
|
|
230
|
+
header.append(headerActions);
|
|
130
231
|
|
|
131
|
-
|
|
132
|
-
|
|
232
|
+
this.stage = document.createElement('div');
|
|
233
|
+
this.stage.style.cssText = `
|
|
234
|
+
position:relative;
|
|
235
|
+
background:#000;
|
|
236
|
+
overflow:hidden;
|
|
237
|
+
display:grid;
|
|
238
|
+
place-items:center;
|
|
239
|
+
`;
|
|
133
240
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
241
|
+
this.snapshotImg = new Image();
|
|
242
|
+
this.snapshotImg.src = this.snapshotUrl;
|
|
243
|
+
this.snapshotImg.style.cssText = `
|
|
244
|
+
max-width:100%;
|
|
245
|
+
max-height:100%;
|
|
246
|
+
display:block;
|
|
247
|
+
background:#000;
|
|
248
|
+
user-select:none;
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
this.overlay = document.createElement('canvas');
|
|
252
|
+
this.overlay.style.cssText = 'position:absolute; inset:0; cursor:crosshair; touch-action:none;';
|
|
253
|
+
this.ctx = this.overlay.getContext('2d');
|
|
254
|
+
|
|
255
|
+
this.stage.append(this.snapshotImg, this.overlay);
|
|
256
|
+
|
|
257
|
+
const toolbar = document.createElement('div');
|
|
258
|
+
toolbar.style.cssText = `
|
|
259
|
+
padding:14px 18px;
|
|
260
|
+
display:flex;
|
|
261
|
+
align-items:center;
|
|
262
|
+
justify-content:space-between;
|
|
263
|
+
background:#181818;
|
|
264
|
+
border-top:1px solid rgba(255,255,255,0.05);
|
|
265
|
+
color:#ddd;
|
|
266
|
+
font-size:14px;
|
|
267
|
+
`;
|
|
137
268
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
269
|
+
this.statusLabel = document.createElement('div');
|
|
270
|
+
this.statusLabel.textContent = '';
|
|
271
|
+
this.statusLabel.style.cssText = 'flex:1; min-height:20px; color:currentColor;';
|
|
141
272
|
|
|
142
|
-
|
|
143
|
-
|
|
273
|
+
const buttons = document.createElement('div');
|
|
274
|
+
buttons.style.cssText = 'display:flex; gap:10px; align-items:center;';
|
|
144
275
|
|
|
145
|
-
|
|
276
|
+
this.btnShot = document.createElement('button');
|
|
277
|
+
this.btnShot.textContent = 'Capture screenshot';
|
|
278
|
+
this.btnShot.style.cssText = this.buttonStyle({ primary: true });
|
|
279
|
+
this.btnShot.addEventListener('click', () => this.handleScreenshot());
|
|
280
|
+
|
|
281
|
+
this.btnRecord = document.createElement('button');
|
|
282
|
+
this.btnRecord.textContent = 'Start recording';
|
|
283
|
+
this.btnRecord.style.cssText = this.buttonStyle();
|
|
284
|
+
this.btnRecord.addEventListener('click', () => this.handleRecordButton());
|
|
285
|
+
|
|
286
|
+
this.btnCancel = document.createElement('button');
|
|
287
|
+
this.btnCancel.textContent = 'Cancel';
|
|
288
|
+
this.btnCancel.style.cssText = this.buttonStyle({
|
|
289
|
+
background: '#1a1a1a',
|
|
290
|
+
color: '#ccc'
|
|
291
|
+
});
|
|
292
|
+
this.btnCancel.addEventListener('click', () => this.handleCancel());
|
|
293
|
+
|
|
294
|
+
buttons.append(this.btnShot, this.btnRecord, this.btnCancel);
|
|
295
|
+
|
|
296
|
+
toolbar.append(this.statusLabel, buttons);
|
|
297
|
+
|
|
298
|
+
frame.append(header, this.stage, toolbar);
|
|
299
|
+
this.root.append(frame);
|
|
300
|
+
document.body.append(this.root);
|
|
301
|
+
|
|
302
|
+
this.overlay.addEventListener('pointerdown', (e) => this.onPointerDown(e));
|
|
303
|
+
this.overlay.addEventListener('pointermove', (e) => this.onPointerMove(e));
|
|
304
|
+
this.overlay.addEventListener('pointerup', () => this.onPointerUp());
|
|
305
|
+
this.overlay.addEventListener('pointerleave', () => this.onPointerUp());
|
|
306
|
+
this.overlay.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
307
|
+
|
|
308
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
309
|
+
window.addEventListener('keydown', this.keydownHandler);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
buttonStyle({ primary = false, background, color } = {}) {
|
|
313
|
+
const baseBg = primary ? '#3a82ff' : (background || '#252525');
|
|
314
|
+
const baseColor = primary ? '#fff' : (color || '#eee');
|
|
315
|
+
return `
|
|
316
|
+
padding:10px 18px;
|
|
317
|
+
border-radius:10px;
|
|
318
|
+
border:1px solid rgba(255,255,255,0.08);
|
|
319
|
+
background:${baseBg};
|
|
320
|
+
color:${baseColor};
|
|
321
|
+
cursor:pointer;
|
|
322
|
+
font-size:14px;
|
|
323
|
+
font-weight:${primary ? '600' : '500'};
|
|
324
|
+
`;
|
|
146
325
|
}
|
|
147
326
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
327
|
+
fit() {
|
|
328
|
+
if (!this.stage || !this.overlay) return;
|
|
329
|
+
const rect = this.stage.getBoundingClientRect();
|
|
330
|
+
const prev = this.stageSize;
|
|
331
|
+
this.stageSize = { width: rect.width, height: rect.height };
|
|
332
|
+
|
|
333
|
+
this.overlay.style.width = rect.width + 'px';
|
|
334
|
+
this.overlay.style.height = rect.height + 'px';
|
|
335
|
+
this.overlay.width = Math.round(rect.width * this.dpr);
|
|
336
|
+
this.overlay.height = Math.round(rect.height * this.dpr);
|
|
337
|
+
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
338
|
+
|
|
339
|
+
if (prev && this.rect) {
|
|
340
|
+
const scaleX = rect.width / prev.width;
|
|
341
|
+
const scaleY = rect.height / prev.height;
|
|
342
|
+
this.rect = this.clampRect({
|
|
343
|
+
x: this.rect.x * scaleX,
|
|
344
|
+
y: this.rect.y * scaleY,
|
|
345
|
+
w: this.rect.w * scaleX,
|
|
346
|
+
h: this.rect.h * scaleY
|
|
347
|
+
});
|
|
161
348
|
}
|
|
349
|
+
|
|
350
|
+
this.drawOverlay();
|
|
351
|
+
this.updateButtons();
|
|
162
352
|
}
|
|
163
353
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
354
|
+
syncAudioToggleState() {
|
|
355
|
+
if (!this.audioCheckbox) return;
|
|
356
|
+
const hasAudioTracks = !!(this.stream && this.stream.getAudioTracks && this.stream.getAudioTracks().length);
|
|
357
|
+
if (!hasAudioTracks) {
|
|
358
|
+
this.audioCheckbox.checked = false;
|
|
359
|
+
this.audioCheckbox.disabled = true;
|
|
360
|
+
if (this.audioToggle) {
|
|
361
|
+
this.audioToggle.style.opacity = '0.6';
|
|
362
|
+
this.audioToggle.title = 'Audio capture is unavailable for this share';
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
this.audioCheckbox.disabled = false;
|
|
366
|
+
if (this.audioToggle) {
|
|
367
|
+
this.audioToggle.style.opacity = '';
|
|
368
|
+
this.audioToggle.title = '';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
167
371
|
}
|
|
168
372
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
373
|
+
clampRect(rect) {
|
|
374
|
+
if (!rect) return rect;
|
|
375
|
+
const bounds = this.getVideoBounds();
|
|
376
|
+
if (!bounds) return rect;
|
|
377
|
+
|
|
378
|
+
const minSize = CAPTURE_MIN_SIZE;
|
|
379
|
+
let { x, y, w, h } = rect;
|
|
380
|
+
if (w < 0) { x += w; w *= -1; }
|
|
381
|
+
if (h < 0) { y += h; h *= -1; }
|
|
382
|
+
|
|
383
|
+
const maxX = bounds.x + bounds.width;
|
|
384
|
+
const maxY = bounds.y + bounds.height;
|
|
385
|
+
|
|
386
|
+
x = Math.max(bounds.x, Math.min(x, maxX));
|
|
387
|
+
y = Math.max(bounds.y, Math.min(y, maxY));
|
|
388
|
+
|
|
389
|
+
const maxW = maxX - x;
|
|
390
|
+
const maxH = maxY - y;
|
|
185
391
|
|
|
186
|
-
|
|
187
|
-
|
|
392
|
+
w = Math.max(Math.min(w, maxW), 1);
|
|
393
|
+
h = Math.max(Math.min(h, maxH), 1);
|
|
188
394
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
395
|
+
if (w < minSize) w = Math.min(minSize, maxW);
|
|
396
|
+
if (h < minSize) h = Math.min(minSize, maxH);
|
|
397
|
+
|
|
398
|
+
return { x, y, w, h };
|
|
192
399
|
}
|
|
193
400
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
401
|
+
getVideoBounds() {
|
|
402
|
+
if (!this.snapshotImg) return null;
|
|
403
|
+
const imgRect = this.snapshotImg.getBoundingClientRect();
|
|
404
|
+
const stageRect = this.stage.getBoundingClientRect();
|
|
198
405
|
return {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
iw: img.naturalWidth,
|
|
204
|
-
ih: img.naturalHeight
|
|
406
|
+
x: imgRect.left - stageRect.left,
|
|
407
|
+
y: imgRect.top - stageRect.top,
|
|
408
|
+
width: imgRect.width,
|
|
409
|
+
height: imgRect.height
|
|
205
410
|
};
|
|
206
411
|
}
|
|
207
412
|
|
|
208
|
-
|
|
209
|
-
|
|
413
|
+
drawOverlay() {
|
|
414
|
+
if (!this.ctx) return;
|
|
415
|
+
const w = this.overlay.width / this.dpr;
|
|
416
|
+
const h = this.overlay.height / this.dpr;
|
|
417
|
+
this.ctx.clearRect(0, 0, w, h);
|
|
418
|
+
|
|
419
|
+
this.ctx.fillStyle = 'rgba(0,0,0,0.45)';
|
|
420
|
+
this.ctx.fillRect(0, 0, w, h);
|
|
421
|
+
|
|
422
|
+
if (this.rect) {
|
|
423
|
+
const { x, y, w: rw, h: rh } = this.rect;
|
|
424
|
+
this.ctx.save();
|
|
425
|
+
this.ctx.globalCompositeOperation = 'destination-out';
|
|
426
|
+
this.ctx.fillRect(x, y, rw, rh);
|
|
427
|
+
this.ctx.restore();
|
|
428
|
+
|
|
429
|
+
this.ctx.save();
|
|
430
|
+
this.ctx.strokeStyle = '#4cc3ff';
|
|
431
|
+
this.ctx.lineWidth = 2;
|
|
432
|
+
this.ctx.setLineDash([8, 6]);
|
|
433
|
+
this.ctx.strokeRect(x + 1, y + 1, rw - 2, rh - 2);
|
|
434
|
+
this.ctx.restore();
|
|
435
|
+
|
|
436
|
+
this.drawHandles();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
210
439
|
|
|
211
|
-
|
|
440
|
+
drawHandles() {
|
|
441
|
+
if (!this.rect) return;
|
|
442
|
+
const handleSize = 10;
|
|
443
|
+
const half = handleSize / 2;
|
|
444
|
+
const points = this.getHandlePoints();
|
|
445
|
+
this.ctx.save();
|
|
446
|
+
this.ctx.fillStyle = '#4cc3ff';
|
|
447
|
+
points.forEach(({ x, y }) => {
|
|
448
|
+
this.ctx.fillRect(x - half, y - half, handleSize, handleSize);
|
|
449
|
+
});
|
|
450
|
+
this.ctx.restore();
|
|
451
|
+
}
|
|
212
452
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
453
|
+
getHandlePoints() {
|
|
454
|
+
if (!this.rect) return [];
|
|
455
|
+
const { x, y, w, h } = this.rect;
|
|
456
|
+
return [
|
|
457
|
+
{ x, y },
|
|
458
|
+
{ x: x + w / 2, y },
|
|
459
|
+
{ x: x + w, y },
|
|
460
|
+
{ x, y: y + h / 2 },
|
|
461
|
+
{ x: x + w, y: y + h / 2 },
|
|
462
|
+
{ x, y: y + h },
|
|
463
|
+
{ x: x + w / 2, y: y + h },
|
|
464
|
+
{ x: x + w, y: y + h }
|
|
465
|
+
];
|
|
466
|
+
}
|
|
218
467
|
|
|
219
|
-
|
|
220
|
-
|
|
468
|
+
pointerToStage(e) {
|
|
469
|
+
const rect = this.overlay.getBoundingClientRect();
|
|
470
|
+
return {
|
|
471
|
+
x: e.clientX - rect.left,
|
|
472
|
+
y: e.clientY - rect.top
|
|
473
|
+
};
|
|
474
|
+
}
|
|
221
475
|
|
|
222
|
-
|
|
223
|
-
|
|
476
|
+
onPointerDown(e) {
|
|
477
|
+
if (this.busy || this.selectionLocked) return;
|
|
478
|
+
const point = this.clampPointToVideo(this.pointerToStage(e));
|
|
479
|
+
const hit = this.hitTest(point);
|
|
480
|
+
|
|
481
|
+
if (hit.mode === 'move') {
|
|
482
|
+
this.dragMode = 'move';
|
|
483
|
+
this.dragState = {
|
|
484
|
+
offsetX: point.x - this.rect.x,
|
|
485
|
+
offsetY: point.y - this.rect.y
|
|
486
|
+
};
|
|
487
|
+
} else if (hit.mode === 'resize') {
|
|
488
|
+
this.dragMode = 'resize';
|
|
489
|
+
this.dragState = {
|
|
490
|
+
edge: hit.edge,
|
|
491
|
+
startRect: { ...this.rect }
|
|
492
|
+
};
|
|
493
|
+
} else {
|
|
494
|
+
this.dragMode = 'create';
|
|
495
|
+
this.rect = { x: point.x, y: point.y, w: 1, h: 1 };
|
|
496
|
+
this.dragState = { start: point };
|
|
497
|
+
}
|
|
224
498
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
499
|
+
this.overlay.setPointerCapture(e.pointerId);
|
|
500
|
+
this.drawOverlay();
|
|
501
|
+
this.updateButtons();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
onPointerMove(e) {
|
|
505
|
+
if (this.busy || this.selectionLocked) {
|
|
506
|
+
this.updateCursor(e, null);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const point = this.clampPointToVideo(this.pointerToStage(e));
|
|
510
|
+
|
|
511
|
+
if (this.dragMode === 'create' && this.dragState) {
|
|
512
|
+
const start = this.dragState.start;
|
|
513
|
+
this.rect = this.clampRect({
|
|
514
|
+
x: Math.min(start.x, point.x),
|
|
515
|
+
y: Math.min(start.y, point.y),
|
|
516
|
+
w: Math.abs(point.x - start.x),
|
|
517
|
+
h: Math.abs(point.y - start.y)
|
|
518
|
+
});
|
|
519
|
+
this.drawOverlay();
|
|
520
|
+
this.updateButtons();
|
|
521
|
+
return;
|
|
229
522
|
}
|
|
230
523
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
524
|
+
if (this.dragMode === 'move' && this.dragState && this.rect) {
|
|
525
|
+
const bounds = this.getVideoBounds();
|
|
526
|
+
if (!bounds) return;
|
|
527
|
+
let nx = point.x - this.dragState.offsetX;
|
|
528
|
+
let ny = point.y - this.dragState.offsetY;
|
|
529
|
+
nx = Math.max(bounds.x, Math.min(nx, bounds.x + bounds.width - this.rect.w));
|
|
530
|
+
ny = Math.max(bounds.y, Math.min(ny, bounds.y + bounds.height - this.rect.h));
|
|
531
|
+
this.rect.x = nx;
|
|
532
|
+
this.rect.y = ny;
|
|
533
|
+
this.drawOverlay();
|
|
534
|
+
this.updateButtons();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
237
537
|
|
|
238
|
-
|
|
239
|
-
|
|
538
|
+
if (this.dragMode === 'resize' && this.dragState && this.rect) {
|
|
539
|
+
const bounds = this.getVideoBounds();
|
|
540
|
+
const { edge, startRect } = this.dragState;
|
|
541
|
+
let { x, y, w, h } = startRect;
|
|
240
542
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
543
|
+
if (edge.includes('left')) {
|
|
544
|
+
const right = x + w;
|
|
545
|
+
x = Math.min(point.x, right - CAPTURE_MIN_SIZE);
|
|
546
|
+
x = Math.max(bounds.x, x);
|
|
547
|
+
w = right - x;
|
|
548
|
+
}
|
|
549
|
+
if (edge.includes('right')) {
|
|
550
|
+
const maxX = bounds.x + bounds.width;
|
|
551
|
+
const newRight = Math.max(point.x, x + CAPTURE_MIN_SIZE);
|
|
552
|
+
w = Math.min(newRight - x, maxX - x);
|
|
553
|
+
}
|
|
554
|
+
if (edge.includes('top')) {
|
|
555
|
+
const bottom = y + h;
|
|
556
|
+
y = Math.min(point.y, bottom - CAPTURE_MIN_SIZE);
|
|
557
|
+
y = Math.max(bounds.y, y);
|
|
558
|
+
h = bottom - y;
|
|
559
|
+
}
|
|
560
|
+
if (edge.includes('bottom')) {
|
|
561
|
+
const maxY = bounds.y + bounds.height;
|
|
562
|
+
const newBottom = Math.max(point.y, y + CAPTURE_MIN_SIZE);
|
|
563
|
+
h = Math.min(newBottom - y, maxY - y);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this.rect = this.clampRect({ x, y, w, h });
|
|
567
|
+
this.drawOverlay();
|
|
568
|
+
this.updateButtons();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
this.updateCursor(e, this.hitTest(point));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
onPointerUp() {
|
|
576
|
+
if (this.dragMode) {
|
|
577
|
+
this.dragMode = null;
|
|
578
|
+
this.dragState = null;
|
|
579
|
+
this.drawOverlay();
|
|
580
|
+
this.updateButtons();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
clampPointToVideo(point) {
|
|
585
|
+
const bounds = this.getVideoBounds();
|
|
586
|
+
if (!bounds) return point;
|
|
587
|
+
return {
|
|
588
|
+
x: Math.max(bounds.x, Math.min(point.x, bounds.x + bounds.width)),
|
|
589
|
+
y: Math.max(bounds.y, Math.min(point.y, bounds.y + bounds.height))
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
hitTest(point) {
|
|
594
|
+
if (!this.rect) {
|
|
595
|
+
return { mode: 'create' };
|
|
596
|
+
}
|
|
597
|
+
const { x, y, w, h } = this.rect;
|
|
598
|
+
const left = x;
|
|
599
|
+
const right = x + w;
|
|
600
|
+
const top = y;
|
|
601
|
+
const bottom = y + h;
|
|
602
|
+
const margin = 10;
|
|
603
|
+
|
|
604
|
+
const nearLeft = Math.abs(point.x - left) <= margin;
|
|
605
|
+
const nearRight = Math.abs(point.x - right) <= margin;
|
|
606
|
+
const nearTop = Math.abs(point.y - top) <= margin;
|
|
607
|
+
const nearBottom = Math.abs(point.y - bottom) <= margin;
|
|
608
|
+
const inside = point.x > left + margin && point.x < right - margin && point.y > top + margin && point.y < bottom - margin;
|
|
609
|
+
|
|
610
|
+
if ((nearLeft && nearTop) || (nearRight && nearBottom) || (nearLeft && nearBottom) || (nearRight && nearTop)) {
|
|
611
|
+
const edge = `${nearTop ? 'top' : 'bottom'}-${nearLeft ? 'left' : 'right'}`;
|
|
612
|
+
return { mode: 'resize', edge };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (nearLeft) return { mode: 'resize', edge: 'left' };
|
|
616
|
+
if (nearRight) return { mode: 'resize', edge: 'right' };
|
|
617
|
+
if (nearTop) return { mode: 'resize', edge: 'top' };
|
|
618
|
+
if (nearBottom) return { mode: 'resize', edge: 'bottom' };
|
|
619
|
+
|
|
620
|
+
if (inside) return { mode: 'move' };
|
|
621
|
+
return { mode: 'create' };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
updateCursor(e, hit) {
|
|
625
|
+
if (!hit) {
|
|
626
|
+
this.overlay.style.cursor = 'crosshair';
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (hit.mode === 'move') {
|
|
630
|
+
this.overlay.style.cursor = 'move';
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (hit.mode === 'resize') {
|
|
634
|
+
const edge = hit.edge;
|
|
635
|
+
const map = {
|
|
636
|
+
top: 'ns-resize',
|
|
637
|
+
bottom: 'ns-resize',
|
|
638
|
+
left: 'ew-resize',
|
|
639
|
+
right: 'ew-resize',
|
|
640
|
+
'top-left': 'nwse-resize',
|
|
641
|
+
'bottom-right': 'nwse-resize',
|
|
642
|
+
'top-right': 'nesw-resize',
|
|
643
|
+
'bottom-left': 'nesw-resize'
|
|
644
|
+
};
|
|
645
|
+
this.overlay.style.cursor = map[edge] || 'crosshair';
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.overlay.style.cursor = 'crosshair';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
hasValidSelection() {
|
|
652
|
+
return this.rect && this.rect.w >= CAPTURE_MIN_SIZE && this.rect.h >= CAPTURE_MIN_SIZE;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
updateButtons() {
|
|
656
|
+
const valid = this.hasValidSelection();
|
|
657
|
+
const disabled = this.busy || (this.recordingState !== 'idle' && this.recordingState !== 'stopping');
|
|
658
|
+
if (this.btnShot) this.btnShot.disabled = disabled || !valid;
|
|
659
|
+
if (this.btnRecord) {
|
|
660
|
+
if (this.recordingState === 'recording') {
|
|
661
|
+
this.btnRecord.textContent = 'Stop recording';
|
|
662
|
+
} else if (this.recordingState === 'stopping') {
|
|
663
|
+
this.btnRecord.textContent = 'Finishing…';
|
|
664
|
+
} else {
|
|
665
|
+
this.btnRecord.textContent = 'Start recording';
|
|
666
|
+
}
|
|
667
|
+
this.btnRecord.disabled = this.busy || (!valid && this.recordingState !== 'recording');
|
|
668
|
+
}
|
|
669
|
+
if (this.btnReset) this.btnReset.disabled = this.busy || this.recordingState === 'recording';
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
updateStatus(message, { error = false } = {}) {
|
|
673
|
+
if (!this.statusLabel) return;
|
|
674
|
+
this.statusLabel.textContent = message;
|
|
675
|
+
this.statusLabel.style.color = error ? this.colorError : this.colorDefault;
|
|
676
|
+
if (this.floatingStatus) {
|
|
677
|
+
this.floatingStatus.textContent = message;
|
|
678
|
+
this.floatingStatus.style.color = error ? '#ff9a9a' : '#fff';
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async handleScreenshot() {
|
|
683
|
+
if (!this.hasValidSelection() || this.busy) return;
|
|
684
|
+
const source = this.computeSourceRect();
|
|
685
|
+
if (!source) {
|
|
686
|
+
this.updateStatus('No area selected', { error: true });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
this.setBusy(true, 'Capturing screenshot…');
|
|
690
|
+
try {
|
|
691
|
+
await this.hideOverlayForCapture();
|
|
692
|
+
const { blob, filename } = await this.captureStill(source);
|
|
693
|
+
await uploadCapture(blob, filename);
|
|
694
|
+
this.showCaptureSavedModal('Screenshot');
|
|
695
|
+
this.resolveAndClose({ type: 'image', filename });
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.error('Screenshot failed', err);
|
|
698
|
+
await this.showOverlayAfterCapture();
|
|
699
|
+
this.setBusy(false);
|
|
700
|
+
this.updateStatus('Failed to capture screenshot', { error: true });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async handleRecordButton() {
|
|
705
|
+
if (this.recordingState === 'recording') {
|
|
706
|
+
await this.stopRecording();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (!this.hasValidSelection() || this.busy) return;
|
|
710
|
+
try {
|
|
711
|
+
await this.startRecording();
|
|
712
|
+
} catch (err) {
|
|
713
|
+
console.error('Unable to start recording', err);
|
|
714
|
+
this.updateStatus('Unable to start recording', { error: true });
|
|
715
|
+
this.resetRecordingState();
|
|
250
716
|
}
|
|
251
|
-
uploadBlob(blob)
|
|
252
|
-
// downloadBlob(blob)
|
|
253
|
-
return { blob, dataURL };
|
|
254
717
|
}
|
|
255
718
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
719
|
+
async startRecording() {
|
|
720
|
+
const source = this.computeSourceRect();
|
|
721
|
+
if (!source) throw new Error('No selection');
|
|
722
|
+
|
|
723
|
+
await this.hideOverlayForCapture({ showControls: true });
|
|
724
|
+
this.selectionLocked = true;
|
|
725
|
+
this.recordingState = 'recording';
|
|
726
|
+
this.setBusy(false, 'Recording… Stay on this page while capturing.');
|
|
727
|
+
this.updateButtons();
|
|
728
|
+
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
|
729
|
+
document.addEventListener('click', this.navigationGuardHandler, true);
|
|
730
|
+
|
|
731
|
+
const { sx, sy, sw, sh } = source;
|
|
732
|
+
this.renderCanvas = document.createElement('canvas');
|
|
733
|
+
this.renderCanvas.width = sw;
|
|
734
|
+
this.renderCanvas.height = sh;
|
|
735
|
+
this.renderCtx = this.renderCanvas.getContext('2d');
|
|
736
|
+
this.renderCtx.imageSmoothingQuality = 'high';
|
|
737
|
+
|
|
738
|
+
const fps = 30;
|
|
739
|
+
const drawFrame = () => {
|
|
740
|
+
this.renderCtx.drawImage(this.captureVideo, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
741
|
+
this.renderRaf = requestAnimationFrame(drawFrame);
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
drawFrame();
|
|
745
|
+
|
|
746
|
+
this.renderStream = this.renderCanvas.captureStream(fps);
|
|
747
|
+
const includeAudio = !!(this.audioCheckbox && this.audioCheckbox.checked);
|
|
748
|
+
this.addedAudioTracks = [];
|
|
749
|
+
if (includeAudio) {
|
|
750
|
+
const audioTracks = this.stream.getAudioTracks();
|
|
751
|
+
audioTracks.forEach(track => {
|
|
752
|
+
const clone = track.clone();
|
|
753
|
+
this.addedAudioTracks.push(clone);
|
|
754
|
+
this.renderStream.addTrack(clone);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const mime = this.selectMimeType([
|
|
759
|
+
'video/webm;codecs=vp9,opus',
|
|
760
|
+
'video/webm;codecs=vp8,opus',
|
|
761
|
+
'video/webm'
|
|
762
|
+
]);
|
|
763
|
+
this.recordChunks = [];
|
|
764
|
+
this.mediaRecorder = new MediaRecorder(this.renderStream, mime ? { mimeType: mime } : undefined);
|
|
765
|
+
|
|
766
|
+
this.mediaRecorder.ondataavailable = (e) => {
|
|
767
|
+
if (e.data && e.data.size) this.recordChunks.push(e.data);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
this.mediaRecorder.onerror = (e) => {
|
|
771
|
+
console.error('MediaRecorder error', e);
|
|
772
|
+
this.updateStatus('Recording error', { error: true });
|
|
773
|
+
this.stopRecording({ discard: true });
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
this.mediaRecorder.onstop = async () => {
|
|
777
|
+
cancelAnimationFrame(this.renderRaf);
|
|
778
|
+
this.renderRaf = 0;
|
|
779
|
+
if (this.renderStream) {
|
|
780
|
+
this.renderStream.getTracks().forEach(t => t.stop());
|
|
781
|
+
}
|
|
782
|
+
const { discard } = this.pendingStopOptions || {};
|
|
783
|
+
this.pendingStopOptions = null;
|
|
784
|
+
const blob = new Blob(this.recordChunks, { type: mime || 'video/webm' });
|
|
785
|
+
if (discard) {
|
|
786
|
+
await this.showOverlayAfterCapture();
|
|
787
|
+
this.resetRecordingState();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
259
790
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
791
|
+
this.setBusy(true, 'Saving recording…');
|
|
792
|
+
const filename = `${Date.now()}.webm`;
|
|
793
|
+
await uploadCapture(blob, filename);
|
|
794
|
+
this.showCaptureSavedModal('Recording');
|
|
795
|
+
this.resolveAndClose({ type: 'video', filename });
|
|
796
|
+
} catch (err) {
|
|
797
|
+
console.error('Failed to save recording', err);
|
|
798
|
+
await this.showOverlayAfterCapture();
|
|
799
|
+
this.setBusy(false);
|
|
800
|
+
this.updateStatus('Failed to save recording', { error: true });
|
|
801
|
+
this.resetRecordingState();
|
|
266
802
|
}
|
|
267
803
|
};
|
|
268
|
-
|
|
804
|
+
|
|
805
|
+
this.mediaRecorder.start();
|
|
806
|
+
this.recordingStart = performance.now();
|
|
807
|
+
this.updateStatus('Recording… Stay on this page while capturing. Press Stop or Esc to finish.');
|
|
808
|
+
this.updateButtons();
|
|
809
|
+
this.tickTimer();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
tickTimer() {
|
|
813
|
+
if (this.recordingState !== 'recording') {
|
|
814
|
+
cancelAnimationFrame(this.timerRaf);
|
|
815
|
+
this.timerRaf = 0;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const elapsed = performance.now() - this.recordingStart;
|
|
819
|
+
const message = `Recording… ${this.formatDuration(elapsed)}`;
|
|
820
|
+
if (this.statusLabel) {
|
|
821
|
+
this.statusLabel.textContent = message;
|
|
822
|
+
this.statusLabel.style.color = this.colorDefault;
|
|
823
|
+
}
|
|
824
|
+
if (this.floatingStatus) {
|
|
825
|
+
this.floatingStatus.textContent = message;
|
|
826
|
+
this.floatingStatus.style.color = '#fff';
|
|
827
|
+
}
|
|
828
|
+
this.timerRaf = requestAnimationFrame(() => this.tickTimer());
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async stopRecording(options = {}) {
|
|
832
|
+
if (this.mediaRecorder && (this.recordingState === 'recording' || this.recordingState === 'stopping')) {
|
|
833
|
+
this.pendingStopOptions = options;
|
|
834
|
+
if (this.mediaRecorder.state !== 'inactive') {
|
|
835
|
+
this.mediaRecorder.stop();
|
|
836
|
+
}
|
|
837
|
+
this.recordingState = 'stopping';
|
|
838
|
+
cancelAnimationFrame(this.timerRaf);
|
|
839
|
+
this.timerRaf = 0;
|
|
840
|
+
this.updateStatus('Finishing recording…');
|
|
841
|
+
this.updateButtons();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
resetRecordingState() {
|
|
846
|
+
this.recordingState = 'idle';
|
|
847
|
+
this.selectionLocked = false;
|
|
848
|
+
this.mediaRecorder = null;
|
|
849
|
+
this.renderCanvas = null;
|
|
850
|
+
this.renderCtx = null;
|
|
851
|
+
this.renderStream = null;
|
|
852
|
+
this.recordChunks = [];
|
|
853
|
+
this.addedAudioTracks = [];
|
|
854
|
+
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
|
855
|
+
document.removeEventListener('click', this.navigationGuardHandler, true);
|
|
856
|
+
this.updateStatus('Drag to select the capture area. Press Esc to cancel or stop.');
|
|
857
|
+
this.updateButtons();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
selectMimeType(candidates) {
|
|
861
|
+
if (!window.MediaRecorder) return null;
|
|
862
|
+
return candidates.find(type => MediaRecorder.isTypeSupported(type)) || null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
computeSourceRect() {
|
|
866
|
+
if (!this.hasValidSelection()) return null;
|
|
867
|
+
const bounds = this.getVideoBounds();
|
|
868
|
+
if (!bounds) return null;
|
|
869
|
+
const { x, y, w, h } = this.rect;
|
|
870
|
+
|
|
871
|
+
const rx = x - bounds.x;
|
|
872
|
+
const ry = y - bounds.y;
|
|
873
|
+
const rw = Math.min(w, bounds.width - rx);
|
|
874
|
+
const rh = Math.min(h, bounds.height - ry);
|
|
875
|
+
|
|
876
|
+
const videoWidth = this.captureVideo ? this.captureVideo.videoWidth : this.snapshotCanvas?.width;
|
|
877
|
+
const videoHeight = this.captureVideo ? this.captureVideo.videoHeight : this.snapshotCanvas?.height;
|
|
878
|
+
if (!videoWidth || !videoHeight) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
const scaleX = videoWidth / bounds.width;
|
|
882
|
+
const scaleY = videoHeight / bounds.height;
|
|
883
|
+
|
|
884
|
+
const sx = Math.max(0, Math.floor(rx * scaleX));
|
|
885
|
+
const sy = Math.max(0, Math.floor(ry * scaleY));
|
|
886
|
+
const sw = Math.max(1, Math.floor(rw * scaleX));
|
|
887
|
+
const sh = Math.max(1, Math.floor(rh * scaleY));
|
|
888
|
+
|
|
889
|
+
return { sx, sy, sw, sh };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async captureStill(source) {
|
|
893
|
+
const region = source || this.computeSourceRect();
|
|
894
|
+
if (!region) throw new Error('No selection');
|
|
895
|
+
const { sx, sy, sw, sh } = region;
|
|
896
|
+
|
|
897
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
898
|
+
|
|
899
|
+
const canvas = document.createElement('canvas');
|
|
900
|
+
canvas.width = sw;
|
|
901
|
+
canvas.height = sh;
|
|
902
|
+
const ctx = canvas.getContext('2d');
|
|
903
|
+
ctx.imageSmoothingQuality = 'high';
|
|
904
|
+
ctx.drawImage(this.captureVideo, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
905
|
+
|
|
906
|
+
const blob = await new Promise((resolve, reject) => {
|
|
907
|
+
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('Canvas export failed'))), 'image/png');
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
return { blob, filename: `${Date.now()}.png` };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
setBusy(state, message) {
|
|
914
|
+
this.busy = state;
|
|
915
|
+
this.updateButtons();
|
|
916
|
+
if (message) {
|
|
917
|
+
this.updateStatus(message);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async hideOverlayForCapture({ showControls = false } = {}) {
|
|
922
|
+
if (!this.root || this.overlayHidden) return;
|
|
923
|
+
this.overlayHidden = true;
|
|
924
|
+
this.root.style.opacity = '0';
|
|
925
|
+
this.root.style.pointerEvents = 'none';
|
|
926
|
+
if (showControls) {
|
|
927
|
+
this.ensureFloatingControls();
|
|
928
|
+
}
|
|
929
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
930
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
931
|
+
if (this.root) {
|
|
932
|
+
this.root.style.display = 'none';
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async showOverlayAfterCapture() {
|
|
937
|
+
if (!this.root) return;
|
|
938
|
+
this.overlayHidden = false;
|
|
939
|
+
this.root.style.display = '';
|
|
940
|
+
this.root.style.opacity = '';
|
|
941
|
+
this.root.style.pointerEvents = '';
|
|
942
|
+
this.removeFloatingControls();
|
|
943
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
944
|
+
this.fit();
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
ensureFloatingControls() {
|
|
948
|
+
if (this.floatingControls) return;
|
|
949
|
+
const controls = document.createElement('div');
|
|
950
|
+
controls.style.cssText = `
|
|
951
|
+
position:fixed; top:16px; right:16px; z-index:2147483647;
|
|
952
|
+
display:flex; align-items:center; gap:12px;
|
|
953
|
+
background:rgba(0,0,0,0.82);
|
|
954
|
+
color:#fff; padding:10px 14px; border-radius:12px;
|
|
955
|
+
box-shadow:0 8px 24px rgba(0,0,0,0.35);
|
|
956
|
+
font-size:14px; font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
|
957
|
+
pointer-events:auto;
|
|
958
|
+
`;
|
|
959
|
+
|
|
960
|
+
const status = document.createElement('div');
|
|
961
|
+
status.textContent = 'Recording…';
|
|
962
|
+
|
|
963
|
+
const stopBtn = document.createElement('button');
|
|
964
|
+
stopBtn.textContent = 'Stop recording';
|
|
965
|
+
stopBtn.style.cssText = `
|
|
966
|
+
padding:8px 14px; border-radius:999px; border:none;
|
|
967
|
+
background:#ff4d4f; color:#fff; cursor:pointer; font-weight:600;
|
|
968
|
+
`;
|
|
969
|
+
stopBtn.addEventListener('click', () => {
|
|
970
|
+
if (stopBtn.disabled) return;
|
|
971
|
+
stopBtn.disabled = true;
|
|
972
|
+
stopBtn.textContent = 'Stopping…';
|
|
973
|
+
this.stopRecording();
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
controls.append(status, stopBtn);
|
|
977
|
+
document.body.appendChild(controls);
|
|
978
|
+
this.floatingControls = controls;
|
|
979
|
+
this.floatingStatus = status;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
removeFloatingControls() {
|
|
983
|
+
if (this.floatingControls && this.floatingControls.parentNode) {
|
|
984
|
+
this.floatingControls.parentNode.removeChild(this.floatingControls);
|
|
985
|
+
}
|
|
986
|
+
this.floatingControls = null;
|
|
987
|
+
this.floatingStatus = null;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
formatDuration(ms) {
|
|
991
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
992
|
+
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
|
993
|
+
const seconds = String(totalSeconds % 60).padStart(2, '0');
|
|
994
|
+
return `${minutes}:${seconds}`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async handleCancel() {
|
|
998
|
+
if (this.recordingState === 'recording') {
|
|
999
|
+
await this.stopRecording({ discard: true });
|
|
1000
|
+
this.updateStatus('Recording discarded');
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
this.rejectAndClose(new DOMException('Canceled', 'AbortError'));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
handleBeforeUnload(e) {
|
|
1007
|
+
if (this.recordingState === 'recording') {
|
|
1008
|
+
const message = 'Screen recording is in progress. Stay on this page to finish saving your capture.';
|
|
1009
|
+
e.preventDefault();
|
|
1010
|
+
e.returnValue = message;
|
|
1011
|
+
return message;
|
|
1012
|
+
}
|
|
1013
|
+
return undefined;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
handleNavigationGuard(e) {
|
|
1017
|
+
if (this.recordingState !== 'recording') return;
|
|
1018
|
+
const anchor = e.target.closest && e.target.closest('a[href]');
|
|
1019
|
+
if (!anchor) return;
|
|
1020
|
+
if (anchor.getAttribute('target') && anchor.getAttribute('target') !== '_self') {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const href = anchor.getAttribute('href');
|
|
1024
|
+
if (!href || href.startsWith('#')) return;
|
|
1025
|
+
e.preventDefault();
|
|
1026
|
+
e.stopPropagation();
|
|
1027
|
+
this.updateStatus('Finish or cancel the recording before navigating away.', { error: true });
|
|
1028
|
+
this.showNavigationWarning();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
onKeyDown(e) {
|
|
1032
|
+
if (e.key === 'Escape') {
|
|
1033
|
+
e.preventDefault();
|
|
1034
|
+
this.handleCancel();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
resolveAndClose(payload) {
|
|
1039
|
+
this.cleanup();
|
|
1040
|
+
if (this.resolveFn) this.resolveFn(payload);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
rejectAndClose(error) {
|
|
1044
|
+
this.cleanup();
|
|
1045
|
+
if (this.rejectFn) this.rejectFn(error);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
cleanup() {
|
|
1049
|
+
cancelAnimationFrame(this.renderRaf);
|
|
1050
|
+
cancelAnimationFrame(this.timerRaf);
|
|
1051
|
+
if (this.renderStream) {
|
|
1052
|
+
this.renderStream.getTracks().forEach(t => t.stop());
|
|
1053
|
+
}
|
|
1054
|
+
if (this.addedAudioTracks && this.addedAudioTracks.length) {
|
|
1055
|
+
this.addedAudioTracks.forEach(track => track.stop());
|
|
1056
|
+
this.addedAudioTracks = [];
|
|
1057
|
+
}
|
|
1058
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
1059
|
+
try { this.mediaRecorder.stop(); } catch (e) { }
|
|
1060
|
+
}
|
|
1061
|
+
if (this.stream) {
|
|
1062
|
+
this.stream.getTracks().forEach(track => track.stop());
|
|
1063
|
+
}
|
|
1064
|
+
if (this.captureVideo) {
|
|
1065
|
+
try { this.captureVideo.pause(); } catch (e) {}
|
|
1066
|
+
this.captureVideo.srcObject = null;
|
|
1067
|
+
this.captureVideo.remove();
|
|
1068
|
+
this.captureVideo = null;
|
|
1069
|
+
}
|
|
1070
|
+
this.stream = null;
|
|
1071
|
+
this.snapshotCanvas = null;
|
|
1072
|
+
this.snapshotUrl = null;
|
|
1073
|
+
this.snapshotImg = null;
|
|
1074
|
+
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
|
1075
|
+
document.removeEventListener('click', this.navigationGuardHandler, true);
|
|
1076
|
+
this.removeFloatingControls();
|
|
1077
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
1078
|
+
window.removeEventListener('keydown', this.keydownHandler);
|
|
1079
|
+
this.hideNavigationWarning(true);
|
|
1080
|
+
if (this.root && this.root.parentNode) {
|
|
1081
|
+
this.root.parentNode.removeChild(this.root);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
showNavigationWarning() {
|
|
1086
|
+
if (!this.navWarningEl) {
|
|
1087
|
+
const wrap = document.createElement('div');
|
|
1088
|
+
wrap.style.cssText = `
|
|
1089
|
+
position:fixed; top:16px; left:50%; transform:translateX(-50%);
|
|
1090
|
+
background:rgba(0,0,0,0.9); color:#fff;
|
|
1091
|
+
padding:12px 20px; border-radius:12px;
|
|
1092
|
+
box-shadow:0 10px 30px rgba(0,0,0,0.3);
|
|
1093
|
+
font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
|
1094
|
+
font-size:14px; z-index:2147483647;
|
|
1095
|
+
`;
|
|
1096
|
+
wrap.textContent = 'You must stay on this page while recording.';
|
|
1097
|
+
document.body.appendChild(wrap);
|
|
1098
|
+
this.navWarningEl = wrap;
|
|
1099
|
+
}
|
|
1100
|
+
if (this.navWarningTimer) {
|
|
1101
|
+
clearTimeout(this.navWarningTimer);
|
|
1102
|
+
}
|
|
1103
|
+
this.navWarningEl.style.opacity = '1';
|
|
1104
|
+
this.navWarningEl.style.transition = 'opacity 0.25s ease';
|
|
1105
|
+
this.navWarningTimer = setTimeout(() => {
|
|
1106
|
+
this.hideNavigationWarning();
|
|
1107
|
+
}, 3000);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
hideNavigationWarning(force = false) {
|
|
1111
|
+
if (this.navWarningTimer) {
|
|
1112
|
+
clearTimeout(this.navWarningTimer);
|
|
1113
|
+
this.navWarningTimer = 0;
|
|
1114
|
+
}
|
|
1115
|
+
if (!this.navWarningEl) return;
|
|
1116
|
+
if (force) {
|
|
1117
|
+
this.navWarningEl.remove();
|
|
1118
|
+
this.navWarningEl = null;
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
this.navWarningEl.style.opacity = '0';
|
|
1122
|
+
setTimeout(() => {
|
|
1123
|
+
if (this.navWarningEl && this.navWarningEl.parentNode) {
|
|
1124
|
+
this.navWarningEl.parentNode.removeChild(this.navWarningEl);
|
|
1125
|
+
}
|
|
1126
|
+
this.navWarningEl = null;
|
|
1127
|
+
}, 250);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
showCaptureSavedModal(kind) {
|
|
1131
|
+
const overlay = document.createElement('div');
|
|
1132
|
+
overlay.className = 'modal-overlay capture-modal-overlay';
|
|
1133
|
+
|
|
1134
|
+
const panel = document.createElement('div');
|
|
1135
|
+
panel.className = 'capture-modal';
|
|
1136
|
+
panel.setAttribute('role', 'dialog');
|
|
1137
|
+
panel.setAttribute('aria-modal', 'true');
|
|
1138
|
+
|
|
1139
|
+
const title = document.createElement('h3');
|
|
1140
|
+
title.className = 'capture-modal-title';
|
|
1141
|
+
title.id = `capture-modal-title-${Date.now().toString(36)}`;
|
|
1142
|
+
title.textContent = `${kind} saved`;
|
|
1143
|
+
|
|
1144
|
+
const description = document.createElement('p');
|
|
1145
|
+
description.className = 'capture-modal-description';
|
|
1146
|
+
description.id = `capture-modal-description-${Date.now().toString(36)}`;
|
|
1147
|
+
description.textContent = 'You can review this capture from the Screen Captures page.';
|
|
1148
|
+
|
|
1149
|
+
panel.setAttribute('aria-labelledby', title.id);
|
|
1150
|
+
panel.setAttribute('aria-describedby', description.id);
|
|
1151
|
+
|
|
1152
|
+
const actions = document.createElement('div');
|
|
1153
|
+
actions.className = 'capture-modal-actions';
|
|
1154
|
+
|
|
1155
|
+
const viewBtn = document.createElement('a');
|
|
1156
|
+
viewBtn.className = 'capture-modal-button primary';
|
|
1157
|
+
viewBtn.href = '/screenshots';
|
|
1158
|
+
viewBtn.textContent = 'Open screen captures';
|
|
1159
|
+
|
|
1160
|
+
const closeBtn = document.createElement('button');
|
|
1161
|
+
closeBtn.type = 'button';
|
|
1162
|
+
closeBtn.className = 'capture-modal-button secondary';
|
|
1163
|
+
closeBtn.textContent = 'Close';
|
|
1164
|
+
|
|
1165
|
+
actions.append(viewBtn, closeBtn);
|
|
1166
|
+
panel.append(title, description, actions);
|
|
1167
|
+
overlay.append(panel);
|
|
1168
|
+
document.body.appendChild(overlay);
|
|
1169
|
+
|
|
1170
|
+
const removeModal = (() => {
|
|
1171
|
+
let closing = false;
|
|
1172
|
+
const finalize = () => {
|
|
1173
|
+
overlay.removeEventListener('transitionend', handleTransitionEnd);
|
|
1174
|
+
if (overlay.parentNode) {
|
|
1175
|
+
overlay.parentNode.removeChild(overlay);
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
const handleTransitionEnd = (event) => {
|
|
1179
|
+
if (event.target === overlay && event.propertyName === 'opacity') {
|
|
1180
|
+
finalize();
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
return () => {
|
|
1184
|
+
if (closing) return;
|
|
1185
|
+
closing = true;
|
|
1186
|
+
overlay.classList.remove('is-visible');
|
|
1187
|
+
overlay.addEventListener('transitionend', handleTransitionEnd);
|
|
1188
|
+
setTimeout(finalize, 240);
|
|
1189
|
+
document.removeEventListener('keydown', onKeydownEscape, true);
|
|
1190
|
+
};
|
|
1191
|
+
})();
|
|
1192
|
+
|
|
1193
|
+
const onKeydownEscape = (event) => {
|
|
1194
|
+
if (event.key === 'Escape') {
|
|
1195
|
+
event.preventDefault();
|
|
1196
|
+
removeModal();
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
closeBtn.addEventListener('click', removeModal);
|
|
1201
|
+
viewBtn.addEventListener('click', () => {
|
|
1202
|
+
removeModal();
|
|
1203
|
+
});
|
|
1204
|
+
overlay.addEventListener('click', (event) => {
|
|
1205
|
+
if (event.target === overlay) removeModal();
|
|
1206
|
+
});
|
|
1207
|
+
document.addEventListener('keydown', onKeydownEscape, true);
|
|
1208
|
+
|
|
1209
|
+
requestAnimationFrame(() => {
|
|
1210
|
+
overlay.classList.add('is-visible');
|
|
1211
|
+
requestAnimationFrame(() => {
|
|
1212
|
+
viewBtn.focus();
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function screenshot(opts = {}) {
|
|
1219
|
+
const modal = new ScreenCaptureModal(null, opts);
|
|
1220
|
+
try {
|
|
1221
|
+
await modal.open();
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
if (err && err.name !== 'AbortError') {
|
|
1224
|
+
console.error('Capture canceled', err);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
269
1227
|
}
|
|
270
1228
|
|
|
271
1229
|
const open_url2 = (href, target, features) => {
|
|
@@ -304,11 +1262,13 @@ hotkeys("ctrl+t,cmd+t,ctrl+n,cmd+n", (e) => {
|
|
|
304
1262
|
// window.open("/", "_blank", "self")
|
|
305
1263
|
})
|
|
306
1264
|
const refreshParent = (e) => {
|
|
307
|
-
if (window.parent === window.top) {
|
|
1265
|
+
// if (window.parent === window.top) {
|
|
308
1266
|
window.parent.postMessage(e, "*")
|
|
309
|
-
}
|
|
1267
|
+
// }
|
|
310
1268
|
}
|
|
311
1269
|
let tippyInstances = [];
|
|
1270
|
+
const COMPACT_LAYOUT_QUERY = '(max-width: 768px)';
|
|
1271
|
+
const compactLayoutMedia = window.matchMedia(COMPACT_LAYOUT_QUERY);
|
|
312
1272
|
|
|
313
1273
|
function initTippy() {
|
|
314
1274
|
try {
|
|
@@ -323,13 +1283,11 @@ function initTippy() {
|
|
|
323
1283
|
}
|
|
324
1284
|
|
|
325
1285
|
function updateTippyPlacement(instance) {
|
|
326
|
-
|
|
327
|
-
const isMinimized = document.body.classList.contains('minimized');
|
|
1286
|
+
const isCompact = compactLayoutMedia.matches;
|
|
328
1287
|
const isHeaderElement = instance.reference.closest('header.navheader');
|
|
329
1288
|
const isSidebarTab = instance.reference.closest('aside') && instance.reference.classList.contains('tab');
|
|
330
1289
|
|
|
331
|
-
|
|
332
|
-
if (isMinimized) {
|
|
1290
|
+
if (isCompact) {
|
|
333
1291
|
instance.setProps({ placement: 'right' });
|
|
334
1292
|
} else if (isSidebarTab) {
|
|
335
1293
|
instance.setProps({ placement: 'left' });
|
|
@@ -354,19 +1312,101 @@ function setTabTooltips() {
|
|
|
354
1312
|
}
|
|
355
1313
|
|
|
356
1314
|
document.addEventListener("DOMContentLoaded", () => {
|
|
357
|
-
|
|
358
|
-
|
|
1315
|
+
if (typeof initUrlDropdown === 'function' && !window.PinokioUrlDropdown) {
|
|
1316
|
+
try {
|
|
1317
|
+
initUrlDropdown();
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
console.error('Failed to initialize URL dropdown', error);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
let urlDropdownLoader = null;
|
|
1324
|
+
let urlDropdownStyleLoader = null;
|
|
359
1325
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1326
|
+
const ensureUrlDropdownStyles = () => {
|
|
1327
|
+
if (document.querySelector('link[href="/urldropdown.css"]')) {
|
|
1328
|
+
return Promise.resolve();
|
|
1329
|
+
}
|
|
1330
|
+
if (!urlDropdownStyleLoader) {
|
|
1331
|
+
urlDropdownStyleLoader = new Promise((resolve, reject) => {
|
|
1332
|
+
const link = document.createElement('link');
|
|
1333
|
+
link.rel = 'stylesheet';
|
|
1334
|
+
link.href = '/urldropdown.css';
|
|
1335
|
+
link.addEventListener('load', () => resolve(), { once: true });
|
|
1336
|
+
link.addEventListener('error', reject, { once: true });
|
|
1337
|
+
document.head.appendChild(link);
|
|
1338
|
+
}).catch((error) => {
|
|
1339
|
+
console.error('Failed to load URL dropdown styles', error);
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
return urlDropdownStyleLoader || Promise.resolve();
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
const ensureUrlDropdown = async () => {
|
|
1346
|
+
if (window.PinokioUrlDropdown && typeof window.PinokioUrlDropdown.openSplitModal === 'function') {
|
|
1347
|
+
await ensureUrlDropdownStyles();
|
|
1348
|
+
return window.PinokioUrlDropdown;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (typeof initUrlDropdown === 'function') {
|
|
1352
|
+
await ensureUrlDropdownStyles();
|
|
1353
|
+
const api = initUrlDropdown();
|
|
1354
|
+
if (api && typeof api.openSplitModal === 'function') {
|
|
1355
|
+
return api;
|
|
1356
|
+
}
|
|
1357
|
+
if (window.PinokioUrlDropdown && typeof window.PinokioUrlDropdown.openSplitModal === 'function') {
|
|
1358
|
+
return window.PinokioUrlDropdown;
|
|
367
1359
|
}
|
|
368
1360
|
}
|
|
369
|
-
|
|
1361
|
+
|
|
1362
|
+
if (!urlDropdownLoader) {
|
|
1363
|
+
urlDropdownLoader = new Promise((resolve, reject) => {
|
|
1364
|
+
const existing = document.querySelector('script[src="/urldropdown.js"]');
|
|
1365
|
+
if (existing) {
|
|
1366
|
+
const waitForLoad = () => ensureUrlDropdownStyles().then(resolve);
|
|
1367
|
+
if (existing.dataset.pinokioLoaded === 'true') {
|
|
1368
|
+
waitForLoad();
|
|
1369
|
+
} else {
|
|
1370
|
+
existing.addEventListener('load', waitForLoad, { once: true });
|
|
1371
|
+
existing.addEventListener('error', reject, { once: true });
|
|
1372
|
+
}
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
ensureUrlDropdownStyles().finally(() => {
|
|
1377
|
+
const script = document.createElement('script');
|
|
1378
|
+
script.src = '/urldropdown.js';
|
|
1379
|
+
script.async = false;
|
|
1380
|
+
script.addEventListener('load', () => {
|
|
1381
|
+
script.dataset.pinokioLoaded = 'true';
|
|
1382
|
+
resolve();
|
|
1383
|
+
}, { once: true });
|
|
1384
|
+
script.addEventListener('error', reject, { once: true });
|
|
1385
|
+
document.head.appendChild(script);
|
|
1386
|
+
});
|
|
1387
|
+
}).then(() => {
|
|
1388
|
+
if (typeof initUrlDropdown === 'function') {
|
|
1389
|
+
return initUrlDropdown();
|
|
1390
|
+
}
|
|
1391
|
+
return null;
|
|
1392
|
+
}).catch((error) => {
|
|
1393
|
+
console.error('Failed to load URL dropdown script', error);
|
|
1394
|
+
return null;
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const api = await urlDropdownLoader;
|
|
1399
|
+
if (api && typeof api.openSplitModal === 'function') {
|
|
1400
|
+
return api;
|
|
1401
|
+
}
|
|
1402
|
+
if (window.PinokioUrlDropdown && typeof window.PinokioUrlDropdown.openSplitModal === 'function') {
|
|
1403
|
+
return window.PinokioUrlDropdown;
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
setTabTooltips();
|
|
1409
|
+
initTippy();
|
|
370
1410
|
|
|
371
1411
|
if (window !== window.top) {
|
|
372
1412
|
document.body.removeAttribute("data-agent")
|
|
@@ -374,8 +1414,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
374
1414
|
|
|
375
1415
|
// Listen for window resize
|
|
376
1416
|
window.addEventListener('resize', updateAllTooltips);
|
|
1417
|
+
if (typeof compactLayoutMedia.addEventListener === 'function') {
|
|
1418
|
+
compactLayoutMedia.addEventListener('change', updateAllTooltips);
|
|
1419
|
+
} else if (typeof compactLayoutMedia.addListener === 'function') {
|
|
1420
|
+
compactLayoutMedia.addListener(updateAllTooltips);
|
|
1421
|
+
}
|
|
377
1422
|
|
|
378
|
-
// Listen for body class changes
|
|
1423
|
+
// Listen for body class changes to refresh tooltip placement
|
|
379
1424
|
const observer = new MutationObserver((mutations) => {
|
|
380
1425
|
mutations.forEach((mutation) => {
|
|
381
1426
|
if (mutation.attributeName === 'class' && mutation.target === document.body) {
|
|
@@ -422,6 +1467,80 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
422
1467
|
})
|
|
423
1468
|
}
|
|
424
1469
|
|
|
1470
|
+
const handleSplitNavigation = async (anchor) => {
|
|
1471
|
+
const href = anchor.getAttribute('href') || '/columns';
|
|
1472
|
+
const originUrl = window.location.href;
|
|
1473
|
+
const modalTitle = href === '/rows' ? 'Split Into Rows' : 'Split Into Columns';
|
|
1474
|
+
|
|
1475
|
+
const api = await ensureUrlDropdown();
|
|
1476
|
+
if (!api) {
|
|
1477
|
+
window.location.href = href;
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
let selectedUrl = null;
|
|
1482
|
+
try {
|
|
1483
|
+
selectedUrl = await api.openSplitModal({
|
|
1484
|
+
title: modalTitle,
|
|
1485
|
+
description: 'Choose a running process or use the current tab URL for the new pane.',
|
|
1486
|
+
confirmLabel: 'Split',
|
|
1487
|
+
includeCurrent: true
|
|
1488
|
+
});
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
console.error('Process picker failed', error);
|
|
1491
|
+
selectedUrl = null;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
if (!selectedUrl) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const layoutApi = window.parent && window.parent.PinokioLayout;
|
|
1499
|
+
const frameId = window.frameElement?.dataset?.nodeId || window.name || null;
|
|
1500
|
+
|
|
1501
|
+
if (layoutApi && typeof layoutApi.split === 'function' && frameId) {
|
|
1502
|
+
try {
|
|
1503
|
+
const ok = layoutApi.split({
|
|
1504
|
+
frameId,
|
|
1505
|
+
direction: href === '/rows' ? 'rows' : 'columns',
|
|
1506
|
+
targetUrl: selectedUrl,
|
|
1507
|
+
});
|
|
1508
|
+
if (ok) {
|
|
1509
|
+
layoutApi.ensureSession?.();
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
console.warn('Pinokio layout split failed, falling back to navigation.', error);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
try {
|
|
1518
|
+
const target = new URL(href, window.location.origin);
|
|
1519
|
+
target.searchParams.set('origin', originUrl);
|
|
1520
|
+
target.searchParams.set('target', selectedUrl);
|
|
1521
|
+
window.location.href = target.toString();
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
console.error('Failed to navigate with selected split URL', error);
|
|
1524
|
+
window.location.href = href;
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
document.addEventListener('click', (event) => {
|
|
1529
|
+
if (event.defaultPrevented) return;
|
|
1530
|
+
if (event.button !== 0) return;
|
|
1531
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
1532
|
+
|
|
1533
|
+
const anchor = event.target.closest('a[href="/columns"], a[href="/rows"]');
|
|
1534
|
+
if (!anchor) return;
|
|
1535
|
+
if (anchor.dataset.pinokioSplit === 'skip') return;
|
|
1536
|
+
|
|
1537
|
+
event.preventDefault();
|
|
1538
|
+
event.stopPropagation();
|
|
1539
|
+
handleSplitNavigation(anchor).catch((error) => {
|
|
1540
|
+
console.error('Split navigation failed', error);
|
|
1541
|
+
});
|
|
1542
|
+
}, true);
|
|
1543
|
+
|
|
425
1544
|
const dropdown = document.querySelector('.dropdown');
|
|
426
1545
|
if (dropdown) {
|
|
427
1546
|
const dropdownBtn = document.getElementById('window-management');
|
|
@@ -470,9 +1589,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
470
1589
|
});
|
|
471
1590
|
|
|
472
1591
|
function openDropdown() {
|
|
473
|
-
const
|
|
1592
|
+
const isCompact = compactLayoutMedia.matches;
|
|
474
1593
|
|
|
475
|
-
if (
|
|
1594
|
+
if (isCompact) {
|
|
476
1595
|
// Create a portal container for centered positioning
|
|
477
1596
|
let portal = document.getElementById('dropdown-portal');
|
|
478
1597
|
if (!portal) {
|
|
@@ -504,9 +1623,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
504
1623
|
}
|
|
505
1624
|
|
|
506
1625
|
function closeDropdown() {
|
|
507
|
-
const
|
|
1626
|
+
const isCompact = compactLayoutMedia.matches;
|
|
508
1627
|
|
|
509
|
-
if (
|
|
1628
|
+
if (isCompact) {
|
|
510
1629
|
// Move dropdown back to original container
|
|
511
1630
|
const portal = document.getElementById('dropdown-portal');
|
|
512
1631
|
if (portal && dropdownContent.parentElement === portal) {
|
|
@@ -539,27 +1658,66 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
539
1658
|
})
|
|
540
1659
|
})
|
|
541
1660
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1661
|
+
const closeWindowButton = document.querySelector("#close-window");
|
|
1662
|
+
if (closeWindowButton) {
|
|
1663
|
+
const isInIframe = (() => {
|
|
1664
|
+
try {
|
|
1665
|
+
return window.self !== window.top;
|
|
1666
|
+
} catch (_) {
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
})();
|
|
1670
|
+
|
|
1671
|
+
const setCloseWindowVisibility = (shouldShow) => {
|
|
1672
|
+
if (shouldShow) {
|
|
1673
|
+
closeWindowButton.classList.remove("hidden");
|
|
548
1674
|
} else {
|
|
549
|
-
|
|
1675
|
+
closeWindowButton.classList.add("hidden");
|
|
550
1676
|
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1679
|
+
if (!isInIframe) {
|
|
1680
|
+
setCloseWindowVisibility(false);
|
|
1681
|
+
} else {
|
|
1682
|
+
setCloseWindowVisibility(false);
|
|
1683
|
+
const parentOrigin = (() => {
|
|
1684
|
+
try {
|
|
1685
|
+
return window.parent.location.origin || "*";
|
|
1686
|
+
} catch (_) {
|
|
1687
|
+
return "*";
|
|
1688
|
+
}
|
|
1689
|
+
})();
|
|
1690
|
+
|
|
1691
|
+
const onLayoutStateMessage = (event) => {
|
|
1692
|
+
if (!event || !event.data || typeof event.data !== "object") {
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (event.source !== window.parent) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (event.data.e === "layout-state") {
|
|
1699
|
+
setCloseWindowVisibility(Boolean(event.data.closable));
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
window.addEventListener("message", onLayoutStateMessage);
|
|
1704
|
+
|
|
1705
|
+
closeWindowButton.addEventListener("click", () => {
|
|
558
1706
|
window.parent.postMessage({
|
|
559
1707
|
e: "close"
|
|
560
|
-
}, "*")
|
|
1708
|
+
}, "*");
|
|
561
1709
|
// open_url2(location.href, "_blank")
|
|
562
|
-
})
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
try {
|
|
1713
|
+
window.parent.postMessage({
|
|
1714
|
+
e: "layout-state-request"
|
|
1715
|
+
}, parentOrigin);
|
|
1716
|
+
} catch (_) {
|
|
1717
|
+
window.parent.postMessage({
|
|
1718
|
+
e: "layout-state-request"
|
|
1719
|
+
}, "*");
|
|
1720
|
+
}
|
|
563
1721
|
}
|
|
564
1722
|
}
|
|
565
1723
|
if (document.querySelector("#create-new-folder")) {
|
|
@@ -662,11 +1820,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
662
1820
|
}
|
|
663
1821
|
|
|
664
1822
|
const overlay = document.createElement('div');
|
|
665
|
-
overlay.className = 'create-launcher-modal-overlay';
|
|
666
|
-
overlay.style.display = 'none';
|
|
1823
|
+
overlay.className = 'modal-overlay create-launcher-modal-overlay';
|
|
667
1824
|
|
|
668
1825
|
const modal = document.createElement('div');
|
|
669
1826
|
modal.className = 'create-launcher-modal';
|
|
1827
|
+
modal.setAttribute('role', 'dialog');
|
|
1828
|
+
modal.setAttribute('aria-modal', 'true');
|
|
670
1829
|
|
|
671
1830
|
const header = document.createElement('div');
|
|
672
1831
|
header.className = 'create-launcher-modal-header';
|
|
@@ -683,12 +1842,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
683
1842
|
headingStack.className = 'create-launcher-modal-headings';
|
|
684
1843
|
|
|
685
1844
|
const title = document.createElement('h3');
|
|
1845
|
+
title.id = 'create-launcher-modal-title';
|
|
686
1846
|
title.textContent = 'Create';
|
|
687
1847
|
|
|
688
1848
|
const description = document.createElement('p');
|
|
689
1849
|
description.className = 'create-launcher-modal-description';
|
|
1850
|
+
description.id = 'create-launcher-modal-description';
|
|
690
1851
|
description.textContent = 'Create a reusable and shareable launcher for any task or any app'
|
|
691
1852
|
|
|
1853
|
+
modal.setAttribute('aria-labelledby', title.id);
|
|
1854
|
+
modal.setAttribute('aria-describedby', description.id);
|
|
1855
|
+
|
|
692
1856
|
headingStack.appendChild(title);
|
|
693
1857
|
headingStack.appendChild(description);
|
|
694
1858
|
header.appendChild(iconWrapper);
|
|
@@ -700,7 +1864,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
700
1864
|
|
|
701
1865
|
const promptTextarea = document.createElement('textarea');
|
|
702
1866
|
promptTextarea.className = 'create-launcher-modal-textarea';
|
|
703
|
-
promptTextarea.placeholder = 'Examples: "a 1-click launcher for ComfyUI", "I want to change file format", "I want to clone a website to run locally", etc.';
|
|
1867
|
+
promptTextarea.placeholder = 'Examples: "a 1-click launcher for ComfyUI", "I want to change file format", "I want to clone a website to run locally", etc. (Leave empty to decide later)';
|
|
704
1868
|
promptLabel.appendChild(promptTextarea);
|
|
705
1869
|
|
|
706
1870
|
const templateWrapper = document.createElement('div');
|
|
@@ -941,7 +2105,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
941
2105
|
return createLauncherModalInstance;
|
|
942
2106
|
}
|
|
943
2107
|
|
|
944
|
-
function showCreateLauncherModal(defaults = {}) {
|
|
2108
|
+
async function showCreateLauncherModal(defaults = {}) {
|
|
2109
|
+
|
|
2110
|
+
let response = await fetch("/bundle/dev").then((res) => {
|
|
2111
|
+
return res.json()
|
|
2112
|
+
})
|
|
2113
|
+
if (response.available) {
|
|
2114
|
+
} else {
|
|
2115
|
+
location.href = "/setup/dev?callback=/"
|
|
2116
|
+
return
|
|
2117
|
+
}
|
|
2118
|
+
|
|
945
2119
|
const modal = ensureCreateLauncherModal();
|
|
946
2120
|
|
|
947
2121
|
modal.error.textContent = '';
|
|
@@ -968,12 +2142,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
968
2142
|
|
|
969
2143
|
modal.syncTemplateFields(modal.promptTextarea.value, defaults.templateValues || {});
|
|
970
2144
|
|
|
971
|
-
modal.overlay.style.display = 'flex';
|
|
972
|
-
|
|
973
2145
|
requestAnimationFrame(() => {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
2146
|
+
modal.overlay.classList.add('is-visible');
|
|
2147
|
+
requestAnimationFrame(() => {
|
|
2148
|
+
modal.folderInput.select();
|
|
2149
|
+
modal.promptTextarea.focus();
|
|
2150
|
+
});
|
|
977
2151
|
});
|
|
978
2152
|
|
|
979
2153
|
createLauncherKeydownHandler = (event) => {
|
|
@@ -991,7 +2165,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
991
2165
|
|
|
992
2166
|
function hideCreateLauncherModal() {
|
|
993
2167
|
if (!createLauncherModalInstance) return;
|
|
994
|
-
createLauncherModalInstance.overlay.
|
|
2168
|
+
createLauncherModalInstance.overlay.classList.remove('is-visible');
|
|
995
2169
|
if (createLauncherKeydownHandler) {
|
|
996
2170
|
document.removeEventListener('keydown', createLauncherKeydownHandler, true);
|
|
997
2171
|
createLauncherKeydownHandler = null;
|
|
@@ -1008,14 +2182,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1008
2182
|
const selectedTool = modal.toolEntries.find((entry) => entry.input.checked)?.input.value || 'claude';
|
|
1009
2183
|
|
|
1010
2184
|
if (!folderName) {
|
|
1011
|
-
debugger
|
|
1012
2185
|
modal.error.textContent = 'Please enter a folder name.';
|
|
1013
2186
|
modal.folderInput.focus();
|
|
1014
2187
|
return;
|
|
1015
2188
|
}
|
|
1016
2189
|
|
|
1017
2190
|
if (folderName.includes(' ')) {
|
|
1018
|
-
debugger
|
|
1019
2191
|
modal.error.textContent = 'Folder names cannot contain spaces.';
|
|
1020
2192
|
modal.folderInput.focus();
|
|
1021
2193
|
return;
|
|
@@ -1046,13 +2218,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1046
2218
|
|
|
1047
2219
|
const prompt = finalPrompt.trim();
|
|
1048
2220
|
|
|
1049
|
-
if (!prompt) {
|
|
1050
|
-
debugger
|
|
1051
|
-
modal.error.textContent = 'Please enter a prompt.';
|
|
1052
|
-
modal.promptTextarea.focus();
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
2221
|
const url = `/pro?name=${encodeURIComponent(folderName)}&message=${encodeURIComponent(prompt)}&tool=${encodeURIComponent(selectedTool)}`;
|
|
1057
2222
|
hideCreateLauncherModal();
|
|
1058
2223
|
window.location.href = url;
|