Rewild Your Web
web browser dweb
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 508943f815334a02aefc47d9da365aba115ab0f5 326 lines 10 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3export class EdgeGestureHandler { 4 constructor(layoutManager) { 5 this.lm = layoutManager; 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; 14 this.touchStartY = 0; 15 this.touchCurrentX = 0; 16 this.touchCurrentY = 0; 17 this.isEdgeSwipe = false; 18 this.edgeDirection = null; // 'left', 'right', 'bottom', 'top' 19 this.isPeeking = false; 20 21 // Peek preview element 22 this.peekPreview = null; 23 24 // Edge overlay elements 25 this.edgeOverlays = {}; 26 27 this.createEdgeOverlays(); 28 } 29 30 createEdgeOverlays() { 31 // Create thin overlay strips along each edge 32 const edges = [ 33 { 34 name: "left", 35 style: `left: 0; top: 0; width: ${this.edgeThreshold}px; height: 100%;`, 36 }, 37 { 38 name: "right", 39 style: `right: 0; top: 0; width: ${this.edgeThreshold}px; height: 100%;`, 40 }, 41 { 42 name: "top", 43 style: `top: 0; left: 0; width: 100%; height: ${this.edgeThreshold}px;`, 44 }, 45 { 46 name: "bottom", 47 style: `bottom: 0; left: 0; width: 100%; height: ${this.edgeThreshold}px;`, 48 }, 49 ]; 50 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; 65 66 // Add listeners to each edge overlay 67 overlay.addEventListener("touchstart", this.onEdgeTouchStart.bind(this), { 68 capture: false, 69 }); 70 overlay.addEventListener("touchmove", this.onTouchMove.bind(this), { 71 capture: false, 72 }); 73 overlay.addEventListener("touchend", this.onTouchEnd.bind(this), { 74 capture: false, 75 }); 76 overlay.addEventListener("touchcancel", this.onTouchCancel.bind(this), { 77 capture: false, 78 }); 79 }); 80 } 81 82 onEdgeTouchStart(event) { 83 console.log(`[EdgeGesture] touchstart`); 84 console.log("onEdgeTouchStart"); 85 if (event.touches.length !== 1) { 86 return; 87 } 88 89 const touch = event.touches[0]; 90 91 this.touchStartX = touch.clientX; 92 this.touchStartY = touch.clientY; 93 this.touchCurrentX = touch.clientX; 94 this.touchCurrentY = touch.clientY; 95 96 // Set edge swipe state based on which edge was touched 97 this.isEdgeSwipe = true; 98 this.edgeDirection = event.currentTarget.dataset.edge; 99 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 } 109 110 const touch = event.touches[0]; 111 this.touchCurrentX = touch.clientX; 112 this.touchCurrentY = touch.clientY; 113 114 const deltaX = this.touchCurrentX - this.touchStartX; 115 const deltaY = this.touchCurrentY - this.touchStartY; 116 117 // Handle horizontal edge swipes (left/right) 118 if (this.edgeDirection === "left" && deltaX > 0) { 119 event.preventDefault(); 120 this.handleLeftEdgeMove(deltaX); 121 } else if (this.edgeDirection === "right" && deltaX < 0) { 122 event.preventDefault(); 123 this.handleRightEdgeMove(-deltaX); 124 } else if (this.edgeDirection === "bottom" && deltaY < 0) { 125 event.preventDefault(); 126 this.handleBottomEdgeMove(-deltaY); 127 } else if (this.edgeDirection === "top" && deltaY > 0) { 128 event.preventDefault(); 129 this.handleTopEdgeMove(deltaY); 130 } 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`); 158 this.isEdgeSwipe = false; 159 this.edgeDirection = null; 160 this.isPeeking = false; 161 this.hidePeekPreview(); 162 } 163 164 // ============================================================================ 165 // Left Edge (Previous WebView) 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 // ============================================================================ 195 // Right Edge (Next WebView) 196 // ============================================================================ 197 198 handleRightEdgeMove(distance) { 199 const progress = Math.min(distance / this.swipeThreshold, 1); 200 201 // Show peek preview of next webview 202 if (!this.isPeeking && progress > 0.1) { 203 this.isPeeking = true; 204 this.showPeekPreview("next", progress); 205 } else if (this.isPeeking) { 206 this.updatePeekPreview(progress); 207 } 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(); 217 } 218 } 219 220 // ============================================================================ 221 // Bottom Edge (Action Bar) 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 } 231 232 // ============================================================================ 233 // Top Edge (Notifications) 234 // ============================================================================ 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 ); 243 } 244 } 245 246 // ============================================================================ 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`); 255 return; 256 } 257 258 const info = this.lm.getWebViewInfo(adjacentId); 259 if (!info) { 260 console.error(`[EdgeGesture] no WebViewInfo for ${adjacentId}`); 261 return; 262 } 263 264 // Create peek preview element if it doesn't exist 265 if (!this.peekPreview) { 266 this.peekPreview = document.createElement("div"); 267 this.peekPreview.className = "mobile-peek-preview"; 268 this.peekPreview.innerHTML = ` 269 <div class="peek-header"> 270 <img class="peek-favicon" src="" alt=""> 271 <span class="peek-title"></span> 272 </div> 273 <img class="peek-screenshot" src="" alt=""> 274 `; 275 document.body.appendChild(this.peekPreview); 276 } 277 278 // Update content 279 const favicon = this.peekPreview.querySelector(".peek-favicon"); 280 const title = this.peekPreview.querySelector(".peek-title"); 281 const screenshot = this.peekPreview.querySelector(".peek-screenshot"); 282 283 favicon.src = info.favicon || ""; 284 title.textContent = info.title; 285 if (info.screenshotUrl) { 286 screenshot.src = info.screenshotUrl; 287 screenshot.style.display = "block"; 288 } else { 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; 311 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 } 326}