feat(ui): integrate download handling with toaster notifications
- commit
- 4baeb0b429b004ca8d1a5f208d85d5960facdece
- author
- brice · · 2026-01-07 09:55 UTC
- parent
- d9269b5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +69 -0
M cmd/dumber/main.go +2 -0
| @@ -25,6 +25,7 @@ import ( | |||
| 25 | 25 | "github.com/bnema/dumber/internal/infrastructure/idle" | |
| 26 | 26 | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite" | |
| 27 | 27 | "github.com/bnema/dumber/internal/infrastructure/updater" | |
| 28 | + | "github.com/bnema/dumber/internal/infrastructure/xdg" | |
| 28 | 29 | "github.com/bnema/dumber/internal/logging" | |
| 29 | 30 | "github.com/bnema/dumber/internal/ui" | |
| 30 | 31 | "github.com/bnema/dumber/internal/ui/theme" | |
| @@ -428,6 +429,7 @@ func buildUIDependencies( | |||
| 428 | 429 | Theme: themeManager, | |
| 429 | 430 | ColorResolver: colorResolver, | |
| 430 | 431 | AdwaitaDetector: adwaitaDetector, | |
| 432 | + | XDG: xdg.New(), | |
| 431 | 433 | WebContext: stack.Context, | |
| 432 | 434 | Pool: stack.Pool, | |
| 433 | 435 | Settings: stack.Settings, | |
| @@ -25,6 +25,7 @@ import ( | |||||
| 25 | "github.com/bnema/dumber/internal/infrastructure/idle" | 25 | "github.com/bnema/dumber/internal/infrastructure/idle" | ||
| 26 | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite" | 26 | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite" | ||
| 27 | "github.com/bnema/dumber/internal/infrastructure/updater" | 27 | "github.com/bnema/dumber/internal/infrastructure/updater" | ||
| 28 | + | "github.com/bnema/dumber/internal/infrastructure/xdg" | |||
| 28 | "github.com/bnema/dumber/internal/logging" | 29 | "github.com/bnema/dumber/internal/logging" | ||
| 29 | "github.com/bnema/dumber/internal/ui" | 30 | "github.com/bnema/dumber/internal/ui" | ||
| 30 | "github.com/bnema/dumber/internal/ui/theme" | 31 | "github.com/bnema/dumber/internal/ui/theme" | ||
| @@ -428,6 +429,7 @@ func buildUIDependencies( | |||||
| 428 | Theme: themeManager, | 429 | Theme: themeManager, | ||
| 429 | ColorResolver: colorResolver, | 430 | ColorResolver: colorResolver, | ||
| 430 | AdwaitaDetector: adwaitaDetector, | 431 | AdwaitaDetector: adwaitaDetector, | ||
| 432 | + | XDG: xdg.New(), | |||
| 431 | WebContext: stack.Context, | 433 | WebContext: stack.Context, | ||
| 432 | Pool: stack.Pool, | 434 | Pool: stack.Pool, | ||
| 433 | Settings: stack.Settings, | 435 | Settings: stack.Settings, | ||
M internal/ui/app.go +64 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "os" | |
| 8 | + | "path/filepath" | |
| 8 | 9 | "sync" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/dumber/internal/application/port" | |
| @@ -240,6 +241,7 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 240 | 241 | a.initAppToasterOverlay() | |
| 241 | 242 | a.initFocusAndBorderOverlay() | |
| 242 | 243 | a.initAccentPicker(ctx) | |
| 244 | + | a.initDownloadHandler(ctx) | |
| 243 | 245 | ||
| 244 | 246 | a.initCoordinators(ctx) | |
| 245 | 247 | a.initKeyboardHandler(ctx) | |
| @@ -453,6 +455,68 @@ func (a *App) initAccentPicker(ctx context.Context) { | |||
| 453 | 455 | log.Debug().Msg("accent picker initialized") | |
| 454 | 456 | } | |
| 455 | 457 | ||
| 458 | + | func (a *App) initDownloadHandler(ctx context.Context) { | |
| 459 | + | log := logging.FromContext(ctx) | |
| 460 | + | ||
| 461 | + | if a.deps == nil || a.deps.WebContext == nil { | |
| 462 | + | log.Debug().Msg("WebContext not available, skipping download handler") | |
| 463 | + | return | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | // Determine download path from config, fallback to XDG. | |
| 467 | + | downloadPath := "" | |
| 468 | + | if a.deps.Config != nil { | |
| 469 | + | downloadPath = a.deps.Config.Downloads.Path | |
| 470 | + | } | |
| 471 | + | if downloadPath == "" && a.deps.XDG != nil { | |
| 472 | + | var err error | |
| 473 | + | downloadPath, err = a.deps.XDG.DownloadDir() | |
| 474 | + | if err != nil { | |
| 475 | + | log.Warn().Err(err).Msg("failed to get XDG download dir") | |
| 476 | + | } | |
| 477 | + | } | |
| 478 | + | if downloadPath == "" { | |
| 479 | + | // Final fallback to ~/Downloads. | |
| 480 | + | home, _ := os.UserHomeDir() | |
| 481 | + | downloadPath = filepath.Join(home, "Downloads") | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | // Create download event adapter to show toasts. | |
| 485 | + | eventAdapter := &downloadEventAdapter{app: a, ctx: ctx} | |
| 486 | + | ||
| 487 | + | // Create and wire the download handler. | |
| 488 | + | handler := webkit.NewDownloadHandler(downloadPath, eventAdapter) | |
| 489 | + | a.deps.WebContext.SetDownloadHandler(ctx, handler) | |
| 490 | + | ||
| 491 | + | log.Info().Str("path", downloadPath).Msg("download handler initialized") | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | // downloadEventAdapter implements port.DownloadEventHandler and shows toasts. | |
| 495 | + | type downloadEventAdapter struct { | |
| 496 | + | app *App | |
| 497 | + | ctx context.Context | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.DownloadEvent) { | |
| 501 | + | // Must schedule on GTK main thread. | |
| 502 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 503 | + | if d.app.appToaster == nil { | |
| 504 | + | return false | |
| 505 | + | } | |
| 506 | + | ||
| 507 | + | switch event.Type { | |
| 508 | + | case port.DownloadEventStarted: | |
| 509 | + | d.app.appToaster.Show(ctx, "Download started: "+event.Filename, component.ToastInfo) | |
| 510 | + | case port.DownloadEventFinished: | |
| 511 | + | d.app.appToaster.Show(ctx, "Download complete: "+event.Filename, component.ToastSuccess) | |
| 512 | + | case port.DownloadEventFailed: | |
| 513 | + | d.app.appToaster.Show(ctx, "Download failed: "+event.Filename, component.ToastError) | |
| 514 | + | } | |
| 515 | + | return false | |
| 516 | + | }) | |
| 517 | + | glib.IdleAdd(&cb, 0) | |
| 518 | + | } | |
| 519 | + | ||
| 456 | 520 | func (a *App) initKeyboardHandler(ctx context.Context) { | |
| 457 | 521 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | |
| 458 | 522 | return | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | + | "path/filepath" | |||
| 8 | "sync" | 9 | "sync" | ||
| 9 | 10 | ||||
| 10 | "github.com/bnema/dumber/internal/application/port" | 11 | "github.com/bnema/dumber/internal/application/port" | ||
| @@ -240,6 +241,7 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 240 | a.initAppToasterOverlay() | 241 | a.initAppToasterOverlay() | ||
| 241 | a.initFocusAndBorderOverlay() | 242 | a.initFocusAndBorderOverlay() | ||
| 242 | a.initAccentPicker(ctx) | 243 | a.initAccentPicker(ctx) | ||
| 244 | + | a.initDownloadHandler(ctx) | |||
| 243 | 245 | ||||
| 244 | a.initCoordinators(ctx) | 246 | a.initCoordinators(ctx) | ||
| 245 | a.initKeyboardHandler(ctx) | 247 | a.initKeyboardHandler(ctx) | ||
| @@ -453,6 +455,68 @@ func (a *App) initAccentPicker(ctx context.Context) { | |||||
| 453 | log.Debug().Msg("accent picker initialized") | 455 | log.Debug().Msg("accent picker initialized") | ||
| 454 | } | 456 | } | ||
| 455 | 457 | ||||
| 458 | + | func (a *App) initDownloadHandler(ctx context.Context) { | |||
| 459 | + | log := logging.FromContext(ctx) | |||
| 460 | + | ||||
| 461 | + | if a.deps == nil || a.deps.WebContext == nil { | |||
| 462 | + | log.Debug().Msg("WebContext not available, skipping download handler") | |||
| 463 | + | return | |||
| 464 | + | } | |||
| 465 | + | ||||
| 466 | + | // Determine download path from config, fallback to XDG. | |||
| 467 | + | downloadPath := "" | |||
| 468 | + | if a.deps.Config != nil { | |||
| 469 | + | downloadPath = a.deps.Config.Downloads.Path | |||
| 470 | + | } | |||
| 471 | + | if downloadPath == "" && a.deps.XDG != nil { | |||
| 472 | + | var err error | |||
| 473 | + | downloadPath, err = a.deps.XDG.DownloadDir() | |||
| 474 | + | if err != nil { | |||
| 475 | + | log.Warn().Err(err).Msg("failed to get XDG download dir") | |||
| 476 | + | } | |||
| 477 | + | } | |||
| 478 | + | if downloadPath == "" { | |||
| 479 | + | // Final fallback to ~/Downloads. | |||
| 480 | + | home, _ := os.UserHomeDir() | |||
| 481 | + | downloadPath = filepath.Join(home, "Downloads") | |||
| 482 | + | } | |||
| 483 | + | ||||
| 484 | + | // Create download event adapter to show toasts. | |||
| 485 | + | eventAdapter := &downloadEventAdapter{app: a, ctx: ctx} | |||
| 486 | + | ||||
| 487 | + | // Create and wire the download handler. | |||
| 488 | + | handler := webkit.NewDownloadHandler(downloadPath, eventAdapter) | |||
| 489 | + | a.deps.WebContext.SetDownloadHandler(ctx, handler) | |||
| 490 | + | ||||
| 491 | + | log.Info().Str("path", downloadPath).Msg("download handler initialized") | |||
| 492 | + | } | |||
| 493 | + | ||||
| 494 | + | // downloadEventAdapter implements port.DownloadEventHandler and shows toasts. | |||
| 495 | + | type downloadEventAdapter struct { | |||
| 496 | + | app *App | |||
| 497 | + | ctx context.Context | |||
| 498 | + | } | |||
| 499 | + | ||||
| 500 | + | func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.DownloadEvent) { | |||
| 501 | + | // Must schedule on GTK main thread. | |||
| 502 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 503 | + | if d.app.appToaster == nil { | |||
| 504 | + | return false | |||
| 505 | + | } | |||
| 506 | + | ||||
| 507 | + | switch event.Type { | |||
| 508 | + | case port.DownloadEventStarted: | |||
| 509 | + | d.app.appToaster.Show(ctx, "Download started: "+event.Filename, component.ToastInfo) | |||
| 510 | + | case port.DownloadEventFinished: | |||
| 511 | + | d.app.appToaster.Show(ctx, "Download complete: "+event.Filename, component.ToastSuccess) | |||
| 512 | + | case port.DownloadEventFailed: | |||
| 513 | + | d.app.appToaster.Show(ctx, "Download failed: "+event.Filename, component.ToastError) | |||
| 514 | + | } | |||
| 515 | + | return false | |||
| 516 | + | }) | |||
| 517 | + | glib.IdleAdd(&cb, 0) | |||
| 518 | + | } | |||
| 519 | + | ||||
| 456 | func (a *App) initKeyboardHandler(ctx context.Context) { | 520 | func (a *App) initKeyboardHandler(ctx context.Context) { | ||
| 457 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | 521 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | ||
| 458 | return | 522 | return | ||
M internal/ui/deps.go +3 -0
| @@ -31,6 +31,9 @@ type Dependencies struct { | |||
| 31 | 31 | ColorResolver port.ColorSchemeResolver | |
| 32 | 32 | AdwaitaDetector *colorscheme.AdwaitaDetector | |
| 33 | 33 | ||
| 34 | + | // XDG paths | |
| 35 | + | XDG port.XDGPaths | |
| 36 | + | ||
| 34 | 37 | // WebKit infrastructure | |
| 35 | 38 | WebContext *webkit.WebKitContext | |
| 36 | 39 | Pool *webkit.WebViewPool | |
| @@ -31,6 +31,9 @@ type Dependencies struct { | |||||
| 31 | ColorResolver port.ColorSchemeResolver | 31 | ColorResolver port.ColorSchemeResolver | ||
| 32 | AdwaitaDetector *colorscheme.AdwaitaDetector | 32 | AdwaitaDetector *colorscheme.AdwaitaDetector | ||
| 33 | 33 | ||||
| 34 | + | // XDG paths | |||
| 35 | + | XDG port.XDGPaths | |||
| 36 | + | ||||
| 34 | // WebKit infrastructure | 37 | // WebKit infrastructure | ||
| 35 | WebContext *webkit.WebKitContext | 38 | WebContext *webkit.WebKitContext | ||
| 36 | Pool *webkit.WebViewPool | 39 | Pool *webkit.WebViewPool | ||