Merge pull request #552 from dweymouth/fyne-2.6-migrate
Fyne 2.6 migrate
This commit is contained in:
+23
-15
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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()
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -72,7 +72,7 @@ func (t *ToastOverlay) showToast(isErr bool, message string) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
t.dismissToast()
|
||||
fyne.Do(t.dismissToast)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user