tangled
alpha
login
or
join now
me.webbeef.org
/
browser.html
Rewild Your Web
web
browser
dweb
10
fork
atom
overview
issues
1
pulls
pipelines
system: improve mobile gestures
webbeef.tngl.sh
4 days ago
246ded04
52b759ec
+162
-112
5 changed files
expand all
collapse all
unified
split
resources
browserhtml
system
edge_gesture_handler.js
index.js
mobile.css
mobile_layout_manager.js
web_view.js
+96
-80
resources/browserhtml/system/edge_gesture_handler.js
···
6
7
// Configuration
8
this.edgeThreshold = 30; // px from edge to trigger edge swipe
9
-
this.swipeThreshold = 50; // px to complete a swipe action
10
-
this.longSwipeThreshold = 200; // px for long swipe (overview mode)
11
12
// Touch state
13
this.touchStartX = 0;
···
20
21
// Peek preview element
22
this.peekPreview = null;
0
0
0
23
24
// Edge overlay elements
25
this.edgeOverlays = {};
···
51
edges.forEach((edge) => {
52
const overlay = document.createElement("div");
53
overlay.className = `gesture-edge-overlay gesture-edge-${edge.name}`;
54
-
overlay.style.cssText = `
55
-
position: fixed;
56
-
${edge.style}
57
-
z-index: var(--z-gesture);
58
-
touch-action: none;
59
-
pointer-events: auto;
60
-
background: transparent; /* rgba(77, 215, 220, 0.49); */
61
-
`;
62
overlay.dataset.edge = edge.name;
63
document.body.appendChild(overlay);
64
this.edgeOverlays[edge.name] = overlay;
···
80
}
81
82
onEdgeTouchStart(event) {
83
-
console.log(`[EdgeGesture] touchstart`);
84
-
console.log("onEdgeTouchStart");
85
if (event.touches.length !== 1) {
86
return;
87
}
···
97
this.isEdgeSwipe = true;
98
this.edgeDirection = event.currentTarget.dataset.edge;
99
0
0
0
0
100
event.preventDefault();
101
}
102
103
onTouchMove(event) {
104
-
console.log(`[EdgeGesture] touchmove isEdgeSwipe=${this.isEdgeSwipe}`);
105
-
106
if (!this.isEdgeSwipe || event.touches.length !== 1) {
107
return;
108
}
···
131
}
132
133
onTouchEnd() {
134
-
console.log(`[EdgeGesture] touchend ${this.edgeDirection}`);
135
if (!this.isEdgeSwipe) {
136
return;
137
}
138
139
const deltaX = this.touchCurrentX - this.touchStartX;
140
-
const deltaY = this.touchCurrentY - this.touchStartY;
141
142
// Complete the gesture based on direction and distance
143
if (this.edgeDirection === "left") {
144
-
this.completeLeftEdgeSwipe(deltaX);
145
} else if (this.edgeDirection === "right") {
146
-
this.completeRightEdgeSwipe(-deltaX);
147
}
148
149
-
// Reset state
150
-
this.isEdgeSwipe = false;
151
-
this.edgeDirection = null;
152
-
this.isPeeking = false;
153
-
this.hidePeekPreview();
154
}
155
156
onTouchCancel() {
157
-
console.log(`[EdgeGesture] touchcancel`);
0
0
0
0
0
0
0
0
158
this.isEdgeSwipe = false;
159
this.edgeDirection = null;
160
this.isPeeking = false;
161
-
this.hidePeekPreview();
162
}
163
164
// ============================================================================
···
166
// ============================================================================
167
168
handleLeftEdgeMove(distance) {
169
-
const progress = Math.min(distance / this.swipeThreshold, 1);
170
-
console.log(
171
-
`[EdgeGesture] handleLeftEdgeMove distance=${distance} progress=${progress} isPeeking=${this.isPeeking}`,
172
-
);
173
-
174
-
// Show peek preview of previous webview
175
-
if (!this.isPeeking && progress > 0.1) {
176
-
this.isPeeking = true;
177
-
this.showPeekPreview("prev", progress);
178
-
} else if (this.isPeeking) {
179
-
this.updatePeekPreview(progress);
180
-
}
181
-
}
182
-
183
-
completeLeftEdgeSwipe(distance) {
184
-
if (distance >= this.longSwipeThreshold) {
185
-
// Long swipe: enter overview mode
186
-
this.lm.showOverview();
187
-
} else if (distance >= this.swipeThreshold) {
188
-
// Normal swipe: switch to previous webview
189
-
this.lm.prevWebView();
190
-
}
191
-
// Otherwise: cancelled, peek snaps back
192
}
193
194
// ============================================================================
···
196
// ============================================================================
197
198
handleRightEdgeMove(distance) {
199
-
const progress = Math.min(distance / this.swipeThreshold, 1);
0
200
201
-
// Show peek preview of next webview
202
-
if (!this.isPeeking && progress > 0.1) {
0
0
0
0
203
this.isPeeking = true;
204
-
this.showPeekPreview("next", progress);
205
-
} else if (this.isPeeking) {
206
-
this.updatePeekPreview(progress);
207
}
0
208
}
209
210
-
completeRightEdgeSwipe(distance) {
211
-
if (distance >= this.longSwipeThreshold) {
212
-
// Long swipe: enter overview mode
213
-
this.lm.showOverview();
214
-
} else if (distance >= this.swipeThreshold) {
215
-
// Normal swipe: switch to next webview
216
-
this.lm.nextWebView();
0
0
0
0
0
0
0
0
217
}
218
}
219
···
222
// ============================================================================
223
224
handleBottomEdgeMove(distance) {
225
-
console.log(`[EdgeGesture] handleBottomEdgeMove ${distance}px`);
226
if (distance >= this.edgeThreshold / 2) {
227
-
// Swipe up from bottom: show action bar
228
this.lm.showActionBar();
229
}
230
}
···
235
236
handleTopEdgeMove(distance) {
237
if (distance >= this.edgeThreshold / 2) {
238
-
// Swipe down from top: show notifications
239
-
// Dispatch event for notification panel
240
document.dispatchEvent(
241
new CustomEvent("mobile-show-notifications", { bubbles: true }),
242
);
···
247
// Peek Preview
248
// ============================================================================
249
250
-
showPeekPreview(direction, progress) {
251
-
console.log(`[EdgeGesture] showPeekPreview ${direction} ${progress}`);
252
const adjacentId = this.lm.getAdjacentWebViewId(direction);
253
if (!adjacentId) {
254
console.error(`[EdgeGesture] no adjacentId`);
···
289
screenshot.style.display = "none";
290
}
291
292
-
// Position based on direction
293
-
this.peekPreview.classList.remove("from-left", "from-right");
294
this.peekPreview.classList.add(
295
direction === "prev" ? "from-left" : "from-right",
296
);
297
this.peekPreview.classList.add("visible");
298
-
299
-
this.updatePeekPreview(progress);
300
}
301
302
-
updatePeekPreview(progress) {
303
-
console.log(`[EdgeGesture] updatePeekPreview ${progress}`);
304
if (!this.peekPreview) {
305
return;
306
}
307
308
-
// Animate the preview sliding in
309
-
const maxTranslate = 30; // percentage of screen width to reveal
310
-
const translatePercent = progress * maxTranslate;
0
0
0
0
0
0
311
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
312
if (this.peekPreview.classList.contains("from-left")) {
313
-
this.peekPreview.style.transform = `translateX(${-100 + translatePercent}%)`;
314
} else {
315
-
this.peekPreview.style.transform = `translateX(${100 - translatePercent}%)`;
316
}
317
}
318
319
hidePeekPreview() {
320
-
console.log(`[EdgeGesture] hidePeekPreview`);
321
if (this.peekPreview) {
322
-
this.peekPreview.classList.remove("visible");
323
this.peekPreview.style.transform = "";
324
}
325
}
···
6
7
// Configuration
8
this.edgeThreshold = 30; // px from edge to trigger edge swipe
9
+
this.commitThreshold = 0.4; // fraction of viewport to commit swipe
0
10
11
// Touch state
12
this.touchStartX = 0;
···
19
20
// Peek preview element
21
this.peekPreview = null;
22
+
23
+
// Active overlay element (expanded during gesture)
24
+
this.activeOverlay = null;
25
26
// Edge overlay elements
27
this.edgeOverlays = {};
···
53
edges.forEach((edge) => {
54
const overlay = document.createElement("div");
55
overlay.className = `gesture-edge-overlay gesture-edge-${edge.name}`;
56
+
overlay.style.cssText = edge.style;
0
0
0
0
0
0
0
57
overlay.dataset.edge = edge.name;
58
document.body.appendChild(overlay);
59
this.edgeOverlays[edge.name] = overlay;
···
75
}
76
77
onEdgeTouchStart(event) {
0
0
78
if (event.touches.length !== 1) {
79
return;
80
}
···
90
this.isEdgeSwipe = true;
91
this.edgeDirection = event.currentTarget.dataset.edge;
92
93
+
// Expand overlay to full screen so we keep receiving touch events
94
+
this.activeOverlay = event.currentTarget;
95
+
this.activeOverlay.classList.add("fullscreen-overlay");
96
+
97
event.preventDefault();
98
}
99
100
onTouchMove(event) {
0
0
101
if (!this.isEdgeSwipe || event.touches.length !== 1) {
102
return;
103
}
···
126
}
127
128
onTouchEnd() {
0
129
if (!this.isEdgeSwipe) {
130
return;
131
}
132
133
const deltaX = this.touchCurrentX - this.touchStartX;
0
134
135
// Complete the gesture based on direction and distance
136
if (this.edgeDirection === "left") {
137
+
this.completeEdgeSwipe(deltaX, "prev");
138
} else if (this.edgeDirection === "right") {
139
+
this.completeEdgeSwipe(-deltaX, "next");
140
}
141
142
+
this.resetGestureState();
0
0
0
0
143
}
144
145
onTouchCancel() {
146
+
this.resetGestureState();
147
+
this.animatePreviewOut();
148
+
}
149
+
150
+
resetGestureState() {
151
+
if (this.activeOverlay) {
152
+
this.activeOverlay.classList.remove("fullscreen-overlay");
153
+
this.activeOverlay = null;
154
+
}
155
this.isEdgeSwipe = false;
156
this.edgeDirection = null;
157
this.isPeeking = false;
0
158
}
159
160
// ============================================================================
···
162
// ============================================================================
163
164
handleLeftEdgeMove(distance) {
165
+
this.handleHorizontalEdgeMove(distance, "prev");
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
166
}
167
168
// ============================================================================
···
170
// ============================================================================
171
172
handleRightEdgeMove(distance) {
173
+
this.handleHorizontalEdgeMove(distance, "next");
174
+
}
175
176
+
// ============================================================================
177
+
// Shared horizontal edge logic
178
+
// ============================================================================
179
+
180
+
handleHorizontalEdgeMove(distance, direction) {
181
+
if (!this.isPeeking) {
182
this.isPeeking = true;
183
+
this.showPeekPreview(direction);
0
0
184
}
185
+
this.updatePeekPreview(distance);
186
}
187
188
+
completeEdgeSwipe(distance, direction) {
189
+
const threshold = window.innerWidth * this.commitThreshold;
190
+
if (distance >= threshold) {
191
+
// Animate preview to fully cover the viewport, then switch
192
+
this.animatePreviewIn(() => {
193
+
if (direction === "prev") {
194
+
this.lm.prevWebView();
195
+
} else {
196
+
this.lm.nextWebView();
197
+
}
198
+
this.hidePeekPreview();
199
+
});
200
+
} else {
201
+
// Animate preview back out
202
+
this.animatePreviewOut();
203
}
204
}
205
···
208
// ============================================================================
209
210
handleBottomEdgeMove(distance) {
0
211
if (distance >= this.edgeThreshold / 2) {
212
+
this.resetGestureState();
213
this.lm.showActionBar();
214
}
215
}
···
220
221
handleTopEdgeMove(distance) {
222
if (distance >= this.edgeThreshold / 2) {
223
+
this.resetGestureState();
0
224
document.dispatchEvent(
225
new CustomEvent("mobile-show-notifications", { bubbles: true }),
226
);
···
231
// Peek Preview
232
// ============================================================================
233
234
+
showPeekPreview(direction) {
0
235
const adjacentId = this.lm.getAdjacentWebViewId(direction);
236
if (!adjacentId) {
237
console.error(`[EdgeGesture] no adjacentId`);
···
272
screenshot.style.display = "none";
273
}
274
275
+
// Position based on direction, no transition during drag
276
+
this.peekPreview.classList.remove("from-left", "from-right", "animating");
277
this.peekPreview.classList.add(
278
direction === "prev" ? "from-left" : "from-right",
279
);
280
this.peekPreview.classList.add("visible");
0
0
281
}
282
283
+
updatePeekPreview(distance) {
0
284
if (!this.peekPreview) {
285
return;
286
}
287
288
+
// Directly track the finger: translateX in pixels
289
+
if (this.peekPreview.classList.contains("from-left")) {
290
+
const offset = -window.innerWidth + distance;
291
+
this.peekPreview.style.transform = `translateX(${Math.min(offset, 0)}px)`;
292
+
} else {
293
+
const offset = window.innerWidth - distance;
294
+
this.peekPreview.style.transform = `translateX(${Math.max(offset, 0)}px)`;
295
+
}
296
+
}
297
298
+
animatePreviewIn(onDone) {
299
+
if (!this.peekPreview) {
300
+
onDone();
301
+
return;
302
+
}
303
+
this.peekPreview.classList.add("animating");
304
+
let called = false;
305
+
const done = () => {
306
+
if (called) return;
307
+
called = true;
308
+
onDone();
309
+
};
310
+
this.peekPreview.addEventListener("transitionend", done, { once: true });
311
+
setTimeout(done, 300);
312
+
this.peekPreview.style.transform = "translateX(0)";
313
+
}
314
+
315
+
animatePreviewOut() {
316
+
if (!this.peekPreview) {
317
+
return;
318
+
}
319
+
this.peekPreview.classList.add("animating");
320
+
let called = false;
321
+
const done = () => {
322
+
if (called) return;
323
+
called = true;
324
+
this.hidePeekPreview();
325
+
};
326
+
this.peekPreview.addEventListener("transitionend", done, { once: true });
327
+
setTimeout(done, 300);
328
+
// Animate back off-screen
329
if (this.peekPreview.classList.contains("from-left")) {
330
+
this.peekPreview.style.transform = `translateX(-100%)`;
331
} else {
332
+
this.peekPreview.style.transform = `translateX(100%)`;
333
}
334
}
335
336
hidePeekPreview() {
0
337
if (this.peekPreview) {
338
+
this.peekPreview.classList.remove("visible", "animating");
339
this.peekPreview.style.transform = "";
340
}
341
}
-2
resources/browserhtml/system/index.js
···
590
591
// Override layout manager's showOverview to use mobile tab overview
592
layoutManager.showOverview = function () {
593
-
// Capture screenshots for all tabs before showing overview
594
-
layoutManager.captureAllScreenshots();
595
updateTabOverviewData();
596
mobileOverview.open = true;
597
};
···
590
591
// Override layout manager's showOverview to use mobile tab overview
592
layoutManager.showOverview = function () {
0
0
593
updateTabOverviewData();
594
mobileOverview.open = true;
595
};
+36
-7
resources/browserhtml/system/mobile.css
···
26
bottom: var(--keyboard-offset);
27
opacity: 0;
28
visibility: hidden;
29
-
transition: opacity 0.3s ease, visibility 0.3s ease, bottom 0.3s ease;
0
0
0
30
z-index: var(--z-base);
31
}
32
···
72
.mobile-peek-preview {
73
position: fixed;
74
top: 0;
75
-
width: 30%;
76
height: 100%;
77
background: var(--bg-menu);
78
z-index: var(--z-gesture);
···
81
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
82
opacity: 0;
83
visibility: hidden;
84
-
transition: opacity 0.2s ease, visibility 0.2s ease;
85
}
86
87
.mobile-peek-preview.visible {
···
89
visibility: visible;
90
}
91
0
0
0
0
0
92
.mobile-peek-preview.from-left {
93
left: 0;
94
transform: translateX(-100%);
95
-
border-right: 2px solid var(--color-focus-ring);
96
}
97
98
.mobile-peek-preview.from-right {
99
right: 0;
100
left: auto;
101
transform: translateX(100%);
102
-
border-left: 2px solid var(--color-focus-ring);
103
}
104
105
.peek-header {
···
155
z-index: var(--z-gesture);
156
opacity: 0;
157
visibility: hidden;
158
-
transition: opacity 0.2s ease, visibility 0.2s ease;
0
0
159
}
160
161
.mobile-tab-indicator.visible {
···
168
height: 8px;
169
border-radius: 50%;
170
background: rgba(255, 255, 255, 0.3);
171
-
transition: background 0.2s ease, transform 0.2s ease;
0
0
172
}
173
174
.tab-dot.active {
···
280
height: 4px;
281
background: linear-gradient(to bottom, var(--color-focus-ring), transparent);
282
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
26
bottom: var(--keyboard-offset);
27
opacity: 0;
28
visibility: hidden;
29
+
transition:
30
+
opacity 0.3s ease,
31
+
visibility 0.3s ease,
32
+
bottom 0.3s ease;
33
z-index: var(--z-base);
34
}
35
···
75
.mobile-peek-preview {
76
position: fixed;
77
top: 0;
78
+
width: 100%;
79
height: 100%;
80
background: var(--bg-menu);
81
z-index: var(--z-gesture);
···
84
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
85
opacity: 0;
86
visibility: hidden;
0
87
}
88
89
.mobile-peek-preview.visible {
···
91
visibility: visible;
92
}
93
94
+
/* Animate transform on release (commit or cancel) */
95
+
.mobile-peek-preview.animating {
96
+
transition: transform var(--transition-fast);
97
+
}
98
+
99
.mobile-peek-preview.from-left {
100
left: 0;
101
transform: translateX(-100%);
0
102
}
103
104
.mobile-peek-preview.from-right {
105
right: 0;
106
left: auto;
107
transform: translateX(100%);
0
108
}
109
110
.peek-header {
···
160
z-index: var(--z-gesture);
161
opacity: 0;
162
visibility: hidden;
163
+
transition:
164
+
opacity 0.2s ease,
165
+
visibility 0.2s ease;
166
}
167
168
.mobile-tab-indicator.visible {
···
175
height: 8px;
176
border-radius: 50%;
177
background: rgba(255, 255, 255, 0.3);
178
+
transition:
179
+
background 0.2s ease,
180
+
transform 0.2s ease;
181
}
182
183
.tab-dot.active {
···
289
height: 4px;
290
background: linear-gradient(to bottom, var(--color-focus-ring), transparent);
291
}
292
+
293
+
.gesture-edge-overlay {
294
+
position: fixed;
295
+
z-index: calc(var(--z-gesture) + 1);
296
+
touch-action: none;
297
+
pointer-events: auto;
298
+
background: transparent;
299
+
/* rgba(77, 215, 220, 0.49); */
300
+
}
301
+
302
+
/* Expand edge overlay to full screen during active gesture,
303
+
above the peek preview so we keep receiving touch events */
304
+
.gesture-edge-overlay.fullscreen-overlay {
305
+
left: 0 !important;
306
+
top: 0 !important;
307
+
right: auto !important;
308
+
bottom: auto !important;
309
+
width: 100% !important;
310
+
height: 100% !important;
311
+
}
+17
-19
resources/browserhtml/system/mobile_layout_manager.js
···
114
115
// Remove from DOM
116
const container = this.root.querySelector(
117
-
`.mobile-webview-container[data-webview-id="${webviewId}"]`
118
);
119
if (container) {
120
container.remove();
···
153
prevEntry.webview.active = false;
154
155
// Capture screenshot of the tab we're switching away from
156
-
if (prevEntry.webview.captureScreenshot) {
157
-
prevEntry.webview.captureScreenshot();
158
-
}
159
160
const prevContainer = this.root.querySelector(
161
-
`.mobile-webview-container[data-webview-id="${this.activeWebviewId}"]`
162
);
163
if (prevContainer) {
164
prevContainer.classList.remove("active");
···
173
entry.webview.active = true;
174
this.activeIndex = entry.index;
175
const container = this.root.querySelector(
176
-
`.mobile-webview-container[data-webview-id="${webviewId}"]`
177
);
178
if (container) {
179
container.classList.add("active");
···
192
193
// Navigate to next webview in carousel (skips homescreen)
194
nextWebView() {
195
-
const regularViews = this.webviewOrder.filter((id) => !this.isHomescreen(id));
0
0
196
if (regularViews.length <= 1) {
197
return;
198
}
···
209
210
// Navigate to previous webview in carousel (skips homescreen)
211
prevWebView() {
212
-
const regularViews = this.webviewOrder.filter((id) => !this.isHomescreen(id));
0
0
213
if (regularViews.length <= 1) {
214
return;
215
}
···
227
228
// Get adjacent webview ID for peek preview (skips homescreen)
229
getAdjacentWebViewId(direction) {
230
-
const regularViews = this.webviewOrder.filter((id) => !this.isHomescreen(id));
0
0
231
if (regularViews.length <= 1) {
232
return null;
233
}
···
364
});
365
}
366
367
-
// Capture screenshots for all webviews (for tab overview)
368
-
captureAllScreenshots() {
369
-
for (const [id, entry] of this.webviews) {
370
-
if (entry.webview.captureScreenshot) {
371
-
entry.webview.captureScreenshot();
372
-
}
373
-
}
374
-
}
375
-
376
// ============================================================================
377
// Overview Mode (for compatibility, minimal implementation for MVP)
378
// ============================================================================
···
388
showOverview() {
389
this.overviewMode = true;
390
// TODO: Implement mobile tab overview grid
391
-
console.log("[MobileLayoutManager] Overview mode enabled (not yet implemented)");
0
0
392
}
393
394
hideOverview() {
···
114
115
// Remove from DOM
116
const container = this.root.querySelector(
117
+
`.mobile-webview-container[data-webview-id="${webviewId}"]`,
118
);
119
if (container) {
120
container.remove();
···
153
prevEntry.webview.active = false;
154
155
// Capture screenshot of the tab we're switching away from
156
+
// TODO: fix screenshot capture when not fully visible.
157
+
// prevEntry.webview.captureScreenshot(true);
0
158
159
const prevContainer = this.root.querySelector(
160
+
`.mobile-webview-container[data-webview-id="${this.activeWebviewId}"]`,
161
);
162
if (prevContainer) {
163
prevContainer.classList.remove("active");
···
172
entry.webview.active = true;
173
this.activeIndex = entry.index;
174
const container = this.root.querySelector(
175
+
`.mobile-webview-container[data-webview-id="${webviewId}"]`,
176
);
177
if (container) {
178
container.classList.add("active");
···
191
192
// Navigate to next webview in carousel (skips homescreen)
193
nextWebView() {
194
+
const regularViews = this.webviewOrder.filter(
195
+
(id) => !this.isHomescreen(id),
196
+
);
197
if (regularViews.length <= 1) {
198
return;
199
}
···
210
211
// Navigate to previous webview in carousel (skips homescreen)
212
prevWebView() {
213
+
const regularViews = this.webviewOrder.filter(
214
+
(id) => !this.isHomescreen(id),
215
+
);
216
if (regularViews.length <= 1) {
217
return;
218
}
···
230
231
// Get adjacent webview ID for peek preview (skips homescreen)
232
getAdjacentWebViewId(direction) {
233
+
const regularViews = this.webviewOrder.filter(
234
+
(id) => !this.isHomescreen(id),
235
+
);
236
if (regularViews.length <= 1) {
237
return null;
238
}
···
369
});
370
}
371
0
0
0
0
0
0
0
0
0
372
// ============================================================================
373
// Overview Mode (for compatibility, minimal implementation for MVP)
374
// ============================================================================
···
384
showOverview() {
385
this.overviewMode = true;
386
// TODO: Implement mobile tab overview grid
387
+
console.log(
388
+
"[MobileLayoutManager] Overview mode enabled (not yet implemented)",
389
+
);
390
}
391
392
hideOverview() {
+13
-4
resources/browserhtml/system/web_view.js
···
791
canGoForward: this.canGoForward,
792
url: this.currentUrl,
793
},
794
-
})
795
);
796
797
// Capture screenshot for overview mode after a short delay
···
800
}
801
802
// Capture a screenshot and cache it for overview mode
803
-
captureScreenshot() {
804
// Debounce: clear any pending capture
805
if (this.screenshotTimeout) {
806
clearTimeout(this.screenshotTimeout);
807
}
808
809
-
// Delay capture to allow page to render
810
-
this.screenshotTimeout = setTimeout(() => {
811
this.getContentIframe()
812
.takeScreenshot()
813
.then((blob) => {
···
822
.catch(() => {
823
// Ignore screenshot errors (e.g., for off-screen webviews)
824
});
0
0
0
0
0
0
0
0
0
0
825
}, 500);
826
}
827
···
791
canGoForward: this.canGoForward,
792
url: this.currentUrl,
793
},
794
+
}),
795
);
796
797
// Capture screenshot for overview mode after a short delay
···
800
}
801
802
// Capture a screenshot and cache it for overview mode
803
+
captureScreenshot(immediate = false) {
804
// Debounce: clear any pending capture
805
if (this.screenshotTimeout) {
806
clearTimeout(this.screenshotTimeout);
807
}
808
809
+
let doCapture = () => {
0
810
this.getContentIframe()
811
.takeScreenshot()
812
.then((blob) => {
···
821
.catch(() => {
822
// Ignore screenshot errors (e.g., for off-screen webviews)
823
});
824
+
};
825
+
826
+
if (immediate) {
827
+
doCapture();
828
+
return;
829
+
}
830
+
831
+
// Delay capture to allow page to render
832
+
this.screenshotTimeout = setTimeout(() => {
833
+
doCapture();
834
}, 500);
835
}
836