diff --git a/backend/albumiterator.go b/backend/albumiterator.go index a801a53..cb91ac4 100644 --- a/backend/albumiterator.go +++ b/backend/albumiterator.go @@ -51,17 +51,17 @@ var ( func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder) AlbumIterator { switch sort { case AlbumSortRecentlyAdded: - return l.newBaseIter("newest") + return l.newBaseIter("newest", make(map[string]string)) case AlbumSortRecentlyPlayed: - return l.newBaseIter("recent") + return l.newBaseIter("recent", make(map[string]string)) case AlbumSortFrequentlyPlayed: - return l.newBaseIter("frequent") + return l.newBaseIter("frequent", make(map[string]string)) case AlbumSortRandom: return l.newRandomIter() case AlbumSortTitleAZ: - return l.newBaseIter("alphabeticalByName") + return l.newBaseIter("alphabeticalByName", make(map[string]string)) case AlbumSortArtistAZ: - return l.newBaseIter("alphabeticalByArtist") + return l.newBaseIter("alphabeticalByArtist", make(map[string]string)) default: log.Printf("Undefined album sort order: %s", sort) return nil @@ -69,11 +69,19 @@ func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder) AlbumIterator { } func (l *LibraryManager) StarredIter() AlbumIterator { - return l.newBaseIter("starred") + return l.newBaseIter("starred", make(map[string]string)) +} + +func (l *LibraryManager) GenreIter(genre string) AlbumIterator { + return l.newBaseIter("byGenre", map[string]string{"genre": genre}) } func (l *LibraryManager) SearchIter(query string) AlbumIterator { - return l.newSearchIter(query) + return l.newSearchIter(query, func(*subsonic.AlbumID3) bool { return true }) +} + +func (l *LibraryManager) SearchIterWithFilter(query string, filter func(*subsonic.AlbumID3) bool) AlbumIterator { + return l.newSearchIter(query, filter) } func (l *LibraryManager) CacheAlbum(a *subsonic.AlbumID3) { @@ -99,16 +107,18 @@ type baseIter struct { pos int l *LibraryManager s *subsonic.Client + opts map[string]string prefetched []*subsonic.AlbumID3 prefetchedPos int done bool } -func (l *LibraryManager) newBaseIter(listType string) *baseIter { +func (l *LibraryManager) newBaseIter(listType string, opts map[string]string) *baseIter { return &baseIter{ listType: listType, l: l, s: l.s.Server, + opts: opts, } } @@ -128,7 +138,8 @@ func (r *baseIter) Next() *subsonic.AlbumID3 { return a } - albums, err := r.s.GetAlbumList2(r.listType, map[string]string{"size": "20", "offset": strconv.Itoa(r.pos)}) + r.opts["offset"] = strconv.Itoa(r.pos) + albums, err := r.s.GetAlbumList2(r.listType, r.opts) if err != nil { log.Println(err) albums = nil @@ -165,17 +176,19 @@ type searchIter struct { songOffset int l *LibraryManager s *subsonic.Client + filter func(*subsonic.AlbumID3) bool prefetched []*subsonic.AlbumID3 prefetchedPos int albumIDset map[string]bool done bool } -func (l *LibraryManager) newSearchIter(query string) *searchIter { +func (l *LibraryManager) newSearchIter(query string, filter func(*subsonic.AlbumID3) bool) *searchIter { return &searchIter{ query: query, l: l, s: l.s.Server, + filter: filter, albumIDset: make(map[string]bool), } } @@ -260,6 +273,9 @@ func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) { if _, have := s.albumIDset[album.ID]; have { continue } + if !s.filter(album) { + continue + } s.prefetched = append(s.prefetched, album) s.albumIDset[album.ID] = true } diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 4782af3..66984a2 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -93,11 +93,12 @@ type AlbumPageHeader struct { albumID string artistID string + genre string cover *canvas.Image titleLabel *widget.RichText artistLabel *widgets.CustomHyperlink - genreLabel *widget.Label // later custom hyperlink + genreLabel *widgets.CustomHyperlink miscLabel *widget.Label playButton *widget.Button @@ -119,7 +120,10 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { a.artistLabel.OnTapped = func() { page.nav(ArtistRoute(a.artistID)) } - a.genreLabel = widget.NewLabel("") + a.genreLabel = widgets.NewCustomHyperlink() + a.genreLabel.OnTapped = func() { + page.nav(GenreRoute(a.genre)) + } a.miscLabel = widget.NewLabel("") a.playButton = widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { page.onPlayTrackAt(0) @@ -144,6 +148,7 @@ func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageMana a.artistID = album.ArtistID a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name a.artistLabel.SetText(album.Artist) + a.genre = album.Genre a.genreLabel.SetText(album.Genre) a.miscLabel.SetText(formatMiscLabelStr(album)) a.Refresh() diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go new file mode 100644 index 0000000..370a1c4 --- /dev/null +++ b/ui/browsing/genrepage.go @@ -0,0 +1,127 @@ +package browsing + +import ( + "supersonic/backend" + "supersonic/ui/widgets" + + "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" +) + +// TODO: there is a lot of code duplication between this and albumspage. Refactor? +type GenrePage struct { + widget.BaseWidget + + genre string + im *backend.ImageManager + lm *backend.LibraryManager + nav func(Route) + grid *widgets.AlbumGrid + searchGrid *widgets.AlbumGrid + searcher *widgets.Searcher + searchText string + titleDisp *widget.RichText + + OnPlayAlbum func(string, int) + + container *fyne.Container +} + +func NewGenrePage(genre string, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *GenrePage { + g := &GenrePage{ + genre: genre, + lm: lm, + im: im, + nav: nav, + } + g.ExtendBaseWidget(g) + + g.titleDisp = widget.NewRichTextWithText(genre) + g.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ + SizeName: theme.SizeNameHeadingText, + } + iter := g.lm.GenreIter(g.genre) + g.grid = widgets.NewAlbumGrid(iter, g.im.GetAlbumThumbnail, false) + g.grid.OnPlayAlbum = g.onPlayAlbum + g.grid.OnShowArtistPage = g.onShowArtistPage + g.grid.OnShowAlbumPage = g.onShowAlbumPage + g.searcher = widgets.NewSearcher() + g.searcher.OnSearched = g.OnSearched + searchVbox := container.NewVBox(layout.NewSpacer(), g.searcher.Entry, layout.NewSpacer()) + g.container = container.NewBorder( + container.NewHBox(widgets.NewHSpace(9), g.titleDisp, layout.NewSpacer(), searchVbox, widgets.NewHSpace(15)), + nil, + nil, + nil, + g.grid, + ) + return g +} + +func (g *GenrePage) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(g.container) +} + +func (a *GenrePage) Route() Route { + return GenreRoute(a.genre) +} + +func (a *GenrePage) SetPlayAlbumCallback(cb func(string, int)) { + a.OnPlayAlbum = cb +} + +func (g *GenrePage) Reload() { + if g.searchText != "" { + g.doSearch(g.searchText) + } else { + g.grid.Reset(g.lm.GenreIter(g.genre)) + g.grid.Refresh() + } +} + +func (a *GenrePage) onPlayAlbum(albumID string) { + if a.OnPlayAlbum != nil { + a.OnPlayAlbum(albumID, 0) + } +} + +func (a *GenrePage) onShowArtistPage(artistID string) { + a.nav(ArtistRoute(artistID)) +} + +func (a *GenrePage) onShowAlbumPage(albumID string) { + a.nav(AlbumRoute(albumID)) +} + +func (g *GenrePage) OnSearched(query string) { + g.searchText = query + if query == "" { + g.container.Objects[0] = g.grid + if g.searchGrid != nil { + g.searchGrid.Clear() + } + g.Refresh() + return + } + g.doSearch(query) +} + +func (g *GenrePage) doSearch(query string) { + iter := g.lm.SearchIterWithFilter(query, func(al *subsonic.AlbumID3) bool { + return al.Genre == g.genre + }) + if g.searchGrid == nil { + g.searchGrid = widgets.NewAlbumGrid(iter, g.im.GetAlbumThumbnail, false /*showYear*/) + g.searchGrid.OnPlayAlbum = g.onPlayAlbum + g.searchGrid.OnShowAlbumPage = g.onShowAlbumPage + g.searchGrid.OnShowArtistPage = g.onShowArtistPage + } else { + g.searchGrid.Reset(iter) + } + g.container.Objects[0] = g.searchGrid + g.Refresh() +} diff --git a/ui/browsing/router.go b/ui/browsing/router.go index b7ba96e..eac11a8 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -12,6 +12,8 @@ const ( Albums Artist Artists + Genre + Genres Playlist Playlists ) @@ -33,6 +35,10 @@ func AlbumRoute(albumID string) Route { return Route{Page: Album, Arg: albumID} } +func GenreRoute(genre string) Route { + return Route{Page: Genre, Arg: genre} +} + type NavigationHandler interface { SetPage(Page) } @@ -57,6 +63,8 @@ func (r Router) CreatePage(rte Route) Page { 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, r.OpenRoute) + case Genre: + return NewGenrePage(rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) } return nil }