Rewild Your Web
18
fork

Configure Feed

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

at main 451 lines 12 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3import { EdgeGestureHandler } from "./edge_gesture_handler.js"; 4 5export class MobileLayoutManager { 6 constructor(rootElement, webViewBuilder) { 7 this.root = rootElement; 8 this.webViewBuilder = webViewBuilder; 9 10 // Linear array of webviews (carousel model) 11 this.webviews = new Map(); // webviewId -> { webview, index } 12 this.webviewOrder = []; // Array of webviewIds in order 13 this.activeIndex = -1; 14 this.activeWebviewId = null; 15 this.nextId = 1; 16 17 // Homescreen webview (special, cannot be closed, excluded from overview/swipe) 18 this.homescreenWebviewId = null; 19 20 // Overview mode state 21 this.overviewMode = false; 22 this.overviewElement = null; 23 24 // Gesture handler 25 this.gestureHandler = new EdgeGestureHandler(this); 26 27 // Action bar component 28 this.actionBar = null; 29 30 // Setup event listeners 31 this.setupEventListeners(); 32 } 33 34 setupEventListeners() { 35 document.addEventListener("webview-close", (e) => { 36 this.removeWebView(e.detail.webviewId); 37 }); 38 39 document.addEventListener("webview-focus", (e) => { 40 this.setActiveWebView(e.detail.webviewId); 41 }); 42 43 // Listen for navigation state changes from webviews 44 document.addEventListener("navigation-state-changed", (e) => { 45 // Only update if it's from the active webview 46 if (e.detail.webviewId === this.activeWebviewId && this.actionBar) { 47 this.actionBar.updateState(); 48 } 49 }); 50 51 // Disable all edges when action bar or notification sheet opens 52 document.addEventListener("action-bar-opened", () => { 53 this.disableAllEdges(); 54 }); 55 document.addEventListener("action-bar-closed", () => { 56 this.restoreEdges(); 57 }); 58 document.addEventListener("sheet-opened", () => { 59 this.disableAllEdges(); 60 }); 61 document.addEventListener("sheet-closed", () => { 62 this.restoreEdges(); 63 }); 64 } 65 66 // Disable all edge gestures (when overlay UI is open) 67 disableAllEdges() { 68 this.gestureHandler.setEdgesEnabled({ 69 left: false, 70 right: false, 71 bottom: false, 72 top: false, 73 }); 74 } 75 76 // Restore edge gestures based on current state 77 restoreEdges() { 78 const onHomescreen = this.isHomescreen(this.activeWebviewId); 79 this.gestureHandler.setEdgesEnabled({ 80 left: !onHomescreen, 81 right: !onHomescreen, 82 bottom: !onHomescreen, 83 top: true, 84 }); 85 } 86 87 generateId() { 88 return `wv-${this.nextId++}`; 89 } 90 91 // Set the homescreen webview 92 setHomescreen(webviewId) { 93 this.homescreenWebviewId = webviewId; 94 // Update edge gestures if homescreen is currently active 95 if (this.activeWebviewId === webviewId) { 96 this.restoreEdges(); 97 } 98 } 99 100 // Check if a webview is the homescreen 101 isHomescreen(webviewId) { 102 return webviewId === this.homescreenWebviewId; 103 } 104 105 // Get the currently active webview entry 106 getActiveEntry() { 107 if (this.activeWebviewId) { 108 return this.webviews.get(this.activeWebviewId); 109 } 110 return null; 111 } 112 113 // Add a new webview to the carousel 114 addWebView(webview) { 115 const id = this.generateId(); 116 webview.webviewId = id; 117 118 // Mark webview as mobile mode for styling 119 webview.classList.add("mobile-mode"); 120 121 // Add to the end of the order 122 const index = this.webviewOrder.length; 123 this.webviewOrder.push(id); 124 this.webviews.set(id, { webview, index }); 125 126 // Create container for the webview 127 const container = document.createElement("div"); 128 container.className = "mobile-webview-container"; 129 container.dataset.webviewId = id; 130 container.appendChild(webview); 131 this.root.appendChild(container); 132 133 // Make this the active webview 134 this.setActiveWebView(id); 135 136 return webview; 137 } 138 139 // Remove a webview from the carousel 140 removeWebView(webviewId) { 141 // Prevent closing the homescreen 142 if (this.isHomescreen(webviewId)) { 143 console.warn("[MobileLayoutManager] Cannot close homescreen"); 144 return; 145 } 146 147 const entry = this.webviews.get(webviewId); 148 if (!entry) { 149 return; 150 } 151 152 const { webview, index } = entry; 153 154 // Remove from DOM 155 const container = this.root.querySelector( 156 `.mobile-webview-container[data-webview-id="${webviewId}"]`, 157 ); 158 if (container) { 159 container.remove(); 160 } 161 162 // Remove from tracking 163 this.webviews.delete(webviewId); 164 this.webviewOrder = this.webviewOrder.filter((id) => id !== webviewId); 165 166 // Update indices for remaining webviews 167 this.webviewOrder.forEach((id, newIndex) => { 168 const e = this.webviews.get(id); 169 if (e) { 170 e.index = newIndex; 171 } 172 }); 173 174 // If we removed the active webview, activate another one 175 if (this.activeWebviewId === webviewId) { 176 if (this.webviewOrder.length > 0) { 177 // Activate the webview at the same position or the last one 178 const newIndex = Math.min(index, this.webviewOrder.length - 1); 179 this.setActiveWebView(this.webviewOrder[newIndex]); 180 } else { 181 this.activeWebviewId = null; 182 this.activeIndex = -1; 183 } 184 } 185 } 186 187 // Set the active webview by ID 188 setActiveWebView(webviewId) { 189 // Remove active state from previous and capture its screenshot 190 if (this.activeWebviewId && this.webviews.has(this.activeWebviewId)) { 191 const prevEntry = this.webviews.get(this.activeWebviewId); 192 prevEntry.webview.active = false; 193 194 // Capture screenshot of the tab we're switching away from 195 // TODO: fix screenshot capture when not fully visible. 196 // prevEntry.webview.captureScreenshot(true); 197 198 const prevContainer = this.root.querySelector( 199 `.mobile-webview-container[data-webview-id="${this.activeWebviewId}"]`, 200 ); 201 if (prevContainer) { 202 prevContainer.classList.remove("active"); 203 } 204 } 205 206 this.activeWebviewId = webviewId; 207 208 // Set active state on new 209 if (webviewId && this.webviews.has(webviewId)) { 210 const entry = this.webviews.get(webviewId); 211 entry.webview.active = true; 212 this.activeIndex = entry.index; 213 const container = this.root.querySelector( 214 `.mobile-webview-container[data-webview-id="${webviewId}"]`, 215 ); 216 if (container) { 217 container.classList.add("active"); 218 } 219 } 220 221 // Update edge gesture areas based on whether we're on the homescreen 222 this.restoreEdges(); 223 } 224 225 // Switch to webview at given index 226 switchTo(index) { 227 if (index < 0 || index >= this.webviewOrder.length) { 228 return; 229 } 230 const webviewId = this.webviewOrder[index]; 231 this.setActiveWebView(webviewId); 232 } 233 234 // Navigate to next webview in carousel (skips homescreen) 235 nextWebView() { 236 const regularViews = this.webviewOrder.filter( 237 (id) => !this.isHomescreen(id), 238 ); 239 if (regularViews.length <= 1) { 240 return; 241 } 242 243 const currentIndex = regularViews.indexOf(this.activeWebviewId); 244 if (currentIndex === -1) { 245 // Currently on homescreen, switch to first regular view 246 this.setActiveWebView(regularViews[0]); 247 } else { 248 const nextIndex = (currentIndex + 1) % regularViews.length; 249 this.setActiveWebView(regularViews[nextIndex]); 250 } 251 } 252 253 // Navigate to previous webview in carousel (skips homescreen) 254 prevWebView() { 255 const regularViews = this.webviewOrder.filter( 256 (id) => !this.isHomescreen(id), 257 ); 258 if (regularViews.length <= 1) { 259 return; 260 } 261 262 const currentIndex = regularViews.indexOf(this.activeWebviewId); 263 if (currentIndex === -1) { 264 // Currently on homescreen, switch to last regular view 265 this.setActiveWebView(regularViews[regularViews.length - 1]); 266 } else { 267 const prevIndex = 268 (currentIndex - 1 + regularViews.length) % regularViews.length; 269 this.setActiveWebView(regularViews[prevIndex]); 270 } 271 } 272 273 // Get adjacent webview ID for peek preview (skips homescreen) 274 getAdjacentWebViewId(direction) { 275 const regularViews = this.webviewOrder.filter( 276 (id) => !this.isHomescreen(id), 277 ); 278 if (regularViews.length <= 1) { 279 return null; 280 } 281 282 const currentIndex = regularViews.indexOf(this.activeWebviewId); 283 if (currentIndex === -1) { 284 // On homescreen, no peek 285 return null; 286 } 287 288 if (direction === "next") { 289 const nextIndex = (currentIndex + 1) % regularViews.length; 290 return regularViews[nextIndex]; 291 } else { 292 const prevIndex = 293 (currentIndex - 1 + regularViews.length) % regularViews.length; 294 return regularViews[prevIndex]; 295 } 296 } 297 298 // Get webview info for peek preview 299 getWebViewInfo(webviewId) { 300 const entry = this.webviews.get(webviewId); 301 if (!entry) { 302 return null; 303 } 304 return { 305 id: webviewId, 306 title: entry.webview.title || "Untitled", 307 favicon: entry.webview.favicon || "", 308 screenshotUrl: entry.webview.screenshotUrl || null, 309 }; 310 } 311 312 // Show action bar (bottom slide-up) 313 // Not allowed when on homescreen 314 showActionBar() { 315 if (this.isHomescreen(this.activeWebviewId)) { 316 return; 317 } 318 if (this.actionBar) { 319 this.actionBar.show(); 320 } 321 } 322 323 // Hide action bar 324 hideActionBar() { 325 if (this.actionBar) { 326 this.actionBar.hide(); 327 } 328 } 329 330 // Toggle action bar 331 toggleActionBar() { 332 if (this.actionBar) { 333 this.actionBar.toggle(); 334 } 335 } 336 337 // Set the action bar component reference 338 setActionBar(actionBar) { 339 this.actionBar = actionBar; 340 } 341 342 // Perform navigation action on active webview 343 goBack() { 344 const entry = this.getActiveEntry(); 345 if (entry) { 346 entry.webview.goBack(); 347 } 348 } 349 350 goForward() { 351 const entry = this.getActiveEntry(); 352 if (entry) { 353 entry.webview.goForward(); 354 } 355 } 356 357 reload() { 358 const entry = this.getActiveEntry(); 359 if (entry) { 360 entry.webview.doReload(); 361 } 362 } 363 364 // Navigate to URL in active webview 365 navigateTo(url) { 366 const entry = this.getActiveEntry(); 367 if (entry) { 368 entry.webview.ensureIframe(); 369 entry.webview.iframe.load(url); 370 } 371 } 372 373 // Get current URL of active webview 374 getCurrentUrl() { 375 const entry = this.getActiveEntry(); 376 if (entry) { 377 return entry.webview.currentUrl || ""; 378 } 379 return ""; 380 } 381 382 // Get navigation state of active webview 383 getNavigationState() { 384 const entry = this.getActiveEntry(); 385 if (entry) { 386 return { 387 canGoBack: entry.webview.canGoBack || false, 388 canGoForward: entry.webview.canGoForward || false, 389 }; 390 } 391 return { canGoBack: false, canGoForward: false }; 392 } 393 394 // Get tab count (excludes homescreen) 395 getTabCount() { 396 return this.webviewOrder.filter((id) => !this.isHomescreen(id)).length; 397 } 398 399 // Get tabs for overview (excludes homescreen) 400 getOverviewTabs() { 401 return this.webviewOrder 402 .filter((id) => !this.isHomescreen(id)) 403 .map((id) => { 404 const entry = this.webviews.get(id); 405 return { 406 id, 407 title: entry.webview.title || "Untitled", 408 favicon: entry.webview.favicon || "", 409 screenshotUrl: entry.webview.screenshotUrl || null, 410 }; 411 }); 412 } 413 414 // ============================================================================ 415 // Overview Mode (for compatibility, minimal implementation for MVP) 416 // ============================================================================ 417 418 toggleOverview() { 419 if (this.overviewMode) { 420 this.hideOverview(); 421 } else { 422 this.showOverview(); 423 } 424 } 425 426 showOverview() { 427 this.overviewMode = true; 428 // TODO: Implement mobile tab overview grid 429 console.log( 430 "[MobileLayoutManager] Overview mode enabled (not yet implemented)", 431 ); 432 } 433 434 hideOverview() { 435 this.overviewMode = false; 436 console.log("[MobileLayoutManager] Overview mode disabled"); 437 } 438 439 // Navigation methods for compatibility with desktop shortcuts 440 nextPanel() { 441 this.nextWebView(); 442 } 443 444 prevPanel() { 445 this.prevWebView(); 446 } 447 448 scrollToPanel(index) { 449 this.switchTo(index); 450 } 451}