refactor(services): remove legacy favicon handlers, simplify omnibox
- Remove handleFaviconChanged() and handleFaviconURIChanged() from BrowserService - Simplify omnibox handler: use favicon_url from DB only (no buildFavicon) - Add GetHistoryEntry() to HistoryQuerier interface - Update omnibox favicon styling: remove border, add border-radius - Improve favicon image rendering (image-rendering, interpolation-mode) - Fix formatting in defaults.go, migrations_test.go
- commit
- 842d401ba2938e6d860c9c901635d1e896fcbc1d
- author
- bnema · · 2025-10-18 10:55 UTC
- parent
- e6af2b6
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
6 files · +21 -147
M gui/src/components/omnibox/OmniboxSuggestions.svelte +6 -1
| @@ -190,17 +190,22 @@ | |||
| 190 | 190 | .suggestion-favicon { | |
| 191 | 191 | width: 20px; | |
| 192 | 192 | height: 20px; | |
| 193 | - | border: 1px solid var(--dynamic-border); | |
| 193 | + | border: none; | |
| 194 | + | border-radius: 3px; | |
| 194 | 195 | display: flex; | |
| 195 | 196 | align-items: center; | |
| 196 | 197 | justify-content: center; | |
| 197 | 198 | background: var(--dynamic-bg); | |
| 199 | + | flex-shrink: 0; | |
| 200 | + | overflow: hidden; | |
| 198 | 201 | } | |
| 199 | 202 | ||
| 200 | 203 | .suggestion-favicon-img { | |
| 201 | 204 | width: 16px; | |
| 202 | 205 | height: 16px; | |
| 203 | 206 | object-fit: contain; | |
| 207 | + | image-rendering: -webkit-optimize-contrast; | |
| 208 | + | -ms-interpolation-mode: nearest-neighbor; | |
| 204 | 209 | } | |
| 205 | 210 | ||
| 206 | 211 | .suggestion-text { | |
| @@ -190,17 +190,22 @@ | |||||
| 190 | .suggestion-favicon { | 190 | .suggestion-favicon { | ||
| 191 | width: 20px; | 191 | width: 20px; | ||
| 192 | height: 20px; | 192 | height: 20px; | ||
| 193 | - | border: 1px solid var(--dynamic-border); | 193 | + | border: none; |
| 194 | + | border-radius: 3px; | |||
| 194 | display: flex; | 195 | display: flex; | ||
| 195 | align-items: center; | 196 | align-items: center; | ||
| 196 | justify-content: center; | 197 | justify-content: center; | ||
| 197 | background: var(--dynamic-bg); | 198 | background: var(--dynamic-bg); | ||
| 199 | + | flex-shrink: 0; | |||
| 200 | + | overflow: hidden; | |||
| 198 | } | 201 | } | ||
| 199 | 202 | ||||
| 200 | .suggestion-favicon-img { | 203 | .suggestion-favicon-img { | ||
| 201 | width: 16px; | 204 | width: 16px; | ||
| 202 | height: 16px; | 205 | height: 16px; | ||
| 203 | object-fit: contain; | 206 | object-fit: contain; | ||
| 207 | + | image-rendering: -webkit-optimize-contrast; | |||
| 208 | + | -ms-interpolation-mode: nearest-neighbor; | |||
| 204 | } | 209 | } | ||
| 205 | 210 | ||||
| 206 | .suggestion-text { | 211 | .suggestion-text { | ||
M internal/app/messaging/handler.go +1 -31
| @@ -5,7 +5,6 @@ import ( | |||
| 5 | 5 | "encoding/json" | |
| 6 | 6 | "log" | |
| 7 | 7 | "strconv" | |
| 8 | - | "strings" | |
| 9 | 8 | "time" | |
| 10 | 9 | ||
| 11 | 10 | "github.com/bnema/dumber/internal/app/constants" | |
| @@ -226,33 +225,6 @@ func (h *Handler) handleQuery(msg Message) { | |||
| 226 | 225 | Favicon string `json:"favicon,omitempty"` | |
| 227 | 226 | } | |
| 228 | 227 | ||
| 229 | - | buildFavicon := func(raw string) string { | |
| 230 | - | if raw == "" { | |
| 231 | - | return "" | |
| 232 | - | } | |
| 233 | - | scheme := "https" | |
| 234 | - | host := raw | |
| 235 | - | if i := strings.Index(raw, "://"); i >= 0 { | |
| 236 | - | if i > 0 { | |
| 237 | - | scheme = raw[:i] | |
| 238 | - | } | |
| 239 | - | rest := raw[i+3:] | |
| 240 | - | if j := strings.IndexByte(rest, '/'); j >= 0 { | |
| 241 | - | host = rest[:j] | |
| 242 | - | } else { | |
| 243 | - | host = rest | |
| 244 | - | } | |
| 245 | - | } else { | |
| 246 | - | if j := strings.IndexByte(raw, '/'); j >= 0 { | |
| 247 | - | host = raw[:j] | |
| 248 | - | } | |
| 249 | - | } | |
| 250 | - | if host == "" { | |
| 251 | - | return "" | |
| 252 | - | } | |
| 253 | - | return scheme + "://" + host + "/favicon.ico" | |
| 254 | - | } | |
| 255 | - | ||
| 256 | 228 | results := make([]suggestion, 0, limit) | |
| 257 | 229 | seen := make(map[string]struct{}, limit*2) | |
| 258 | 230 | ||
| @@ -284,12 +256,10 @@ func (h *Handler) handleQuery(msg Message) { | |||
| 284 | 256 | if _, ok := seen[url]; ok { | |
| 285 | 257 | continue | |
| 286 | 258 | } | |
| 287 | - | // Use favicon_url from database if available, otherwise build it | |
| 259 | + | // Use favicon_url from database (populated by FaviconService) | |
| 288 | 260 | favicon := "" | |
| 289 | 261 | if s, ok := m["favicon_url"].(string); ok && s != "" { | |
| 290 | 262 | favicon = s | |
| 291 | - | } else { | |
| 292 | - | favicon = buildFavicon(url) | |
| 293 | 263 | } | |
| 294 | 264 | results = append(results, suggestion{URL: url, Favicon: favicon}) | |
| 295 | 265 | seen[url] = struct{}{} | |
| @@ -5,7 +5,6 @@ import ( | |||||
| 5 | "encoding/json" | 5 | "encoding/json" | ||
| 6 | "log" | 6 | "log" | ||
| 7 | "strconv" | 7 | "strconv" | ||
| 8 | - | "strings" | |||
| 9 | "time" | 8 | "time" | ||
| 10 | 9 | ||||
| 11 | "github.com/bnema/dumber/internal/app/constants" | 10 | "github.com/bnema/dumber/internal/app/constants" | ||
| @@ -226,33 +225,6 @@ func (h *Handler) handleQuery(msg Message) { | |||||
| 226 | Favicon string `json:"favicon,omitempty"` | 225 | Favicon string `json:"favicon,omitempty"` | ||
| 227 | } | 226 | } | ||
| 228 | 227 | ||||
| 229 | - | buildFavicon := func(raw string) string { | |||
| 230 | - | if raw == "" { | |||
| 231 | - | return "" | |||
| 232 | - | } | |||
| 233 | - | scheme := "https" | |||
| 234 | - | host := raw | |||
| 235 | - | if i := strings.Index(raw, "://"); i >= 0 { | |||
| 236 | - | if i > 0 { | |||
| 237 | - | scheme = raw[:i] | |||
| 238 | - | } | |||
| 239 | - | rest := raw[i+3:] | |||
| 240 | - | if j := strings.IndexByte(rest, '/'); j >= 0 { | |||
| 241 | - | host = rest[:j] | |||
| 242 | - | } else { | |||
| 243 | - | host = rest | |||
| 244 | - | } | |||
| 245 | - | } else { | |||
| 246 | - | if j := strings.IndexByte(raw, '/'); j >= 0 { | |||
| 247 | - | host = raw[:j] | |||
| 248 | - | } | |||
| 249 | - | } | |||
| 250 | - | if host == "" { | |||
| 251 | - | return "" | |||
| 252 | - | } | |||
| 253 | - | return scheme + "://" + host + "/favicon.ico" | |||
| 254 | - | } | |||
| 255 | - | ||||
| 256 | results := make([]suggestion, 0, limit) | 228 | results := make([]suggestion, 0, limit) | ||
| 257 | seen := make(map[string]struct{}, limit*2) | 229 | seen := make(map[string]struct{}, limit*2) | ||
| 258 | 230 | ||||
| @@ -284,12 +256,10 @@ func (h *Handler) handleQuery(msg Message) { | |||||
| 284 | if _, ok := seen[url]; ok { | 256 | if _, ok := seen[url]; ok { | ||
| 285 | continue | 257 | continue | ||
| 286 | } | 258 | } | ||
| 287 | - | // Use favicon_url from database if available, otherwise build it | 259 | + | // Use favicon_url from database (populated by FaviconService) |
| 288 | favicon := "" | 260 | favicon := "" | ||
| 289 | if s, ok := m["favicon_url"].(string); ok && s != "" { | 261 | if s, ok := m["favicon_url"].(string); ok && s != "" { | ||
| 290 | favicon = s | 262 | favicon = s | ||
| 291 | - | } else { | |||
| 292 | - | favicon = buildFavicon(url) | |||
| 293 | } | 263 | } | ||
| 294 | results = append(results, suggestion{URL: url, Favicon: favicon}) | 264 | results = append(results, suggestion{URL: url, Favicon: favicon}) | ||
| 295 | seen[url] = struct{}{} | 265 | seen[url] = struct{}{} | ||
M internal/config/defaults.go +5 -5
| @@ -32,11 +32,11 @@ const ( | |||
| 32 | 32 | defaultPopupPlacement = "right" | |
| 33 | 33 | ||
| 34 | 34 | // Workspace styling defaults | |
| 35 | - | defaultBorderWidth = 2 | |
| 36 | - | defaultBorderColor = "@theme_selected_bg_color" | |
| 37 | - | defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator | |
| 38 | - | defaultTransitionDuration = 120 | |
| 39 | - | defaultBorderRadius = 0 | |
| 35 | + | defaultBorderWidth = 2 | |
| 36 | + | defaultBorderColor = "@theme_selected_bg_color" | |
| 37 | + | defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator | |
| 38 | + | defaultTransitionDuration = 120 | |
| 39 | + | defaultBorderRadius = 0 | |
| 40 | 40 | ) | |
| 41 | 41 | ||
| 42 | 42 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | |
| @@ -32,11 +32,11 @@ const ( | |||||
| 32 | defaultPopupPlacement = "right" | 32 | defaultPopupPlacement = "right" | ||
| 33 | 33 | ||||
| 34 | // Workspace styling defaults | 34 | // Workspace styling defaults | ||
| 35 | - | defaultBorderWidth = 2 | 35 | + | defaultBorderWidth = 2 |
| 36 | - | defaultBorderColor = "@theme_selected_bg_color" | 36 | + | defaultBorderColor = "@theme_selected_bg_color" |
| 37 | - | defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator | 37 | + | defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator |
| 38 | - | defaultTransitionDuration = 120 | 38 | + | defaultTransitionDuration = 120 |
| 39 | - | defaultBorderRadius = 0 | 39 | + | defaultBorderRadius = 0 |
| 40 | ) | 40 | ) | ||
| 41 | 41 | ||||
| 42 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | 42 | // getDefaultLogDir returns the default log directory, falls back to empty string on error | ||
M internal/db/interfaces.go +1 -0
| @@ -17,6 +17,7 @@ type ZoomQuerier interface { | |||
| 17 | 17 | // HistoryQuerier defines the interface for history-related database operations | |
| 18 | 18 | type HistoryQuerier interface { | |
| 19 | 19 | GetHistory(ctx context.Context, limit int64) ([]History, error) | |
| 20 | + | GetHistoryEntry(ctx context.Context, url string) (History, error) | |
| 20 | 21 | GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) | |
| 21 | 22 | SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error) | |
| 22 | 23 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | |
| @@ -17,6 +17,7 @@ type ZoomQuerier interface { | |||||
| 17 | // HistoryQuerier defines the interface for history-related database operations | 17 | // HistoryQuerier defines the interface for history-related database operations | ||
| 18 | type HistoryQuerier interface { | 18 | type HistoryQuerier interface { | ||
| 19 | GetHistory(ctx context.Context, limit int64) ([]History, error) | 19 | GetHistory(ctx context.Context, limit int64) ([]History, error) | ||
| 20 | + | GetHistoryEntry(ctx context.Context, url string) (History, error) | |||
| 20 | GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) | 21 | GetHistoryWithOffset(ctx context.Context, limit int64, offset int64) ([]History, error) | ||
| 21 | SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error) | 22 | SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error) | ||
| 22 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | 23 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | ||
M internal/migrations/migrations_test.go +4 -4
| @@ -418,10 +418,10 @@ func TestExpectedTablesExistAfterMigrations(t *testing.T) { | |||
| 418 | 418 | ||
| 419 | 419 | // Expected tables based on current migrations | |
| 420 | 420 | expectedTables := []string{ | |
| 421 | - | "schema_migrations", // Migration tracking | |
| 422 | - | "history", // 001_initial.sql | |
| 423 | - | "shortcuts", // 001_initial.sql | |
| 424 | - | "zoom_levels", // 003_add_zoom_levels.sql | |
| 421 | + | "schema_migrations", // Migration tracking | |
| 422 | + | "history", // 001_initial.sql | |
| 423 | + | "shortcuts", // 001_initial.sql | |
| 424 | + | "zoom_levels", // 003_add_zoom_levels.sql | |
| 425 | 425 | "certificate_validations", // 005_add_certificate_validations.sql | |
| 426 | 426 | } | |
| 427 | 427 | ||
| @@ -418,10 +418,10 @@ func TestExpectedTablesExistAfterMigrations(t *testing.T) { | |||||
| 418 | 418 | ||||
| 419 | // Expected tables based on current migrations | 419 | // Expected tables based on current migrations | ||
| 420 | expectedTables := []string{ | 420 | expectedTables := []string{ | ||
| 421 | - | "schema_migrations", // Migration tracking | 421 | + | "schema_migrations", // Migration tracking |
| 422 | - | "history", // 001_initial.sql | 422 | + | "history", // 001_initial.sql |
| 423 | - | "shortcuts", // 001_initial.sql | 423 | + | "shortcuts", // 001_initial.sql |
| 424 | - | "zoom_levels", // 003_add_zoom_levels.sql | 424 | + | "zoom_levels", // 003_add_zoom_levels.sql |
| 425 | "certificate_validations", // 005_add_certificate_validations.sql | 425 | "certificate_validations", // 005_add_certificate_validations.sql | ||
| 426 | } | 426 | } | ||
| 427 | 427 | ||||
M internal/services/browser.go +4 -106
| @@ -3,7 +3,6 @@ package services | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "context" | |
| 6 | - | "crypto/md5" | |
| 7 | 6 | "database/sql" | |
| 8 | 7 | "embed" | |
| 9 | 8 | "errors" | |
| @@ -13,7 +12,6 @@ import ( | |||
| 13 | 12 | neturl "net/url" | |
| 14 | 13 | "time" | |
| 15 | 14 | ||
| 16 | - | "github.com/bnema/dumber/internal/cache" | |
| 17 | 15 | "github.com/bnema/dumber/internal/config" | |
| 18 | 16 | "github.com/bnema/dumber/internal/db" | |
| 19 | 17 | "github.com/bnema/dumber/pkg/webkit" | |
| @@ -91,20 +89,7 @@ func (s *BrowserService) SetWindowTitleUpdater(updater WindowTitleUpdater) { | |||
| 91 | 89 | // AttachWebView connects a native WebKit WebView to this service for integration. | |
| 92 | 90 | func (s *BrowserService) AttachWebView(view *webkit.WebView) { | |
| 93 | 91 | s.webView = view | |
| 94 | - | ||
| 95 | - | // Register legacy favicon change handler (binary data - fallback) | |
| 96 | - | // This is kept for backward compatibility but is rarely used | |
| 97 | - | // Modern favicon handling uses the URI-based handler registered in buildPane() | |
| 98 | - | view.RegisterFaviconChangedHandler(func(data []byte) { | |
| 99 | - | if currentURL := view.GetCurrentURL(); currentURL != "" { | |
| 100 | - | log.Printf("[favicon] Favicon detected for %s, data size: %d bytes", currentURL, len(data)) | |
| 101 | - | s.handleFaviconChanged(currentURL, data) | |
| 102 | - | } | |
| 103 | - | }) | |
| 104 | - | ||
| 105 | - | // Note: RegisterFaviconURIChangedHandler is now only called in buildPane() | |
| 106 | - | // to avoid duplicate handler registration which causes multiple downloads | |
| 107 | - | // of the same favicon and file handle race conditions | |
| 92 | + | // Favicon handling is now managed by FaviconService | |
| 108 | 93 | } | |
| 109 | 94 | ||
| 110 | 95 | // LoadGUIBundle loads the unified GUI bundle from assets | |
| @@ -302,11 +287,13 @@ func (s *BrowserService) SearchHistory(ctx context.Context, query string, limit | |||
| 302 | 287 | result := make([]HistoryEntry, len(entries)) | |
| 303 | 288 | for i, entry := range entries { | |
| 304 | 289 | vc := clampToInt32(entry.VisitCount.Int64) | |
| 290 | + | // For omnibox display, convert favicon URI to file path when available | |
| 291 | + | faviconURL := entry.FaviconUrl.String | |
| 305 | 292 | result[i] = HistoryEntry{ | |
| 306 | 293 | ID: entry.ID, | |
| 307 | 294 | URL: entry.Url, | |
| 308 | 295 | Title: entry.Title.String, | |
| 309 | - | FaviconURL: entry.FaviconUrl.String, | |
| 296 | + | FaviconURL: faviconURL, | |
| 310 | 297 | VisitCount: vc, | |
| 311 | 298 | LastVisited: entry.LastVisited.Time, | |
| 312 | 299 | CreatedAt: entry.CreatedAt.Time, | |
| @@ -568,95 +555,6 @@ func zoomKeyFromURL(raw string) string { | |||
| 568 | 555 | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | |
| 569 | 556 | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | |
| 570 | 557 | ||
| 571 | - | // handleFaviconChanged processes favicon data received from WebKit (legacy binary data method) | |
| 572 | - | func (s *BrowserService) handleFaviconChanged(pageURL string, pngData []byte) { | |
| 573 | - | log.Printf("[favicon] Processing favicon for %s, data size: %d bytes", pageURL, len(pngData)) | |
| 574 | - | ||
| 575 | - | if len(pngData) == 0 { | |
| 576 | - | log.Printf("[favicon] No favicon data for %s", pageURL) | |
| 577 | - | return | |
| 578 | - | } | |
| 579 | - | ||
| 580 | - | ctx := context.Background() | |
| 581 | - | ||
| 582 | - | // Parse URL for validation | |
| 583 | - | parsedURL, err := neturl.Parse(pageURL) | |
| 584 | - | if err != nil { | |
| 585 | - | return | |
| 586 | - | } | |
| 587 | - | ||
| 588 | - | // Skip favicon update for localhost, file://, or special schemes | |
| 589 | - | if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { | |
| 590 | - | return | |
| 591 | - | } | |
| 592 | - | if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" { | |
| 593 | - | return | |
| 594 | - | } | |
| 595 | - | ||
| 596 | - | // Cache the PNG data directly | |
| 597 | - | if faviconCache, err := cache.NewFaviconCache(); err == nil { | |
| 598 | - | if err := faviconCache.CachePNGData(pageURL, pngData); err != nil { | |
| 599 | - | // Log error but don't fail | |
| 600 | - | log.Printf("[favicon] Failed to cache favicon for %s: %v", pageURL, err) | |
| 601 | - | } else { | |
| 602 | - | log.Printf("[favicon] Successfully cached favicon for %s (%d bytes)", pageURL, len(pngData)) | |
| 603 | - | } | |
| 604 | - | } else { | |
| 605 | - | log.Printf("[favicon] Failed to create favicon cache: %v", err) | |
| 606 | - | } | |
| 607 | - | ||
| 608 | - | // Store a reference to the cached file in the database | |
| 609 | - | // We use a consistent path format for the favicon URL | |
| 610 | - | faviconCacheURL := fmt.Sprintf("dumb://favicon/%x.png", md5.Sum([]byte(pageURL))) | |
| 611 | - | faviconNullString := sql.NullString{String: faviconCacheURL, Valid: true} | |
| 612 | - | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |
| 613 | - | // Silently fail - favicon is not critical | |
| 614 | - | log.Printf("[browser] Failed to update favicon URL in database for %s: %v", pageURL, err) | |
| 615 | - | } | |
| 616 | - | } | |
| 617 | - | ||
| 618 | - | // ProcessFaviconURI is a public wrapper for handling favicon URI changes from any webview | |
| 619 | - | func (s *BrowserService) ProcessFaviconURI(pageURL string, faviconURI string) { | |
| 620 | - | s.handleFaviconURIChanged(pageURL, faviconURI) | |
| 621 | - | } | |
| 622 | - | ||
| 623 | - | // handleFaviconURIChanged processes favicon URI changes from WebKit's native favicon database | |
| 624 | - | func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI string) { | |
| 625 | - | log.Printf("[favicon] Processing favicon URI for %s: %s", pageURL, faviconURI) | |
| 626 | - | ||
| 627 | - | ctx := context.Background() | |
| 628 | - | ||
| 629 | - | // Parse URL for validation | |
| 630 | - | parsedURL, err := neturl.Parse(pageURL) | |
| 631 | - | if err != nil { | |
| 632 | - | log.Printf("[favicon] Invalid page URL: %s", pageURL) | |
| 633 | - | return | |
| 634 | - | } | |
| 635 | - | ||
| 636 | - | // Skip favicon update for localhost, file://, or special schemes | |
| 637 | - | if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { | |
| 638 | - | return | |
| 639 | - | } | |
| 640 | - | if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" { | |
| 641 | - | return | |
| 642 | - | } | |
| 643 | - | ||
| 644 | - | // Store the favicon URI directly in the database | |
| 645 | - | // The frontend will handle SVG vs PNG/ICO detection | |
| 646 | - | faviconNullString := sql.NullString{String: faviconURI, Valid: true} | |
| 647 | - | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |
| 648 | - | log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err) | |
| 649 | - | } | |
| 650 | - | ||
| 651 | - | // Download and cache the favicon asynchronously (same approach as dmenu) | |
| 652 | - | if faviconCache, err := cache.NewFaviconCache(); err == nil { | |
| 653 | - | faviconCache.CacheAsync(faviconURI) | |
| 654 | - | log.Printf("[favicon] Started async download of favicon from %s for page %s", faviconURI, pageURL) | |
| 655 | - | } else { | |
| 656 | - | log.Printf("[favicon] Failed to create favicon cache: %v", err) | |
| 657 | - | } | |
| 658 | - | } | |
| 659 | - | ||
| 660 | 558 | // ColorPalettesResponse holds light and dark palettes for JSON marshaling | |
| 661 | 559 | type ColorPalettesResponse struct { | |
| 662 | 560 | Light config.ColorPalette `json:"light"` | |
| @@ -3,7 +3,6 @@ package services | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "context" | 5 | "context" | ||
| 6 | - | "crypto/md5" | |||
| 7 | "database/sql" | 6 | "database/sql" | ||
| 8 | "embed" | 7 | "embed" | ||
| 9 | "errors" | 8 | "errors" | ||
| @@ -13,7 +12,6 @@ import ( | |||||
| 13 | neturl "net/url" | 12 | neturl "net/url" | ||
| 14 | "time" | 13 | "time" | ||
| 15 | 14 | ||||
| 16 | - | "github.com/bnema/dumber/internal/cache" | |||
| 17 | "github.com/bnema/dumber/internal/config" | 15 | "github.com/bnema/dumber/internal/config" | ||
| 18 | "github.com/bnema/dumber/internal/db" | 16 | "github.com/bnema/dumber/internal/db" | ||
| 19 | "github.com/bnema/dumber/pkg/webkit" | 17 | "github.com/bnema/dumber/pkg/webkit" | ||
| @@ -91,20 +89,7 @@ func (s *BrowserService) SetWindowTitleUpdater(updater WindowTitleUpdater) { | |||||
| 91 | // AttachWebView connects a native WebKit WebView to this service for integration. | 89 | // AttachWebView connects a native WebKit WebView to this service for integration. | ||
| 92 | func (s *BrowserService) AttachWebView(view *webkit.WebView) { | 90 | func (s *BrowserService) AttachWebView(view *webkit.WebView) { | ||
| 93 | s.webView = view | 91 | s.webView = view | ||
| 94 | - | 92 | + | // Favicon handling is now managed by FaviconService | |
| 95 | - | // Register legacy favicon change handler (binary data - fallback) | |||
| 96 | - | // This is kept for backward compatibility but is rarely used | |||
| 97 | - | // Modern favicon handling uses the URI-based handler registered in buildPane() | |||
| 98 | - | view.RegisterFaviconChangedHandler(func(data []byte) { | |||
| 99 | - | if currentURL := view.GetCurrentURL(); currentURL != "" { | |||
| 100 | - | log.Printf("[favicon] Favicon detected for %s, data size: %d bytes", currentURL, len(data)) | |||
| 101 | - | s.handleFaviconChanged(currentURL, data) | |||
| 102 | - | } | |||
| 103 | - | }) | |||
| 104 | - | ||||
| 105 | - | // Note: RegisterFaviconURIChangedHandler is now only called in buildPane() | |||
| 106 | - | // to avoid duplicate handler registration which causes multiple downloads | |||
| 107 | - | // of the same favicon and file handle race conditions | |||
| 108 | } | 93 | } | ||
| 109 | 94 | ||||
| 110 | // LoadGUIBundle loads the unified GUI bundle from assets | 95 | // LoadGUIBundle loads the unified GUI bundle from assets | ||
| @@ -302,11 +287,13 @@ func (s *BrowserService) SearchHistory(ctx context.Context, query string, limit | |||||
| 302 | result := make([]HistoryEntry, len(entries)) | 287 | result := make([]HistoryEntry, len(entries)) | ||
| 303 | for i, entry := range entries { | 288 | for i, entry := range entries { | ||
| 304 | vc := clampToInt32(entry.VisitCount.Int64) | 289 | vc := clampToInt32(entry.VisitCount.Int64) | ||
| 290 | + | // For omnibox display, convert favicon URI to file path when available | |||
| 291 | + | faviconURL := entry.FaviconUrl.String | |||
| 305 | result[i] = HistoryEntry{ | 292 | result[i] = HistoryEntry{ | ||
| 306 | ID: entry.ID, | 293 | ID: entry.ID, | ||
| 307 | URL: entry.Url, | 294 | URL: entry.Url, | ||
| 308 | Title: entry.Title.String, | 295 | Title: entry.Title.String, | ||
| 309 | - | FaviconURL: entry.FaviconUrl.String, | 296 | + | FaviconURL: faviconURL, |
| 310 | VisitCount: vc, | 297 | VisitCount: vc, | ||
| 311 | LastVisited: entry.LastVisited.Time, | 298 | LastVisited: entry.LastVisited.Time, | ||
| 312 | CreatedAt: entry.CreatedAt.Time, | 299 | CreatedAt: entry.CreatedAt.Time, | ||
| @@ -568,95 +555,6 @@ func zoomKeyFromURL(raw string) string { | |||||
| 568 | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | 555 | // ZoomKeyForLog exposes the derived zoom key (host or raw URL) for logging from other packages. | ||
| 569 | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | 556 | func ZoomKeyForLog(raw string) string { return zoomKeyFromURL(raw) } | ||
| 570 | 557 | ||||
| 571 | - | // handleFaviconChanged processes favicon data received from WebKit (legacy binary data method) | |||
| 572 | - | func (s *BrowserService) handleFaviconChanged(pageURL string, pngData []byte) { | |||
| 573 | - | log.Printf("[favicon] Processing favicon for %s, data size: %d bytes", pageURL, len(pngData)) | |||
| 574 | - | ||||
| 575 | - | if len(pngData) == 0 { | |||
| 576 | - | log.Printf("[favicon] No favicon data for %s", pageURL) | |||
| 577 | - | return | |||
| 578 | - | } | |||
| 579 | - | ||||
| 580 | - | ctx := context.Background() | |||
| 581 | - | ||||
| 582 | - | // Parse URL for validation | |||
| 583 | - | parsedURL, err := neturl.Parse(pageURL) | |||
| 584 | - | if err != nil { | |||
| 585 | - | return | |||
| 586 | - | } | |||
| 587 | - | ||||
| 588 | - | // Skip favicon update for localhost, file://, or special schemes | |||
| 589 | - | if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { | |||
| 590 | - | return | |||
| 591 | - | } | |||
| 592 | - | if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" { | |||
| 593 | - | return | |||
| 594 | - | } | |||
| 595 | - | ||||
| 596 | - | // Cache the PNG data directly | |||
| 597 | - | if faviconCache, err := cache.NewFaviconCache(); err == nil { | |||
| 598 | - | if err := faviconCache.CachePNGData(pageURL, pngData); err != nil { | |||
| 599 | - | // Log error but don't fail | |||
| 600 | - | log.Printf("[favicon] Failed to cache favicon for %s: %v", pageURL, err) | |||
| 601 | - | } else { | |||
| 602 | - | log.Printf("[favicon] Successfully cached favicon for %s (%d bytes)", pageURL, len(pngData)) | |||
| 603 | - | } | |||
| 604 | - | } else { | |||
| 605 | - | log.Printf("[favicon] Failed to create favicon cache: %v", err) | |||
| 606 | - | } | |||
| 607 | - | ||||
| 608 | - | // Store a reference to the cached file in the database | |||
| 609 | - | // We use a consistent path format for the favicon URL | |||
| 610 | - | faviconCacheURL := fmt.Sprintf("dumb://favicon/%x.png", md5.Sum([]byte(pageURL))) | |||
| 611 | - | faviconNullString := sql.NullString{String: faviconCacheURL, Valid: true} | |||
| 612 | - | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |||
| 613 | - | // Silently fail - favicon is not critical | |||
| 614 | - | log.Printf("[browser] Failed to update favicon URL in database for %s: %v", pageURL, err) | |||
| 615 | - | } | |||
| 616 | - | } | |||
| 617 | - | ||||
| 618 | - | // ProcessFaviconURI is a public wrapper for handling favicon URI changes from any webview | |||
| 619 | - | func (s *BrowserService) ProcessFaviconURI(pageURL string, faviconURI string) { | |||
| 620 | - | s.handleFaviconURIChanged(pageURL, faviconURI) | |||
| 621 | - | } | |||
| 622 | - | ||||
| 623 | - | // handleFaviconURIChanged processes favicon URI changes from WebKit's native favicon database | |||
| 624 | - | func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI string) { | |||
| 625 | - | log.Printf("[favicon] Processing favicon URI for %s: %s", pageURL, faviconURI) | |||
| 626 | - | ||||
| 627 | - | ctx := context.Background() | |||
| 628 | - | ||||
| 629 | - | // Parse URL for validation | |||
| 630 | - | parsedURL, err := neturl.Parse(pageURL) | |||
| 631 | - | if err != nil { | |||
| 632 | - | log.Printf("[favicon] Invalid page URL: %s", pageURL) | |||
| 633 | - | return | |||
| 634 | - | } | |||
| 635 | - | ||||
| 636 | - | // Skip favicon update for localhost, file://, or special schemes | |||
| 637 | - | if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { | |||
| 638 | - | return | |||
| 639 | - | } | |||
| 640 | - | if parsedURL.Host == "localhost" || parsedURL.Host == "127.0.0.1" { | |||
| 641 | - | return | |||
| 642 | - | } | |||
| 643 | - | ||||
| 644 | - | // Store the favicon URI directly in the database | |||
| 645 | - | // The frontend will handle SVG vs PNG/ICO detection | |||
| 646 | - | faviconNullString := sql.NullString{String: faviconURI, Valid: true} | |||
| 647 | - | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |||
| 648 | - | log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err) | |||
| 649 | - | } | |||
| 650 | - | ||||
| 651 | - | // Download and cache the favicon asynchronously (same approach as dmenu) | |||
| 652 | - | if faviconCache, err := cache.NewFaviconCache(); err == nil { | |||
| 653 | - | faviconCache.CacheAsync(faviconURI) | |||
| 654 | - | log.Printf("[favicon] Started async download of favicon from %s for page %s", faviconURI, pageURL) | |||
| 655 | - | } else { | |||
| 656 | - | log.Printf("[favicon] Failed to create favicon cache: %v", err) | |||
| 657 | - | } | |||
| 658 | - | } | |||
| 659 | - | ||||
| 660 | // ColorPalettesResponse holds light and dark palettes for JSON marshaling | 558 | // ColorPalettesResponse holds light and dark palettes for JSON marshaling | ||
| 661 | type ColorPalettesResponse struct { | 559 | type ColorPalettesResponse struct { | ||
| 662 | Light config.ColorPalette `json:"light"` | 560 | Light config.ColorPalette `json:"light"` | ||