browser_window_favorites_sidebar.go RawHistoryFolder
1package ui
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/dumber/internal/ui/component"
12)
13
14// newFavoritesSidebarComponent constructs the native favorites sidebar. It is
15// a package-level seam so headless tests can observe on-demand construction
16// without instantiating GTK widgets. Mutable global: tests must restore it
17// via defer and must not use t.Parallel alongside seam users.
18var newFavoritesSidebarComponent = component.NewFavoritesSidebar
19
20// ensureFavoritesSidebar constructs the favorites sidebar on first request.
21// Construction is separated from mounting and shared visibility mutation:
22// the caller mounts and reveals through mountNativeSidebar/showFavoritesSidebar.
23// The component lifetime is detached from the caller's request context via
24// context.WithoutCancel so a short-lived window-creation or shortcut context
25// cannot cancel the sidebar's in-flight loads. It returns false when the
26// window, dependencies, or native construction are unavailable.
27func (bw *browserWindow) ensureFavoritesSidebar(ctx context.Context, a *App) bool {
28 if bw == nil || a == nil || bw.mainWindow == nil {
29 return false
30 }
31 if bw.favoritesSidebar != nil {
32 return true
33 }
34 if a.deps == nil || a.deps.FavoritesUC == nil {
35 return false
36 }
37 log := logging.FromContext(ctx)
38
39 cfg := a.buildFavoritesSidebarConfig(bw)
40 sidebar := newFavoritesSidebarComponent(context.WithoutCancel(ctx), cfg)
41 if sidebar == nil {
42 log.Warn().Msg("failed to create favorites sidebar")
43 return false
44 }
45
46 bw.favoritesSidebar = sidebar
47 bw.applySidebarWidthConfig(a)
48 log.Debug().Msg("favorites sidebar initialized")
49 return true
50}
51
52//nolint:dupl // Favorites and history sidebar configs intentionally mirror each other for separate component types.
53func (a *App) buildFavoritesSidebarConfig(bw *browserWindow) component.FavoritesSidebarConfig {
54 var favoritesUC port.FavoritesSidebarFavorites
55 if a.deps != nil {
56 favoritesUC = a.deps.FavoritesUC
57 }
58 return component.FavoritesSidebarConfig{
59 FavoritesUC: favoritesUC,
60 OnNavigate: func(navCtx context.Context, url string) error {
61 return a.navigateFavoritesSidebarSelection(navCtx, bw, url)
62 },
63 OnNavigateKeepOpen: func(navCtx context.Context, url string) error {
64 return a.navigateFavoritesSidebarSelection(navCtx, bw, url)
65 },
66 OnOpenInNewPane: func(splitCtx context.Context, url string) error {
67 if a.wsCoord == nil || !a.hasBrowserWindow(bw) {
68 return nil
69 }
70 a.activateBrowserWindow(bw)
71 return a.wsCoord.SplitWithURL(splitCtx, usecase.SplitRight, url)
72 },
73 OnClose: func() {
74 a.hideAndRestoreFocusForBrowserWindow(bw)
75 },
76 }
77}
78
79func (a *App) navigateFavoritesSidebarSelection(ctx context.Context, bw *browserWindow, url string) error {
80 if a == nil || bw == nil || !a.hasBrowserWindow(bw) {
81 return nil
82 }
83 return a.navigateFromBrowserWindow(ctx, bw, url)
84}
85
86func (bw *browserWindow) toggleFavoritesSidebar() {
87 if bw == nil || bw.favoritesSidebar == nil {
88 return
89 }
90 if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites {
91 bw.hideFavoritesSidebar()
92 return
93 }
94 bw.showFavoritesSidebar()
95}
96
97func (bw *browserWindow) showFavoritesSidebar() {
98 if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil {
99 return
100 }
101 bw.mountNativeSidebar(nativeSidebarFavorites)
102 bw.favoritesSidebar.Show()
103 bw.mainWindow.SetSidebarVisible(true)
104 bw.sidebarVisible = true
105 bw.activeSidebarKind = nativeSidebarFavorites
106}
107
108func (bw *browserWindow) hideFavoritesSidebar() {
109 if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil {
110 return
111 }
112 bw.favoritesSidebar.Hide()
113 bw.mainWindow.SetSidebarVisible(false)
114 bw.sidebarVisible = false
115 if bw.activeSidebarKind == nativeSidebarFavorites {
116 bw.activeSidebarKind = nativeSidebarNone
117 }
118}
119
120func (a *App) toggleFavoritesSidebarAction(ctx context.Context) error {
121 bw := a.lastFocusedBrowserWindow()
122 if bw == nil {
123 return fmt.Errorf("favorites sidebar unavailable: no focused browser window")
124 }
125 if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites {
126 a.hideAndRestoreFocusForBrowserWindow(bw)
127 return nil
128 }
129 if !bw.ensureFavoritesSidebar(ctx, a) {
130 return fmt.Errorf("favorites sidebar unavailable: native sidebar not initialized")
131 }
132 bw.toggleFavoritesSidebar()
133 return nil
134}
135
136func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
137 if a == nil || a.deps == nil || a.deps.FavoritesUC == nil {
138 return fmt.Errorf("favorites unavailable: usecase not configured")
139 }
140 bw := a.lastFocusedBrowserWindow()
141 _, wv := a.activeWebViewForBrowserWindow(bw)
142 if wv == nil {
143 return fmt.Errorf("favorites unavailable: no active webview")
144 }
145 uri := strings.TrimSpace(wv.URI())
146 if uri == "" {
147 return fmt.Errorf("favorites unavailable: active page has no URI")
148 }
149 result, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title())
150 if err != nil {
151 return err
152 }
153 if result != nil && strings.TrimSpace(result.Message) != "" {
154 a.showToastOnBrowserWindow(ctx, bw, result.Message, component.ToastSuccess,
155 component.WithDuration(component.ToastBriefDurationMs),
156 component.WithPosition(component.ToastPositionBottomRight),
157 )
158 }
159 a.reloadVisibleFavoritesSidebars("current-page-favorite-toggle")
160 return nil
161}
162
163func (a *App) reloadVisibleFavoritesSidebars(reason string) {
164 if a == nil {
165 return
166 }
167 for _, bw := range a.browserWindows {
168 if bw == nil || bw.favoritesSidebar == nil {
169 continue
170 }
171 bw.favoritesSidebar.RequestReloadIfVisible(reason)
172 }
173}