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 ( | |||
| 8 | 8 | "sync" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/jwijenbergh/puregotk/v4/gio" | |
| 11 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 11 | 12 | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 12 | 13 | ||
| 13 | 14 | "github.com/bnema/dumber/internal/application/usecase" | |
| 14 | 15 | "github.com/bnema/dumber/internal/domain/entity" | |
| 16 | + | "github.com/bnema/dumber/internal/infrastructure/filtering" | |
| 15 | 17 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 16 | 18 | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | |
| 17 | 19 | "github.com/bnema/dumber/internal/logging" | |
| @@ -71,6 +73,9 @@ type App struct { | |||
| 71 | 73 | settings *webkit.SettingsManager | |
| 72 | 74 | faviconAdapter *adapter.FaviconAdapter | |
| 73 | 75 | ||
| 76 | + | // App-level toaster for system notifications (filter status, etc.) | |
| 77 | + | appToaster *component.Toaster | |
| 78 | + | ||
| 74 | 79 | // ID generator for tabs/panes | |
| 75 | 80 | idCounter uint64 | |
| 76 | 81 | idMu sync.Mutex | |
| @@ -179,6 +184,14 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 179 | 184 | // Initialize stacked pane manager for incremental widget operations | |
| 180 | 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 | 195 | // Initialize focus manager for geometric navigation | |
| 183 | 196 | a.focusMgr = focus.NewManager(a.panesUC) | |
| 184 | 197 | ||
| @@ -240,6 +253,9 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 240 | 253 | a.mainWindow.Show() | |
| 241 | 254 | ||
| 242 | 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 | 261 | // onShutdown is called when the GTK application is shutting down. | |
| @@ -617,3 +633,46 @@ func (a *App) SetOmniboxOnNavigate(fn func(url string)) { | |||
| 617 | 633 | // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox | |
| 618 | 634 | // 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 | + | } | |
| @@ -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.SettingsManager | 73 | settings *webkit.SettingsManager | ||
| 72 | faviconAdapter *adapter.FaviconAdapter | 74 | 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/panes | 79 | // ID generator for tabs/panes | ||
| 75 | idCounter uint64 | 80 | idCounter uint64 | ||
| 76 | idMu sync.Mutex | 81 | idMu sync.Mutex | ||
| @@ -179,6 +184,14 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 179 | // Initialize stacked pane manager for incremental widget operations | 184 | // 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 navigation | 195 | // 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.ShowOmnibox | 633 | // 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 SetOnNavigate | 634 | // 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 | |||
| 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 | 313 | // GetTitle returns the current title for a pane. | |
| 301 | 314 | func (c *ContentCoordinator) GetTitle(paneID entity.PaneID) string { | |
| 302 | 315 | 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 ( | |||
| 9 | 9 | "github.com/bnema/dumber/internal/domain/repository" | |
| 10 | 10 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 11 | 11 | "github.com/bnema/dumber/internal/infrastructure/favicon" | |
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/filtering" | |
| 12 | 13 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 13 | 14 | "github.com/bnema/dumber/internal/ui/theme" | |
| 14 | 15 | ) | |
| @@ -50,6 +51,7 @@ type Dependencies struct { | |||
| 50 | 51 | // Infrastructure Adapters | |
| 51 | 52 | Clipboard port.Clipboard | |
| 52 | 53 | FaviconService *favicon.Service | |
| 54 | + | FilterManager *filtering.Manager | |
| 53 | 55 | } | |
| 54 | 56 | ||
| 55 | 57 | // 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 Adapters | 51 | // Infrastructure Adapters | ||
| 51 | Clipboard port.Clipboard | 52 | Clipboard port.Clipboard | ||
| 52 | FaviconService *favicon.Service | 53 | 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. | ||