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}