migrate to Fyne 2.6 - WIP - updates for new threading model

This commit is contained in:
Drew Weymouth
2025-02-08 08:34:39 -03:00
parent 3dcf8aa9d7
commit c60727f92a
13 changed files with 262 additions and 195 deletions
+4 -2
View File
@@ -316,8 +316,10 @@ func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backe
go func() {
if cover, err := im.GetCoverThumbnail(album.CoverArtID); err == nil {
a.cover.SetImage(cover, true)
a.cover.Refresh()
fyne.Do(func() {
a.cover.SetImage(cover, true)
a.cover.Refresh()
})
} else {
log.Printf("error fetching cover: %v", err)
}
+61 -40
View File
@@ -174,8 +174,10 @@ func (a *ArtistPage) playArtistRadio() {
go func() {
err := a.pm.PlaySimilarSongs(a.artistID)
if err != nil {
log.Println("error playing similar songs: %v", err)
a.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play artist radio"))
fyne.Do(func() {
log.Println("error playing similar songs: %v", err)
a.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play artist radio"))
})
}
}()
}
@@ -246,19 +248,23 @@ func (a *ArtistPage) load() {
if a.disposed {
return
}
a.artistInfo = artist
a.header.Update(artist, a.im)
if a.activeView == 0 {
a.showAlbumGrid(false /*reSort*/)
} else {
a.showTopTracks()
}
fyne.Do(func() {
a.artistInfo = artist
a.header.Update(artist, a.im)
if a.activeView == 0 {
a.showAlbumGrid(false /*reSort*/)
} else {
a.showTopTracks()
}
})
info, err := a.mp.GetArtistInfo(a.artistID)
if err != nil {
log.Printf("Failed to get artist info: %s", err.Error())
}
if !a.disposed {
a.header.UpdateInfo(info)
fyne.Do(func() { a.header.UpdateInfo(info) })
}
}
@@ -287,7 +293,15 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) {
a.container.Objects[0].Refresh()
}
// should be called asynchronously
func (a *ArtistPage) showTopTracks() {
updated := false
updatePage := func() {
a.sortButton.Hide()
a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr
a.container.Objects[0].Refresh()
}
if a.tracklistCtr == nil {
if a.artistInfo == nil {
// page not loaded yet or invalid artist
@@ -302,33 +316,38 @@ func (a *ArtistPage) showTopTracks() {
if a.disposed {
return
}
var tl *widgets.Tracklist
if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil {
tl = t.(*widgets.Tracklist)
tl.Reset()
tl.SetTracks(ts)
} else {
tl = widgets.NewTracklist(ts, a.im, false)
}
tl.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tl.Options.DisableRating = !canRate
tl.Options.DisableSharing = !canShare
tl.SetVisibleColumns(a.cfg.TracklistColumns)
tl.SetSorting(a.trackSort)
tl.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols
}
tl.SetNowPlaying(a.nowPlayingID)
a.contr.ConnectTracklistActions(tl)
a.tracklistCtr = container.New(
&layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10},
tl)
updated = true // mark that updatePage() will be called here
fyne.Do(func() {
var tl *widgets.Tracklist
if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil {
tl = t.(*widgets.Tracklist)
tl.Reset()
tl.SetTracks(ts)
} else {
tl = widgets.NewTracklist(ts, a.im, false)
}
tl.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tl.Options.DisableRating = !canRate
tl.Options.DisableSharing = !canShare
tl.SetVisibleColumns(a.cfg.TracklistColumns)
tl.SetSorting(a.trackSort)
tl.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols
}
tl.SetNowPlaying(a.nowPlayingID)
a.contr.ConnectTracklistActions(tl)
a.tracklistCtr = container.New(
&layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10},
tl)
updatePage()
})
}
if !updated {
fyne.Do(updatePage)
}
a.sortButton.Hide()
a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr
a.container.Objects[0].Refresh()
}
func (a *ArtistPage) onViewChange(num int) {
@@ -512,10 +531,12 @@ func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) {
if a.artistImage.HaveImage() {
_ = a.artistPage.im.RefreshCachedArtistImageIfExpired(a.artistID, info.ImageURL)
} else {
im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.ImageURL)
if err == nil {
a.artistImage.SetImage(im, true /*tappable*/)
}
go func() {
im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.ImageURL)
if err == nil {
fyne.Do(func() { a.artistImage.SetImage(im, true /*tappable*/) })
}
}()
}
}
}
+63 -57
View File
@@ -207,22 +207,24 @@ func (a *FavoritesPage) Reload() {
if a.disposed {
return
}
if tr := a.tracklistOrNil(); tr != nil {
// refresh favorite songs view
tr.SetTracks(starred.Tracks)
if a.toggleBtns.ActivatedButtonIndex() == 2 {
// favorite songs view is visible
tr.Refresh()
fyne.Do(func() {
if tr := a.tracklistOrNil(); tr != nil {
// refresh favorite songs view
tr.SetTracks(starred.Tracks)
if a.toggleBtns.ActivatedButtonIndex() == 2 {
// favorite songs view is visible
tr.Refresh()
}
}
}
if a.artistGrid != nil {
// refresh favorite artists view
a.artistGrid.ResetFixed(buildArtistGridViewModel(starred.Artists))
if a.toggleBtns.ActivatedButtonIndex() == 1 {
// favorite artists view is visible
a.artistGrid.Refresh()
if a.artistGrid != nil {
// refresh favorite artists view
a.artistGrid.ResetFixed(buildArtistGridViewModel(starred.Artists))
if a.toggleBtns.ActivatedButtonIndex() == 1 {
// favorite artists view is visible
a.artistGrid.Refresh()
}
}
}
})
}()
}
}
@@ -354,23 +356,25 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
if a.disposed {
return
}
model := buildArtistGridViewModel(fav.Artists)
if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil {
a.artistGrid = g.(*widgets.GridView)
a.artistGrid.Placeholder = myTheme.ArtistIcon
a.artistGrid.ResetFixed(model)
} else {
a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon)
}
canShareArtists := false
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
canShareArtists = r.CanShareArtists()
}
a.artistGrid.DisableSharing = !canShareArtists
a.contr.ConnectArtistGridActions(a.artistGrid)
a.container.Objects[0] = a.artistGrid
a.Refresh()
a.pendingViewSwitch = false
fyne.Do(func() {
model := buildArtistGridViewModel(fav.Artists)
if g := a.pool.Obtain(util.WidgetTypeGridView); g != nil {
a.artistGrid = g.(*widgets.GridView)
a.artistGrid.Placeholder = myTheme.ArtistIcon
a.artistGrid.ResetFixed(model)
} else {
a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon)
}
canShareArtists := false
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
canShareArtists = r.CanShareArtists()
}
a.artistGrid.DisableSharing = !canShareArtists
a.contr.ConnectArtistGridActions(a.artistGrid)
a.container.Objects[0] = a.artistGrid
a.Refresh()
a.pendingViewSwitch = false
})
}()
} else {
a.container.Objects[0] = a.artistGrid
@@ -419,32 +423,34 @@ func (a *FavoritesPage) onShowFavoriteSongs() {
if a.disposed {
return
}
var tracklist *widgets.Tracklist
if tl := a.pool.Obtain(util.WidgetTypeTracklist); tl != nil {
tracklist = tl.(*widgets.Tracklist)
tracklist.Reset()
tracklist.SetTracks(fav.Tracks)
} else {
tracklist = widgets.NewTracklist(fav.Tracks, a.im, false)
}
tracklist.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tracklist.Options.DisableRating = !canRate
tracklist.Options.DisableSharing = !canShare
tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.SetSorting(a.trackSort)
tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols
}
tracklist.SetNowPlaying(a.nowPlayingID)
a.contr.ConnectTracklistActions(tracklist)
a.tracklistCtr = container.New(
&layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, TopPadding: 5, BottomPadding: 15},
tracklist)
a.container.Objects[0] = a.tracklistCtr
a.Refresh()
a.pendingViewSwitch = false
fyne.Do(func() {
var tracklist *widgets.Tracklist
if tl := a.pool.Obtain(util.WidgetTypeTracklist); tl != nil {
tracklist = tl.(*widgets.Tracklist)
tracklist.Reset()
tracklist.SetTracks(fav.Tracks)
} else {
tracklist = widgets.NewTracklist(fav.Tracks, a.im, false)
}
tracklist.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing)
tracklist.Options.DisableRating = !canRate
tracklist.Options.DisableSharing = !canShare
tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.SetSorting(a.trackSort)
tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols
}
tracklist.SetNowPlaying(a.nowPlayingID)
a.contr.ConnectTracklistActions(tracklist)
a.tracklistCtr = container.New(
&layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, TopPadding: 5, BottomPadding: 15},
tracklist)
a.container.Objects[0] = a.tracklistCtr
a.Refresh()
a.pendingViewSwitch = false
})
}()
} else {
a.container.Objects[0] = a.tracklistCtr
+3 -1
View File
@@ -65,7 +65,9 @@ func (g *genrePageAdapter) ActionButton() *widget.Button {
err := g.pm.PlayRandomSongs(g.genre)
if err != nil {
log.Println("error playing random tracks: %v", err)
g.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play random tracks"))
fyne.Do(func() {
g.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play random tracks"))
})
}
}()
}
+16 -8
View File
@@ -193,20 +193,28 @@ func (a *PlaylistPage) doSetNewTrackOrder(ids []string, newPos int) {
}
}
newTracks := sharedutil.ReorderItems(a.tracks, idxs, newPos)
ids = sharedutil.TracksToIDs(newTracks)
// we can't block the UI waiting for the server so assume it will succeed
go func() {
ids = sharedutil.TracksToIDs(newTracks)
if err := a.sm.Server.ReplacePlaylistTracks(a.playlistID, ids); err != nil {
log.Printf("error updating playlist: %s", err.Error())
fyne.Do(func() {
a.contr.ToastProvider.ShowErrorToast(
lang.L("An error occurred updating the playlist"),
)
})
} else {
fyne.Do(func() {
renumberTracks(newTracks)
// force-switch back to unsorted view to show new track order
a.tracklist.SetSorting(widgets.TracklistSort{})
a.tracklist.SetTracks(newTracks)
a.tracklist.UnselectAll()
a.tracks = newTracks
})
}
}()
renumberTracks(newTracks)
// force-switch back to unsorted view to show new track order
a.tracklist.SetSorting(widgets.TracklistSort{})
a.tracklist.SetTracks(newTracks)
a.tracklist.UnselectAll()
a.tracks = newTracks
}
func (a *PlaylistPage) onRemoveSelectedFromPlaylist() {
+4 -2
View File
@@ -150,7 +150,9 @@ func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) {
log.Printf("error loading playlist: %s", err.Error())
return
}
a.contr.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(pl.Tracks))
fyne.Do(func() {
a.contr.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(pl.Tracks))
})
}()
}
a.gridView.OnDownload = func(id string) {
@@ -160,7 +162,7 @@ func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) {
log.Printf("error loading playlist: %s", err.Error())
return
}
a.contr.ShowDownloadDialog(pl.Tracks, pl.Name)
fyne.Do(func() { a.contr.ShowDownloadDialog(pl.Tracks, pl.Name) })
}()
}
}
+3 -1
View File
@@ -211,7 +211,9 @@ func (t *TracksPage) playRandomSongs() {
err := t.contr.App.PlaybackManager.PlayRandomSongs("")
if err != nil {
log.Println("error playing random tracks: %v", err)
t.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play random tracks"))
fyne.Do(func() {
t.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play random tracks"))
})
}
}()
}
+72 -62
View File
@@ -88,48 +88,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller)
app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
if item == nil {
m.Window.SetTitle(displayAppName)
return
}
meta := item.Metadata()
artistDisp := ""
if tr, ok := item.(*mediaprovider.Track); ok {
artistDisp = " " + strings.Join(tr.ArtistNames, ", ")
}
m.Window.SetTitle(fmt.Sprintf("%s%s · %s", meta.Name, artistDisp, displayAppName))
if m.App.Config.Application.ShowTrackChangeNotification {
if runtime.GOOS == "linux" {
if notifySend, err := exec.LookPath("notify-send"); err == nil {
go func() {
args := []string{
"--app-name", "supersonic",
"--urgency", "low",
"--expire-time", "10000",
// replace previous notification
"--hint", "string:x-canonical-private-synchronous:supersonic-track",
meta.Name, strings.TrimPrefix(artistDisp, " "),
}
app.ImageManager.GetCoverThumbnail(meta.CoverArtID)
if path, err := app.ImageManager.GetCoverArtPath(meta.CoverArtID); err == nil {
args = append([]string{"--icon", path}, args...)
}
if out, err := exec.Command(notifySend, args...).CombinedOutput(); err != nil {
log.Printf("notify-send error: %s %s", strings.TrimSpace(string(out)), err)
}
}()
return
}
}
// TODO: Once Fyne issue #2935 is resolved, show album cover
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: meta.Name,
Content: strings.TrimPrefix(artistDisp, " "),
})
}
fyne.Do(func() { m.UpdateOnTrackChange(item) })
})
app.ServerManager.OnServerConnected(func() {
go m.RunOnServerConnectedTasks(app, displayAppName)
@@ -200,6 +159,51 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
return m
}
func (m *MainWindow) UpdateOnTrackChange(item mediaprovider.MediaItem) {
if item == nil {
m.Window.SetTitle(res.DisplayName)
return
}
meta := item.Metadata()
artistDisp := ""
if tr, ok := item.(*mediaprovider.Track); ok {
artistDisp = " " + strings.Join(tr.ArtistNames, ", ")
}
m.Window.SetTitle(fmt.Sprintf("%s%s · %s", meta.Name, artistDisp, res.DisplayName))
if m.App.Config.Application.ShowTrackChangeNotification {
if runtime.GOOS == "linux" {
if notifySend, err := exec.LookPath("notify-send"); err == nil {
go func() {
args := []string{
"--app-name", "supersonic",
"--urgency", "low",
"--expire-time", "10000",
// replace previous notification
"--hint", "string:x-canonical-private-synchronous:supersonic-track",
meta.Name, strings.TrimPrefix(artistDisp, " "),
}
app.ImageManager.GetCoverThumbnail(meta.CoverArtID)
if path, err := app.ImageManager.GetCoverArtPath(meta.CoverArtID); err == nil {
args = append([]string{"--icon", path}, args...)
}
if out, err := exec.Command(notifySend, args...).CombinedOutput(); err != nil {
log.Printf("notify-send error: %s %s", strings.TrimSpace(string(out)), err)
}
}()
return
}
}
// TODO: Once Fyne issue #2935 is resolved, show album cover
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: meta.Name,
Content: artistDisp,
})
}
}
func (m *MainWindow) DesiredSize() fyne.Size {
w := float32(m.App.Config.Application.WindowWidth)
if w <= 1 {
@@ -229,10 +233,6 @@ func (m *MainWindow) StartupPage() controller.Route {
func (m *MainWindow) RunOnServerConnectedTasks(app *backend.App, displayAppName string) {
time.Sleep(1 * time.Millisecond) // ensure this runs after sync tasks
m.BrowsingPane.EnableNavigationButtons()
m.Router.NavigateTo(m.StartupPage())
_, canRate := m.App.ServerManager.Server.(mediaprovider.SupportsRating)
m.BottomPanel.NowPlaying.DisableRating = !canRate
if app.Config.Application.SavePlayQueue {
go func() {
@@ -242,12 +242,19 @@ func (m *MainWindow) RunOnServerConnectedTasks(app *backend.App, displayAppName
}()
}
_, supportsRadio := m.App.ServerManager.Server.(mediaprovider.RadioProvider)
if supportsRadio {
m.radioBtn.Show()
} else {
m.radioBtn.Hide()
}
fyne.Do(func() {
m.BrowsingPane.EnableNavigationButtons()
m.Router.NavigateTo(m.StartupPage())
_, canRate := m.App.ServerManager.Server.(mediaprovider.SupportsRating)
m.BottomPanel.NowPlaying.DisableRating = !canRate
_, supportsRadio := m.App.ServerManager.Server.(mediaprovider.RadioProvider)
if supportsRadio {
m.radioBtn.Show()
} else {
m.radioBtn.Hide()
}
})
m.App.SaveConfigFile()
@@ -257,17 +264,20 @@ func (m *MainWindow) RunOnServerConnectedTasks(app *backend.App, displayAppName
}
// check if launching new version, else if found available update on startup
if l := app.Config.Application.LastLaunchedVersion; app.VersionTag() != l {
if !app.IsFirstLaunch() {
m.ShowWhatsNewDialog()
fyne.Do(func() {
if l := app.Config.Application.LastLaunchedVersion; app.VersionTag() != l {
if !app.IsFirstLaunch() {
m.ShowWhatsNewDialog()
}
m.App.Config.Application.LastLaunchedVersion = app.VersionTag()
} else if t := app.UpdateChecker.VersionTagFound(); t != "" && t != app.Config.Application.LastCheckedVersion {
if t != app.VersionTag() {
m.ShowNewVersionDialog(displayAppName, t)
}
m.App.Config.Application.LastCheckedVersion = t
}
m.App.Config.Application.LastLaunchedVersion = app.VersionTag()
} else if t := app.UpdateChecker.VersionTagFound(); t != "" && t != app.Config.Application.LastCheckedVersion {
if t != app.VersionTag() {
m.ShowNewVersionDialog(displayAppName, t)
}
m.App.Config.Application.LastCheckedVersion = t
}
})
// register callback for the ongoing periodic update check
m.App.UpdateChecker.OnUpdatedVersionFound = func() {
t := m.App.UpdateChecker.VersionTagFound()
+3 -1
View File
@@ -4,6 +4,8 @@ import (
"context"
"image"
"log"
"fyne.io/fyne/v2"
)
// ThumbnailLoader is a utility type that exposes a single API to load
@@ -50,7 +52,7 @@ func (i *ThumbnailLoader) Load(coverID string) {
if err != nil {
log.Printf("Error loading cover image: %s", err.Error())
} else {
i.callOnLoaded(img)
fyne.Do(func() { i.callOnLoaded(img) })
}
i.prevLoadCancel() // Done. Release resources associated with un-cancelled ctx
})