detached d70b2b0

refactor(internal): update browser controls and filtering for svelte integration

commit
d70b2b0be66e5ecc5d42c9f36507c8c55669a350
author
bamen · · 2025-09-15 05:21 UTC
parent
5e9cbe6
signature
unsigned
5 files · +83 -29
M internal/app/browser/shortcuts.go +12 -5
@@ -29,15 +29,22 @@ func (s *ShortcutHandler) RegisterShortcuts() {
2929 _ = s.webView.ShowDevTools()
3030 })
3131
32- // Omnibox (Ctrl+L): rely on injected script listener
32+ // Omnibox (Ctrl+L): use new keyboard service bridge
3333 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() {
34- log.Printf("Shortcut: Omnibox toggle")
35- _ = s.webView.InjectScript("window.__dumber_toggle && window.__dumber_toggle()")
34+ log.Printf("Shortcut: Omnibox toggle via keyboard service")
35+ _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+l')")
3636 })
3737
38- // Copy URL (Ctrl+Shift+C)
38+ // Find in page (Ctrl+F): use keyboard service bridge
39+ _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+f", func() {
40+ log.Printf("Shortcut: Find in page via keyboard service")
41+ _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')")
42+ })
43+
44+ // Copy URL (Ctrl+Shift+C): use keyboard service with native fallback
3945 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() {
40- s.clipboardController.CopyCurrentURL()
46+ log.Printf("Shortcut: Copy URL via keyboard service")
47+ _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+shift+c')")
4148 })
4249
4350 // Page refresh shortcuts
@@ -29,15 +29,22 @@ func (s *ShortcutHandler) RegisterShortcuts() {
29 _ = s.webView.ShowDevTools()29 _ = s.webView.ShowDevTools()
30 })30 })
31 31
32- // Omnibox (Ctrl+L): rely on injected script listener32+ // Omnibox (Ctrl+L): use new keyboard service bridge
33 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() {33 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() {
34- log.Printf("Shortcut: Omnibox toggle")34+ log.Printf("Shortcut: Omnibox toggle via keyboard service")
35- _ = s.webView.InjectScript("window.__dumber_toggle && window.__dumber_toggle()")35+ _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+l')")
36 })36 })
37 37
38- // Copy URL (Ctrl+Shift+C)38+ // Find in page (Ctrl+F): use keyboard service bridge
39+ _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+f", func() {
40+ log.Printf("Shortcut: Find in page via keyboard service")
41+ _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')")
42+ })
43+
44+ // Copy URL (Ctrl+Shift+C): use keyboard service with native fallback
39 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() {45 _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() {
40- s.clipboardController.CopyCurrentURL()46+ log.Printf("Shortcut: Copy URL via keyboard service")
47+ _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+shift+c')")
41 })48 })
42 49
43 // Page refresh shortcuts50 // Page refresh shortcuts
M internal/app/browser/webview.go +12 -1
@@ -54,6 +54,7 @@ func (app *BrowserApp) createWebView() error {
5454 _ = os.MkdirAll(webkitCache, constants.DirPerm)
5555
5656 view, err := webkit.NewWebView(&webkit.Config{
57+ Assets: app.assets,
5758 InitialURL: "dumb://homepage",
5859 ZoomDefault: 1.0,
5960 EnableDeveloperExtras: true,
@@ -107,6 +108,10 @@ func (app *BrowserApp) createWebView() error {
107108 // setupWebViewIntegration connects the WebView to browser services
108109 func (app *BrowserApp) setupWebViewIntegration() {
109110 app.browserService.AttachWebView(app.webView)
111+
112+ // GUI bundle is loaded via WebKit user scripts in enableUserContentManager
113+ // No need to load separately in browser service
114+
110115 // Use native window as title updater
111116 if win := app.webView.Window(); win != nil {
112117 app.browserService.SetWindowTitleUpdater(win)
@@ -129,6 +134,7 @@ func (app *BrowserApp) setupWebViewHandlers() {
129134 // Set WebView reference for message handler and register script messages
130135 app.messageHandler.SetWebView(app.webView)
131136 app.webView.RegisterScriptMessageHandler(app.messageHandler.Handle)
137+
132138 }
133139
134140 // setupControllers initializes and configures controller objects
@@ -212,15 +218,20 @@ func (app *BrowserApp) setupContentBlocking() error {
212218 // Continue without content blocking rather than failing
213219 }
214220
215- // Register navigation handler for domain-specific filtering
221+ // Register navigation handler for domain-specific filtering and GUI injection
216222 // Only register after content blocking is initialized
217223 app.webView.RegisterURIChangedHandler(func(uri string) {
218224 // Add small delay to avoid conflicts with page load
219225 go func() {
220226 time.Sleep(200 * time.Millisecond) // Slightly longer delay for stability
227+
228+ // Apply content filtering if available
221229 if app.filterManager != nil {
222230 app.webView.OnNavigate(uri, app.filterManager)
223231 }
232+
233+ // Note: GUI bundle (controls and toast) is now injected as User Script
234+ // in WebKit's enableUserContentManager, so it persists across all navigations
224235 }()
225236 })
226237 }()
@@ -54,6 +54,7 @@ func (app *BrowserApp) createWebView() error {
54 _ = os.MkdirAll(webkitCache, constants.DirPerm)54 _ = os.MkdirAll(webkitCache, constants.DirPerm)
55 55
56 view, err := webkit.NewWebView(&webkit.Config{56 view, err := webkit.NewWebView(&webkit.Config{
57+ Assets: app.assets,
57 InitialURL: "dumb://homepage",58 InitialURL: "dumb://homepage",
58 ZoomDefault: 1.0,59 ZoomDefault: 1.0,
59 EnableDeveloperExtras: true,60 EnableDeveloperExtras: true,
@@ -107,6 +108,10 @@ func (app *BrowserApp) createWebView() error {
107 // setupWebViewIntegration connects the WebView to browser services108 // setupWebViewIntegration connects the WebView to browser services
108 func (app *BrowserApp) setupWebViewIntegration() {109 func (app *BrowserApp) setupWebViewIntegration() {
109 app.browserService.AttachWebView(app.webView)110 app.browserService.AttachWebView(app.webView)
111+
112+ // GUI bundle is loaded via WebKit user scripts in enableUserContentManager
113+ // No need to load separately in browser service
114+
110 // Use native window as title updater115 // Use native window as title updater
111 if win := app.webView.Window(); win != nil {116 if win := app.webView.Window(); win != nil {
112 app.browserService.SetWindowTitleUpdater(win)117 app.browserService.SetWindowTitleUpdater(win)
@@ -129,6 +134,7 @@ func (app *BrowserApp) setupWebViewHandlers() {
129 // Set WebView reference for message handler and register script messages134 // Set WebView reference for message handler and register script messages
130 app.messageHandler.SetWebView(app.webView)135 app.messageHandler.SetWebView(app.webView)
131 app.webView.RegisterScriptMessageHandler(app.messageHandler.Handle)136 app.webView.RegisterScriptMessageHandler(app.messageHandler.Handle)
137+
132 }138 }
133 139
134 // setupControllers initializes and configures controller objects140 // setupControllers initializes and configures controller objects
@@ -212,15 +218,20 @@ func (app *BrowserApp) setupContentBlocking() error {
212 // Continue without content blocking rather than failing218 // Continue without content blocking rather than failing
213 }219 }
214 220
215- // Register navigation handler for domain-specific filtering221+ // Register navigation handler for domain-specific filtering and GUI injection
216 // Only register after content blocking is initialized222 // Only register after content blocking is initialized
217 app.webView.RegisterURIChangedHandler(func(uri string) {223 app.webView.RegisterURIChangedHandler(func(uri string) {
218 // Add small delay to avoid conflicts with page load224 // Add small delay to avoid conflicts with page load
219 go func() {225 go func() {
220 time.Sleep(200 * time.Millisecond) // Slightly longer delay for stability226 time.Sleep(200 * time.Millisecond) // Slightly longer delay for stability
227+
228+ // Apply content filtering if available
221 if app.filterManager != nil {229 if app.filterManager != nil {
222 app.webView.OnNavigate(uri, app.filterManager)230 app.webView.OnNavigate(uri, app.filterManager)
223 }231 }
232+
233+ // Note: GUI bundle (controls and toast) is now injected as User Script
234+ // in WebKit's enableUserContentManager, so it persists across all navigations
224 }()235 }()
225 })236 })
226 }()237 }()
M internal/app/control/zoom.go +35 -9
@@ -86,19 +86,45 @@ func (z *ZoomController) handleZoomChange(level float64) {
8686 }
8787 }
8888
89-// showZoomToast displays a zoom level notification
89+// showZoomToast displays a zoom level notification using TypeScript toast system
9090 func (z *ZoomController) showZoomToast(level float64) {
91- js := fmt.Sprintf(`try {
92- if (typeof window.__dumber_showZoomToast === 'function') {
93- window.__dumber_showZoomToast(%f);
94- } else {
95- console.warn('[dumber] Zoom toast function not available yet');
91+ log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)
92+
93+ percentage := int(level * 100)
94+
95+ // Enhanced approach that ensures toast always appears
96+ js := fmt.Sprintf(`(function() {
97+ try {
98+ console.log('[dumber] Attempting to show zoom toast...');
99+ const zoomLevel = %f;
100+ const percentage = %d;
101+ let toastShown = false;
102+
103+ // Strategy 1: Try the Svelte toast system first
104+ if (typeof window.__dumber_showZoomToast === 'function') {
105+ console.log('[dumber] Using Svelte zoom toast system');
106+ window.__dumber_showZoomToast(zoomLevel);
107+ toastShown = true;
108+ } else if (typeof window.__dumber_showToast === 'function') {
109+ console.log('[dumber] Using Svelte general toast system');
110+ window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info');
111+ toastShown = true;
112+ }
113+
114+
115+ if (!toastShown) {
116+ console.warn('[dumber] Failed to show zoom toast - no DOM body available');
117+ }
118+
119+ } catch(e) {
120+ console.error('[dumber] Zoom toast error:', e);
96121 }
97- } catch(e) {
98- console.error('[dumber] Zoom toast error:', e);
99- }`, level)
122+ })();`, level, percentage)
123+
100124 if err := z.webView.InjectScript(js); err != nil {
101125 log.Printf("[zoom] failed to show toast: %v", err)
126+ } else {
127+ log.Printf("[zoom] Toast script injected successfully")
102128 }
103129 }
104130
@@ -86,19 +86,45 @@ func (z *ZoomController) handleZoomChange(level float64) {
86 }86 }
87 }87 }
88 88
89-// showZoomToast displays a zoom level notification89+// showZoomToast displays a zoom level notification using TypeScript toast system
90 func (z *ZoomController) showZoomToast(level float64) {90 func (z *ZoomController) showZoomToast(level float64) {
91- js := fmt.Sprintf(`try { 91+ log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)
92- if (typeof window.__dumber_showZoomToast === 'function') {92+
93- window.__dumber_showZoomToast(%f);93+ percentage := int(level * 100)
94- } else {94+
95- console.warn('[dumber] Zoom toast function not available yet');95+ // Enhanced approach that ensures toast always appears
96+ js := fmt.Sprintf(`(function() {
97+ try {
98+ console.log('[dumber] Attempting to show zoom toast...');
99+ const zoomLevel = %f;
100+ const percentage = %d;
101+ let toastShown = false;
102+
103+ // Strategy 1: Try the Svelte toast system first
104+ if (typeof window.__dumber_showZoomToast === 'function') {
105+ console.log('[dumber] Using Svelte zoom toast system');
106+ window.__dumber_showZoomToast(zoomLevel);
107+ toastShown = true;
108+ } else if (typeof window.__dumber_showToast === 'function') {
109+ console.log('[dumber] Using Svelte general toast system');
110+ window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info');
111+ toastShown = true;
112+ }
113+
114+
115+ if (!toastShown) {
116+ console.warn('[dumber] Failed to show zoom toast - no DOM body available');
117+ }
118+
119+ } catch(e) {
120+ console.error('[dumber] Zoom toast error:', e);
96 }121 }
97- } catch(e) { 122+ })();`, level, percentage)
98- console.error('[dumber] Zoom toast error:', e); 123+
99- }`, level)
100 if err := z.webView.InjectScript(js); err != nil {124 if err := z.webView.InjectScript(js); err != nil {
101 log.Printf("[zoom] failed to show toast: %v", err)125 log.Printf("[zoom] failed to show toast: %v", err)
126+ } else {
127+ log.Printf("[zoom] Toast script injected successfully")
102 }128 }
103 }129 }
104 130
M internal/filtering/cosmetic/injector.go +12 -4
@@ -100,9 +100,13 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
100100 (function() {
101101 if (typeof window.__dumber_cosmetic_init === 'function') {
102102 window.__dumber_cosmetic_init(%s);
103- console.log('[dumber] Cosmetic filtering initialized with %d rules');
103+ if (window.self === window.top) {
104+ console.log('[dumber] Cosmetic filtering initialized with %d rules');
105+ }
104106 } else {
105- console.warn('[dumber] Cosmetic filtering not available');
107+ if (window.self === window.top) {
108+ console.warn('[dumber] Cosmetic filtering not available');
109+ }
106110 }
107111 })();`, domain, string(rulesJSON), len(rules))
108112
@@ -149,7 +153,9 @@ func (ci *CosmeticInjector) GetUpdateScript(newSelectors []string) string {
149153 (function() {
150154 if (typeof window.__dumber_cosmetic_update === 'function') {
151155 window.__dumber_cosmetic_update(%s);
152- console.log('[dumber] Updated cosmetic rules with %d new selectors');
156+ if (window.self === window.top) {
157+ console.log('[dumber] Updated cosmetic rules with %d new selectors');
158+ }
153159 }
154160 })();`, string(selectorsJSON), len(newSelectors))
155161 }
@@ -160,7 +166,9 @@ func (ci *CosmeticInjector) GetCleanupScript() string {
160166 (function() {
161167 if (typeof window.__dumber_cosmetic_cleanup === 'function') {
162168 window.__dumber_cosmetic_cleanup();
163- console.log('[dumber] Cosmetic filtering cleaned up');
169+ if (window.self === window.top) {
170+ console.log('[dumber] Cosmetic filtering cleaned up');
171+ }
164172 }
165173 })();`
166174 }
@@ -100,9 +100,13 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
100 (function() {100 (function() {
101 if (typeof window.__dumber_cosmetic_init === 'function') {101 if (typeof window.__dumber_cosmetic_init === 'function') {
102 window.__dumber_cosmetic_init(%s);102 window.__dumber_cosmetic_init(%s);
103- console.log('[dumber] Cosmetic filtering initialized with %d rules');103+ if (window.self === window.top) {
104+ console.log('[dumber] Cosmetic filtering initialized with %d rules');
105+ }
104 } else {106 } else {
105- console.warn('[dumber] Cosmetic filtering not available');107+ if (window.self === window.top) {
108+ console.warn('[dumber] Cosmetic filtering not available');
109+ }
106 }110 }
107 })();`, domain, string(rulesJSON), len(rules))111 })();`, domain, string(rulesJSON), len(rules))
108 112
@@ -149,7 +153,9 @@ func (ci *CosmeticInjector) GetUpdateScript(newSelectors []string) string {
149 (function() {153 (function() {
150 if (typeof window.__dumber_cosmetic_update === 'function') {154 if (typeof window.__dumber_cosmetic_update === 'function') {
151 window.__dumber_cosmetic_update(%s);155 window.__dumber_cosmetic_update(%s);
152- console.log('[dumber] Updated cosmetic rules with %d new selectors');156+ if (window.self === window.top) {
157+ console.log('[dumber] Updated cosmetic rules with %d new selectors');
158+ }
153 }159 }
154 })();`, string(selectorsJSON), len(newSelectors))160 })();`, string(selectorsJSON), len(newSelectors))
155 }161 }
@@ -160,7 +166,9 @@ func (ci *CosmeticInjector) GetCleanupScript() string {
160 (function() {166 (function() {
161 if (typeof window.__dumber_cosmetic_cleanup === 'function') {167 if (typeof window.__dumber_cosmetic_cleanup === 'function') {
162 window.__dumber_cosmetic_cleanup();168 window.__dumber_cosmetic_cleanup();
163- console.log('[dumber] Cosmetic filtering cleaned up');169+ if (window.self === window.top) {
170+ console.log('[dumber] Cosmetic filtering cleaned up');
171+ }
164 }172 }
165 })();`173 })();`
166 }174 }
M internal/filtering/cosmetic/injector.js +12 -10
@@ -182,16 +182,18 @@ const CosmeticFilter = (() => {
182182 antiBreakage.neutralizeAdPromises();
183183 antiBreakage.cleanupLoadingIndicators();
184184
185- // Parse and categorize rules
186- rules.forEach(rule => {
187- if (rule.includes(':has(') || rule.includes(':not(')) {
188- selectors.procedural.push(rule);
189- } else if (rule.domain === window.location.hostname) {
190- selectors.specific.push(rule.selector);
191- } else {
192- selectors.generic.push(rule.selector);
193- }
194- });
185+ // Parse and categorize rules (with null safety)
186+ if (rules && Array.isArray(rules)) {
187+ rules.forEach(rule => {
188+ if (rule.includes(':has(') || rule.includes(':not(')) {
189+ selectors.procedural.push(rule);
190+ } else if (rule.domain === window.location.hostname) {
191+ selectors.specific.push(rule.selector);
192+ } else {
193+ selectors.generic.push(rule.selector);
194+ }
195+ });
196+ }
195197
196198 // Apply initial hiding
197199 hideElements([...selectors.generic, ...selectors.specific]);
@@ -182,16 +182,18 @@ const CosmeticFilter = (() => {
182 antiBreakage.neutralizeAdPromises();182 antiBreakage.neutralizeAdPromises();
183 antiBreakage.cleanupLoadingIndicators();183 antiBreakage.cleanupLoadingIndicators();
184 184
185- // Parse and categorize rules185+ // Parse and categorize rules (with null safety)
186- rules.forEach(rule => {186+ if (rules && Array.isArray(rules)) {
187- if (rule.includes(':has(') || rule.includes(':not(')) {187+ rules.forEach(rule => {
188- selectors.procedural.push(rule);188+ if (rule.includes(':has(') || rule.includes(':not(')) {
189- } else if (rule.domain === window.location.hostname) {189+ selectors.procedural.push(rule);
190- selectors.specific.push(rule.selector);190+ } else if (rule.domain === window.location.hostname) {
191- } else {191+ selectors.specific.push(rule.selector);
192- selectors.generic.push(rule.selector);192+ } else {
193- }193+ selectors.generic.push(rule.selector);
194- });194+ }
195+ });
196+ }
195 197
196 // Apply initial hiding198 // Apply initial hiding
197 hideElements([...selectors.generic, ...selectors.specific]);199 hideElements([...selectors.generic, ...selectors.specific]);

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close