1package coordinator
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/infrastructure/webkit"
11 "github.com/bnema/dumber/internal/logging"
12)
13
14// OmniboxProvider provides access to omnibox operations.
15type OmniboxProvider interface {
16 ToggleOmnibox(ctx context.Context)
17 UpdateOmniboxZoom(factor float64)
18 SetOmniboxOnNavigate(fn func(url string))
19}
20
21// NavigationCoordinator handles URL navigation, history, and browser controls.
22type NavigationCoordinator struct {
23 contextProvider func() context.Context
24 navigateUC *usecase.NavigateUseCase
25 contentCoord *ContentCoordinator
26 omniboxProvider OmniboxProvider
27}
28
29const faviconPreloadTimeout = 300 * time.Millisecond
30
31// NewNavigationCoordinator creates a new NavigationCoordinator.
32func NewNavigationCoordinator(
33 ctx context.Context,
34 navigateUC *usecase.NavigateUseCase,
35 contentCoord *ContentCoordinator,
36) *NavigationCoordinator {
37 log := logging.FromContext(ctx)
38 log.Debug().Msg("creating navigation coordinator")
39 callbackLogger := *log
40
41 return &NavigationCoordinator{
42 contextProvider: func() context.Context {
43 return logging.WithContext(context.Background(), callbackLogger)
44 },
45 navigateUC: navigateUC,
46 contentCoord: contentCoord,
47 }
48}
49
50// SetOmniboxProvider sets the omnibox provider for toggle/zoom operations.
51func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) {
52 c.omniboxProvider = provider
53 if c.omniboxProvider != nil {
54 c.omniboxProvider.SetOmniboxOnNavigate(func(url string) {
55 navCtx := context.Background()
56 if c.contextProvider != nil {
57 navCtx = c.contextProvider()
58 }
59 if err := c.Navigate(navCtx, url); err != nil {
60 logging.FromContext(navCtx).Warn().Err(err).Str("url", url).Msg("omnibox-initiated navigation failed")
61 }
62 })
63 }
64}
65
66// Navigate loads a URL in the active pane using NavigateUseCase.
67// This properly handles history recording and zoom application.
68func (c *NavigationCoordinator) Navigate(ctx context.Context, url string) error {
69 log := logging.FromContext(ctx)
70 if c.contentCoord == nil {
71 log.Warn().Str("url", url).Msg("content coordinator not initialized")
72 return fmt.Errorf("content coordinator not initialized")
73 }
74
75 wv := c.contentCoord.ActiveWebView(ctx)
76 if wv == nil {
77 log.Warn().Str("url", url).Msg("no active webview for navigation")
78 return fmt.Errorf("no active webview")
79 }
80
81 // Get active pane ID for tracking
82 activePaneID := c.contentCoord.ActivePaneID(ctx)
83 paneID := string(activePaneID)
84 if activePaneID != "" {
85 // Track original URL for cross-domain redirect favicon caching
86 c.contentCoord.SetNavigationOrigin(activePaneID, url)
87 // Pre-load cached favicon asynchronously (don't block navigation start)
88 preloadCtx, cancelPreload := context.WithTimeout(ctx, faviconPreloadTimeout)
89 go func() {
90 defer cancelPreload()
91 c.contentCoord.PreloadCachedFavicon(preloadCtx, activePaneID, url)
92 }()
93 }
94
95 // Use NavigateUseCase which handles history + zoom
96 if c.navigateUC != nil {
97 input := usecase.NavigateInput{
98 URL: url,
99 PaneID: paneID,
100 WebView: wv, // webkit.WebView implements port.WebView
101 }
102 output, err := c.navigateUC.Execute(ctx, input)
103 if err != nil {
104 log.Error().Err(err).Str("url", url).Msg("navigation failed")
105 return err
106 }
107 log.Debug().
108 Str("url", url).
109 Float64("zoom", output.AppliedZoom).
110 Msg("navigation initiated via usecase")
111 return nil
112 }
113
114 // Fallback: direct navigation without usecase
115 log.Warn().Msg("navigateUC not available, using direct LoadURI")
116 if err := wv.LoadURI(ctx, url); err != nil {
117 log.Error().Err(err).Str("url", url).Msg("failed to navigate")
118 return err
119 }
120
121 log.Debug().Str("url", url).Msg("navigated active pane (direct)")
122 return nil
123}
124
125// Reload reloads the current page.
126func (c *NavigationCoordinator) Reload(ctx context.Context) error {
127 log := logging.FromContext(ctx)
128
129 wv := c.contentCoord.ActiveWebView(ctx)
130 if wv == nil {
131 log.Debug().Msg("no active webview for reload")
132 return nil
133 }
134
135 if c.navigateUC != nil {
136 return c.navigateUC.Reload(ctx, wv, false)
137 }
138
139 return wv.Reload(ctx)
140}
141
142// HardReload reloads the current page bypassing cache.
143func (c *NavigationCoordinator) HardReload(ctx context.Context) error {
144 log := logging.FromContext(ctx)
145
146 wv := c.contentCoord.ActiveWebView(ctx)
147 if wv == nil {
148 log.Debug().Msg("no active webview for hard reload")
149 return nil
150 }
151
152 if c.navigateUC != nil {
153 return c.navigateUC.Reload(ctx, wv, true)
154 }
155
156 return wv.ReloadBypassCache(ctx)
157}
158
159// GoBack navigates back in history.
160// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
161func (c *NavigationCoordinator) GoBack(ctx context.Context) error {
162 log := logging.FromContext(ctx)
163
164 wv := c.contentCoord.ActiveWebView(ctx)
165 if wv == nil {
166 log.Debug().Msg("no active webview for go back")
167 return nil
168 }
169
170 return wv.GoBack(ctx)
171}
172
173// GoForward navigates forward in history.
174// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
175func (c *NavigationCoordinator) GoForward(ctx context.Context) error {
176 log := logging.FromContext(ctx)
177
178 wv := c.contentCoord.ActiveWebView(ctx)
179 if wv == nil {
180 log.Debug().Msg("no active webview for go forward")
181 return nil
182 }
183
184 return wv.GoForward(ctx)
185}
186
187// OpenOmnibox toggles the omnibox visibility.
188func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error {
189 log := logging.FromContext(ctx)
190
191 if c.omniboxProvider == nil {
192 log.Error().Msg("omnibox provider not initialized")
193 return fmt.Errorf("omnibox provider not initialized")
194 }
195
196 log.Debug().Msg("toggling omnibox")
197 c.omniboxProvider.ToggleOmnibox(ctx)
198 return nil
199}
200
201// OpenDevTools opens the WebKit inspector for the active WebView.
202func (c *NavigationCoordinator) OpenDevTools(ctx context.Context) error {
203 log := logging.FromContext(ctx)
204
205 wv := c.contentCoord.ActiveWebView(ctx)
206 if wv == nil {
207 log.Warn().Msg("no active webview for devtools")
208 return fmt.Errorf("no active webview")
209 }
210
211 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening devtools")
212
213 // Type assert to access ShowDevTools (not in port.WebView interface)
214 if webkitWV, ok := interface{}(wv).(*webkit.WebView); ok {
215 return webkitWV.ShowDevTools()
216 }
217
218 return fmt.Errorf("webview does not support devtools")
219}
220
221// PrintPage opens the print dialog for the active WebView.
222func (c *NavigationCoordinator) PrintPage(ctx context.Context) error {
223 log := logging.FromContext(ctx)
224
225 wv := c.contentCoord.ActiveWebView(ctx)
226 if wv == nil {
227 log.Warn().Msg("no active webview for print")
228 return fmt.Errorf("no active webview")
229 }
230
231 log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening print dialog")
232
233 // Type assert to access Print (not in port.WebView interface)
234 if webkitWV, ok := interface{}(wv).(*webkit.WebView); ok {
235 return webkitWV.Print()
236 }
237
238 return fmt.Errorf("webview does not support printing")
239}
240
241// UpdateHistoryTitle updates the title of a history entry after page load.
242func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
243 log := logging.FromContext(ctx)
244
245 if c.navigateUC == nil {
246 return
247 }
248
249 if err := c.navigateUC.UpdateHistoryTitle(ctx, url, title); err != nil {
250 log.Warn().Err(err).Str("url", url).Msg("failed to update history title")
251 }
252}
253
254// RecordHistory records a URL in history on page commit.
255func (c *NavigationCoordinator) RecordHistory(ctx context.Context, paneID entity.PaneID, url string) {
256 if c.navigateUC == nil {
257 return
258 }
259
260 c.navigateUC.RecordHistory(ctx, string(paneID), url)
261}
262
263// ClearPaneHistory clears per-pane navigation history deduplication state.
264func (c *NavigationCoordinator) ClearPaneHistory(paneID entity.PaneID) {
265 if c.navigateUC == nil {
266 return
267 }
268 c.navigateUC.ClearPaneHistory(string(paneID))
269}
270
271// ActiveWebView returns the WebView for the active pane (for zoom operations).
272func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
273 return c.contentCoord.ActiveWebView(ctx)
274}
275
276// NotifyZoomChanged updates the omnibox zoom indicator.
277func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
278 if c.omniboxProvider != nil {
279 c.omniboxProvider.UpdateOmniboxZoom(factor)
280 }
281}