v0.8.0 0fa6785

zoom.go RawHistoryFolder

1package control
2
3import (
4	"context"
5	"fmt"
6	"log"
7
8	"github.com/bnema/dumber/pkg/webkit"
9	"github.com/bnema/dumber/services"
10)
11
12// ZoomController manages zoom functionality for the WebView
13type ZoomController struct {
14	currentURL         string
15	lastZoomDomain     string
16	programmaticChange bool
17	browserService     *services.BrowserService
18	webView            *webkit.WebView
19}
20
21// WebViewInterface defines the interface for WebView zoom operations
22type WebViewInterface interface {
23	GetCurrentURL() string
24	SetZoom(level float64) error
25	InjectScript(script string) error
26	RegisterZoomChangedHandler(handler func(float64))
27	RegisterURIChangedHandler(handler func(string))
28}
29
30// NewZoomController creates a new zoom controller
31func NewZoomController(browserService *services.BrowserService, webView *webkit.WebView) *ZoomController {
32	return &ZoomController{
33		currentURL:     "dumb://homepage",
34		browserService: browserService,
35		webView:        webView,
36	}
37}
38
39// RegisterHandlers sets up the zoom-related event handlers
40func (z *ZoomController) RegisterHandlers() {
41	z.webView.RegisterZoomChangedHandler(z.handleZoomChange)
42	z.webView.RegisterURIChangedHandler(z.handleURIChange)
43}
44
45// handleURIChange responds to URL changes and applies saved zoom levels
46func (z *ZoomController) handleURIChange(url string) {
47	z.currentURL = url
48	if url == "" {
49		return
50	}
51	ctx := context.Background()
52	currentDomain := services.ZoomKeyForLog(url)
53
54	if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
55		z.programmaticChange = true
56		if err := z.webView.SetZoom(zoomLevel); err == nil {
57			log.Printf("[zoom] loaded %.2f for %s", zoomLevel, currentDomain)
58
59			// Only show toast when entering a new domain (not on first load)
60			if currentDomain != z.lastZoomDomain && z.lastZoomDomain != "" {
61				z.showZoomToast(zoomLevel)
62			}
63			z.lastZoomDomain = currentDomain
64		}
65		z.programmaticChange = false
66	}
67}
68
69// handleZoomChange responds to zoom level changes and persists them
70func (z *ZoomController) handleZoomChange(level float64) {
71	url := z.webView.GetCurrentURL()
72	if url == "" {
73		return
74	}
75	ctx := context.Background()
76	if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
77		log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
78		return
79	}
80	key := services.ZoomKeyForLog(url)
81	log.Printf("[zoom] saved %.2f for %s", level, key)
82
83	// Only show toast for user-initiated zoom changes, not programmatic ones
84	if !z.programmaticChange {
85		z.showZoomToast(level)
86	}
87}
88
89// showZoomToast displays a zoom level notification using TypeScript toast system
90func (z *ZoomController) showZoomToast(level float64) {
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			// Prefer unified page-world API
104			if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.zoom === 'function') {
105				console.log('[dumber] Using unified toast.zoom');
106				window.__dumber.toast.zoom(zoomLevel);
107				toastShown = true;
108			} else if (typeof window.__dumber_showZoomToast === 'function') {
109				console.log('[dumber] Using legacy zoom toast');
110				window.__dumber_showZoomToast(zoomLevel);
111				toastShown = true;
112			} else if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.show === 'function') {
113				console.log('[dumber] Using unified toast.show');
114				window.__dumber.toast.show('Zoom: ' + percentage + '%%', 1500, 'info');
115				toastShown = true;
116			} else if (typeof window.__dumber_showToast === 'function') {
117				console.log('[dumber] Using legacy toast.show');
118				window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info');
119				toastShown = true;
120			}
121
122
123			if (!toastShown) {
124				console.warn('[dumber] Failed to show zoom toast - no DOM body available');
125			}
126
127		} catch(e) {
128			console.error('[dumber] Zoom toast error:', e);
129		}
130	})();`, level, percentage)
131
132	if err := z.webView.InjectScript(js); err != nil {
133		log.Printf("[zoom] failed to show toast: %v", err)
134	} else {
135		log.Printf("[zoom] Toast script injected successfully")
136	}
137}
138
139// ApplyInitialZoom sets the initial zoom level for the current URL
140func (z *ZoomController) ApplyInitialZoom() {
141	ctx := context.Background()
142	url := z.webView.GetCurrentURL()
143	if url == "" {
144		url = "dumb://homepage"
145	}
146	if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
147		z.programmaticChange = true
148		if err := z.webView.SetZoom(zoomLevel); err != nil {
149			log.Printf("Warning: failed to set initial zoom: %v", err)
150		} else {
151			key := services.ZoomKeyForLog(url)
152			log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key)
153		}
154		z.programmaticChange = false
155	}
156}
157
158// ApplyZoomForURL applies zoom for a specific URL (used for navigation)
159func (z *ZoomController) ApplyZoomForURL(url string) {
160	ctx := context.Background()
161	if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
162		z.programmaticChange = true
163		if err := z.webView.SetZoom(zoomLevel); err != nil {
164			log.Printf("Warning: failed to set zoom: %v", err)
165		} else {
166			key := services.ZoomKeyForLog(url)
167			log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key)
168		}
169		z.programmaticChange = false
170	}
171}

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