From a7ed3eed8f5133ab2abcfc5ce86aacb03549ebba Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 5 Jan 2025 10:08:42 -0800 Subject: [PATCH 1/5] initial work on toast messages --- ui/mainwindow.go | 12 ++- ui/toastoverlay.go | 194 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 ui/toastoverlay.go diff --git a/ui/mainwindow.go b/ui/mainwindow.go index ad852c7c..43c93620 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -34,6 +34,7 @@ type MainWindow struct { Controller *controller.Controller BrowsingPane *browsing.BrowsingPane BottomPanel *BottomPanel + ToastOverlay *ToastOverlay theme *theme.MyTheme haveSystemTray bool @@ -137,10 +138,19 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, m.BrowsingPane.DisableNavigationButtons() m.addShortcuts() - m.content = newMainWindowContent(container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane), + 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 new file mode 100644 index 00000000..c8ec936d --- /dev/null +++ b/ui/toastoverlay.go @@ -0,0 +1,194 @@ +package ui + +import ( + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type ToastOverlay struct { + widget.BaseWidget + + currentToast *toast + currentToastAnim *fyne.Animation + + container *fyne.Container +} + +func NewToastOverlay() *ToastOverlay { + t := &ToastOverlay{container: container.NewWithoutLayout()} + t.container.Objects = make([]fyne.CanvasObject, 0, 1) + t.ExtendBaseWidget(t) + return t +} + +func (t *ToastOverlay) ShowSuccessToast(message string) { + t.cancelPreviousToast() + + t.currentToast = newToast(false, message) + t.container.Objects = append(t.container.Objects, t.currentToast) + + s := t.Size() + min := t.currentToast.MinSize() + pad := theme.Padding() + 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) { + if ct := t.currentToast; ct != nil { + ct.Move(p) + } + if p == endPos { + t.currentToastAnim = nil + } + }) + t.currentToastAnim.Curve = fyne.AnimationEaseOut + t.currentToastAnim.Start() + t.Refresh() +} + +func (t *ToastOverlay) Resize(size fyne.Size) { + if t.currentToast != nil && t.currentToastAnim == nil { + // move to the anchor position + min := t.currentToast.MinSize() + pad := theme.Padding() + t.currentToast.Move(fyne.NewPos(size.Width-min.Width-pad, size.Height-min.Height-pad)) + } // else if animation is running -- well, hope this doesn't happen ;) + + t.BaseWidget.Resize(size) +} + +func (t *ToastOverlay) cancelPreviousToast() { + if t.currentToast == nil { + return + } + if t.currentToastAnim != nil { + t.currentToastAnim.Stop() + t.currentToastAnim = nil + } + t.container.Objects = t.container.Objects[:0] + t.currentToast = nil +} + +func (t *ToastOverlay) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(t.container) +} + +type toast struct { + widget.BaseWidget + + isErr bool + message string +} + +func newToast(isErr bool, message string) *toast { + t := &toast{isErr: isErr, message: message} + t.ExtendBaseWidget(t) + return t +} + +func (t *toast) CreateRenderer() fyne.WidgetRenderer { + return newToastRenderer(t) +} + +// swallow all tap/mouse events because toast is transparent +var ( + _ fyne.Tappable = (*toast)(nil) + _ fyne.SecondaryTappable = (*toast)(nil) + _ desktop.Hoverable = (*toast)(nil) + _ desktop.Mouseable = (*toast)(nil) +) + +func (*toast) Tapped(*fyne.PointEvent) {} +func (*toast) TappedSecondary(*fyne.PointEvent) {} +func (*toast) MouseIn(*desktop.MouseEvent) {} +func (*toast) MouseOut() {} +func (*toast) MouseMoved(*desktop.MouseEvent) {} +func (*toast) MouseUp(*desktop.MouseEvent) {} +func (*toast) MouseDown(*desktop.MouseEvent) {} + +type toastRenderer struct { + container *fyne.Container + background *canvas.Rectangle + accent *canvas.Rectangle + accentColor fyne.ThemeColorName +} + +func newToastRenderer(t *toast) *toastRenderer { + title := lang.L("Success") + accentColor := theme.ColorNamePrimary + if t.isErr { + title = lang.L("Error") + accentColor = theme.ColorNameError + } + + th := fyne.CurrentApp().Settings().Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + background := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v)) + background.CornerRadius = th.Size(theme.SizeNameInputRadius) + background.StrokeColor = th.Color(theme.ColorNameInputBorder, v) + background.StrokeWidth = th.Size(theme.SizeNameInputBorder) * 2 + accent := canvas.NewRectangle(th.Color(accentColor, v)) + accent.SetMinSize(fyne.NewSize(4, 1)) + + pad := theme.Padding() + return &toastRenderer{ + background: background, + accent: accent, + accentColor: accentColor, + container: container.NewStack( + background, + container.New(&layout.CustomPaddedLayout{ + TopPadding: 2 * pad, + BottomPadding: 2 * pad, + LeftPadding: 2 * pad, + RightPadding: pad, + }, + container.NewBorder(nil, nil, accent, nil, + widget.NewRichText( + &widget.TextSegment{Text: title, Style: widget.RichTextStyleSubHeading}, + &widget.TextSegment{Text: t.message}, + )), + ), + ), + } +} + +var _ fyne.WidgetRenderer = (*toastRenderer)(nil) + +func (*toastRenderer) Destroy() {} + +func (t *toastRenderer) Layout(s fyne.Size) { + t.container.Layout.Layout(t.container.Objects, s) +} + +func (t *toastRenderer) MinSize() fyne.Size { + return t.container.MinSize() +} + +func (t *toastRenderer) Objects() []fyne.CanvasObject { + return t.container.Objects +} + +func (t *toastRenderer) Refresh() { + th := fyne.CurrentApp().Settings().Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + t.background.FillColor = th.Color(theme.ColorNameOverlayBackground, v) + t.background.CornerRadius = th.Size(theme.SizeNameInputRadius) + t.background.StrokeColor = th.Color(theme.ColorNameInputBorder, v) + t.background.StrokeWidth = th.Size(theme.SizeNameInputBorder) * 2 + t.background.Refresh() + + t.accent.FillColor = th.Color(t.accentColor, v) + t.accent.Refresh() + + canvas.Refresh(t.container) +} From 18913780bf35b1751d85c6aae329cd8c82aa6cba Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 5 Jan 2025 11:01:07 -0800 Subject: [PATCH 2/5] 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 89096217..fff66689 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 43c93620..c8f1f89a 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 c8ec936d..9a1e3c18 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), + ), + ), ), ), } From dd92d64143ca84eeb78eb59cf19ba72fea174866 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 5 Jan 2025 13:31:45 -0800 Subject: [PATCH 3/5] add translations for playlist add toast --- res/translations/de.json | 7 ++++++- res/translations/en.json | 7 ++++++- res/translations/es.json | 7 ++++++- res/translations/fr.json | 7 ++++++- res/translations/it.json | 7 ++++++- res/translations/ja.json | 7 ++++++- res/translations/pl.json | 7 ++++++- res/translations/pt_BR.json | 7 ++++++- res/translations/ro.json | 7 ++++++- res/translations/zh.json | 7 ++++++- res/translations/zhHans.json | 7 ++++++- ui/controller/controller.go | 15 +++++++++------ ui/toastoverlay.go | 11 ++++++++--- 13 files changed, 83 insertions(+), 20 deletions(-) diff --git a/res/translations/de.json b/res/translations/de.json index 6c9c9ed2..09691c8b 100644 --- a/res/translations/de.json +++ b/res/translations/de.json @@ -239,5 +239,10 @@ "Sept": "Sept", "Oct": "Okt", "Nov": "Nov", - "Dec": "Dec" + "Dec": "Dec", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/en.json b/res/translations/en.json index b84bb009..b793e6e5 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -242,5 +242,10 @@ "Sept": "Sept", "Oct": "Oct", "Nov": "Nov", - "Dec": "Dec" + "Dec": "Dec", + + "playlist.addedtracks": { + "one": "Added one track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/es.json b/res/translations/es.json index 7830ff53..84589792 100644 --- a/res/translations/es.json +++ b/res/translations/es.json @@ -241,5 +241,10 @@ "Sept": "Set", "Oct": "Oct", "Nov": "Nov", - "Dec": "Dic" + "Dec": "Dic", + + "playlist.addedtracks": { + "one": "Se ha añadido una pista a la lista de reproducción", + "other": "Se han añadido {{.trackCount}} pistas a la lista de reproducción" + } } diff --git a/res/translations/fr.json b/res/translations/fr.json index e5729444..d886c167 100644 --- a/res/translations/fr.json +++ b/res/translations/fr.json @@ -240,5 +240,10 @@ "Sept": "Sept", "Oct": "Oct", "Nov": "Nov", - "Dec": "Déc" + "Dec": "Déc", + + "playlist.addedtracks": { + "one": "Une piste ajoutée à la liste de lecture", + "other": "{{.trackCount}} pistes ajoutées à la liste de lecture" + } } diff --git a/res/translations/it.json b/res/translations/it.json index 2d657f1a..a7023719 100644 --- a/res/translations/it.json +++ b/res/translations/it.json @@ -238,5 +238,10 @@ "Sept": "Set", "Oct": "Ott", "Nov": "Nov", - "Dec": "Dic" + "Dec": "Dic", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/ja.json b/res/translations/ja.json index 2898c57a..6d7eb543 100644 --- a/res/translations/ja.json +++ b/res/translations/ja.json @@ -228,5 +228,10 @@ "Remove from queue": "キューから削除", "Play song radio": "曲をラジオで再生", "to":"to", - "by":"by" + "by":"by", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/pl.json b/res/translations/pl.json index a920e603..edf39cc7 100644 --- a/res/translations/pl.json +++ b/res/translations/pl.json @@ -228,5 +228,10 @@ "Remove from queue": "Usuń z kolejki odtwarzania", "Play song radio": "Odtwórz mix utworów", "to":"do", - "by":"przez" + "by":"przez", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/pt_BR.json b/res/translations/pt_BR.json index 6555288d..4d63bbbe 100644 --- a/res/translations/pt_BR.json +++ b/res/translations/pt_BR.json @@ -228,5 +228,10 @@ "Remove from queue": "Remover da fila", "Play song radio": "Reproduzir rádio da faixa", "to":"a", - "by":"de" + "by":"de", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/ro.json b/res/translations/ro.json index cd0b9d79..dc075662 100644 --- a/res/translations/ro.json +++ b/res/translations/ro.json @@ -230,5 +230,10 @@ "Sept": "Sept", "Oct": "Oct", "Nov": "Noiem", - "Dec": "Dec" + "Dec": "Dec", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/zh.json b/res/translations/zh.json index a71f69b3..7102cff5 100644 --- a/res/translations/zh.json +++ b/res/translations/zh.json @@ -216,5 +216,10 @@ "Unset favorite": "取消收藏", "Remove from queue": "从队列中移除", "Size": "大小", - "Play song radio": "播放歌曲电台" + "Play song radio": "播放歌曲电台", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/res/translations/zhHans.json b/res/translations/zhHans.json index cb3e9b02..727b0f64 100644 --- a/res/translations/zhHans.json +++ b/res/translations/zhHans.json @@ -216,5 +216,10 @@ "Unset favorite": "取消收藏", "Remove from queue": "从队列中移除", "Size": "大小", - "Play song radio": "播放歌曲电台" + "Play song radio": "播放歌曲电台", + + "playlist.addedtracks": { + "one": "Added {{.trackCount}} track to playlist", + "other": "Added {{.trackCount}} tracks to playlist" + } } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index fff66689..f67b9a98 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "sync" "time" @@ -304,14 +305,18 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { m.doModalClosed() }) sp.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) { + notifySuccess := func(n int) { + msg := lang.LocalizePluralKey("playlist.addedtracks", + "Added tracks to playlist", n, map[string]string{"trackCount": strconv.Itoa(n)}) + m.ToastProvider.ShowSuccessToast(msg) + } pop.Hide() m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates if id == "" /* creating new playlist */ { 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))) + notifySuccess(len(trackIDs)) } }() } else { @@ -331,8 +336,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { }) 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))) + notifySuccess(len(filterTrackIDs)) } } }() @@ -340,8 +344,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { 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))) + notifySuccess(len(trackIDs)) } }() } diff --git a/ui/toastoverlay.go b/ui/toastoverlay.go index 9a1e3c18..e78b3ec3 100644 --- a/ui/toastoverlay.go +++ b/ui/toastoverlay.go @@ -16,6 +16,11 @@ import ( "github.com/dweymouth/supersonic/ui/widgets" ) +const ( + toastAutoDismissalTime = 3 * time.Second + toastAnimationDuration = 100 * time.Millisecond +) + type ToastOverlay struct { widget.BaseWidget @@ -46,7 +51,7 @@ func (t *ToastOverlay) ShowSuccessToast(message string) { endPos := fyne.NewPos(s.Width-min.Width-pad, s.Height-min.Height-pad) startPos := fyne.NewPos(s.Width, endPos.Y) f := t.makeToastAnimFunc(endPos, false) - t.currentToastAnim = canvas.NewPositionAnimation(startPos, endPos, 100*time.Millisecond, f) + t.currentToastAnim = canvas.NewPositionAnimation(startPos, endPos, toastAnimationDuration, f) t.currentToastAnim.Curve = fyne.AnimationEaseOut t.currentToastAnim.Start() t.Refresh() @@ -54,7 +59,7 @@ func (t *ToastOverlay) ShowSuccessToast(message string) { ctx, cancel := context.WithCancel(context.Background()) t.dismissCancel = cancel // always canceled by dismissToast go func() { - time.Sleep(2 * time.Second) + time.Sleep(toastAutoDismissalTime) select { case <-ctx.Done(): return @@ -120,7 +125,7 @@ func (t *ToastOverlay) dismissToast() { 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 = canvas.NewPositionAnimation(startPos, endPos, toastAnimationDuration, f) t.currentToastAnim.Curve = fyne.AnimationEaseIn t.currentToastAnim.Start() } From 24364c590d7df00da2600243618dc29aad59459f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 5 Jan 2025 13:39:20 -0800 Subject: [PATCH 4/5] add "Success" and "Error" to en and es translations --- res/translations/en.json | 2 ++ res/translations/es.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/res/translations/en.json b/res/translations/en.json index b793e6e5..d2fdfa8b 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -69,6 +69,7 @@ "Enter": "Enter", "EP": "EP", "Equalizer": "Equalizer", + "Error": "Error", "Exclusive mode": "Exclusive mode", "Favorites": "Favorites", "Field Recording": "Field Recording", @@ -188,6 +189,7 @@ "Spoken Word": "Spoken Word", "Startup page": "Startup page", "Stopped": "Stopped", + "Success": "Success", "Support the project": "Support the project", "Switch Servers": "Switch Servers", "Testing connection": "Testing connection", diff --git a/res/translations/es.json b/res/translations/es.json index 84589792..bdc3d877 100644 --- a/res/translations/es.json +++ b/res/translations/es.json @@ -69,6 +69,7 @@ "Enter": "Ingresar", "EP": "EP", "Equalizer": "Ecualizador", + "Error": "Error", "Exclusive mode": "Modo exclusivo", "Favorites": "Favoritos", "Field Recording": "Grabación en campo", @@ -187,6 +188,7 @@ "Spoken Word": "Palabra Hablada", "Startup page": "Página de inicio", "Stopped": "Detenida", + "Success": "Éxito", "Support the project": "Apoya el proyecto", "Switch Servers": "Cambiar servidores", "Testing connection": "Probando conexión", From 7c657accc11da3843bab84c7a9f6e5ab6ca15abf Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 5 Jan 2025 16:38:36 -0800 Subject: [PATCH 5/5] show error toast when failing to add to playlist --- res/translations/en.json | 1 + res/translations/es.json | 1 + ui/controller/controller.go | 16 ++++++++++++++++ ui/toastoverlay.go | 10 +++++++++- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/res/translations/en.json b/res/translations/en.json index d2fdfa8b..bf7b4380 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -17,6 +17,7 @@ "All": "All", "All Tracks": "All Tracks", "Alt. URL": "Alt. URL", + "An error occurred adding tracks to the playlist": "An error occurred adding tracks to the playlist", "Are you sure you want to delete the server": "Are you sure you want to delete the server", "Artist": "Artist", "Artist (A-Z)": "Artist (A-Z)", diff --git a/res/translations/es.json b/res/translations/es.json index bdc3d877..2a2bf926 100644 --- a/res/translations/es.json +++ b/res/translations/es.json @@ -17,6 +17,7 @@ "All": "Todos", "All Tracks": "Todas las pistas", "Alt. URL": "URL alternativa", + "An error occurred adding tracks to the playlist": "Ha ocurrido un error al añadir pistas a la lista de reproducción", "Are you sure you want to delete the server": "¿Estás seguro de que quieres eliminar el servidor?", "Artist": "Artista", "Artist (A-Z)": "Artista (A-Z)", diff --git a/ui/controller/controller.go b/ui/controller/controller.go index f67b9a98..9b1bee71 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -42,6 +42,7 @@ type CurPageFunc func() Route type ToastProvider interface { ShowSuccessToast(string) + ShowErrorToast(string) } type Controller struct { @@ -310,6 +311,11 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { "Added tracks to playlist", n, map[string]string{"trackCount": strconv.Itoa(n)}) m.ToastProvider.ShowSuccessToast(msg) } + notifyError := func() { + m.ToastProvider.ShowErrorToast( + lang.L("An error occurred adding tracks to the playlist"), + ) + } pop.Hide() m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates if id == "" /* creating new playlist */ { @@ -317,6 +323,9 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { err := m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs) if err == nil { notifySuccess(len(trackIDs)) + } else { + log.Println("error adding tracks to playlist: %s", err.Error()) + notifyError() } }() } else { @@ -326,6 +335,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { currentTrackIDs := make(map[string]struct{}) if selectedPlaylist, err := m.App.ServerManager.Server.GetPlaylist(id); err != nil { log.Printf("error getting playlist: %s", err.Error()) + notifyError() } else { for _, track := range selectedPlaylist.Tracks { currentTrackIDs[track.ID] = struct{}{} @@ -337,6 +347,9 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { err := m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs) if err == nil { notifySuccess(len(filterTrackIDs)) + } else { + log.Println("error adding tracks to playlist: %s", err.Error()) + notifyError() } } }() @@ -345,6 +358,9 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { err := m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) if err == nil { notifySuccess(len(trackIDs)) + } else { + log.Println("error adding tracks to playlist: %s", err.Error()) + notifyError() } }() } diff --git a/ui/toastoverlay.go b/ui/toastoverlay.go index e78b3ec3..d4801901 100644 --- a/ui/toastoverlay.go +++ b/ui/toastoverlay.go @@ -39,9 +39,17 @@ func NewToastOverlay() *ToastOverlay { } func (t *ToastOverlay) ShowSuccessToast(message string) { + t.showToast(false, message) +} + +func (t *ToastOverlay) ShowErrorToast(message string) { + t.showToast(true, message) +} + +func (t *ToastOverlay) showToast(isErr bool, message string) { t.cancelPreviousToast() - t.currentToast = newToast(false, message, t.dismissToast) + t.currentToast = newToast(isErr, message, t.dismissToast) t.container.Objects = append(t.container.Objects, t.currentToast) s := t.Size()