add option to show album years in grid views
This commit is contained in:
@@ -59,6 +59,7 @@ type AlbumPageConfig struct {
|
|||||||
|
|
||||||
type AlbumsPageConfig struct {
|
type AlbumsPageConfig struct {
|
||||||
SortOrder string
|
SortOrder string
|
||||||
|
ShowYears bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArtistPageConfig struct {
|
type ArtistPageConfig struct {
|
||||||
@@ -73,6 +74,7 @@ type ArtistsPageConfig struct {
|
|||||||
type FavoritesPageConfig struct {
|
type FavoritesPageConfig struct {
|
||||||
InitialView string
|
InitialView string
|
||||||
TracklistColumns []string
|
TracklistColumns []string
|
||||||
|
ShowAlbumYears bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaylistPageConfig struct {
|
type PlaylistPageConfig struct {
|
||||||
@@ -167,6 +169,7 @@ func DefaultConfig(appVersionTag string) *Config {
|
|||||||
},
|
},
|
||||||
AlbumsPage: AlbumsPageConfig{
|
AlbumsPage: AlbumsPageConfig{
|
||||||
SortOrder: string("Recently Added"),
|
SortOrder: string("Recently Added"),
|
||||||
|
ShowYears: false,
|
||||||
},
|
},
|
||||||
ArtistPage: ArtistPageConfig{
|
ArtistPage: ArtistPageConfig{
|
||||||
InitialView: "Discography",
|
InitialView: "Discography",
|
||||||
@@ -178,6 +181,7 @@ func DefaultConfig(appVersionTag string) *Config {
|
|||||||
FavoritesPage: FavoritesPageConfig{
|
FavoritesPage: FavoritesPageConfig{
|
||||||
TracklistColumns: []string{"Album", "Time", "Plays"},
|
TracklistColumns: []string{"Album", "Time", "Plays"},
|
||||||
InitialView: "Albums",
|
InitialView: "Albums",
|
||||||
|
ShowAlbumYears: false,
|
||||||
},
|
},
|
||||||
PlaylistPage: PlaylistPageConfig{
|
PlaylistPage: PlaylistPageConfig{
|
||||||
TracklistColumns: []string{"Album", "Time", "Plays"},
|
TracklistColumns: []string{"Album", "Time", "Plays"},
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ func (a *albumsPageAdapter) SearchIter(query string, filter mediaprovider.AlbumF
|
|||||||
return widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, filter))
|
return widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *albumsPageAdapter) ConnectGridActions(gv *widgets.GridView) {
|
func (a *albumsPageAdapter) InitGrid(gv *widgets.GridView) {
|
||||||
a.contr.ConnectAlbumGridActions(gv)
|
a.contr.ConnectAlbumGridActions(gv)
|
||||||
|
gv.ShowSuffix = a.cfg.ShowYears
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *albumsPageAdapter) RefreshGrid(gv *widgets.GridView) {
|
||||||
|
gv.ShowSuffix = a.cfg.ShowYears
|
||||||
|
gv.Refresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (a *artistsPageAdapter) SearchIter(query string, filter mediaprovider.Artis
|
|||||||
return widgets.NewGridViewArtistIterator(a.mp.SearchArtists(query, filter))
|
return widgets.NewGridViewArtistIterator(a.mp.SearchArtists(query, filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *artistsPageAdapter) ConnectGridActions(gv *widgets.GridView) {
|
func (a *artistsPageAdapter) InitGrid(gv *widgets.GridView) {
|
||||||
canShareArtists := false
|
canShareArtists := false
|
||||||
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
|
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
|
||||||
canShareArtists = r.CanShareArtists()
|
canShareArtists = r.CanShareArtists()
|
||||||
@@ -76,3 +76,7 @@ func (a *artistsPageAdapter) ConnectGridActions(gv *widgets.GridView) {
|
|||||||
gv.DisableSharing = !canShareArtists
|
gv.DisableSharing = !canShareArtists
|
||||||
a.contr.ConnectArtistGridActions(gv)
|
a.contr.ConnectArtistGridActions(gv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *artistsPageAdapter) RefreshGrid(gv *widgets.GridView) {
|
||||||
|
gv.Refresh()
|
||||||
|
}
|
||||||
|
|||||||
@@ -195,6 +195,12 @@ func (b *BrowsingPane) ScrollDown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BrowsingPane) RefreshPage() {
|
||||||
|
if b.curPage != nil {
|
||||||
|
b.curPage.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BrowsingPane) doSetPage(p Page) bool {
|
func (b *BrowsingPane) doSetPage(p Page) bool {
|
||||||
if b.curPage != nil && b.curPage.Route() == p.Route() {
|
if b.curPage != nil && b.curPage.Route() == p.Route() {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, c
|
|||||||
} else {
|
} else {
|
||||||
a.albumGrid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon)
|
a.albumGrid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon)
|
||||||
}
|
}
|
||||||
|
a.albumGrid.ShowSuffix = cfg.ShowAlbumYears
|
||||||
a.contr.ConnectAlbumGridActions(a.albumGrid)
|
a.contr.ConnectAlbumGridActions(a.albumGrid)
|
||||||
if cfg.InitialView == "Artists" {
|
if cfg.InitialView == "Artists" {
|
||||||
a.toggleBtns.SetActivatedButton(1)
|
a.toggleBtns.SetActivatedButton(1)
|
||||||
@@ -285,6 +286,13 @@ func (a *FavoritesPage) SelectAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *FavoritesPage) Refresh() {
|
||||||
|
if a.albumGrid != nil {
|
||||||
|
a.albumGrid.ShowSuffix = a.cfg.ShowAlbumYears
|
||||||
|
}
|
||||||
|
a.BaseWidget.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) tracklistOrNil() *widgets.Tracklist {
|
func (a *FavoritesPage) tracklistOrNil() *widgets.Tracklist {
|
||||||
if a.tracklistCtr != nil {
|
if a.tracklistCtr != nil {
|
||||||
return a.tracklistCtr.Objects[0].(*widgets.Tracklist)
|
return a.tracklistCtr.Objects[0].(*widgets.Tracklist)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type genrePageAdapter struct {
|
type genrePageAdapter struct {
|
||||||
genre string
|
genre string
|
||||||
|
cfg *backend.AlbumsPageConfig
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
mp mediaprovider.MediaProvider
|
mp mediaprovider.MediaProvider
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
@@ -21,8 +22,8 @@ type genrePageAdapter struct {
|
|||||||
filterBtn *widgets.AlbumFilterButton
|
filterBtn *widgets.AlbumFilterButton
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGenrePage(genre string, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page {
|
func NewGenrePage(genre string, cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page {
|
||||||
adapter := &genrePageAdapter{genre: genre, contr: contr, mp: mp, pm: pm}
|
adapter := &genrePageAdapter{genre: genre, cfg: cfg, contr: contr, mp: mp, pm: pm}
|
||||||
return NewGridViewPage(adapter, pool, mp, im)
|
return NewGridViewPage(adapter, pool, mp, im)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +69,12 @@ func (a *genrePageAdapter) SearchIter(query string, filter mediaprovider.AlbumFi
|
|||||||
return widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, filter))
|
return widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *genrePageAdapter) ConnectGridActions(gv *widgets.GridView) {
|
func (g *genrePageAdapter) InitGrid(gv *widgets.GridView) {
|
||||||
g.contr.ConnectAlbumGridActions(gv)
|
g.contr.ConnectAlbumGridActions(gv)
|
||||||
|
gv.ShowSuffix = g.cfg.ShowYears
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genrePageAdapter) RefreshGrid(gv *widgets.GridView) {
|
||||||
|
gv.ShowSuffix = g.cfg.ShowYears
|
||||||
|
gv.Refresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,13 @@ type GridViewPageAdapter[M, F any] interface {
|
|||||||
// Returns the iterator for the given search query and filter.
|
// Returns the iterator for the given search query and filter.
|
||||||
SearchIter(query string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator
|
SearchIter(query string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator
|
||||||
|
|
||||||
// Function that connects the GridView callbacks to the appropriate action handlers.
|
// Function that initialized the GridView with page-specific settings
|
||||||
ConnectGridActions(*widgets.GridView)
|
// and connects the GridView callbacks to the appropriate action handlers.
|
||||||
|
InitGrid(*widgets.GridView)
|
||||||
|
|
||||||
|
// Function called when settings may have changed and the grid needs
|
||||||
|
// reconfiguring with possible settings changes.
|
||||||
|
RefreshGrid(*widgets.GridView)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortableGridViewPageAdapter interface {
|
type SortableGridViewPageAdapter interface {
|
||||||
@@ -124,7 +129,7 @@ func NewGridViewPage[M, F any](
|
|||||||
gp.grid = widgets.NewGridView(iter, im, adapter.PlaceholderResource())
|
gp.grid = widgets.NewGridView(iter, im, adapter.PlaceholderResource())
|
||||||
}
|
}
|
||||||
gp.grid.DisableSharing = !canShare
|
gp.grid.DisableSharing = !canShare
|
||||||
adapter.ConnectGridActions(gp.grid)
|
adapter.InitGrid(gp.grid)
|
||||||
gp.createSearchAndFilter()
|
gp.createSearchAndFilter()
|
||||||
gp.createContainer()
|
gp.createContainer()
|
||||||
return gp
|
return gp
|
||||||
@@ -206,6 +211,10 @@ func (g *GridViewPage[M, F]) OnSearched(query string) {
|
|||||||
g.searchText = query
|
g.searchText = query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GridViewPage[M, F]) Refresh() {
|
||||||
|
g.adapter.RefreshGrid(g.grid)
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GridViewPage[M, F]) doSearch(query string) {
|
func (g *GridViewPage[M, F]) doSearch(query string) {
|
||||||
if g.searchText == "" {
|
if g.searchText == "" {
|
||||||
g.gridState = g.grid.SaveToState()
|
g.gridState = g.grid.SaveToState()
|
||||||
@@ -294,7 +303,7 @@ func (s *savedGridViewPage[M, F]) Restore() Page {
|
|||||||
} else {
|
} else {
|
||||||
gp.grid = widgets.NewGridViewFromState(state)
|
gp.grid = widgets.NewGridViewFromState(state)
|
||||||
}
|
}
|
||||||
gp.adapter.ConnectGridActions(gp.grid)
|
gp.adapter.InitGrid(gp.grid)
|
||||||
gp.createSearchAndFilter()
|
gp.createSearchAndFilter()
|
||||||
gp.createContainer()
|
gp.createContainer()
|
||||||
return gp
|
return gp
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (r Router) CreatePage(rte controller.Route) Page {
|
|||||||
case controller.Favorites:
|
case controller.Favorites:
|
||||||
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
|
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
|
||||||
case controller.Genre:
|
case controller.Genre:
|
||||||
return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
|
return NewGenrePage(rte.Arg, &r.App.Config.AlbumsPage, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
|
||||||
case controller.Genres:
|
case controller.Genres:
|
||||||
return NewGenresPage(r.Controller, r.App.ServerManager.Server)
|
return NewGenresPage(r.Controller, r.App.ServerManager.Server)
|
||||||
case controller.NowPlaying:
|
case controller.NowPlaying:
|
||||||
|
|||||||
@@ -32,17 +32,16 @@ import (
|
|||||||
|
|
||||||
type NavigationHandler func(Route)
|
type NavigationHandler func(Route)
|
||||||
|
|
||||||
type ReloadFunc func()
|
|
||||||
|
|
||||||
type CurPageFunc func() Route
|
type CurPageFunc func() Route
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
AppVersion string
|
AppVersion string
|
||||||
MainWindow fyne.Window
|
App *backend.App
|
||||||
App *backend.App
|
MainWindow fyne.Window
|
||||||
NavHandler NavigationHandler
|
NavHandler NavigationHandler
|
||||||
CurPageFunc CurPageFunc
|
CurPageFunc CurPageFunc
|
||||||
ReloadFunc ReloadFunc
|
ReloadFunc func()
|
||||||
|
RefreshPageFunc func()
|
||||||
|
|
||||||
escapablePopUp *widget.PopUp
|
escapablePopUp *widget.PopUp
|
||||||
haveModal bool
|
haveModal bool
|
||||||
@@ -559,6 +558,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
|||||||
copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands)
|
copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands)
|
||||||
c.App.LocalPlayer.SetEqualizer(eq)
|
c.App.LocalPlayer.SetEqualizer(eq)
|
||||||
}
|
}
|
||||||
|
dlg.OnPageNeedsRefresh = c.RefreshPageFunc
|
||||||
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
||||||
dlg.OnDismiss = func() {
|
dlg.OnDismiss = func() {
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type SettingsDialog struct {
|
|||||||
OnThemeSettingChanged func()
|
OnThemeSettingChanged func()
|
||||||
OnDismiss func()
|
OnDismiss func()
|
||||||
OnEqualizerSettingsChanged func()
|
OnEqualizerSettingsChanged func()
|
||||||
|
OnPageNeedsRefresh func()
|
||||||
|
|
||||||
config *backend.Config
|
config *backend.Config
|
||||||
audioDevices []mpv.AudioDevice
|
audioDevices []mpv.AudioDevice
|
||||||
@@ -185,6 +186,14 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
|
|||||||
|
|
||||||
trackNotif := widget.NewCheckWithData("Show notification on track change",
|
trackNotif := widget.NewCheckWithData("Show notification on track change",
|
||||||
binding.BindBool(&s.config.Application.ShowTrackChangeNotification))
|
binding.BindBool(&s.config.Application.ShowTrackChangeNotification))
|
||||||
|
albumGridYears := widget.NewCheck("Show year in album grid cards", func(b bool) {
|
||||||
|
s.config.AlbumsPage.ShowYears = b
|
||||||
|
s.config.FavoritesPage.ShowAlbumYears = b
|
||||||
|
if s.OnPageNeedsRefresh != nil {
|
||||||
|
s.OnPageNeedsRefresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
albumGridYears.Checked = s.config.AlbumsPage.ShowYears
|
||||||
|
|
||||||
// Scrobble settings
|
// Scrobble settings
|
||||||
|
|
||||||
@@ -276,6 +285,7 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
|
|||||||
container.NewHBox(systemTrayEnable, closeToTray),
|
container.NewHBox(systemTrayEnable, closeToTray),
|
||||||
saveQueueHBox,
|
saveQueueHBox,
|
||||||
trackNotif,
|
trackNotif,
|
||||||
|
albumGridYears,
|
||||||
s.newSectionSeparator(),
|
s.newSectionSeparator(),
|
||||||
|
|
||||||
widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: util.BoldRichTextStyle}),
|
widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: util.BoldRichTextStyle}),
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
|||||||
m.Controller.NavHandler = m.Router.NavigateTo
|
m.Controller.NavHandler = m.Router.NavigateTo
|
||||||
m.Controller.ReloadFunc = m.BrowsingPane.Reload
|
m.Controller.ReloadFunc = m.BrowsingPane.Reload
|
||||||
m.Controller.CurPageFunc = m.BrowsingPane.CurrentPage
|
m.Controller.CurPageFunc = m.BrowsingPane.CurrentPage
|
||||||
|
m.Controller.RefreshPageFunc = m.BrowsingPane.RefreshPage
|
||||||
|
|
||||||
m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller)
|
m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller)
|
||||||
m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane)
|
m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane)
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ const (
|
|||||||
ColorNameIconButton fyne.ThemeColorName = "IconButton"
|
ColorNameIconButton fyne.ThemeColorName = "IconButton"
|
||||||
ColorNameHoveredIconButton fyne.ThemeColorName = "HoveredIconButton"
|
ColorNameHoveredIconButton fyne.ThemeColorName = "HoveredIconButton"
|
||||||
ColorNameNowPlayingPanel fyne.ThemeColorName = "NowPlayingPanel"
|
ColorNameNowPlayingPanel fyne.ThemeColorName = "NowPlayingPanel"
|
||||||
|
|
||||||
|
SizeNameSubText fyne.ThemeSizeName = "subText" // in between Text and Caption
|
||||||
|
SizeNameSuffixText fyne.ThemeSizeName = "suffixText" // a tiny bit smaller than subText
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -243,6 +246,11 @@ func (m *MyTheme) Font(style fyne.TextStyle) fyne.Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MyTheme) Size(name fyne.ThemeSizeName) float32 {
|
func (m *MyTheme) Size(name fyne.ThemeSizeName) float32 {
|
||||||
|
if name == SizeNameSubText {
|
||||||
|
return 13
|
||||||
|
} else if name == SizeNameSuffixText {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
return theme.DefaultTheme().Size(name)
|
return theme.DefaultTheme().Size(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -3,6 +3,7 @@ package widgets
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
@@ -51,13 +52,17 @@ type gridViewAlbumIterator struct {
|
|||||||
func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
|
func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
|
||||||
albums := g.iter.NextN(n)
|
albums := g.iter.NextN(n)
|
||||||
return sharedutil.MapSlice(albums, func(al *mediaprovider.Album) GridViewItemModel {
|
return sharedutil.MapSlice(albums, func(al *mediaprovider.Album) GridViewItemModel {
|
||||||
return GridViewItemModel{
|
model := GridViewItemModel{
|
||||||
Name: al.Name,
|
Name: al.Name,
|
||||||
ID: al.ID,
|
ID: al.ID,
|
||||||
CoverArtID: al.CoverArtID,
|
CoverArtID: al.CoverArtID,
|
||||||
Secondary: al.ArtistNames,
|
Secondary: al.ArtistNames,
|
||||||
SecondaryIDs: al.ArtistIDs,
|
SecondaryIDs: al.ArtistIDs,
|
||||||
}
|
}
|
||||||
|
if al.Year > 0 {
|
||||||
|
model.Suffix = strconv.Itoa(al.Year)
|
||||||
|
}
|
||||||
|
return model
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +97,8 @@ func NewGridViewArtistIterator(iter mediaprovider.ArtistIterator) GridViewIterat
|
|||||||
type GridView struct {
|
type GridView struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
|
ShowSuffix bool
|
||||||
|
|
||||||
stateMutex sync.RWMutex
|
stateMutex sync.RWMutex
|
||||||
fetchCancel context.CancelFunc
|
fetchCancel context.CancelFunc
|
||||||
GridViewState
|
GridViewState
|
||||||
@@ -322,9 +329,11 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
|
|||||||
card.ItemIndex = itemIdx
|
card.ItemIndex = itemIdx
|
||||||
g.itemForIndex[itemIdx] = card
|
g.itemForIndex[itemIdx] = card
|
||||||
card.Cover.Im.PlaceholderIcon = g.Placeholder
|
card.Cover.Im.PlaceholderIcon = g.Placeholder
|
||||||
|
card.ShowSuffix = g.ShowSuffix
|
||||||
if !card.NeedsUpdate(item) && card.ItemIndex == itemIdx {
|
if !card.NeedsUpdate(item) && card.ItemIndex == itemIdx {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
g.stateMutex.Unlock()
|
g.stateMutex.Unlock()
|
||||||
|
card.Refresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.stateMutex.Unlock()
|
g.stateMutex.Unlock()
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/res"
|
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
"fyne.io/fyne/v2/canvas"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
@@ -15,6 +12,10 @@ import (
|
|||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
"github.com/dweymouth/supersonic/res"
|
||||||
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ fyne.Widget = (*GridViewItem)(nil)
|
var _ fyne.Widget = (*GridViewItem)(nil)
|
||||||
@@ -131,15 +132,19 @@ type GridViewItemModel struct {
|
|||||||
CoverArtID string
|
CoverArtID string
|
||||||
Secondary []string
|
Secondary []string
|
||||||
SecondaryIDs []string
|
SecondaryIDs []string
|
||||||
|
Suffix string
|
||||||
}
|
}
|
||||||
|
|
||||||
type GridViewItem struct {
|
type GridViewItem struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
|
ShowSuffix bool
|
||||||
|
|
||||||
itemID string
|
itemID string
|
||||||
secondaryIDs []string
|
secondaryIDs []string
|
||||||
primaryText *widget.Hyperlink
|
primaryText *widget.Hyperlink
|
||||||
secondaryText *MultiHyperlink
|
secondaryText *MultiHyperlink
|
||||||
|
suffix string
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
focused bool
|
focused bool
|
||||||
focusRect *canvas.Rectangle
|
focusRect *canvas.Rectangle
|
||||||
@@ -164,8 +169,10 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
|
|||||||
secondaryText: NewMultiHyperlink(),
|
secondaryText: NewMultiHyperlink(),
|
||||||
Cover: newCoverImage(placeholderResource),
|
Cover: newCoverImage(placeholderResource),
|
||||||
}
|
}
|
||||||
g.primaryText.TextStyle.Bold = true
|
|
||||||
g.primaryText.Truncation = fyne.TextTruncateEllipsis
|
g.primaryText.Truncation = fyne.TextTruncateEllipsis
|
||||||
|
g.primaryText.TextStyle.Bold = true
|
||||||
|
g.secondaryText.SizeName = myTheme.SizeNameSubText
|
||||||
|
g.secondaryText.SuffixSizeName = myTheme.SizeNameSuffixText
|
||||||
g.ExtendBaseWidget(g)
|
g.ExtendBaseWidget(g)
|
||||||
g.Cover.OnPlay = func() {
|
g.Cover.OnPlay = func() {
|
||||||
if g.OnPlay != nil {
|
if g.OnPlay != nil {
|
||||||
@@ -195,7 +202,7 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) createContainer() {
|
func (g *GridViewItem) createContainer() {
|
||||||
info := container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-16), g.primaryText, g.secondaryText)
|
info := container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-17), g.primaryText, g.secondaryText)
|
||||||
g.focusRect = canvas.NewRectangle(color.Transparent)
|
g.focusRect = canvas.NewRectangle(color.Transparent)
|
||||||
g.focusRect.StrokeWidth = 3
|
g.focusRect.StrokeWidth = 3
|
||||||
coverStack := container.NewStack(g.Cover, g.focusRect)
|
coverStack := container.NewStack(g.Cover, g.focusRect)
|
||||||
@@ -205,7 +212,8 @@ func (g *GridViewItem) createContainer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) NeedsUpdate(model GridViewItemModel) bool {
|
func (g *GridViewItem) NeedsUpdate(model GridViewItemModel) bool {
|
||||||
return g.itemID != model.ID || !slices.Equal(g.secondaryIDs, model.SecondaryIDs)
|
return g.itemID != model.ID || !slices.Equal(g.secondaryIDs, model.SecondaryIDs) ||
|
||||||
|
g.secondaryText.Suffix != model.Suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) Update(model GridViewItemModel) {
|
func (g *GridViewItem) Update(model GridViewItemModel) {
|
||||||
@@ -213,6 +221,11 @@ func (g *GridViewItem) Update(model GridViewItemModel) {
|
|||||||
g.secondaryIDs = model.SecondaryIDs
|
g.secondaryIDs = model.SecondaryIDs
|
||||||
g.primaryText.SetText(model.Name)
|
g.primaryText.SetText(model.Name)
|
||||||
g.secondaryText.BuildSegments(model.Secondary, model.SecondaryIDs)
|
g.secondaryText.BuildSegments(model.Secondary, model.SecondaryIDs)
|
||||||
|
if g.ShowSuffix {
|
||||||
|
g.secondaryText.Suffix = model.Suffix
|
||||||
|
} else {
|
||||||
|
g.secondaryText.Suffix = ""
|
||||||
|
}
|
||||||
g.secondaryText.Refresh()
|
g.secondaryText.Refresh()
|
||||||
g.Cover.ResetPlayButton()
|
g.Cover.ResetPlayButton()
|
||||||
if g.focused {
|
if g.focused {
|
||||||
@@ -222,9 +235,15 @@ func (g *GridViewItem) Update(model GridViewItemModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) Refresh() {
|
func (g *GridViewItem) Refresh() {
|
||||||
|
if g.ShowSuffix && g.secondaryText.Suffix == "" && g.suffix != "" {
|
||||||
|
g.secondaryText.Suffix = g.suffix
|
||||||
|
g.secondaryText.Refresh()
|
||||||
|
} else if !g.ShowSuffix && g.secondaryText.Suffix != "" {
|
||||||
|
g.secondaryText.Suffix = ""
|
||||||
|
g.secondaryText.Refresh()
|
||||||
|
}
|
||||||
g.focusRect.StrokeColor = util.MakeOpaque(theme.FocusColor())
|
g.focusRect.StrokeColor = util.MakeOpaque(theme.FocusColor())
|
||||||
g.focusRect.Hidden = !g.focused
|
g.focusRect.Hidden = !g.focused
|
||||||
g.BaseWidget.Refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GridViewItem) ItemID() string {
|
func (g *GridViewItem) ItemID() string {
|
||||||
|
|||||||
@@ -11,8 +11,16 @@ type MultiHyperlink struct {
|
|||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
Segments []MultiHyperlinkSegment
|
Segments []MultiHyperlinkSegment
|
||||||
|
|
||||||
|
// Suffix string that is appended (with · separator)
|
||||||
|
// only if there is enough room
|
||||||
|
Suffix string
|
||||||
|
|
||||||
OnTapped func(string)
|
OnTapped func(string)
|
||||||
|
|
||||||
|
SizeName fyne.ThemeSizeName
|
||||||
|
SuffixSizeName fyne.ThemeSizeName
|
||||||
|
|
||||||
minSegWidthCached float32
|
minSegWidthCached float32
|
||||||
minHeightCached float32
|
minHeightCached float32
|
||||||
separatorWCached float32
|
separatorWCached float32
|
||||||
@@ -20,7 +28,10 @@ type MultiHyperlink struct {
|
|||||||
// TODO: Once https://github.com/fyne-io/fyne/issues/4336 is resolved,
|
// TODO: Once https://github.com/fyne-io/fyne/issues/4336 is resolved,
|
||||||
// we can switch to the much cleaner RichText implementation
|
// we can switch to the much cleaner RichText implementation
|
||||||
//provider *widget.RichText
|
//provider *widget.RichText
|
||||||
content *fyne.Container
|
|
||||||
|
objects []fyne.CanvasObject
|
||||||
|
suffixLabel *widget.RichText
|
||||||
|
content *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
type MultiHyperlinkSegment struct {
|
type MultiHyperlinkSegment struct {
|
||||||
@@ -52,14 +63,14 @@ func (m *MultiHyperlink) BuildSegments(texts, links []string) {
|
|||||||
|
|
||||||
func (c *MultiHyperlink) getMinSegWidth() float32 {
|
func (c *MultiHyperlink) getMinSegWidth() float32 {
|
||||||
if c.minSegWidthCached == 0 {
|
if c.minSegWidthCached == 0 {
|
||||||
c.minSegWidthCached = fyne.MeasureText(", W", theme.TextSize(), fyne.TextStyle{}).Width
|
c.minSegWidthCached = fyne.MeasureText(", W", theme.Size(c.sizeName()), fyne.TextStyle{}).Width
|
||||||
}
|
}
|
||||||
return c.minSegWidthCached
|
return c.minSegWidthCached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MultiHyperlink) getSeparatorWidth() float32 {
|
func (c *MultiHyperlink) getSeparatorWidth() float32 {
|
||||||
if c.separatorWCached == 0 {
|
if c.separatorWCached == 0 {
|
||||||
c.separatorWCached = fyne.MeasureText(",", theme.TextSize(), fyne.TextStyle{}).Width
|
c.separatorWCached = fyne.MeasureText(",", theme.Size(c.sizeName()), fyne.TextStyle{}).Width
|
||||||
}
|
}
|
||||||
return c.separatorWCached
|
return c.separatorWCached
|
||||||
}
|
}
|
||||||
@@ -71,7 +82,7 @@ func (c *MultiHyperlink) layoutObjects() {
|
|||||||
}
|
}
|
||||||
x := float32(0)
|
x := float32(0)
|
||||||
width := c.Size().Width
|
width := c.Size().Width
|
||||||
l := len(c.content.Objects)
|
l := len(c.objects)
|
||||||
|
|
||||||
var i int // at end of loop should be index of last seg that was laid out for display
|
var i int // at end of loop should be index of last seg that was laid out for display
|
||||||
var seg MultiHyperlinkSegment
|
var seg MultiHyperlinkSegment
|
||||||
@@ -87,9 +98,9 @@ func (c *MultiHyperlink) layoutObjects() {
|
|||||||
appendingSegments := 2*i >= l
|
appendingSegments := 2*i >= l
|
||||||
var obj fyne.CanvasObject
|
var obj fyne.CanvasObject
|
||||||
if !appendingSegments {
|
if !appendingSegments {
|
||||||
obj = c.content.Objects[2*i]
|
obj = c.objects[2*i]
|
||||||
} else if i > 0 {
|
} else if i > 0 {
|
||||||
c.content.Objects = append(c.content.Objects, c.newSeparatorLabel())
|
c.objects = append(c.objects, c.newSeparatorLabel())
|
||||||
}
|
}
|
||||||
if seg.LinkID == "" {
|
if seg.LinkID == "" {
|
||||||
obj = c.updateOrReplaceLabel(obj, seg.Text)
|
obj = c.updateOrReplaceLabel(obj, seg.Text)
|
||||||
@@ -97,14 +108,14 @@ func (c *MultiHyperlink) layoutObjects() {
|
|||||||
obj = c.updateOrReplaceHyperlink(obj, seg.Text, seg.LinkID)
|
obj = c.updateOrReplaceHyperlink(obj, seg.Text, seg.LinkID)
|
||||||
}
|
}
|
||||||
if appendingSegments {
|
if appendingSegments {
|
||||||
c.content.Objects = append(c.content.Objects, obj)
|
c.objects = append(c.objects, obj)
|
||||||
} else {
|
} else {
|
||||||
c.content.Objects[2*i] = obj
|
c.objects[2*i] = obj
|
||||||
}
|
}
|
||||||
|
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
// move and resize separator
|
// move and resize separator
|
||||||
obj = c.content.Objects[2*i-1]
|
obj = c.objects[2*i-1]
|
||||||
ms := obj.MinSize()
|
ms := obj.MinSize()
|
||||||
obj.Resize(ms)
|
obj.Resize(ms)
|
||||||
obj.Move(fyne.NewPos(x-ms.Width+c.getSeparatorWidth()+1, 0)) // this is really ugly
|
obj.Move(fyne.NewPos(x-ms.Width+c.getSeparatorWidth()+1, 0)) // this is really ugly
|
||||||
@@ -112,8 +123,8 @@ func (c *MultiHyperlink) layoutObjects() {
|
|||||||
}
|
}
|
||||||
// move and resize text object
|
// move and resize text object
|
||||||
// extra +3 to textW gives it just enough space to not trigger ellipsis truncation
|
// extra +3 to textW gives it just enough space to not trigger ellipsis truncation
|
||||||
textW := fyne.MeasureText(seg.Text, theme.TextSize(), fyne.TextStyle{}).Width + theme.Padding()*2 + theme.InnerPadding() + 3
|
textW := fyne.MeasureText(seg.Text, theme.Size(c.sizeName()), fyne.TextStyle{}).Width + theme.Padding()*2 + theme.InnerPadding() + 3
|
||||||
obj = c.content.Objects[2*i]
|
obj = c.objects[2*i]
|
||||||
ms := obj.MinSize()
|
ms := obj.MinSize()
|
||||||
w := fyne.Min(width-x, textW)
|
w := fyne.Min(width-x, textW)
|
||||||
obj.Resize(fyne.NewSize(w, ms.Height))
|
obj.Resize(fyne.NewSize(w, ms.Height))
|
||||||
@@ -122,17 +133,39 @@ func (c *MultiHyperlink) layoutObjects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
c.content.Objects = c.content.Objects[:2*i-1]
|
c.content.Objects = c.objects[:2*i-1]
|
||||||
|
if i == len(c.Segments) && c.Suffix != "" {
|
||||||
|
if c.suffixLabel == nil {
|
||||||
|
c.suffixLabel = widget.NewRichTextWithText("· " + c.Suffix)
|
||||||
|
} else {
|
||||||
|
c.suffixLabel.Segments[0].(*widget.TextSegment).Text = "· " + c.Suffix
|
||||||
|
}
|
||||||
|
sizeName := c.sizeName()
|
||||||
|
if c.SuffixSizeName != "" {
|
||||||
|
sizeName = c.SuffixSizeName
|
||||||
|
}
|
||||||
|
// TODO: the magic numbers to get exact positioning here are gross
|
||||||
|
c.suffixLabel.Segments[0].(*widget.TextSegment).Style.SizeName = sizeName
|
||||||
|
innerPad2 := theme.InnerPadding() * 2
|
||||||
|
if x+c.suffixLabel.MinSize().Width-innerPad2*1.3 < width {
|
||||||
|
y := theme.Size(c.sizeName()) - theme.Size(sizeName)
|
||||||
|
c.suffixLabel.Move(fyne.NewPos(x-innerPad2+1, y))
|
||||||
|
c.content.Objects = append(c.content.Objects, c.suffixLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MultiHyperlink) updateOrReplaceLabel(obj fyne.CanvasObject, text string) fyne.CanvasObject {
|
func (c *MultiHyperlink) updateOrReplaceLabel(obj fyne.CanvasObject, text string) fyne.CanvasObject {
|
||||||
if obj != nil {
|
if obj != nil {
|
||||||
if label, ok := obj.(*widget.Label); ok {
|
if label, ok := obj.(*widget.RichText); ok {
|
||||||
label.Text = text
|
ts := label.Segments[0].(*widget.TextSegment)
|
||||||
|
ts.Text = text
|
||||||
|
ts.Style.SizeName = c.sizeName()
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
l := widget.NewLabel(text)
|
l := widget.NewRichTextWithText(text)
|
||||||
|
l.Segments[0].(*widget.TextSegment).Style.SizeName = c.sizeName()
|
||||||
l.Truncation = fyne.TextTruncateEllipsis
|
l.Truncation = fyne.TextTruncateEllipsis
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
@@ -141,22 +174,34 @@ func (c *MultiHyperlink) updateOrReplaceHyperlink(obj fyne.CanvasObject, text, l
|
|||||||
if obj != nil {
|
if obj != nil {
|
||||||
if l, ok := obj.(*widget.Hyperlink); ok {
|
if l, ok := obj.(*widget.Hyperlink); ok {
|
||||||
l.Text = text
|
l.Text = text
|
||||||
|
l.SizeName = c.sizeName()
|
||||||
l.OnTapped = func() { c.onSegmentTapped(link) }
|
l.OnTapped = func() { c.onSegmentTapped(link) }
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
l := widget.NewHyperlink(text, nil)
|
l := widget.NewHyperlink(text, nil)
|
||||||
|
l.SizeName = c.sizeName()
|
||||||
l.Truncation = fyne.TextTruncateEllipsis
|
l.Truncation = fyne.TextTruncateEllipsis
|
||||||
l.OnTapped = func() { c.onSegmentTapped(link) }
|
l.OnTapped = func() { c.onSegmentTapped(link) }
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MultiHyperlink) newSeparatorLabel() *widget.Label {
|
func (c *MultiHyperlink) newSeparatorLabel() *widget.RichText {
|
||||||
return widget.NewLabel(", ")
|
rt := widget.NewRichTextWithText(", ")
|
||||||
|
rt.Segments[0].(*widget.TextSegment).Style.SizeName = c.sizeName()
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiHyperlink) sizeName() fyne.ThemeSizeName {
|
||||||
|
if c.SizeName == "" {
|
||||||
|
return theme.SizeNameText
|
||||||
|
}
|
||||||
|
return c.SizeName
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* RichText implementation
|
* RichText implementation
|
||||||
|
* TODO: add support for SizeName, suffix
|
||||||
|
|
||||||
func (c *MultiHyperlink) syncSegments() {
|
func (c *MultiHyperlink) syncSegments() {
|
||||||
l := len(c.provider.Segments)
|
l := len(c.provider.Segments)
|
||||||
|
|||||||
Reference in New Issue
Block a user