detached f7dbba0

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 {
5151 RemoveFromWhitelist(ctx context.Context, domain string) error
5252 RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error
5353 SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error)
54+ SearchHistoryFTS(ctx context.Context, arg SearchHistoryFTSParams) ([]History, error)
5455 SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error
5556 SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error
5657 SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error
@@ -51,6 +51,7 @@ type Querier interface {
51 RemoveFromWhitelist(ctx context.Context, domain string) error51 RemoveFromWhitelist(ctx context.Context, domain string) error
52 RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error52 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) error55 SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error
55 SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error56 SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error
56 SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error57 SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error
M internal/ui/component/pane_view.go +67 -21
@@ -23,6 +23,7 @@ type PaneView struct {
2323 webViewWidget layout.Widget // The actual WebView widget
2424 borderBox layout.BoxWidget // Border overlay for active indication
2525 progressBar *ProgressBar // Loading progress indicator
26+ toaster *Toaster // Toast notification overlay
2627 paneID entity.PaneID
2728 isActive bool
2829
@@ -63,18 +64,15 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg
6364 overlay.SetClipOverlay(borderBox, false)
6465 overlay.SetMeasureOverlay(borderBox, false)
6566
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
7169
7270 return &PaneView{
7371 factory: factory,
7472 overlay: overlay,
7573 webViewWidget: webViewWidget,
7674 borderBox: borderBox,
77- progressBar: progressBar,
75+ progressBar: nil, // Created lazily in ensureProgressBar()
7876 paneID: paneID,
7977 isActive: false,
8078 }
@@ -276,29 +274,77 @@ func (pv *PaneView) GetContentDimensions() (width, height int) {
276274 return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight()
277275 }
278276
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+
279294 // SetLoadProgress updates the progress bar with the current load progress.
280295 // progress should be between 0.0 and 1.0.
281296 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()
285300
286- if pb != nil {
287- pb.SetProgress(progress)
288- }
301+ pb.SetProgress(progress)
289302 }
290303
291304 // SetLoading shows or hides the progress bar.
292305 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()
296309
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()
303314 }
304315 }
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 widget23 webViewWidget layout.Widget // The actual WebView widget
24 borderBox layout.BoxWidget // Border overlay for active indication24 borderBox layout.BoxWidget // Border overlay for active indication
25 progressBar *ProgressBar // Loading progress indicator25 progressBar *ProgressBar // Loading progress indicator
26+ toaster *Toaster // Toast notification overlay
26 paneID entity.PaneID27 paneID entity.PaneID
27 isActive bool28 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 bottom67+ // 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.progressBar298+ 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.progressBar307+ 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 {
4747 progressBar.SetCanTarget(false)
4848 progressBar.SetCanFocus(false)
4949
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+
5054 // Hidden by default
5155 progressBar.SetVisible(false)
5256
@@ -110,7 +114,11 @@ func (pb *ProgressBar) startAnimation() {
110114 pb.progressBar.SetFraction(pb.currentValue)
111115
112116 // 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
114122 }
115123
116124 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 default54 // 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 target116 // Continue animation if not at target
113- return pb.currentValue < pb.targetValue117+ 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)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close