detached 2708968

feat(ui): apply filters to existing webviews and show filter status

commit
2708968e43938578902ebf24661a060e83165a6b
author
brice · · 2025-12-18 22:48 UTC
committer
brice · · 2025-12-18 22:27 UTC
parent
8987eec
signature
unsigned
3 files · +74 -0
M internal/ui/app.go +59 -0
@@ -8,10 +8,12 @@ import (
88 "sync"
99
1010 "github.com/jwijenbergh/puregotk/v4/gio"
11+ "github.com/jwijenbergh/puregotk/v4/glib"
1112 "github.com/jwijenbergh/puregotk/v4/gtk"
1213
1314 "github.com/bnema/dumber/internal/application/usecase"
1415 "github.com/bnema/dumber/internal/domain/entity"
16+ "github.com/bnema/dumber/internal/infrastructure/filtering"
1517 "github.com/bnema/dumber/internal/infrastructure/webkit"
1618 "github.com/bnema/dumber/internal/infrastructure/webkit/handlers"
1719 "github.com/bnema/dumber/internal/logging"
@@ -71,6 +73,9 @@ type App struct {
7173 settings *webkit.SettingsManager
7274 faviconAdapter *adapter.FaviconAdapter
7375
76+ // App-level toaster for system notifications (filter status, etc.)
77+ appToaster *component.Toaster
78+
7479 // ID generator for tabs/panes
7580 idCounter uint64
7681 idMu sync.Mutex
@@ -179,6 +184,14 @@ func (a *App) onActivate(ctx context.Context) {
179184 // Initialize stacked pane manager for incremental widget operations
180185 a.stackedPaneMgr = component.NewStackedPaneManager(a.widgetFactory)
181186
187+ // Create app-level toaster for system notifications
188+ a.appToaster = component.NewToaster(a.widgetFactory)
189+ if toasterWidget := a.appToaster.Widget(); toasterWidget != nil {
190+ if gtkWidget := toasterWidget.GtkWidget(); gtkWidget != nil {
191+ a.mainWindow.AddOverlay(gtkWidget)
192+ }
193+ }
194+
182195 // Initialize focus manager for geometric navigation
183196 a.focusMgr = focus.NewManager(a.panesUC)
184197
@@ -240,6 +253,9 @@ func (a *App) onActivate(ctx context.Context) {
240253 a.mainWindow.Show()
241254
242255 log.Info().Msg("main window displayed")
256+
257+ // Start async filter loading after window is visible
258+ a.initFilteringAsync(ctx)
243259 }
244260
245261 // onShutdown is called when the GTK application is shutting down.
@@ -617,3 +633,46 @@ func (a *App) SetOmniboxOnNavigate(fn func(url string)) {
617633 // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox
618634 // The WorkspaceView uses the stored omniboxCfg and sets up navigation via the omnibox's SetOnNavigate
619635 }
636+
637+// initFilteringAsync starts background filter loading with toast feedback.
638+func (a *App) initFilteringAsync(ctx context.Context) {
639+ if a.deps.FilterManager == nil {
640+ return
641+ }
642+
643+ a.deps.FilterManager.SetStatusCallback(func(status filtering.FilterStatus) {
644+ statusCopy := status // Capture for closure
645+ var cb glib.SourceFunc
646+ cb = func(_ uintptr) bool {
647+ a.showFilterStatus(ctx, statusCopy)
648+ return false // Don't repeat
649+ }
650+ glib.IdleAdd(&cb, 0)
651+ })
652+ a.deps.FilterManager.LoadAsync(ctx)
653+}
654+
655+// showFilterStatus displays toast notification for filter status.
656+func (a *App) showFilterStatus(ctx context.Context, status filtering.FilterStatus) {
657+ log := logging.FromContext(ctx)
658+
659+ switch status.State {
660+ case filtering.StateLoading:
661+ if a.appToaster != nil {
662+ a.appToaster.Show(ctx, status.Message, component.ToastInfo)
663+ }
664+ case filtering.StateActive:
665+ // Apply filters to existing webviews that were created before filters loaded
666+ if a.contentCoord != nil && a.deps.FilterManager != nil {
667+ a.contentCoord.ApplyFiltersToAll(ctx, a.deps.FilterManager)
668+ log.Debug().Msg("applied filters to all existing webviews")
669+ }
670+ if a.appToaster != nil {
671+ a.appToaster.Show(ctx, fmt.Sprintf("Filters active (%s)", status.Version), component.ToastSuccess)
672+ }
673+ case filtering.StateError:
674+ if a.appToaster != nil {
675+ a.appToaster.Show(ctx, "Filter load failed: "+status.Message, component.ToastError)
676+ }
677+ }
678+}
@@ -8,10 +8,12 @@ import (
8 "sync"8 "sync"
9 9
10 "github.com/jwijenbergh/puregotk/v4/gio"10 "github.com/jwijenbergh/puregotk/v4/gio"
11+ "github.com/jwijenbergh/puregotk/v4/glib"
11 "github.com/jwijenbergh/puregotk/v4/gtk"12 "github.com/jwijenbergh/puregotk/v4/gtk"
12 13
13 "github.com/bnema/dumber/internal/application/usecase"14 "github.com/bnema/dumber/internal/application/usecase"
14 "github.com/bnema/dumber/internal/domain/entity"15 "github.com/bnema/dumber/internal/domain/entity"
16+ "github.com/bnema/dumber/internal/infrastructure/filtering"
15 "github.com/bnema/dumber/internal/infrastructure/webkit"17 "github.com/bnema/dumber/internal/infrastructure/webkit"
16 "github.com/bnema/dumber/internal/infrastructure/webkit/handlers"18 "github.com/bnema/dumber/internal/infrastructure/webkit/handlers"
17 "github.com/bnema/dumber/internal/logging"19 "github.com/bnema/dumber/internal/logging"
@@ -71,6 +73,9 @@ type App struct {
71 settings *webkit.SettingsManager73 settings *webkit.SettingsManager
72 faviconAdapter *adapter.FaviconAdapter74 faviconAdapter *adapter.FaviconAdapter
73 75
76+ // App-level toaster for system notifications (filter status, etc.)
77+ appToaster *component.Toaster
78+
74 // ID generator for tabs/panes79 // ID generator for tabs/panes
75 idCounter uint6480 idCounter uint64
76 idMu sync.Mutex81 idMu sync.Mutex
@@ -179,6 +184,14 @@ func (a *App) onActivate(ctx context.Context) {
179 // Initialize stacked pane manager for incremental widget operations184 // Initialize stacked pane manager for incremental widget operations
180 a.stackedPaneMgr = component.NewStackedPaneManager(a.widgetFactory)185 a.stackedPaneMgr = component.NewStackedPaneManager(a.widgetFactory)
181 186
187+ // Create app-level toaster for system notifications
188+ a.appToaster = component.NewToaster(a.widgetFactory)
189+ if toasterWidget := a.appToaster.Widget(); toasterWidget != nil {
190+ if gtkWidget := toasterWidget.GtkWidget(); gtkWidget != nil {
191+ a.mainWindow.AddOverlay(gtkWidget)
192+ }
193+ }
194+
182 // Initialize focus manager for geometric navigation195 // Initialize focus manager for geometric navigation
183 a.focusMgr = focus.NewManager(a.panesUC)196 a.focusMgr = focus.NewManager(a.panesUC)
184 197
@@ -240,6 +253,9 @@ func (a *App) onActivate(ctx context.Context) {
240 a.mainWindow.Show()253 a.mainWindow.Show()
241 254
242 log.Info().Msg("main window displayed")255 log.Info().Msg("main window displayed")
256+
257+ // Start async filter loading after window is visible
258+ a.initFilteringAsync(ctx)
243 }259 }
244 260
245 // onShutdown is called when the GTK application is shutting down.261 // onShutdown is called when the GTK application is shutting down.
@@ -617,3 +633,46 @@ func (a *App) SetOmniboxOnNavigate(fn func(url string)) {
617 // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox633 // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox
618 // The WorkspaceView uses the stored omniboxCfg and sets up navigation via the omnibox's SetOnNavigate634 // The WorkspaceView uses the stored omniboxCfg and sets up navigation via the omnibox's SetOnNavigate
619 }635 }
636+
637+// initFilteringAsync starts background filter loading with toast feedback.
638+func (a *App) initFilteringAsync(ctx context.Context) {
639+ if a.deps.FilterManager == nil {
640+ return
641+ }
642+
643+ a.deps.FilterManager.SetStatusCallback(func(status filtering.FilterStatus) {
644+ statusCopy := status // Capture for closure
645+ var cb glib.SourceFunc
646+ cb = func(_ uintptr) bool {
647+ a.showFilterStatus(ctx, statusCopy)
648+ return false // Don't repeat
649+ }
650+ glib.IdleAdd(&cb, 0)
651+ })
652+ a.deps.FilterManager.LoadAsync(ctx)
653+}
654+
655+// showFilterStatus displays toast notification for filter status.
656+func (a *App) showFilterStatus(ctx context.Context, status filtering.FilterStatus) {
657+ log := logging.FromContext(ctx)
658+
659+ switch status.State {
660+ case filtering.StateLoading:
661+ if a.appToaster != nil {
662+ a.appToaster.Show(ctx, status.Message, component.ToastInfo)
663+ }
664+ case filtering.StateActive:
665+ // Apply filters to existing webviews that were created before filters loaded
666+ if a.contentCoord != nil && a.deps.FilterManager != nil {
667+ a.contentCoord.ApplyFiltersToAll(ctx, a.deps.FilterManager)
668+ log.Debug().Msg("applied filters to all existing webviews")
669+ }
670+ if a.appToaster != nil {
671+ a.appToaster.Show(ctx, fmt.Sprintf("Filters active (%s)", status.Version), component.ToastSuccess)
672+ }
673+ case filtering.StateError:
674+ if a.appToaster != nil {
675+ a.appToaster.Show(ctx, "Filter load failed: "+status.Message, component.ToastError)
676+ }
677+ }
678+}
M internal/ui/coordinator/content.go +13 -0
@@ -297,6 +297,19 @@ func (c *ContentCoordinator) RegisterPopupWebView(paneID entity.PaneID, wv *webk
297297 }
298298 }
299299
300+// ApplyFiltersToAll applies content filters to all active webviews.
301+// Called when filters become available after webviews were already created.
302+func (c *ContentCoordinator) ApplyFiltersToAll(ctx context.Context, applier webkit.FilterApplier) {
303+ log := logging.FromContext(ctx)
304+
305+ for paneID, wv := range c.webViews {
306+ if wv != nil && !wv.IsDestroyed() {
307+ applier.ApplyTo(ctx, wv.UserContentManager())
308+ log.Debug().Str("pane_id", string(paneID)).Msg("applied filters to existing webview")
309+ }
310+ }
311+}
312+
300313 // GetTitle returns the current title for a pane.
301314 func (c *ContentCoordinator) GetTitle(paneID entity.PaneID) string {
302315 c.titleMu.RLock()
@@ -297,6 +297,19 @@ func (c *ContentCoordinator) RegisterPopupWebView(paneID entity.PaneID, wv *webk
297 }297 }
298 }298 }
299 299
300+// ApplyFiltersToAll applies content filters to all active webviews.
301+// Called when filters become available after webviews were already created.
302+func (c *ContentCoordinator) ApplyFiltersToAll(ctx context.Context, applier webkit.FilterApplier) {
303+ log := logging.FromContext(ctx)
304+
305+ for paneID, wv := range c.webViews {
306+ if wv != nil && !wv.IsDestroyed() {
307+ applier.ApplyTo(ctx, wv.UserContentManager())
308+ log.Debug().Str("pane_id", string(paneID)).Msg("applied filters to existing webview")
309+ }
310+ }
311+}
312+
300 // GetTitle returns the current title for a pane.313 // GetTitle returns the current title for a pane.
301 func (c *ContentCoordinator) GetTitle(paneID entity.PaneID) string {314 func (c *ContentCoordinator) GetTitle(paneID entity.PaneID) string {
302 c.titleMu.RLock()315 c.titleMu.RLock()
M internal/ui/deps.go +2 -0
@@ -9,6 +9,7 @@ import (
99 "github.com/bnema/dumber/internal/domain/repository"
1010 "github.com/bnema/dumber/internal/infrastructure/config"
1111 "github.com/bnema/dumber/internal/infrastructure/favicon"
12+ "github.com/bnema/dumber/internal/infrastructure/filtering"
1213 "github.com/bnema/dumber/internal/infrastructure/webkit"
1314 "github.com/bnema/dumber/internal/ui/theme"
1415 )
@@ -50,6 +51,7 @@ type Dependencies struct {
5051 // Infrastructure Adapters
5152 Clipboard port.Clipboard
5253 FaviconService *favicon.Service
54+ FilterManager *filtering.Manager
5355 }
5456
5557 // Validate checks that all required dependencies are set.
@@ -9,6 +9,7 @@ import (
9 "github.com/bnema/dumber/internal/domain/repository"9 "github.com/bnema/dumber/internal/domain/repository"
10 "github.com/bnema/dumber/internal/infrastructure/config"10 "github.com/bnema/dumber/internal/infrastructure/config"
11 "github.com/bnema/dumber/internal/infrastructure/favicon"11 "github.com/bnema/dumber/internal/infrastructure/favicon"
12+ "github.com/bnema/dumber/internal/infrastructure/filtering"
12 "github.com/bnema/dumber/internal/infrastructure/webkit"13 "github.com/bnema/dumber/internal/infrastructure/webkit"
13 "github.com/bnema/dumber/internal/ui/theme"14 "github.com/bnema/dumber/internal/ui/theme"
14 )15 )
@@ -50,6 +51,7 @@ type Dependencies struct {
50 // Infrastructure Adapters51 // Infrastructure Adapters
51 Clipboard port.Clipboard52 Clipboard port.Clipboard
52 FaviconService *favicon.Service53 FaviconService *favicon.Service
54+ FilterManager *filtering.Manager
53 }55 }
54 56
55 // Validate checks that all required dependencies are set.57 // Validate checks that all required dependencies are set.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close