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