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