feat(webkit): add content blocking stubs and keyboard dispatcher improvements
- commit
- 6dfc8843bab0e256d276dc536edbff1d4762d1d7
- author
- bamen · · 2025-09-15 05:21 UTC
- parent
- 66a30d0
- signature
- unsigned
6 files · +462 -55
M pkg/webkit/codec_control.go +47 -31
| @@ -59,8 +59,13 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 59 | 59 | ||
| 60 | 60 | js.WriteString(`(() => { | |
| 61 | 61 | 'use strict'; | |
| 62 | - | ||
| 63 | - | console.log('[dumber-codec] Initializing codec control'); | |
| 62 | + | ||
| 63 | + | const isMainFrame = (window.self === window.top); | |
| 64 | + | function log(msg) { | |
| 65 | + | if (isMainFrame) console.log('[dumber-codec] ' + msg); | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | log('Initializing codec control'); | |
| 64 | 69 | ||
| 65 | 70 | // Smart fullscreen handling that works with WebKit's native behavior | |
| 66 | 71 | const videoFullscreenStates = new WeakMap(); | |
| @@ -80,7 +85,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 80 | 85 | }; | |
| 81 | 86 | ||
| 82 | 87 | videoFullscreenStates.set(video, state); | |
| 83 | - | console.log('[dumber-codec] Tracking video for fullscreen - playing:', state.wasPlaying, 'time:', state.currentTime); | |
| 88 | + | log('Tracking video for fullscreen - playing: ' + state.wasPlaying + ', time: ' + state.currentTime); | |
| 84 | 89 | } | |
| 85 | 90 | ||
| 86 | 91 | // Gentle recovery without forcing reloads | |
| @@ -88,20 +93,20 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 88 | 93 | const state = videoFullscreenStates.get(video); | |
| 89 | 94 | if (!state) return; | |
| 90 | 95 | ||
| 91 | - | console.log('[dumber-codec] Recovering video after fullscreen transition'); | |
| 96 | + | log('Recovering video after fullscreen transition'); | |
| 92 | 97 | ||
| 93 | 98 | // Only intervene if video is stuck in a loading state | |
| 94 | 99 | const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready | |
| 95 | 100 | const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2; | |
| 96 | 101 | ||
| 97 | 102 | if (isStuck || hasLostTime) { | |
| 98 | - | console.log('[dumber-codec] Video appears stuck, applying gentle recovery'); | |
| 103 | + | log('Video appears stuck, applying gentle recovery'); | |
| 99 | 104 | ||
| 100 | 105 | // Gentle nudge - just try to resume if it was playing | |
| 101 | 106 | if (state.wasPlaying && video.paused) { | |
| 102 | 107 | setTimeout(() => { | |
| 103 | 108 | if (video.readyState >= 2) { | |
| 104 | - | video.play().catch(e => console.log('[dumber-codec] Play failed:', e)); | |
| 109 | + | video.play().catch(e => log('Play failed: ' + e)); | |
| 105 | 110 | } | |
| 106 | 111 | }, 200); | |
| 107 | 112 | } | |
| @@ -115,11 +120,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 115 | 120 | if (fullscreenTransitionInProgress) return; | |
| 116 | 121 | fullscreenTransitionInProgress = true; | |
| 117 | 122 | ||
| 118 | - | console.log('[dumber-codec] WebKit fullscreen transition detected'); | |
| 123 | + | log('WebKit fullscreen transition detected'); | |
| 119 | 124 | ||
| 120 | 125 | // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing | |
| 121 | 126 | if (location.hostname.includes('twitch.tv')) { | |
| 122 | - | console.log('[dumber-codec] Skipping fullscreen recovery on Twitch for stability'); | |
| 127 | + | log('Skipping fullscreen recovery on Twitch for stability'); | |
| 123 | 128 | fullscreenTransitionInProgress = false; | |
| 124 | 129 | return; | |
| 125 | 130 | } | |
| @@ -148,7 +153,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 148 | 153 | // Override canPlayType for HTMLMediaElement (primary codec detection) | |
| 149 | 154 | const originalCanPlayType = HTMLMediaElement.prototype.canPlayType; | |
| 150 | 155 | HTMLMediaElement.prototype.canPlayType = function(type) { | |
| 151 | - | console.log('[dumber-codec] canPlayType called with:', type); | |
| 156 | + | log('canPlayType called with: ' + type); | |
| 152 | 157 | ||
| 153 | 158 | // Block unwanted codecs`) | |
| 154 | 159 | ||
| @@ -156,7 +161,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 156 | 161 | js.WriteString(` | |
| 157 | 162 | const blockedRegex = /` + blockedPattern + `/i; | |
| 158 | 163 | if (blockedRegex.test(type)) { | |
| 159 | - | console.log('[dumber-codec] Blocking codec via canPlayType:', type); | |
| 164 | + | log('Blocking codec via canPlayType: ' + type); | |
| 160 | 165 | return ''; | |
| 161 | 166 | }`) | |
| 162 | 167 | } | |
| @@ -168,7 +173,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 168 | 173 | const preferredCodecs = ` + preferredCodecsJS + `; | |
| 169 | 174 | for (const preferred of preferredCodecs) { | |
| 170 | 175 | if (type.toLowerCase().includes(preferred)) { | |
| 171 | - | console.log('[dumber-codec] Boosting preferred codec via canPlayType:', type); | |
| 176 | + | log('Boosting preferred codec via canPlayType: ' + type); | |
| 172 | 177 | return 'probably'; | |
| 173 | 178 | } | |
| 174 | 179 | }`) | |
| @@ -184,7 +189,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 184 | 189 | if (window.MediaSource && MediaSource.isTypeSupported) { | |
| 185 | 190 | const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource); | |
| 186 | 191 | MediaSource.isTypeSupported = function(type) { | |
| 187 | - | console.log('[dumber-codec] MediaSource.isTypeSupported called with:', type);`) | |
| 192 | + | log('MediaSource.isTypeSupported called with: ' + type);`) | |
| 188 | 193 | ||
| 189 | 194 | if hasBlockedCodecs { | |
| 190 | 195 | js.WriteString(` | |
| @@ -192,7 +197,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 192 | 197 | // Block unwanted codecs | |
| 193 | 198 | const blockedRegex = /` + blockedPattern + `/i; | |
| 194 | 199 | if (blockedRegex.test(type)) { | |
| 195 | - | console.log('[dumber-codec] Blocking codec via MediaSource.isTypeSupported:', type); | |
| 200 | + | log('Blocking codec via MediaSource.isTypeSupported: ' + type); | |
| 196 | 201 | return false; | |
| 197 | 202 | }`) | |
| 198 | 203 | } | |
| @@ -204,7 +209,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 204 | 209 | const preferredCodecs = ` + preferredCodecsJS + `; | |
| 205 | 210 | for (const preferred of preferredCodecs) { | |
| 206 | 211 | if (type.toLowerCase().includes(preferred)) { | |
| 207 | - | console.log('[dumber-codec] Boosting preferred codec via MediaSource.isTypeSupported:', type); | |
| 212 | + | log('Boosting preferred codec via MediaSource.isTypeSupported: ' + type); | |
| 208 | 213 | return true; | |
| 209 | 214 | } | |
| 210 | 215 | }`) | |
| @@ -237,8 +242,8 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 237 | 242 | const is1080p60 = (height >= 1080 || width >= 1920) && | |
| 238 | 243 | (config.video?.framerate >= 50 || config.video?.frameRate >= 50); | |
| 239 | 244 | ||
| 240 | - | console.log('[dumber-codec] MediaCapabilities query - contentType:', contentType, | |
| 241 | - | 'resolution:', width + 'x' + height, 'highRes:', isHighRes, '4K:', is4K, '1080p60:', is1080p60); | |
| 245 | + | log('MediaCapabilities query - contentType: ' + contentType + | |
| 246 | + | ', resolution: ' + width + 'x' + height + ', highRes: ' + isHighRes + ', 4K: ' + is4K + ', 1080p60: ' + is1080p60); | |
| 242 | 247 | ||
| 243 | 248 | // Block unwanted codecs`) | |
| 244 | 249 | ||
| @@ -246,7 +251,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 246 | 251 | js.WriteString(` | |
| 247 | 252 | const blockedRegex = /` + blockedPattern + `/i; | |
| 248 | 253 | if (blockedRegex.test(contentType)) { | |
| 249 | - | console.log('[dumber-codec] Blocking codec:', contentType); | |
| 254 | + | log('Blocking codec: ' + contentType); | |
| 250 | 255 | return { | |
| 251 | 256 | supported: false, | |
| 252 | 257 | smooth: false, | |
| @@ -264,7 +269,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 264 | 269 | const preferredCodecs = ` + preferredCodecsJS + `; | |
| 265 | 270 | for (const preferred of preferredCodecs) { | |
| 266 | 271 | if (contentType.toLowerCase().includes(preferred)) { | |
| 267 | - | console.log('[dumber-codec] Boosting preferred codec:', contentType); | |
| 272 | + | log('Boosting preferred codec: ' + contentType); | |
| 268 | 273 | ||
| 269 | 274 | // Resolution-aware capability reporting for AV1 | |
| 270 | 275 | if (preferred === 'av01' || preferred === 'av1') { | |
| @@ -328,7 +333,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 328 | 333 | ||
| 329 | 334 | // YouTube-specific codec forcing | |
| 330 | 335 | if (location.hostname.includes('youtube.com')) { | |
| 331 | - | console.log('[dumber-codec] Applying YouTube codec preferences'); | |
| 336 | + | log('Applying YouTube codec preferences'); | |
| 332 | 337 | ||
| 333 | 338 | // Set YouTube's localStorage preferences for smart AV1 usage | |
| 334 | 339 | // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res | |
| @@ -340,7 +345,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 340 | 345 | writable: false, | |
| 341 | 346 | configurable: false | |
| 342 | 347 | }); | |
| 343 | - | console.log('[dumber-codec] YouTube: Set smart AV1 localStorage preference (2048)'); | |
| 348 | + | log('YouTube: Set smart AV1 localStorage preference (2048)'); | |
| 344 | 349 | } catch (e) { | |
| 345 | 350 | console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e); | |
| 346 | 351 | } | |
| @@ -358,7 +363,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 358 | 363 | js.WriteString(` | |
| 359 | 364 | // Force AV1 if enabled | |
| 360 | 365 | player.config.args.preferred_codecs = 'av01'; | |
| 361 | - | console.log('[dumber-codec] YouTube: Forced AV1 codec');`) | |
| 366 | + | log('YouTube: Forced AV1 codec');`) | |
| 362 | 367 | } | |
| 363 | 368 | ||
| 364 | 369 | if hasPreferredCodecs { | |
| @@ -375,7 +380,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 375 | 380 | } | |
| 376 | 381 | }).join(','); | |
| 377 | 382 | player.config.args.preferred_codecs = codecOrder; | |
| 378 | - | console.log('[dumber-codec] YouTube: Set codec order:', codecOrder);`) | |
| 383 | + | log('YouTube: Set codec order: ' + codecOrder);`) | |
| 379 | 384 | } | |
| 380 | 385 | ||
| 381 | 386 | js.WriteString(` | |
| @@ -388,7 +393,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 388 | 393 | const originalFetch = window.fetch; | |
| 389 | 394 | window.fetch = function(input, init) { | |
| 390 | 395 | if (typeof input === 'string' && input.includes('/youtubei/v1/player')) { | |
| 391 | - | console.log('[dumber-codec] YouTube: Intercepting player request');`) | |
| 396 | + | log('YouTube: Intercepting player request');`) | |
| 392 | 397 | ||
| 393 | 398 | if hasPreferredCodecs { | |
| 394 | 399 | js.WriteString(` | |
| @@ -422,7 +427,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 422 | 427 | ||
| 423 | 428 | // Manipulate streaming data if present | |
| 424 | 429 | if (data.streamingData && data.streamingData.formats) { | |
| 425 | - | console.log('[dumber-codec] YouTube: Processing format manifest'); | |
| 430 | + | log('YouTube: Processing format manifest'); | |
| 426 | 431 | ||
| 427 | 432 | // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher | |
| 428 | 433 | data.streamingData.formats.sort((a, b) => { | |
| @@ -447,7 +452,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 447 | 452 | return 0; | |
| 448 | 453 | }); | |
| 449 | 454 | ||
| 450 | - | console.log('[dumber-codec] YouTube: Reordered formats for optimal codec selection'); | |
| 455 | + | log('YouTube: Reordered formats for optimal codec selection'); | |
| 451 | 456 | } | |
| 452 | 457 | ||
| 453 | 458 | return new Response(JSON.stringify(data), { | |
| @@ -473,7 +478,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 473 | 478 | ||
| 474 | 479 | // Twitch: No codec interference - let Twitch handle codec selection natively | |
| 475 | 480 | if (location.hostname.includes('twitch.tv')) { | |
| 476 | - | console.log('[dumber-codec] Detected Twitch domain - no codec interference for stability'); | |
| 481 | + | log('Detected Twitch domain - no codec interference for stability'); | |
| 477 | 482 | // Twitch codec control completely removed to prevent theater/fullscreen freezing | |
| 478 | 483 | } | |
| 479 | 484 | ||
| @@ -483,11 +488,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 483 | 488 | videos.forEach(video => { | |
| 484 | 489 | if (!video.__codecMonitored) { | |
| 485 | 490 | video.__codecMonitored = true; | |
| 486 | - | console.log('[dumber-codec] Found video element, adding passive monitoring'); | |
| 491 | + | log('Found video element, adding passive monitoring'); | |
| 487 | 492 | ||
| 488 | 493 | // Passive codec detection only | |
| 489 | 494 | video.addEventListener('canplay', function() { | |
| 490 | - | console.log('[dumber-codec] Video can play - resolution:', this.videoWidth + 'x' + this.videoHeight); | |
| 495 | + | log('Video can play - resolution: ' + this.videoWidth + 'x' + this.videoHeight); | |
| 491 | 496 | }, { passive: true, once: true }); | |
| 492 | 497 | } | |
| 493 | 498 | }); | |
| @@ -495,10 +500,21 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 495 | 500 | ||
| 496 | 501 | // Monitor for video elements periodically without overriding createElement | |
| 497 | 502 | const videoObserver = new MutationObserver(monitorExistingVideos); | |
| 498 | - | videoObserver.observe(document.body, { childList: true, subtree: true }); | |
| 499 | - | monitorExistingVideos(); // Check existing videos | |
| 503 | + | ||
| 504 | + | // Wait for document.body to be available before observing | |
| 505 | + | function startVideoObserver() { | |
| 506 | + | if (document.body) { | |
| 507 | + | videoObserver.observe(document.body, { childList: true, subtree: true }); | |
| 508 | + | monitorExistingVideos(); // Check existing videos | |
| 509 | + | log('Video observer started'); | |
| 510 | + | } else { | |
| 511 | + | // Retry after a short delay if body not ready | |
| 512 | + | setTimeout(startVideoObserver, 50); | |
| 513 | + | } | |
| 514 | + | } | |
| 515 | + | startVideoObserver(); | |
| 500 | 516 | ||
| 501 | - | console.log('[dumber-codec] Codec control initialization complete'); | |
| 517 | + | log('Codec control initialization complete'); | |
| 502 | 518 | })();`) | |
| 503 | 519 | ||
| 504 | 520 | return js.String() | |
| @@ -59,8 +59,13 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 59 | 59 | ||||
| 60 | js.WriteString(`(() => { | 60 | js.WriteString(`(() => { | ||
| 61 | 'use strict'; | 61 | 'use strict'; | ||
| 62 | - | 62 | + | ||
| 63 | - | console.log('[dumber-codec] Initializing codec control'); | 63 | + | const isMainFrame = (window.self === window.top); |
| 64 | + | function log(msg) { | |||
| 65 | + | if (isMainFrame) console.log('[dumber-codec] ' + msg); | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | log('Initializing codec control'); | |||
| 64 | 69 | ||||
| 65 | // Smart fullscreen handling that works with WebKit's native behavior | 70 | // Smart fullscreen handling that works with WebKit's native behavior | ||
| 66 | const videoFullscreenStates = new WeakMap(); | 71 | const videoFullscreenStates = new WeakMap(); | ||
| @@ -80,7 +85,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 80 | }; | 85 | }; | ||
| 81 | 86 | ||||
| 82 | videoFullscreenStates.set(video, state); | 87 | videoFullscreenStates.set(video, state); | ||
| 83 | - | console.log('[dumber-codec] Tracking video for fullscreen - playing:', state.wasPlaying, 'time:', state.currentTime); | 88 | + | log('Tracking video for fullscreen - playing: ' + state.wasPlaying + ', time: ' + state.currentTime); |
| 84 | } | 89 | } | ||
| 85 | 90 | ||||
| 86 | // Gentle recovery without forcing reloads | 91 | // Gentle recovery without forcing reloads | ||
| @@ -88,20 +93,20 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 88 | const state = videoFullscreenStates.get(video); | 93 | const state = videoFullscreenStates.get(video); | ||
| 89 | if (!state) return; | 94 | if (!state) return; | ||
| 90 | 95 | ||||
| 91 | - | console.log('[dumber-codec] Recovering video after fullscreen transition'); | 96 | + | log('Recovering video after fullscreen transition'); |
| 92 | 97 | ||||
| 93 | // Only intervene if video is stuck in a loading state | 98 | // Only intervene if video is stuck in a loading state | ||
| 94 | const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready | 99 | const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready | ||
| 95 | const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2; | 100 | const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2; | ||
| 96 | 101 | ||||
| 97 | if (isStuck || hasLostTime) { | 102 | if (isStuck || hasLostTime) { | ||
| 98 | - | console.log('[dumber-codec] Video appears stuck, applying gentle recovery'); | 103 | + | log('Video appears stuck, applying gentle recovery'); |
| 99 | 104 | ||||
| 100 | // Gentle nudge - just try to resume if it was playing | 105 | // Gentle nudge - just try to resume if it was playing | ||
| 101 | if (state.wasPlaying && video.paused) { | 106 | if (state.wasPlaying && video.paused) { | ||
| 102 | setTimeout(() => { | 107 | setTimeout(() => { | ||
| 103 | if (video.readyState >= 2) { | 108 | if (video.readyState >= 2) { | ||
| 104 | - | video.play().catch(e => console.log('[dumber-codec] Play failed:', e)); | 109 | + | video.play().catch(e => log('Play failed: ' + e)); |
| 105 | } | 110 | } | ||
| 106 | }, 200); | 111 | }, 200); | ||
| 107 | } | 112 | } | ||
| @@ -115,11 +120,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 115 | if (fullscreenTransitionInProgress) return; | 120 | if (fullscreenTransitionInProgress) return; | ||
| 116 | fullscreenTransitionInProgress = true; | 121 | fullscreenTransitionInProgress = true; | ||
| 117 | 122 | ||||
| 118 | - | console.log('[dumber-codec] WebKit fullscreen transition detected'); | 123 | + | log('WebKit fullscreen transition detected'); |
| 119 | 124 | ||||
| 120 | // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing | 125 | // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing | ||
| 121 | if (location.hostname.includes('twitch.tv')) { | 126 | if (location.hostname.includes('twitch.tv')) { | ||
| 122 | - | console.log('[dumber-codec] Skipping fullscreen recovery on Twitch for stability'); | 127 | + | log('Skipping fullscreen recovery on Twitch for stability'); |
| 123 | fullscreenTransitionInProgress = false; | 128 | fullscreenTransitionInProgress = false; | ||
| 124 | return; | 129 | return; | ||
| 125 | } | 130 | } | ||
| @@ -148,7 +153,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 148 | // Override canPlayType for HTMLMediaElement (primary codec detection) | 153 | // Override canPlayType for HTMLMediaElement (primary codec detection) | ||
| 149 | const originalCanPlayType = HTMLMediaElement.prototype.canPlayType; | 154 | const originalCanPlayType = HTMLMediaElement.prototype.canPlayType; | ||
| 150 | HTMLMediaElement.prototype.canPlayType = function(type) { | 155 | HTMLMediaElement.prototype.canPlayType = function(type) { | ||
| 151 | - | console.log('[dumber-codec] canPlayType called with:', type); | 156 | + | log('canPlayType called with: ' + type); |
| 152 | 157 | ||||
| 153 | // Block unwanted codecs`) | 158 | // Block unwanted codecs`) | ||
| 154 | 159 | ||||
| @@ -156,7 +161,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 156 | js.WriteString(` | 161 | js.WriteString(` | ||
| 157 | const blockedRegex = /` + blockedPattern + `/i; | 162 | const blockedRegex = /` + blockedPattern + `/i; | ||
| 158 | if (blockedRegex.test(type)) { | 163 | if (blockedRegex.test(type)) { | ||
| 159 | - | console.log('[dumber-codec] Blocking codec via canPlayType:', type); | 164 | + | log('Blocking codec via canPlayType: ' + type); |
| 160 | return ''; | 165 | return ''; | ||
| 161 | }`) | 166 | }`) | ||
| 162 | } | 167 | } | ||
| @@ -168,7 +173,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 168 | const preferredCodecs = ` + preferredCodecsJS + `; | 173 | const preferredCodecs = ` + preferredCodecsJS + `; | ||
| 169 | for (const preferred of preferredCodecs) { | 174 | for (const preferred of preferredCodecs) { | ||
| 170 | if (type.toLowerCase().includes(preferred)) { | 175 | if (type.toLowerCase().includes(preferred)) { | ||
| 171 | - | console.log('[dumber-codec] Boosting preferred codec via canPlayType:', type); | 176 | + | log('Boosting preferred codec via canPlayType: ' + type); |
| 172 | return 'probably'; | 177 | return 'probably'; | ||
| 173 | } | 178 | } | ||
| 174 | }`) | 179 | }`) | ||
| @@ -184,7 +189,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 184 | if (window.MediaSource && MediaSource.isTypeSupported) { | 189 | if (window.MediaSource && MediaSource.isTypeSupported) { | ||
| 185 | const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource); | 190 | const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource); | ||
| 186 | MediaSource.isTypeSupported = function(type) { | 191 | MediaSource.isTypeSupported = function(type) { | ||
| 187 | - | console.log('[dumber-codec] MediaSource.isTypeSupported called with:', type);`) | 192 | + | log('MediaSource.isTypeSupported called with: ' + type);`) |
| 188 | 193 | ||||
| 189 | if hasBlockedCodecs { | 194 | if hasBlockedCodecs { | ||
| 190 | js.WriteString(` | 195 | js.WriteString(` | ||
| @@ -192,7 +197,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 192 | // Block unwanted codecs | 197 | // Block unwanted codecs | ||
| 193 | const blockedRegex = /` + blockedPattern + `/i; | 198 | const blockedRegex = /` + blockedPattern + `/i; | ||
| 194 | if (blockedRegex.test(type)) { | 199 | if (blockedRegex.test(type)) { | ||
| 195 | - | console.log('[dumber-codec] Blocking codec via MediaSource.isTypeSupported:', type); | 200 | + | log('Blocking codec via MediaSource.isTypeSupported: ' + type); |
| 196 | return false; | 201 | return false; | ||
| 197 | }`) | 202 | }`) | ||
| 198 | } | 203 | } | ||
| @@ -204,7 +209,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 204 | const preferredCodecs = ` + preferredCodecsJS + `; | 209 | const preferredCodecs = ` + preferredCodecsJS + `; | ||
| 205 | for (const preferred of preferredCodecs) { | 210 | for (const preferred of preferredCodecs) { | ||
| 206 | if (type.toLowerCase().includes(preferred)) { | 211 | if (type.toLowerCase().includes(preferred)) { | ||
| 207 | - | console.log('[dumber-codec] Boosting preferred codec via MediaSource.isTypeSupported:', type); | 212 | + | log('Boosting preferred codec via MediaSource.isTypeSupported: ' + type); |
| 208 | return true; | 213 | return true; | ||
| 209 | } | 214 | } | ||
| 210 | }`) | 215 | }`) | ||
| @@ -237,8 +242,8 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 237 | const is1080p60 = (height >= 1080 || width >= 1920) && | 242 | const is1080p60 = (height >= 1080 || width >= 1920) && | ||
| 238 | (config.video?.framerate >= 50 || config.video?.frameRate >= 50); | 243 | (config.video?.framerate >= 50 || config.video?.frameRate >= 50); | ||
| 239 | 244 | ||||
| 240 | - | console.log('[dumber-codec] MediaCapabilities query - contentType:', contentType, | 245 | + | log('MediaCapabilities query - contentType: ' + contentType + |
| 241 | - | 'resolution:', width + 'x' + height, 'highRes:', isHighRes, '4K:', is4K, '1080p60:', is1080p60); | 246 | + | ', resolution: ' + width + 'x' + height + ', highRes: ' + isHighRes + ', 4K: ' + is4K + ', 1080p60: ' + is1080p60); |
| 242 | 247 | ||||
| 243 | // Block unwanted codecs`) | 248 | // Block unwanted codecs`) | ||
| 244 | 249 | ||||
| @@ -246,7 +251,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 246 | js.WriteString(` | 251 | js.WriteString(` | ||
| 247 | const blockedRegex = /` + blockedPattern + `/i; | 252 | const blockedRegex = /` + blockedPattern + `/i; | ||
| 248 | if (blockedRegex.test(contentType)) { | 253 | if (blockedRegex.test(contentType)) { | ||
| 249 | - | console.log('[dumber-codec] Blocking codec:', contentType); | 254 | + | log('Blocking codec: ' + contentType); |
| 250 | return { | 255 | return { | ||
| 251 | supported: false, | 256 | supported: false, | ||
| 252 | smooth: false, | 257 | smooth: false, | ||
| @@ -264,7 +269,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 264 | const preferredCodecs = ` + preferredCodecsJS + `; | 269 | const preferredCodecs = ` + preferredCodecsJS + `; | ||
| 265 | for (const preferred of preferredCodecs) { | 270 | for (const preferred of preferredCodecs) { | ||
| 266 | if (contentType.toLowerCase().includes(preferred)) { | 271 | if (contentType.toLowerCase().includes(preferred)) { | ||
| 267 | - | console.log('[dumber-codec] Boosting preferred codec:', contentType); | 272 | + | log('Boosting preferred codec: ' + contentType); |
| 268 | 273 | ||||
| 269 | // Resolution-aware capability reporting for AV1 | 274 | // Resolution-aware capability reporting for AV1 | ||
| 270 | if (preferred === 'av01' || preferred === 'av1') { | 275 | if (preferred === 'av01' || preferred === 'av1') { | ||
| @@ -328,7 +333,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 328 | 333 | ||||
| 329 | // YouTube-specific codec forcing | 334 | // YouTube-specific codec forcing | ||
| 330 | if (location.hostname.includes('youtube.com')) { | 335 | if (location.hostname.includes('youtube.com')) { | ||
| 331 | - | console.log('[dumber-codec] Applying YouTube codec preferences'); | 336 | + | log('Applying YouTube codec preferences'); |
| 332 | 337 | ||||
| 333 | // Set YouTube's localStorage preferences for smart AV1 usage | 338 | // Set YouTube's localStorage preferences for smart AV1 usage | ||
| 334 | // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res | 339 | // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res | ||
| @@ -340,7 +345,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 340 | writable: false, | 345 | writable: false, | ||
| 341 | configurable: false | 346 | configurable: false | ||
| 342 | }); | 347 | }); | ||
| 343 | - | console.log('[dumber-codec] YouTube: Set smart AV1 localStorage preference (2048)'); | 348 | + | log('YouTube: Set smart AV1 localStorage preference (2048)'); |
| 344 | } catch (e) { | 349 | } catch (e) { | ||
| 345 | console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e); | 350 | console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e); | ||
| 346 | } | 351 | } | ||
| @@ -358,7 +363,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 358 | js.WriteString(` | 363 | js.WriteString(` | ||
| 359 | // Force AV1 if enabled | 364 | // Force AV1 if enabled | ||
| 360 | player.config.args.preferred_codecs = 'av01'; | 365 | player.config.args.preferred_codecs = 'av01'; | ||
| 361 | - | console.log('[dumber-codec] YouTube: Forced AV1 codec');`) | 366 | + | log('YouTube: Forced AV1 codec');`) |
| 362 | } | 367 | } | ||
| 363 | 368 | ||||
| 364 | if hasPreferredCodecs { | 369 | if hasPreferredCodecs { | ||
| @@ -375,7 +380,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 375 | } | 380 | } | ||
| 376 | }).join(','); | 381 | }).join(','); | ||
| 377 | player.config.args.preferred_codecs = codecOrder; | 382 | player.config.args.preferred_codecs = codecOrder; | ||
| 378 | - | console.log('[dumber-codec] YouTube: Set codec order:', codecOrder);`) | 383 | + | log('YouTube: Set codec order: ' + codecOrder);`) |
| 379 | } | 384 | } | ||
| 380 | 385 | ||||
| 381 | js.WriteString(` | 386 | js.WriteString(` | ||
| @@ -388,7 +393,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 388 | const originalFetch = window.fetch; | 393 | const originalFetch = window.fetch; | ||
| 389 | window.fetch = function(input, init) { | 394 | window.fetch = function(input, init) { | ||
| 390 | if (typeof input === 'string' && input.includes('/youtubei/v1/player')) { | 395 | if (typeof input === 'string' && input.includes('/youtubei/v1/player')) { | ||
| 391 | - | console.log('[dumber-codec] YouTube: Intercepting player request');`) | 396 | + | log('YouTube: Intercepting player request');`) |
| 392 | 397 | ||||
| 393 | if hasPreferredCodecs { | 398 | if hasPreferredCodecs { | ||
| 394 | js.WriteString(` | 399 | js.WriteString(` | ||
| @@ -422,7 +427,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 422 | 427 | ||||
| 423 | // Manipulate streaming data if present | 428 | // Manipulate streaming data if present | ||
| 424 | if (data.streamingData && data.streamingData.formats) { | 429 | if (data.streamingData && data.streamingData.formats) { | ||
| 425 | - | console.log('[dumber-codec] YouTube: Processing format manifest'); | 430 | + | log('YouTube: Processing format manifest'); |
| 426 | 431 | ||||
| 427 | // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher | 432 | // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher | ||
| 428 | data.streamingData.formats.sort((a, b) => { | 433 | data.streamingData.formats.sort((a, b) => { | ||
| @@ -447,7 +452,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 447 | return 0; | 452 | return 0; | ||
| 448 | }); | 453 | }); | ||
| 449 | 454 | ||||
| 450 | - | console.log('[dumber-codec] YouTube: Reordered formats for optimal codec selection'); | 455 | + | log('YouTube: Reordered formats for optimal codec selection'); |
| 451 | } | 456 | } | ||
| 452 | 457 | ||||
| 453 | return new Response(JSON.stringify(data), { | 458 | return new Response(JSON.stringify(data), { | ||
| @@ -473,7 +478,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 473 | 478 | ||||
| 474 | // Twitch: No codec interference - let Twitch handle codec selection natively | 479 | // Twitch: No codec interference - let Twitch handle codec selection natively | ||
| 475 | if (location.hostname.includes('twitch.tv')) { | 480 | if (location.hostname.includes('twitch.tv')) { | ||
| 476 | - | console.log('[dumber-codec] Detected Twitch domain - no codec interference for stability'); | 481 | + | log('Detected Twitch domain - no codec interference for stability'); |
| 477 | // Twitch codec control completely removed to prevent theater/fullscreen freezing | 482 | // Twitch codec control completely removed to prevent theater/fullscreen freezing | ||
| 478 | } | 483 | } | ||
| 479 | 484 | ||||
| @@ -483,11 +488,11 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 483 | videos.forEach(video => { | 488 | videos.forEach(video => { | ||
| 484 | if (!video.__codecMonitored) { | 489 | if (!video.__codecMonitored) { | ||
| 485 | video.__codecMonitored = true; | 490 | video.__codecMonitored = true; | ||
| 486 | - | console.log('[dumber-codec] Found video element, adding passive monitoring'); | 491 | + | log('Found video element, adding passive monitoring'); |
| 487 | 492 | ||||
| 488 | // Passive codec detection only | 493 | // Passive codec detection only | ||
| 489 | video.addEventListener('canplay', function() { | 494 | video.addEventListener('canplay', function() { | ||
| 490 | - | console.log('[dumber-codec] Video can play - resolution:', this.videoWidth + 'x' + this.videoHeight); | 495 | + | log('Video can play - resolution: ' + this.videoWidth + 'x' + this.videoHeight); |
| 491 | }, { passive: true, once: true }); | 496 | }, { passive: true, once: true }); | ||
| 492 | } | 497 | } | ||
| 493 | }); | 498 | }); | ||
| @@ -495,10 +500,21 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 495 | 500 | ||||
| 496 | // Monitor for video elements periodically without overriding createElement | 501 | // Monitor for video elements periodically without overriding createElement | ||
| 497 | const videoObserver = new MutationObserver(monitorExistingVideos); | 502 | const videoObserver = new MutationObserver(monitorExistingVideos); | ||
| 498 | - | videoObserver.observe(document.body, { childList: true, subtree: true }); | 503 | + | |
| 499 | - | monitorExistingVideos(); // Check existing videos | 504 | + | // Wait for document.body to be available before observing |
| 505 | + | function startVideoObserver() { | |||
| 506 | + | if (document.body) { | |||
| 507 | + | videoObserver.observe(document.body, { childList: true, subtree: true }); | |||
| 508 | + | monitorExistingVideos(); // Check existing videos | |||
| 509 | + | log('Video observer started'); | |||
| 510 | + | } else { | |||
| 511 | + | // Retry after a short delay if body not ready | |||
| 512 | + | setTimeout(startVideoObserver, 50); | |||
| 513 | + | } | |||
| 514 | + | } | |||
| 515 | + | startVideoObserver(); | |||
| 500 | 516 | ||||
| 501 | - | console.log('[dumber-codec] Codec control initialization complete'); | 517 | + | log('Codec control initialization complete'); |
| 502 | })();`) | 518 | })();`) | ||
| 503 | 519 | ||||
| 504 | return js.String() | 520 | return js.String() | ||
M pkg/webkit/keyboard_cgo.go +3 -0
| @@ -153,6 +153,8 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 153 | 153 | keyName = "r" | |
| 154 | 154 | case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L): | |
| 155 | 155 | keyName = "l" | |
| 156 | + | case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F): | |
| 157 | + | keyName = "f" | |
| 156 | 158 | default: | |
| 157 | 159 | // If Control held, log unknown keyval for diagnostics | |
| 158 | 160 | if ctrl { | |
| @@ -305,6 +307,7 @@ s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}'; | |||
| 305 | 307 | _ = vw.InjectScript(js) | |
| 306 | 308 | } | |
| 307 | 309 | ||
| 310 | + | ||
| 308 | 311 | //export goOnUcmMessage | |
| 309 | 312 | func goOnUcmMessage(id C.ulong, json *C.char) { | |
| 310 | 313 | uid := uintptr(id) | |
| @@ -153,6 +153,8 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 153 | keyName = "r" | 153 | keyName = "r" | ||
| 154 | case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L): | 154 | case uint(C.GDK_KEY_l), uint(C.GDK_KEY_L): | ||
| 155 | keyName = "l" | 155 | keyName = "l" | ||
| 156 | + | case uint(C.GDK_KEY_f), uint(C.GDK_KEY_F): | |||
| 157 | + | keyName = "f" | |||
| 156 | default: | 158 | default: | ||
| 157 | // If Control held, log unknown keyval for diagnostics | 159 | // If Control held, log unknown keyval for diagnostics | ||
| 158 | if ctrl { | 160 | if ctrl { | ||
| @@ -305,6 +307,7 @@ s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}'; | |||||
| 305 | _ = vw.InjectScript(js) | 307 | _ = vw.InjectScript(js) | ||
| 306 | } | 308 | } | ||
| 307 | 309 | ||||
| 310 | + | ||||
| 308 | //export goOnUcmMessage | 311 | //export goOnUcmMessage | ||
| 309 | func goOnUcmMessage(id C.ulong, json *C.char) { | 312 | func goOnUcmMessage(id C.ulong, json *C.char) { | ||
| 310 | uid := uintptr(id) | 313 | uid := uintptr(id) | ||
M pkg/webkit/settings.go +3 -0
| @@ -3,6 +3,9 @@ package webkit | |||
| 3 | 3 | // Config holds initialization settings for the WebKit-based browser components. | |
| 4 | 4 | // This will map to WebKit2GTK and GTK settings in the real implementation. | |
| 5 | 5 | type Config struct { | |
| 6 | + | // Assets provides access to embedded GUI files | |
| 7 | + | Assets interface{ ReadFile(string) ([]byte, error) } | |
| 8 | + | ||
| 6 | 9 | // InitialURL is the first URL to load when creating a WebView. | |
| 7 | 10 | InitialURL string | |
| 8 | 11 | ||
| @@ -3,6 +3,9 @@ package webkit | |||||
| 3 | // Config holds initialization settings for the WebKit-based browser components. | 3 | // Config holds initialization settings for the WebKit-based browser components. | ||
| 4 | // This will map to WebKit2GTK and GTK settings in the real implementation. | 4 | // This will map to WebKit2GTK and GTK settings in the real implementation. | ||
| 5 | type Config struct { | 5 | type Config struct { | ||
| 6 | + | // Assets provides access to embedded GUI files | |||
| 7 | + | Assets interface{ ReadFile(string) ([]byte, error) } | |||
| 8 | + | ||||
| 6 | // InitialURL is the first URL to load when creating a WebView. | 9 | // InitialURL is the first URL to load when creating a WebView. | ||
| 7 | InitialURL string | 10 | InitialURL string | ||
| 8 | 11 | ||||
A pkg/webkit/ucm_js_cgo.go.deprecated +335 -0
| @@ -0,0 +1,335 @@ | |||
| 1 | + | //go:build webkit_cgo | |
| 2 | + | ||
| 3 | + | package webkit | |
| 4 | + | ||
| 5 | + | // getOmniboxScript returns the injected omnibox/find component script. | |
| 6 | + | // Kept in a cgo-tagged file to ensure availability when referenced | |
| 7 | + | // from other cgo files in this package. | |
| 8 | + | func getOmniboxScript() string { | |
| 9 | + | return `(() => { | |
| 10 | + | try { | |
| 11 | + | if (window.__dumber_omnibox_loaded) return; // idempotent | |
| 12 | + | window.__dumber_omnibox_loaded = true; | |
| 13 | + | ||
| 14 | + | const MAX_MATCHES = 2000; | |
| 15 | + | const H = { | |
| 16 | + | el: null, | |
| 17 | + | box: null, | |
| 18 | + | input: null, | |
| 19 | + | list: null, | |
| 20 | + | visible: false, | |
| 21 | + | mode: 'omnibox', // 'omnibox' | 'find' | |
| 22 | + | suggestions: [], | |
| 23 | + | matches: [], // {el, context} | |
| 24 | + | selectedIndex: -1, | |
| 25 | + | activeIndex: -1, | |
| 26 | + | debounceTimer: 0, | |
| 27 | + | highlightNodes: [], | |
| 28 | + | faded: false, | |
| 29 | + | prevOverflow: '', | |
| 30 | + | post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} }, | |
| 31 | + | render(){ | |
| 32 | + | // Performance optimization: elements are pre-mounted, just show/hide | |
| 33 | + | H.el.style.display = H.visible ? 'block' : 'none'; | |
| 34 | + | if (!H.visible) return; | |
| 35 | + | H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…'; | |
| 36 | + | H.input.focus(); | |
| 37 | + | }, | |
| 38 | + | setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); }, | |
| 39 | + | open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } }, | |
| 40 | + | close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); }, | |
| 41 | + | toggle(v){ | |
| 42 | + | const newState = (typeof v==='boolean')? v : !H.visible; | |
| 43 | + | if (newState && !H.visible) { | |
| 44 | + | // Disable page scrolling while overlay is visible | |
| 45 | + | H.prevOverflow = document.documentElement.style.overflow; | |
| 46 | + | document.documentElement.style.overflow = 'hidden'; | |
| 47 | + | } else if (!newState && H.visible) { | |
| 48 | + | // Restore page scrolling on close | |
| 49 | + | document.documentElement.style.overflow = H.prevOverflow || ''; | |
| 50 | + | H.prevOverflow = ''; | |
| 51 | + | } | |
| 52 | + | H.visible = newState; | |
| 53 | + | H.render(); | |
| 54 | + | }, | |
| 55 | + | setFaded(v){ | |
| 56 | + | H.faded = !!v; | |
| 57 | + | if (!H.box || !H.input || !H.list) return; | |
| 58 | + | if (H.faded) { | |
| 59 | + | H.box.style.background = 'rgba(27,27,27,0.25)'; | |
| 60 | + | H.box.style.backdropFilter = 'blur(2px) saturate(110%)'; | |
| 61 | + | H.box.style.webkitBackdropFilter = 'blur(2px) saturate(110%)'; | |
| 62 | + | H.input.style.background = 'rgba(18,18,18,0.35)'; | |
| 63 | + | H.input.style.color = '#eee'; | |
| 64 | + | } else { | |
| 65 | + | H.box.style.background = '#1b1b1b'; | |
| 66 | + | H.box.style.backdropFilter = ''; | |
| 67 | + | H.box.style.webkitBackdropFilter = ''; | |
| 68 | + | H.input.style.background = '#121212'; | |
| 69 | + | H.input.style.color = '#eee'; | |
| 70 | + | } | |
| 71 | + | }, | |
| 72 | + | scrollListToSelection(){ | |
| 73 | + | if (!H.list) return; | |
| 74 | + | let idx = H.selectedIndex|0; | |
| 75 | + | if (idx < 0) return; | |
| 76 | + | // Account for header in find mode | |
| 77 | + | const childIdx = (H.mode==='find' ? 1 : 0) + idx; | |
| 78 | + | const el = H.list.children[childIdx]; | |
| 79 | + | if (el && el.scrollIntoView) { | |
| 80 | + | try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); } | |
| 81 | + | } | |
| 82 | + | }, | |
| 83 | + | mount(){ | |
| 84 | + | const root = document.createElement('div'); | |
| 85 | + | root.id = 'dumber-omnibox-root'; | |
| 86 | + | root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;'; | |
| 87 | + | const box = document.createElement('div'); | |
| 88 | + | box.style.cssText = 'width:min(90vw, 720px);margin:8vh auto;padding:8px 12px;background:#1b1b1b;color:#eee;border:1px solid #444;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.6);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;'; | |
| 89 | + | const input = document.createElement('input'); | |
| 90 | + | input.type = 'text'; | |
| 91 | + | input.placeholder = 'Type URL or search…'; | |
| 92 | + | input.style.cssText = 'width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;'; | |
| 93 | + | const list = document.createElement('div'); | |
| 94 | + | list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;'; | |
| 95 | + | const style = document.createElement('style'); | |
| 96 | + | style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}.dumber-find-active{background:#ff9800 !important;color:#000;box-shadow:0 0 0 1px #b36b00 inset}'; | |
| 97 | + | document.documentElement.appendChild(style); | |
| 98 | + | box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root); | |
| 99 | + | H.box = box; | |
| 100 | + | // Dynamic style updater for responsive behavior | |
| 101 | + | const updateStyles = () => { | |
| 102 | + | const vw = window.innerWidth; | |
| 103 | + | const isSmall = vw < 640; | |
| 104 | + | const isLarge = vw > 1920; | |
| 105 | + | ||
| 106 | + | if (box) { | |
| 107 | + | box.style.width = isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)'; | |
| 108 | + | box.style.padding = isSmall ? '6px 8px' : '8px 12px'; | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | if (input) { | |
| 112 | + | input.style.fontSize = isSmall ? '14px' : isLarge ? '17px' : '16px'; | |
| 113 | + | input.style.padding = isSmall ? '8px 10px' : '10px 12px'; | |
| 114 | + | input.style.boxSizing = 'border-box'; | |
| 115 | + | } | |
| 116 | + | }; | |
| 117 | + | updateStyles(); | |
| 118 | + | window.addEventListener('resize', updateStyles); | |
| 119 | + | // Click outside the box closes the overlay | |
| 120 | + | root.addEventListener('mousedown', (e)=>{ | |
| 121 | + | const tgt = e.target; | |
| 122 | + | if (tgt && box && !box.contains(tgt)) { | |
| 123 | + | e.preventDefault(); | |
| 124 | + | e.stopPropagation(); | |
| 125 | + | H.close(); | |
| 126 | + | } | |
| 127 | + | }, true); | |
| 128 | + | box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true); | |
| 129 | + | // Focus and select input on hover for quick typing | |
| 130 | + | input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } }); | |
| 131 | + | root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } }); | |
| 132 | + | // Clear fading when user goes back to input typing | |
| 133 | + | input.addEventListener('focus', ()=> H.setFaded(false)); | |
| 134 | + | input.addEventListener('keydown', (e)=>{ | |
| 135 | + | if (e.key === 'Escape'){ H.close(); } | |
| 136 | + | else if (H.mode==='omnibox' && e.key === 'Enter'){ | |
| 137 | + | const pick = H.suggestions && H.suggestions[H.selectedIndex|0]; | |
| 138 | + | const v = (pick && pick.url) || input.value || ''; | |
| 139 | + | if (v) H.post({type:'navigate', url:v}); | |
| 140 | + | H.toggle(false); | |
| 141 | + | } else if (H.mode==='find' && e.key === 'Enter'){ | |
| 142 | + | e.preventDefault(); | |
| 143 | + | if (e.shiftKey) { | |
| 144 | + | H.jump(-1); | |
| 145 | + | } else if (e.altKey) { | |
| 146 | + | // Center on current match but keep overlay open for further navigation | |
| 147 | + | H.revealSelection(); | |
| 148 | + | } else { | |
| 149 | + | // Default: center and close | |
| 150 | + | H.revealSelection(); | |
| 151 | + | H.close(); | |
| 152 | + | } | |
| 153 | + | } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){ | |
| 154 | + | e.preventDefault(); e.stopPropagation(); | |
| 155 | + | const n = (H.mode==='find'? H.matches.length : H.suggestions.length); | |
| 156 | + | if (n){ | |
| 157 | + | H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1); | |
| 158 | + | if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0; | |
| 159 | + | H.paintList(); | |
| 160 | + | H.scrollListToSelection(); | |
| 161 | + | // Fade overlay while navigating results; also reveal active match in find mode | |
| 162 | + | H.setFaded(true); | |
| 163 | + | if (H.mode==='find') { H.revealSelection(); } | |
| 164 | + | } | |
| 165 | + | } else { | |
| 166 | + | // Any other key input should restore full opacity while editing | |
| 167 | + | H.setFaded(false); | |
| 168 | + | } | |
| 169 | + | }); | |
| 170 | + | input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); }); | |
| 171 | + | input.addEventListener('mousedown', ()=> H.setFaded(false)); | |
| 172 | + | input.addEventListener('click', ()=> H.setFaded(false)); | |
| 173 | + | H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList(); | |
| 174 | + | }, | |
| 175 | + | onInput(){ | |
| 176 | + | const q = H.input.value || ''; | |
| 177 | + | if (H.mode === 'omnibox'){ | |
| 178 | + | clearTimeout(H.debounceTimer); | |
| 179 | + | H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120); | |
| 180 | + | } else { | |
| 181 | + | H.find(q); | |
| 182 | + | } | |
| 183 | + | }, | |
| 184 | + | paintList(){ | |
| 185 | + | const list = H.list; if (!list) return; list.textContent = ''; | |
| 186 | + | ||
| 187 | + | // Show/hide separator based on content | |
| 188 | + | const hasContent = (H.mode==='omnibox' && H.suggestions.length > 0) || (H.mode==='find' && H.matches.length > 0); | |
| 189 | + | list.style.borderTop = hasContent ? '1px solid #333' : 'none'; | |
| 190 | + | ||
| 191 | + | if (H.mode==='omnibox'){ | |
| 192 | + | H.suggestions.forEach((s, i)=>{ | |
| 193 | + | const item = document.createElement('div'); | |
| 194 | + | item.style.cssText = 'padding:8px 10px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':''); | |
| 195 | + | // Favicon chip (wrapper ensures perfect circle sizing) | |
| 196 | + | const chip = document.createElement('div'); | |
| 197 | + | chip.style.cssText = 'flex:0 0 20px;width:20px;height:20px;border-radius:50%;background:#ccc;border:1px solid rgba(0,0,0,.12);box-shadow:0 1px 2px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;'; | |
| 198 | + | const icon = document.createElement('img'); | |
| 199 | + | icon.src = s.favicon || ''; | |
| 200 | + | icon.loading = 'lazy'; | |
| 201 | + | icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;'; | |
| 202 | + | icon.onerror = ()=>{ chip.style.display='none'; }; | |
| 203 | + | chip.appendChild(icon); | |
| 204 | + | // Text line: Domain | full path (one line, fade at end) | |
| 205 | + | let domain = '', path = ''; | |
| 206 | + | try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; } | |
| 207 | + | const text = document.createElement('div'); | |
| 208 | + | text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;'; | |
| 209 | + | // Apply gradient fade using mask for WebKit | |
| 210 | + | text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)'; | |
| 211 | + | text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)'; | |
| 212 | + | const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;'; | |
| 213 | + | const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;'; | |
| 214 | + | const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;'; | |
| 215 | + | text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl); | |
| 216 | + | item.appendChild(chip); item.appendChild(text); | |
| 217 | + | item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); }); | |
| 218 | + | item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); }); | |
| 219 | + | list.appendChild(item); | |
| 220 | + | }); | |
| 221 | + | } else { | |
| 222 | + | const total = H.matches.length; | |
| 223 | + | const header = document.createElement('div'); | |
| 224 | + | header.textContent = total ? total + ' matches' : 'No matches'; | |
| 225 | + | header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;'; | |
| 226 | + | list.appendChild(header); | |
| 227 | + | H.matches.forEach((m, i)=>{ | |
| 228 | + | const item = document.createElement('div'); | |
| 229 | + | item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':''); | |
| 230 | + | const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; | |
| 231 | + | item.appendChild(ctx); | |
| 232 | + | item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); }); | |
| 233 | + | item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); }); | |
| 234 | + | list.appendChild(item); | |
| 235 | + | }); | |
| 236 | + | } | |
| 237 | + | }, | |
| 238 | + | setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); }, | |
| 239 | + | // FIND MODE IMPLEMENTATION | |
| 240 | + | clearHighlights(){ | |
| 241 | + | try { | |
| 242 | + | H.highlightNodes.forEach(({span, text})=>{ | |
| 243 | + | const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize(); | |
| 244 | + | }); | |
| 245 | + | } catch(_){} | |
| 246 | + | H.highlightNodes = []; | |
| 247 | + | H.matches = []; | |
| 248 | + | H.selectedIndex = -1; | |
| 249 | + | H.activeIndex = -1; | |
| 250 | + | H.paintList(); | |
| 251 | + | }, | |
| 252 | + | find(q){ | |
| 253 | + | H.clearHighlights(); | |
| 254 | + | q = (q||'').trim(); if (!q) return; | |
| 255 | + | const body = document.body; if (!body) return; | |
| 256 | + | const root = document.getElementById('dumber-omnibox-root'); | |
| 257 | + | const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, { | |
| 258 | + | acceptNode(node){ | |
| 259 | + | if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; | |
| 260 | + | let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT; | |
| 261 | + | if (root && root.contains(el)) return NodeFilter.FILTER_REJECT; | |
| 262 | + | const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT; | |
| 263 | + | if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT; | |
| 264 | + | return NodeFilter.FILTER_ACCEPT; | |
| 265 | + | } | |
| 266 | + | }); | |
| 267 | + | const lc = q.toLowerCase(); | |
| 268 | + | while (walker.nextNode()){ | |
| 269 | + | const text = walker.currentNode; | |
| 270 | + | let s = text.nodeValue; let i = 0; | |
| 271 | + | while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) { | |
| 272 | + | const before = document.createTextNode(s.slice(0, i)); | |
| 273 | + | const match = document.createTextNode(s.slice(i, i+q.length)); | |
| 274 | + | const afterVal = s.slice(i+q.length); | |
| 275 | + | const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match); | |
| 276 | + | const parent = text.parentNode; if (!parent) break; | |
| 277 | + | parent.insertBefore(before, text); | |
| 278 | + | parent.insertBefore(span, text); | |
| 279 | + | text.nodeValue = afterVal; | |
| 280 | + | H.highlightNodes.push({span, text: match}); | |
| 281 | + | const leftCtx = before.nodeValue.slice(-30); | |
| 282 | + | let rightRaw = afterVal.slice(0, 30); | |
| 283 | + | const boundaryIdx = rightRaw.search(/[\.,;:\-]/); | |
| 284 | + | if (boundaryIdx !== -1) { | |
| 285 | + | rightRaw = rightRaw.slice(0, boundaryIdx+1); | |
| 286 | + | } | |
| 287 | + | const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim(); | |
| 288 | + | H.matches.push({el: span, context}); | |
| 289 | + | if (H.matches.length >= MAX_MATCHES) break; | |
| 290 | + | s = afterVal; | |
| 291 | + | } | |
| 292 | + | if (H.matches.length >= MAX_MATCHES) break; | |
| 293 | + | } | |
| 294 | + | H.selectedIndex = H.matches.length ? 0 : -1; | |
| 295 | + | H.paintList(); | |
| 296 | + | H.revealSelection(); | |
| 297 | + | }, | |
| 298 | + | revealSelection(){ | |
| 299 | + | const prev = H.matches[H.activeIndex|0]; | |
| 300 | + | if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); } | |
| 301 | + | const m = H.matches[H.selectedIndex|0]; if (!m) return; | |
| 302 | + | if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); } | |
| 303 | + | H.activeIndex = H.selectedIndex|0; | |
| 304 | + | try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); } | |
| 305 | + | }, | |
| 306 | + | jump(delta){ | |
| 307 | + | const n = H.matches.length; if (!n) return; | |
| 308 | + | H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1; | |
| 309 | + | H.paintList(); H.revealSelection(); | |
| 310 | + | } | |
| 311 | + | }; | |
| 312 | + | ||
| 313 | + | // Keyboard hooks | |
| 314 | + | window.addEventListener('keydown', (e)=>{ | |
| 315 | + | const mod = (e.ctrlKey||e.metaKey); | |
| 316 | + | if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); } | |
| 317 | + | if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); } | |
| 318 | + | }, true); | |
| 319 | + | ||
| 320 | + | // Public API for native | |
| 321 | + | window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr); | |
| 322 | + | window.__dumber_toggle = ()=> H.toggle(); | |
| 323 | + | window.__dumber_find_open = (q)=> H.open('find', q||''); | |
| 324 | + | window.__dumber_find_close = ()=> H.close(); | |
| 325 | + | window.__dumber_find_query = (q)=> { if (H.mode!=='find') H.setMode('find'); if (!H.visible) H.toggle(true); H.input.value = q||''; H.find(q||''); }; | |
| 326 | + | ||
| 327 | + | // Performance optimization: Pre-mount DOM elements immediately for instant show/hide | |
| 328 | + | if (document.readyState === 'loading') { | |
| 329 | + | document.addEventListener('DOMContentLoaded', ()=> H.mount()); | |
| 330 | + | } else { | |
| 331 | + | H.mount(); // DOM already ready | |
| 332 | + | } | |
| 333 | + | } catch (e) { /* no-op */ } | |
| 334 | + | })();` | |
| 335 | + | } | |
| @@ -0,0 +1,335 @@ | |||||
| 1 | + | //go:build webkit_cgo | |||
| 2 | + | ||||
| 3 | + | package webkit | |||
| 4 | + | ||||
| 5 | + | // getOmniboxScript returns the injected omnibox/find component script. | |||
| 6 | + | // Kept in a cgo-tagged file to ensure availability when referenced | |||
| 7 | + | // from other cgo files in this package. | |||
| 8 | + | func getOmniboxScript() string { | |||
| 9 | + | return `(() => { | |||
| 10 | + | try { | |||
| 11 | + | if (window.__dumber_omnibox_loaded) return; // idempotent | |||
| 12 | + | window.__dumber_omnibox_loaded = true; | |||
| 13 | + | ||||
| 14 | + | const MAX_MATCHES = 2000; | |||
| 15 | + | const H = { | |||
| 16 | + | el: null, | |||
| 17 | + | box: null, | |||
| 18 | + | input: null, | |||
| 19 | + | list: null, | |||
| 20 | + | visible: false, | |||
| 21 | + | mode: 'omnibox', // 'omnibox' | 'find' | |||
| 22 | + | suggestions: [], | |||
| 23 | + | matches: [], // {el, context} | |||
| 24 | + | selectedIndex: -1, | |||
| 25 | + | activeIndex: -1, | |||
| 26 | + | debounceTimer: 0, | |||
| 27 | + | highlightNodes: [], | |||
| 28 | + | faded: false, | |||
| 29 | + | prevOverflow: '', | |||
| 30 | + | post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} }, | |||
| 31 | + | render(){ | |||
| 32 | + | // Performance optimization: elements are pre-mounted, just show/hide | |||
| 33 | + | H.el.style.display = H.visible ? 'block' : 'none'; | |||
| 34 | + | if (!H.visible) return; | |||
| 35 | + | H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…'; | |||
| 36 | + | H.input.focus(); | |||
| 37 | + | }, | |||
| 38 | + | setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); }, | |||
| 39 | + | open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } }, | |||
| 40 | + | close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); }, | |||
| 41 | + | toggle(v){ | |||
| 42 | + | const newState = (typeof v==='boolean')? v : !H.visible; | |||
| 43 | + | if (newState && !H.visible) { | |||
| 44 | + | // Disable page scrolling while overlay is visible | |||
| 45 | + | H.prevOverflow = document.documentElement.style.overflow; | |||
| 46 | + | document.documentElement.style.overflow = 'hidden'; | |||
| 47 | + | } else if (!newState && H.visible) { | |||
| 48 | + | // Restore page scrolling on close | |||
| 49 | + | document.documentElement.style.overflow = H.prevOverflow || ''; | |||
| 50 | + | H.prevOverflow = ''; | |||
| 51 | + | } | |||
| 52 | + | H.visible = newState; | |||
| 53 | + | H.render(); | |||
| 54 | + | }, | |||
| 55 | + | setFaded(v){ | |||
| 56 | + | H.faded = !!v; | |||
| 57 | + | if (!H.box || !H.input || !H.list) return; | |||
| 58 | + | if (H.faded) { | |||
| 59 | + | H.box.style.background = 'rgba(27,27,27,0.25)'; | |||
| 60 | + | H.box.style.backdropFilter = 'blur(2px) saturate(110%)'; | |||
| 61 | + | H.box.style.webkitBackdropFilter = 'blur(2px) saturate(110%)'; | |||
| 62 | + | H.input.style.background = 'rgba(18,18,18,0.35)'; | |||
| 63 | + | H.input.style.color = '#eee'; | |||
| 64 | + | } else { | |||
| 65 | + | H.box.style.background = '#1b1b1b'; | |||
| 66 | + | H.box.style.backdropFilter = ''; | |||
| 67 | + | H.box.style.webkitBackdropFilter = ''; | |||
| 68 | + | H.input.style.background = '#121212'; | |||
| 69 | + | H.input.style.color = '#eee'; | |||
| 70 | + | } | |||
| 71 | + | }, | |||
| 72 | + | scrollListToSelection(){ | |||
| 73 | + | if (!H.list) return; | |||
| 74 | + | let idx = H.selectedIndex|0; | |||
| 75 | + | if (idx < 0) return; | |||
| 76 | + | // Account for header in find mode | |||
| 77 | + | const childIdx = (H.mode==='find' ? 1 : 0) + idx; | |||
| 78 | + | const el = H.list.children[childIdx]; | |||
| 79 | + | if (el && el.scrollIntoView) { | |||
| 80 | + | try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); } | |||
| 81 | + | } | |||
| 82 | + | }, | |||
| 83 | + | mount(){ | |||
| 84 | + | const root = document.createElement('div'); | |||
| 85 | + | root.id = 'dumber-omnibox-root'; | |||
| 86 | + | root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;'; | |||
| 87 | + | const box = document.createElement('div'); | |||
| 88 | + | box.style.cssText = 'width:min(90vw, 720px);margin:8vh auto;padding:8px 12px;background:#1b1b1b;color:#eee;border:1px solid #444;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.6);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;'; | |||
| 89 | + | const input = document.createElement('input'); | |||
| 90 | + | input.type = 'text'; | |||
| 91 | + | input.placeholder = 'Type URL or search…'; | |||
| 92 | + | input.style.cssText = 'width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;'; | |||
| 93 | + | const list = document.createElement('div'); | |||
| 94 | + | list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;'; | |||
| 95 | + | const style = document.createElement('style'); | |||
| 96 | + | style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}.dumber-find-active{background:#ff9800 !important;color:#000;box-shadow:0 0 0 1px #b36b00 inset}'; | |||
| 97 | + | document.documentElement.appendChild(style); | |||
| 98 | + | box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root); | |||
| 99 | + | H.box = box; | |||
| 100 | + | // Dynamic style updater for responsive behavior | |||
| 101 | + | const updateStyles = () => { | |||
| 102 | + | const vw = window.innerWidth; | |||
| 103 | + | const isSmall = vw < 640; | |||
| 104 | + | const isLarge = vw > 1920; | |||
| 105 | + | ||||
| 106 | + | if (box) { | |||
| 107 | + | box.style.width = isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)'; | |||
| 108 | + | box.style.padding = isSmall ? '6px 8px' : '8px 12px'; | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | if (input) { | |||
| 112 | + | input.style.fontSize = isSmall ? '14px' : isLarge ? '17px' : '16px'; | |||
| 113 | + | input.style.padding = isSmall ? '8px 10px' : '10px 12px'; | |||
| 114 | + | input.style.boxSizing = 'border-box'; | |||
| 115 | + | } | |||
| 116 | + | }; | |||
| 117 | + | updateStyles(); | |||
| 118 | + | window.addEventListener('resize', updateStyles); | |||
| 119 | + | // Click outside the box closes the overlay | |||
| 120 | + | root.addEventListener('mousedown', (e)=>{ | |||
| 121 | + | const tgt = e.target; | |||
| 122 | + | if (tgt && box && !box.contains(tgt)) { | |||
| 123 | + | e.preventDefault(); | |||
| 124 | + | e.stopPropagation(); | |||
| 125 | + | H.close(); | |||
| 126 | + | } | |||
| 127 | + | }, true); | |||
| 128 | + | box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true); | |||
| 129 | + | // Focus and select input on hover for quick typing | |||
| 130 | + | input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } }); | |||
| 131 | + | root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } }); | |||
| 132 | + | // Clear fading when user goes back to input typing | |||
| 133 | + | input.addEventListener('focus', ()=> H.setFaded(false)); | |||
| 134 | + | input.addEventListener('keydown', (e)=>{ | |||
| 135 | + | if (e.key === 'Escape'){ H.close(); } | |||
| 136 | + | else if (H.mode==='omnibox' && e.key === 'Enter'){ | |||
| 137 | + | const pick = H.suggestions && H.suggestions[H.selectedIndex|0]; | |||
| 138 | + | const v = (pick && pick.url) || input.value || ''; | |||
| 139 | + | if (v) H.post({type:'navigate', url:v}); | |||
| 140 | + | H.toggle(false); | |||
| 141 | + | } else if (H.mode==='find' && e.key === 'Enter'){ | |||
| 142 | + | e.preventDefault(); | |||
| 143 | + | if (e.shiftKey) { | |||
| 144 | + | H.jump(-1); | |||
| 145 | + | } else if (e.altKey) { | |||
| 146 | + | // Center on current match but keep overlay open for further navigation | |||
| 147 | + | H.revealSelection(); | |||
| 148 | + | } else { | |||
| 149 | + | // Default: center and close | |||
| 150 | + | H.revealSelection(); | |||
| 151 | + | H.close(); | |||
| 152 | + | } | |||
| 153 | + | } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){ | |||
| 154 | + | e.preventDefault(); e.stopPropagation(); | |||
| 155 | + | const n = (H.mode==='find'? H.matches.length : H.suggestions.length); | |||
| 156 | + | if (n){ | |||
| 157 | + | H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1); | |||
| 158 | + | if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0; | |||
| 159 | + | H.paintList(); | |||
| 160 | + | H.scrollListToSelection(); | |||
| 161 | + | // Fade overlay while navigating results; also reveal active match in find mode | |||
| 162 | + | H.setFaded(true); | |||
| 163 | + | if (H.mode==='find') { H.revealSelection(); } | |||
| 164 | + | } | |||
| 165 | + | } else { | |||
| 166 | + | // Any other key input should restore full opacity while editing | |||
| 167 | + | H.setFaded(false); | |||
| 168 | + | } | |||
| 169 | + | }); | |||
| 170 | + | input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); }); | |||
| 171 | + | input.addEventListener('mousedown', ()=> H.setFaded(false)); | |||
| 172 | + | input.addEventListener('click', ()=> H.setFaded(false)); | |||
| 173 | + | H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList(); | |||
| 174 | + | }, | |||
| 175 | + | onInput(){ | |||
| 176 | + | const q = H.input.value || ''; | |||
| 177 | + | if (H.mode === 'omnibox'){ | |||
| 178 | + | clearTimeout(H.debounceTimer); | |||
| 179 | + | H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120); | |||
| 180 | + | } else { | |||
| 181 | + | H.find(q); | |||
| 182 | + | } | |||
| 183 | + | }, | |||
| 184 | + | paintList(){ | |||
| 185 | + | const list = H.list; if (!list) return; list.textContent = ''; | |||
| 186 | + | ||||
| 187 | + | // Show/hide separator based on content | |||
| 188 | + | const hasContent = (H.mode==='omnibox' && H.suggestions.length > 0) || (H.mode==='find' && H.matches.length > 0); | |||
| 189 | + | list.style.borderTop = hasContent ? '1px solid #333' : 'none'; | |||
| 190 | + | ||||
| 191 | + | if (H.mode==='omnibox'){ | |||
| 192 | + | H.suggestions.forEach((s, i)=>{ | |||
| 193 | + | const item = document.createElement('div'); | |||
| 194 | + | item.style.cssText = 'padding:8px 10px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':''); | |||
| 195 | + | // Favicon chip (wrapper ensures perfect circle sizing) | |||
| 196 | + | const chip = document.createElement('div'); | |||
| 197 | + | chip.style.cssText = 'flex:0 0 20px;width:20px;height:20px;border-radius:50%;background:#ccc;border:1px solid rgba(0,0,0,.12);box-shadow:0 1px 2px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;'; | |||
| 198 | + | const icon = document.createElement('img'); | |||
| 199 | + | icon.src = s.favicon || ''; | |||
| 200 | + | icon.loading = 'lazy'; | |||
| 201 | + | icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;'; | |||
| 202 | + | icon.onerror = ()=>{ chip.style.display='none'; }; | |||
| 203 | + | chip.appendChild(icon); | |||
| 204 | + | // Text line: Domain | full path (one line, fade at end) | |||
| 205 | + | let domain = '', path = ''; | |||
| 206 | + | try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; } | |||
| 207 | + | const text = document.createElement('div'); | |||
| 208 | + | text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;'; | |||
| 209 | + | // Apply gradient fade using mask for WebKit | |||
| 210 | + | text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)'; | |||
| 211 | + | text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)'; | |||
| 212 | + | const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;'; | |||
| 213 | + | const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;'; | |||
| 214 | + | const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;'; | |||
| 215 | + | text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl); | |||
| 216 | + | item.appendChild(chip); item.appendChild(text); | |||
| 217 | + | item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); }); | |||
| 218 | + | item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); }); | |||
| 219 | + | list.appendChild(item); | |||
| 220 | + | }); | |||
| 221 | + | } else { | |||
| 222 | + | const total = H.matches.length; | |||
| 223 | + | const header = document.createElement('div'); | |||
| 224 | + | header.textContent = total ? total + ' matches' : 'No matches'; | |||
| 225 | + | header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;'; | |||
| 226 | + | list.appendChild(header); | |||
| 227 | + | H.matches.forEach((m, i)=>{ | |||
| 228 | + | const item = document.createElement('div'); | |||
| 229 | + | item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':''); | |||
| 230 | + | const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; | |||
| 231 | + | item.appendChild(ctx); | |||
| 232 | + | item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); }); | |||
| 233 | + | item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); }); | |||
| 234 | + | list.appendChild(item); | |||
| 235 | + | }); | |||
| 236 | + | } | |||
| 237 | + | }, | |||
| 238 | + | setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); }, | |||
| 239 | + | // FIND MODE IMPLEMENTATION | |||
| 240 | + | clearHighlights(){ | |||
| 241 | + | try { | |||
| 242 | + | H.highlightNodes.forEach(({span, text})=>{ | |||
| 243 | + | const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize(); | |||
| 244 | + | }); | |||
| 245 | + | } catch(_){} | |||
| 246 | + | H.highlightNodes = []; | |||
| 247 | + | H.matches = []; | |||
| 248 | + | H.selectedIndex = -1; | |||
| 249 | + | H.activeIndex = -1; | |||
| 250 | + | H.paintList(); | |||
| 251 | + | }, | |||
| 252 | + | find(q){ | |||
| 253 | + | H.clearHighlights(); | |||
| 254 | + | q = (q||'').trim(); if (!q) return; | |||
| 255 | + | const body = document.body; if (!body) return; | |||
| 256 | + | const root = document.getElementById('dumber-omnibox-root'); | |||
| 257 | + | const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, { | |||
| 258 | + | acceptNode(node){ | |||
| 259 | + | if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; | |||
| 260 | + | let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT; | |||
| 261 | + | if (root && root.contains(el)) return NodeFilter.FILTER_REJECT; | |||
| 262 | + | const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT; | |||
| 263 | + | if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT; | |||
| 264 | + | return NodeFilter.FILTER_ACCEPT; | |||
| 265 | + | } | |||
| 266 | + | }); | |||
| 267 | + | const lc = q.toLowerCase(); | |||
| 268 | + | while (walker.nextNode()){ | |||
| 269 | + | const text = walker.currentNode; | |||
| 270 | + | let s = text.nodeValue; let i = 0; | |||
| 271 | + | while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) { | |||
| 272 | + | const before = document.createTextNode(s.slice(0, i)); | |||
| 273 | + | const match = document.createTextNode(s.slice(i, i+q.length)); | |||
| 274 | + | const afterVal = s.slice(i+q.length); | |||
| 275 | + | const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match); | |||
| 276 | + | const parent = text.parentNode; if (!parent) break; | |||
| 277 | + | parent.insertBefore(before, text); | |||
| 278 | + | parent.insertBefore(span, text); | |||
| 279 | + | text.nodeValue = afterVal; | |||
| 280 | + | H.highlightNodes.push({span, text: match}); | |||
| 281 | + | const leftCtx = before.nodeValue.slice(-30); | |||
| 282 | + | let rightRaw = afterVal.slice(0, 30); | |||
| 283 | + | const boundaryIdx = rightRaw.search(/[\.,;:\-]/); | |||
| 284 | + | if (boundaryIdx !== -1) { | |||
| 285 | + | rightRaw = rightRaw.slice(0, boundaryIdx+1); | |||
| 286 | + | } | |||
| 287 | + | const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim(); | |||
| 288 | + | H.matches.push({el: span, context}); | |||
| 289 | + | if (H.matches.length >= MAX_MATCHES) break; | |||
| 290 | + | s = afterVal; | |||
| 291 | + | } | |||
| 292 | + | if (H.matches.length >= MAX_MATCHES) break; | |||
| 293 | + | } | |||
| 294 | + | H.selectedIndex = H.matches.length ? 0 : -1; | |||
| 295 | + | H.paintList(); | |||
| 296 | + | H.revealSelection(); | |||
| 297 | + | }, | |||
| 298 | + | revealSelection(){ | |||
| 299 | + | const prev = H.matches[H.activeIndex|0]; | |||
| 300 | + | if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); } | |||
| 301 | + | const m = H.matches[H.selectedIndex|0]; if (!m) return; | |||
| 302 | + | if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); } | |||
| 303 | + | H.activeIndex = H.selectedIndex|0; | |||
| 304 | + | try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); } | |||
| 305 | + | }, | |||
| 306 | + | jump(delta){ | |||
| 307 | + | const n = H.matches.length; if (!n) return; | |||
| 308 | + | H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1; | |||
| 309 | + | H.paintList(); H.revealSelection(); | |||
| 310 | + | } | |||
| 311 | + | }; | |||
| 312 | + | ||||
| 313 | + | // Keyboard hooks | |||
| 314 | + | window.addEventListener('keydown', (e)=>{ | |||
| 315 | + | const mod = (e.ctrlKey||e.metaKey); | |||
| 316 | + | if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); } | |||
| 317 | + | if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); } | |||
| 318 | + | }, true); | |||
| 319 | + | ||||
| 320 | + | // Public API for native | |||
| 321 | + | window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr); | |||
| 322 | + | window.__dumber_toggle = ()=> H.toggle(); | |||
| 323 | + | window.__dumber_find_open = (q)=> H.open('find', q||''); | |||
| 324 | + | window.__dumber_find_close = ()=> H.close(); | |||
| 325 | + | window.__dumber_find_query = (q)=> { if (H.mode!=='find') H.setMode('find'); if (!H.visible) H.toggle(true); H.input.value = q||''; H.find(q||''); }; | |||
| 326 | + | ||||
| 327 | + | // Performance optimization: Pre-mount DOM elements immediately for instant show/hide | |||
| 328 | + | if (document.readyState === 'loading') { | |||
| 329 | + | document.addEventListener('DOMContentLoaded', ()=> H.mount()); | |||
| 330 | + | } else { | |||
| 331 | + | H.mount(); // DOM already ready | |||
| 332 | + | } | |||
| 333 | + | } catch (e) { /* no-op */ } | |||
| 334 | + | })();` | |||
| 335 | + | } | |||
M pkg/webkit/webview.go +15 -0
| @@ -140,3 +140,18 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||
| 140 | 140 | w.zoomHandler(level) | |
| 141 | 141 | } | |
| 142 | 142 | } | |
| 143 | + | ||
| 144 | + | // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub) | |
| 145 | + | func (w *WebView) InitializeContentBlocking(filterManager interface{}) error { | |
| 146 | + | return ErrNotImplemented | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // OnNavigate sets up domain-specific cosmetic filtering on navigation (stub) | |
| 150 | + | func (w *WebView) OnNavigate(url string, filterManager interface{}) { | |
| 151 | + | // No-op in stub | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | // UpdateContentFilters updates the content filters dynamically (stub) | |
| 155 | + | func (w *WebView) UpdateContentFilters(filterManager interface{}) error { | |
| 156 | + | return ErrNotImplemented | |
| 157 | + | } | |
| @@ -140,3 +140,18 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||||
| 140 | w.zoomHandler(level) | 140 | w.zoomHandler(level) | ||
| 141 | } | 141 | } | ||
| 142 | } | 142 | } | ||
| 143 | + | ||||
| 144 | + | // InitializeContentBlocking initializes WebKit content blocking with filter manager (stub) | |||
| 145 | + | func (w *WebView) InitializeContentBlocking(filterManager interface{}) error { | |||
| 146 | + | return ErrNotImplemented | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // OnNavigate sets up domain-specific cosmetic filtering on navigation (stub) | |||
| 150 | + | func (w *WebView) OnNavigate(url string, filterManager interface{}) { | |||
| 151 | + | // No-op in stub | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | // UpdateContentFilters updates the content filters dynamically (stub) | |||
| 155 | + | func (w *WebView) UpdateContentFilters(filterManager interface{}) error { | |||
| 156 | + | return ErrNotImplemented | |||
| 157 | + | } | |||
M pkg/webkit/webview_cgo.go +59 -24
| @@ -948,11 +948,17 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 948 | 948 | _ = v.SetZoom(nz) | |
| 949 | 949 | }) | |
| 950 | 950 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | |
| 951 | - | // Find in page (Ctrl/Cmd+F) reuses omnibox component in find mode | |
| 952 | - | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { _ = v.OpenFind("") }) | |
| 953 | - | // Navigation with Alt+Arrow keys | |
| 954 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() }) | |
| 955 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() }) | |
| 951 | + | // Find in page (Ctrl/Cmd+F): use new keyboard service bridge | |
| 952 | + | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { | |
| 953 | + | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')") | |
| 954 | + | }) | |
| 955 | + | // Navigation with Alt+Arrow keys: use new keyboard service bridge | |
| 956 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { | |
| 957 | + | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowleft')") | |
| 958 | + | }) | |
| 959 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { | |
| 960 | + | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowright')") | |
| 961 | + | }) | |
| 956 | 962 | // Update window title when page title changes | |
| 957 | 963 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | |
| 958 | 964 | // Also dispatch title change to Go with view id | |
| @@ -1235,6 +1241,7 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||
| 1235 | 1241 | } | |
| 1236 | 1242 | } | |
| 1237 | 1243 | ||
| 1244 | + | ||
| 1238 | 1245 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | |
| 1239 | 1246 | func (w *WebView) enableUserContentManager(cfg *Config) { | |
| 1240 | 1247 | if w == nil || w.native == nil || w.native.ucm == nil { | |
| @@ -1248,6 +1255,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 1248 | 1255 | } | |
| 1249 | 1256 | C.free(unsafe.Pointer(cname)) | |
| 1250 | 1257 | ||
| 1258 | + | ||
| 1251 | 1259 | // Inject color-scheme preference script at document-start to inform sites of system theme | |
| 1252 | 1260 | preferDark := C.gtk_prefers_dark() != 0 | |
| 1253 | 1261 | if preferDark { | |
| @@ -1263,37 +1271,64 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 1263 | 1271 | } | |
| 1264 | 1272 | cScheme := C.CString(schemeJS) | |
| 1265 | 1273 | defer C.free(unsafe.Pointer(cScheme)) | |
| 1266 | - | schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1274 | + | schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1267 | 1275 | if schemeScript != nil { | |
| 1268 | 1276 | C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript) | |
| 1269 | 1277 | C.webkit_user_script_unref(schemeScript) | |
| 1270 | 1278 | } | |
| 1271 | 1279 | ||
| 1272 | - | // Add user script at document-start (omnibox/find reusable component) | |
| 1273 | - | src := C.CString(getOmniboxScript()) | |
| 1274 | - | defer C.free(unsafe.Pointer(src)) | |
| 1275 | - | script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1276 | - | if script != nil { | |
| 1277 | - | C.webkit_user_content_manager_add_script(w.native.ucm, script) | |
| 1278 | - | C.webkit_user_script_unref(script) | |
| 1279 | - | log.Printf("[webkit] UCM omnibox script injected at document-start") | |
| 1280 | - | } | |
| 1280 | + | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) | |
| 1281 | + | if cfg != nil && cfg.Assets != nil { | |
| 1282 | + | log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js") | |
| 1283 | + | if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil { | |
| 1284 | + | guiScript := string(guiBytes) | |
| 1285 | + | log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes)) | |
| 1286 | + | ||
| 1287 | + | cGui := C.CString(guiScript) | |
| 1288 | + | defer C.free(unsafe.Pointer(cGui)) | |
| 1289 | + | ||
| 1290 | + | log.Printf("[webkit] Creating GUI user script for document-start injection in main world") | |
| 1291 | + | guiUserScript := C.webkit_user_script_new((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1292 | + | if guiUserScript != nil { | |
| 1293 | + | C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript) | |
| 1294 | + | C.webkit_user_script_unref(guiUserScript) | |
| 1295 | + | log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in main world") | |
| 1296 | + | } else { | |
| 1297 | + | log.Printf("[webkit] ERROR: Failed to create GUI user script") | |
| 1298 | + | } | |
| 1299 | + | } else { | |
| 1300 | + | log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err) | |
| 1301 | + | } | |
| 1281 | 1302 | ||
| 1282 | - | // Add user script at document-start (toast notification system) | |
| 1283 | - | toastSrc := C.CString(getToastScript()) | |
| 1284 | - | defer C.free(unsafe.Pointer(toastSrc)) | |
| 1285 | - | toastScript := C.webkit_user_script_new((*C.gchar)(toastSrc), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1286 | - | if toastScript != nil { | |
| 1287 | - | C.webkit_user_content_manager_add_script(w.native.ucm, toastScript) | |
| 1288 | - | C.webkit_user_script_unref(toastScript) | |
| 1289 | - | log.Printf("[webkit] UCM toast script injected at document-start") | |
| 1303 | + | // Add GUI CSS styles as user stylesheet using WebKit native API | |
| 1304 | + | if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil { | |
| 1305 | + | cssContent := string(cssBytes) | |
| 1306 | + | cCss := C.CString(cssContent) | |
| 1307 | + | defer C.free(unsafe.Pointer(cCss)) | |
| 1308 | + | ||
| 1309 | + | styleSheet := C.webkit_user_style_sheet_new( | |
| 1310 | + | (*C.gchar)(cCss), | |
| 1311 | + | C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, | |
| 1312 | + | C.WEBKIT_USER_STYLE_LEVEL_AUTHOR, | |
| 1313 | + | nil, // allow_list | |
| 1314 | + | nil, // block_list | |
| 1315 | + | ) | |
| 1316 | + | if styleSheet != nil { | |
| 1317 | + | C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet) | |
| 1318 | + | C.webkit_user_style_sheet_unref(styleSheet) | |
| 1319 | + | } else { | |
| 1320 | + | log.Printf("[webkit] ERROR: Failed to create GUI stylesheet") | |
| 1321 | + | } | |
| 1322 | + | } else { | |
| 1323 | + | log.Printf("[webkit] ERROR: Failed to load GUI CSS from assets: %v", err) | |
| 1324 | + | } | |
| 1290 | 1325 | } | |
| 1291 | 1326 | ||
| 1292 | 1327 | // Inject Wails runtime fetch interceptor for homepage bridging | |
| 1293 | 1328 | wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();` | |
| 1294 | 1329 | cWails := C.CString(wailsBridge) | |
| 1295 | 1330 | defer C.free(unsafe.Pointer(cWails)) | |
| 1296 | - | wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1331 | + | wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 1297 | 1332 | if wailsScript != nil { | |
| 1298 | 1333 | C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript) | |
| 1299 | 1334 | C.webkit_user_script_unref(wailsScript) | |
| @@ -948,11 +948,17 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 948 | _ = v.SetZoom(nz) | 948 | _ = v.SetZoom(nz) | ||
| 949 | }) | 949 | }) | ||
| 950 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | 950 | _ = v.RegisterKeyboardShortcut("cmdorctrl+0", func() { _ = v.SetZoom(1.0) }) | ||
| 951 | - | // Find in page (Ctrl/Cmd+F) reuses omnibox component in find mode | 951 | + | // Find in page (Ctrl/Cmd+F): use new keyboard service bridge |
| 952 | - | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { _ = v.OpenFind("") }) | 952 | + | _ = v.RegisterKeyboardShortcut("cmdorctrl+f", func() { |
| 953 | - | // Navigation with Alt+Arrow keys | 953 | + | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')") |
| 954 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() }) | 954 | + | }) |
| 955 | - | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() }) | 955 | + | // Navigation with Alt+Arrow keys: use new keyboard service bridge |
| 956 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { | |||
| 957 | + | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowleft')") | |||
| 958 | + | }) | |||
| 959 | + | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { | |||
| 960 | + | _ = v.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('alt+arrowright')") | |||
| 961 | + | }) | |||
| 956 | // Update window title when page title changes | 962 | // Update window title when page title changes | ||
| 957 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | 963 | C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win))) | ||
| 958 | // Also dispatch title change to Go with view id | 964 | // Also dispatch title change to Go with view id | ||
| @@ -1235,6 +1241,7 @@ func (w *WebView) dispatchZoomChanged(level float64) { | |||||
| 1235 | } | 1241 | } | ||
| 1236 | } | 1242 | } | ||
| 1237 | 1243 | ||||
| 1244 | + | ||||
| 1238 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | 1245 | // enableUserContentManager registers the 'dumber' message handler and injects the omnibox script. | ||
| 1239 | func (w *WebView) enableUserContentManager(cfg *Config) { | 1246 | func (w *WebView) enableUserContentManager(cfg *Config) { | ||
| 1240 | if w == nil || w.native == nil || w.native.ucm == nil { | 1247 | if w == nil || w.native == nil || w.native.ucm == nil { | ||
| @@ -1248,6 +1255,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 1248 | } | 1255 | } | ||
| 1249 | C.free(unsafe.Pointer(cname)) | 1256 | C.free(unsafe.Pointer(cname)) | ||
| 1250 | 1257 | ||||
| 1258 | + | ||||
| 1251 | // Inject color-scheme preference script at document-start to inform sites of system theme | 1259 | // Inject color-scheme preference script at document-start to inform sites of system theme | ||
| 1252 | preferDark := C.gtk_prefers_dark() != 0 | 1260 | preferDark := C.gtk_prefers_dark() != 0 | ||
| 1253 | if preferDark { | 1261 | if preferDark { | ||
| @@ -1263,37 +1271,64 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 1263 | } | 1271 | } | ||
| 1264 | cScheme := C.CString(schemeJS) | 1272 | cScheme := C.CString(schemeJS) | ||
| 1265 | defer C.free(unsafe.Pointer(cScheme)) | 1273 | defer C.free(unsafe.Pointer(cScheme)) | ||
| 1266 | - | schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | 1274 | + | schemeScript := C.webkit_user_script_new((*C.gchar)(cScheme), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) |
| 1267 | if schemeScript != nil { | 1275 | if schemeScript != nil { | ||
| 1268 | C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript) | 1276 | C.webkit_user_content_manager_add_script(w.native.ucm, schemeScript) | ||
| 1269 | C.webkit_user_script_unref(schemeScript) | 1277 | C.webkit_user_script_unref(schemeScript) | ||
| 1270 | } | 1278 | } | ||
| 1271 | 1279 | ||||
| 1272 | - | // Add user script at document-start (omnibox/find reusable component) | 1280 | + | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) |
| 1273 | - | src := C.CString(getOmniboxScript()) | 1281 | + | if cfg != nil && cfg.Assets != nil { |
| 1274 | - | defer C.free(unsafe.Pointer(src)) | 1282 | + | log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js") |
| 1275 | - | script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | 1283 | + | if guiBytes, err := cfg.Assets.ReadFile("assets/gui/gui.min.js"); err == nil { |
| 1276 | - | if script != nil { | 1284 | + | guiScript := string(guiBytes) |
| 1277 | - | C.webkit_user_content_manager_add_script(w.native.ucm, script) | 1285 | + | log.Printf("[webkit] GUI bundle loaded successfully, size: %d bytes", len(guiBytes)) |
| 1278 | - | C.webkit_user_script_unref(script) | 1286 | + | |
| 1279 | - | log.Printf("[webkit] UCM omnibox script injected at document-start") | 1287 | + | cGui := C.CString(guiScript) |
| 1280 | - | } | 1288 | + | defer C.free(unsafe.Pointer(cGui)) |
| 1289 | + | ||||
| 1290 | + | log.Printf("[webkit] Creating GUI user script for document-start injection in main world") | |||
| 1291 | + | guiUserScript := C.webkit_user_script_new((*C.gchar)(cGui), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |||
| 1292 | + | if guiUserScript != nil { | |||
| 1293 | + | C.webkit_user_content_manager_add_script(w.native.ucm, guiUserScript) | |||
| 1294 | + | C.webkit_user_script_unref(guiUserScript) | |||
| 1295 | + | log.Printf("[webkit] GUI bundle successfully injected as user script at document-start in main world") | |||
| 1296 | + | } else { | |||
| 1297 | + | log.Printf("[webkit] ERROR: Failed to create GUI user script") | |||
| 1298 | + | } | |||
| 1299 | + | } else { | |||
| 1300 | + | log.Printf("[webkit] ERROR: Failed to load GUI bundle from assets: %v", err) | |||
| 1301 | + | } | |||
| 1281 | 1302 | ||||
| 1282 | - | // Add user script at document-start (toast notification system) | 1303 | + | // Add GUI CSS styles as user stylesheet using WebKit native API |
| 1283 | - | toastSrc := C.CString(getToastScript()) | 1304 | + | if cssBytes, err := cfg.Assets.ReadFile("assets/gui/style.css"); err == nil { |
| 1284 | - | defer C.free(unsafe.Pointer(toastSrc)) | 1305 | + | cssContent := string(cssBytes) |
| 1285 | - | toastScript := C.webkit_user_script_new((*C.gchar)(toastSrc), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | 1306 | + | cCss := C.CString(cssContent) |
| 1286 | - | if toastScript != nil { | 1307 | + | defer C.free(unsafe.Pointer(cCss)) |
| 1287 | - | C.webkit_user_content_manager_add_script(w.native.ucm, toastScript) | 1308 | + | |
| 1288 | - | C.webkit_user_script_unref(toastScript) | 1309 | + | styleSheet := C.webkit_user_style_sheet_new( |
| 1289 | - | log.Printf("[webkit] UCM toast script injected at document-start") | 1310 | + | (*C.gchar)(cCss), |
| 1311 | + | C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, | |||
| 1312 | + | C.WEBKIT_USER_STYLE_LEVEL_AUTHOR, | |||
| 1313 | + | nil, // allow_list | |||
| 1314 | + | nil, // block_list | |||
| 1315 | + | ) | |||
| 1316 | + | if styleSheet != nil { | |||
| 1317 | + | C.webkit_user_content_manager_add_style_sheet(w.native.ucm, styleSheet) | |||
| 1318 | + | C.webkit_user_style_sheet_unref(styleSheet) | |||
| 1319 | + | } else { | |||
| 1320 | + | log.Printf("[webkit] ERROR: Failed to create GUI stylesheet") | |||
| 1321 | + | } | |||
| 1322 | + | } else { | |||
| 1323 | + | log.Printf("[webkit] ERROR: Failed to load GUI CSS from assets: %v", err) | |||
| 1324 | + | } | |||
| 1290 | } | 1325 | } | ||
| 1291 | 1326 | ||||
| 1292 | // Inject Wails runtime fetch interceptor for homepage bridging | 1327 | // Inject Wails runtime fetch interceptor for homepage bridging | ||
| 1293 | wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();` | 1328 | wailsBridge := `(() => { try { const origFetch = window.fetch.bind(window); const waiters = Object.create(null); window.__dumber_wails_resolve = (id, json) => { const w = waiters[id]; if(!w) return; delete waiters[id]; try { const headers = new Headers({"Content-Type":"application/json"}); w.resolve(new Response(json, { headers })); } catch(e){ w.reject(e); } }; window.fetch = (input, init) => { try { const url = new URL(input instanceof Request ? input.url : input, window.location.origin); if (url.pathname === '/wails/runtime') { const args = url.searchParams.get('args'); let payload = {}; try { payload = args ? JSON.parse(args) : {}; } catch(_){} const id = String(Date.now()) + '-' + Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { waiters[id] = { resolve, reject }; try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify({ type: 'wails', id, payload })); } catch (e) { reject(e); } }); } } catch(_){} return origFetch(input, init); }; } catch(_){} })();` | ||
| 1294 | cWails := C.CString(wailsBridge) | 1329 | cWails := C.CString(wailsBridge) | ||
| 1295 | defer C.free(unsafe.Pointer(cWails)) | 1330 | defer C.free(unsafe.Pointer(cWails)) | ||
| 1296 | - | wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | 1331 | + | wailsScript := C.webkit_user_script_new((*C.gchar)(cWails), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) |
| 1297 | if wailsScript != nil { | 1332 | if wailsScript != nil { | ||
| 1298 | C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript) | 1333 | C.webkit_user_content_manager_add_script(w.native.ucm, wailsScript) | ||
| 1299 | C.webkit_user_script_unref(wailsScript) | 1334 | C.webkit_user_script_unref(wailsScript) | ||