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}
30
31// NewZoomController creates a new zoom controller
32func NewZoomController(browserService *services.BrowserService, webView *webkit.WebView) *ZoomController {
33 return &ZoomController{
34 currentURL: "dumb://homepage",
35 browserService: browserService,
36 webView: webView,
37 }
38}
39
40// RegisterHandlers sets up the zoom-related event handlers
41func (z *ZoomController) RegisterHandlers() {
42 z.webView.RegisterZoomChangedHandler(z.handleZoomChange)
43 z.webView.RegisterURIChangedHandler(z.handleURIChange)
44}
45
46// handleURIChange responds to URL changes and applies saved zoom levels
47func (z *ZoomController) handleURIChange(url string) {
48 z.currentURL = url
49 if url == "" {
50 return
51 }
52 currentDomain := services.ZoomKeyForLog(url)
53 z.loadZoomLevelAsync(url, currentDomain, false)
54}
55
56// handleZoomChange responds to zoom level changes and persists them
57func (z *ZoomController) handleZoomChange(level float64) {
58 url := z.webView.GetCurrentURL()
59 if url == "" {
60 return
61 }
62
63 go func(url string, level float64) {
64 ctx := context.Background()
65 if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
66 log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
67 return
68 }
69 key := services.ZoomKeyForLog(url)
70 log.Printf("[zoom] saved %.2f for %s", level, key)
71 }(url, level)
72
73 // Only show toast for user-initiated zoom changes, not programmatic ones
74 if !z.programmaticChange {
75 z.showZoomToast(level)
76 }
77}
78
79// showZoomToast displays a zoom level notification using TypeScript toast system
80func (z *ZoomController) showZoomToast(level float64) {
81 log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)
82
83 if z.webView == nil {
84 log.Printf("[zoom] webview unavailable for zoom toast")
85 return
86 }
87
88 if err := z.webView.DispatchCustomEvent("dumber:toast:zoom", map[string]any{"level": level}); err != nil {
89 log.Printf("[zoom] failed to dispatch zoom toast: %v", err)
90 }
91}
92
93// ApplyInitialZoom sets the initial zoom level for the current URL
94func (z *ZoomController) ApplyInitialZoom() {
95 ctx := context.Background()
96 url := z.webView.GetCurrentURL()
97 if url == "" {
98 url = "dumb://homepage"
99 }
100 if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
101 z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, false)
102 }
103}
104
105// ApplyZoomForURL applies zoom for a specific URL (used for navigation)
106func (z *ZoomController) ApplyZoomForURL(url string) {
107 if url == "" {
108 return
109 }
110 currentDomain := services.ZoomKeyForLog(url)
111 z.loadZoomLevelAsync(url, currentDomain, false)
112}
113
114// ApplyZoomForURLWithLevel applies a known zoom level without hitting the database again.
115func (z *ZoomController) ApplyZoomForURLWithLevel(url string, zoomLevel float64, allowToast bool) {
116 if z == nil || url == "" {
117 return
118 }
119 z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, allowToast)
120}
121
122func (z *ZoomController) loadZoomLevelAsync(url, domain string, allowToast bool) {
123 if z.browserService == nil || z.webView == nil || url == "" {
124 return
125 }
126 go func(url, domain string, allowToast bool) {
127 ctx := context.Background()
128 zoomLevel, err := z.browserService.GetZoomLevel(ctx, url)
129 if err != nil {
130 return
131 }
132
133 z.applyZoomLevel(url, domain, zoomLevel, allowToast)
134 }(url, domain, allowToast)
135}
136
137func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, allowToast bool) {
138 if z == nil || z.webView == nil || url == "" {
139 return
140 }
141
142 z.webView.RunOnMainThread(func() {
143 if z.webView == nil {
144 return
145 }
146 if z.currentURL != url {
147 z.currentURL = url
148 }
149
150 // Clear any existing timer
151 if z.programmaticTimer != nil {
152 z.programmaticTimer.Stop()
153 }
154
155 z.programmaticChange = true
156
157 // Reset programmatic change flag after a delay to handle async zoom events
158 z.programmaticTimer = time.AfterFunc(200*time.Millisecond, func() {
159 z.programmaticChange = false
160 z.programmaticTimer = nil
161 })
162
163 if err := z.webView.SetZoom(zoomLevel); err != nil {
164 log.Printf("Warning: failed to set zoom: %v", err)
165 return
166 }
167
168 log.Printf("[zoom] loaded %.2f for %s", zoomLevel, domain)
169 if allowToast && domain != z.lastZoomDomain && z.lastZoomDomain != "" {
170 z.showZoomToast(zoomLevel)
171 }
172 z.lastZoomDomain = domain
173 })
174}