feat(app): wire favicon support through coordinators and omnibox
Initialize FaviconCache in App and inject into ContentCoordinator and Omnibox. Wire favicon callbacks in ContentCoordinator to track page favicons and update cache. Track navigation origins in NavigationCoordinator to handle cross-domain redirects (e.g., google.fr to google.com). Refactor Omnibox row creation to unified createRowWithFavicon method supporting async favicon loading from cache with icon fallback. Display favicon images in suggestion and favorite rows.
- commit
- f9e2d438a9b7afb8427f28634e24031afb42e1fa
- author
- brice · · 2025-12-15 18:19 UTC
- committer
- brice · · 2025-12-15 21:53 UTC
- parent
- 458bd84
- signature
- unsigned
4 files · +165 -49
M internal/ui/app.go +15 -4
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/bnema/dumber/internal/domain/entity" | |
| 14 | 14 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 15 | 15 | "github.com/bnema/dumber/internal/logging" | |
| 16 | + | "github.com/bnema/dumber/internal/ui/cache" | |
| 16 | 17 | "github.com/bnema/dumber/internal/ui/component" | |
| 17 | 18 | "github.com/bnema/dumber/internal/ui/coordinator" | |
| 18 | 19 | "github.com/bnema/dumber/internal/ui/dispatcher" | |
| @@ -61,10 +62,11 @@ type App struct { | |||
| 61 | 62 | omnibox *component.Omnibox | |
| 62 | 63 | ||
| 63 | 64 | // Web content (managed by ContentCoordinator) | |
| 64 | - | pool *webkit.WebViewPool | |
| 65 | - | injector *webkit.ContentInjector | |
| 66 | - | router *webkit.MessageRouter | |
| 67 | - | settings *webkit.SettingsManager | |
| 65 | + | pool *webkit.WebViewPool | |
| 66 | + | injector *webkit.ContentInjector | |
| 67 | + | router *webkit.MessageRouter | |
| 68 | + | settings *webkit.SettingsManager | |
| 69 | + | faviconCache *cache.FaviconCache | |
| 68 | 70 | ||
| 69 | 71 | // ID generator for tabs/panes | |
| 70 | 72 | idCounter uint64 | |
| @@ -190,6 +192,7 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 190 | 192 | a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{ | |
| 191 | 193 | HistoryUC: a.deps.HistoryUC, | |
| 192 | 194 | FavoritesUC: a.deps.FavoritesUC, | |
| 195 | + | FaviconCache: a.faviconCache, | |
| 193 | 196 | Shortcuts: a.deps.Config.SearchShortcuts, | |
| 194 | 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | |
| 195 | 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | |
| @@ -241,11 +244,19 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 241 | 244 | return a.activeWorkspace(), a.activeWorkspaceView() | |
| 242 | 245 | } | |
| 243 | 246 | ||
| 247 | + | // Create FaviconCache with FaviconDatabase from WebKitContext | |
| 248 | + | if a.deps.WebContext != nil { | |
| 249 | + | a.faviconCache = cache.NewFaviconCache(a.deps.WebContext.FaviconDatabase()) | |
| 250 | + | } else { | |
| 251 | + | a.faviconCache = cache.NewFaviconCache(nil) | |
| 252 | + | } | |
| 253 | + | ||
| 244 | 254 | // 1. Content Coordinator (no dependencies on other coordinators) | |
| 245 | 255 | a.contentCoord = coordinator.NewContentCoordinator( | |
| 246 | 256 | ctx, | |
| 247 | 257 | a.pool, | |
| 248 | 258 | a.widgetFactory, | |
| 259 | + | a.faviconCache, | |
| 249 | 260 | getActiveWS, | |
| 250 | 261 | ) | |
| 251 | 262 | ||
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "github.com/bnema/dumber/internal/domain/entity" | 13 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 14 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 14 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 15 | "github.com/bnema/dumber/internal/logging" | 15 | "github.com/bnema/dumber/internal/logging" | ||
| 16 | + | "github.com/bnema/dumber/internal/ui/cache" | |||
| 16 | "github.com/bnema/dumber/internal/ui/component" | 17 | "github.com/bnema/dumber/internal/ui/component" | ||
| 17 | "github.com/bnema/dumber/internal/ui/coordinator" | 18 | "github.com/bnema/dumber/internal/ui/coordinator" | ||
| 18 | "github.com/bnema/dumber/internal/ui/dispatcher" | 19 | "github.com/bnema/dumber/internal/ui/dispatcher" | ||
| @@ -61,10 +62,11 @@ type App struct { | |||||
| 61 | omnibox *component.Omnibox | 62 | omnibox *component.Omnibox | ||
| 62 | 63 | ||||
| 63 | // Web content (managed by ContentCoordinator) | 64 | // Web content (managed by ContentCoordinator) | ||
| 64 | - | pool *webkit.WebViewPool | 65 | + | pool *webkit.WebViewPool |
| 65 | - | injector *webkit.ContentInjector | 66 | + | injector *webkit.ContentInjector |
| 66 | - | router *webkit.MessageRouter | 67 | + | router *webkit.MessageRouter |
| 67 | - | settings *webkit.SettingsManager | 68 | + | settings *webkit.SettingsManager |
| 69 | + | faviconCache *cache.FaviconCache | |||
| 68 | 70 | ||||
| 69 | // ID generator for tabs/panes | 71 | // ID generator for tabs/panes | ||
| 70 | idCounter uint64 | 72 | idCounter uint64 | ||
| @@ -190,6 +192,7 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 190 | a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{ | 192 | a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{ | ||
| 191 | HistoryUC: a.deps.HistoryUC, | 193 | HistoryUC: a.deps.HistoryUC, | ||
| 192 | FavoritesUC: a.deps.FavoritesUC, | 194 | FavoritesUC: a.deps.FavoritesUC, | ||
| 195 | + | FaviconCache: a.faviconCache, | |||
| 193 | Shortcuts: a.deps.Config.SearchShortcuts, | 196 | Shortcuts: a.deps.Config.SearchShortcuts, | ||
| 194 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | ||
| 195 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | ||
| @@ -241,11 +244,19 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 241 | return a.activeWorkspace(), a.activeWorkspaceView() | 244 | return a.activeWorkspace(), a.activeWorkspaceView() | ||
| 242 | } | 245 | } | ||
| 243 | 246 | ||||
| 247 | + | // Create FaviconCache with FaviconDatabase from WebKitContext | |||
| 248 | + | if a.deps.WebContext != nil { | |||
| 249 | + | a.faviconCache = cache.NewFaviconCache(a.deps.WebContext.FaviconDatabase()) | |||
| 250 | + | } else { | |||
| 251 | + | a.faviconCache = cache.NewFaviconCache(nil) | |||
| 252 | + | } | |||
| 253 | + | ||||
| 244 | // 1. Content Coordinator (no dependencies on other coordinators) | 254 | // 1. Content Coordinator (no dependencies on other coordinators) | ||
| 245 | a.contentCoord = coordinator.NewContentCoordinator( | 255 | a.contentCoord = coordinator.NewContentCoordinator( | ||
| 246 | ctx, | 256 | ctx, | ||
| 247 | a.pool, | 257 | a.pool, | ||
| 248 | a.widgetFactory, | 258 | a.widgetFactory, | ||
| 259 | + | a.faviconCache, | |||
| 249 | getActiveWS, | 260 | getActiveWS, | ||
| 250 | ) | 261 | ) | ||
| 251 | 262 | ||||
M internal/ui/component/omnibox.go +43 -45
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "github.com/bnema/dumber/internal/domain/entity" | |
| 11 | 11 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 12 | 12 | "github.com/bnema/dumber/internal/logging" | |
| 13 | + | "github.com/bnema/dumber/internal/ui/cache" | |
| 13 | 14 | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 14 | 15 | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 15 | 16 | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| @@ -71,6 +72,7 @@ type Omnibox struct { | |||
| 71 | 72 | // Dependencies | |
| 72 | 73 | historyUC *usecase.SearchHistoryUseCase | |
| 73 | 74 | favoritesUC *usecase.ManageFavoritesUseCase | |
| 75 | + | faviconCache *cache.FaviconCache | |
| 74 | 76 | shortcuts map[string]config.SearchShortcut | |
| 75 | 77 | defaultSearch string | |
| 76 | 78 | initialBehavior string | |
| @@ -90,6 +92,7 @@ type Omnibox struct { | |||
| 90 | 92 | type OmniboxConfig struct { | |
| 91 | 93 | HistoryUC *usecase.SearchHistoryUseCase | |
| 92 | 94 | FavoritesUC *usecase.ManageFavoritesUseCase | |
| 95 | + | FaviconCache *cache.FaviconCache | |
| 93 | 96 | Shortcuts map[string]config.SearchShortcut | |
| 94 | 97 | DefaultSearch string | |
| 95 | 98 | InitialBehavior string | |
| @@ -105,6 +108,7 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||
| 105 | 108 | selectedIndex: -1, | |
| 106 | 109 | historyUC: cfg.HistoryUC, | |
| 107 | 110 | favoritesUC: cfg.FavoritesUC, | |
| 111 | + | faviconCache: cfg.FaviconCache, | |
| 108 | 112 | shortcuts: cfg.Shortcuts, | |
| 109 | 113 | defaultSearch: cfg.DefaultSearch, | |
| 110 | 114 | initialBehavior: cfg.InitialBehavior, | |
| @@ -535,55 +539,34 @@ func (o *Omnibox) rebuildList() { | |||
| 535 | 539 | } | |
| 536 | 540 | } | |
| 537 | 541 | ||
| 538 | - | // createSuggestionRow creates a ListBoxRow for a suggestion. | |
| 539 | - | func (o *Omnibox) createSuggestionRow(s Suggestion, index int) *gtk.ListBoxRow { | |
| 540 | - | row := gtk.NewListBoxRow() | |
| 541 | - | if row == nil { | |
| 542 | + | // createFaviconImage creates a favicon image with async loading from cache. | |
| 543 | + | func (o *Omnibox) createFaviconImage(url, fallbackIcon string) *gtk.Image { | |
| 544 | + | favicon := gtk.NewImage() | |
| 545 | + | if favicon == nil { | |
| 542 | 546 | return nil | |
| 543 | 547 | } | |
| 544 | - | row.AddCssClass("omnibox-row") | |
| 545 | - | ||
| 546 | - | box := gtk.NewBox(gtk.OrientationHorizontalValue, 8) | |
| 547 | - | if box == nil { | |
| 548 | - | return nil | |
| 549 | - | } | |
| 550 | - | box.SetHexpand(true) | |
| 551 | - | ||
| 552 | - | // URL/Title label | |
| 553 | - | displayText := s.Title | |
| 554 | - | if displayText == "" { | |
| 555 | - | displayText = s.URL | |
| 556 | - | } | |
| 557 | - | label := gtk.NewLabel(nil) | |
| 558 | - | if label != nil { | |
| 559 | - | label.SetText(displayText) // Explicitly set text | |
| 560 | - | label.AddCssClass("omnibox-suggestion-title") | |
| 561 | - | label.SetHalign(gtk.AlignStartValue) | |
| 562 | - | label.SetHexpand(true) | |
| 563 | - | label.SetEllipsize(2) // PANGO_ELLIPSIZE_END | |
| 564 | - | box.Append(&label.Widget) | |
| 565 | - | } | |
| 566 | - | ||
| 567 | - | // Shortcut badge (Ctrl+1-9, Ctrl+0 for 10th) | |
| 568 | - | if index <= 9 { | |
| 569 | - | shortcutLabel := gtk.NewLabel(nil) | |
| 570 | - | if shortcutLabel != nil { | |
| 571 | - | if index < 9 { | |
| 572 | - | shortcutLabel.SetText(formatShortcut(index + 1)) | |
| 573 | - | } else { | |
| 574 | - | shortcutLabel.SetText("Ctrl+0") | |
| 548 | + | favicon.SetFromIconName(&fallbackIcon) | |
| 549 | + | favicon.SetPixelSize(16) | |
| 550 | + | favicon.AddCssClass("omnibox-favicon") | |
| 551 | + | ||
| 552 | + | // Async load favicon from cache | |
| 553 | + | if o.faviconCache != nil && url != "" { | |
| 554 | + | o.faviconCache.GetOrFetch(o.ctx, url, func(texture *gdk.Texture) { | |
| 555 | + | if texture != nil { | |
| 556 | + | var cb glib.SourceFunc = func(data uintptr) bool { | |
| 557 | + | favicon.SetFromPaintable(texture) | |
| 558 | + | return false | |
| 559 | + | } | |
| 560 | + | glib.IdleAdd(&cb, 0) | |
| 575 | 561 | } | |
| 576 | - | shortcutLabel.AddCssClass("omnibox-shortcut-badge") | |
| 577 | - | box.Append(&shortcutLabel.Widget) | |
| 578 | - | } | |
| 562 | + | }) | |
| 579 | 563 | } | |
| 580 | 564 | ||
| 581 | - | row.SetChild(&box.Widget) | |
| 582 | - | return row | |
| 565 | + | return favicon | |
| 583 | 566 | } | |
| 584 | 567 | ||
| 585 | - | // createFavoriteRow creates a ListBoxRow for a favorite. | |
| 586 | - | func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |
| 568 | + | // createRowWithFavicon creates a ListBoxRow with favicon, title, and shortcut badge. | |
| 569 | + | func (o *Omnibox) createRowWithFavicon(url, title, fallbackIcon string, index int) *gtk.ListBoxRow { | |
| 587 | 570 | row := gtk.NewListBoxRow() | |
| 588 | 571 | if row == nil { | |
| 589 | 572 | return nil | |
| @@ -596,10 +579,15 @@ func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |||
| 596 | 579 | } | |
| 597 | 580 | box.SetHexpand(true) | |
| 598 | 581 | ||
| 582 | + | // Favicon image | |
| 583 | + | if favicon := o.createFaviconImage(url, fallbackIcon); favicon != nil { | |
| 584 | + | box.Append(&favicon.Widget) | |
| 585 | + | } | |
| 586 | + | ||
| 599 | 587 | // Title label | |
| 600 | - | displayText := f.Title | |
| 588 | + | displayText := title | |
| 601 | 589 | if displayText == "" { | |
| 602 | - | displayText = f.URL | |
| 590 | + | displayText = url | |
| 603 | 591 | } | |
| 604 | 592 | label := gtk.NewLabel(nil) | |
| 605 | 593 | if label != nil { | |
| @@ -607,7 +595,7 @@ func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |||
| 607 | 595 | label.AddCssClass("omnibox-suggestion-title") | |
| 608 | 596 | label.SetHalign(gtk.AlignStartValue) | |
| 609 | 597 | label.SetHexpand(true) | |
| 610 | - | label.SetEllipsize(2) | |
| 598 | + | label.SetEllipsize(2) // PANGO_ELLIPSIZE_END | |
| 611 | 599 | box.Append(&label.Widget) | |
| 612 | 600 | } | |
| 613 | 601 | ||
| @@ -629,6 +617,16 @@ func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |||
| 629 | 617 | return row | |
| 630 | 618 | } | |
| 631 | 619 | ||
| 620 | + | // createSuggestionRow creates a ListBoxRow for a suggestion. | |
| 621 | + | func (o *Omnibox) createSuggestionRow(s Suggestion, index int) *gtk.ListBoxRow { | |
| 622 | + | return o.createRowWithFavicon(s.URL, s.Title, "web-browser-symbolic", index) | |
| 623 | + | } | |
| 624 | + | ||
| 625 | + | // createFavoriteRow creates a ListBoxRow for a favorite. | |
| 626 | + | func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |
| 627 | + | return o.createRowWithFavicon(f.URL, f.Title, "starred-symbolic", index) | |
| 628 | + | } | |
| 629 | + | ||
| 632 | 630 | // selectIndex selects a row by index. | |
| 633 | 631 | func (o *Omnibox) selectIndex(index int) { | |
| 634 | 632 | // Note: Don't hold mutex when calling SelectRow - it triggers rowSelectedCb | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "github.com/bnema/dumber/internal/domain/entity" | 10 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 11 | "github.com/bnema/dumber/internal/infrastructure/config" | 11 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 12 | "github.com/bnema/dumber/internal/logging" | 12 | "github.com/bnema/dumber/internal/logging" | ||
| 13 | + | "github.com/bnema/dumber/internal/ui/cache" | |||
| 13 | "github.com/jwijenbergh/puregotk/v4/gdk" | 14 | "github.com/jwijenbergh/puregotk/v4/gdk" | ||
| 14 | "github.com/jwijenbergh/puregotk/v4/glib" | 15 | "github.com/jwijenbergh/puregotk/v4/glib" | ||
| 15 | "github.com/jwijenbergh/puregotk/v4/gtk" | 16 | "github.com/jwijenbergh/puregotk/v4/gtk" | ||
| @@ -71,6 +72,7 @@ type Omnibox struct { | |||||
| 71 | // Dependencies | 72 | // Dependencies | ||
| 72 | historyUC *usecase.SearchHistoryUseCase | 73 | historyUC *usecase.SearchHistoryUseCase | ||
| 73 | favoritesUC *usecase.ManageFavoritesUseCase | 74 | favoritesUC *usecase.ManageFavoritesUseCase | ||
| 75 | + | faviconCache *cache.FaviconCache | |||
| 74 | shortcuts map[string]config.SearchShortcut | 76 | shortcuts map[string]config.SearchShortcut | ||
| 75 | defaultSearch string | 77 | defaultSearch string | ||
| 76 | initialBehavior string | 78 | initialBehavior string | ||
| @@ -90,6 +92,7 @@ type Omnibox struct { | |||||
| 90 | type OmniboxConfig struct { | 92 | type OmniboxConfig struct { | ||
| 91 | HistoryUC *usecase.SearchHistoryUseCase | 93 | HistoryUC *usecase.SearchHistoryUseCase | ||
| 92 | FavoritesUC *usecase.ManageFavoritesUseCase | 94 | FavoritesUC *usecase.ManageFavoritesUseCase | ||
| 95 | + | FaviconCache *cache.FaviconCache | |||
| 93 | Shortcuts map[string]config.SearchShortcut | 96 | Shortcuts map[string]config.SearchShortcut | ||
| 94 | DefaultSearch string | 97 | DefaultSearch string | ||
| 95 | InitialBehavior string | 98 | InitialBehavior string | ||
| @@ -105,6 +108,7 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||||
| 105 | selectedIndex: -1, | 108 | selectedIndex: -1, | ||
| 106 | historyUC: cfg.HistoryUC, | 109 | historyUC: cfg.HistoryUC, | ||
| 107 | favoritesUC: cfg.FavoritesUC, | 110 | favoritesUC: cfg.FavoritesUC, | ||
| 111 | + | faviconCache: cfg.FaviconCache, | |||
| 108 | shortcuts: cfg.Shortcuts, | 112 | shortcuts: cfg.Shortcuts, | ||
| 109 | defaultSearch: cfg.DefaultSearch, | 113 | defaultSearch: cfg.DefaultSearch, | ||
| 110 | initialBehavior: cfg.InitialBehavior, | 114 | initialBehavior: cfg.InitialBehavior, | ||
| @@ -535,55 +539,34 @@ func (o *Omnibox) rebuildList() { | |||||
| 535 | } | 539 | } | ||
| 536 | } | 540 | } | ||
| 537 | 541 | ||||
| 538 | - | // createSuggestionRow creates a ListBoxRow for a suggestion. | 542 | + | // createFaviconImage creates a favicon image with async loading from cache. |
| 539 | - | func (o *Omnibox) createSuggestionRow(s Suggestion, index int) *gtk.ListBoxRow { | 543 | + | func (o *Omnibox) createFaviconImage(url, fallbackIcon string) *gtk.Image { |
| 540 | - | row := gtk.NewListBoxRow() | 544 | + | favicon := gtk.NewImage() |
| 541 | - | if row == nil { | 545 | + | if favicon == nil { |
| 542 | return nil | 546 | return nil | ||
| 543 | } | 547 | } | ||
| 544 | - | row.AddCssClass("omnibox-row") | 548 | + | favicon.SetFromIconName(&fallbackIcon) |
| 545 | - | 549 | + | favicon.SetPixelSize(16) | |
| 546 | - | box := gtk.NewBox(gtk.OrientationHorizontalValue, 8) | 550 | + | favicon.AddCssClass("omnibox-favicon") |
| 547 | - | if box == nil { | 551 | + | |
| 548 | - | return nil | 552 | + | // Async load favicon from cache |
| 549 | - | } | 553 | + | if o.faviconCache != nil && url != "" { |
| 550 | - | box.SetHexpand(true) | 554 | + | o.faviconCache.GetOrFetch(o.ctx, url, func(texture *gdk.Texture) { |
| 551 | - | 555 | + | if texture != nil { | |
| 552 | - | // URL/Title label | 556 | + | var cb glib.SourceFunc = func(data uintptr) bool { |
| 553 | - | displayText := s.Title | 557 | + | favicon.SetFromPaintable(texture) |
| 554 | - | if displayText == "" { | 558 | + | return false |
| 555 | - | displayText = s.URL | 559 | + | } |
| 556 | - | } | 560 | + | glib.IdleAdd(&cb, 0) |
| 557 | - | label := gtk.NewLabel(nil) | |||
| 558 | - | if label != nil { | |||
| 559 | - | label.SetText(displayText) // Explicitly set text | |||
| 560 | - | label.AddCssClass("omnibox-suggestion-title") | |||
| 561 | - | label.SetHalign(gtk.AlignStartValue) | |||
| 562 | - | label.SetHexpand(true) | |||
| 563 | - | label.SetEllipsize(2) // PANGO_ELLIPSIZE_END | |||
| 564 | - | box.Append(&label.Widget) | |||
| 565 | - | } | |||
| 566 | - | ||||
| 567 | - | // Shortcut badge (Ctrl+1-9, Ctrl+0 for 10th) | |||
| 568 | - | if index <= 9 { | |||
| 569 | - | shortcutLabel := gtk.NewLabel(nil) | |||
| 570 | - | if shortcutLabel != nil { | |||
| 571 | - | if index < 9 { | |||
| 572 | - | shortcutLabel.SetText(formatShortcut(index + 1)) | |||
| 573 | - | } else { | |||
| 574 | - | shortcutLabel.SetText("Ctrl+0") | |||
| 575 | } | 561 | } | ||
| 576 | - | shortcutLabel.AddCssClass("omnibox-shortcut-badge") | 562 | + | }) |
| 577 | - | box.Append(&shortcutLabel.Widget) | |||
| 578 | - | } | |||
| 579 | } | 563 | } | ||
| 580 | 564 | ||||
| 581 | - | row.SetChild(&box.Widget) | 565 | + | return favicon |
| 582 | - | return row | |||
| 583 | } | 566 | } | ||
| 584 | 567 | ||||
| 585 | - | // createFavoriteRow creates a ListBoxRow for a favorite. | 568 | + | // createRowWithFavicon creates a ListBoxRow with favicon, title, and shortcut badge. |
| 586 | - | func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | 569 | + | func (o *Omnibox) createRowWithFavicon(url, title, fallbackIcon string, index int) *gtk.ListBoxRow { |
| 587 | row := gtk.NewListBoxRow() | 570 | row := gtk.NewListBoxRow() | ||
| 588 | if row == nil { | 571 | if row == nil { | ||
| 589 | return nil | 572 | return nil | ||
| @@ -596,10 +579,15 @@ func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |||||
| 596 | } | 579 | } | ||
| 597 | box.SetHexpand(true) | 580 | box.SetHexpand(true) | ||
| 598 | 581 | ||||
| 582 | + | // Favicon image | |||
| 583 | + | if favicon := o.createFaviconImage(url, fallbackIcon); favicon != nil { | |||
| 584 | + | box.Append(&favicon.Widget) | |||
| 585 | + | } | |||
| 586 | + | ||||
| 599 | // Title label | 587 | // Title label | ||
| 600 | - | displayText := f.Title | 588 | + | displayText := title |
| 601 | if displayText == "" { | 589 | if displayText == "" { | ||
| 602 | - | displayText = f.URL | 590 | + | displayText = url |
| 603 | } | 591 | } | ||
| 604 | label := gtk.NewLabel(nil) | 592 | label := gtk.NewLabel(nil) | ||
| 605 | if label != nil { | 593 | if label != nil { | ||
| @@ -607,7 +595,7 @@ func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |||||
| 607 | label.AddCssClass("omnibox-suggestion-title") | 595 | label.AddCssClass("omnibox-suggestion-title") | ||
| 608 | label.SetHalign(gtk.AlignStartValue) | 596 | label.SetHalign(gtk.AlignStartValue) | ||
| 609 | label.SetHexpand(true) | 597 | label.SetHexpand(true) | ||
| 610 | - | label.SetEllipsize(2) | 598 | + | label.SetEllipsize(2) // PANGO_ELLIPSIZE_END |
| 611 | box.Append(&label.Widget) | 599 | box.Append(&label.Widget) | ||
| 612 | } | 600 | } | ||
| 613 | 601 | ||||
| @@ -629,6 +617,16 @@ func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |||||
| 629 | return row | 617 | return row | ||
| 630 | } | 618 | } | ||
| 631 | 619 | ||||
| 620 | + | // createSuggestionRow creates a ListBoxRow for a suggestion. | |||
| 621 | + | func (o *Omnibox) createSuggestionRow(s Suggestion, index int) *gtk.ListBoxRow { | |||
| 622 | + | return o.createRowWithFavicon(s.URL, s.Title, "web-browser-symbolic", index) | |||
| 623 | + | } | |||
| 624 | + | ||||
| 625 | + | // createFavoriteRow creates a ListBoxRow for a favorite. | |||
| 626 | + | func (o *Omnibox) createFavoriteRow(f Favorite, index int) *gtk.ListBoxRow { | |||
| 627 | + | return o.createRowWithFavicon(f.URL, f.Title, "starred-symbolic", index) | |||
| 628 | + | } | |||
| 629 | + | ||||
| 632 | // selectIndex selects a row by index. | 630 | // selectIndex selects a row by index. | ||
| 633 | func (o *Omnibox) selectIndex(index int) { | 631 | func (o *Omnibox) selectIndex(index int) { | ||
| 634 | // Note: Don't hold mutex when calling SelectRow - it triggers rowSelectedCb | 632 | // Note: Don't hold mutex when calling SelectRow - it triggers rowSelectedCb | ||
M internal/ui/coordinator/content.go +105 -0
| @@ -8,19 +8,27 @@ import ( | |||
| 8 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 10 | 10 | "github.com/bnema/dumber/internal/logging" | |
| 11 | + | "github.com/bnema/dumber/internal/ui/cache" | |
| 11 | 12 | "github.com/bnema/dumber/internal/ui/component" | |
| 12 | 13 | "github.com/bnema/dumber/internal/ui/layout" | |
| 14 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 13 | 15 | ) | |
| 14 | 16 | ||
| 15 | 17 | // ContentCoordinator manages WebView lifecycle, title tracking, and content attachment. | |
| 16 | 18 | type ContentCoordinator struct { | |
| 17 | 19 | pool *webkit.WebViewPool | |
| 18 | 20 | widgetFactory layout.WidgetFactory | |
| 21 | + | faviconCache *cache.FaviconCache | |
| 19 | 22 | ||
| 20 | 23 | webViews map[entity.PaneID]*webkit.WebView | |
| 21 | 24 | paneTitles map[entity.PaneID]string | |
| 22 | 25 | titleMu sync.RWMutex | |
| 23 | 26 | ||
| 27 | + | // Track original navigation URLs to handle cross-domain redirects | |
| 28 | + | // e.g., google.fr → google.com: cache favicon under both domains | |
| 29 | + | navOrigins map[entity.PaneID]string | |
| 30 | + | navOriginMu sync.RWMutex | |
| 31 | + | ||
| 24 | 32 | // Callback to get active workspace state (avoids circular dependency) | |
| 25 | 33 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView) | |
| 26 | 34 | } | |
| @@ -30,6 +38,7 @@ func NewContentCoordinator( | |||
| 30 | 38 | ctx context.Context, | |
| 31 | 39 | pool *webkit.WebViewPool, | |
| 32 | 40 | widgetFactory layout.WidgetFactory, | |
| 41 | + | faviconCache *cache.FaviconCache, | |
| 33 | 42 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView), | |
| 34 | 43 | ) *ContentCoordinator { | |
| 35 | 44 | log := logging.FromContext(ctx) | |
| @@ -38,8 +47,10 @@ func NewContentCoordinator( | |||
| 38 | 47 | return &ContentCoordinator{ | |
| 39 | 48 | pool: pool, | |
| 40 | 49 | widgetFactory: widgetFactory, | |
| 50 | + | faviconCache: faviconCache, | |
| 41 | 51 | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 42 | 52 | paneTitles: make(map[entity.PaneID]string), | |
| 53 | + | navOrigins: make(map[entity.PaneID]string), | |
| 43 | 54 | getActiveWS: getActiveWS, | |
| 44 | 55 | } | |
| 45 | 56 | } | |
| @@ -68,6 +79,11 @@ func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.Pa | |||
| 68 | 79 | c.onTitleChanged(ctx, paneID, title) | |
| 69 | 80 | } | |
| 70 | 81 | ||
| 82 | + | // Set up favicon change callback | |
| 83 | + | wv.OnFaviconChanged = func(favicon *gdk.Texture) { | |
| 84 | + | c.onFaviconChanged(ctx, paneID, favicon) | |
| 85 | + | } | |
| 86 | + | ||
| 71 | 87 | log.Debug().Str("pane_id", string(paneID)).Msg("webview acquired for pane") | |
| 72 | 88 | return wv, nil | |
| 73 | 89 | } | |
| @@ -87,6 +103,11 @@ func (c *ContentCoordinator) ReleaseWebView(ctx context.Context, paneID entity.P | |||
| 87 | 103 | delete(c.paneTitles, paneID) | |
| 88 | 104 | c.titleMu.Unlock() | |
| 89 | 105 | ||
| 106 | + | // Clean up navigation origin tracking | |
| 107 | + | c.navOriginMu.Lock() | |
| 108 | + | delete(c.navOrigins, paneID) | |
| 109 | + | c.navOriginMu.Unlock() | |
| 110 | + | ||
| 90 | 111 | if c.pool != nil { | |
| 91 | 112 | c.pool.Release(wv) | |
| 92 | 113 | } else { | |
| @@ -241,3 +262,87 @@ func (c *ContentCoordinator) updateStackedPaneTitle(ctx context.Context, ws *ent | |||
| 241 | 262 | } | |
| 242 | 263 | } | |
| 243 | 264 | } | |
| 265 | + | ||
| 266 | + | // onFaviconChanged updates favicon tracking when a WebView's favicon changes. | |
| 267 | + | func (c *ContentCoordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, favicon *gdk.Texture) { | |
| 268 | + | log := logging.FromContext(ctx) | |
| 269 | + | ||
| 270 | + | // Get current URI to extract domain for caching | |
| 271 | + | wv := c.webViews[paneID] | |
| 272 | + | if wv == nil { | |
| 273 | + | return | |
| 274 | + | } | |
| 275 | + | uri := wv.URI() | |
| 276 | + | ||
| 277 | + | // Update favicon cache with domain key (final URL after redirects) | |
| 278 | + | if c.faviconCache != nil && favicon != nil && uri != "" { | |
| 279 | + | c.faviconCache.SetByURL(uri, favicon) | |
| 280 | + | ||
| 281 | + | // Also cache under original navigation URL to handle cross-domain redirects | |
| 282 | + | // e.g., google.fr → google.com: cache favicon under both domains | |
| 283 | + | c.navOriginMu.RLock() | |
| 284 | + | originURL := c.navOrigins[paneID] | |
| 285 | + | c.navOriginMu.RUnlock() | |
| 286 | + | if originURL != "" && originURL != uri { | |
| 287 | + | c.faviconCache.SetByURL(originURL, favicon) | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // Update StackedView favicon if this pane is in a stack | |
| 292 | + | ws, wsView := c.getActiveWS() | |
| 293 | + | if wsView != nil { | |
| 294 | + | tr := wsView.TreeRenderer() | |
| 295 | + | if tr != nil { | |
| 296 | + | stackedView := tr.GetStackedViewForPane(string(paneID)) | |
| 297 | + | if stackedView != nil { | |
| 298 | + | c.updateStackedPaneFavicon(ctx, ws, stackedView, paneID, favicon) | |
| 299 | + | } | |
| 300 | + | } | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | log.Debug(). | |
| 304 | + | Str("pane_id", string(paneID)). | |
| 305 | + | Str("uri", uri). | |
| 306 | + | Bool("has_favicon", favicon != nil). | |
| 307 | + | Msg("pane favicon updated") | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | // updateStackedPaneFavicon updates the favicon of a pane in a StackedView. | |
| 311 | + | func (c *ContentCoordinator) updateStackedPaneFavicon(ctx context.Context, ws *entity.Workspace, sv *layout.StackedView, paneID entity.PaneID, favicon *gdk.Texture) { | |
| 312 | + | log := logging.FromContext(ctx) | |
| 313 | + | ||
| 314 | + | if ws == nil { | |
| 315 | + | return | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | paneNode := ws.FindPane(paneID) | |
| 319 | + | if paneNode == nil { | |
| 320 | + | return | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | // If the pane is in a stacked parent, find its index | |
| 324 | + | if paneNode.Parent != nil && paneNode.Parent.IsStacked { | |
| 325 | + | for i, child := range paneNode.Parent.Children { | |
| 326 | + | if child.Pane != nil && child.Pane.ID == paneID { | |
| 327 | + | if err := sv.UpdateFaviconTexture(i, favicon); err != nil { | |
| 328 | + | log.Warn().Err(err).Int("index", i).Msg("failed to update stacked pane favicon") | |
| 329 | + | } | |
| 330 | + | return | |
| 331 | + | } | |
| 332 | + | } | |
| 333 | + | } | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | // FaviconCache returns the favicon cache for external use (e.g., omnibox). | |
| 337 | + | func (c *ContentCoordinator) FaviconCache() *cache.FaviconCache { | |
| 338 | + | return c.faviconCache | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | // SetNavigationOrigin records the original URL before navigation starts. | |
| 342 | + | // This allows caching favicons under both original and final domains | |
| 343 | + | // when cross-domain redirects occur (e.g., google.fr → google.com). | |
| 344 | + | func (c *ContentCoordinator) SetNavigationOrigin(paneID entity.PaneID, url string) { | |
| 345 | + | c.navOriginMu.Lock() | |
| 346 | + | c.navOrigins[paneID] = url | |
| 347 | + | c.navOriginMu.Unlock() | |
| 348 | + | } | |
| @@ -8,19 +8,27 @@ import ( | |||||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 10 | "github.com/bnema/dumber/internal/logging" | 10 | "github.com/bnema/dumber/internal/logging" | ||
| 11 | + | "github.com/bnema/dumber/internal/ui/cache" | |||
| 11 | "github.com/bnema/dumber/internal/ui/component" | 12 | "github.com/bnema/dumber/internal/ui/component" | ||
| 12 | "github.com/bnema/dumber/internal/ui/layout" | 13 | "github.com/bnema/dumber/internal/ui/layout" | ||
| 14 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |||
| 13 | ) | 15 | ) | ||
| 14 | 16 | ||||
| 15 | // ContentCoordinator manages WebView lifecycle, title tracking, and content attachment. | 17 | // ContentCoordinator manages WebView lifecycle, title tracking, and content attachment. | ||
| 16 | type ContentCoordinator struct { | 18 | type ContentCoordinator struct { | ||
| 17 | pool *webkit.WebViewPool | 19 | pool *webkit.WebViewPool | ||
| 18 | widgetFactory layout.WidgetFactory | 20 | widgetFactory layout.WidgetFactory | ||
| 21 | + | faviconCache *cache.FaviconCache | |||
| 19 | 22 | ||||
| 20 | webViews map[entity.PaneID]*webkit.WebView | 23 | webViews map[entity.PaneID]*webkit.WebView | ||
| 21 | paneTitles map[entity.PaneID]string | 24 | paneTitles map[entity.PaneID]string | ||
| 22 | titleMu sync.RWMutex | 25 | titleMu sync.RWMutex | ||
| 23 | 26 | ||||
| 27 | + | // Track original navigation URLs to handle cross-domain redirects | |||
| 28 | + | // e.g., google.fr → google.com: cache favicon under both domains | |||
| 29 | + | navOrigins map[entity.PaneID]string | |||
| 30 | + | navOriginMu sync.RWMutex | |||
| 31 | + | ||||
| 24 | // Callback to get active workspace state (avoids circular dependency) | 32 | // Callback to get active workspace state (avoids circular dependency) | ||
| 25 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView) | 33 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView) | ||
| 26 | } | 34 | } | ||
| @@ -30,6 +38,7 @@ func NewContentCoordinator( | |||||
| 30 | ctx context.Context, | 38 | ctx context.Context, | ||
| 31 | pool *webkit.WebViewPool, | 39 | pool *webkit.WebViewPool, | ||
| 32 | widgetFactory layout.WidgetFactory, | 40 | widgetFactory layout.WidgetFactory, | ||
| 41 | + | faviconCache *cache.FaviconCache, | |||
| 33 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView), | 42 | getActiveWS func() (*entity.Workspace, *component.WorkspaceView), | ||
| 34 | ) *ContentCoordinator { | 43 | ) *ContentCoordinator { | ||
| 35 | log := logging.FromContext(ctx) | 44 | log := logging.FromContext(ctx) | ||
| @@ -38,8 +47,10 @@ func NewContentCoordinator( | |||||
| 38 | return &ContentCoordinator{ | 47 | return &ContentCoordinator{ | ||
| 39 | pool: pool, | 48 | pool: pool, | ||
| 40 | widgetFactory: widgetFactory, | 49 | widgetFactory: widgetFactory, | ||
| 50 | + | faviconCache: faviconCache, | |||
| 41 | webViews: make(map[entity.PaneID]*webkit.WebView), | 51 | webViews: make(map[entity.PaneID]*webkit.WebView), | ||
| 42 | paneTitles: make(map[entity.PaneID]string), | 52 | paneTitles: make(map[entity.PaneID]string), | ||
| 53 | + | navOrigins: make(map[entity.PaneID]string), | |||
| 43 | getActiveWS: getActiveWS, | 54 | getActiveWS: getActiveWS, | ||
| 44 | } | 55 | } | ||
| 45 | } | 56 | } | ||
| @@ -68,6 +79,11 @@ func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.Pa | |||||
| 68 | c.onTitleChanged(ctx, paneID, title) | 79 | c.onTitleChanged(ctx, paneID, title) | ||
| 69 | } | 80 | } | ||
| 70 | 81 | ||||
| 82 | + | // Set up favicon change callback | |||
| 83 | + | wv.OnFaviconChanged = func(favicon *gdk.Texture) { | |||
| 84 | + | c.onFaviconChanged(ctx, paneID, favicon) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 71 | log.Debug().Str("pane_id", string(paneID)).Msg("webview acquired for pane") | 87 | log.Debug().Str("pane_id", string(paneID)).Msg("webview acquired for pane") | ||
| 72 | return wv, nil | 88 | return wv, nil | ||
| 73 | } | 89 | } | ||
| @@ -87,6 +103,11 @@ func (c *ContentCoordinator) ReleaseWebView(ctx context.Context, paneID entity.P | |||||
| 87 | delete(c.paneTitles, paneID) | 103 | delete(c.paneTitles, paneID) | ||
| 88 | c.titleMu.Unlock() | 104 | c.titleMu.Unlock() | ||
| 89 | 105 | ||||
| 106 | + | // Clean up navigation origin tracking | |||
| 107 | + | c.navOriginMu.Lock() | |||
| 108 | + | delete(c.navOrigins, paneID) | |||
| 109 | + | c.navOriginMu.Unlock() | |||
| 110 | + | ||||
| 90 | if c.pool != nil { | 111 | if c.pool != nil { | ||
| 91 | c.pool.Release(wv) | 112 | c.pool.Release(wv) | ||
| 92 | } else { | 113 | } else { | ||
| @@ -241,3 +262,87 @@ func (c *ContentCoordinator) updateStackedPaneTitle(ctx context.Context, ws *ent | |||||
| 241 | } | 262 | } | ||
| 242 | } | 263 | } | ||
| 243 | } | 264 | } | ||
| 265 | + | ||||
| 266 | + | // onFaviconChanged updates favicon tracking when a WebView's favicon changes. | |||
| 267 | + | func (c *ContentCoordinator) onFaviconChanged(ctx context.Context, paneID entity.PaneID, favicon *gdk.Texture) { | |||
| 268 | + | log := logging.FromContext(ctx) | |||
| 269 | + | ||||
| 270 | + | // Get current URI to extract domain for caching | |||
| 271 | + | wv := c.webViews[paneID] | |||
| 272 | + | if wv == nil { | |||
| 273 | + | return | |||
| 274 | + | } | |||
| 275 | + | uri := wv.URI() | |||
| 276 | + | ||||
| 277 | + | // Update favicon cache with domain key (final URL after redirects) | |||
| 278 | + | if c.faviconCache != nil && favicon != nil && uri != "" { | |||
| 279 | + | c.faviconCache.SetByURL(uri, favicon) | |||
| 280 | + | ||||
| 281 | + | // Also cache under original navigation URL to handle cross-domain redirects | |||
| 282 | + | // e.g., google.fr → google.com: cache favicon under both domains | |||
| 283 | + | c.navOriginMu.RLock() | |||
| 284 | + | originURL := c.navOrigins[paneID] | |||
| 285 | + | c.navOriginMu.RUnlock() | |||
| 286 | + | if originURL != "" && originURL != uri { | |||
| 287 | + | c.faviconCache.SetByURL(originURL, favicon) | |||
| 288 | + | } | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // Update StackedView favicon if this pane is in a stack | |||
| 292 | + | ws, wsView := c.getActiveWS() | |||
| 293 | + | if wsView != nil { | |||
| 294 | + | tr := wsView.TreeRenderer() | |||
| 295 | + | if tr != nil { | |||
| 296 | + | stackedView := tr.GetStackedViewForPane(string(paneID)) | |||
| 297 | + | if stackedView != nil { | |||
| 298 | + | c.updateStackedPaneFavicon(ctx, ws, stackedView, paneID, favicon) | |||
| 299 | + | } | |||
| 300 | + | } | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | log.Debug(). | |||
| 304 | + | Str("pane_id", string(paneID)). | |||
| 305 | + | Str("uri", uri). | |||
| 306 | + | Bool("has_favicon", favicon != nil). | |||
| 307 | + | Msg("pane favicon updated") | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | // updateStackedPaneFavicon updates the favicon of a pane in a StackedView. | |||
| 311 | + | func (c *ContentCoordinator) updateStackedPaneFavicon(ctx context.Context, ws *entity.Workspace, sv *layout.StackedView, paneID entity.PaneID, favicon *gdk.Texture) { | |||
| 312 | + | log := logging.FromContext(ctx) | |||
| 313 | + | ||||
| 314 | + | if ws == nil { | |||
| 315 | + | return | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | paneNode := ws.FindPane(paneID) | |||
| 319 | + | if paneNode == nil { | |||
| 320 | + | return | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | // If the pane is in a stacked parent, find its index | |||
| 324 | + | if paneNode.Parent != nil && paneNode.Parent.IsStacked { | |||
| 325 | + | for i, child := range paneNode.Parent.Children { | |||
| 326 | + | if child.Pane != nil && child.Pane.ID == paneID { | |||
| 327 | + | if err := sv.UpdateFaviconTexture(i, favicon); err != nil { | |||
| 328 | + | log.Warn().Err(err).Int("index", i).Msg("failed to update stacked pane favicon") | |||
| 329 | + | } | |||
| 330 | + | return | |||
| 331 | + | } | |||
| 332 | + | } | |||
| 333 | + | } | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | // FaviconCache returns the favicon cache for external use (e.g., omnibox). | |||
| 337 | + | func (c *ContentCoordinator) FaviconCache() *cache.FaviconCache { | |||
| 338 | + | return c.faviconCache | |||
| 339 | + | } | |||
| 340 | + | ||||
| 341 | + | // SetNavigationOrigin records the original URL before navigation starts. | |||
| 342 | + | // This allows caching favicons under both original and final domains | |||
| 343 | + | // when cross-domain redirects occur (e.g., google.fr → google.com). | |||
| 344 | + | func (c *ContentCoordinator) SetNavigationOrigin(paneID entity.PaneID, url string) { | |||
| 345 | + | c.navOriginMu.Lock() | |||
| 346 | + | c.navOrigins[paneID] = url | |||
| 347 | + | c.navOriginMu.Unlock() | |||
| 348 | + | } | |||
M internal/ui/coordinator/navigation.go +2 -0
| @@ -55,6 +55,8 @@ func (c *NavigationCoordinator) Navigate(ctx context.Context, url string) error | |||
| 55 | 55 | if ws != nil { | |
| 56 | 56 | if pane := ws.ActivePane(); pane != nil && pane.Pane != nil { | |
| 57 | 57 | paneID = string(pane.Pane.ID) | |
| 58 | + | // Track original URL for cross-domain redirect favicon caching | |
| 59 | + | c.contentCoord.SetNavigationOrigin(pane.Pane.ID, url) | |
| 58 | 60 | } | |
| 59 | 61 | } | |
| 60 | 62 | ||
| @@ -55,6 +55,8 @@ func (c *NavigationCoordinator) Navigate(ctx context.Context, url string) error | |||||
| 55 | if ws != nil { | 55 | if ws != nil { | ||
| 56 | if pane := ws.ActivePane(); pane != nil && pane.Pane != nil { | 56 | if pane := ws.ActivePane(); pane != nil && pane.Pane != nil { | ||
| 57 | paneID = string(pane.Pane.ID) | 57 | paneID = string(pane.Pane.ID) | ||
| 58 | + | // Track original URL for cross-domain redirect favicon caching | |||
| 59 | + | c.contentCoord.SetNavigationOrigin(pane.Pane.ID, url) | |||
| 58 | } | 60 | } | ||
| 59 | } | 61 | } | ||
| 60 | 62 | ||||