Merge pull request #552 from dweymouth/fyne-2.6-migrate

Fyne 2.6 migrate
This commit is contained in:
Drew Weymouth
2025-02-11 17:38:48 -03:00
committed by GitHub
43 changed files with 716 additions and 1141 deletions
+23 -15
View File
@@ -33,19 +33,21 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
pm.OnSongChange(bp.onSongChange)
pm.OnPlayTimeUpdate(func(cur, total float64, _ bool) {
if !pm.IsSeeking() {
bp.Controls.UpdatePlayTime(cur, total)
}
fyne.Do(func() {
if !pm.IsSeeking() {
bp.Controls.UpdatePlayTime(cur, total)
}
})
})
pm.OnPaused(func() {
bp.Controls.SetPlaying(false)
fyne.Do(func() { bp.Controls.SetPlaying(false) })
})
pm.OnPlaying(func() {
bp.Controls.SetPlaying(true)
fyne.Do(func() { bp.Controls.SetPlaying(true) })
})
pm.OnStopped(func() {
bp.Controls.SetPlaying(false)
fyne.Do(func() { bp.Controls.SetPlaying(false) })
})
bp.NowPlaying = widgets.NewNowPlayingCard()
@@ -103,8 +105,12 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
})
bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay())
pm.OnLoopModeChange(bp.AuxControls.SetLoopMode)
pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume)
pm.OnLoopModeChange(func(lm backend.LoopMode) {
fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) })
})
pm.OnVolumeChange(func(vol int) {
fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) })
})
bp.AuxControls.VolumeControl.OnSetVolume = func(v int) {
pm.SetVolume(v)
}
@@ -124,13 +130,15 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
}
func (bp *BottomPanel) onSongChange(song mediaprovider.MediaItem, _ *mediaprovider.Track) {
if song == nil {
bp.NowPlaying.Update(nil)
bp.imageLoader.Load("")
} else {
bp.NowPlaying.Update(song)
bp.imageLoader.Load(song.Metadata().CoverArtID)
}
fyne.Do(func() {
if song == nil {
bp.NowPlaying.Update(nil)
bp.imageLoader.Load("")
} else {
bp.NowPlaying.Update(song)
bp.imageLoader.Load(song.Metadata().CoverArtID)
}
})
}
func (bp *BottomPanel) CreateRenderer() fyne.WidgetRenderer {
+13 -8
View File
@@ -168,11 +168,13 @@ func (a *AlbumPage) load() {
if a.disposed {
return
}
a.header.Update(album, a.im)
a.tracklist.Options.ShowDiscNumber = len(album.Tracks) > 0 && album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber
a.tracks = album.Tracks
a.tracklist.SetTracks(album.Tracks)
a.tracklist.SetNowPlaying(a.nowPlayingID)
fyne.Do(func() {
a.header.Update(album, a.im)
a.tracklist.Options.ShowDiscNumber = len(album.Tracks) > 0 && album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber
a.tracks = album.Tracks
a.tracklist.SetTracks(album.Tracks)
a.tracklist.SetNowPlaying(a.nowPlayingID)
})
}
type AlbumPageHeader struct {
@@ -316,8 +318,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)
}
@@ -342,6 +346,7 @@ func (a *AlbumPageHeader) toggleFavorited() {
a.page.mp.SetFavorite(params, a.toggleFavButton.IsFavorited)
}
// should be called asynchronously
func (a *AlbumPageHeader) showPopUpCover() {
if a.fullSizeCoverFetching {
return
@@ -354,7 +359,7 @@ func (a *AlbumPageHeader) showPopUpCover() {
return
}
if a.page != nil {
a.page.contr.ShowPopUpImage(cover)
fyne.Do(func() { a.page.contr.ShowPopUpImage(cover) })
}
}
+81 -55
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.Printf("error playing similar songs: %v", err)
a.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play artist radio"))
})
}
}()
}
@@ -243,23 +245,29 @@ func (a *ArtistPage) load() {
log.Printf("Failed to get artist: %s", err.Error())
return
}
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() {
if a.disposed {
return
}
a.artistInfo = artist
a.header.Update(artist, a.im)
if a.activeView == 0 {
a.showAlbumGrid(false /*reSort*/)
} else {
go 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() {
if !a.disposed {
a.header.UpdateInfo(info)
}
})
}
func (a *ArtistPage) showAlbumGrid(reSort bool) {
@@ -287,7 +295,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 +318,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) {
@@ -390,7 +411,7 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
SizeName: theme.SizeNameHeadingText,
}
a.artistImage = widgets.NewImagePlaceholder(myTheme.ArtistIcon, 225)
a.artistImage.OnTapped = func(*fyne.PointEvent) { go a.showPopUpCover() }
a.artistImage.OnTapped = func(*fyne.PointEvent) { a.showPopUpCover() }
a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() })
a.playBtn = widget.NewButtonWithIcon(lang.L("Play Discography"), theme.MediaPlayIcon(), func() {
go a.artistPage.pm.PlayArtistDiscography(a.artistID, false /*shuffle*/)
@@ -512,14 +533,17 @@ 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*/) })
}
}()
}
}
}
// should NOT be called asynchronously
func (a *ArtistPageHeader) showPopUpCover() {
if a.artistImageID == "" {
if im := a.artistImage.Image(); im != nil {
@@ -530,15 +554,17 @@ func (a *ArtistPageHeader) showPopUpCover() {
return
}
a.fullSizeCoverFetching = true
defer func() { a.fullSizeCoverFetching = false }()
cover, err := a.artistPage.im.GetFullSizeCoverArt(a.artistImageID)
if err != nil {
log.Printf("error getting full size album cover: %s", err.Error())
return
}
if a.artistPage != nil {
a.artistPage.contr.ShowPopUpImage(cover)
}
go func() {
defer func() { a.fullSizeCoverFetching = false }()
cover, err := a.artistPage.im.GetFullSizeCoverArt(a.artistImageID)
if err != nil {
log.Printf("error getting full size album cover: %s", err.Error())
return
}
if a.artistPage != nil {
fyne.Do(func() { a.artistPage.contr.ShowPopUpImage(cover) })
}
}()
}
}
+24 -18
View File
@@ -255,30 +255,36 @@ func (b *BrowsingPane) doSetPage(p Page) bool {
}
func (b *BrowsingPane) onSongChange(song mediaprovider.MediaItem, lastScrobbledIfAny *mediaprovider.Track) {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowNowPlaying); ok {
p.OnSongChange(song, lastScrobbledIfAny)
}
fyne.Do(func() {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowNowPlaying); ok {
p.OnSongChange(song, lastScrobbledIfAny)
}
})
}
func (b *BrowsingPane) onPlayTimeUpdate(cur, total float64, seeked bool) {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowPlayTime); ok {
p.OnPlayTimeUpdate(cur, total, seeked)
}
fyne.Do(func() {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowPlayTime); ok {
p.OnPlayTimeUpdate(cur, total, seeked)
}
})
}
func (b *BrowsingPane) onQueueChange() {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowPlayQueue); ok {
p.OnPlayQueueChange()
}
fyne.Do(func() {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowPlayQueue); ok {
p.OnPlayQueueChange()
}
})
}
func (b *BrowsingPane) addPageToHistory(p Page, truncate bool) {
+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"))
})
}
}()
}
+15 -13
View File
@@ -64,20 +64,22 @@ func (a *GenresPage) load(searchOnLoad bool, scrollPos float32) {
if err != nil {
log.Printf("error loading genres: %v", err.Error())
}
a.genres = genres
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
fyne.Do(func() {
a.genres = genres
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
}
} else {
a.list.SetGenres(a.genres)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
return
}
a.list.Refresh()
}
} else {
a.list.SetGenres(a.genres)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
return
}
a.list.Refresh()
}
})
}
func (a *GenresPage) onSearched(query string) {
+23 -36
View File
@@ -9,7 +9,6 @@ import (
"net/url"
"slices"
"strings"
"sync"
"time"
"github.com/cenkalti/dominantcolor"
@@ -49,9 +48,6 @@ type NowPlayingPage struct {
related []*mediaprovider.Track
alreadyLoaded bool
lyricLock sync.Mutex
relatedLock sync.Mutex
// widgets for render
background *canvas.LinearGradient
queueList *widgets.PlayQueueList
@@ -106,9 +102,10 @@ func NewNowPlayingPage(
a := &NowPlayingPage{nowPlayingPageState: state}
a.ExtendBaseWidget(a)
pm.OnPaused(a.formatStatusLine)
pm.OnPlaying(a.formatStatusLine)
pm.OnStopped(a.formatStatusLine)
doFmtStatus := func() { fyne.Do(a.formatStatusLine) }
pm.OnPaused(doFmtStatus)
pm.OnPlaying(doFmtStatus)
pm.OnStopped(doFmtStatus)
a.card = widgets.NewLargeNowPlayingCard()
a.card.OnAlbumNameTapped = func() {
@@ -297,7 +294,8 @@ func (a *NowPlayingPage) onImageLoaded(img image.Image, err error) {
if err != nil {
log.Printf("error loading cover art: %v\n", err)
}
a.card.SetCoverImage(img)
fyne.Do(func() { a.card.SetCoverImage(img) })
if img == nil {
return
}
@@ -307,24 +305,18 @@ func (a *NowPlayingPage) onImageLoaded(img image.Image, err error) {
return
}
evenFrame := true
// Fyne animation starting is currently thread-safe,
// despite not being marked as such
// TODO: if this changes, use fyne.Do
anim := canvas.NewColorRGBAAnimation(
a.background.StartColor, c, 75*time.Millisecond, func(c color.Color) {
// reduce fps to reduce mem allocations
if evenFrame {
a.background.StartColor = c
a.background.Refresh()
}
evenFrame = !evenFrame
a.background.StartColor = c
a.background.Refresh()
})
anim.Start()
}
func (a *NowPlayingPage) updateLyrics() {
a.lyricLock.Lock()
defer a.lyricLock.Unlock()
if a.lyricFetchCancel != nil {
a.lyricFetchCancel()
}
@@ -371,20 +363,17 @@ func (a *NowPlayingPage) fetchLyrics(ctx context.Context, song *mediaprovider.Tr
case <-ctx.Done():
return
default:
a.lyricLock.Lock()
a.lyricsLoading.Stop()
a.lyricsViewer.SetLyrics(lyrics)
if lyrics != nil {
a.lyricsViewer.OnSeeked(a.lastPlayPos)
}
a.lyricLock.Unlock()
fyne.Do(func() {
a.lyricsLoading.Stop()
a.lyricsViewer.SetLyrics(lyrics)
if lyrics != nil {
a.lyricsViewer.OnSeeked(a.lastPlayPos)
}
})
}
}
func (a *NowPlayingPage) updateRelatedList() {
a.relatedLock.Lock()
defer a.relatedLock.Unlock()
if a.relatedFetchCancel != nil {
a.relatedFetchCancel()
}
@@ -409,11 +398,11 @@ func (a *NowPlayingPage) updateRelatedList() {
case <-ctx.Done():
return
default:
a.relatedLock.Lock()
a.related = related
a.relatedList.SetTracks(a.related)
a.relatedLoading.Stop()
a.relatedLock.Unlock()
fyne.Do(func() {
a.related = related
a.relatedList.SetTracks(a.related)
a.relatedLoading.Stop()
})
}
}(ctx)
}
@@ -461,8 +450,6 @@ func (a *NowPlayingPage) OnPlayTimeUpdate(curTime, _ float64, seeked bool) {
if a.tabs == nil || a.tabs.SelectedIndex() != 1 /*lyrics*/ {
return
}
a.lyricLock.Lock()
defer a.lyricLock.Unlock()
if seeked {
a.lyricsViewer.OnSeeked(curTime)
} else {
+24 -13
View File
@@ -166,10 +166,12 @@ func (a *PlaylistPage) load() {
return
}
renumberTracks(playlist.Tracks)
a.tracks = playlist.Tracks
a.tracklist.SetTracks(playlist.Tracks)
a.tracklist.SetNowPlaying(a.nowPlayingID)
a.header.Update(playlist)
fyne.Do(func() {
a.tracks = playlist.Tracks
a.tracklist.SetTracks(playlist.Tracks)
a.tracklist.SetNowPlaying(a.nowPlayingID)
a.header.Update(playlist)
})
}
func renumberTracks(tracks []*mediaprovider.Track) {
@@ -193,20 +195,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 {
renumberTracks(newTracks)
fyne.Do(func() {
// 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() {
@@ -339,6 +349,7 @@ func (a *PlaylistPageHeader) Update(playlist *mediaprovider.PlaylistWithTracks)
a.Refresh()
}
// should be called asynchronously
func (a *PlaylistPageHeader) showPopUpCover() {
if a.fullSizeCoverFetching || a.playlistInfo == nil {
return
@@ -351,7 +362,7 @@ func (a *PlaylistPageHeader) showPopUpCover() {
return
}
if a.page != nil {
a.page.contr.ShowPopUpImage(cover)
fyne.Do(func() { a.page.contr.ShowPopUpImage(cover) })
}
}
+15 -9
View File
@@ -108,17 +108,21 @@ func (p *PlaylistsPage) Scroll(scrollAmt float32) {
}
}
// should be called asynchronously
func (a *PlaylistsPage) load(searchOnLoad bool) {
playlists, err := a.mp.GetPlaylists()
if err != nil {
log.Printf("error loading playlists: %v", err.Error())
}
a.playlists = playlists
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
} else {
a.refreshView(playlists)
}
fyne.Do(func() {
a.playlists = playlists
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
} else {
a.refreshView(playlists)
}
})
}
func (a *PlaylistsPage) createListView() {
@@ -150,7 +154,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 +166,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) })
}()
}
}
@@ -378,7 +384,7 @@ func (p *PlaylistList) buildHeaderAndLayout() {
p.header = widgets.NewListHeader([]widgets.ListColumn{
{Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: lang.L("Description"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: lang.L("_Description"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: lang.L("Owner"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: trackCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}}, p.columnsLayout)
p.header.SetSorting(p.sorting)
+20 -17
View File
@@ -78,25 +78,28 @@ func (a *RadiosPage) load(searchOnLoad bool, scrollPos float32) {
if err != nil {
log.Printf("error loading radios: %v", err.Error())
}
if len(radios) == 0 {
a.noRadiosMsg.Show()
} else {
a.noRadiosMsg.Hide()
}
a.radios = radios
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
fyne.Do(func() {
if len(radios) == 0 {
a.noRadiosMsg.Show()
} else {
a.noRadiosMsg.Hide()
}
} else {
a.list.SetRadios(a.radios)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
return
a.radios = radios
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
}
} else {
a.list.SetRadios(a.radios)
if scrollPos != 0 {
a.list.list.ScrollToOffset(scrollPos)
return
}
a.list.Refresh()
}
a.list.Refresh()
}
})
}
func (a *RadiosPage) onPlay(station *mediaprovider.RadioStation) {
+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"))
})
}
}()
}
+115 -91
View File
@@ -307,14 +307,18 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
})
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)
fyne.Do(func() {
msg := lang.LocalizePluralKey("playlist.addedtracks",
"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"),
)
fyne.Do(func() {
m.ToastProvider.ShowErrorToast(
lang.L("An error occurred adding tracks to the playlist"),
)
})
}
pop.Hide()
m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates
@@ -324,7 +328,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
if err == nil {
notifySuccess(len(trackIDs))
} else {
log.Println("error adding tracks to playlist: %s", err.Error())
log.Printf("error adding tracks to playlist: %s", err.Error())
notifyError()
}
}()
@@ -348,7 +352,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
if err == nil {
notifySuccess(len(filterTrackIDs))
} else {
log.Println("error adding tracks to playlist: %s", err.Error())
log.Printf("error adding tracks to playlist: %s", err.Error())
notifyError()
}
}
@@ -359,7 +363,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
if err == nil {
notifySuccess(len(trackIDs))
} else {
log.Println("error adding tracks to playlist: %s", err.Error())
log.Printf("error adding tracks to playlist: %s", err.Error())
notifyError()
}
}()
@@ -399,7 +403,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
log.Printf("error deleting playlist: %s", err.Error())
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
// navigate to playlists page if user is still on the page of the deleted playlist
m.NavigateTo(PlaylistsRoute())
fyne.Do(func() { m.NavigateTo(PlaylistsRoute()) })
}
}()
}
@@ -414,7 +418,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
log.Printf("error updating playlist: %s", err.Error())
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
// if user is on playlist page, reload to get the updates
m.ReloadFunc()
fyne.Do(m.ReloadFunc)
}
}()
}
@@ -434,7 +438,6 @@ func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) {
// try connecting to last used server - set up cancelable modal dialog
canceled := false
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
dlg := dialog.NewCustom(lang.L("Connecting"), lang.L("Cancel"),
widget.NewLabel(fmt.Sprintf(lang.L("Connecting to")+" %s", server.Nickname)), c.MainWindow)
dlg.SetOnClosed(func() {
@@ -443,25 +446,34 @@ func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) {
})
c.haveModal = true
dlg.Show()
// try to connect
if err := c.tryConnectToServer(ctx, server, pass); err != nil {
dlg.Hide()
c.haveModal = false
if canceled {
c.PromptForLoginAndConnect()
} else {
// connection failure
dlg := dialog.NewError(err, c.MainWindow)
dlg.SetOnClosed(func() {
c.PromptForLoginAndConnect()
go func() {
defer cancel() // make sure to free up ctx resources if user does not cancel
if err := c.tryConnectToServer(ctx, server, pass); err != nil {
fyne.Do(func() {
dlg.Hide()
c.haveModal = false
if canceled {
c.PromptForLoginAndConnect()
} else {
// connection failure
dlg := dialog.NewError(err, c.MainWindow)
dlg.SetOnClosed(func() {
c.PromptForLoginAndConnect()
})
c.haveModal = true
dlg.Show()
}
})
} else {
fyne.Do(func() {
dlg.Hide()
c.haveModal = false
})
c.haveModal = true
dlg.Show()
}
} else {
dlg.Hide()
c.haveModal = false
}
}()
}
func (m *Controller) PromptForLoginAndConnect() {
@@ -475,16 +487,18 @@ func (m *Controller) PromptForLoginAndConnect() {
defer cancel()
err := m.App.ServerManager.TestConnectionAndAuth(ctx, server.ServerConnection, password)
if err == backend.ErrUnreachable {
d.SetErrorText(lang.L("Server unreachable"))
} else if err != nil {
d.SetErrorText(lang.L("Authentication failed"))
} else {
pop.Hide()
m.trySetPasswordAndConnectToServer(server, password)
m.doModalClosed()
}
d.EnableSubmit()
fyne.Do(func() {
if err == backend.ErrUnreachable {
d.SetErrorText(lang.L("Server unreachable"))
} else if err != nil {
d.SetErrorText(lang.L("Authentication failed"))
} else {
pop.Hide()
m.trySetPasswordAndConnectToServer(server, password)
m.doModalClosed()
}
d.EnableSubmit()
})
}()
}
d.OnEditServer = func(server *backend.ServerConfig) {
@@ -494,18 +508,21 @@ func (m *Controller) PromptForLoginAndConnect() {
editD.OnSubmit = func() {
d.DisableSubmit()
go func() {
if m.testConnectionAndUpdateDialogText(editD) {
// connection is good
editPop.Hide()
server.Hostname = editD.Host
server.AltHostname = editD.AltHost
server.Nickname = editD.Nickname
server.Username = editD.Username
server.LegacyAuth = editD.LegacyAuth
m.trySetPasswordAndConnectToServer(server, editD.Password)
m.doModalClosed()
}
d.EnableSubmit()
success := m.testConnectionAndUpdateDialogText(editD)
fyne.Do(func() {
if success {
// connection is good
editPop.Hide()
server.Hostname = editD.Host
server.AltHostname = editD.AltHost
server.Nickname = editD.Nickname
server.Username = editD.Username
server.LegacyAuth = editD.LegacyAuth
m.trySetPasswordAndConnectToServer(server, editD.Password)
m.doModalClosed()
}
d.EnableSubmit()
})
}()
}
editD.OnCancel = func() {
@@ -521,21 +538,24 @@ func (m *Controller) PromptForLoginAndConnect() {
newD.OnSubmit = func() {
d.DisableSubmit()
go func() {
if m.testConnectionAndUpdateDialogText(newD) {
// connection is good
newPop.Hide()
conn := backend.ServerConnection{
ServerType: newD.ServerType,
Hostname: newD.Host,
AltHostname: newD.AltHost,
Username: newD.Username,
LegacyAuth: newD.LegacyAuth,
success := m.testConnectionAndUpdateDialogText(newD)
fyne.Do(func() {
if success {
// connection is good
newPop.Hide()
conn := backend.ServerConnection{
ServerType: newD.ServerType,
Hostname: newD.Host,
AltHostname: newD.AltHost,
Username: newD.Username,
LegacyAuth: newD.LegacyAuth,
}
server := m.App.ServerManager.AddServer(newD.Nickname, conn)
m.trySetPasswordAndConnectToServer(server, newD.Password)
m.doModalClosed()
}
server := m.App.ServerManager.AddServer(newD.Nickname, conn)
m.trySetPasswordAndConnectToServer(server, newD.Password)
m.doModalClosed()
}
d.EnableSubmit()
d.EnableSubmit()
})
}()
}
newD.OnCancel = func() {
@@ -753,24 +773,26 @@ func (c *Controller) ShowShareDialog(id string) {
return
}
hyperlink := widget.NewHyperlink(shareUrl.String(), shareUrl)
dlg := dialog.NewCustom(lang.L("Share content"), lang.L("OK"),
container.NewHBox(
hyperlink,
widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
c.MainWindow.Clipboard().SetContent(hyperlink.Text)
}),
widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), func() {
if shareUrl, err := c.createShareURL(id); err == nil {
hyperlink.Text = shareUrl.String()
hyperlink.URL = shareUrl
hyperlink.Refresh()
}
}),
),
c.MainWindow,
)
dlg.Show()
fyne.Do(func() {
hyperlink := widget.NewHyperlink(shareUrl.String(), shareUrl)
dlg := dialog.NewCustom(lang.L("Share content"), lang.L("OK"),
container.NewHBox(
hyperlink,
widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
c.MainWindow.Clipboard().SetContent(hyperlink.Text)
}),
widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), func() {
if shareUrl, err := c.createShareURL(id); err == nil {
hyperlink.Text = shareUrl.String()
hyperlink.URL = shareUrl
hyperlink.Refresh()
}
}),
),
c.MainWindow,
)
dlg.Show()
})
}()
}
@@ -904,15 +926,17 @@ func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover i
log.Print("Error getting album info: ", err)
return
}
dlg := dialogs.NewAlbumInfoDialog(albumInfo, albumName, albumCover)
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
dlg.OnDismiss = func() {
pop.Hide()
c.doModalClosed()
}
c.ClosePopUpOnEscape(pop)
c.haveModal = true
pop.Show()
fyne.Do(func() {
dlg := dialogs.NewAlbumInfoDialog(albumInfo, albumName, albumCover)
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
dlg.OnDismiss = func() {
pop.Hide()
c.doModalClosed()
}
c.ClosePopUpOnEscape(pop)
c.haveModal = true
pop.Show()
})
}()
}
+1 -1
View File
@@ -62,7 +62,7 @@ func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck boo
container.New(layout.NewFormLayout(),
widget.NewLabel(lang.L("Name")),
nameEntry,
widget.NewLabel(lang.L("Description")),
widget.NewLabel(lang.L("_Description")),
descriptionEntry,
),
container.NewHBox(isPublicCheck, layout.NewSpacer(), deleteBtn),
+13 -9
View File
@@ -101,7 +101,7 @@ func (sd *SearchDialog) SearchQuery() string {
func (sd *SearchDialog) Show() {
sd.BaseWidget.Show()
go sd.onSearched("")
sd.onSearched("")
}
func (sd *SearchDialog) Refresh() {
@@ -163,14 +163,18 @@ func (sd *SearchDialog) setResults(results []*mediaprovider.SearchResult) {
func (sd *SearchDialog) onSearched(query string) {
sd.loadingDots.Start()
var results []*mediaprovider.SearchResult
res := sd.OnSearched(query)
if len(res) == 0 {
log.Println("No results matched the query.")
} else {
results = res
}
sd.loadingDots.Stop()
sd.setResults(results)
go func() {
res := sd.OnSearched(query)
if len(res) == 0 {
log.Println("No results matched the query.")
} else {
results = res
}
fyne.Do(func() {
sd.loadingDots.Stop()
sd.setResults(results)
})
}()
}
func (sd *SearchDialog) CreateRenderer() fyne.WidgetRenderer {
+81 -85
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)
@@ -152,11 +111,13 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
m.BrowsingPane.AddSettingsMenuItem(lang.L("Check for Updates"), func() {
go func() {
if t := app.UpdateChecker.CheckLatestVersionTag(); t != "" && t != app.VersionTag() {
m.ShowNewVersionDialog(displayAppName, t)
fyne.Do(func() { m.ShowNewVersionDialog(displayAppName, t) })
} else {
dialog.ShowInformation(lang.L("No new version found"),
lang.L("You are running the latest version of")+" "+displayAppName,
m.Window)
fyne.Do(func() {
dialog.ShowInformation(lang.L("No new version found"),
lang.L("You are running the latest version of")+" "+displayAppName,
m.Window)
})
}
}()
})
@@ -174,32 +135,61 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
m.Window.SetCloseIntercept(func() {
m.SaveWindowSize()
// save settings in case we crash during shutdown
// TODO: when all shutdowns exit cleanly, remove these lines
// as they are already executed in app.Shutdown()
app.Config.LocalPlayback.Volume = app.LocalPlayer.GetVolume()
repeatMode := "None"
switch app.PlaybackManager.GetLoopMode() {
case backend.LoopOne:
repeatMode = "One"
case backend.LoopAll:
repeatMode = "All"
}
app.Config.Playback.RepeatMode = repeatMode
app.Config.Playback.Autoplay = app.PlaybackManager.IsAutoplay()
app.SavePlayQueueIfEnabled()
app.SaveConfigFile()
if app.Config.Application.CloseToSystemTray && m.HaveSystemTray() {
m.Window.Hide()
} else {
fyneApp.Quit()
m.Window.Close()
}
})
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, " "),
}
m.App.ImageManager.GetCoverThumbnail(meta.CoverArtID)
if path, err := m.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 +219,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 +228,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 +250,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()
@@ -304,8 +300,8 @@ func (m *MainWindow) SetupSystemTrayMenu(appName string, fyneApp fyne.App) {
m.App.PlaybackManager.SetVolume(vol)
}),
fyne.NewMenuItemSeparator(),
fyne.NewMenuItem(lang.L("Show"), m.Window.Show),
fyne.NewMenuItem(lang.L("Hide"), m.Window.Hide),
fyne.NewMenuItem(lang.L("Show"), func() { fyne.Do(m.Window.Show) }),
fyne.NewMenuItem(lang.L("Hide"), func() { fyne.Do(m.Window.Hide) }),
)
desk.SetSystemTrayMenu(menu)
desk.SetSystemTrayIcon(res.ResAppicon256Png)
+1 -1
View File
@@ -72,7 +72,7 @@ func (t *ToastOverlay) showToast(isErr bool, message string) {
case <-ctx.Done():
return
default:
t.dismissToast()
fyne.Do(t.dismissToast)
}
}()
}
+43
View File
@@ -0,0 +1,43 @@
package util
import (
"time"
)
type EventCounter struct {
buf []time.Time
ptr int
}
func NewEventCounter(maxN int) *EventCounter {
buffer := make([]time.Time, maxN)
return &EventCounter{
buf: buffer,
}
}
func (e *EventCounter) Add() {
e.buf[e.ptr] = time.Now()
e.ptr = (e.ptr + 1) % len(e.buf)
}
func (e *EventCounter) NumEventsSince(t time.Time) int {
i := e.ptr - 1
count := 0
for {
if i < 0 {
i = len(e.buf) - 1
}
if !e.buf[i].IsZero() && e.buf[i].After(t) {
count++
} else {
break
}
if count == len(e.buf) {
break
}
i--
}
return count
}
+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
})
+3 -1
View File
@@ -199,7 +199,9 @@ func NewDebouncer(dur time.Duration, callOnDone func()) func() {
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(dur, callOnDone)
timer = time.AfterFunc(dur, func() {
fyne.Do(callOnDone)
})
}
}
+6 -4
View File
@@ -391,14 +391,16 @@ func (g *GridView) checkFetchMoreItems(count int) {
g.stateMutex.Lock()
g.items = append(g.items, items...)
g.stateMutex.Unlock()
g.loadingDots.Stop()
if len(items) < batchFetchSize {
g.done = true
}
n += len(items)
if len(items) > 0 {
g.grid.Refresh()
}
fyne.DoAndWait(func() {
g.loadingDots.Stop()
if len(items) > 0 {
g.grid.Refresh()
}
})
}
}
}
+2 -2
View File
@@ -62,16 +62,16 @@ func (l *LoadingDots) Stop() {
func (l *LoadingDots) animate(ctx context.Context) {
foreground := theme.ForegroundColor()
disabled := theme.DisabledColor()
l.doTick(foreground, disabled)
ticker := time.NewTicker(333 * time.Millisecond)
for {
fyne.Do(func() { l.doTick(foreground, disabled) })
select {
case <-ctx.Done():
ticker.Stop()
l.running.Store(false)
return
case <-ticker.C:
l.doTick(foreground, disabled)
continue
}
}
}
+5 -55
View File
@@ -5,7 +5,6 @@ import (
"slices"
"sort"
"strings"
"sync"
"fyne.io/fyne/v2/lang"
@@ -91,7 +90,6 @@ type Tracklist struct {
visibleColumns []bool
sorting TracklistSort
tracksMutex sync.RWMutex
tracks []*util.TrackListModel
tracksOrigOrder []*util.TrackListModel
@@ -148,7 +146,7 @@ func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCo
}
t.list = NewFocusList(
t.lenTracks,
func() int { return len(t.tracks) },
func() fyne.CanvasObject {
var tr TracklistRow
if t.compactRows {
@@ -169,16 +167,13 @@ func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCo
return tr
},
func(itemID widget.ListItemID, item fyne.CanvasObject) {
t.tracksMutex.RLock()
// we could have removed tracks from the list in between
// Fyne calling the length callback and this update callback
// so the itemID may be out of bounds. if so, do nothing.
if itemID >= len(t.tracks) {
t.tracksMutex.RUnlock()
return
}
model := t.tracks[itemID]
t.tracksMutex.RUnlock()
tr := item.(TracklistRow)
if tr.TrackID() != model.Item.Metadata().ID || tr.ItemID() != itemID {
@@ -219,10 +214,8 @@ func (t *Tracklist) Scroll(amount float32) {
t.list.ScrollToOffset(t.list.GetScrollOffset() + amount)
}
// Gets the track at the given index. Thread-safe.
// Gets the track at the given index.
func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
if idx >= len(t.tracks) {
log.Println("error: Tracklist.TrackAt: index out of range")
return nil
@@ -291,10 +284,8 @@ func (t *Tracklist) SetSorting(sorting TracklistSort) {
// Sets the currently playing track ID and updates the list rendering
func (t *Tracklist) SetNowPlaying(trackID string) {
prevNowPlaying := t.nowPlayingID
t.tracksMutex.RLock()
trPrev, idxPrev := util.FindItemByID(t.tracks, prevNowPlaying)
tr, idx := util.FindItemByID(t.tracks, trackID)
t.tracksMutex.RUnlock()
t.nowPlayingID = trackID
if trPrev != nil {
t.list.RefreshItem(idxPrev)
@@ -306,58 +297,46 @@ func (t *Tracklist) SetNowPlaying(trackID string) {
// Increments the play count of the given track and updates the list rendering
func (t *Tracklist) IncrementPlayCount(trackID string) {
t.tracksMutex.RLock()
tr, idx := util.FindItemByID(t.tracks, trackID)
t.tracksMutex.RUnlock()
if tr != nil {
tr.(*mediaprovider.Track).PlayCount += 1
t.list.RefreshItem(idx)
}
}
// Remove all tracks from the tracklist. Does not issue Refresh call. Thread-safe.
// Remove all tracks from the tracklist. Does not issue Refresh call.
func (t *Tracklist) Clear() {
t.tracksMutex.Lock()
defer t.tracksMutex.Unlock()
t.tracks = nil
t.tracksOrigOrder = nil
}
// Sets the tracks in the tracklist. Thread-safe.
// Sets the tracks in the tracklist.
func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) {
t._setTracks(trs)
t.Refresh()
}
func (t *Tracklist) _setTracks(trs []*mediaprovider.Track) {
t.tracksMutex.Lock()
defer t.tracksMutex.Unlock()
t.tracksOrigOrder = util.ToTrackListModels(trs)
t.doSortTracks()
}
// Returns the tracks in the tracklist in the current display order.
func (t *Tracklist) GetTracks() []*mediaprovider.Track {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
return sharedutil.MapSlice(t.tracks, func(tm *util.TrackListModel) *mediaprovider.Track {
return tm.Track()
})
}
// Append more tracks to the tracklist. Thread-safe.
// Append more tracks to the tracklist.
func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) {
t.tracksMutex.Lock()
t.tracksOrigOrder = append(t.tracks, util.ToTrackListModels(trs)...)
t.doSortTracks()
t.tracksMutex.Unlock()
t.list.Refresh()
}
func (t *Tracklist) SelectAll() {
t.tracksMutex.RLock()
util.SelectAllItems(t.tracks)
t.tracksMutex.RUnlock()
t.list.Refresh()
}
@@ -367,13 +346,10 @@ func (t *Tracklist) UnselectAll() {
}
func (t *Tracklist) unselectAll() {
t.tracksMutex.RLock()
util.UnselectAllItems(t.tracks)
t.tracksMutex.RUnlock()
}
func (t *Tracklist) SelectAndScrollToTrack(trackID string) {
t.tracksMutex.RLock()
idx := -1
for i, tr := range t.tracks {
if tr.Item.Metadata().ID == trackID {
@@ -383,7 +359,6 @@ func (t *Tracklist) SelectAndScrollToTrack(trackID string) {
tr.Selected = false
}
}
t.tracksMutex.RUnlock()
if idx >= 0 {
t.list.ScrollTo(idx)
}
@@ -474,9 +449,7 @@ func (t *Tracklist) doSortTracks() {
func (t *Tracklist) onSorted(sort ListHeaderSort) {
t.sorting = TracklistSort{ColumnName: t.colName(sort.ColNumber), SortOrder: sort.Type}
t.tracksMutex.Lock()
t.doSortTracks()
t.tracksMutex.Unlock()
t.Refresh()
}
@@ -503,20 +476,14 @@ func (t *Tracklist) onSelectTrack(idx int) {
}
func (t *Tracklist) selectAddOrRemove(idx int) {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
t.tracks[idx].Selected = !t.tracks[idx].Selected
}
func (t *Tracklist) selectTrack(idx int) {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
util.SelectItem(t.tracks, idx)
}
func (t *Tracklist) selectRange(idx int) {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
util.SelectItemRange(t.tracks, idx)
}
@@ -606,9 +573,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
}
func (t *Tracklist) onSetFavorite(trackID string, fav bool) {
t.tracksMutex.RLock()
item, _ := util.FindItemByID(t.tracks, trackID)
t.tracksMutex.RUnlock()
t.onSetFavorites([]*mediaprovider.Track{item.(*mediaprovider.Track)}, fav, false)
}
@@ -627,9 +592,7 @@ func (t *Tracklist) onSetFavorites(tracks []*mediaprovider.Track, fav bool, need
func (t *Tracklist) onSetRating(trackID string, rating int) {
// update our own track model
t.tracksMutex.RLock()
item, _ := util.FindItemByID(t.tracks, trackID)
t.tracksMutex.RUnlock()
t.onSetRatings([]*mediaprovider.Track{item.(*mediaprovider.Track)}, rating, false)
}
@@ -681,14 +644,10 @@ func (t *Tracklist) onPlaySongRadio(tracks []*mediaprovider.Track) {
}
func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
return util.SelectedTracks(t.tracks)
}
func (t *Tracklist) SelectedTrackIDs() []string {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
return util.SelectedItemIDs(t.tracks)
}
@@ -696,9 +655,6 @@ func (t *Tracklist) SelectedTrackIDs() []string {
// original sort order (ie if tracklist is sorted by some column), the indexes
// returned will correspond to the order of tracks when the list was initialized.
func (t *Tracklist) SelectedTrackIndexes() []int {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
idx := -1
return sharedutil.FilterMapSlice(t.tracksOrigOrder, func(t *util.TrackListModel) (int, bool) {
idx++
@@ -706,12 +662,6 @@ func (t *Tracklist) SelectedTrackIndexes() []int {
})
}
func (t *Tracklist) lenTracks() int {
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
return len(t.tracks)
}
func (t *Tracklist) ColNumber(colName string) int {
i := slices.IndexFunc(t.columns, func(c TracklistColumn) bool {
return c.Name == colName
+5 -2
View File
@@ -3,6 +3,7 @@ package widgets
import (
"sync/atomic"
"fyne.io/fyne/v2"
"github.com/dweymouth/supersonic/backend/mediaprovider"
)
@@ -69,8 +70,10 @@ func (t *TracklistLoader) loadMoreTracks(num int) {
if t.disposed.Load() {
return
}
t.tracklist.AppendTracks(t.trackBuffer)
t.len += len(t.trackBuffer)
fyne.Do(func() {
t.tracklist.AppendTracks(t.trackBuffer)
t.len += len(t.trackBuffer)
})
}
if t.done {
t.trackBuffer = nil
+46
View File
@@ -5,6 +5,7 @@ import (
"image"
"strconv"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
@@ -15,10 +16,31 @@ import (
"fyne.io/fyne/v2/widget"
ttwidget "github.com/dweymouth/fyne-tooltip/widget"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
)
var (
tracklistUpdateCounter = util.NewEventCounter(30)
emptyTrack = util.TrackListModel{
Item: &mediaprovider.Track{
ID: "dummy",
Title: "—",
ArtistIDs: []string{"—"},
ArtistNames: []string{"—"},
Album: "—",
Genres: []string{"—"},
ComposerNames: []string{"—"},
ComposerIDs: []string{"—"},
Comment: "—",
FilePath: "—",
ContentType: "—",
},
}
)
const tracklistThumbnailSize = 48
type TracklistColumn struct {
@@ -151,6 +173,9 @@ type tracklistRowBase struct {
isFavorite bool
playCount int
nextUpdateModel *util.TrackListModel
nextUpdateRowNum int
num *widget.Label
name *ttwidget.RichText
artist *MultiHyperlink
@@ -318,6 +343,26 @@ func (t *tracklistRowBase) TrackID() string {
}
func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int) {
if tracklistUpdateCounter.NumEventsSince(time.Now().Add(-150*time.Millisecond)) > 20 {
t.doUpdate(&emptyTrack, 1)
if t.nextUpdateModel == nil {
// queue to run later
fyne.Do(func() {
if t.nextUpdateModel != nil {
t.doUpdate(t.nextUpdateModel, t.nextUpdateRowNum)
}
t.nextUpdateModel = nil
})
}
t.nextUpdateModel = tm
t.nextUpdateRowNum = rowNum
} else {
t.nextUpdateModel = nil
t.doUpdate(tm, rowNum)
}
}
func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) {
changed := false
if tm.Selected != t.Selected {
t.Selected = tm.Selected
@@ -415,6 +460,7 @@ func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int) {
}
if changed {
tracklistUpdateCounter.Add()
t.Refresh()
}
}