feat(webkit): migrate toast notifications from javascript to svelte components
- commit
- 66a30d06e8974e4a8100ddf2a4a8d64da7849290
- author
- bamen · · 2025-09-15 05:21 UTC
- parent
- 5e2466e
- signature
- unsigned
3 files · +73 -527
D pkg/webkit/toast_js_cgo.go +0 -199
| @@ -1,199 +0,0 @@ | |||
| 1 | - | //go:build webkit_cgo | |
| 2 | - | ||
| 3 | - | package webkit | |
| 4 | - | ||
| 5 | - | // getToastScript returns the injected toast notification component script. | |
| 6 | - | // Provides a reusable toast system that can display messages across all pages. | |
| 7 | - | func getToastScript() string { | |
| 8 | - | return `(() => { | |
| 9 | - | try { | |
| 10 | - | if (window.__dumber_toast_loaded) return; // idempotent | |
| 11 | - | window.__dumber_toast_loaded = true; | |
| 12 | - | ||
| 13 | - | const T = { | |
| 14 | - | container: null, | |
| 15 | - | toasts: [], // {id, element, timeout} | |
| 16 | - | counter: 0, | |
| 17 | - | zoomToastId: null, | |
| 18 | - | zoomDebounceTimer: null, | |
| 19 | - | ||
| 20 | - | init() { | |
| 21 | - | if (T.container) return; | |
| 22 | - | ||
| 23 | - | // Create toast container | |
| 24 | - | const container = document.createElement('div'); | |
| 25 | - | container.id = 'dumber-toast-container'; | |
| 26 | - | container.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483646;pointer-events:none;'; | |
| 27 | - | document.documentElement.appendChild(container); | |
| 28 | - | T.container = container; | |
| 29 | - | ||
| 30 | - | // Add CSS styles | |
| 31 | - | const style = document.createElement('style'); | |
| 32 | - | style.textContent = ` + | |
| 33 | - | "`" + ` | |
| 34 | - | .dumber-toast { | |
| 35 | - | background: rgba(18, 18, 18, 0.95); | |
| 36 | - | color: #eee; | |
| 37 | - | padding: 12px 16px; | |
| 38 | - | border-radius: 8px; | |
| 39 | - | border: 1px solid #444; | |
| 40 | - | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); | |
| 41 | - | font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif; | |
| 42 | - | font-size: 14px; | |
| 43 | - | line-height: 1.4; | |
| 44 | - | margin-bottom: 8px; | |
| 45 | - | max-width: 320px; | |
| 46 | - | word-wrap: break-word; | |
| 47 | - | backdrop-filter: blur(8px); | |
| 48 | - | -webkit-backdrop-filter: blur(8px); | |
| 49 | - | pointer-events: auto; | |
| 50 | - | cursor: default; | |
| 51 | - | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| 52 | - | transform: translateX(100%); | |
| 53 | - | opacity: 0; | |
| 54 | - | } | |
| 55 | - | ||
| 56 | - | .dumber-toast.show { | |
| 57 | - | transform: translateX(0); | |
| 58 | - | opacity: 1; | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | .dumber-toast:hover { | |
| 62 | - | background: rgba(27, 27, 27, 0.95); | |
| 63 | - | border-color: #555; | |
| 64 | - | } | |
| 65 | - | ` + "`" + `; | |
| 66 | - | document.documentElement.appendChild(style); | |
| 67 | - | }, | |
| 68 | - | ||
| 69 | - | show(message, duration = 2500) { | |
| 70 | - | T.init(); | |
| 71 | - | ||
| 72 | - | const id = ++T.counter; | |
| 73 | - | const toast = document.createElement('div'); | |
| 74 | - | toast.className = 'dumber-toast'; | |
| 75 | - | toast.textContent = message || 'Notification'; | |
| 76 | - | toast.setAttribute('data-toast-id', id); | |
| 77 | - | ||
| 78 | - | // Click to dismiss | |
| 79 | - | toast.addEventListener('click', () => T.dismiss(id)); | |
| 80 | - | ||
| 81 | - | T.container.appendChild(toast); | |
| 82 | - | ||
| 83 | - | // Trigger show animation | |
| 84 | - | requestAnimationFrame(() => { | |
| 85 | - | toast.classList.add('show'); | |
| 86 | - | }); | |
| 87 | - | ||
| 88 | - | // Auto-dismiss | |
| 89 | - | const timeout = setTimeout(() => T.dismiss(id), duration); | |
| 90 | - | ||
| 91 | - | T.toasts.push({ id, element: toast, timeout }); | |
| 92 | - | ||
| 93 | - | console.log('📋 Toast:', message); | |
| 94 | - | return id; | |
| 95 | - | }, | |
| 96 | - | ||
| 97 | - | dismiss(id) { | |
| 98 | - | const toastIndex = T.toasts.findIndex(t => t.id === id); | |
| 99 | - | if (toastIndex === -1) return; | |
| 100 | - | ||
| 101 | - | const toast = T.toasts[toastIndex]; | |
| 102 | - | clearTimeout(toast.timeout); | |
| 103 | - | ||
| 104 | - | // Clear zoom toast tracking if this is the zoom toast | |
| 105 | - | if (T.zoomToastId === id) { | |
| 106 | - | T.zoomToastId = null; | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | // Animate out | |
| 110 | - | toast.element.style.transform = 'translateX(100%)'; | |
| 111 | - | toast.element.style.opacity = '0'; | |
| 112 | - | ||
| 113 | - | setTimeout(() => { | |
| 114 | - | if (toast.element && toast.element.parentNode) { | |
| 115 | - | toast.element.parentNode.removeChild(toast.element); | |
| 116 | - | } | |
| 117 | - | T.toasts.splice(toastIndex, 1); | |
| 118 | - | }, 300); | |
| 119 | - | }, | |
| 120 | - | ||
| 121 | - | clear() { | |
| 122 | - | T.toasts.forEach(toast => { | |
| 123 | - | clearTimeout(toast.timeout); | |
| 124 | - | if (toast.element && toast.element.parentNode) { | |
| 125 | - | toast.element.parentNode.removeChild(toast.element); | |
| 126 | - | } | |
| 127 | - | }); | |
| 128 | - | T.toasts = []; | |
| 129 | - | T.zoomToastId = null; | |
| 130 | - | clearTimeout(T.zoomDebounceTimer); | |
| 131 | - | T.zoomDebounceTimer = null; | |
| 132 | - | } | |
| 133 | - | }; | |
| 134 | - | ||
| 135 | - | // Public API for native integration | |
| 136 | - | window.__dumber_showToast = (message, duration) => T.show(message, duration); | |
| 137 | - | window.__dumber_dismissToast = (id) => T.dismiss(id); | |
| 138 | - | window.__dumber_clearToasts = () => T.clear(); | |
| 139 | - | window.__dumber_showZoomToast = (zoomLevel) => { | |
| 140 | - | console.log('[dumber] showZoomToast called with level:', zoomLevel); | |
| 141 | - | ||
| 142 | - | // Clear existing debounce timer | |
| 143 | - | clearTimeout(T.zoomDebounceTimer); | |
| 144 | - | ||
| 145 | - | // Force remove ALL zoom toasts immediately (aggressive cleanup) | |
| 146 | - | for (let i = T.toasts.length - 1; i >= 0; i--) { | |
| 147 | - | const toast = T.toasts[i]; | |
| 148 | - | if (toast.element && (toast.element.textContent.includes('Zoom level:') || toast.id === T.zoomToastId)) { | |
| 149 | - | clearTimeout(toast.timeout); | |
| 150 | - | if (toast.element.parentNode) { | |
| 151 | - | toast.element.parentNode.removeChild(toast.element); | |
| 152 | - | } | |
| 153 | - | T.toasts.splice(i, 1); | |
| 154 | - | } | |
| 155 | - | } | |
| 156 | - | T.zoomToastId = null; | |
| 157 | - | ||
| 158 | - | // Also cleanup any orphaned zoom toast elements in DOM | |
| 159 | - | const orphanedZoomToasts = document.querySelectorAll('.dumber-toast'); | |
| 160 | - | orphanedZoomToasts.forEach(el => { | |
| 161 | - | if (el.textContent.includes('Zoom level:')) { | |
| 162 | - | el.parentNode && el.parentNode.removeChild(el); | |
| 163 | - | } | |
| 164 | - | }); | |
| 165 | - | ||
| 166 | - | // Debounce the toast display | |
| 167 | - | T.zoomDebounceTimer = setTimeout(() => { | |
| 168 | - | const percentage = Math.round(zoomLevel * 100); | |
| 169 | - | const sign = percentage > 100 ? '+' : ''; | |
| 170 | - | const diff = percentage - 100; | |
| 171 | - | const message = 'Zoom level: ' + sign + diff + '% (' + percentage + '%)'; | |
| 172 | - | console.log('[dumber] Showing zoom toast:', message); | |
| 173 | - | T.zoomToastId = T.show(message, 2000); | |
| 174 | - | }, 150); | |
| 175 | - | }; | |
| 176 | - | ||
| 177 | - | // Test function for manual debugging | |
| 178 | - | window.__dumber_testToast = () => { | |
| 179 | - | console.log('[dumber] Testing toast system...'); | |
| 180 | - | return T.show('Toast system is working!', 2000); | |
| 181 | - | }; | |
| 182 | - | ||
| 183 | - | // Emergency cleanup function for stuck toasts | |
| 184 | - | window.__dumber_forceCleanupToasts = () => { | |
| 185 | - | console.log('[dumber] Force cleaning up all toasts...'); | |
| 186 | - | T.clear(); | |
| 187 | - | // Also force remove any orphaned toast elements | |
| 188 | - | const orphans = document.querySelectorAll('.dumber-toast'); | |
| 189 | - | orphans.forEach(el => el.parentNode && el.parentNode.removeChild(el)); | |
| 190 | - | console.log('[dumber] Cleanup complete, removed', orphans.length, 'orphaned toasts'); | |
| 191 | - | }; | |
| 192 | - | ||
| 193 | - | console.log('✅ Dumber Browser toast system loaded'); | |
| 194 | - | ||
| 195 | - | } catch (e) { | |
| 196 | - | console.error('❌ Failed to initialize Dumber toast system:', e); | |
| 197 | - | } | |
| 198 | - | })();` | |
| 199 | - | } | |
| @@ -1,199 +0,0 @@ | |||||
| 1 | - | //go:build webkit_cgo | |||
| 2 | - | ||||
| 3 | - | package webkit | |||
| 4 | - | ||||
| 5 | - | // getToastScript returns the injected toast notification component script. | |||
| 6 | - | // Provides a reusable toast system that can display messages across all pages. | |||
| 7 | - | func getToastScript() string { | |||
| 8 | - | return `(() => { | |||
| 9 | - | try { | |||
| 10 | - | if (window.__dumber_toast_loaded) return; // idempotent | |||
| 11 | - | window.__dumber_toast_loaded = true; | |||
| 12 | - | ||||
| 13 | - | const T = { | |||
| 14 | - | container: null, | |||
| 15 | - | toasts: [], // {id, element, timeout} | |||
| 16 | - | counter: 0, | |||
| 17 | - | zoomToastId: null, | |||
| 18 | - | zoomDebounceTimer: null, | |||
| 19 | - | ||||
| 20 | - | init() { | |||
| 21 | - | if (T.container) return; | |||
| 22 | - | ||||
| 23 | - | // Create toast container | |||
| 24 | - | const container = document.createElement('div'); | |||
| 25 | - | container.id = 'dumber-toast-container'; | |||
| 26 | - | container.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483646;pointer-events:none;'; | |||
| 27 | - | document.documentElement.appendChild(container); | |||
| 28 | - | T.container = container; | |||
| 29 | - | ||||
| 30 | - | // Add CSS styles | |||
| 31 | - | const style = document.createElement('style'); | |||
| 32 | - | style.textContent = ` + | |||
| 33 | - | "`" + ` | |||
| 34 | - | .dumber-toast { | |||
| 35 | - | background: rgba(18, 18, 18, 0.95); | |||
| 36 | - | color: #eee; | |||
| 37 | - | padding: 12px 16px; | |||
| 38 | - | border-radius: 8px; | |||
| 39 | - | border: 1px solid #444; | |||
| 40 | - | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); | |||
| 41 | - | font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif; | |||
| 42 | - | font-size: 14px; | |||
| 43 | - | line-height: 1.4; | |||
| 44 | - | margin-bottom: 8px; | |||
| 45 | - | max-width: 320px; | |||
| 46 | - | word-wrap: break-word; | |||
| 47 | - | backdrop-filter: blur(8px); | |||
| 48 | - | -webkit-backdrop-filter: blur(8px); | |||
| 49 | - | pointer-events: auto; | |||
| 50 | - | cursor: default; | |||
| 51 | - | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||
| 52 | - | transform: translateX(100%); | |||
| 53 | - | opacity: 0; | |||
| 54 | - | } | |||
| 55 | - | ||||
| 56 | - | .dumber-toast.show { | |||
| 57 | - | transform: translateX(0); | |||
| 58 | - | opacity: 1; | |||
| 59 | - | } | |||
| 60 | - | ||||
| 61 | - | .dumber-toast:hover { | |||
| 62 | - | background: rgba(27, 27, 27, 0.95); | |||
| 63 | - | border-color: #555; | |||
| 64 | - | } | |||
| 65 | - | ` + "`" + `; | |||
| 66 | - | document.documentElement.appendChild(style); | |||
| 67 | - | }, | |||
| 68 | - | ||||
| 69 | - | show(message, duration = 2500) { | |||
| 70 | - | T.init(); | |||
| 71 | - | ||||
| 72 | - | const id = ++T.counter; | |||
| 73 | - | const toast = document.createElement('div'); | |||
| 74 | - | toast.className = 'dumber-toast'; | |||
| 75 | - | toast.textContent = message || 'Notification'; | |||
| 76 | - | toast.setAttribute('data-toast-id', id); | |||
| 77 | - | ||||
| 78 | - | // Click to dismiss | |||
| 79 | - | toast.addEventListener('click', () => T.dismiss(id)); | |||
| 80 | - | ||||
| 81 | - | T.container.appendChild(toast); | |||
| 82 | - | ||||
| 83 | - | // Trigger show animation | |||
| 84 | - | requestAnimationFrame(() => { | |||
| 85 | - | toast.classList.add('show'); | |||
| 86 | - | }); | |||
| 87 | - | ||||
| 88 | - | // Auto-dismiss | |||
| 89 | - | const timeout = setTimeout(() => T.dismiss(id), duration); | |||
| 90 | - | ||||
| 91 | - | T.toasts.push({ id, element: toast, timeout }); | |||
| 92 | - | ||||
| 93 | - | console.log('📋 Toast:', message); | |||
| 94 | - | return id; | |||
| 95 | - | }, | |||
| 96 | - | ||||
| 97 | - | dismiss(id) { | |||
| 98 | - | const toastIndex = T.toasts.findIndex(t => t.id === id); | |||
| 99 | - | if (toastIndex === -1) return; | |||
| 100 | - | ||||
| 101 | - | const toast = T.toasts[toastIndex]; | |||
| 102 | - | clearTimeout(toast.timeout); | |||
| 103 | - | ||||
| 104 | - | // Clear zoom toast tracking if this is the zoom toast | |||
| 105 | - | if (T.zoomToastId === id) { | |||
| 106 | - | T.zoomToastId = null; | |||
| 107 | - | } | |||
| 108 | - | ||||
| 109 | - | // Animate out | |||
| 110 | - | toast.element.style.transform = 'translateX(100%)'; | |||
| 111 | - | toast.element.style.opacity = '0'; | |||
| 112 | - | ||||
| 113 | - | setTimeout(() => { | |||
| 114 | - | if (toast.element && toast.element.parentNode) { | |||
| 115 | - | toast.element.parentNode.removeChild(toast.element); | |||
| 116 | - | } | |||
| 117 | - | T.toasts.splice(toastIndex, 1); | |||
| 118 | - | }, 300); | |||
| 119 | - | }, | |||
| 120 | - | ||||
| 121 | - | clear() { | |||
| 122 | - | T.toasts.forEach(toast => { | |||
| 123 | - | clearTimeout(toast.timeout); | |||
| 124 | - | if (toast.element && toast.element.parentNode) { | |||
| 125 | - | toast.element.parentNode.removeChild(toast.element); | |||
| 126 | - | } | |||
| 127 | - | }); | |||
| 128 | - | T.toasts = []; | |||
| 129 | - | T.zoomToastId = null; | |||
| 130 | - | clearTimeout(T.zoomDebounceTimer); | |||
| 131 | - | T.zoomDebounceTimer = null; | |||
| 132 | - | } | |||
| 133 | - | }; | |||
| 134 | - | ||||
| 135 | - | // Public API for native integration | |||
| 136 | - | window.__dumber_showToast = (message, duration) => T.show(message, duration); | |||
| 137 | - | window.__dumber_dismissToast = (id) => T.dismiss(id); | |||
| 138 | - | window.__dumber_clearToasts = () => T.clear(); | |||
| 139 | - | window.__dumber_showZoomToast = (zoomLevel) => { | |||
| 140 | - | console.log('[dumber] showZoomToast called with level:', zoomLevel); | |||
| 141 | - | ||||
| 142 | - | // Clear existing debounce timer | |||
| 143 | - | clearTimeout(T.zoomDebounceTimer); | |||
| 144 | - | ||||
| 145 | - | // Force remove ALL zoom toasts immediately (aggressive cleanup) | |||
| 146 | - | for (let i = T.toasts.length - 1; i >= 0; i--) { | |||
| 147 | - | const toast = T.toasts[i]; | |||
| 148 | - | if (toast.element && (toast.element.textContent.includes('Zoom level:') || toast.id === T.zoomToastId)) { | |||
| 149 | - | clearTimeout(toast.timeout); | |||
| 150 | - | if (toast.element.parentNode) { | |||
| 151 | - | toast.element.parentNode.removeChild(toast.element); | |||
| 152 | - | } | |||
| 153 | - | T.toasts.splice(i, 1); | |||
| 154 | - | } | |||
| 155 | - | } | |||
| 156 | - | T.zoomToastId = null; | |||
| 157 | - | ||||
| 158 | - | // Also cleanup any orphaned zoom toast elements in DOM | |||
| 159 | - | const orphanedZoomToasts = document.querySelectorAll('.dumber-toast'); | |||
| 160 | - | orphanedZoomToasts.forEach(el => { | |||
| 161 | - | if (el.textContent.includes('Zoom level:')) { | |||
| 162 | - | el.parentNode && el.parentNode.removeChild(el); | |||
| 163 | - | } | |||
| 164 | - | }); | |||
| 165 | - | ||||
| 166 | - | // Debounce the toast display | |||
| 167 | - | T.zoomDebounceTimer = setTimeout(() => { | |||
| 168 | - | const percentage = Math.round(zoomLevel * 100); | |||
| 169 | - | const sign = percentage > 100 ? '+' : ''; | |||
| 170 | - | const diff = percentage - 100; | |||
| 171 | - | const message = 'Zoom level: ' + sign + diff + '% (' + percentage + '%)'; | |||
| 172 | - | console.log('[dumber] Showing zoom toast:', message); | |||
| 173 | - | T.zoomToastId = T.show(message, 2000); | |||
| 174 | - | }, 150); | |||
| 175 | - | }; | |||
| 176 | - | ||||
| 177 | - | // Test function for manual debugging | |||
| 178 | - | window.__dumber_testToast = () => { | |||
| 179 | - | console.log('[dumber] Testing toast system...'); | |||
| 180 | - | return T.show('Toast system is working!', 2000); | |||
| 181 | - | }; | |||
| 182 | - | ||||
| 183 | - | // Emergency cleanup function for stuck toasts | |||
| 184 | - | window.__dumber_forceCleanupToasts = () => { | |||
| 185 | - | console.log('[dumber] Force cleaning up all toasts...'); | |||
| 186 | - | T.clear(); | |||
| 187 | - | // Also force remove any orphaned toast elements | |||
| 188 | - | const orphans = document.querySelectorAll('.dumber-toast'); | |||
| 189 | - | orphans.forEach(el => el.parentNode && el.parentNode.removeChild(el)); | |||
| 190 | - | console.log('[dumber] Cleanup complete, removed', orphans.length, 'orphaned toasts'); | |||
| 191 | - | }; | |||
| 192 | - | ||||
| 193 | - | console.log('✅ Dumber Browser toast system loaded'); | |||
| 194 | - | ||||
| 195 | - | } catch (e) { | |||
| 196 | - | console.error('❌ Failed to initialize Dumber toast system:', e); | |||
| 197 | - | } | |||
| 198 | - | })();` | |||
| 199 | - | } | |||
A pkg/webkit/toast_svelte_cgo.go +73 -0
| @@ -0,0 +1,73 @@ | |||
| 1 | + | //go:build webkit_cgo | |
| 2 | + | ||
| 3 | + | package webkit | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "fmt" | |
| 7 | + | "log" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // getToastScript returns the unified GUI bundle-based toast notification script with GTK theme integration. | |
| 11 | + | // This function is deprecated and should use the new unified approach via BrowserService.InjectToastSystem | |
| 12 | + | func getToastScript(assets interface{ ReadFile(string) ([]byte, error) }) string { | |
| 13 | + | // Read the unified GUI bundle from assets | |
| 14 | + | var guiBundle string | |
| 15 | + | if assets != nil { | |
| 16 | + | bundleBytes, err := assets.ReadFile("gui/gui.min.js") | |
| 17 | + | if err != nil { | |
| 18 | + | // Fallback to error message if file not found | |
| 19 | + | guiBundle = `console.error("❌ Failed to load unified GUI bundle: ` + err.Error() + `");` | |
| 20 | + | log.Printf("[webkit] Failed to read GUI bundle: %v", err) | |
| 21 | + | } else { | |
| 22 | + | guiBundle = string(bundleBytes) | |
| 23 | + | log.Printf("[webkit] Successfully loaded unified GUI bundle (%d bytes)", len(bundleBytes)) | |
| 24 | + | } | |
| 25 | + | } else { | |
| 26 | + | guiBundle = `console.error("❌ Failed to load unified GUI bundle: assets not provided");` | |
| 27 | + | log.Printf("[webkit] GUI bundle error: assets not provided") | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // GTK Theme Integration with new unified approach | |
| 31 | + | themeIntegration := ` | |
| 32 | + | // Set up theme change listener for dynamic updates | |
| 33 | + | window.__dumber_setTheme = (theme) => { | |
| 34 | + | window.__dumber_initial_theme = theme; | |
| 35 | + | if (theme === 'dark') { | |
| 36 | + | document.documentElement.classList.add('dark'); | |
| 37 | + | } else { | |
| 38 | + | document.documentElement.classList.remove('dark'); | |
| 39 | + | } | |
| 40 | + | }; | |
| 41 | + | ` | |
| 42 | + | ||
| 43 | + | // Use unified GUI bundle approach | |
| 44 | + | return fmt.Sprintf(`(function() { | |
| 45 | + | function initializeToast() { | |
| 46 | + | try { | |
| 47 | + | %s | |
| 48 | + | ||
| 49 | + | %s | |
| 50 | + | ||
| 51 | + | // Initialize toast system via unified GUI bundle | |
| 52 | + | if (window.__dumber_gui && window.__dumber_gui.initializeToast) { | |
| 53 | + | window.__dumber_gui.initializeToast().then(() => { | |
| 54 | + | console.log('✅ Toast system initialized via unified GUI bundle'); | |
| 55 | + | }).catch((e) => { | |
| 56 | + | console.error('❌ Toast initialization failed:', e); | |
| 57 | + | }); | |
| 58 | + | } else { | |
| 59 | + | console.error('❌ Unified GUI bundle not properly loaded'); | |
| 60 | + | } | |
| 61 | + | } catch (e) { | |
| 62 | + | console.error('❌ Failed to initialize toast system:', e); | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Ensure DOM is ready before initializing | |
| 67 | + | if (document.readyState === 'loading') { | |
| 68 | + | document.addEventListener('DOMContentLoaded', initializeToast); | |
| 69 | + | } else { | |
| 70 | + | initializeToast(); | |
| 71 | + | } | |
| 72 | + | })();`, themeIntegration, guiBundle) | |
| 73 | + | }⏎̸ | |
| @@ -0,0 +1,73 @@ | |||||
| 1 | + | //go:build webkit_cgo | |||
| 2 | + | ||||
| 3 | + | package webkit | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "fmt" | |||
| 7 | + | "log" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // getToastScript returns the unified GUI bundle-based toast notification script with GTK theme integration. | |||
| 11 | + | // This function is deprecated and should use the new unified approach via BrowserService.InjectToastSystem | |||
| 12 | + | func getToastScript(assets interface{ ReadFile(string) ([]byte, error) }) string { | |||
| 13 | + | // Read the unified GUI bundle from assets | |||
| 14 | + | var guiBundle string | |||
| 15 | + | if assets != nil { | |||
| 16 | + | bundleBytes, err := assets.ReadFile("gui/gui.min.js") | |||
| 17 | + | if err != nil { | |||
| 18 | + | // Fallback to error message if file not found | |||
| 19 | + | guiBundle = `console.error("❌ Failed to load unified GUI bundle: ` + err.Error() + `");` | |||
| 20 | + | log.Printf("[webkit] Failed to read GUI bundle: %v", err) | |||
| 21 | + | } else { | |||
| 22 | + | guiBundle = string(bundleBytes) | |||
| 23 | + | log.Printf("[webkit] Successfully loaded unified GUI bundle (%d bytes)", len(bundleBytes)) | |||
| 24 | + | } | |||
| 25 | + | } else { | |||
| 26 | + | guiBundle = `console.error("❌ Failed to load unified GUI bundle: assets not provided");` | |||
| 27 | + | log.Printf("[webkit] GUI bundle error: assets not provided") | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // GTK Theme Integration with new unified approach | |||
| 31 | + | themeIntegration := ` | |||
| 32 | + | // Set up theme change listener for dynamic updates | |||
| 33 | + | window.__dumber_setTheme = (theme) => { | |||
| 34 | + | window.__dumber_initial_theme = theme; | |||
| 35 | + | if (theme === 'dark') { | |||
| 36 | + | document.documentElement.classList.add('dark'); | |||
| 37 | + | } else { | |||
| 38 | + | document.documentElement.classList.remove('dark'); | |||
| 39 | + | } | |||
| 40 | + | }; | |||
| 41 | + | ` | |||
| 42 | + | ||||
| 43 | + | // Use unified GUI bundle approach | |||
| 44 | + | return fmt.Sprintf(`(function() { | |||
| 45 | + | function initializeToast() { | |||
| 46 | + | try { | |||
| 47 | + | %s | |||
| 48 | + | ||||
| 49 | + | %s | |||
| 50 | + | ||||
| 51 | + | // Initialize toast system via unified GUI bundle | |||
| 52 | + | if (window.__dumber_gui && window.__dumber_gui.initializeToast) { | |||
| 53 | + | window.__dumber_gui.initializeToast().then(() => { | |||
| 54 | + | console.log('✅ Toast system initialized via unified GUI bundle'); | |||
| 55 | + | }).catch((e) => { | |||
| 56 | + | console.error('❌ Toast initialization failed:', e); | |||
| 57 | + | }); | |||
| 58 | + | } else { | |||
| 59 | + | console.error('❌ Unified GUI bundle not properly loaded'); | |||
| 60 | + | } | |||
| 61 | + | } catch (e) { | |||
| 62 | + | console.error('❌ Failed to initialize toast system:', e); | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Ensure DOM is ready before initializing | |||
| 67 | + | if (document.readyState === 'loading') { | |||
| 68 | + | document.addEventListener('DOMContentLoaded', initializeToast); | |||
| 69 | + | } else { | |||
| 70 | + | initializeToast(); | |||
| 71 | + | } | |||
| 72 | + | })();`, themeIntegration, guiBundle) | |||
| 73 | + | }⏎̸ | |||
D pkg/webkit/ucm_js_cgo.go +0 -328
| @@ -1,328 +0,0 @@ | |||
| 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 | - | if (!H.el) H.mount(); | |
| 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 | - | } catch (e) { /* no-op */ } | |
| 327 | - | })();` | |
| 328 | - | } | |
| @@ -1,328 +0,0 @@ | |||||
| 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 | - | if (!H.el) H.mount(); | |||
| 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 | - | } catch (e) { /* no-op */ } | |||
| 327 | - | })();` | |||
| 328 | - | } | |||