From 9928861db90c68631bb9344dad5a656432bebc75 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 29 May 2024 16:56:37 -0700 Subject: [PATCH 01/10] refactor: extract out TracklistRow interface and base impl --- ui/widgets/focuslist.go | 5 + ui/widgets/tracklist.go | 197 +------------------------------ ui/widgets/tracklistrow.go | 233 +++++++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 193 deletions(-) create mode 100644 ui/widgets/tracklistrow.go diff --git a/ui/widgets/focuslist.go b/ui/widgets/focuslist.go index 7b30460..2710704 100644 --- a/ui/widgets/focuslist.go +++ b/ui/widgets/focuslist.go @@ -24,6 +24,7 @@ type FocusList struct { type FocusListRow interface { fyne.Focusable ItemID() widget.ListItemID + SetItemID(widget.ListItemID) } func NewFocusList(len func() int, create func() fyne.CanvasObject, update func(widget.GridWrapItemID, fyne.CanvasObject)) *FocusList { @@ -105,6 +106,10 @@ func (l *FocusListRowBase) ItemID() widget.ListItemID { return l.ListItemID } +func (l *FocusListRowBase) SetItemID(id widget.ListItemID) { + l.ListItemID = id +} + func (l *FocusListRowBase) EnsureUnfocused() { if l.Focused { fyne.CurrentApp().Driver().CanvasForObject(l).Unfocus() diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 0d9dc2e..e607f35 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -1,11 +1,9 @@ package widgets import ( - "fmt" "log" "slices" "sort" - "strconv" "strings" "sync" @@ -146,7 +144,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { t.list = NewFocusList( t.lenTracks, func() fyne.CanvasObject { - tr := NewTrackRow(t, playingIcon) + tr := NewCompactTracklistRow(t, playingIcon) tr.OnTapped = func() { t.onSelectTrack(tr.ListItemID) } @@ -171,10 +169,10 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { model := t.tracks[itemID] t.tracksMutex.RUnlock() - tr := item.(*TrackRow) + tr := item.(TracklistRow) t.list.SetItemForID(itemID, tr) - if tr.trackID != model.Track.ID || tr.ListItemID != itemID { - tr.ListItemID = itemID + if tr.TrackID() != model.Track.ID || tr.ItemID() != itemID { + tr.SetItemID(itemID) } i := -1 // signal that we want to display the actual track num. if t.Options.AutoNumber { @@ -704,190 +702,3 @@ func colName(i int) string { log.Println("notReached: Tracklist.colName") return "" } - -type TrackRow struct { - FocusListRowBase - - // internal state - tracklist *Tracklist - trackNum int - trackID string - isPlaying bool - isFavorite bool - playCount int - - num *widget.Label - name *widget.RichText // for bold support - artist *MultiHyperlink - album *MultiHyperlink // for disabled support, if albumID is "" - dur *widget.Label - year *widget.Label - favorite *fyne.Container - rating *StarRating - bitrate *widget.Label - plays *widget.Label - comment *widget.Label - size *widget.Label - path *widget.Label - - OnTappedSecondary func(e *fyne.PointEvent, trackIdx int) - - playingIcon fyne.CanvasObject -} - -func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow { - t := &TrackRow{tracklist: tracklist, playingIcon: playingIcon} - t.ExtendBaseWidget(t) - t.num = util.NewTrailingAlignLabel() - t.name = util.NewTruncatingRichText() - t.artist = NewMultiHyperlink() - t.artist.OnTapped = tracklist.onArtistTapped - t.album = NewMultiHyperlink() - t.album.OnTapped = func(id string) { tracklist.onAlbumTapped(id) } - t.dur = util.NewTrailingAlignLabel() - t.year = util.NewTrailingAlignLabel() - favorite := NewFavoriteIcon() - favorite.OnTapped = t.toggleFavorited - t.favorite = container.NewCenter(favorite) - t.rating = NewStarRating() - t.rating.IsDisabled = t.tracklist.Options.DisableRating - t.rating.StarSize = 16 - t.rating.OnRatingChanged = t.setTrackRating - t.plays = util.NewTrailingAlignLabel() - t.comment = util.NewTruncatingLabel() - t.bitrate = util.NewTrailingAlignLabel() - t.size = util.NewTrailingAlignLabel() - t.path = util.NewTruncatingLabel() - - t.Content = container.New(tracklist.colLayout, - t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bitrate, t.size, t.path) - return t -} - -func (t *TrackRow) Update(tm *util.TrackListModel, rowNum int) { - changed := false - if tm.Selected != t.Selected { - t.Selected = tm.Selected - changed = true - } - - // Update info that can change if this row is bound to - // a new track (*mediaprovider.Track) - tr := tm.Track - if tr.ID != t.trackID { - t.EnsureUnfocused() - t.trackID = tr.ID - - t.name.Segments[0].(*widget.TextSegment).Text = tr.Name - t.artist.BuildSegments(tr.ArtistNames, tr.ArtistIDs) - t.album.BuildSegments([]string{tr.Album}, []string{tr.AlbumID}) - t.dur.Text = util.SecondsToTimeString(float64(tr.Duration)) - t.year.Text = strconv.Itoa(tr.Year) - t.plays.Text = strconv.Itoa(int(tr.PlayCount)) - t.comment.Text = tr.Comment - t.bitrate.Text = strconv.Itoa(tr.BitRate) - t.size.Text = util.BytesToSizeString(tr.Size) - t.path.Text = tr.FilePath - changed = true - } - - // Update track num if needed - // (which can change based on bound *mediaprovider.Track or tracklist.AutoNumber) - if t.trackNum != rowNum { - discNum := -1 - var str string - if rowNum < 0 { - rowNum = tr.TrackNumber - if t.tracklist.Options.ShowDiscNumber { - discNum = tr.DiscNumber - } - } - t.trackNum = rowNum - if discNum >= 0 { - str = fmt.Sprintf("%d.%02d", discNum, rowNum) - } else { - str = strconv.Itoa(rowNum) - } - t.num.Text = str - changed = true - } - - // Update play count if needed - if tr.PlayCount != t.playCount { - t.playCount = tr.PlayCount - t.plays.Text = strconv.Itoa(int(tr.PlayCount)) - changed = true - } - - // Render whether track is playing or not - if isPlaying := t.tracklist.nowPlayingID == tr.ID; isPlaying != t.isPlaying { - t.isPlaying = isPlaying - t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying - - if isPlaying { - t.Content.(*fyne.Container).Objects[0] = t.playingIcon - } else { - t.Content.(*fyne.Container).Objects[0] = t.num - } - changed = true - } - - // Update favorite column - if tr.Favorite != t.isFavorite { - t.isFavorite = tr.Favorite - t.favorite.Objects[0].(*FavoriteIcon).Favorite = tr.Favorite - changed = true - } - - // Update rating column - if t.rating.Rating != tr.Rating { - t.rating.Rating = tr.Rating - t.rating.Refresh() - } - if t.rating.IsDisabled != t.tracklist.Options.DisableRating { - t.rating.IsDisabled = t.tracklist.Options.DisableRating - t.rating.Refresh() - } - - // Show only columns configured to be visible - updateHidden := func(hiddenPtr *bool, colName string) { - colHidden := !t.tracklist.visibleColumns[ColNumber(colName)] - if colHidden != *hiddenPtr { - *hiddenPtr = colHidden - changed = true - } - } - updateHidden(&t.artist.Hidden, ColumnArtist) - updateHidden(&t.album.Hidden, ColumnAlbum) - updateHidden(&t.dur.Hidden, ColumnTime) - updateHidden(&t.year.Hidden, ColumnYear) - updateHidden(&t.favorite.Hidden, ColumnFavorite) - updateHidden(&t.rating.Hidden, ColumnRating) - updateHidden(&t.plays.Hidden, ColumnPlays) - updateHidden(&t.comment.Hidden, ColumnComment) - updateHidden(&t.bitrate.Hidden, ColumnBitrate) - updateHidden(&t.size.Hidden, ColumnSize) - updateHidden(&t.path.Hidden, ColumnPath) - - if changed { - t.Refresh() - } -} - -func (t *TrackRow) toggleFavorited() { - t.isFavorite = !t.isFavorite - favIcon := t.favorite.Objects[0].(*FavoriteIcon) - favIcon.Favorite = t.isFavorite - t.favorite.Refresh() - t.tracklist.onSetFavorite(t.trackID, t.isFavorite) -} - -func (t *TrackRow) setTrackRating(rating int) { - t.tracklist.onSetRating(t.trackID, rating) -} - -func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) { - if t.OnTappedSecondary != nil { - t.OnTappedSecondary(e, t.ListItemID) - } -} diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go new file mode 100644 index 0000000..8d6ccb6 --- /dev/null +++ b/ui/widgets/tracklistrow.go @@ -0,0 +1,233 @@ +package widgets + +import ( + "fmt" + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/ui/util" +) + +type tracklistRowBase struct { + FocusListRowBase + + OnTappedSecondary func(e *fyne.PointEvent, trackIdx int) + + // set by extending widget + playingIcon fyne.CanvasObject + + // internal state + tracklist *Tracklist + trackNum int + trackID string + isPlaying bool + isFavorite bool + playCount int + + num *widget.Label + name *widget.RichText // for bold support + artist *MultiHyperlink + album *MultiHyperlink // for disabled support, if albumID is "" + dur *widget.Label + year *widget.Label + favorite *fyne.Container + rating *StarRating + bitrate *widget.Label + plays *widget.Label + comment *widget.Label + size *widget.Label + path *widget.Label +} + +type TracklistRow interface { + FocusListRow + + TrackID() string + Update(model *util.TrackListModel, rowNum int) +} + +type ExpandedTracklistRow struct { + tracklistRowBase +} + +type CompactTracklistRow struct { + tracklistRowBase +} + +var ( + _ TracklistRow = (*CompactTracklistRow)(nil) + _ TracklistRow = (*ExpandedTracklistRow)(nil) +) + +func NewTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *ExpandedTracklistRow { + return nil +} + +func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *CompactTracklistRow { + t := &CompactTracklistRow{} + t.ExtendBaseWidget(t) + t.tracklistRowBase.create(tracklist) + t.playingIcon = playingIcon + + t.Content = container.New(tracklist.colLayout, + t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bitrate, t.size, t.path) + return t +} + +func (t *tracklistRowBase) create(tracklist *Tracklist) { + t.tracklist = tracklist + t.num = util.NewTrailingAlignLabel() + t.name = util.NewTruncatingRichText() + t.artist = NewMultiHyperlink() + t.artist.OnTapped = tracklist.onArtistTapped + t.album = NewMultiHyperlink() + t.album.OnTapped = func(id string) { tracklist.onAlbumTapped(id) } + t.dur = util.NewTrailingAlignLabel() + t.year = util.NewTrailingAlignLabel() + favorite := NewFavoriteIcon() + favorite.OnTapped = t.toggleFavorited + t.favorite = container.NewCenter(favorite) + t.rating = NewStarRating() + t.rating.IsDisabled = t.tracklist.Options.DisableRating + t.rating.StarSize = 16 + t.rating.OnRatingChanged = t.setTrackRating + t.plays = util.NewTrailingAlignLabel() + t.comment = util.NewTruncatingLabel() + t.bitrate = util.NewTrailingAlignLabel() + t.size = util.NewTrailingAlignLabel() + t.path = util.NewTruncatingLabel() +} + +func (t *tracklistRowBase) TrackID() string { + return t.trackID +} + +func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int) { + changed := false + if tm.Selected != t.Selected { + t.Selected = tm.Selected + changed = true + } + + // Update info that can change if this row is bound to + // a new track (*mediaprovider.Track) + tr := tm.Track + if tr.ID != t.trackID { + t.EnsureUnfocused() + t.trackID = tr.ID + + t.name.Segments[0].(*widget.TextSegment).Text = tr.Name + t.artist.BuildSegments(tr.ArtistNames, tr.ArtistIDs) + t.album.BuildSegments([]string{tr.Album}, []string{tr.AlbumID}) + t.dur.Text = util.SecondsToTimeString(float64(tr.Duration)) + t.year.Text = strconv.Itoa(tr.Year) + t.plays.Text = strconv.Itoa(int(tr.PlayCount)) + t.comment.Text = tr.Comment + t.bitrate.Text = strconv.Itoa(tr.BitRate) + t.size.Text = util.BytesToSizeString(tr.Size) + t.path.Text = tr.FilePath + changed = true + } + + // Update track num if needed + // (which can change based on bound *mediaprovider.Track or tracklist.AutoNumber) + if t.trackNum != rowNum { + discNum := -1 + var str string + if rowNum < 0 { + rowNum = tr.TrackNumber + if t.tracklist.Options.ShowDiscNumber { + discNum = tr.DiscNumber + } + } + t.trackNum = rowNum + if discNum >= 0 { + str = fmt.Sprintf("%d.%02d", discNum, rowNum) + } else { + str = strconv.Itoa(rowNum) + } + t.num.Text = str + changed = true + } + + // Update play count if needed + if tr.PlayCount != t.playCount { + t.playCount = tr.PlayCount + t.plays.Text = strconv.Itoa(int(tr.PlayCount)) + changed = true + } + + // Render whether track is playing or not + if isPlaying := t.tracklist.nowPlayingID == tr.ID; isPlaying != t.isPlaying { + t.isPlaying = isPlaying + t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + + if isPlaying { + t.Content.(*fyne.Container).Objects[0] = t.playingIcon + } else { + t.Content.(*fyne.Container).Objects[0] = t.num + } + changed = true + } + + // Update favorite column + if tr.Favorite != t.isFavorite { + t.isFavorite = tr.Favorite + t.favorite.Objects[0].(*FavoriteIcon).Favorite = tr.Favorite + changed = true + } + + // Update rating column + if t.rating.Rating != tr.Rating { + t.rating.Rating = tr.Rating + t.rating.Refresh() + } + if t.rating.IsDisabled != t.tracklist.Options.DisableRating { + t.rating.IsDisabled = t.tracklist.Options.DisableRating + t.rating.Refresh() + } + + // Show only columns configured to be visible + updateHidden := func(hiddenPtr *bool, colName string) { + colHidden := !t.tracklist.visibleColumns[ColNumber(colName)] + if colHidden != *hiddenPtr { + *hiddenPtr = colHidden + changed = true + } + } + updateHidden(&t.artist.Hidden, ColumnArtist) + updateHidden(&t.album.Hidden, ColumnAlbum) + updateHidden(&t.dur.Hidden, ColumnTime) + updateHidden(&t.year.Hidden, ColumnYear) + updateHidden(&t.favorite.Hidden, ColumnFavorite) + updateHidden(&t.rating.Hidden, ColumnRating) + updateHidden(&t.plays.Hidden, ColumnPlays) + updateHidden(&t.comment.Hidden, ColumnComment) + updateHidden(&t.bitrate.Hidden, ColumnBitrate) + updateHidden(&t.size.Hidden, ColumnSize) + updateHidden(&t.path.Hidden, ColumnPath) + + if changed { + t.Refresh() + } +} + +func (t *tracklistRowBase) toggleFavorited() { + t.isFavorite = !t.isFavorite + favIcon := t.favorite.Objects[0].(*FavoriteIcon) + favIcon.Favorite = t.isFavorite + t.favorite.Refresh() + t.tracklist.onSetFavorite(t.trackID, t.isFavorite) +} + +func (t *tracklistRowBase) setTrackRating(rating int) { + t.tracklist.onSetRating(t.trackID, rating) +} + +func (t *tracklistRowBase) TappedSecondary(e *fyne.PointEvent) { + if t.OnTappedSecondary != nil { + t.OnTappedSecondary(e, t.ListItemID) + } +} From a9b6d0cc2ae83caf47a48eb5753572b9c88de139 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 29 May 2024 18:57:36 -0700 Subject: [PATCH 02/10] begin expandedtracklistrow --- ui/widgets/playqueuelist.go | 4 +--- ui/widgets/tracklistrow.go | 46 +++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 314f55e..1d3de92 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -21,8 +21,6 @@ import ( "github.com/dweymouth/supersonic/ui/util" ) -const thumbnailSize = 52 - type PlayQueueList struct { widget.BaseWidget @@ -298,7 +296,7 @@ func NewPlayQueueListRow(playQueueList *PlayQueueList, im *backend.ImageManager, playingIcon: playingIcon, playQueueList: playQueueList, num: widget.NewLabel(""), - cover: NewImagePlaceholder(myTheme.TracksIcon, thumbnailSize), + cover: NewImagePlaceholder(myTheme.TracksIcon, tracklistThumbnailSize), title: util.NewTruncatingLabel(), artist: NewMultiHyperlink(), time: util.NewTrailingAlignLabel(), diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index 8d6ccb6..e84005f 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -2,14 +2,21 @@ package widgets import ( "fmt" + "image" "strconv" "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" + myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" ) +const tracklistThumbnailSize = 52 + type tracklistRowBase struct { FocusListRowBase @@ -50,6 +57,9 @@ type TracklistRow interface { type ExpandedTracklistRow struct { tracklistRowBase + + img *ImagePlaceholder + imageLoader util.ThumbnailLoader } type CompactTracklistRow struct { @@ -61,8 +71,36 @@ var ( _ TracklistRow = (*ExpandedTracklistRow)(nil) ) -func NewTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *ExpandedTracklistRow { - return nil +func NewExpandedTracklistRow(tracklist *Tracklist, im *backend.ImageManager, playingIcon fyne.CanvasObject) *ExpandedTracklistRow { + t := &ExpandedTracklistRow{} + t.ExtendBaseWidget(t) + t.tracklistRowBase.create(tracklist) + t.playingIcon = playingIcon + t.img = NewImagePlaceholder(myTheme.TracksIcon, tracklistThumbnailSize) + + t.imageLoader = util.NewThumbnailLoader(im, func(i image.Image) { + t.img.SetImage(i, false) + }) + t.imageLoader.OnBeforeLoad = func() { + t.img.SetImage(nil, false) + } + + titleArtistImg := container.NewBorder(nil, nil, + container.NewPadded(t.img) /*left*/, nil, + container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-15), + t.name, t.artist)) + + v := makeVerticallyCentered // func alias + t.Content = container.New(tracklist.colLayout, + v(t.num), titleArtistImg, v(t.album), v(t.dur), v(t.year), v(t.favorite), v(t.rating), v(t.plays), v(t.comment), v(t.bitrate), v(t.size), v(t.path)) + return t +} + +func (t *ExpandedTracklistRow) Update(tm *util.TrackListModel, rowNum int) { + if t.trackID != tm.Track.ID { + t.imageLoader.Load(tm.Track.CoverArtID) + } + t.tracklistRowBase.Update(tm, rowNum) } func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *CompactTracklistRow { @@ -231,3 +269,7 @@ func (t *tracklistRowBase) TappedSecondary(e *fyne.PointEvent) { t.OnTappedSecondary(e, t.ListItemID) } } + +func makeVerticallyCentered(obj fyne.CanvasObject) fyne.CanvasObject { + return container.NewVBox(layout.NewSpacer(), obj, layout.NewSpacer()) +} From 680cfb42d236d3c0966cfd2734ff32fa137fb7d3 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 29 May 2024 20:04:06 -0700 Subject: [PATCH 03/10] WIP - doesn't quite work yet --- ui/browsing/albumpage.go | 4 +- ui/browsing/artistpage.go | 2 +- ui/browsing/favoritespage.go | 2 +- ui/browsing/playlistpage.go | 2 +- ui/browsing/router.go | 2 +- ui/browsing/trackspage.go | 9 +-- ui/util/widgetpool.go | 1 + ui/widgets/focuslist.go | 16 ++++++ ui/widgets/playqueuelist.go | 4 +- ui/widgets/tracklist.go | 104 ++++++++++++++++++++++++----------- ui/widgets/tracklistrow.go | 10 +++- 11 files changed, 112 insertions(+), 44 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 25ed12a..3efc688 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -90,7 +90,7 @@ func newAlbumPage( a.tracklist = t.(*widgets.Tracklist) a.tracklist.Reset() } else { - a.tracklist = widgets.NewTracklist(nil) + a.tracklist = widgets.NewTracklist(nil, a.im, true) } a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns) a.tracklist.SetSorting(sort) @@ -122,7 +122,7 @@ func (a *AlbumPage) Save() SavedPage { a.header.page = nil a.pool.Release(util.WidgetTypeAlbumPageHeader, a.header) a.tracklist.Clear() - a.pool.Release(util.WidgetTypeTracklist, a.tracklist) + a.pool.Release(util.WidgetTypeCompactTracklist, a.tracklist) return &s } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 180d5a1..35439fd 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -233,7 +233,7 @@ func (a *ArtistPage) showTopTracks() { tl.Reset() tl.SetTracks(ts) } else { - tl = widgets.NewTracklist(ts) + tl = widgets.NewTracklist(ts, a.im, false) } tl.Options = widgets.TracklistOptions{AutoNumber: true} _, canRate := a.mp.(mediaprovider.SupportsRating) diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index f026264..48c60d8 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -397,7 +397,7 @@ func (a *FavoritesPage) onShowFavoriteSongs() { tracklist.Reset() tracklist.SetTracks(fav.Tracks) } else { - tracklist = widgets.NewTracklist(fav.Tracks) + tracklist = widgets.NewTracklist(fav.Tracks, a.im, false) } tracklist.Options = widgets.TracklistOptions{AutoNumber: true} _, canRate := a.mp.(mediaprovider.SupportsRating) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 243a0b8..104e23d 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -79,7 +79,7 @@ func newPlaylistPage( a.tracklist = tl.(*widgets.Tracklist) a.tracklist.Reset() } else { - a.tracklist = widgets.NewTracklist(nil) + a.tracklist = widgets.NewTracklist(nil, a.im, false) } a.tracklist.SetVisibleColumns(conf.TracklistColumns) a.tracklist.SetSorting(trackSort) diff --git a/ui/browsing/router.go b/ui/browsing/router.go index d8967c0..fd7089a 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -54,7 +54,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Playlists: return NewPlaylistsPage(r.Controller, r.widgetPool, &r.App.Config.PlaylistsPage, r.App.ServerManager.Server) case controller.Tracks: - return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.widgetPool, r.App.ServerManager.Server) + return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.widgetPool, r.App.ServerManager.Server, r.App.ImageManager) } return nil } diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index a9e4809..fea8fdc 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -38,12 +38,13 @@ type tracksPageState struct { contr *controller.Controller conf *backend.TracksPageConfig mp mediaprovider.MediaProvider + im *backend.ImageManager canRate bool canShare bool } -func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage { - t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, widgetPool: pool, mp: mp}} +func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider, im *backend.ImageManager) *TracksPage { + t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, widgetPool: pool, mp: mp, im: im}} t.ExtendBaseWidget(t) t.tracklist = t.obtainTracklist() @@ -175,7 +176,7 @@ func (t *TracksPage) Save() SavedPage { } func (s *tracksPageState) Restore() Page { - t := NewTracksPage(s.contr, s.conf, s.widgetPool, s.mp) + t := NewTracksPage(s.contr, s.conf, s.widgetPool, s.mp, s.im) t.searchText = s.searchText if t.searchText != "" { t.searcher.Entry.Text = t.searchText @@ -194,5 +195,5 @@ func (t *TracksPage) obtainTracklist() *widgets.Tracklist { tracklist.Reset() return tracklist } - return widgets.NewTracklist(nil) + return widgets.NewTracklist(nil, t.im, false) } diff --git a/ui/util/widgetpool.go b/ui/util/widgetpool.go index b8c574e..f79d247 100644 --- a/ui/util/widgetpool.go +++ b/ui/util/widgetpool.go @@ -15,6 +15,7 @@ const ( WidgetTypeGridView WidgetTypePlaylistPageHeader WidgetTypeTracklist + WidgetTypeCompactTracklist WidgetTypeNowPlayingPage // keep at bottom diff --git a/ui/widgets/focuslist.go b/ui/widgets/focuslist.go index 2710704..6dabac7 100644 --- a/ui/widgets/focuslist.go +++ b/ui/widgets/focuslist.go @@ -25,6 +25,10 @@ type FocusListRow interface { fyne.Focusable ItemID() widget.ListItemID SetItemID(widget.ListItemID) + + SetOnTapped(func()) + SetOnDoubleTapped(func()) + SetOnFocusNeighbor(func(up bool)) } func NewFocusList(len func() int, create func() fyne.CanvasObject, update func(widget.GridWrapItemID, fyne.CanvasObject)) *FocusList { @@ -102,6 +106,18 @@ type FocusListRowBase struct { selectionRect *canvas.Rectangle } +func (l *FocusListRowBase) SetOnTapped(f func()) { + l.OnTapped = f +} + +func (l *FocusListRowBase) SetOnDoubleTapped(f func()) { + l.OnDoubleTapped = f +} + +func (l *FocusListRowBase) SetOnFocusNeighbor(f func(up bool)) { + l.OnFocusNeighbor = f +} + func (l *FocusListRowBase) ItemID() widget.ListItemID { return l.ListItemID } diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 1d3de92..2c9d80f 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -21,6 +21,8 @@ import ( "github.com/dweymouth/supersonic/ui/util" ) +const playQueueListThumbnailSize = 52 + type PlayQueueList struct { widget.BaseWidget @@ -296,7 +298,7 @@ func NewPlayQueueListRow(playQueueList *PlayQueueList, im *backend.ImageManager, playingIcon: playingIcon, playQueueList: playQueueList, num: widget.NewLabel(""), - cover: NewImagePlaceholder(myTheme.TracksIcon, tracklistThumbnailSize), + cover: NewImagePlaceholder(myTheme.TracksIcon, playQueueListThumbnailSize), title: util.NewTruncatingLabel(), artist: NewMultiHyperlink(), time: util.NewTrailingAlignLabel(), diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index e607f35..0268fde 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -7,6 +7,7 @@ import ( "strings" "sync" + "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/layouts" @@ -79,6 +80,8 @@ type TracklistOptions struct { type Tracklist struct { widget.BaseWidget + compactRows bool + Options TracklistOptions // user action callbacks @@ -118,16 +121,22 @@ type Tracklist struct { container *fyne.Container } -func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { - t := &Tracklist{visibleColumns: make([]bool, numColumns)} +func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCompactRows bool) *Tracklist { + t := &Tracklist{visibleColumns: make([]bool, numColumns), compactRows: useCompactRows} t.ExtendBaseWidget(t) if len(tracks) > 0 { t._setTracks(tracks) } - // #, Title, Artist, Album, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path - t.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1}) + // #, Title/Artist, Album, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path + colWidths := []float32{40, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1} + if useCompactRows { + // #, Title, Artist, Album, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path + colWidths = []float32{40, -1, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1} + } + + t.colLayout = layouts.NewColumnsLayout(colWidths) t.buildHeader() t.hdr.OnColumnSortChanged = t.onSorted t.hdr.OnColumnVisibilityChanged = t.setColumnVisible @@ -144,17 +153,22 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { t.list = NewFocusList( t.lenTracks, func() fyne.CanvasObject { - tr := NewCompactTracklistRow(t, playingIcon) - tr.OnTapped = func() { - t.onSelectTrack(tr.ListItemID) - } - tr.OnTappedSecondary = t.onShowContextMenu - tr.OnDoubleTapped = func() { - t.onPlayTrackAt(tr.ListItemID) - } - tr.OnFocusNeighbor = func(up bool) { - t.list.FocusNeighbor(tr.ListItemID, up) + var tr TracklistRow + if t.compactRows { + tr = NewCompactTracklistRow(t, playingIcon) + } else { + tr = NewExpandedTracklistRow(t, im, playingIcon) } + tr.SetOnTapped(func() { + t.onSelectTrack(tr.ItemID()) + }) + tr.SetOnTappedSecondary(t.onShowContextMenu) + tr.SetOnDoubleTapped(func() { + t.onPlayTrackAt(tr.ItemID()) + }) + tr.SetOnFocusNeighbor(func(up bool) { + t.list.FocusNeighbor(tr.ItemID(), up) + }) return tr }, func(itemID widget.ListItemID, item fyne.CanvasObject) { @@ -199,21 +213,31 @@ func (t *Tracklist) Scroll(amount float32) { } func (t *Tracklist) buildHeader() { - t.hdr = NewListHeader([]ListColumn{ - {Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}, - {Text: "Title", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, - {Text: "Artist", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - {Text: "Album", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - {Text: "Time", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - {Text: "Year", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - {Text: " Fav.", Alignment: fyne.TextAlignCenter, CanToggleVisible: true}, - {Text: "Rating", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - {Text: "Plays", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - {Text: "Comment", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - {Text: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - {Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - {Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, - t.colLayout) + cols := []ListColumn{{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}} + if t.compactRows { + cols = append(cols, + ListColumn{Text: "Title", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, + ListColumn{Text: "Artist", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, + ) + } else { + cols = append(cols, + ListColumn{Text: " Title / Aritst", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, + ) + } + cols = append(cols, + ListColumn{Text: "Album", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, + ListColumn{Text: "Time", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, + ListColumn{Text: "Year", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, + ListColumn{Text: " Fav.", Alignment: fyne.TextAlignCenter, CanToggleVisible: true}, + ListColumn{Text: "Rating", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, + ListColumn{Text: "Plays", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, + ListColumn{Text: "Comment", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, + ListColumn{Text: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, + ListColumn{Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, + ListColumn{Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, + ) + + t.hdr = NewListHeader(cols, t.colLayout) } // Gets the track at the given index. Thread-safe. @@ -230,15 +254,30 @@ func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track { func (t *Tracklist) SetVisibleColumns(cols []string) { t.visibleColumns[0] = true t.visibleColumns[1] = true - for i := 2; i < len(t.visibleColumns); i++ { - t.visibleColumns[i] = false + if !t.compactRows { + // hard-code artist column to visible since it's part of title col + t.visibleColumns[2] = true + } + + l := len(t.visibleColumns) + if !t.compactRows { + l-- // expanded rows have one fewer column due to merged title/artist + } + for i := 2; i < l; i++ { t.hdr.SetColumnVisible(i, false) } for _, col := range cols { if num := ColNumber(col); num < 0 { log.Printf("Unknown tracklist column %q", col) } else { + if num == 2 && !t.compactRows { + // Artist column is hard-coded visible (part of title), skip + continue + } t.visibleColumns[num] = true + if !t.compactRows && num >= 3 { + num -= 1 // shift cols by 1 to account for Title/Artist combined col + } t.hdr.SetColumnVisible(num, true) } } @@ -259,6 +298,9 @@ func (t *Tracklist) setColumnVisible(colNum int, vis bool) { log.Printf("error: Tracklist.SetColumnVisible: column index %d out of range", colNum) return } + if !t.compactRows && colNum >= 2 { + colNum++ // account for off-by-one from merged title/artist column + } t.visibleColumns[colNum] = vis t.list.Refresh() if t.OnVisibleColumnsChanged != nil { diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index e84005f..0cec7df 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -15,7 +15,7 @@ import ( "github.com/dweymouth/supersonic/ui/util" ) -const tracklistThumbnailSize = 52 +const tracklistThumbnailSize = 48 type tracklistRowBase struct { FocusListRowBase @@ -49,8 +49,11 @@ type tracklistRowBase struct { } type TracklistRow interface { + fyne.CanvasObject FocusListRow + SetOnTappedSecondary(func(_ *fyne.PointEvent, trackNum int)) + TrackID() string Update(model *util.TrackListModel, rowNum int) } @@ -87,7 +90,7 @@ func NewExpandedTracklistRow(tracklist *Tracklist, im *backend.ImageManager, pla titleArtistImg := container.NewBorder(nil, nil, container.NewPadded(t.img) /*left*/, nil, - container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-15), + container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-16), t.name, t.artist)) v := makeVerticallyCentered // func alias @@ -138,6 +141,9 @@ func (t *tracklistRowBase) create(tracklist *Tracklist) { t.path = util.NewTruncatingLabel() } +func (t *tracklistRowBase) SetOnTappedSecondary(f func(*fyne.PointEvent, int)) { + t.OnTappedSecondary = f +} func (t *tracklistRowBase) TrackID() string { return t.trackID } From 4e531e5b4b0859f454883f16475c9d3b59539486 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 May 2024 08:32:50 -0700 Subject: [PATCH 04/10] get column hiding working properly with expanded track rows --- ui/widgets/tracklistrow.go | 59 +++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index 0cec7df..fecda8d 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -46,6 +46,8 @@ type tracklistRowBase struct { comment *widget.Label size *widget.Label path *widget.Label + + colHiddenPtrMap map[string]*bool } type TracklistRow interface { @@ -94,8 +96,23 @@ func NewExpandedTracklistRow(tracklist *Tracklist, im *backend.ImageManager, pla t.name, t.artist)) v := makeVerticallyCentered // func alias - t.Content = container.New(tracklist.colLayout, + container := container.New(tracklist.colLayout, v(t.num), titleArtistImg, v(t.album), v(t.dur), v(t.year), v(t.favorite), v(t.rating), v(t.plays), v(t.comment), v(t.bitrate), v(t.size), v(t.path)) + t.Content = container + var dummy bool + t.colHiddenPtrMap = map[string]*bool{ + ColumnArtist: &dummy, // artist column is never hidden + ColumnAlbum: &container.Objects[2].(*fyne.Container).Hidden, + ColumnTime: &container.Objects[3].(*fyne.Container).Hidden, + ColumnYear: &container.Objects[4].(*fyne.Container).Hidden, + ColumnFavorite: &container.Objects[5].(*fyne.Container).Hidden, + ColumnRating: &container.Objects[6].(*fyne.Container).Hidden, + ColumnPlays: &container.Objects[7].(*fyne.Container).Hidden, + ColumnComment: &container.Objects[8].(*fyne.Container).Hidden, + ColumnBitrate: &container.Objects[9].(*fyne.Container).Hidden, + ColumnSize: &container.Objects[10].(*fyne.Container).Hidden, + ColumnPath: &container.Objects[11].(*fyne.Container).Hidden, + } return t } @@ -109,11 +126,26 @@ func (t *ExpandedTracklistRow) Update(tm *util.TrackListModel, rowNum int) { func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *CompactTracklistRow { t := &CompactTracklistRow{} t.ExtendBaseWidget(t) + t.tracklistRowBase.create(tracklist) t.playingIcon = playingIcon t.Content = container.New(tracklist.colLayout, t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bitrate, t.size, t.path) + + t.colHiddenPtrMap = map[string]*bool{ + ColumnArtist: &t.artist.Hidden, + ColumnAlbum: &t.album.Hidden, + ColumnTime: &t.dur.Hidden, + ColumnYear: &t.year.Hidden, + ColumnFavorite: &t.favorite.Hidden, + ColumnRating: &t.rating.Hidden, + ColumnPlays: &t.plays.Hidden, + ColumnComment: &t.comment.Hidden, + ColumnBitrate: &t.bitrate.Hidden, + ColumnSize: &t.size.Hidden, + ColumnPath: &t.path.Hidden, + } return t } @@ -234,24 +266,25 @@ func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int) { } // Show only columns configured to be visible - updateHidden := func(hiddenPtr *bool, colName string) { + updateHidden := func(colName string) { colHidden := !t.tracklist.visibleColumns[ColNumber(colName)] + hiddenPtr := t.colHiddenPtrMap[colName] if colHidden != *hiddenPtr { *hiddenPtr = colHidden changed = true } } - updateHidden(&t.artist.Hidden, ColumnArtist) - updateHidden(&t.album.Hidden, ColumnAlbum) - updateHidden(&t.dur.Hidden, ColumnTime) - updateHidden(&t.year.Hidden, ColumnYear) - updateHidden(&t.favorite.Hidden, ColumnFavorite) - updateHidden(&t.rating.Hidden, ColumnRating) - updateHidden(&t.plays.Hidden, ColumnPlays) - updateHidden(&t.comment.Hidden, ColumnComment) - updateHidden(&t.bitrate.Hidden, ColumnBitrate) - updateHidden(&t.size.Hidden, ColumnSize) - updateHidden(&t.path.Hidden, ColumnPath) + updateHidden(ColumnArtist) + updateHidden(ColumnAlbum) + updateHidden(ColumnTime) + updateHidden(ColumnYear) + updateHidden(ColumnFavorite) + updateHidden(ColumnRating) + updateHidden(ColumnPlays) + updateHidden(ColumnComment) + updateHidden(ColumnBitrate) + updateHidden(ColumnSize) + updateHidden(ColumnPath) if changed { t.Refresh() From 6ad98af466e6520f2886047037d0bd096cc77554 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 May 2024 08:58:59 -0700 Subject: [PATCH 05/10] fix sorting columns in expanded rows tracklist --- ui/widgets/tracklist.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 0268fde..aa6497c 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -287,7 +287,7 @@ func (t *Tracklist) VisibleColumns() []string { var cols []string for i := 2; i < len(t.visibleColumns); i++ { if t.visibleColumns[i] { - cols = append(cols, string(colName(i))) + cols = append(cols, string(t.colName(i, false))) } } return cols @@ -511,7 +511,7 @@ func (t *Tracklist) doSortTracks() { } func (t *Tracklist) onSorted(sort ListHeaderSort) { - t.sorting = TracklistSort{ColumnName: colName(sort.ColNumber), SortOrder: sort.Type} + t.sorting = TracklistSort{ColumnName: t.colName(sort.ColNumber, true), SortOrder: sort.Type} t.tracksMutex.Lock() t.doSortTracks() t.tracksMutex.Unlock() @@ -737,7 +737,10 @@ func ColNumber(colName string) int { return i } -func colName(i int) string { +func (t *Tracklist) colName(i int, adjustForExpandedRows bool) string { + if adjustForExpandedRows && !t.compactRows && i >= 2 { + i += 1 // adjust for off-by-one from combined title/artist column + } if i < len(columns) { return columns[i] } From d51e4d216a7579f85febbff927b2dc2520cfb296 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 May 2024 18:29:25 -0700 Subject: [PATCH 06/10] much cleaner approach to handling different columns between compact/expanded tracklists --- backend/config.go | 6 +- ui/widgets/tracklist.go | 120 ++++++++------------------------- ui/widgets/tracklistrow.go | 132 +++++++++++++++++++++++++------------ 3 files changed, 120 insertions(+), 138 deletions(-) diff --git a/backend/config.go b/backend/config.go index 0b1fe9a..b232ef9 100644 --- a/backend/config.go +++ b/backend/config.go @@ -176,11 +176,11 @@ func DefaultConfig(appVersionTag string) *Config { SortOrder: string("Name (A-Z)"), }, FavoritesPage: FavoritesPageConfig{ - TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, + TracklistColumns: []string{"Album", "Time", "Plays"}, InitialView: "Albums", }, PlaylistPage: PlaylistPageConfig{ - TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, + TracklistColumns: []string{"Album", "Time", "Plays"}, }, PlaylistsPage: PlaylistsPageConfig{ InitialView: "List", @@ -189,7 +189,7 @@ func DefaultConfig(appVersionTag string) *Config { InitialView: "Play Queue", }, TracksPage: TracksPageConfig{ - TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, + TracklistColumns: []string{"Album", "Time", "Plays"}, }, LocalPlayback: LocalPlaybackConfig{ // "auto" is the name to pass to MPV for autoselecting the output device diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index aa6497c..98c0ec3 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -22,29 +22,6 @@ import ( "fyne.io/fyne/v2/widget" ) -const ( - ColumnNum = "Num" - ColumnTitle = "Title" - ColumnArtist = "Artist" - ColumnAlbum = "Album" - ColumnTime = "Time" - ColumnYear = "Year" - ColumnFavorite = "Favorite" - ColumnRating = "Rating" - ColumnPlays = "Plays" - ColumnComment = "Comment" - ColumnBitrate = "Bitrate" - ColumnSize = "Size" - ColumnPath = "Path" - - numColumns = 13 -) - -var columns = []string{ - ColumnNum, ColumnTitle, ColumnArtist, ColumnAlbum, ColumnTime, ColumnYear, ColumnFavorite, - ColumnRating, ColumnPlays, ColumnComment, ColumnBitrate, ColumnSize, ColumnPath, -} - type TracklistSort struct { SortOrder SortType ColumnName string @@ -81,6 +58,7 @@ type Tracklist struct { widget.BaseWidget compactRows bool + columns []TracklistColumn Options TracklistOptions @@ -122,22 +100,23 @@ type Tracklist struct { } func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCompactRows bool) *Tracklist { - t := &Tracklist{visibleColumns: make([]bool, numColumns), compactRows: useCompactRows} + t := &Tracklist{compactRows: useCompactRows} + t.columns = ExpandedTracklistRowColumns + colWidths := ExpandedTracklistRowColumnWidths + if useCompactRows { + t.columns = CompactTracklistRowColumns + colWidths = CompactTracklistRowColumnWidths + } + t.visibleColumns = make([]bool, len(t.columns)) t.ExtendBaseWidget(t) if len(tracks) > 0 { t._setTracks(tracks) } - // #, Title/Artist, Album, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path - colWidths := []float32{40, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1} - if useCompactRows { - // #, Title, Artist, Album, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path - colWidths = []float32{40, -1, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1} - } - t.colLayout = layouts.NewColumnsLayout(colWidths) - t.buildHeader() + t.hdr = NewListHeader(sharedutil.MapSlice(t.columns, + func(t TracklistColumn) ListColumn { return t.Col }), t.colLayout) t.hdr.OnColumnSortChanged = t.onSorted t.hdr.OnColumnVisibilityChanged = t.setColumnVisible t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { @@ -212,34 +191,6 @@ func (t *Tracklist) Scroll(amount float32) { t.list.ScrollToOffset(t.list.GetScrollOffset() + amount) } -func (t *Tracklist) buildHeader() { - cols := []ListColumn{{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}} - if t.compactRows { - cols = append(cols, - ListColumn{Text: "Title", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, - ListColumn{Text: "Artist", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - ) - } else { - cols = append(cols, - ListColumn{Text: " Title / Aritst", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, - ) - } - cols = append(cols, - ListColumn{Text: "Album", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - ListColumn{Text: "Time", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - ListColumn{Text: "Year", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - ListColumn{Text: " Fav.", Alignment: fyne.TextAlignCenter, CanToggleVisible: true}, - ListColumn{Text: "Rating", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - ListColumn{Text: "Plays", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - ListColumn{Text: "Comment", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - ListColumn{Text: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - ListColumn{Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, - ListColumn{Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}, - ) - - t.hdr = NewListHeader(cols, t.colLayout) -} - // Gets the track at the given index. Thread-safe. func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track { t.tracksMutex.RLock() @@ -254,30 +205,15 @@ func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track { func (t *Tracklist) SetVisibleColumns(cols []string) { t.visibleColumns[0] = true t.visibleColumns[1] = true - if !t.compactRows { - // hard-code artist column to visible since it's part of title col - t.visibleColumns[2] = true - } - l := len(t.visibleColumns) - if !t.compactRows { - l-- // expanded rows have one fewer column due to merged title/artist - } - for i := 2; i < l; i++ { + for i := 2; i < len(t.columns); i++ { t.hdr.SetColumnVisible(i, false) } for _, col := range cols { - if num := ColNumber(col); num < 0 { + if num := t.ColNumber(col); num < 0 { log.Printf("Unknown tracklist column %q", col) } else { - if num == 2 && !t.compactRows { - // Artist column is hard-coded visible (part of title), skip - continue - } t.visibleColumns[num] = true - if !t.compactRows && num >= 3 { - num -= 1 // shift cols by 1 to account for Title/Artist combined col - } t.hdr.SetColumnVisible(num, true) } } @@ -287,7 +223,7 @@ func (t *Tracklist) VisibleColumns() []string { var cols []string for i := 2; i < len(t.visibleColumns); i++ { if t.visibleColumns[i] { - cols = append(cols, string(t.colName(i, false))) + cols = append(cols, string(t.colName(i))) } } return cols @@ -298,9 +234,6 @@ func (t *Tracklist) setColumnVisible(colNum int, vis bool) { log.Printf("error: Tracklist.SetColumnVisible: column index %d out of range", colNum) return } - if !t.compactRows && colNum >= 2 { - colNum++ // account for off-by-one from merged title/artist column - } t.visibleColumns[colNum] = vis t.list.Refresh() if t.OnVisibleColumnsChanged != nil { @@ -315,13 +248,15 @@ func (t *Tracklist) Sorting() TracklistSort { func (t *Tracklist) SetSorting(sorting TracklistSort) { if sorting.ColumnName == "" { // nil case - reset current sort - if slices.Contains(columns, t.sorting.ColumnName) { - t.hdr.SetSorting(ListHeaderSort{ColNumber: ColNumber(t.sorting.ColumnName), Type: SortNone}) + if slices.ContainsFunc(t.columns, func(c TracklistColumn) bool { + return c.Name == t.sorting.ColumnName + }) { + t.hdr.SetSorting(ListHeaderSort{ColNumber: t.ColNumber(t.sorting.ColumnName), Type: SortNone}) } return } // actual sorting will be handled in callback from header - t.hdr.SetSorting(ListHeaderSort{ColNumber: ColNumber(sorting.ColumnName), Type: sorting.SortOrder}) + t.hdr.SetSorting(ListHeaderSort{ColNumber: t.ColNumber(sorting.ColumnName), Type: sorting.SortOrder}) } // Sets the currently playing track ID and updates the list rendering @@ -511,7 +446,7 @@ func (t *Tracklist) doSortTracks() { } func (t *Tracklist) onSorted(sort ListHeaderSort) { - t.sorting = TracklistSort{ColumnName: t.colName(sort.ColNumber, true), SortOrder: sort.Type} + t.sorting = TracklistSort{ColumnName: t.colName(sort.ColNumber), SortOrder: sort.Type} t.tracksMutex.Lock() t.doSortTracks() t.tracksMutex.Unlock() @@ -729,20 +664,19 @@ func (t *Tracklist) lenTracks() int { return len(t.tracks) } -func ColNumber(colName string) int { - i := slices.Index(columns, colName) +func (t *Tracklist) ColNumber(colName string) int { + i := slices.IndexFunc(t.columns, func(c TracklistColumn) bool { + return c.Name == colName + }) if i < 0 { log.Printf("error: Tracklist: invalid column name %s", colName) } return i } -func (t *Tracklist) colName(i int, adjustForExpandedRows bool) string { - if adjustForExpandedRows && !t.compactRows && i >= 2 { - i += 1 // adjust for off-by-one from combined title/artist column - } - if i < len(columns) { - return columns[i] +func (t *Tracklist) colName(i int) string { + if i < len(t.columns) { + return t.columns[i].Name } log.Println("notReached: Tracklist.colName") return "" diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index fecda8d..06c1e6d 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -17,6 +17,67 @@ import ( const tracklistThumbnailSize = 48 +type TracklistColumn struct { + Name string + Col ListColumn +} + +const ( + ColumnNum = "Num" + ColumnTitle = "Title" + ColumnArtist = "Artist" + ColumnTitleArtist = "Title/Artist" + ColumnAlbum = "Album" + ColumnTime = "Time" + ColumnYear = "Year" + ColumnFavorite = "Favorite" + ColumnRating = "Rating" + ColumnPlays = "Plays" + ColumnComment = "Comment" + ColumnBitrate = "Bitrate" + ColumnSize = "Size" + ColumnPath = "Path" +) + +var ( + ExpandedTracklistRowColumns = []TracklistColumn{ + {Name: ColumnNum, Col: ListColumn{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}}, + {Name: ColumnTitleArtist, Col: ListColumn{Text: "Title / Artist", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}}, + {Name: ColumnAlbum, Col: ListColumn{Text: "Album", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + {Name: ColumnTime, Col: ListColumn{Text: "Time", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnYear, Col: ListColumn{Text: "Year", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnFavorite, Col: ListColumn{Text: " Fav.", Alignment: fyne.TextAlignCenter, CanToggleVisible: true}}, + {Name: ColumnRating, Col: ListColumn{Text: "Rating", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + {Name: ColumnPlays, Col: ListColumn{Text: "Plays", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnComment, Col: ListColumn{Text: "Comment", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + {Name: ColumnBitrate, Col: ListColumn{Text: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnSize, Col: ListColumn{Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnPath, Col: ListColumn{Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + } + + // #, Title/Artist, Album, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path + ExpandedTracklistRowColumnWidths = []float32{40, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1} + + CompactTracklistRowColumns = []TracklistColumn{ + {Name: ColumnNum, Col: ListColumn{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}}, + {Name: ColumnTitle, Col: ListColumn{Text: "Title", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}}, + {Name: ColumnArtist, Col: ListColumn{Text: "Artist", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + {Name: ColumnAlbum, Col: ListColumn{Text: "Album", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + {Name: ColumnTime, Col: ListColumn{Text: "Time", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnYear, Col: ListColumn{Text: "Year", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnFavorite, Col: ListColumn{Text: " Fav.", Alignment: fyne.TextAlignCenter, CanToggleVisible: true}}, + {Name: ColumnRating, Col: ListColumn{Text: "Rating", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + {Name: ColumnPlays, Col: ListColumn{Text: "Plays", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnComment, Col: ListColumn{Text: "Comment", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + {Name: ColumnBitrate, Col: ListColumn{Text: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnSize, Col: ListColumn{Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnPath, Col: ListColumn{Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, + } + + // #, Title, Artist, Album, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path + CompactTracklistRowColumnWidths = []float32{40, -1, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1} +) + type tracklistRowBase struct { FocusListRowBase @@ -47,7 +108,7 @@ type tracklistRowBase struct { size *widget.Label path *widget.Label - colHiddenPtrMap map[string]*bool + setColVisibility func(int, bool) bool } type TracklistRow interface { @@ -99,19 +160,11 @@ func NewExpandedTracklistRow(tracklist *Tracklist, im *backend.ImageManager, pla container := container.New(tracklist.colLayout, v(t.num), titleArtistImg, v(t.album), v(t.dur), v(t.year), v(t.favorite), v(t.rating), v(t.plays), v(t.comment), v(t.bitrate), v(t.size), v(t.path)) t.Content = container - var dummy bool - t.colHiddenPtrMap = map[string]*bool{ - ColumnArtist: &dummy, // artist column is never hidden - ColumnAlbum: &container.Objects[2].(*fyne.Container).Hidden, - ColumnTime: &container.Objects[3].(*fyne.Container).Hidden, - ColumnYear: &container.Objects[4].(*fyne.Container).Hidden, - ColumnFavorite: &container.Objects[5].(*fyne.Container).Hidden, - ColumnRating: &container.Objects[6].(*fyne.Container).Hidden, - ColumnPlays: &container.Objects[7].(*fyne.Container).Hidden, - ColumnComment: &container.Objects[8].(*fyne.Container).Hidden, - ColumnBitrate: &container.Objects[9].(*fyne.Container).Hidden, - ColumnSize: &container.Objects[10].(*fyne.Container).Hidden, - ColumnPath: &container.Objects[11].(*fyne.Container).Hidden, + t.setColVisibility = func(colNum int, vis bool) bool { + c := container.Objects[colNum].(*fyne.Container) + wasHidden := c.Hidden + c.Hidden = vis + return c.Hidden != wasHidden } return t } @@ -133,18 +186,27 @@ func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) t.Content = container.New(tracklist.colLayout, t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bitrate, t.size, t.path) - t.colHiddenPtrMap = map[string]*bool{ - ColumnArtist: &t.artist.Hidden, - ColumnAlbum: &t.album.Hidden, - ColumnTime: &t.dur.Hidden, - ColumnYear: &t.year.Hidden, - ColumnFavorite: &t.favorite.Hidden, - ColumnRating: &t.rating.Hidden, - ColumnPlays: &t.plays.Hidden, - ColumnComment: &t.comment.Hidden, - ColumnBitrate: &t.bitrate.Hidden, - ColumnSize: &t.size.Hidden, - ColumnPath: &t.path.Hidden, + colHiddenPtrMap := map[int]*bool{ + 2: &t.artist.Hidden, + 3: &t.album.Hidden, + 4: &t.dur.Hidden, + 5: &t.year.Hidden, + 6: &t.favorite.Hidden, + 7: &t.rating.Hidden, + 8: &t.plays.Hidden, + 9: &t.comment.Hidden, + 10: &t.bitrate.Hidden, + 11: &t.size.Hidden, + 12: &t.path.Hidden, + } + t.setColVisibility = func(colNum int, vis bool) bool { + ptr, ok := colHiddenPtrMap[colNum] + if !ok { + return false // column is always visible + } + wasHidden := *ptr + *ptr = vis + return vis != wasHidden } return t } @@ -266,25 +328,11 @@ func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int) { } // Show only columns configured to be visible - updateHidden := func(colName string) { - colHidden := !t.tracklist.visibleColumns[ColNumber(colName)] - hiddenPtr := t.colHiddenPtrMap[colName] - if colHidden != *hiddenPtr { - *hiddenPtr = colHidden + for i := 2; i < len(t.tracklist.columns); i++ { + if ch := t.setColVisibility(i, !t.tracklist.visibleColumns[i]); ch { changed = true } } - updateHidden(ColumnArtist) - updateHidden(ColumnAlbum) - updateHidden(ColumnTime) - updateHidden(ColumnYear) - updateHidden(ColumnFavorite) - updateHidden(ColumnRating) - updateHidden(ColumnPlays) - updateHidden(ColumnComment) - updateHidden(ColumnBitrate) - updateHidden(ColumnSize) - updateHidden(ColumnPath) if changed { t.Refresh() From 41af03c2bce8ef8e0689353b2dd74196995bc65f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 May 2024 18:33:58 -0700 Subject: [PATCH 07/10] fix misalignment bug --- ui/widgets/tracklist.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 98c0ec3..4734d7c 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -207,6 +207,7 @@ func (t *Tracklist) SetVisibleColumns(cols []string) { t.visibleColumns[1] = true for i := 2; i < len(t.columns); i++ { + t.visibleColumns[i] = false t.hdr.SetColumnVisible(i, false) } for _, col := range cols { From 5f8a25be257743966ffab50ecc8b834623c27fd4 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 May 2024 18:47:30 -0700 Subject: [PATCH 08/10] use larger playing icon for expanded tracklist rows and fix positioning bug when replacing back with track number --- ui/widgets/tracklist.go | 17 ++++++++++++----- ui/widgets/tracklistrow.go | 8 +++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 4734d7c..bcdd395 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -16,6 +16,7 @@ import ( "github.com/dweymouth/supersonic/ui/util" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/theme" @@ -100,15 +101,25 @@ type Tracklist struct { } func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCompactRows bool) *Tracklist { + playIcon := theme.NewThemedResource(theme.MediaPlayIcon()) + playIcon.ColorName = theme.ColorNamePrimary + t := &Tracklist{compactRows: useCompactRows} + t.ExtendBaseWidget(t) t.columns = ExpandedTracklistRowColumns colWidths := ExpandedTracklistRowColumnWidths + var playingIcon fyne.CanvasObject if useCompactRows { t.columns = CompactTracklistRowColumns colWidths = CompactTracklistRowColumnWidths + playingIcon = container.NewCenter(container.NewHBox(util.NewHSpace(2), widget.NewIcon(playIcon))) + } else { + playIconImg := canvas.NewImageFromResource(playIcon) + playIconImg.FillMode = canvas.ImageFillContain + playIconImg.SetMinSize(fyne.NewSquareSize(theme.IconInlineSize() * 1.5)) + playingIcon = container.NewCenter(playIconImg) } t.visibleColumns = make([]bool, len(t.columns)) - t.ExtendBaseWidget(t) if len(tracks) > 0 { t._setTracks(tracks) @@ -125,10 +136,6 @@ func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCo } } - playIcon := theme.NewThemedResource(theme.MediaPlayIcon()) - playIcon.ColorName = theme.ColorNamePrimary - playingIcon := container.NewCenter(container.NewHBox(util.NewHSpace(2), widget.NewIcon(playIcon))) - t.list = NewFocusList( t.lenTracks, func() fyne.CanvasObject { diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index 06c1e6d..6d269f3 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -86,6 +86,10 @@ type tracklistRowBase struct { // set by extending widget playingIcon fyne.CanvasObject + // when replacing the content of col 0 with playing icon, + // the original content is saved here for resetting + originalNumColContent fyne.CanvasObject + // internal state tracklist *Tracklist trackNum int @@ -108,6 +112,7 @@ type tracklistRowBase struct { size *widget.Label path *widget.Label + // must be injected by extending widget setColVisibility func(int, bool) bool } @@ -303,9 +308,10 @@ func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int) { t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying if isPlaying { + t.originalNumColContent = t.Content.(*fyne.Container).Objects[0] t.Content.(*fyne.Container).Objects[0] = t.playingIcon } else { - t.Content.(*fyne.Container).Objects[0] = t.num + t.Content.(*fyne.Container).Objects[0] = t.originalNumColContent } changed = true } From 379af9cc504a3da39da0665f1c83422bf7e82ccd Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 May 2024 18:53:27 -0700 Subject: [PATCH 09/10] adjust padding around tracklist row thumbnail --- ui/widgets/tracklistrow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index 6d269f3..41654cd 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -157,7 +157,7 @@ func NewExpandedTracklistRow(tracklist *Tracklist, im *backend.ImageManager, pla } titleArtistImg := container.NewBorder(nil, nil, - container.NewPadded(t.img) /*left*/, nil, + container.New(layout.NewCustomPaddedLayout(2, 2, 2, -4), t.img) /*left*/, nil, container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-16), t.name, t.artist)) From 84d9372e085be658ec53ed8fb9b1abb6baa97a28 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 May 2024 19:08:44 -0700 Subject: [PATCH 10/10] fix widget pool tracklist retrieval for album page --- ui/browsing/albumpage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 3efc688..cc6197d 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -86,7 +86,7 @@ func newAlbumPage( a.header = NewAlbumPageHeader(a) } a.header.page = a - if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { + if t := a.pool.Obtain(util.WidgetTypeCompactTracklist); t != nil { a.tracklist = t.(*widgets.Tracklist) a.tracklist.Reset() } else {