Rewild Your Web
web browser dweb
at main 409 lines 11 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 52 generateId() { 53 return `wv-${this.nextId++}`; 54 } 55 56 // Set the homescreen webview 57 setHomescreen(webviewId) { 58 this.homescreenWebviewId = webviewId; 59 } 60 61 // Check if a webview is the homescreen 62 isHomescreen(webviewId) { 63 return webviewId === this.homescreenWebviewId; 64 } 65 66 // Get the currently active webview entry 67 getActiveEntry() { 68 if (this.activeWebviewId) { 69 return this.webviews.get(this.activeWebviewId); 70 } 71 return null; 72 } 73 74 // Add a new webview to the carousel 75 addWebView(webview) { 76 const id = this.generateId(); 77 webview.webviewId = id; 78 79 // Mark webview as mobile mode for styling 80 webview.classList.add("mobile-mode"); 81 82 // Add to the end of the order 83 const index = this.webviewOrder.length; 84 this.webviewOrder.push(id); 85 this.webviews.set(id, { webview, index }); 86 87 // Create container for the webview 88 const container = document.createElement("div"); 89 container.className = "mobile-webview-container"; 90 container.dataset.webviewId = id; 91 container.appendChild(webview); 92 this.root.appendChild(container); 93 94 // Make this the active webview 95 this.setActiveWebView(id); 96 97 return webview; 98 } 99 100 // Remove a webview from the carousel 101 removeWebView(webviewId) { 102 // Prevent closing the homescreen 103 if (this.isHomescreen(webviewId)) { 104 console.warn("[MobileLayoutManager] Cannot close homescreen"); 105 return; 106 } 107 108 const entry = this.webviews.get(webviewId); 109 if (!entry) { 110 return; 111 } 112 113 const { webview, index } = entry; 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(); 121 } 122 123 // Remove from tracking 124 this.webviews.delete(webviewId); 125 this.webviewOrder = this.webviewOrder.filter((id) => id !== webviewId); 126 127 // Update indices for remaining webviews 128 this.webviewOrder.forEach((id, newIndex) => { 129 const e = this.webviews.get(id); 130 if (e) { 131 e.index = newIndex; 132 } 133 }); 134 135 // If we removed the active webview, activate another one 136 if (this.activeWebviewId === webviewId) { 137 if (this.webviewOrder.length > 0) { 138 // Activate the webview at the same position or the last one 139 const newIndex = Math.min(index, this.webviewOrder.length - 1); 140 this.setActiveWebView(this.webviewOrder[newIndex]); 141 } else { 142 this.activeWebviewId = null; 143 this.activeIndex = -1; 144 } 145 } 146 } 147 148 // Set the active webview by ID 149 setActiveWebView(webviewId) { 150 // Remove active state from previous and capture its screenshot 151 if (this.activeWebviewId && this.webviews.has(this.activeWebviewId)) { 152 const prevEntry = this.webviews.get(this.activeWebviewId); 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); 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"); 164 } 165 } 166 167 this.activeWebviewId = webviewId; 168 169 // Set active state on new 170 if (webviewId && this.webviews.has(webviewId)) { 171 const entry = this.webviews.get(webviewId); 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"); 179 } 180 } 181 } 182 183 // Switch to webview at given index 184 switchTo(index) { 185 if (index < 0 || index >= this.webviewOrder.length) { 186 return; 187 } 188 const webviewId = this.webviewOrder[index]; 189 this.setActiveWebView(webviewId); 190 } 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 } 200 201 const currentIndex = regularViews.indexOf(this.activeWebviewId); 202 if (currentIndex === -1) { 203 // Currently on homescreen, switch to first regular view 204 this.setActiveWebView(regularViews[0]); 205 } else { 206 const nextIndex = (currentIndex + 1) % regularViews.length; 207 this.setActiveWebView(regularViews[nextIndex]); 208 } 209 } 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 } 219 220 const currentIndex = regularViews.indexOf(this.activeWebviewId); 221 if (currentIndex === -1) { 222 // Currently on homescreen, switch to last regular view 223 this.setActiveWebView(regularViews[regularViews.length - 1]); 224 } else { 225 const prevIndex = 226 (currentIndex - 1 + regularViews.length) % regularViews.length; 227 this.setActiveWebView(regularViews[prevIndex]); 228 } 229 } 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 } 239 240 const currentIndex = regularViews.indexOf(this.activeWebviewId); 241 if (currentIndex === -1) { 242 // On homescreen, no peek 243 return null; 244 } 245 246 if (direction === "next") { 247 const nextIndex = (currentIndex + 1) % regularViews.length; 248 return regularViews[nextIndex]; 249 } else { 250 const prevIndex = 251 (currentIndex - 1 + regularViews.length) % regularViews.length; 252 return regularViews[prevIndex]; 253 } 254 } 255 256 // Get webview info for peek preview 257 getWebViewInfo(webviewId) { 258 const entry = this.webviews.get(webviewId); 259 if (!entry) { 260 return null; 261 } 262 return { 263 id: webviewId, 264 title: entry.webview.title || "Untitled", 265 favicon: entry.webview.favicon || "", 266 screenshotUrl: entry.webview.screenshotUrl || null, 267 }; 268 } 269 270 // Show action bar (bottom slide-up) 271 // Not allowed when on homescreen 272 showActionBar() { 273 if (this.isHomescreen(this.activeWebviewId)) { 274 return; 275 } 276 if (this.actionBar) { 277 this.actionBar.show(); 278 } 279 } 280 281 // Hide action bar 282 hideActionBar() { 283 if (this.actionBar) { 284 this.actionBar.hide(); 285 } 286 } 287 288 // Toggle action bar 289 toggleActionBar() { 290 if (this.actionBar) { 291 this.actionBar.toggle(); 292 } 293 } 294 295 // Set the action bar component reference 296 setActionBar(actionBar) { 297 this.actionBar = actionBar; 298 } 299 300 // Perform navigation action on active webview 301 goBack() { 302 const entry = this.getActiveEntry(); 303 if (entry) { 304 entry.webview.goBack(); 305 } 306 } 307 308 goForward() { 309 const entry = this.getActiveEntry(); 310 if (entry) { 311 entry.webview.goForward(); 312 } 313 } 314 315 reload() { 316 const entry = this.getActiveEntry(); 317 if (entry) { 318 entry.webview.doReload(); 319 } 320 } 321 322 // Navigate to URL in active webview 323 navigateTo(url) { 324 const entry = this.getActiveEntry(); 325 if (entry) { 326 entry.webview.ensureIframe(); 327 entry.webview.iframe.load(url); 328 } 329 } 330 331 // Get current URL of active webview 332 getCurrentUrl() { 333 const entry = this.getActiveEntry(); 334 if (entry) { 335 return entry.webview.currentUrl || ""; 336 } 337 return ""; 338 } 339 340 // Get navigation state of active webview 341 getNavigationState() { 342 const entry = this.getActiveEntry(); 343 if (entry) { 344 return { 345 canGoBack: entry.webview.canGoBack || false, 346 canGoForward: entry.webview.canGoForward || false, 347 }; 348 } 349 return { canGoBack: false, canGoForward: false }; 350 } 351 352 // Get tab count (excludes homescreen) 353 getTabCount() { 354 return this.webviewOrder.filter((id) => !this.isHomescreen(id)).length; 355 } 356 357 // Get tabs for overview (excludes homescreen) 358 getOverviewTabs() { 359 return this.webviewOrder 360 .filter((id) => !this.isHomescreen(id)) 361 .map((id) => { 362 const entry = this.webviews.get(id); 363 return { 364 id, 365 title: entry.webview.title || "Untitled", 366 favicon: entry.webview.favicon || "", 367 screenshotUrl: entry.webview.screenshotUrl || null, 368 }; 369 }); 370 } 371 372 // ============================================================================ 373 // Overview Mode (for compatibility, minimal implementation for MVP) 374 // ============================================================================ 375 376 toggleOverview() { 377 if (this.overviewMode) { 378 this.hideOverview(); 379 } else { 380 this.showOverview(); 381 } 382 } 383 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() { 393 this.overviewMode = false; 394 console.log("[MobileLayoutManager] Overview mode disabled"); 395 } 396 397 // Navigation methods for compatibility with desktop shortcuts 398 nextPanel() { 399 this.nextWebView(); 400 } 401 402 prevPanel() { 403 this.prevWebView(); 404 } 405 406 scrollToPanel(index) { 407 this.switchTo(index); 408 } 409}