diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index ac9ef47..708c449 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -2,7 +2,7 @@ package backend import ( "context" - "gomuse/player" + "supersonic/player" "time" subsonic "github.com/dweymouth/go-subsonic" diff --git a/go.mod b/go.mod index 6750755..9b9549e 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,18 @@ -module gomuse +module supersonic go 1.19 require ( fyne.io/fyne/v2 v2.2.4 + github.com/20after4/configdir v0.1.1 github.com/bluele/gcache v0.0.2 github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863 + github.com/pelletier/go-toml v1.9.3 github.com/wildeyedskies/go-mpv v0.0.0-20221204042335-e8961dc66756 ) require ( fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 // indirect - github.com/20after4/configdir v0.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect @@ -39,4 +40,4 @@ require ( honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect ) -replace fyne.io/fyne/v2 v2.2.4 => github.com/dweymouth/fyne/v2 v2.2.5-0.20221217035723-f7a791ca52e4 +replace fyne.io/fyne/v2 v2.2.4 => github.com/dweymouth/fyne/v2 v2.2.5-0.20221220015227-bd9b07f93ac4 diff --git a/go.sum b/go.sum index 4ec5909..3d34b40 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dweymouth/fyne/v2 v2.2.5-0.20221217035723-f7a791ca52e4 h1:BoEOyrmFpJ5JsMnd/kVDFnzPE5kgXvMhK7C+f80ISMw= -github.com/dweymouth/fyne/v2 v2.2.5-0.20221217035723-f7a791ca52e4/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= +github.com/dweymouth/fyne/v2 v2.2.5-0.20221220015227-bd9b07f93ac4 h1:Q/+eS7II+u1xjnWHXMgQ9G1pssn/Yi6BoxY6iJCVsfU= +github.com/dweymouth/fyne/v2 v2.2.5-0.20221220015227-bd9b07f93ac4/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863 h1:bOWMpFJ9zY839T1EngQ5nxVCiMlatPtpGkg/yHK6szg= github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -236,6 +236,7 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= diff --git a/main.go b/main.go index c4bc10b..272a128 100644 --- a/main.go +++ b/main.go @@ -3,28 +3,42 @@ package main import ( "context" "fmt" - "gomuse/backend" - "gomuse/player" - "gomuse/ui" "log" "net/http" + "supersonic/backend" + "supersonic/player" + "supersonic/ui" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" "github.com/20after4/configdir" "github.com/dweymouth/go-subsonic" ) -const appname = "gomuse" +const ( + appname = "supersonic" + configFile = "config.toml" +) func main() { ctx, cancel := context.WithCancel(context.Background()) - cachePath := configdir.LocalCache(appname) - log.Println(cachePath) p := player.NewWithClientName(appname) - fmt.Println(p.Init()) + if err := p.Init(); err != nil { + log.Fatalf("failed to initialize mpv player: %s", err.Error()) + } + + myApp := app.New() + myWindow := myApp.NewWindow(appname) + + b := ui.NewBottomPanel(p) + c := container.NewBorder(nil, b, nil, nil, &layout.Spacer{}) + + myWindow.SetContent(c) + myWindow.Resize(fyne.NewSize(1000, 800)) + myWindow.Show() s := &subsonic.Client{ Client: &http.Client{}, @@ -33,21 +47,16 @@ func main() { ClientName: appname, } - lm := backend.NewLibraryManager(s) - pm := backend.NewPlaybackManager(ctx, s, p) - im := backend.NewImageManager(s, configdir.LocalCache(appname, "covers")) - if err := s.Authenticate("***REMOVED***"); err != nil { fmt.Printf("error authenticating: %v\n", err) } - myApp := app.New() - myWindow := myApp.NewWindow(appname) - - b := ui.NewBottomPanel(p, pm, im) - ag := ui.NewAlbumGrid(lm.RecentlyAddedIter(), pm, im) - c := container.NewBorder(nil, b, nil, nil, ag) + lm := backend.NewLibraryManager(s) + pm := backend.NewPlaybackManager(ctx, s, p) + im := backend.NewImageManager(s, configdir.LocalCache(appname, "covers")) + b.ImageManager = im + b.SetPlaybackManager(pm) pm.OnSongChange(func(song *subsonic.Child) { if song == nil { myWindow.SetTitle("gomuse") @@ -56,10 +65,8 @@ func main() { myWindow.SetTitle(song.Title) }) - myWindow.SetContent(c) - myWindow.Resize(fyne.NewSize(1350, 1000)) - myWindow.Show() - + ag := ui.NewAlbumGrid(lm.RecentlyAddedIter(), pm, im) + myWindow.SetContent(container.NewBorder(nil, b, nil, nil, ag)) myApp.Run() cancel() p.Destroy() diff --git a/ui/albumcard.go b/ui/albumcard.go index b9e4b97..333316e 100644 --- a/ui/albumcard.go +++ b/ui/albumcard.go @@ -5,7 +5,7 @@ import ( "image" "log" - "gomuse/ui/layout" + "supersonic/ui/layout" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" diff --git a/ui/albumgrid.go b/ui/albumgrid.go index 23cd7ec..5bbe986 100644 --- a/ui/albumgrid.go +++ b/ui/albumgrid.go @@ -2,7 +2,7 @@ package ui import ( "context" - "gomuse/backend" + "supersonic/backend" "fyne.io/fyne/v2" "fyne.io/fyne/v2/widget" @@ -12,18 +12,20 @@ import ( type AlbumGrid struct { widget.BaseWidget - grid *widget.GridWrapList - albums []*subsonic.AlbumID3 - iter backend.AlbumIterator - fetching bool - done bool + grid *widget.GridWrapList + albums []*subsonic.AlbumID3 + iter backend.AlbumIterator + imageManager *backend.ImageManager + fetching bool + done bool } var _ fyne.Widget = (*AlbumGrid)(nil) func NewAlbumGrid(iter backend.AlbumIterator, pm *backend.PlaybackManager, im *backend.ImageManager) *AlbumGrid { ag := &AlbumGrid{ - iter: iter, + iter: iter, + imageManager: im, } ag.ExtendBaseWidget(ag) @@ -42,38 +44,7 @@ func NewAlbumGrid(iter backend.AlbumIterator, pm *backend.PlaybackManager, im *b // update func func(itemID int, obj fyne.CanvasObject) { ac := obj.(*AlbumCard) - album := ag.albums[itemID] - if ac.PrevAlbumID == album.ID { - // nothing to do - return - } - ac.Update(album) - ac.PrevAlbumID = album.ID - // TODO: set image to a placeholder before spinning off async fetch - // cancel any previous image fetch - if ac.ImgLoadCancel != nil { - ac.ImgLoadCancel() - ac.ImgLoadCancel = nil - } - ctx, cancel := context.WithCancel(context.Background()) - go func(ctx context.Context) { - i, err := im.GetAlbumThumbnail(album.ID) - select { - case <-ctx.Done(): - return - default: - if err == nil { - ac.Cover.SetImage(i) - ac.Refresh() - } - } - }(ctx) - ac.ImgLoadCancel = cancel - - // TODO: remove magic number 10 - if !ag.done && !ag.fetching && itemID > len(ag.albums)-10 { - ag.fetchMoreAlbums(10) - } + ag.doUpdateAlbumCard(itemID, ac) }, ) ag.grid = g @@ -83,7 +54,45 @@ func NewAlbumGrid(iter backend.AlbumIterator, pm *backend.PlaybackManager, im *b return ag } +func (ag *AlbumGrid) doUpdateAlbumCard(albumIdx int, ac *AlbumCard) { + album := ag.albums[albumIdx] + if ac.PrevAlbumID == album.ID { + // nothing to do + return + } + ac.Update(album) + ac.PrevAlbumID = album.ID + // TODO: set image to a placeholder before spinning off async fetch + // cancel any previous image fetch + if ac.ImgLoadCancel != nil { + ac.ImgLoadCancel() + ac.ImgLoadCancel = nil + } + ctx, cancel := context.WithCancel(context.Background()) + go func(ctx context.Context) { + i, err := ag.imageManager.GetAlbumThumbnail(album.ID) + select { + case <-ctx.Done(): + return + default: + if err == nil { + ac.Cover.SetImage(i) + ac.Refresh() + } + } + }(ctx) + ac.ImgLoadCancel = cancel + + // TODO: remove magic number 10 + if !ag.done && !ag.fetching && albumIdx > len(ag.albums)-10 { + ag.fetchMoreAlbums(10) + } +} + func (a *AlbumGrid) fetchMoreAlbums(count int) { + if a.iter == nil { + a.done = true + } i := 0 a.fetching = true a.iter.NextN(count, func(al *subsonic.AlbumID3) { diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 04095f7..ce1ea2e 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -1,13 +1,15 @@ package ui import ( - "gomuse/backend" - "gomuse/player" + "image" + "supersonic/backend" + "supersonic/player" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" "github.com/dweymouth/go-subsonic" ) @@ -48,18 +50,51 @@ func (b *bottomPanelLayout) Layout(_ []fyne.CanvasObject, size fyne.Size) { } } -func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, im *backend.ImageManager) fyne.CanvasObject { - n := NewNowPlayingCard() - c := NewPlayerControls(p, pm) +type BottomPanel struct { + widget.BaseWidget - pm.OnSongChange(func(song *subsonic.Child) { - if song == nil { - n.Update("", "", "", nil) - } else { - im, _ := im.GetAlbumThumbnail(song.AlbumID) - n.Update(song.Title, song.Artist, song.Album, im) - } - }) + ImageManager *backend.ImageManager - return container.New(newBottomPanelLayout(500, n, c, nil), n, c) + playbackManager *backend.PlaybackManager + + nowPlaying *NowPlayingCard + controls *PlayerControls + container *fyne.Container +} + +var _ fyne.Widget = (*BottomPanel)(nil) + +func NewBottomPanel(p *player.Player) *BottomPanel { + bp := &BottomPanel{} + bp.ExtendBaseWidget(bp) + + bp.nowPlaying = NewNowPlayingCard() + bp.controls = NewPlayerControls(p) + + bp.container = container.New(newBottomPanelLayout(500, bp.nowPlaying, bp.controls, nil), bp.nowPlaying, bp.controls) + return bp +} + +func (bp *BottomPanel) SetPlaybackManager(pm *backend.PlaybackManager) { + bp.playbackManager = pm + bp.controls.SetPlaybackManager(pm) + pm.OnSongChange(func(song *subsonic.Child) { + bp.onSongChange(song) + }) +} + +func (bp *BottomPanel) onSongChange(song *subsonic.Child) { + if song == nil { + bp.nowPlaying.Update("", "", "", nil) + } else { + var im image.Image + if bp.ImageManager != nil { + im, _ = bp.ImageManager.GetAlbumThumbnail(song.AlbumID) + } + bp.nowPlaying.Update(song.Title, song.Artist, song.Album, im) + } +} + +func (bp *BottomPanel) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(bp.container) } diff --git a/ui/nowplayingcard.go b/ui/nowplayingcard.go index 2d4d585..4220939 100644 --- a/ui/nowplayingcard.go +++ b/ui/nowplayingcard.go @@ -3,7 +3,7 @@ package ui import ( "image" - "gomuse/ui/layout" + "supersonic/ui/layout" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" diff --git a/ui/playercontrols.go b/ui/playercontrols.go index 1253bad..cde90b1 100644 --- a/ui/playercontrols.go +++ b/ui/playercontrols.go @@ -2,8 +2,8 @@ package ui import ( "fmt" - "gomuse/backend" - "gomuse/player" + "supersonic/backend" + "supersonic/player" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" @@ -67,14 +67,15 @@ type PlayerControls struct { container *fyne.Container totalTime float64 + player *player.Player playbackManager *backend.PlaybackManager } var _ fyne.Widget = (*PlayerControls)(nil) // NewPlayerControls sets up the seek bar, and transport buttons, and returns the encompassing Container. -func NewPlayerControls(p *player.Player, pm *backend.PlaybackManager) *PlayerControls { - pc := &PlayerControls{playbackManager: pm} +func NewPlayerControls(p *player.Player) *PlayerControls { + pc := &PlayerControls{player: p} pc.ExtendBaseWidget(pc) pc.slider = NewTrackPosSlider() @@ -115,14 +116,19 @@ func NewPlayerControls(p *player.Player, pm *backend.PlaybackManager) *PlayerCon c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, pc.slider) pc.container = container.NewVBox(c, b) - pm.OnPlayTimeUpdate(func(curTime float64, totalTime float64) { - pc.doPlayTimeUpdate(curTime, totalTime) - }) - return pc } +func (pc *PlayerControls) SetPlaybackManager(pm *backend.PlaybackManager) { + pc.playbackManager = pm + pm.OnPlayTimeUpdate(func(curTime float64, totalTime float64) { + pc.doPlayTimeUpdate(curTime, totalTime) + }) +} + func (pc *PlayerControls) doPlayTimeUpdate(curTime, totalTime float64) { + // TODO: there is a bug with very long tracks (~20min) where the + // curtime label will bounce back and forth +- 1sec (rounding issue?) pc.totalTime = totalTime if !pc.playbackManager.IsSeeking() { v := 0.0