Merge branch 'main' into feature/sort-tracklist-cols

This commit is contained in:
Drew Weymouth
2023-05-24 09:18:59 -07:00
25 changed files with 607 additions and 447 deletions
+1
View File
@@ -20,6 +20,7 @@ type AlbumWithTracks struct {
type Artist struct {
ID string
CoverArtID string
Name string
Favorite bool
AlbumCount int
@@ -311,6 +311,7 @@ func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist {
}
return &mediaprovider.Artist{
ID: ar.ID,
CoverArtID: ar.CoverArt,
Name: ar.Name,
Favorite: !ar.Starred.IsZero(),
AlbumCount: ar.AlbumCount,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

-5
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -1,7 +1,6 @@
#!/bin/sh
fyne bundle -package res -prefix Res appicon-256.png > bundled.go
fyne bundle -append -prefix Res albumplaceholder.png >> bundled.go
fyne bundle -append -prefix Res icons/freepik/disc.png >> bundled.go
fyne bundle -append -prefix Res icons/freepik/disc-invert.png >> bundled.go
fyne bundle -append -prefix Res icons/freepik/headphones.png >> bundled.go
+1 -1
View File
@@ -164,7 +164,7 @@ type AlbumPageHeader struct {
func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
a := &AlbumPageHeader{page: page}
a.ExtendBaseWidget(a)
a.cover = widgets.NewTappableImage(func() { go a.showPopUpCover() })
a.cover = widgets.NewTappableImage(func(*fyne.PointEvent) { go a.showPopUpCover() })
a.cover.FillMode = canvas.ImageFillContain
a.cover.SetMinSize(fyne.NewSize(225, 225))
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets"
@@ -75,7 +76,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller,
}
a.sortOrder.Selected = cfg.SortOrder
iter := mp.IterateAlbums(a.sortOrder.Selected, a.filter)
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im)
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im, myTheme.AlbumIcon)
contr.ConnectAlbumGridActions(a.grid)
a.createSearchAndFilter()
a.createContainer(false)
@@ -190,7 +191,7 @@ func (a *AlbumsPage) Save() SavedPage {
func (a *AlbumsPage) doSearch(query string) {
iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter))
if a.searchGrid == nil {
a.searchGrid = widgets.NewGridView(iter, a.im)
a.searchGrid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon)
a.contr.ConnectAlbumGridActions(a.searchGrid)
} else {
a.searchGrid.Reset(iter)
+6 -14
View File
@@ -7,7 +7,6 @@ import (
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
@@ -131,15 +130,6 @@ func (a *ArtistPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track
}
}
func (a *ArtistPage) playAllTracks() {
if a.artistInfo != nil { // page loaded
for i, album := range a.artistInfo.Albums {
a.pm.LoadAlbum(album.ID, i > 0 /*append*/, false /*shuffle*/)
}
a.pm.PlayFromBeginning()
}
}
func (a *ArtistPage) playArtistRadio() {
go a.pm.PlaySimilarSongs(a.artistID)
}
@@ -180,7 +170,7 @@ func (a *ArtistPage) showAlbumGrid() {
Secondary: strconv.Itoa(al.Year),
}
})
a.albumGrid = widgets.NewFixedGridView(model, a.im)
a.albumGrid = widgets.NewFixedGridView(model, a.im, myTheme.AlbumIcon)
a.contr.ConnectAlbumGridActions(a.albumGrid)
}
a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid
@@ -266,14 +256,16 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
SizeName: theme.SizeNameHeadingText,
}
a.artistImage = widgets.NewImagePlaceholder(res.ResPeopleInvertPng, 225)
a.artistImage.OnTapped = func() {
a.artistImage = widgets.NewImagePlaceholder(myTheme.ArtistIcon, 225)
a.artistImage.OnTapped = func(*fyne.PointEvent) {
if im := a.artistImage.Image(); im != nil {
a.artistPage.contr.ShowPopUpImage(im)
}
}
a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() })
a.playBtn = widget.NewButtonWithIcon("Play Discography", theme.MediaPlayIcon(), page.playAllTracks)
a.playBtn = widget.NewButtonWithIcon("Play Discography", theme.MediaPlayIcon(), func() {
go a.artistPage.contr.PlayArtistDiscography(a.artistID, false /*shuffle*/)
})
a.playRadioBtn = widget.NewButtonWithIcon(" Play Artist Radio", myTheme.ShuffleIcon, page.playArtistRadio)
a.biographyDisp.Wrapping = fyne.TextWrapWord
a.ExtendBaseWidget(a)
-183
View File
@@ -1,183 +0,0 @@
package browsing
import (
"log"
"strings"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/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 ArtistsGenresPage struct {
widget.BaseWidget
isGenresPage bool
contr *controller.Controller
mp mediaprovider.MediaProvider
model []widgets.ArtistGenreListItemModel
list *widgets.ArtistGenreList
titleDisp *widget.RichText
container *fyne.Container
searcher *widgets.SearchEntry
}
func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, mp mediaprovider.MediaProvider) *ArtistsGenresPage {
return newArtistsGenresPage(isGenresPage, contr, mp, "")
}
func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, mp mediaprovider.MediaProvider, searchText string) *ArtistsGenresPage {
title := "Artists"
if isGenresPage {
title = "Genres"
}
a := &ArtistsGenresPage{
isGenresPage: isGenresPage,
contr: contr,
mp: mp,
titleDisp: widget.NewRichTextWithText(title),
}
a.ExtendBaseWidget(a)
a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText
a.list = widgets.NewArtistGenreList(nil)
a.list.ShowAlbumCount = true
a.list.ShowTrackCount = isGenresPage
a.list.OnNavTo = func(id string) {
if a.isGenresPage {
a.contr.NavigateTo(controller.GenreRoute(id))
} else {
a.contr.NavigateTo(controller.ArtistRoute(id))
}
}
a.searcher = widgets.NewSearchEntry()
a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText
a.buildContainer()
go a.load(searchText != "")
return a
}
// should be called asynchronously
func (a *ArtistsGenresPage) load(searchOnLoad bool) {
if a.isGenresPage {
genres, err := a.mp.GetGenres()
if err != nil {
log.Printf("error loading genres: %v", err.Error())
}
a.model = a.buildGenresListModel(genres)
} else {
artists, err := a.mp.GetArtists()
if err != nil {
log.Printf("error loading artists: %v", err.Error())
}
a.model = a.buildArtistListModel(artists)
}
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
} else {
a.list.Items = a.model
a.list.Refresh()
}
}
func (a *ArtistsGenresPage) onSearched(query string) {
// since the artists and genres lists are returned in full non-paginated, we will do our own
// simple search based on the artist/genre name, rather than calling a server API
if query == "" {
a.list.Items = a.model
} else {
result := sharedutil.FilterSlice(a.model, func(x widgets.ArtistGenreListItemModel) bool {
return strings.Contains(strings.ToLower(x.Name), strings.ToLower(query))
})
a.list.Items = result
}
a.list.Refresh()
}
var _ Searchable = (*ArtistsGenresPage)(nil)
func (a *ArtistsGenresPage) SearchWidget() fyne.Focusable {
return a.searcher
}
func (a *ArtistsGenresPage) Route() controller.Route {
if a.isGenresPage {
return controller.GenresRoute()
}
return controller.ArtistsRoute()
}
func (a *ArtistsGenresPage) Reload() {
go a.load(false)
}
func (a *ArtistsGenresPage) Save() SavedPage {
return &savedArtistsGenresPage{
isGenresPage: a.isGenresPage,
contr: a.contr,
mp: a.mp,
searchText: a.searcher.Entry.Text,
}
}
type savedArtistsGenresPage struct {
isGenresPage bool
contr *controller.Controller
mp mediaprovider.MediaProvider
searchText string
}
func (s *savedArtistsGenresPage) Restore() Page {
return newArtistsGenresPage(s.isGenresPage, s.contr, s.mp, s.searchText)
}
func (a *ArtistsGenresPage) buildArtistListModel(artists []*mediaprovider.Artist) []widgets.ArtistGenreListItemModel {
model := make([]widgets.ArtistGenreListItemModel, 0)
for _, artist := range artists {
model = append(model, widgets.ArtistGenreListItemModel{
ID: artist.ID,
Name: artist.Name,
AlbumCount: artist.AlbumCount,
Favorite: artist.Favorite,
})
}
return model
}
func (a *ArtistsGenresPage) buildGenresListModel(genres []*mediaprovider.Genre) []widgets.ArtistGenreListItemModel {
model := make([]widgets.ArtistGenreListItemModel, 0)
for _, genre := range genres {
model = append(model, widgets.ArtistGenreListItemModel{
ID: genre.Name,
Name: genre.Name,
AlbumCount: genre.AlbumCount,
TrackCount: genre.TrackCount,
Favorite: false,
})
}
return model
}
func (a *ArtistsGenresPage) buildContainer() {
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
container.NewBorder(
container.New(&layouts.MaxPadLayout{PadLeft: -5},
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox)),
nil, nil, nil, a.list))
}
func (a *ArtistsGenresPage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
+187
View File
@@ -0,0 +1,187 @@
package browsing
import (
"fmt"
"log"
"strings"
"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/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/widgets"
)
type ArtistsPage struct {
widget.BaseWidget
contr *controller.Controller
im *backend.ImageManager
pm *backend.PlaybackManager
mp mediaprovider.MediaProvider
artists []*mediaprovider.Artist
searchedArtists []*mediaprovider.Artist
grid *widgets.GridView
fullGridScrollPos float32
searchGridScrollPos float32
searcher *widgets.SearchEntry
searchText string
titleDisp *widget.RichText
container *fyne.Container
}
func NewArtistsPage(
contr *controller.Controller,
pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider,
im *backend.ImageManager,
) *ArtistsPage {
return newArtistsPage(contr, pm, mp, im, "", 0, 0)
}
func newArtistsPage(
contr *controller.Controller,
pm *backend.PlaybackManager,
mp mediaprovider.MediaProvider,
im *backend.ImageManager,
searchText string,
fullGridScrollPos float32,
searchGridScrollPos float32,
) *ArtistsPage {
a := &ArtistsPage{
contr: contr,
pm: pm,
mp: mp,
im: im,
searchText: searchText,
fullGridScrollPos: fullGridScrollPos,
searchGridScrollPos: searchGridScrollPos,
}
a.ExtendBaseWidget(a)
a.titleDisp = widget.NewRichTextWithText("Artists")
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
SizeName: theme.SizeNameHeadingText,
}
a.searcher = widgets.NewSearchEntry()
a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) }
a.searcher.Entry.Text = searchText
a.grid = widgets.NewFixedGridView(nil, a.im, myTheme.ArtistIcon)
a.contr.ConnectArtistGridActions(a.grid)
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
a.container = container.NewBorder(
container.NewHBox(util.NewHSpace(6),
a.titleDisp, layout.NewSpacer(), searchVbox, util.NewHSpace(15)),
nil, nil, nil, a.grid,
)
go a.load()
return a
}
func (a *ArtistsPage) Reload() {
a.searchGridScrollPos = 0
a.fullGridScrollPos = 0
go a.load()
}
func (a *ArtistsPage) load() {
artists, err := a.mp.GetArtists()
if err != nil {
log.Printf("error loading artists: %v", err.Error())
}
a.artists = artists
a.onSearched(a.searcher.Entry.Text, true)
}
func (a *ArtistsPage) onSearched(query string, firstLoad bool) {
// since the playlist list is returned in full non-paginated, we will do our own
// simple search based on the name, description, and owner, rather than calling a server API
var artists []*mediaprovider.Artist
scrollPos := float32(0)
if query == "" {
a.searchedArtists = nil
artists = a.artists
scrollPos = a.fullGridScrollPos
} else {
if firstLoad { // if reloading with a saved search state, set scroll position
scrollPos = a.searchGridScrollPos
}
if a.searchText == "" {
// if first search, capture scroll position of full, unsearched grid
a.fullGridScrollPos = a.grid.GetScrollOffset()
}
qLower := strings.ToLower(query)
a.searchedArtists = sharedutil.FilterSlice(a.artists, func(p *mediaprovider.Artist) bool {
return strings.Contains(strings.ToLower(p.Name), qLower)
})
artists = a.searchedArtists
}
a.searchText = query
a.grid.ResetFixed(createArtistsGridViewModel(artists))
a.grid.Refresh()
a.grid.ScrollToOffset(scrollPos)
}
func createArtistsGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel {
return sharedutil.MapSlice(artists, func(ar *mediaprovider.Artist) widgets.GridViewItemModel {
albums := "albums"
if ar.AlbumCount == 1 {
albums = "album"
}
return widgets.GridViewItemModel{
Name: ar.Name,
ID: ar.ID,
CoverArtID: ar.CoverArtID,
Secondary: fmt.Sprintf("%d %s", ar.AlbumCount, albums),
}
})
}
func (a *ArtistsPage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *ArtistsPage) Route() controller.Route {
return controller.ArtistsRoute()
}
func (a *ArtistsPage) Save() SavedPage {
s := &savedArtistsPage{
contr: a.contr,
im: a.im,
pm: a.pm,
mp: a.mp,
searchText: a.searchText,
fullGridScrollPos: a.fullGridScrollPos,
}
if a.searchText == "" {
s.fullGridScrollPos = a.grid.GetScrollOffset()
} else {
s.searchGridScrollPos = a.grid.GetScrollOffset()
}
return s
}
type savedArtistsPage struct {
contr *controller.Controller
im *backend.ImageManager
pm *backend.PlaybackManager
mp mediaprovider.MediaProvider
searchText string
fullGridScrollPos float32
searchGridScrollPos float32
}
func (s *savedArtistsPage) Restore() Page {
return newArtistsPage(s.contr, s.pm, s.mp, s.im, s.searchText, s.fullGridScrollPos, s.searchGridScrollPos)
}
+31 -32
View File
@@ -1,6 +1,7 @@
package browsing
import (
"fmt"
"log"
"github.com/dweymouth/supersonic/backend"
@@ -33,15 +34,15 @@ type FavoritesPage struct {
nowPlayingID string
pendingViewSwitch bool
grid *widgets.GridView
searchGrid *widgets.GridView
artistListCtr *fyne.Container
tracklistCtr *fyne.Container
searcher *widgets.SearchEntry
filterBtn *widgets.AlbumFilterButton
titleDisp *widget.RichText
toggleBtns *widgets.ToggleButtonGroup
container *fyne.Container
grid *widgets.GridView
searchGrid *widgets.GridView
artistGrid *widgets.GridView
tracklistCtr *fyne.Container
searcher *widgets.SearchEntry
filterBtn *widgets.AlbumFilterButton
titleDisp *widget.RichText
toggleBtns *widgets.ToggleButtonGroup
container *fyne.Container
}
func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage {
@@ -56,7 +57,7 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Contro
a.ExtendBaseWidget(a)
a.createHeader(0)
iter := mp.IterateAlbums("", a.filter)
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im, myTheme.AlbumIcon)
a.contr.ConnectAlbumGridActions(a.grid)
if cfg.InitialView == "Artists" {
a.toggleBtns.SetActivatedButton(1)
@@ -145,7 +146,7 @@ func (a *FavoritesPage) Reload() {
iter := a.mp.IterateAlbums("", a.filter)
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
}
if a.tracklistCtr != nil || a.artistListCtr != nil {
if a.tracklistCtr != nil || a.artistGrid != nil {
go func() {
// re-fetch starred info from server
starred, err := a.mp.GetFavorites()
@@ -162,13 +163,12 @@ func (a *FavoritesPage) Reload() {
tr.Refresh()
}
}
if a.artistListCtr != nil {
if a.artistGrid != nil {
// refresh favorite artists view
al := a.artistListCtr.Objects[0].(*widgets.ArtistGenreList)
al.Items = buildArtistListModel(starred.Artists)
a.artistGrid.ResetFixed(buildArtistGridViewModel(starred.Artists))
if a.toggleBtns.ActivatedButtonIndex() == 1 {
// favorite artists view is visible
al.Refresh()
a.artistGrid.Refresh()
}
}
}()
@@ -238,7 +238,7 @@ func (a *FavoritesPage) SelectAll() {
func (a *FavoritesPage) doSearchAlbums(query string) {
iter := a.mp.SearchAlbums(query, a.filter)
if a.searchGrid == nil {
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im, myTheme.AlbumIcon)
a.contr.ConnectAlbumGridActions(a.searchGrid)
} else {
a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
@@ -263,7 +263,7 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
a.cfg.InitialView = "Artists" // save setting
a.searcher.Entry.Hide() // disable search on artists for now
a.filterBtn.Hide()
if a.artistListCtr == nil {
if a.artistGrid == nil {
if a.pendingViewSwitch {
return
}
@@ -277,32 +277,31 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
log.Printf("error getting starred items: %s", err.Error())
return
}
model := buildArtistListModel(fav.Artists)
artistList := widgets.NewArtistGenreList(model)
artistList.ShowAlbumCount = true
artistList.OnNavTo = func(artistID string) {
a.contr.NavigateTo(controller.ArtistRoute(artistID))
}
a.artistListCtr = container.New(
&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
artistList)
a.container.Objects[0] = a.artistListCtr
model := buildArtistGridViewModel(fav.Artists)
a.artistGrid = widgets.NewFixedGridView(model, a.im, myTheme.ArtistIcon)
a.contr.ConnectArtistGridActions(a.artistGrid)
a.container.Objects[0] = a.artistGrid
a.Refresh()
a.pendingViewSwitch = false
}()
} else {
a.container.Objects[0] = a.artistListCtr
a.container.Objects[0] = a.artistGrid
a.Refresh()
}
}
func buildArtistListModel(artists []*mediaprovider.Artist) []widgets.ArtistGenreListItemModel {
model := make([]widgets.ArtistGenreListItemModel, 0)
func buildArtistGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel {
model := make([]widgets.GridViewItemModel, 0)
for _, ar := range artists {
model = append(model, widgets.ArtistGenreListItemModel{
albums := "albums"
if ar.AlbumCount == 1 {
albums = "album"
}
model = append(model, widgets.GridViewItemModel{
ID: ar.ID,
CoverArtID: ar.CoverArtID,
Name: ar.Name,
AlbumCount: ar.AlbumCount,
Secondary: fmt.Sprintf("%d %s", ar.AlbumCount, albums),
})
}
return model
+2 -4
View File
@@ -33,8 +33,6 @@ type GenrePage struct {
titleDisp *widget.RichText
playRandom *widget.Button
OnPlayAlbum func(string, int)
container *fyne.Container
}
@@ -55,7 +53,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba
}
g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs)
iter := g.mp.IterateAlbums("", g.filter)
g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im, myTheme.AlbumIcon)
g.contr.ConnectAlbumGridActions(g.grid)
g.createSearchAndFilter()
g.createContainer(false)
@@ -171,7 +169,7 @@ func (g *GenrePage) OnSearched(query string) {
func (g *GenrePage) doSearch(query string) {
iter := g.mp.SearchAlbums(query, g.filter)
if g.searchGrid == nil {
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im, myTheme.AlbumIcon)
g.contr.ConnectAlbumGridActions(g.searchGrid)
} else {
g.searchGrid.Reset(widgets.NewGridViewAlbumIterator(iter))
+228
View File
@@ -0,0 +1,228 @@
package browsing
import (
"log"
"strconv"
"strings"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/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 GenresPage struct {
widget.BaseWidget
contr *controller.Controller
mp mediaprovider.MediaProvider
genres []*mediaprovider.Genre
list *GenreList
titleDisp *widget.RichText
container *fyne.Container
searcher *widgets.SearchEntry
}
func NewGenresPage(contr *controller.Controller, mp mediaprovider.MediaProvider) *GenresPage {
return newGenresPage(contr, mp, "")
}
func newGenresPage(contr *controller.Controller, mp mediaprovider.MediaProvider, searchText string) *GenresPage {
a := &GenresPage{
contr: contr,
mp: mp,
titleDisp: widget.NewRichTextWithText("Genres"),
}
a.ExtendBaseWidget(a)
a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText
a.list = NewGenreList(nil)
a.list.ShowAlbumCount = true
a.list.ShowTrackCount = true
a.list.OnNavTo = func(id string) { a.contr.NavigateTo(controller.GenreRoute(id)) }
a.searcher = widgets.NewSearchEntry()
a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText
a.buildContainer()
go a.load(searchText != "")
return a
}
// should be called asynchronously
func (a *GenresPage) load(searchOnLoad bool) {
genres, err := a.mp.GetGenres()
if err != nil {
log.Printf("error loading genres: %v", err.Error())
}
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
} else {
a.list.Items = genres
a.list.Refresh()
}
}
func (a *GenresPage) onSearched(query string) {
// since the artists and genres lists are returned in full non-paginated, we will do our own
// simple search based on the artist/genre name, rather than calling a server API
if query == "" {
a.list.Items = a.genres
} else {
result := sharedutil.FilterSlice(a.genres, func(x *mediaprovider.Genre) bool {
return strings.Contains(strings.ToLower(x.Name), strings.ToLower(query))
})
a.list.Items = result
}
a.list.Refresh()
}
var _ Searchable = (*GenresPage)(nil)
func (a *GenresPage) SearchWidget() fyne.Focusable {
return a.searcher
}
func (a *GenresPage) Route() controller.Route {
return controller.GenresRoute()
}
func (a *GenresPage) Reload() {
go a.load(false)
}
func (a *GenresPage) Save() SavedPage {
return &savedArtistsGenresPage{
contr: a.contr,
mp: a.mp,
searchText: a.searcher.Entry.Text,
}
}
type savedArtistsGenresPage struct {
isGenresPage bool
contr *controller.Controller
mp mediaprovider.MediaProvider
searchText string
}
func (s *savedArtistsGenresPage) Restore() Page {
return newGenresPage(s.contr, s.mp, s.searchText)
}
func (a *GenresPage) buildContainer() {
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
container.NewBorder(
container.New(&layouts.MaxPadLayout{PadLeft: -5},
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox)),
nil, nil, nil, a.list))
}
func (a *GenresPage) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
type GenreList struct {
widget.BaseWidget
Items []*mediaprovider.Genre
ShowAlbumCount bool
ShowTrackCount bool
OnNavTo func(string)
columnsLayout *layouts.ColumnsLayout
hdr *widgets.ListHeader
list *widget.List
container *fyne.Container
}
type GenreListRow struct {
widget.BaseWidget
Item *mediaprovider.Genre
OnTapped func()
nameLabel *widget.Label
albumCountLabel *widget.Label
trackCountLabel *widget.Label
container *fyne.Container
}
func NewGenreListRow(layout *layouts.ColumnsLayout) *GenreListRow {
a := &GenreListRow{
nameLabel: widget.NewLabel(""),
albumCountLabel: widget.NewLabel(""),
trackCountLabel: widget.NewLabel(""),
}
a.ExtendBaseWidget(a)
a.albumCountLabel.Alignment = fyne.TextAlignTrailing
a.trackCountLabel.Alignment = fyne.TextAlignTrailing
a.container = container.New(layout, a.nameLabel, a.albumCountLabel, a.trackCountLabel)
return a
}
func NewGenreList(items []*mediaprovider.Genre) *GenreList {
a := &GenreList{
Items: items,
columnsLayout: layouts.NewColumnsLayout([]float32{-1, 125, 125}),
}
a.ExtendBaseWidget(a)
a.hdr = widgets.NewListHeader([]widgets.ListColumn{
{"Name", false, false}, {"Album Count", true, false}, {"Track Count", true, false}}, a.columnsLayout)
a.list = widget.NewList(
func() int { return len(a.Items) },
func() fyne.CanvasObject {
r := NewGenreListRow(a.columnsLayout)
r.OnTapped = func() { a.onRowDoubleTapped(r.Item) }
return r
},
func(id widget.ListItemID, item fyne.CanvasObject) {
row := item.(*GenreListRow)
row.Item = a.Items[id]
row.albumCountLabel.Hidden = !a.ShowAlbumCount
row.trackCountLabel.Hidden = !a.ShowTrackCount
row.nameLabel.Text = row.Item.Name
row.albumCountLabel.Text = strconv.Itoa(row.Item.AlbumCount)
row.trackCountLabel.Text = strconv.Itoa(row.Item.TrackCount)
row.Refresh()
},
)
a.container = container.NewBorder(a.hdr, nil, nil, nil, a.list)
return a
}
func (a *GenreList) Refresh() {
a.hdr.SetColumnVisible(1, a.ShowAlbumCount)
a.hdr.SetColumnVisible(2, a.ShowTrackCount)
a.BaseWidget.Refresh()
}
func (a *GenreList) onRowDoubleTapped(item *mediaprovider.Genre) {
if a.OnNavTo != nil {
a.OnNavTo(item.Name)
}
}
func (a *GenreListRow) Tapped(*fyne.PointEvent) {
if a.OnTapped != nil {
a.OnTapped()
}
}
func (a *GenreList) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *GenreListRow) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
+1 -2
View File
@@ -6,7 +6,6 @@ import (
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
@@ -208,7 +207,7 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader {
a := &PlaylistPageHeader{page: page}
a.ExtendBaseWidget(a)
a.image = widgets.NewImagePlaceholder(res.ResPlaylistInvertPng, 225)
a.image = widgets.NewImagePlaceholder(myTheme.PlaylistIcon, 225)
a.titleLabel = widget.NewRichTextWithText("")
a.titleLabel.Wrapping = fyne.TextTruncate
a.titleLabel.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/widgets"
"fyne.io/fyne/v2"
@@ -94,7 +95,7 @@ func (a *PlaylistsPage) createListView() {
func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) {
model := createPlaylistGridViewModel(playlists)
a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager)
a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager, myTheme.PlaylistIcon)
a.gridView.OnPlay = func(id string, shuffle bool) {
go a.contr.App.PlaybackManager.PlayPlaylist(id, 0, shuffle)
}
+2 -2
View File
@@ -34,13 +34,13 @@ func (r Router) CreatePage(rte controller.Route) Page {
case controller.Artist:
return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
case controller.Artists:
return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager.Server)
return NewArtistsPage(r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
case controller.Favorites:
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
case controller.Genre:
return NewGenrePage(rte.Arg, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
case controller.Genres:
return NewArtistsGenresPage(true, r.Controller, r.App.ServerManager.Server)
return NewGenresPage(r.Controller, r.App.ServerManager.Server)
case controller.NowPlaying:
return NewNowPlayingPage(rte.Arg, r.Controller, &r.App.Config.NowPlayingPage, r.App.PlaybackManager)
case controller.Playlist:
+41
View File
@@ -140,6 +140,47 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
}
}
func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) {
grid.OnShowItemPage = func(id string) { m.NavigateTo(ArtistRoute(id)) }
grid.OnPlay = func(artistID string, shuffle bool) { go m.PlayArtistDiscography(artistID, shuffle) }
grid.OnAddToQueue = func(artistID string) {
go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), true /*append*/, false /*shuffle*/)
}
grid.OnAddToPlaylist = func(artistID string) {
go m.DoAddTracksToPlaylistWorkflow(
sharedutil.TracksToIDs(m.GetArtistTracks(artistID)))
}
}
func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track {
server := m.App.ServerManager.Server
if server == nil {
log.Println("error playing artist discography: logged out")
return nil
}
artist, err := server.GetArtist(artistID)
if err != nil {
log.Printf("error getting artist discography: %v", err.Error())
return nil
}
var allTracks []*mediaprovider.Track
for _, album := range artist.Albums {
album, err := server.GetAlbum(album.ID)
if err != nil {
log.Printf("error loading album tracks: %v", err.Error())
return nil
}
allTracks = append(allTracks, album.Tracks...)
}
return allTracks
}
func (m *Controller) PlayArtistDiscography(artistID string, shuffle bool) {
m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), false, shuffle)
m.App.PlaybackManager.PlayFromBeginning()
}
func (m *Controller) PromptForFirstServer() {
d := dialogs.NewAddEditServerDialog("Connect to Server", false, nil, m.MainWindow.Canvas().Focus)
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
+1 -3
View File
@@ -94,14 +94,12 @@ func (a *AboutDialog) buildCreditsContainer() fyne.CanvasObject {
freepikURL, _ := url.Parse("https://www.flaticon.com/authors/freepik")
appIconCredit := widget.NewLabel("The Supersonic app icon is a derivative of a work created by Piotr Siedlecki and placed in the public domain.")
appIconCredit.Wrapping = fyne.TextWrapWord
albumPlaceholderCredit := widget.NewLabel("The album cover placeholder image was created by @LaurenBacall originally for the MP3Tag project.")
albumPlaceholderCredit.Wrapping = fyne.TextWrapWord
return container.New(&layouts.VboxCustomPadding{ExtraPad: -10},
widget.NewLabel("Major frameworks and modules used in this application include:"),
container.NewHBox(widget.NewHyperlink("Fyne toolkit", fyneURL), widget.NewLabel("BSD 3-Clause License")),
container.NewHBox(widget.NewHyperlink("go-subsonic", goSubsonicURL), widget.NewLabel("GPL v3 License")),
container.NewHBox(widget.NewHyperlink("go-toml", goTomlURL), widget.NewLabel("MIT License")),
appIconCredit, albumPlaceholderCredit,
appIconCredit,
container.NewHBox(widget.NewLabel("Additional icons by:"), widget.NewHyperlink("Freepik on flaticon.com", freepikURL)),
)
}
+8 -3
View File
@@ -11,7 +11,9 @@ type ThemedRectangle struct {
rect *canvas.Rectangle
ColorName fyne.ThemeColorName
ColorName fyne.ThemeColorName
BorderWidth float32
BorderColorName fyne.ThemeColorName
}
func NewThemedRectangle(colorName fyne.ThemeColorName) *ThemedRectangle {
@@ -25,8 +27,11 @@ func NewThemedRectangle(colorName fyne.ThemeColorName) *ThemedRectangle {
}
func (t *ThemedRectangle) Refresh() {
t.rect.FillColor = fyne.CurrentApp().Settings().Theme().Color(t.ColorName,
fyne.CurrentApp().Settings().ThemeVariant())
settings := fyne.CurrentApp().Settings()
theme := settings.Theme()
t.rect.FillColor = theme.Color(t.ColorName, settings.ThemeVariant())
t.rect.StrokeWidth = t.BorderWidth
t.rect.StrokeColor = theme.Color(t.BorderColorName, settings.ThemeVariant())
t.BaseWidget.Refresh()
}
-116
View File
@@ -1,116 +0,0 @@
package widgets
import (
"strconv"
"github.com/dweymouth/supersonic/ui/layouts"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
type ArtistGenreListItemModel struct {
ID string
Name string
AlbumCount int
TrackCount int
Favorite bool
}
type ArtistGenreList struct {
widget.BaseWidget
Items []ArtistGenreListItemModel
ShowAlbumCount bool
ShowTrackCount bool
OnNavTo func(string)
columnsLayout *layouts.ColumnsLayout
hdr *ListHeader
list *widget.List
container *fyne.Container
}
type ArtistGenreListRow struct {
widget.BaseWidget
Item ArtistGenreListItemModel
OnTapped func()
nameLabel *widget.Label
albumCountLabel *widget.Label
trackCountLabel *widget.Label
container *fyne.Container
}
func NewArtistGenreListRow(layout *layouts.ColumnsLayout) *ArtistGenreListRow {
a := &ArtistGenreListRow{
nameLabel: widget.NewLabel(""),
albumCountLabel: widget.NewLabel(""),
trackCountLabel: widget.NewLabel(""),
}
a.ExtendBaseWidget(a)
a.albumCountLabel.Alignment = fyne.TextAlignTrailing
a.trackCountLabel.Alignment = fyne.TextAlignTrailing
a.container = container.New(layout, a.nameLabel, a.albumCountLabel, a.trackCountLabel)
return a
}
func NewArtistGenreList(items []ArtistGenreListItemModel) *ArtistGenreList {
a := &ArtistGenreList{
Items: items,
columnsLayout: layouts.NewColumnsLayout([]float32{-1, 125, 125}),
}
a.ExtendBaseWidget(a)
a.hdr = NewListHeader([]ListColumn{
{"Name", fyne.TextAlignLeading, false}, {"Album Count", fyne.TextAlignTrailing, false}, {"Track Count", fyne.TextAlignTrailing, false}}, a.columnsLayout)
a.hdr.DisableSorting = true
a.list = widget.NewList(
func() int { return len(a.Items) },
func() fyne.CanvasObject {
r := NewArtistGenreListRow(a.columnsLayout)
r.OnTapped = func() { a.onRowDoubleTapped(r.Item) }
return r
},
func(id widget.ListItemID, item fyne.CanvasObject) {
row := item.(*ArtistGenreListRow)
row.Item = a.Items[id]
row.albumCountLabel.Hidden = !a.ShowAlbumCount
row.trackCountLabel.Hidden = !a.ShowTrackCount
row.nameLabel.Text = row.Item.Name
row.albumCountLabel.Text = strconv.Itoa(row.Item.AlbumCount)
row.trackCountLabel.Text = strconv.Itoa(row.Item.TrackCount)
row.Refresh()
},
)
a.container = container.NewBorder(a.hdr, nil, nil, nil, a.list)
return a
}
func (a *ArtistGenreList) Refresh() {
a.hdr.SetColumnVisible(1, a.ShowAlbumCount)
a.hdr.SetColumnVisible(2, a.ShowTrackCount)
a.BaseWidget.Refresh()
}
func (a *ArtistGenreList) onRowDoubleTapped(item ArtistGenreListItemModel) {
if a.OnNavTo != nil {
a.OnNavTo(item.ID)
}
}
func (a *ArtistGenreListRow) Tapped(*fyne.PointEvent) {
if a.OnTapped != nil {
a.OnTapped()
}
}
func (a *ArtistGenreList) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
func (a *ArtistGenreListRow) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
+16 -8
View File
@@ -7,7 +7,6 @@ import (
"sync"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/sharedutil"
"fyne.io/fyne/v2"
@@ -81,6 +80,7 @@ type GridViewState struct {
itemsMutex sync.RWMutex
iter GridViewIterator
imageFetcher ImageFetcher
placeholder fyne.Resource
highestShown int
fetchCancel context.CancelFunc
done bool
@@ -96,12 +96,13 @@ type GridViewState struct {
var _ fyne.Widget = (*GridView)(nil)
func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher) *GridView {
func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher, placeholder fyne.Resource) *GridView {
g := &GridView{
GridViewState: GridViewState{
items: items,
done: true,
imageFetcher: fetch,
placeholder: placeholder,
},
}
g.ExtendBaseWidget(g)
@@ -109,7 +110,7 @@ func NewFixedGridView(items []GridViewItemModel, fetch ImageFetcher) *GridView {
return g
}
func NewGridView(iter GridViewIterator, fetch ImageFetcher) *GridView {
func NewGridView(iter GridViewIterator, fetch ImageFetcher, placeholder fyne.Resource) *GridView {
g := &GridView{
GridViewState: GridViewState{
iter: iter,
@@ -117,7 +118,6 @@ func NewGridView(iter GridViewIterator, fetch ImageFetcher) *GridView {
},
}
g.ExtendBaseWidget(g)
g.createGridWrap()
// fetch initial items
@@ -174,6 +174,14 @@ func (g *GridView) ResetFixed(items []GridViewItemModel) {
g.iter = nil
}
func (g *GridView) GetScrollOffset() float32 {
return g.grid.GetScrollOffset()
}
func (g *GridView) ScrollToOffset(offs float32) {
g.grid.ScrollToOffset(offs)
}
func (g *GridView) createGridWrap() {
g.grid = NewGridWrap(
func() int {
@@ -181,7 +189,7 @@ func (g *GridView) createGridWrap() {
},
// create func
func() fyne.CanvasObject {
card := NewGridViewItem()
card := NewGridViewItem(g.placeholder)
card.OnPlay = func(shuffle bool) {
if g.OnPlay != nil {
g.OnPlay(card.ItemID(), shuffle)
@@ -244,7 +252,7 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
if img, ok := g.imageFetcher.GetCoverThumbnailFromCache(item.CoverArtID); ok {
card.Cover.SetImage(img)
} else {
card.Cover.SetImageResource(res.ResAlbumplaceholderPng)
card.Cover.SetImage(nil)
// asynchronously fetch cover image
ctx, cancel := context.WithCancel(context.Background())
card.ImgLoadCancel = cancel
@@ -263,8 +271,8 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
}(ctx)
}
} else {
// use the placeholder image for an item that has no cover art ID
card.Cover.SetImageResource(res.ResAlbumplaceholderPng)
// use the placeholder for an item that has no cover art ID
card.Cover.SetImage(nil)
}
// if user has scrolled near the bottom, fetch more
+10 -18
View File
@@ -17,27 +17,27 @@ import (
var _ fyne.Widget = (*GridViewItem)(nil)
var _ fyne.Widget = (*coverImage)(nil)
var _ fyne.Tappable = (*coverImage)(nil)
var _ fyne.SecondaryTappable = (*coverImage)(nil)
type coverImage struct {
widget.BaseWidget
Im *canvas.Image
Im *ImagePlaceholder
playbtn *canvas.Image
OnPlay func()
OnShowPage func()
OnShowContextMenu func(fyne.Position)
}
func newCoverImage() *coverImage {
func newCoverImage(placeholderResource fyne.Resource) *coverImage {
c := &coverImage{}
c.ExtendBaseWidget(c)
c.Im = &canvas.Image{FillMode: canvas.ImageFillContain, ScaleMode: canvas.ImageScaleFastest}
c.Im.SetMinSize(fyne.NewSize(200, 200))
c.Im = NewImagePlaceholder(placeholderResource, 200)
c.Im.OnTapped = c.Tapped
c.Im.OnTappedSecondary = c.TappedSecondary
c.Im.ScaleMode = canvas.ImageScaleFastest
c.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng}
c.playbtn.SetMinSize(fyne.NewSize(60, 60))
c.playbtn.Hidden = true
c.ExtendBaseWidget(c)
return c
}
@@ -93,15 +93,7 @@ func (a *coverImage) center() fyne.Position {
}
func (a *coverImage) SetImage(im image.Image) {
a.Im.Resource = nil
a.Im.Image = im
a.Refresh()
}
func (a *coverImage) SetImageResource(res *fyne.StaticResource) {
a.Im.Image = nil
a.Im.Resource = res
a.Refresh()
a.Im.SetImage(im, true)
}
func isInside(origin fyne.Position, radius float32, point fyne.Position) bool {
@@ -141,11 +133,11 @@ type GridViewItem struct {
OnShowSecondaryPage func()
}
func NewGridViewItem() *GridViewItem {
func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
g := &GridViewItem{
primaryText: NewCustomHyperlink(),
secondaryText: NewCustomHyperlink(),
Cover: newCoverImage(),
Cover: newCoverImage(placeholderResource),
}
g.ExtendBaseWidget(g)
g.Cover.OnPlay = func() { g.onPlay(false) }
+53 -40
View File
@@ -2,48 +2,52 @@ package widgets
import (
"image"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
myTheme "github.com/dweymouth/supersonic/ui/theme"
)
// A widget that can display an image or else
// a placeholder with a rectangular border frame
// and an icon positioned in the center of the frame.
type ImagePlaceholder struct {
ScaleMode canvas.ImageScale
widget.BaseWidget
container *fyne.Container
content *fyne.Container
imageDisp *TappableImage
image image.Image
iconImage *canvas.Image
border *myTheme.ThemedRectangle
minSize float32
OnTapped func()
OnTapped func(*fyne.PointEvent)
OnTappedSecondary func(*fyne.PointEvent)
}
func NewImagePlaceholder(centerIcon fyne.Resource, minSize float32) *ImagePlaceholder {
m := &ImagePlaceholder{minSize: minSize}
m.ExtendBaseWidget(m)
img := canvas.NewImageFromResource(centerIcon)
img.FillMode = canvas.ImageFillContain
img.SetMinSize(fyne.NewSize(minSize/4, minSize/4))
rect := canvas.NewRectangle(theme.BackgroundColor())
rect.StrokeColor = color.Black
rect.StrokeWidth = 3
rect.SetMinSize(fyne.NewSize(minSize, minSize))
m.container = container.NewMax(
rect,
container.NewCenter(img),
i := &ImagePlaceholder{minSize: minSize}
i.ExtendBaseWidget(i)
i.iconImage = canvas.NewImageFromResource(centerIcon)
i.iconImage.FillMode = canvas.ImageFillContain
i.iconImage.SetMinSize(fyne.NewSize(minSize/4, minSize/4))
i.imageDisp = NewTappableImage(i.onTapped)
i.imageDisp.OnTappedSecondary = i.onTappedSecondary
i.imageDisp.FillMode = canvas.ImageFillContain
i.imageDisp.Hidden = true
i.border = myTheme.NewThemedRectangle(theme.ColorNameBackground)
i.border.BorderColorName = theme.ColorNameForeground
i.border.BorderWidth = 3
i.content = container.NewMax(
i.border,
container.NewCenter(i.iconImage),
i.imageDisp,
)
return m
}
type CanvasImage interface {
fyne.CanvasObject
SetMinSize(fyne.Size)
return i
}
func (i *ImagePlaceholder) HaveImage() bool {
@@ -51,34 +55,43 @@ func (i *ImagePlaceholder) HaveImage() bool {
}
func (i *ImagePlaceholder) SetImage(img image.Image, tappable bool) {
var cIm CanvasImage
i.image = img
if tappable {
cImg := NewTappableImage(i.onTapped)
cImg.Image.Image = img
cImg.FillMode = canvas.ImageFillContain
cIm = cImg
} else {
cImg := canvas.NewImageFromImage(img)
cImg.FillMode = canvas.ImageFillContain
cIm = cImg
if img != nil {
i.imageDisp.DisableTapping = !tappable
i.imageDisp.Image.Image = img
}
cIm.SetMinSize(fyne.NewSize(i.minSize, i.minSize))
i.container.RemoveAll()
i.container.Add(cIm)
i.container.Refresh()
i.Refresh()
}
func (i *ImagePlaceholder) Image() image.Image {
return i.image
}
func (i *ImagePlaceholder) onTapped() {
func (i *ImagePlaceholder) onTapped(e *fyne.PointEvent) {
if i.OnTapped != nil {
i.OnTapped()
i.OnTapped(e)
}
}
func (i *ImagePlaceholder) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(i.container)
func (i *ImagePlaceholder) onTappedSecondary(e *fyne.PointEvent) {
if i.OnTappedSecondary != nil {
i.OnTappedSecondary(e)
}
}
func (i *ImagePlaceholder) MinSize() fyne.Size {
return fyne.NewSize(i.minSize, i.minSize)
}
func (i *ImagePlaceholder) Refresh() {
i.border.Hidden = i.HaveImage()
i.iconImage.Hidden = i.HaveImage()
i.imageDisp.Hidden = !i.HaveImage()
i.imageDisp.ScaleMode = i.ScaleMode
i.BaseWidget.Refresh()
}
func (i *ImagePlaceholder) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(i.content)
}
+3 -3
View File
@@ -55,7 +55,7 @@ func NewNowPlayingCard() *NowPlayingCard {
return n
}
func (n *NowPlayingCard) onShowCoverImage() {
func (n *NowPlayingCard) onShowCoverImage(*fyne.PointEvent) {
if n.OnShowCoverImage != nil {
n.OnShowCoverImage()
}
@@ -107,7 +107,7 @@ func (n *NowPlayingCard) OnTrackNameTapped(f func()) {
n.trackName.OnTapped = f
}
func (n *NowPlayingCard) showMenu(pos fyne.Position) {
func (n *NowPlayingCard) showMenu(e *fyne.PointEvent) {
if n.menu == nil {
ratingMenu := util.NewRatingSubmenu(n.onSetRating)
m := fyne.NewMenu("",
@@ -117,5 +117,5 @@ func (n *NowPlayingCard) showMenu(pos fyne.Position) {
fyne.NewMenuItem("Add to playlist...", func() { n.onAddToPlaylist() }))
n.menu = widget.NewPopUpMenu(m, fyne.CurrentApp().Driver().CanvasForObject(n))
}
n.menu.ShowAtPosition(pos)
n.menu.ShowAtPosition(e.AbsolutePosition)
}
+10 -9
View File
@@ -54,11 +54,12 @@ type TappableImage struct {
widget.BaseWidget
canvas.Image
OnTapped func()
OnTappedSecondary func(pos fyne.Position)
DisableTapping bool
OnTapped func(*fyne.PointEvent)
OnTappedSecondary func(*fyne.PointEvent)
}
func NewTappableImage(onTapped func()) *TappableImage {
func NewTappableImage(onTapped func(*fyne.PointEvent)) *TappableImage {
t := &TappableImage{OnTapped: onTapped}
t.ExtendBaseWidget(t)
return t
@@ -85,21 +86,21 @@ func (t *TappableImage) Resize(size fyne.Size) {
}
func (t *TappableImage) Cursor() desktop.Cursor {
if t.haveImage() {
if !t.DisableTapping && t.haveImage() {
return desktop.PointerCursor
}
return desktop.DefaultCursor
}
func (t *TappableImage) Tapped(_ *fyne.PointEvent) {
if t.haveImage() && t.OnTapped != nil {
t.OnTapped()
func (t *TappableImage) Tapped(e *fyne.PointEvent) {
if !t.DisableTapping && t.haveImage() && t.OnTapped != nil {
t.OnTapped(e)
}
}
func (t *TappableImage) TappedSecondary(e *fyne.PointEvent) {
if t.haveImage() && t.OnTappedSecondary != nil {
t.OnTappedSecondary(e.AbsolutePosition)
if !t.DisableTapping && t.haveImage() && t.OnTappedSecondary != nil {
t.OnTappedSecondary(e)
}
}