refactoring package organization

This commit is contained in:
Drew Weymouth
2023-01-04 17:33:44 -08:00
parent 9ac497e25b
commit 7b3aca6027
8 changed files with 36 additions and 29 deletions
+158
View File
@@ -0,0 +1,158 @@
package browsing
import (
"fmt"
"log"
"supersonic/backend"
"supersonic/ui/layouts"
"supersonic/ui/util"
"supersonic/ui/widgets"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic"
)
type AlbumPage struct {
widget.BaseWidget
albumID string
im *backend.ImageManager
lm *backend.LibraryManager
nav func(Route)
header *AlbumPageHeader
tracklist *widgets.Tracklist
nowPlayingID string
container *fyne.Container
OnPlayAlbum func(string, int)
}
func NewAlbumPage(albumID string, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *AlbumPage {
a := &AlbumPage{albumID: albumID, lm: lm, im: im, nav: nav}
a.ExtendBaseWidget(a)
a.header = NewAlbumPageHeader(a)
a.tracklist = widgets.NewTracklist(nil)
a.tracklist.OnPlayTrackAt = a.onPlayTrackAt
a.container = container.NewBorder(
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header),
nil, nil, nil, container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadBottom: 15}, a.tracklist))
a.loadAsync()
return a
}
func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *AlbumPage) SetPlayAlbumCallback(cb func(string, int)) {
a.OnPlayAlbum = cb
}
func (a *AlbumPage) Route() Route {
return AlbumRoute(a.albumID)
}
func (a *AlbumPage) OnSongChange(song *subsonic.Child) {
if song == nil {
a.nowPlayingID = ""
} else {
a.nowPlayingID = song.ID
}
a.tracklist.SetNowPlaying(a.nowPlayingID)
}
func (a *AlbumPage) onPlayTrackAt(tracknum int) {
if a.OnPlayAlbum != nil {
a.OnPlayAlbum(a.albumID, tracknum)
}
}
func (a *AlbumPage) loadAsync() {
go func() {
album, err := a.lm.GetAlbum(a.albumID)
if err != nil {
log.Printf("Failed to get album: %s", err.Error())
return
}
a.header.Update(album, a.im)
a.tracklist.Tracks = album.Song
a.tracklist.SetNowPlaying(a.nowPlayingID)
}()
}
type AlbumPageHeader struct {
widget.BaseWidget
albumID string
artistID string
cover *canvas.Image
titleLabel *widget.RichText
artistLabel *widgets.CustomHyperlink
genreLabel *widget.Label // later custom hyperlink
miscLabel *widget.Label
playButton *widget.Button
container *fyne.Container
}
func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
a := &AlbumPageHeader{}
a.ExtendBaseWidget(a)
a.cover = &canvas.Image{FillMode: canvas.ImageFillContain}
a.cover.SetMinSize(fyne.NewSize(225, 225))
a.titleLabel = widget.NewRichTextWithText("Album Title")
a.titleLabel.Wrapping = fyne.TextTruncate
a.titleLabel.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
SizeName: theme.SizeNameHeadingText,
}
a.artistLabel = widgets.NewCustomHyperlink()
a.artistLabel.OnTapped = func() {
page.nav(ArtistRoute(a.artistID))
}
a.genreLabel = widget.NewLabel("")
a.miscLabel = widget.NewLabel("")
a.playButton = widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() {
page.onPlayTrackAt(0)
})
a.container = container.NewBorder(nil, nil, a.cover, nil,
container.NewVBox(
a.titleLabel,
container.New(&layouts.VboxCustomPadding{ExtraPad: -10}, a.artistLabel, a.genreLabel, a.miscLabel),
container.NewHBox(a.playButton),
),
)
return a
}
func (a *AlbumPageHeader) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageManager) {
a.albumID = album.ID
a.artistID = album.ArtistID
a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name
a.artistLabel.SetText(album.Artist)
a.genreLabel.SetText(album.Genre)
a.miscLabel.SetText(formatMiscLabelStr(album))
a.Refresh()
go func() {
if cover, err := im.GetAlbumThumbnail(album.ID); err == nil {
a.cover.Image = cover
a.cover.Refresh()
} else {
log.Printf("error fetching cover: %v", err)
}
}()
}
func formatMiscLabelStr(a *subsonic.AlbumID3) string {
return fmt.Sprintf("%d · %d tracks · %s", a.Year, a.SongCount, util.SecondsToTimeString(float64(a.Duration)))
}
+138
View File
@@ -0,0 +1,138 @@
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"
)
var _ fyne.Widget = (*AlbumsPage)(nil)
type AlbumsPage struct {
widget.BaseWidget
im *backend.ImageManager
lm *backend.LibraryManager
nav func(Route)
grid *widgets.AlbumGrid
searchGrid *widgets.AlbumGrid
searchText string
titleDisp *widget.RichText
sortOrder *selectWidget
container *fyne.Container
OnPlayAlbum func(string, int)
}
type selectWidget struct {
widget.Select
height float32
}
func NewSelect(options []string, onChanged func(string)) *selectWidget {
s := &selectWidget{
Select: widget.Select{
Options: options,
OnChanged: onChanged,
},
}
s.height = widget.NewSelect(nil, nil).MinSize().Height
s.ExtendBaseWidget(s)
return s
}
func (s *selectWidget) MinSize() fyne.Size {
return fyne.NewSize(170, s.height)
}
func NewAlbumsPage(title string, sortOrder string, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *AlbumsPage {
a := &AlbumsPage{
lm: lm,
im: im,
nav: nav,
}
a.ExtendBaseWidget(a)
a.titleDisp = widget.NewRichTextWithText(title)
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
SizeName: theme.SizeNameHeadingText,
}
a.sortOrder = NewSelect(backend.AlbumSortOrders, nil)
a.sortOrder.Selected = sortOrder
a.sortOrder.OnChanged = a.onSortOrderChanged
sortVbox := container.NewVBox(layout.NewSpacer(), a.sortOrder, layout.NewSpacer())
iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected))
a.grid = widgets.NewAlbumGrid(iter, im.GetAlbumThumbnail, false /*showYear*/)
a.grid.OnPlayAlbum = a.onPlayAlbum
a.grid.OnShowArtistPage = a.onShowArtistPage
a.grid.OnShowAlbumPage = a.onShowAlbumPage
a.container = container.NewBorder(
container.NewHBox(widgets.NewHSpace(9), a.titleDisp, sortVbox),
nil,
nil,
nil,
a.grid,
)
return a
}
func (a *AlbumsPage) OnSearched(query string) {
a.searchText = query
if query == "" {
a.container.Objects[0] = a.grid
if a.searchGrid != nil {
a.searchGrid.Clear()
}
a.Refresh()
return
}
if a.searchGrid == nil {
a.searchGrid = widgets.NewAlbumGrid(a.lm.SearchIter(query), a.im.GetAlbumThumbnail, false /*showYear*/)
a.searchGrid.OnPlayAlbum = a.onPlayAlbum
a.searchGrid.OnShowAlbumPage = a.onShowAlbumPage
a.searchGrid.OnShowArtistPage = a.onShowArtistPage
} else {
a.searchGrid.Reset(a.lm.SearchIter(query))
}
a.container.Objects[0] = a.searchGrid
a.Refresh()
}
func (a *AlbumsPage) Route() Route {
return AlbumsRoute(backend.AlbumSortOrder(a.sortOrder.Selected))
}
func (a *AlbumsPage) SetPlayAlbumCallback(cb func(string, int)) {
a.OnPlayAlbum = cb
}
func (a *AlbumsPage) onPlayAlbum(albumID string) {
if a.OnPlayAlbum != nil {
a.OnPlayAlbum(albumID, 0)
}
}
func (a *AlbumsPage) onShowArtistPage(artistID string) {
a.nav(ArtistRoute(artistID))
}
func (a *AlbumsPage) onShowAlbumPage(albumID string) {
a.nav(AlbumRoute(albumID))
}
func (a *AlbumsPage) onSortOrderChanged(order string) {
a.grid.Reset(a.lm.AlbumsIter(backend.AlbumSortOrder(order)))
if a.searchText == "" {
a.container.Objects[0] = a.grid
a.Refresh()
}
}
func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer {
a.ExtendBaseWidget(a)
return widget.NewSimpleRenderer(a.container)
}
+86
View File
@@ -0,0 +1,86 @@
package browsing
import (
"log"
"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"
)
var _ fyne.Widget = (*ArtistPage)(nil)
type ArtistPage struct {
widget.BaseWidget
artistID string
im *backend.ImageManager
sm *backend.ServerManager
nav func(Route)
grid *widgets.AlbumGrid
titleDisp *widget.RichText
container *fyne.Container
OnPlayAlbum func(string, int)
}
func NewArtistPage(artistID string, sm *backend.ServerManager, im *backend.ImageManager, nav func(Route)) *ArtistPage {
a := &ArtistPage{
artistID: artistID,
sm: sm,
im: im,
nav: nav,
}
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) Route() Route {
return ArtistRoute(a.artistID)
}
func (a *ArtistPage) SetPlayAlbumCallback(cb func(string, int)) {
a.OnPlayAlbum = cb
}
func (a *ArtistPage) onPlayAlbum(albumID string) {
if a.OnPlayAlbum != nil {
a.OnPlayAlbum(albumID, 0)
}
}
func (a *ArtistPage) onShowAlbumPage(albumID string) {
a.nav(AlbumRoute(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 := widgets.NewFixedAlbumGrid(artist.Album, a.im.GetAlbumThumbnail, true /*showYear*/)
ag.OnPlayAlbum = a.onPlayAlbum
ag.OnShowAlbumPage = a.onShowAlbumPage
a.container.Objects[0] = ag
a.container.Refresh()
}()
}
func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer {
a.ExtendBaseWidget(a)
return widget.NewSimpleRenderer(a.container)
}
+172
View File
@@ -0,0 +1,172 @@
package browsing
import (
"image/color"
"supersonic/backend"
"supersonic/ui/widgets"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"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"
)
type Page interface {
fyne.CanvasObject
Route() Route
}
type Searchable interface {
OnSearched(string)
}
type CanPlayAlbum interface {
SetPlayAlbumCallback(func(albumID string, startingTrack int))
}
type CanShowNowPlaying interface {
OnSongChange(song *subsonic.Child)
}
type BrowsingPane struct {
widget.BaseWidget
app *backend.App
curPage Page
forward *widget.Button
back *widget.Button
history []Page
historyIdx int
searchBar *widgets.SearchEntry
pendingSearchLock sync.Mutex
pendingSearch bool
searchGoroutine bool
pageContainer *fyne.Container
container *fyne.Container
}
func NewBrowsingPane(app *backend.App) *BrowsingPane {
b := &BrowsingPane{app: app}
b.ExtendBaseWidget(b)
b.searchBar = widgets.NewSearchEntry()
b.searchBar.OnTextChanged = b.onSearchTextChanged
b.back = widget.NewButtonWithIcon("", theme.NavigateBackIcon(), b.GoBack)
b.forward = widget.NewButtonWithIcon("", theme.NavigateNextIcon(), b.GoForward)
b.app.PlaybackManager.OnSongChange(b.onSongChange)
b.pageContainer = container.NewMax(
canvas.NewRectangle(color.RGBA{R: 24, G: 24, B: 24, A: 255}),
layout.NewSpacer())
b.container = container.NewBorder(
container.NewHBox(b.back, b.forward, b.searchBar),
nil, nil, nil, b.pageContainer)
return b
}
func (b *BrowsingPane) SetPage(p Page) {
if b.doSetPage(p) {
b.addPageToHistory(p)
}
}
func (b *BrowsingPane) doSetPage(p Page) bool {
if b.curPage != nil && b.curPage.Route() == p.Route() {
return false
}
b.curPage = p
if pa, ok := p.(CanPlayAlbum); ok {
pa.SetPlayAlbumCallback(func(albumID string, firstTrack int) {
_ = b.app.PlaybackManager.PlayAlbum(albumID, firstTrack)
})
}
if np, ok := p.(CanShowNowPlaying); ok {
np.OnSongChange(b.app.PlaybackManager.NowPlaying())
}
_, s := p.(Searchable)
b.searchBar.Hidden = !s
b.pageContainer.Objects[1] = p
b.Refresh()
return true
}
func (b *BrowsingPane) onSongChange(song *subsonic.Child) {
if b.curPage == nil {
return
}
if p, ok := b.curPage.(CanShowNowPlaying); ok {
p.OnSongChange(song)
}
}
func (b *BrowsingPane) addPageToHistory(p Page) {
b.history = b.history[:b.historyIdx]
b.history = append(b.history, p)
b.historyIdx++
}
func (b *BrowsingPane) GoBack() {
if b.historyIdx > 1 {
b.historyIdx -= 1
b.doSetPage(b.history[b.historyIdx-1])
}
}
func (b *BrowsingPane) GoForward() {
if b.historyIdx < len(b.history) {
b.historyIdx++
b.doSetPage(b.history[b.historyIdx-1])
}
}
func (b *BrowsingPane) onSearchTextChanged(text string) {
if text == "" {
b.sendSearch("")
return
}
b.pendingSearchLock.Lock()
defer b.pendingSearchLock.Unlock()
b.pendingSearch = true
if !b.searchGoroutine {
go b.waitAndSearch()
b.searchGoroutine = true
}
}
func (b *BrowsingPane) waitAndSearch() {
t := time.NewTicker(200 * time.Millisecond)
var getReadyToSearch bool
var done bool
for !done {
<-t.C
b.pendingSearchLock.Lock()
if b.pendingSearch {
getReadyToSearch = true
b.pendingSearch = false
} else if getReadyToSearch {
b.sendSearch(b.searchBar.Text)
t.Stop()
b.searchGoroutine = false
done = true
}
b.pendingSearchLock.Unlock()
}
}
func (b *BrowsingPane) sendSearch(query string) {
if s, ok := b.curPage.(Searchable); ok {
s.OnSearched(query)
}
}
func (b *BrowsingPane) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(b.container)
}
+66
View File
@@ -0,0 +1,66 @@
package browsing
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}
}
func AlbumRoute(albumID string) Route {
return Route{Page: Album, Arg: albumID}
}
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 Album:
return NewAlbumPage(rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute)
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, r.OpenRoute)
}
return nil
}
func (r Router) OpenRoute(rte Route) {
r.Nav.SetPage(r.CreatePage(rte))
}