From 18913780bf35b1751d85c6aae329cd8c82aa6cba Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 5 Jan 2025 11:01:07 -0800 Subject: [PATCH] toast dismissal, hook up to playlist workflow (success case) --- ui/controller/controller.go | 35 ++++++++++++--- ui/mainwindow.go | 9 +--- ui/toastoverlay.go | 90 +++++++++++++++++++++++++++++++------ 3 files changed, 107 insertions(+), 27 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 8909621..fff6668 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -39,17 +39,24 @@ type NavigationHandler func(Route) type CurPageFunc func() Route +type ToastProvider interface { + ShowSuccessToast(string) +} + type Controller struct { visualizationData - AppVersion string - App *backend.App - MainWindow fyne.Window + AppVersion string + App *backend.App + MainWindow fyne.Window + + // dependencies injected from MainWindow NavHandler NavigationHandler CurPageFunc CurPageFunc ReloadFunc func() RefreshPageFunc func() SelectAllPageFunc func() UnselectAllPageFunc func() + ToastProvider ToastProvider popUpQueueMutex sync.Mutex popUpQueue *widget.PopUp @@ -300,7 +307,13 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { pop.Hide() m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates if id == "" /* creating new playlist */ { - go m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs) + go func() { + err := m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs) + if err == nil { + // TODO: translate, adjust by plurality + m.ToastProvider.ShowSuccessToast(fmt.Sprintf("Added %d tracks to playlist", len(trackIDs))) + } + }() } else { m.App.Config.Application.DefaultPlaylistID = id if sp.SkipDuplicates { @@ -316,11 +329,21 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { _, ok := currentTrackIDs[trackID] return !ok }) - m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs) + err := m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs) + if err == nil { + // TODO: translate, adjust by plurality + m.ToastProvider.ShowSuccessToast(fmt.Sprintf("Added %d tracks to playlist", len(filterTrackIDs))) + } } }() } else { - go m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) + go func() { + err := m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) + if err == nil { + // TODO: translate, adjust by plurality + m.ToastProvider.ShowSuccessToast(fmt.Sprintf("Added %d tracks to playlist", len(trackIDs))) + } + }() } } diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 43c9362..c8f1f89 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -62,6 +62,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, } m.Controller = controller.New(app, appVersion, m.Window) m.BrowsingPane = browsing.NewBrowsingPane(app, m.Controller, func() { m.Router.NavigateTo(m.StartupPage()) }) + m.ToastOverlay = NewToastOverlay() m.Router = browsing.NewRouter(app, m.Controller, m.BrowsingPane) // inject controller dependencies m.Controller.NavHandler = m.Router.NavigateTo @@ -70,6 +71,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, m.Controller.RefreshPageFunc = m.BrowsingPane.RefreshPage m.Controller.SelectAllPageFunc = m.BrowsingPane.SelectAll m.Controller.UnselectAllPageFunc = m.BrowsingPane.UnselectAll + m.Controller.ToastProvider = m.ToastOverlay if runtime.GOOS == "darwin" { // Fyne will extract out an "About" menu item and @@ -138,19 +140,12 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, m.BrowsingPane.DisableNavigationButtons() m.addShortcuts() - m.ToastOverlay = NewToastOverlay() center := container.NewStack(m.BrowsingPane, m.ToastOverlay) m.content = newMainWindowContent(container.NewBorder(nil, m.BottomPanel, nil, nil, center), m.Controller.UnselectAll) m.Window.SetContent(fynetooltip.AddWindowToolTipLayer(m.content, m.Window.Canvas())) m.setInitialSize() - // TODO: Remove me!! - go func() { - time.Sleep(2 * time.Second) - m.ToastOverlay.ShowSuccessToast("Added 20 songs to playlist.") - }() - m.Window.SetCloseIntercept(func() { m.SaveWindowSize() // save settings in case we crash during shutdown diff --git a/ui/toastoverlay.go b/ui/toastoverlay.go index c8ec936..9a1e3c1 100644 --- a/ui/toastoverlay.go +++ b/ui/toastoverlay.go @@ -1,6 +1,7 @@ package ui import ( + "context" "time" "fyne.io/fyne/v2" @@ -11,6 +12,8 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/ui/util" + "github.com/dweymouth/supersonic/ui/widgets" ) type ToastOverlay struct { @@ -18,6 +21,7 @@ type ToastOverlay struct { currentToast *toast currentToastAnim *fyne.Animation + dismissCancel context.CancelFunc container *fyne.Container } @@ -32,7 +36,7 @@ func NewToastOverlay() *ToastOverlay { func (t *ToastOverlay) ShowSuccessToast(message string) { t.cancelPreviousToast() - t.currentToast = newToast(false, message) + t.currentToast = newToast(false, message, t.dismissToast) t.container.Objects = append(t.container.Objects, t.currentToast) s := t.Size() @@ -41,17 +45,38 @@ func (t *ToastOverlay) ShowSuccessToast(message string) { t.currentToast.Resize(min) endPos := fyne.NewPos(s.Width-min.Width-pad, s.Height-min.Height-pad) startPos := fyne.NewPos(s.Width, endPos.Y) - t.currentToastAnim = canvas.NewPositionAnimation(startPos, endPos, 100*time.Millisecond, func(p fyne.Position) { + f := t.makeToastAnimFunc(endPos, false) + t.currentToastAnim = canvas.NewPositionAnimation(startPos, endPos, 100*time.Millisecond, f) + t.currentToastAnim.Curve = fyne.AnimationEaseOut + t.currentToastAnim.Start() + t.Refresh() + + ctx, cancel := context.WithCancel(context.Background()) + t.dismissCancel = cancel // always canceled by dismissToast + go func() { + time.Sleep(2 * time.Second) + select { + case <-ctx.Done(): + return + default: + t.dismissToast() + } + }() +} + +func (t *ToastOverlay) makeToastAnimFunc(endPos fyne.Position, dismissal bool) func(fyne.Position) { + return func(p fyne.Position) { if ct := t.currentToast; ct != nil { ct.Move(p) } if p == endPos { t.currentToastAnim = nil + if dismissal { + t.cancelPreviousToast() + } } - }) - t.currentToastAnim.Curve = fyne.AnimationEaseOut - t.currentToastAnim.Start() - t.Refresh() + + } } func (t *ToastOverlay) Resize(size fyne.Size) { @@ -73,10 +98,33 @@ func (t *ToastOverlay) cancelPreviousToast() { t.currentToastAnim.Stop() t.currentToastAnim = nil } + t.container.Objects[0] = nil t.container.Objects = t.container.Objects[:0] t.currentToast = nil } +func (t *ToastOverlay) dismissToast() { + if t.currentToast == nil { + return + } + if t.dismissCancel != nil { + t.dismissCancel() + t.dismissCancel = nil + } + if t.currentToastAnim != nil { + t.currentToastAnim.Stop() + } + s := t.Size() + min := t.currentToast.MinSize() + pad := theme.Padding() + startPos := fyne.NewPos(s.Width-min.Width-pad, s.Height-min.Height-pad) + endPos := fyne.NewPos(s.Width, startPos.Y) + f := t.makeToastAnimFunc(endPos, true) + t.currentToastAnim = canvas.NewPositionAnimation(startPos, endPos, 100*time.Millisecond, f) + t.currentToastAnim.Curve = fyne.AnimationEaseIn + t.currentToastAnim.Start() +} + func (t *ToastOverlay) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(t.container) } @@ -84,12 +132,13 @@ func (t *ToastOverlay) CreateRenderer() fyne.WidgetRenderer { type toast struct { widget.BaseWidget - isErr bool - message string + isErr bool + message string + onDismiss func() } -func newToast(isErr bool, message string) *toast { - t := &toast{isErr: isErr, message: message} +func newToast(isErr bool, message string, onDismiss func()) *toast { + t := &toast{isErr: isErr, message: message, onDismiss: onDismiss} t.ExtendBaseWidget(t) return t } @@ -98,6 +147,12 @@ func (t *toast) CreateRenderer() fyne.WidgetRenderer { return newToastRenderer(t) } +func (t *toast) Dismiss() { + if t.onDismiss != nil { + t.onDismiss() + } +} + // swallow all tap/mouse events because toast is transparent var ( _ fyne.Tappable = (*toast)(nil) @@ -138,6 +193,12 @@ func newToastRenderer(t *toast) *toastRenderer { accent := canvas.NewRectangle(th.Color(accentColor, v)) accent.SetMinSize(fyne.NewSize(4, 1)) + close := widgets.NewIconButton(theme.CancelIcon(), t.Dismiss) + close.IconSize = widgets.IconButtonSizeSmaller + + titleText := widget.NewRichTextWithText(title) + titleText.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyleSubHeading + pad := theme.Padding() return &toastRenderer{ background: background, @@ -152,10 +213,11 @@ func newToastRenderer(t *toast) *toastRenderer { RightPadding: pad, }, container.NewBorder(nil, nil, accent, nil, - widget.NewRichText( - &widget.TextSegment{Text: title, Style: widget.RichTextStyleSubHeading}, - &widget.TextSegment{Text: t.message}, - )), + container.NewVBox( + container.NewBorder(nil, nil, nil, container.NewHBox(close, util.NewHSpace(2)), titleText), + widget.NewLabel(t.message), + ), + ), ), ), }