diff --git a/ui/albumgrid.go b/ui/albumgrid.go index 4b5de41..6a1f3ea 100644 --- a/ui/albumgrid.go +++ b/ui/albumgrid.go @@ -22,12 +22,24 @@ type AlbumGrid struct { fetching bool done bool - imageFetcher ImageFetcher - OnPlayAlbum func(string) + imageFetcher ImageFetcher + OnPlayAlbum func(string) + OnShowArtistPage func(string) } var _ fyne.Widget = (*AlbumGrid)(nil) +func NewFixedAlbumGrid(albums []*subsonic.AlbumID3, fetch ImageFetcher) *AlbumGrid { + ag := &AlbumGrid{ + albums: albums, + done: true, + imageFetcher: fetch, + } + ag.ExtendBaseWidget(ag) + ag.createGridWrapList() + return ag +} + func NewAlbumGrid(iter backend.AlbumIterator, fetch ImageFetcher) *AlbumGrid { ag := &AlbumGrid{ iter: iter, @@ -35,27 +47,7 @@ func NewAlbumGrid(iter backend.AlbumIterator, fetch ImageFetcher) *AlbumGrid { } ag.ExtendBaseWidget(ag) - g := widget.NewGridWrapList( - func() int { - return len(ag.albums) - }, - // create func - func() fyne.CanvasObject { - ac := widgets.NewAlbumCard() - ac.OnPlay = func() { - if ag.OnPlayAlbum != nil { - ag.OnPlayAlbum(ac.AlbumID()) - } - } - return ac - }, - // update func - func(itemID int, obj fyne.CanvasObject) { - ac := obj.(*widgets.AlbumCard) - ag.doUpdateAlbumCard(itemID, ac) - }, - ) - ag.grid = g + ag.createGridWrapList() // fetch initial albums ag.fetchMoreAlbums(36) @@ -75,6 +67,35 @@ func (ag *AlbumGrid) Reset(iter backend.AlbumIterator) { ag.fetchMoreAlbums(36) } +func (ag *AlbumGrid) createGridWrapList() { + g := widget.NewGridWrapList( + func() int { + return len(ag.albums) + }, + // create func + func() fyne.CanvasObject { + ac := widgets.NewAlbumCard() + ac.OnPlay = func() { + if ag.OnPlayAlbum != nil { + ag.OnPlayAlbum(ac.AlbumID()) + } + } + ac.OnShowArtistPage = func() { + if ag.OnShowArtistPage != nil { + ag.OnShowArtistPage(ac.ArtistID()) + } + } + return ac + }, + // update func + func(itemID int, obj fyne.CanvasObject) { + ac := obj.(*widgets.AlbumCard) + ag.doUpdateAlbumCard(itemID, ac) + }, + ) + ag.grid = g +} + func (ag *AlbumGrid) doUpdateAlbumCard(albumIdx int, ac *widgets.AlbumCard) { album := ag.albums[albumIdx] if ac.PrevAlbumID == album.ID { diff --git a/ui/albumspage.go b/ui/albumspage.go index e891459..a36cbb6 100644 --- a/ui/albumspage.go +++ b/ui/albumspage.go @@ -18,6 +18,7 @@ type AlbumsPage struct { im *backend.ImageManager lm *backend.LibraryManager + nav func(Route) grid *AlbumGrid searchGrid *AlbumGrid searchText string @@ -48,10 +49,11 @@ func (s *selectWidget) MinSize() fyne.Size { return fyne.NewSize(170, s.height) } -func NewAlbumsPage(title string, lm *backend.LibraryManager, im *backend.ImageManager) *AlbumsPage { +func NewAlbumsPage(title string, sortOrder string, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *AlbumsPage { a := &AlbumsPage{ - lm: lm, - im: im, + lm: lm, + im: im, + nav: nav, } a.ExtendBaseWidget(a) @@ -60,11 +62,12 @@ func NewAlbumsPage(title string, lm *backend.LibraryManager, im *backend.ImageMa SizeName: theme.SizeNameHeadingText, } a.sortOrder = NewSelect(backend.AlbumSortOrders, nil) - a.sortOrder.Selected = a.sortOrder.Options[0] + a.sortOrder.Selected = sortOrder a.sortOrder.OnChanged = a.onSortOrderChanged sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer()) a.grid = NewAlbumGrid(lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected)), im.GetAlbumThumbnail) a.grid.OnPlayAlbum = a.onPlayAlbum + a.grid.OnShowArtistPage = a.onShowArtistPage a.container = container.NewBorder( container.NewHBox(widgets.NewHSpace(15), a.titleDisp, sortVbox), nil, @@ -95,12 +98,20 @@ func (a *AlbumsPage) OnSearched(query string) { a.Refresh() } +func (a *AlbumsPage) SetPlayAlbumCallback(cb func(string)) { + a.OnPlayAlbum = cb +} + func (a *AlbumsPage) onPlayAlbum(albumID string) { if a.OnPlayAlbum != nil { a.OnPlayAlbum(albumID) } } +func (a *AlbumsPage) onShowArtistPage(artistID string) { + a.nav(ArtistRoute(artistID)) +} + func (a *AlbumsPage) onSortOrderChanged(order string) { a.grid.Reset(a.lm.AlbumsIter(backend.AlbumSortOrder(order))) if a.searchText == "" { diff --git a/ui/artistpage.go b/ui/artistpage.go new file mode 100644 index 0000000..80232c7 --- /dev/null +++ b/ui/artistpage.go @@ -0,0 +1,73 @@ +package ui + +import ( + "log" + "supersonic/backend" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var _ fyne.Widget = (*ArtistPage)(nil) + +type ArtistPage struct { + widget.BaseWidget + + artistID string + im *backend.ImageManager + sm *backend.ServerManager + grid *AlbumGrid + titleDisp *widget.RichText + container *fyne.Container + OnPlayAlbum func(string) +} + +func NewArtistPage(artistID string, sm *backend.ServerManager, im *backend.ImageManager) *ArtistPage { + a := &ArtistPage{ + artistID: artistID, + sm: sm, + im: im, + } + a.ExtendBaseWidget(a) + a.titleDisp = widget.NewRichTextWithText("Artist") + a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ + SizeName: theme.SizeNameHeadingText, + } + a.container = container.NewBorder(a.titleDisp, nil, nil, nil, layout.NewSpacer()) + a.loadAsync() + return a +} + +func (a *ArtistPage) SetPlayAlbumCallback(cb func(string)) { + a.OnPlayAlbum = cb +} + +func (a *ArtistPage) onPlayAlbum(albumID string) { + if a.OnPlayAlbum != nil { + a.OnPlayAlbum(albumID) + } +} + +func (a *ArtistPage) loadAsync() { + go func() { + artist, err := a.sm.Server.GetArtist(a.artistID) + if err != nil { + log.Printf("Failed to get artist: %s", err.Error()) + return + } + a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name + a.titleDisp.Refresh() + ag := NewFixedAlbumGrid(artist.Album, a.im.GetAlbumThumbnail) + ag.OnPlayAlbum = a.onPlayAlbum + a.container.Objects[0] = ag + a.container.Refresh() + }() +} + +func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { + a.ExtendBaseWidget(a) + return widget.NewSimpleRenderer(a.container) +} diff --git a/ui/browsingpane.go b/ui/browsingpane.go index 110a97e..352bceb 100644 --- a/ui/browsingpane.go +++ b/ui/browsingpane.go @@ -1,6 +1,7 @@ package ui import ( + "supersonic/backend" "supersonic/ui/widgets" "sync" "time" @@ -18,9 +19,15 @@ type Searchable interface { OnSearched(string) } +type CanPlayAlbum interface { + SetPlayAlbumCallback(func(albumID string)) +} + type BrowsingPane struct { widget.BaseWidget + app *backend.App + searchBar *widgets.SearchEntry pendingSearchLock sync.Mutex pendingSearch bool @@ -34,8 +41,8 @@ type blankPage struct { widget.Separator } -func NewBrowsingPane() *BrowsingPane { - b := &BrowsingPane{} +func NewBrowsingPane(app *backend.App) *BrowsingPane { + b := &BrowsingPane{app: app} b.ExtendBaseWidget(b) b.searchBar = widgets.NewSearchEntry() b.searchBar.OnTextChanged = b.onSearchTextChanged @@ -48,6 +55,13 @@ func NewBrowsingPane() *BrowsingPane { func (b *BrowsingPane) SetPage(p Page) { b.curPage = p + if pa, ok := p.(CanPlayAlbum); ok { + pa.SetPlayAlbumCallback(func(albumID string) { + _ = b.app.PlaybackManager.PlayAlbum(albumID) + }) + } + _, s := p.(Searchable) + b.searchBar.Hidden = !s b.container.Objects[0] = p b.Refresh() } diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 686637f..def8fc5 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -13,6 +13,7 @@ import ( type MainWindow struct { Window fyne.Window + Router Router BrowsingPane *BrowsingPane BottomPanel *BottomPanel @@ -22,9 +23,10 @@ type MainWindow struct { func NewMainWindow(fyneApp fyne.App, appName string, app *backend.App) MainWindow { m := MainWindow{ Window: fyneApp.NewWindow(appName), - BrowsingPane: NewBrowsingPane(), + BrowsingPane: NewBrowsingPane(app), BottomPanel: NewBottomPanel(app.Player), } + m.Router = NewRouter(app, m.BrowsingPane) m.BottomPanel.SetPlaybackManager(app.PlaybackManager) m.BottomPanel.ImageManager = app.ImageManager m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) @@ -38,11 +40,7 @@ func NewMainWindow(fyneApp fyne.App, appName string, app *backend.App) MainWindo m.Window.SetTitle(song.Title) }) app.ServerManager.OnServerConnected(func() { - ap := NewAlbumsPage("Albums", app.LibraryManager, app.ImageManager) - ap.OnPlayAlbum = func(albumID string) { - _ = app.PlaybackManager.PlayAlbum(albumID) - } - m.BrowsingPane.SetPage(ap) + m.Router.OpenRoute(AlbumsRoute(backend.AlbumSortFrequentlyPlayed)) }) return m } diff --git a/ui/router.go b/ui/router.go new file mode 100644 index 0000000..0ea6280 --- /dev/null +++ b/ui/router.go @@ -0,0 +1,58 @@ +package ui + +import "supersonic/backend" + +type PageName int + +const ( + Blank PageName = iota + Album + Albums + Artist + Artists + Playlist + Playlists +) + +type Route struct { + Page PageName + Arg string +} + +func AlbumsRoute(sortOrder backend.AlbumSortOrder) Route { + return Route{Page: Albums, Arg: string(sortOrder)} +} + +func ArtistRoute(artistID string) Route { + return Route{Page: Artist, Arg: artistID} +} + +type NavigationHandler interface { + SetPage(Page) +} + +type Router struct { + App *backend.App + Nav NavigationHandler +} + +func NewRouter(app *backend.App, nav NavigationHandler) Router { + return Router{ + App: app, + Nav: nav, + } +} + +func (r Router) CreatePage(rte Route) Page { + switch rte.Page { + case Albums: + return NewAlbumsPage("Albums", rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) + case Artist: + return NewArtistPage(rte.Arg, r.App.ServerManager, r.App.ImageManager) + } + return nil +} + +func (r Router) OpenRoute(rte Route) { + r.Nav.SetPage(r.CreatePage(rte)) +} diff --git a/ui/widgets/albumcard.go b/ui/widgets/albumcard.go index 354dce8..7a49536 100644 --- a/ui/widgets/albumcard.go +++ b/ui/widgets/albumcard.go @@ -62,8 +62,9 @@ type AlbumCard struct { widget.BaseWidget albumID string + artistID string title *widget.Label - artist *widget.Label + artist *CustomHyperlink container *fyne.Container // updated by AlbumGrid @@ -73,7 +74,9 @@ type AlbumCard struct { PrevAlbumID string ImgLoadCancel context.CancelFunc - OnPlay func() + OnPlay func() + OnShowAlbumPage func() + OnShowArtistPage func() } func (a *AlbumCard) MouseIn(*desktop.MouseEvent) {} @@ -85,7 +88,7 @@ func (a *AlbumCard) MouseMoved(*desktop.MouseEvent) {} func NewAlbumCard() *AlbumCard { a := &AlbumCard{ title: widget.NewLabel(""), - artist: widget.NewLabel(""), + artist: NewCustomHyperlink(), Cover: newAlbumCover(), } a.ExtendBaseWidget(a) @@ -94,8 +97,12 @@ func NewAlbumCard() *AlbumCard { a.OnPlay() } } + a.artist.OnTapped = func() { + if a.OnShowArtistPage != nil { + a.OnShowArtistPage() + } + } a.title.Wrapping = fyne.TextTruncate - a.artist.Wrapping = fyne.TextTruncate a.title.TextStyle = fyne.TextStyle{Bold: true} a.createContainer() @@ -113,12 +120,17 @@ func (a *AlbumCard) Update(al *subsonic.AlbumID3) { a.title.SetText(al.Name) a.artist.SetText(al.Artist) a.albumID = al.ID + a.artistID = al.ArtistID } func (a *AlbumCard) AlbumID() string { return a.albumID } +func (a *AlbumCard) ArtistID() string { + return a.artistID +} + func (a *AlbumCard) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(a.container) } diff --git a/ui/widgets/customhyperlink.go b/ui/widgets/customhyperlink.go index 6ec42d0..99367fe 100644 --- a/ui/widgets/customhyperlink.go +++ b/ui/widgets/customhyperlink.go @@ -44,6 +44,8 @@ type CustomHyperlink struct { widget.BaseWidget h *hyperlinkWrapper + OnTapped func() + container *fyne.Container minSize fyne.Size } @@ -52,6 +54,11 @@ func NewCustomHyperlink() *CustomHyperlink { c := &CustomHyperlink{ h: newHyperlinkWrapper(200), } + c.h.h.OnTapped = func() { + if c.OnTapped != nil { + c.OnTapped() + } + } c.ExtendBaseWidget(c) c.container = container.NewHBox(c.h, layout.NewSpacer()) return c