v0.14.1 0da30ba

zoom.go RawHistoryFolder

1package control
2
3import (
4	"context"
5	"log"
6	"time"
7
8	"github.com/bnema/dumber/internal/services"
9	"github.com/bnema/dumber/pkg/webkit"
10)
11
12// ZoomController manages zoom functionality for the WebView
13type ZoomController struct {
14	currentURL         string
15	lastZoomDomain     string
16	programmaticChange bool
17	programmaticTimer  *time.Timer
18	browserService     *services.BrowserService
19	webView            *webkit.WebView
20}
21
22// WebViewInterface defines the interface for WebView zoom operations
23type WebViewInterface interface {
24	GetCurrentURL() string
25	SetZoom(level float64) error
26	InjectScript(script string) error
27	RegisterZoomChangedHandler(handler func(float64))
28	RegisterURIChangedHandler(handler func(string))
29	RegisterLoadCommittedHandler(handler func(string))
30}
31
32// NewZoomController creates a new zoom controller
33func NewZoomController(browserService *services.BrowserService, webView *webkit.WebView) *ZoomController {
34	return &ZoomController{
35		currentURL:     "dumb://homepage",
36		browserService: browserService,
37		webView:        webView,
38	}
39}
40
41// RegisterHandlers sets up the zoom-related event handlers
42func (z *ZoomController) RegisterHandlers() {
43	z.webView.RegisterZoomChangedHandler(z.handleZoomChange)
44	z.webView.RegisterURIChangedHandler(z.handleURIChange)
45	z.webView.RegisterLoadCommittedHandler(z.handleLoadCommitted)
46}
47
48// handleURIChange tracks URL changes but doesn't apply zoom (wait for load-committed)
49func (z *ZoomController) handleURIChange(url string) {
50	z.currentURL = url
51}
52
53// handleLoadCommitted responds to load committed events and applies saved zoom levels
54// This fires when the page actually starts loading content, preventing visual glitches
55func (z *ZoomController) handleLoadCommitted(url string) {
56	if url == "" {
57		return
58	}
59	currentDomain := services.ZoomKeyForLog(url)
60	z.loadZoomLevelAsync(url, currentDomain, false)
61}
62
63// handleZoomChange responds to zoom level changes and persists them
64func (z *ZoomController) handleZoomChange(level float64) {
65	url := z.webView.GetCurrentURL()
66	if url == "" {
67		return
68	}
69
70	go func(url string, level float64) {
71		ctx := context.Background()
72		if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
73			log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
74			return
75		}
76		key := services.ZoomKeyForLog(url)
77		log.Printf("[zoom] saved %.2f for %s", level, key)
78	}(url, level)
79
80	// Only show toast for user-initiated zoom changes, not programmatic ones
81	if !z.programmaticChange {
82		z.showZoomToast(level)
83	}
84}
85
86// showZoomToast displays a zoom level notification using TypeScript toast system
87func (z *ZoomController) showZoomToast(level float64) {
88	log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)
89
90	if z.webView == nil {
91		log.Printf("[zoom] webview unavailable for zoom toast")
92		return
93	}
94
95	if err := z.webView.DispatchCustomEvent("dumber:toast:zoom", map[string]any{"level": level}); err != nil {
96		log.Printf("[zoom] failed to dispatch zoom toast: %v", err)
97	}
98}
99
100// ApplyInitialZoom sets the initial zoom level for the current URL
101func (z *ZoomController) ApplyInitialZoom() {
102	ctx := context.Background()
103	url := z.webView.GetCurrentURL()
104	if url == "" {
105		url = "dumb://homepage"
106	}
107	if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
108		z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, false)
109	}
110}
111
112// ApplyZoomForURL applies zoom for a specific URL (used for navigation)
113func (z *ZoomController) ApplyZoomForURL(url string) {
114	if url == "" {
115		return
116	}
117	currentDomain := services.ZoomKeyForLog(url)
118	z.loadZoomLevelAsync(url, currentDomain, false)
119}
120
121// ApplyZoomForURLWithLevel applies a known zoom level without hitting the database again.
122func (z *ZoomController) ApplyZoomForURLWithLevel(url string, zoomLevel float64, allowToast bool) {
123	if z == nil || url == "" {
124		return
125	}
126	z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, allowToast)
127}
128
129func (z *ZoomController) loadZoomLevelAsync(url, domain string, allowToast bool) {
130	if z.browserService == nil || z.webView == nil || url == "" {
131		return
132	}
133	go func(url, domain string, allowToast bool) {
134		ctx := context.Background()
135		zoomLevel, err := z.browserService.GetZoomLevel(ctx, url)
136		if err != nil {
137			return
138		}
139
140		z.applyZoomLevel(url, domain, zoomLevel, allowToast)
141	}(url, domain, allowToast)
142}
143
144func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, allowToast bool) {
145	if z == nil || z.webView == nil || url == "" {
146		return
147	}
148
149	z.webView.RunOnMainThread(func() {
150		if z.webView == nil {
151			return
152		}
153		if z.currentURL != url {
154			z.currentURL = url
155		}
156
157		// Clear any existing timer
158		if z.programmaticTimer != nil {
159			z.programmaticTimer.Stop()
160		}
161
162		z.programmaticChange = true
163
164		// Reset programmatic change flag after a delay to handle async zoom events
165		z.programmaticTimer = time.AfterFunc(200*time.Millisecond, func() {
166			z.programmaticChange = false
167			z.programmaticTimer = nil
168		})
169
170		if err := z.webView.SetZoom(zoomLevel); err != nil {
171			log.Printf("Warning: failed to set zoom: %v", err)
172			return
173		}
174
175		log.Printf("[zoom] loaded %.2f for %s", zoomLevel, domain)
176		if allowToast && domain != z.lastZoomDomain && z.lastZoomDomain != "" {
177			z.showZoomToast(zoomLevel)
178		}
179		z.lastZoomDomain = domain
180	})
181}
182
183// DetachWebView releases the WebView reference and stops timers.
184func (z *ZoomController) DetachWebView() {
185	if z == nil {
186		return
187	}
188	if z.programmaticTimer != nil {
189		z.programmaticTimer.Stop()
190		z.programmaticTimer = nil
191	}
192	z.webView = nil
193}

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