fix(ui): integrate toast notifications into pane view and fix progress bar issues
- commit
- f7dbba0d7fb9178f07e0d44d8ec780f6eac089a9
- author
- brice · · 2025-12-17 20:02 UTC
- committer
- brice · · 2025-12-17 17:40 UTC
- parent
- 83a6fc8
- signature
- unsigned
3 files · +77 -22
M internal/infrastructure/persistence/sqlite/sqlc/querier.go +1 -0
| @@ -51,6 +51,7 @@ type Querier interface { | |||
| 51 | 51 | RemoveFromWhitelist(ctx context.Context, domain string) error | |
| 52 | 52 | RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error | |
| 53 | 53 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | |
| 54 | + | SearchHistoryFTS(ctx context.Context, arg SearchHistoryFTSParams) ([]History, error) | |
| 54 | 55 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | |
| 55 | 56 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | |
| 56 | 57 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | |
| @@ -51,6 +51,7 @@ type Querier interface { | |||||
| 51 | RemoveFromWhitelist(ctx context.Context, domain string) error | 51 | RemoveFromWhitelist(ctx context.Context, domain string) error | ||
| 52 | RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error | 52 | RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error | ||
| 53 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | 53 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | ||
| 54 | + | SearchHistoryFTS(ctx context.Context, arg SearchHistoryFTSParams) ([]History, error) | |||
| 54 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | 55 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | ||
| 55 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | 56 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | ||
| 56 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | 57 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | ||
M internal/ui/component/pane_view.go +67 -21
| @@ -23,6 +23,7 @@ type PaneView struct { | |||
| 23 | 23 | webViewWidget layout.Widget // The actual WebView widget | |
| 24 | 24 | borderBox layout.BoxWidget // Border overlay for active indication | |
| 25 | 25 | progressBar *ProgressBar // Loading progress indicator | |
| 26 | + | toaster *Toaster // Toast notification overlay | |
| 26 | 27 | paneID entity.PaneID | |
| 27 | 28 | isActive bool | |
| 28 | 29 | ||
| @@ -63,18 +64,15 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg | |||
| 63 | 64 | overlay.SetClipOverlay(borderBox, false) | |
| 64 | 65 | overlay.SetMeasureOverlay(borderBox, false) | |
| 65 | 66 | ||
| 66 | - | // Create progress bar overlay at bottom | |
| 67 | - | progressBar := NewProgressBar(factory) | |
| 68 | - | overlay.AddOverlay(progressBar.Widget()) | |
| 69 | - | overlay.SetClipOverlay(progressBar.Widget(), false) | |
| 70 | - | overlay.SetMeasureOverlay(progressBar.Widget(), false) | |
| 67 | + | // Progress bar is created lazily on first use to avoid GTK measurement | |
| 68 | + | // issues with the internal progress gizmo before the overlay is realized | |
| 71 | 69 | ||
| 72 | 70 | return &PaneView{ | |
| 73 | 71 | factory: factory, | |
| 74 | 72 | overlay: overlay, | |
| 75 | 73 | webViewWidget: webViewWidget, | |
| 76 | 74 | borderBox: borderBox, | |
| 77 | - | progressBar: progressBar, | |
| 75 | + | progressBar: nil, // Created lazily in ensureProgressBar() | |
| 78 | 76 | paneID: paneID, | |
| 79 | 77 | isActive: false, | |
| 80 | 78 | } | |
| @@ -276,29 +274,77 @@ func (pv *PaneView) GetContentDimensions() (width, height int) { | |||
| 276 | 274 | return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight() | |
| 277 | 275 | } | |
| 278 | 276 | ||
| 277 | + | // ensureProgressBar creates the progress bar lazily on first use. | |
| 278 | + | // This avoids GTK measurement issues with the internal progress gizmo | |
| 279 | + | // that can occur when the widget is added before the overlay is realized. | |
| 280 | + | // Must be called with write lock held. | |
| 281 | + | func (pv *PaneView) ensureProgressBar() *ProgressBar { | |
| 282 | + | if pv.progressBar != nil { | |
| 283 | + | return pv.progressBar | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | pb := NewProgressBar(pv.factory) | |
| 287 | + | pv.overlay.AddOverlay(pb.Widget()) | |
| 288 | + | pv.overlay.SetClipOverlay(pb.Widget(), false) | |
| 289 | + | pv.overlay.SetMeasureOverlay(pb.Widget(), false) | |
| 290 | + | pv.progressBar = pb | |
| 291 | + | return pb | |
| 292 | + | } | |
| 293 | + | ||
| 279 | 294 | // SetLoadProgress updates the progress bar with the current load progress. | |
| 280 | 295 | // progress should be between 0.0 and 1.0. | |
| 281 | 296 | func (pv *PaneView) SetLoadProgress(progress float64) { | |
| 282 | - | pv.mu.RLock() | |
| 283 | - | pb := pv.progressBar | |
| 284 | - | pv.mu.RUnlock() | |
| 297 | + | pv.mu.Lock() | |
| 298 | + | pb := pv.ensureProgressBar() | |
| 299 | + | pv.mu.Unlock() | |
| 285 | 300 | ||
| 286 | - | if pb != nil { | |
| 287 | - | pb.SetProgress(progress) | |
| 288 | - | } | |
| 301 | + | pb.SetProgress(progress) | |
| 289 | 302 | } | |
| 290 | 303 | ||
| 291 | 304 | // SetLoading shows or hides the progress bar. | |
| 292 | 305 | func (pv *PaneView) SetLoading(loading bool) { | |
| 293 | - | pv.mu.RLock() | |
| 294 | - | pb := pv.progressBar | |
| 295 | - | pv.mu.RUnlock() | |
| 306 | + | pv.mu.Lock() | |
| 307 | + | pb := pv.ensureProgressBar() | |
| 308 | + | pv.mu.Unlock() | |
| 296 | 309 | ||
| 297 | - | if pb != nil { | |
| 298 | - | if loading { | |
| 299 | - | pb.Show() | |
| 300 | - | } else { | |
| 301 | - | pb.Hide() | |
| 302 | - | } | |
| 310 | + | if loading { | |
| 311 | + | pb.Show() | |
| 312 | + | } else { | |
| 313 | + | pb.Hide() | |
| 303 | 314 | } | |
| 304 | 315 | } | |
| 316 | + | ||
| 317 | + | // ensureToaster creates the toaster lazily on first use. | |
| 318 | + | // Must be called with write lock held. | |
| 319 | + | func (pv *PaneView) ensureToaster() *Toaster { | |
| 320 | + | if pv.toaster != nil { | |
| 321 | + | return pv.toaster | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | t := NewToaster(pv.factory) | |
| 325 | + | pv.overlay.AddOverlay(t.Widget()) | |
| 326 | + | pv.overlay.SetClipOverlay(t.Widget(), false) | |
| 327 | + | pv.overlay.SetMeasureOverlay(t.Widget(), false) | |
| 328 | + | pv.toaster = t | |
| 329 | + | return t | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | // ShowToast displays a toast notification with the given message and level. | |
| 333 | + | // If a toast is already visible, updates the text and resets the dismiss timer. | |
| 334 | + | func (pv *PaneView) ShowToast(ctx context.Context, message string, level ToastLevel) { | |
| 335 | + | pv.mu.Lock() | |
| 336 | + | t := pv.ensureToaster() | |
| 337 | + | pv.mu.Unlock() | |
| 338 | + | ||
| 339 | + | t.Show(ctx, message, level) | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | // ShowZoomToast displays a zoom level toast notification. | |
| 343 | + | // Formats the zoom percentage with a % suffix. | |
| 344 | + | func (pv *PaneView) ShowZoomToast(ctx context.Context, zoomPercent int) { | |
| 345 | + | pv.mu.Lock() | |
| 346 | + | t := pv.ensureToaster() | |
| 347 | + | pv.mu.Unlock() | |
| 348 | + | ||
| 349 | + | t.ShowZoom(ctx, zoomPercent) | |
| 350 | + | } | |
| @@ -23,6 +23,7 @@ type PaneView struct { | |||||
| 23 | webViewWidget layout.Widget // The actual WebView widget | 23 | webViewWidget layout.Widget // The actual WebView widget | ||
| 24 | borderBox layout.BoxWidget // Border overlay for active indication | 24 | borderBox layout.BoxWidget // Border overlay for active indication | ||
| 25 | progressBar *ProgressBar // Loading progress indicator | 25 | progressBar *ProgressBar // Loading progress indicator | ||
| 26 | + | toaster *Toaster // Toast notification overlay | |||
| 26 | paneID entity.PaneID | 27 | paneID entity.PaneID | ||
| 27 | isActive bool | 28 | isActive bool | ||
| 28 | 29 | ||||
| @@ -63,18 +64,15 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg | |||||
| 63 | overlay.SetClipOverlay(borderBox, false) | 64 | overlay.SetClipOverlay(borderBox, false) | ||
| 64 | overlay.SetMeasureOverlay(borderBox, false) | 65 | overlay.SetMeasureOverlay(borderBox, false) | ||
| 65 | 66 | ||||
| 66 | - | // Create progress bar overlay at bottom | 67 | + | // Progress bar is created lazily on first use to avoid GTK measurement |
| 67 | - | progressBar := NewProgressBar(factory) | 68 | + | // issues with the internal progress gizmo before the overlay is realized |
| 68 | - | overlay.AddOverlay(progressBar.Widget()) | |||
| 69 | - | overlay.SetClipOverlay(progressBar.Widget(), false) | |||
| 70 | - | overlay.SetMeasureOverlay(progressBar.Widget(), false) | |||
| 71 | 69 | ||||
| 72 | return &PaneView{ | 70 | return &PaneView{ | ||
| 73 | factory: factory, | 71 | factory: factory, | ||
| 74 | overlay: overlay, | 72 | overlay: overlay, | ||
| 75 | webViewWidget: webViewWidget, | 73 | webViewWidget: webViewWidget, | ||
| 76 | borderBox: borderBox, | 74 | borderBox: borderBox, | ||
| 77 | - | progressBar: progressBar, | 75 | + | progressBar: nil, // Created lazily in ensureProgressBar() |
| 78 | paneID: paneID, | 76 | paneID: paneID, | ||
| 79 | isActive: false, | 77 | isActive: false, | ||
| 80 | } | 78 | } | ||
| @@ -276,29 +274,77 @@ func (pv *PaneView) GetContentDimensions() (width, height int) { | |||||
| 276 | return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight() | 274 | return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight() | ||
| 277 | } | 275 | } | ||
| 278 | 276 | ||||
| 277 | + | // ensureProgressBar creates the progress bar lazily on first use. | |||
| 278 | + | // This avoids GTK measurement issues with the internal progress gizmo | |||
| 279 | + | // that can occur when the widget is added before the overlay is realized. | |||
| 280 | + | // Must be called with write lock held. | |||
| 281 | + | func (pv *PaneView) ensureProgressBar() *ProgressBar { | |||
| 282 | + | if pv.progressBar != nil { | |||
| 283 | + | return pv.progressBar | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | pb := NewProgressBar(pv.factory) | |||
| 287 | + | pv.overlay.AddOverlay(pb.Widget()) | |||
| 288 | + | pv.overlay.SetClipOverlay(pb.Widget(), false) | |||
| 289 | + | pv.overlay.SetMeasureOverlay(pb.Widget(), false) | |||
| 290 | + | pv.progressBar = pb | |||
| 291 | + | return pb | |||
| 292 | + | } | |||
| 293 | + | ||||
| 279 | // SetLoadProgress updates the progress bar with the current load progress. | 294 | // SetLoadProgress updates the progress bar with the current load progress. | ||
| 280 | // progress should be between 0.0 and 1.0. | 295 | // progress should be between 0.0 and 1.0. | ||
| 281 | func (pv *PaneView) SetLoadProgress(progress float64) { | 296 | func (pv *PaneView) SetLoadProgress(progress float64) { | ||
| 282 | - | pv.mu.RLock() | 297 | + | pv.mu.Lock() |
| 283 | - | pb := pv.progressBar | 298 | + | pb := pv.ensureProgressBar() |
| 284 | - | pv.mu.RUnlock() | 299 | + | pv.mu.Unlock() |
| 285 | 300 | ||||
| 286 | - | if pb != nil { | 301 | + | pb.SetProgress(progress) |
| 287 | - | pb.SetProgress(progress) | |||
| 288 | - | } | |||
| 289 | } | 302 | } | ||
| 290 | 303 | ||||
| 291 | // SetLoading shows or hides the progress bar. | 304 | // SetLoading shows or hides the progress bar. | ||
| 292 | func (pv *PaneView) SetLoading(loading bool) { | 305 | func (pv *PaneView) SetLoading(loading bool) { | ||
| 293 | - | pv.mu.RLock() | 306 | + | pv.mu.Lock() |
| 294 | - | pb := pv.progressBar | 307 | + | pb := pv.ensureProgressBar() |
| 295 | - | pv.mu.RUnlock() | 308 | + | pv.mu.Unlock() |
| 296 | 309 | ||||
| 297 | - | if pb != nil { | 310 | + | if loading { |
| 298 | - | if loading { | 311 | + | pb.Show() |
| 299 | - | pb.Show() | 312 | + | } else { |
| 300 | - | } else { | 313 | + | pb.Hide() |
| 301 | - | pb.Hide() | |||
| 302 | - | } | |||
| 303 | } | 314 | } | ||
| 304 | } | 315 | } | ||
| 316 | + | ||||
| 317 | + | // ensureToaster creates the toaster lazily on first use. | |||
| 318 | + | // Must be called with write lock held. | |||
| 319 | + | func (pv *PaneView) ensureToaster() *Toaster { | |||
| 320 | + | if pv.toaster != nil { | |||
| 321 | + | return pv.toaster | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | t := NewToaster(pv.factory) | |||
| 325 | + | pv.overlay.AddOverlay(t.Widget()) | |||
| 326 | + | pv.overlay.SetClipOverlay(t.Widget(), false) | |||
| 327 | + | pv.overlay.SetMeasureOverlay(t.Widget(), false) | |||
| 328 | + | pv.toaster = t | |||
| 329 | + | return t | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | // ShowToast displays a toast notification with the given message and level. | |||
| 333 | + | // If a toast is already visible, updates the text and resets the dismiss timer. | |||
| 334 | + | func (pv *PaneView) ShowToast(ctx context.Context, message string, level ToastLevel) { | |||
| 335 | + | pv.mu.Lock() | |||
| 336 | + | t := pv.ensureToaster() | |||
| 337 | + | pv.mu.Unlock() | |||
| 338 | + | ||||
| 339 | + | t.Show(ctx, message, level) | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | // ShowZoomToast displays a zoom level toast notification. | |||
| 343 | + | // Formats the zoom percentage with a % suffix. | |||
| 344 | + | func (pv *PaneView) ShowZoomToast(ctx context.Context, zoomPercent int) { | |||
| 345 | + | pv.mu.Lock() | |||
| 346 | + | t := pv.ensureToaster() | |||
| 347 | + | pv.mu.Unlock() | |||
| 348 | + | ||||
| 349 | + | t.ShowZoom(ctx, zoomPercent) | |||
| 350 | + | } | |||
M internal/ui/component/progress_bar.go +9 -1
| @@ -47,6 +47,10 @@ func NewProgressBar(factory layout.WidgetFactory) *ProgressBar { | |||
| 47 | 47 | progressBar.SetCanTarget(false) | |
| 48 | 48 | progressBar.SetCanFocus(false) | |
| 49 | 49 | ||
| 50 | + | // Initialize fraction to 0 to force proper sizing of internal progress gizmo | |
| 51 | + | // This prevents GTK warning about negative minimum width | |
| 52 | + | progressBar.SetFraction(0) | |
| 53 | + | ||
| 50 | 54 | // Hidden by default | |
| 51 | 55 | progressBar.SetVisible(false) | |
| 52 | 56 | ||
| @@ -110,7 +114,11 @@ func (pb *ProgressBar) startAnimation() { | |||
| 110 | 114 | pb.progressBar.SetFraction(pb.currentValue) | |
| 111 | 115 | ||
| 112 | 116 | // Continue animation if not at target | |
| 113 | - | return pb.currentValue < pb.targetValue | |
| 117 | + | if pb.currentValue >= pb.targetValue { | |
| 118 | + | pb.animationTimer = 0 // Clear before GLib auto-removes | |
| 119 | + | return false | |
| 120 | + | } | |
| 121 | + | return true | |
| 114 | 122 | } | |
| 115 | 123 | ||
| 116 | 124 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | |
| @@ -47,6 +47,10 @@ func NewProgressBar(factory layout.WidgetFactory) *ProgressBar { | |||||
| 47 | progressBar.SetCanTarget(false) | 47 | progressBar.SetCanTarget(false) | ||
| 48 | progressBar.SetCanFocus(false) | 48 | progressBar.SetCanFocus(false) | ||
| 49 | 49 | ||||
| 50 | + | // Initialize fraction to 0 to force proper sizing of internal progress gizmo | |||
| 51 | + | // This prevents GTK warning about negative minimum width | |||
| 52 | + | progressBar.SetFraction(0) | |||
| 53 | + | ||||
| 50 | // Hidden by default | 54 | // Hidden by default | ||
| 51 | progressBar.SetVisible(false) | 55 | progressBar.SetVisible(false) | ||
| 52 | 56 | ||||
| @@ -110,7 +114,11 @@ func (pb *ProgressBar) startAnimation() { | |||||
| 110 | pb.progressBar.SetFraction(pb.currentValue) | 114 | pb.progressBar.SetFraction(pb.currentValue) | ||
| 111 | 115 | ||||
| 112 | // Continue animation if not at target | 116 | // Continue animation if not at target | ||
| 113 | - | return pb.currentValue < pb.targetValue | 117 | + | if pb.currentValue >= pb.targetValue { |
| 118 | + | pb.animationTimer = 0 // Clear before GLib auto-removes | |||
| 119 | + | return false | |||
| 120 | + | } | |||
| 121 | + | return true | |||
| 114 | } | 122 | } | ||
| 115 | 123 | ||||
| 116 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | 124 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | ||