From 0d02c1fe9db65837eefd5a1e6f7517ee89bc99ce Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 21 May 2023 15:34:19 -0700 Subject: [PATCH 01/11] layout fixes and upgrades, add list hdr sort icon --- ui/layouts/columnslayout.go | 14 ++++-- ui/layouts/hboxcustompadding.go | 53 +++++++++++++++----- ui/widgets/listheader.go | 87 +++++++++++++++++++++++++++++---- ui/widgets/tracklist.go | 2 +- 4 files changed, 130 insertions(+), 26 deletions(-) diff --git a/ui/layouts/columnslayout.go b/ui/layouts/columnslayout.go index 4cd9089..9ae141f 100644 --- a/ui/layouts/columnslayout.go +++ b/ui/layouts/columnslayout.go @@ -1,6 +1,8 @@ package layouts -import "fyne.io/fyne/v2" +import ( + "fyne.io/fyne/v2" +) // ColumnsLayout lays out a number of items into columns. // There are two types of columns: fixed-width and variable width. @@ -58,11 +60,13 @@ func (c *ColumnsLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { if !objects[i].Visible() { continue } + w := objects[i].MinSize().Width - if i < len(c.ColumnWidths) && c.ColumnWidths[i] > w { - w = c.ColumnWidths[i] - } else if c.ColumnWidths[i] < 0 && expandObjW > w { - w = expandObjW + if i >= len(c.ColumnWidths) || c.ColumnWidths[i] < 0 { + // expanding width column + w = fyne.Max(expandObjW, w) + } else { + w = fyne.Max(c.ColumnWidths[i], w) } objects[i].Resize(fyne.NewSize(w, size.Height)) objects[i].Move(fyne.NewPos(x, 0)) diff --git a/ui/layouts/hboxcustompadding.go b/ui/layouts/hboxcustompadding.go index ddc71c5..6005d35 100644 --- a/ui/layouts/hboxcustompadding.go +++ b/ui/layouts/hboxcustompadding.go @@ -2,6 +2,7 @@ package layouts import ( "fyne.io/fyne/v2" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" ) @@ -12,47 +13,77 @@ type HboxCustomPadding struct { DisableThemePad bool } -func (v *HboxCustomPadding) MinSize(objects []fyne.CanvasObject) fyne.Size { +func (*HboxCustomPadding) isSpacer(obj fyne.CanvasObject) bool { + if !obj.Visible() { + return false + } + if spacer, ok := obj.(layout.SpacerObject); ok { + return spacer.ExpandHorizontal() + } + + return false +} + +func (h *HboxCustomPadding) MinSize(objects []fyne.CanvasObject) fyne.Size { minSize := fyne.NewSize(0, 0) + addPadding := false + padding := h.themePad() + h.ExtraPad for _, child := range objects { if !child.Visible() { continue } + if h.isSpacer(child) { + continue + } minSize.Height = fyne.Max(child.MinSize().Height, minSize.Height) minSize.Width += child.MinSize().Width + if addPadding { + minSize.Width += padding + } + addPadding = true } - minSize.Width += (v.themePad() + v.ExtraPad) * float32(len(objects)-1) return minSize } -func (v *HboxCustomPadding) Layout(objects []fyne.CanvasObject, size fyne.Size) { +func (h *HboxCustomPadding) Layout(objects []fyne.CanvasObject, size fyne.Size) { + spacers := 0 total := float32(0) for _, child := range objects { if !child.Visible() { continue } + if h.isSpacer(child) { + spacers++ + continue + } total += child.MinSize().Width } x, y := float32(0), float32(0) - - padding := v.themePad() + v.ExtraPad - extra := float32(0) + padding := h.themePad() + h.ExtraPad + extra := size.Width - total - (padding * float32(len(objects)-spacers-1)) + extraCell := float32(0) + if spacers > 0 { + extraCell = extra / float32(spacers) + } for _, child := range objects { if !child.Visible() { continue } + + if h.isSpacer(child) { + x += extraCell + } width := child.MinSize().Width - child.Move(fyne.NewPos(x+extra, y)) - x += width + child.Move(fyne.NewPos(x, y)) child.Resize(fyne.NewSize(width, size.Height)) - extra += padding + x += padding + width } } -func (v *HboxCustomPadding) themePad() float32 { - if v.DisableThemePad { +func (h *HboxCustomPadding) themePad() float32 { + if h.DisableThemePad { return 0 } return theme.Padding() diff --git a/ui/widgets/listheader.go b/ui/widgets/listheader.go index 41aa076..79ebdc5 100644 --- a/ui/widgets/listheader.go +++ b/ui/widgets/listheader.go @@ -5,9 +5,12 @@ import ( "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" + "github.com/dweymouth/supersonic/ui/util" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -40,7 +43,7 @@ func NewListHeader(cols []ListColumn, layout *layouts.ColumnsLayout) *ListHeader columnsContainer: container.New(layout), } l.columnVisible = make([]bool, len(cols)) - for i, _ := range l.columnVisible { + for i := range l.columnVisible { l.columnVisible[i] = true } l.container = container.NewMax(myTheme.NewThemedRectangle(theme.ColorNameBackground), l.columnsContainer) @@ -65,14 +68,16 @@ func (l *ListHeader) SetColumnVisible(colNum int, visible bool) { func (l *ListHeader) buildColumns() { for _, c := range l.columns { - t := widget.NewRichTextWithText(c.Text) - t.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true - al := fyne.TextAlignLeading - if c.AlignTrailing { - al = fyne.TextAlignTrailing - } - t.Segments[0].(*widget.TextSegment).Style.Alignment = al - l.columnsContainer.Add(t) + hdr := newColHeader(c) + hdr.SortVisible = true + l.columnsContainer.Add( + // hdr, + // TODO: remove debugging background + container.NewMax(container.New(&layouts.MaxPadLayout{PadLeft: 2, PadRight: 2}, + canvas.NewRectangle(theme.SelectionColor())), + hdr, + ), + ) } } @@ -119,3 +124,67 @@ func (l *ListHeader) createOnChangedCallbk(colNum int) func(bool) { } } } + +type colHeader struct { + widget.BaseWidget + + SortDescending bool + SortVisible bool + + columnCfg ListColumn + + label *widget.RichText + sortIcon *widget.Icon + sortIconNegSpacer fyne.CanvasObject + container *fyne.Container +} + +func newColHeader(columnCfg ListColumn) *colHeader { + c := &colHeader{columnCfg: columnCfg} + c.ExtendBaseWidget(c) + + c.label = widget.NewRichTextWithText(columnCfg.Text) + c.label.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true + al := fyne.TextAlignLeading + if columnCfg.AlignTrailing { + al = fyne.TextAlignTrailing + } + c.label.Segments[0].(*widget.TextSegment).Style.Alignment = al + c.sortIcon = widget.NewIcon(theme.MenuDropDownIcon()) + // hack to remove extra icon space + // should be hidden whenever sortIcon is hidden + c.sortIconNegSpacer = util.NewHSpace(0) + + return c +} + +func (c *colHeader) Refresh() { + if c.SortDescending { + c.sortIcon.Resource = theme.MenuDropDownIcon() + } else { + c.sortIcon.Resource = theme.MenuDropUpIcon() + } + + if c.SortVisible && c.sortIcon.Hidden { + c.sortIcon.Show() + c.container.Add(c.sortIconNegSpacer) + } else if !c.sortIcon.Hidden { + c.sortIcon.Hide() + c.container.Remove(c.sortIconNegSpacer) + } + + c.BaseWidget.Refresh() +} + +func (c *colHeader) CreateRenderer() fyne.WidgetRenderer { + if c.container == nil { + c.container = container.New(&layouts.HboxCustomPadding{DisableThemePad: true, ExtraPad: -8}) + if c.columnCfg.AlignTrailing { + c.container.Add(layout.NewSpacer()) + } + c.container.Add(c.label) + c.container.Add(c.sortIcon) + c.container.Add(c.sortIconNegSpacer) + } + return widget.NewSimpleRenderer(c.container) +} diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 7c583cf..a3ebbf3 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -90,7 +90,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { t.ExtendBaseWidget(t) t.selectionMgr = util.NewListSelectionManager(t.lenTracks) // #, Title, Artist, Album, Time, Year, Favorite, Rating, Plays, Bitrate, Size, Path - t.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 50, 50, 45, 95, 55, 65, 70, -1}) + t.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 60, 60, 55, 100, 65, 75, 75, -1}) t.buildHeader() t.hdr.OnColumnVisibilityChanged = t.setColumnVisible t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { From e434b55dda84ac4e0383eb1d3e968e71b5de6004 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 21 May 2023 16:09:53 -0700 Subject: [PATCH 02/11] add tapped handler for changing column sort --- ui/widgets/listheader.go | 64 +++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/ui/widgets/listheader.go b/ui/widgets/listheader.go index 79ebdc5..a464fd5 100644 --- a/ui/widgets/listheader.go +++ b/ui/widgets/listheader.go @@ -15,6 +15,14 @@ import ( "fyne.io/fyne/v2/widget" ) +type ColumnSort int + +const ( + SortNone ColumnSort = iota + SortAscending + SortDescending +) + type ListColumn struct { Text string AlignTrailing bool @@ -24,6 +32,7 @@ type ListColumn struct { type ListHeader struct { widget.BaseWidget + OnColumnSortChanged func(int, ColumnSort) OnColumnVisibilityChanged func(int, bool) OnColumnVisibilityMenuShown func(*widget.PopUp) @@ -67,9 +76,12 @@ func (l *ListHeader) SetColumnVisible(colNum int, visible bool) { } func (l *ListHeader) buildColumns() { - for _, c := range l.columns { + for i, c := range l.columns { hdr := newColHeader(c) - hdr.SortVisible = true + hdr.OnSortChanged = func(i int) func(ColumnSort) { + return func(sort ColumnSort) { l.onSortChanged(i, sort) } + }(i) + hdr.OnTappedSecondary = l.TappedSecondary l.columnsContainer.Add( // hdr, // TODO: remove debugging background @@ -81,6 +93,20 @@ func (l *ListHeader) buildColumns() { } } +func (l *ListHeader) onSortChanged(colNum int, sort ColumnSort) { + for i, c := range l.columnsContainer.Objects { + if i != colNum { + c.(*fyne.Container).Objects[1].(*colHeader).Sort = SortNone + // TODO + //c.(*colHeader).Sort = sortNone + } + } + l.Refresh() + if l.OnColumnSortChanged != nil { + l.OnColumnSortChanged(colNum, sort) + } +} + func (l *ListHeader) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(l.container) } @@ -128,8 +154,9 @@ func (l *ListHeader) createOnChangedCallbk(colNum int) func(bool) { type colHeader struct { widget.BaseWidget - SortDescending bool - SortVisible bool + Sort ColumnSort + OnSortChanged func(ColumnSort) + OnTappedSecondary func(*fyne.PointEvent) columnCfg ListColumn @@ -158,17 +185,40 @@ func newColHeader(columnCfg ListColumn) *colHeader { return c } +func (c *colHeader) Tapped(*fyne.PointEvent) { + switch c.Sort { + case SortNone: + c.Sort = SortAscending + case SortAscending: + c.Sort = SortDescending + case SortDescending: + c.Sort = SortNone + default: + log.Println("notReached colHeader.Tapped") + } + c.Refresh() + if c.OnSortChanged != nil { + c.OnSortChanged(c.Sort) + } +} + +func (c *colHeader) TappedSecondary(e *fyne.PointEvent) { + if c.OnTappedSecondary != nil { + c.OnTappedSecondary(e) + } +} + func (c *colHeader) Refresh() { - if c.SortDescending { + if c.Sort == SortDescending { c.sortIcon.Resource = theme.MenuDropDownIcon() } else { c.sortIcon.Resource = theme.MenuDropUpIcon() } - if c.SortVisible && c.sortIcon.Hidden { + if c.Sort > 0 && c.sortIcon.Hidden { c.sortIcon.Show() c.container.Add(c.sortIconNegSpacer) - } else if !c.sortIcon.Hidden { + } else if c.Sort == SortNone && !c.sortIcon.Hidden { c.sortIcon.Hide() c.container.Remove(c.sortIconNegSpacer) } From 1f3559de4311f1642d7788fc12b463fe58063b21 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 21 May 2023 17:11:55 -0700 Subject: [PATCH 03/11] add tracklist sorting logic --- sharedutil/sharedutil.go | 22 ++++ ui/browsing/albumpage.go | 10 +- ui/browsing/favoritespage.go | 2 +- ui/browsing/nowplayingpage.go | 2 +- ui/browsing/playlistpage.go | 15 +-- ui/controller/controller.go | 2 +- ui/widgets/tracklist.go | 188 ++++++++++++++++++++++------------ 7 files changed, 162 insertions(+), 79 deletions(-) diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index d01268c..d7ad92b 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -40,6 +40,28 @@ func MapSlice[T any, U any](ts []T, f func(T) U) []U { return result } +func IndexOf[T comparable](ts []T, t T) int { + for i, tt := range ts { + if t == tt { + return i + } + } + return -1 +} + +func Reversed[T any](ts []T) []T { + if ts == nil { + return nil + } + new := make([]T, len(ts)) + j := len(ts) - 1 + for i := range ts { + new[i] = ts[j] + j-- + } + return new +} + func FindTrackByID(id string, tracks []*mediaprovider.Track) *mediaprovider.Track { for _, tr := range tracks { if id == tr.ID { diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 333b612..8135342 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -26,6 +26,7 @@ type AlbumPage struct { albumPageState header *AlbumPageHeader + tracks []*mediaprovider.Track tracklist *widgets.Tracklist nowPlayingID string container *fyne.Container @@ -119,7 +120,8 @@ func (a *AlbumPage) load() { } a.header.Update(album, a.im) a.tracklist.ShowDiscNumber = album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber - a.tracklist.Tracks = album.Tracks + a.tracks = album.Tracks + a.tracklist.SetTracks(album.Tracks) a.tracklist.SetNowPlaying(a.nowPlayingID) } @@ -169,7 +171,7 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { go page.pm.PlayAlbum(page.albumID, 0, false) }) shuffleBtn := widget.NewButtonWithIcon(" Shuffle", myTheme.ShuffleIcon, func() { - page.pm.LoadTracks(page.tracklist.Tracks, false, true) + page.pm.LoadTracks(page.tracklist.GetTracks(), false, true) page.pm.PlayFromBeginning() }) var pop *widget.PopUpMenu @@ -178,11 +180,11 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { if pop == nil { menu := fyne.NewMenu("", fyne.NewMenuItem("Add to queue", func() { - a.page.pm.LoadAlbum(a.albumID, true /*append*/, false /*shuffle*/) + go a.page.pm.LoadAlbum(a.albumID, true /*append*/, false /*shuffle*/) }), fyne.NewMenuItem("Add to playlist...", func() { a.page.contr.DoAddTracksToPlaylistWorkflow( - sharedutil.TracksToIDs(a.page.tracklist.Tracks)) + sharedutil.TracksToIDs(a.page.tracks)) })) pop = widget.NewPopUpMenu(menu, fyne.CurrentApp().Driver().CanvasForObject(a)) } diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 1cadbeb..1201fe4 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -154,7 +154,7 @@ func (a *FavoritesPage) Reload() { if a.tracklistCtr != nil { // refresh favorite songs view tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist) - tr.Tracks = starred.Tracks + tr.SetTracks(starred.Tracks) if a.toggleBtns.ActivatedButtonIndex() == 2 { // favorite songs view is visible tr.Refresh() diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index ff1f0b4..46b121e 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -107,7 +107,7 @@ func (a *NowPlayingPage) onRemoveSelectedFromQueue() { // does not make calls to server - can safely be run in UI callbacks func (a *NowPlayingPage) load(highlightedTrackID string) { queue := a.pm.GetPlayQueue() - a.tracklist.Tracks = queue + a.tracklist.SetTracks(queue) a.tracklist.SetNowPlaying(a.nowPlayingID) if highlightedTrackID != "" { a.tracklist.SelectAndScrollToTrack(highlightedTrackID) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 15d411c..42420d4 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -27,6 +27,7 @@ type PlaylistPage struct { header *PlaylistPageHeader tracklist *widgets.Tracklist + tracks []*mediaprovider.Track nowPlayingID string container *fyne.Container } @@ -118,7 +119,8 @@ func (a *PlaylistPage) load() { log.Printf("Failed to get playlist: %s", err.Error()) return } - a.tracklist.Tracks = playlist.Tracks + a.tracks = playlist.Tracks + a.tracklist.SetTracks(playlist.Tracks) a.tracklist.SetNowPlaying(a.nowPlayingID) a.tracklist.Refresh() a.header.Update(playlist) @@ -141,8 +143,9 @@ func (a *PlaylistPage) onMoveSelectedToBottom() { } func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { + // TODO: revisit this for how to deal with sort order idxs := a.tracklist.SelectedTrackIndexes() - newTracks := sharedutil.ReorderTracks(a.tracklist.Tracks, idxs, op) + newTracks := sharedutil.ReorderTracks(a.tracklist.GetTracks(), idxs, op) ids := make([]string, len(newTracks)) for i, tr := range newTracks { ids[i] = tr.ID @@ -150,7 +153,7 @@ func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { if err := a.sm.Server.ReplacePlaylistTracks(a.playlistID, ids); err != nil { log.Printf("error updating playlist: %s", err.Error()) } else { - a.tracklist.Tracks = newTracks + a.tracklist.SetTracks(newTracks) a.tracklist.UnselectAll() a.tracklist.Refresh() } @@ -200,12 +203,12 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { }) a.editButton.Hidden = true playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { - page.pm.LoadTracks(page.tracklist.Tracks, false, false) + page.pm.LoadTracks(page.tracks, false, false) page.pm.PlayFromBeginning() }) // TODO: find way to pad shuffle svg rather than using a space in the label string shuffleBtn := widget.NewButtonWithIcon(" Shuffle", myTheme.ShuffleIcon, func() { - page.pm.LoadTracks(page.tracklist.Tracks, false /*append*/, true /*shuffle*/) + page.pm.LoadTracks(page.tracks, false /*append*/, true /*shuffle*/) page.pm.PlayFromBeginning() }) var pop *widget.PopUpMenu @@ -218,7 +221,7 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { }), fyne.NewMenuItem("Add to playlist...", func() { a.page.contr.DoAddTracksToPlaylistWorkflow( - sharedutil.TracksToIDs(a.page.tracklist.Tracks)) + sharedutil.TracksToIDs(a.page.tracks)) })) pop = widget.NewPopUpMenu(menu, fyne.CurrentApp().Driver().CanvasForObject(a)) } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index e340cfc..78ff0c1 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -95,7 +95,7 @@ func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) { m.App.PlaybackManager.LoadTracks(tracks, true, false) } tracklist.OnPlayTrackAt = func(idx int) { - m.App.PlaybackManager.LoadTracks(tracklist.Tracks, false, false) + m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), false, false) m.App.PlaybackManager.PlayTrackAt(idx) } tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) { diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index a3ebbf3..44af417 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -3,7 +3,9 @@ package widgets import ( "fmt" "log" + "sort" "strconv" + "strings" "sync" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -21,6 +23,8 @@ import ( ) const ( + ColumnNum = "Num" + ColumnTitle = "Title" ColumnArtist = "Artist" ColumnAlbum = "Album" ColumnTime = "Time" @@ -33,14 +37,14 @@ const ( ColumnPath = "Path" ) +var columns = []string{ + ColumnNum, ColumnTitle, ColumnArtist, ColumnAlbum, ColumnTime, ColumnYear, + ColumnFavorite, ColumnRating, ColumnPlays, ColumnBitrate, ColumnSize, ColumnPath, +} + type Tracklist struct { widget.BaseWidget - // Tracks is the set of tracks displayed by the widget. - // Direct access to this is not thread-safe but OK for - // views that only load tracks into the widget once at page load. - Tracks []*mediaprovider.Track - // AutoNumber sets whether to auto-number the tracks 1..N in display order, // or to use the number from the track's metadata AutoNumber bool @@ -74,7 +78,10 @@ type Tracklist struct { visibleColumns []bool - tracksMutex sync.RWMutex + tracksMutex sync.RWMutex + tracks []*mediaprovider.Track + tracksOrigOrder []*mediaprovider.Track + selectionMgr util.ListSelectionManager nowPlayingID string colLayout *layouts.ColumnsLayout @@ -85,13 +92,16 @@ type Tracklist struct { } func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { - t := &Tracklist{Tracks: tracks, visibleColumns: make([]bool, 12)} + t := &Tracklist{tracks: tracks, visibleColumns: make([]bool, 12)} t.ExtendBaseWidget(t) t.selectionMgr = util.NewListSelectionManager(t.lenTracks) // #, Title, Artist, Album, Time, Year, Favorite, Rating, Plays, Bitrate, Size, Path t.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 60, 60, 55, 100, 65, 75, 75, -1}) t.buildHeader() + t.hdr.OnColumnSortChanged = func(idx int, sort ColumnSort) { + t.SortByColumn(colName(idx), sort) + } t.hdr.OnColumnVisibilityChanged = t.setColumnVisible t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { if t.OnColumnVisibilityMenuShown != nil { @@ -146,10 +156,10 @@ func (t *Tracklist) buildHeader() { func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - if idx >= len(t.Tracks) { + if idx >= len(t.tracks) { return nil } - return t.Tracks[idx] + return t.tracks[idx] } func (t *Tracklist) SetVisibleColumns(cols []string) { @@ -191,6 +201,78 @@ func (t *Tracklist) setColumnVisible(colNum int, vis bool) { } } +func (t *Tracklist) stringSort(fieldFn func(*mediaprovider.Track) string, sortOrder ColumnSort) { + new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) + copy(new, t.tracksOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) + if sortOrder == SortDescending { + return cmp > 0 + } + return cmp < 0 + }) + t.tracks = new +} + +func (t *Tracklist) intSort(fieldFn func(*mediaprovider.Track) int64, sortOrder ColumnSort) { + new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) + copy(new, t.tracksOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + if sortOrder == SortDescending { + return fieldFn(new[i]) > fieldFn(new[j]) + } + return fieldFn(new[i]) < fieldFn(new[j]) + }) + t.tracks = new +} + +func (t *Tracklist) SortByColumn(columnName string, sortOrder ColumnSort) { + t.tracksMutex.Lock() + if sortOrder == SortNone { + t.tracks = t.tracksOrigOrder + t.tracksMutex.Unlock() + t.Refresh() + return + } + switch columnName { + case ColumnNum: + if sortOrder == SortDescending { + t.tracks = sharedutil.Reversed(t.tracksOrigOrder) + } else { + t.tracks = t.tracksOrigOrder + } + case ColumnTitle: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.Name }, sortOrder) + case ColumnArtist: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.ArtistNames[0] }, sortOrder) + case ColumnAlbum: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.Album }, sortOrder) + case ColumnPath: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.FilePath }, sortOrder) + case ColumnRating: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Rating) }, sortOrder) + case ColumnTime: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Duration) }, sortOrder) + case ColumnYear: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Year) }, sortOrder) + case ColumnSize: + t.intSort(func(tr *mediaprovider.Track) int64 { return tr.Size }, sortOrder) + case ColumnPlays: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.PlayCount) }, sortOrder) + case ColumnBitrate: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.BitRate) }, sortOrder) + case ColumnFavorite: + t.intSort(func(tr *mediaprovider.Track) int64 { + if tr.Favorite { + return 1 + } + return 0 + }, sortOrder) + } + t.tracksMutex.Unlock() + t.Refresh() +} + func (t *Tracklist) SetNowPlaying(trackID string) { t.nowPlayingID = trackID t.list.Refresh() @@ -198,7 +280,7 @@ func (t *Tracklist) SetNowPlaying(trackID string) { func (t *Tracklist) IncrementPlayCount(trackID string) { t.tracksMutex.RLock() - tr := sharedutil.FindTrackByID(trackID, t.Tracks) + tr := sharedutil.FindTrackByID(trackID, t.tracks) t.tracksMutex.RUnlock() if tr != nil { tr.PlayCount += 1 @@ -211,14 +293,29 @@ func (t *Tracklist) Clear() { t.selectionMgr.UnselectAll() t.tracksMutex.Lock() defer t.tracksMutex.Unlock() - t.Tracks = nil + t.tracks = nil +} + +// Sets the tracks in the tracklist. Thread-safe. +func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) { + t.tracksMutex.Lock() + defer t.tracksMutex.Unlock() + t.tracks = trs + t.tracksOrigOrder = trs +} + +// Returns the tracks in the tracklist in the current display order. +func (t *Tracklist) GetTracks() []*mediaprovider.Track { + t.tracksMutex.RLock() + defer t.tracksMutex.RUnlock() + return t.tracks } // Append more tracks to the tracklist. Thread-safe. func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) { t.tracksMutex.Lock() defer t.tracksMutex.Unlock() - t.Tracks = append(t.Tracks, trs...) + t.tracks = append(t.tracks, trs...) } func (t *Tracklist) SelectAll() { @@ -234,7 +331,7 @@ func (t *Tracklist) UnselectAll() { func (t *Tracklist) SelectAndScrollToTrack(trackID string) { t.tracksMutex.RLock() idx := -1 - for i, tr := range t.Tracks { + for i, tr := range t.tracks { if tr.ID == trackID { idx = i break @@ -327,7 +424,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { func (t *Tracklist) onSetFavorite(trackID string, fav bool) { t.tracksMutex.RLock() - tr := sharedutil.FindTrackByID(trackID, t.Tracks) + tr := sharedutil.FindTrackByID(trackID, t.tracks) t.tracksMutex.RUnlock() t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false) } @@ -348,7 +445,7 @@ func (t *Tracklist) onSetFavorites(tracks []*mediaprovider.Track, fav bool, need func (t *Tracklist) onSetRating(trackID string, rating int) { // update our own track model t.tracksMutex.RLock() - tr := sharedutil.FindTrackByID(trackID, t.Tracks) + tr := sharedutil.FindTrackByID(trackID, t.tracks) t.tracksMutex.RUnlock() t.onSetRatings([]*mediaprovider.Track{tr}, rating, false) } @@ -384,7 +481,7 @@ func (t *Tracklist) selectedTracks() []*mediaprovider.Track { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() for _, idx := range sel { - tracks = append(tracks, t.Tracks[idx]) + tracks = append(tracks, t.tracks[idx]) } return tracks } @@ -395,7 +492,7 @@ func (t *Tracklist) selectedTrackIDs() []string { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() for _, idx := range sel { - tracks = append(tracks, t.Tracks[idx].ID) + tracks = append(tracks, t.tracks[idx].ID) } return tracks } @@ -407,64 +504,23 @@ func (t *Tracklist) SelectedTrackIndexes() []int { func (t *Tracklist) lenTracks() int { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - return len(t.Tracks) + return len(t.tracks) } func ColNumber(colName string) int { - // built-in columns # and Title are always visible - switch colName { - case ColumnArtist: - return 2 - case ColumnAlbum: - return 3 - case ColumnTime: - return 4 - case ColumnYear: - return 5 - case ColumnFavorite: - return 6 - case ColumnRating: - return 7 - case ColumnPlays: - return 8 - case ColumnBitrate: - return 9 - case ColumnSize: - return 10 - case ColumnPath: - return 11 - default: + i := sharedutil.IndexOf(columns, colName) + if i < 0 { log.Printf("error: Tracklist: invalid column name %s", colName) - return -100 } + return i } func colName(i int) string { - // built-in columns # and Title are always visible - switch i { - case 2: - return ColumnArtist - case 3: - return ColumnAlbum - case 4: - return ColumnTime - case 5: - return ColumnYear - case 6: - return ColumnFavorite - case 7: - return ColumnRating - case 8: - return ColumnPlays - case 9: - return ColumnBitrate - case 10: - return ColumnSize - case 11: - return ColumnPath - default: - return "" + if i < len(columns) { + return columns[i] } + log.Println("notReached: Tracklist.colName") + return "" } type TrackRow struct { From 137e0334e0ce7148561ebfd387ea1196c06552af Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 21 May 2023 20:54:39 -0700 Subject: [PATCH 04/11] add disabling of sorting where appropriate --- ui/browsing/playlistspage.go | 1 + ui/browsing/trackspage.go | 1 + ui/widgets/artistgenrelist.go | 1 + ui/widgets/listheader.go | 16 +++++++++++----- ui/widgets/tracklist.go | 10 +++++++++- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index ba9eb07..e211c97 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -290,6 +290,7 @@ func (p *PlaylistList) buildHeader() { {"Description", false, false}, {"Owner", false, false}, {"Track Count", true, false}}, p.columnsLayout) + p.header.DisableSorting = true } diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index d8a94bc..2c21197 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -44,6 +44,7 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, t.ExtendBaseWidget(t) t.tracklist = widgets.NewTracklist(nil) + t.tracklist.DisableSorting = true t.tracklist.AutoNumber = true t.tracklist.SetVisibleColumns(conf.TracklistColumns) t.tracklist.OnVisibleColumnsChanged = func(cols []string) { diff --git a/ui/widgets/artistgenrelist.go b/ui/widgets/artistgenrelist.go index 08a60bf..a032ab9 100644 --- a/ui/widgets/artistgenrelist.go +++ b/ui/widgets/artistgenrelist.go @@ -66,6 +66,7 @@ func NewArtistGenreList(items []ArtistGenreListItemModel) *ArtistGenreList { a.ExtendBaseWidget(a) a.hdr = NewListHeader([]ListColumn{ {"Name", false, false}, {"Album Count", true, false}, {"Track Count", true, false}}, a.columnsLayout) + a.hdr.DisableSorting = true a.list = widget.NewList( func() int { return len(a.Items) }, func() fyne.CanvasObject { diff --git a/ui/widgets/listheader.go b/ui/widgets/listheader.go index a464fd5..f26f6da 100644 --- a/ui/widgets/listheader.go +++ b/ui/widgets/listheader.go @@ -32,6 +32,8 @@ type ListColumn struct { type ListHeader struct { widget.BaseWidget + DisableSorting bool + OnColumnSortChanged func(int, ColumnSort) OnColumnVisibilityChanged func(int, bool) OnColumnVisibilityMenuShown func(*widget.PopUp) @@ -77,7 +79,7 @@ func (l *ListHeader) SetColumnVisible(colNum int, visible bool) { func (l *ListHeader) buildColumns() { for i, c := range l.columns { - hdr := newColHeader(c) + hdr := newColHeader(c, &l.DisableSorting) hdr.OnSortChanged = func(i int) func(ColumnSort) { return func(sort ColumnSort) { l.onSortChanged(i, sort) } }(i) @@ -158,7 +160,8 @@ type colHeader struct { OnSortChanged func(ColumnSort) OnTappedSecondary func(*fyne.PointEvent) - columnCfg ListColumn + sortDisabled *bool + columnCfg ListColumn label *widget.RichText sortIcon *widget.Icon @@ -166,8 +169,8 @@ type colHeader struct { container *fyne.Container } -func newColHeader(columnCfg ListColumn) *colHeader { - c := &colHeader{columnCfg: columnCfg} +func newColHeader(columnCfg ListColumn, sortDisabled *bool) *colHeader { + c := &colHeader{columnCfg: columnCfg, sortDisabled: sortDisabled} c.ExtendBaseWidget(c) c.label = widget.NewRichTextWithText(columnCfg.Text) @@ -186,6 +189,9 @@ func newColHeader(columnCfg ListColumn) *colHeader { } func (c *colHeader) Tapped(*fyne.PointEvent) { + if *c.sortDisabled { + return + } switch c.Sort { case SortNone: c.Sort = SortAscending @@ -218,7 +224,7 @@ func (c *colHeader) Refresh() { if c.Sort > 0 && c.sortIcon.Hidden { c.sortIcon.Show() c.container.Add(c.sortIconNegSpacer) - } else if c.Sort == SortNone && !c.sortIcon.Hidden { + } else if (c.Sort == SortNone || *c.sortDisabled) && !c.sortIcon.Hidden { c.sortIcon.Hide() c.container.Remove(c.sortIconNegSpacer) } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 44af417..603fdde 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -61,6 +61,9 @@ type Tracklist struct { // the tracklist context menu. DisablePlaybackMenu bool + // Disables sorting the tracklist by clicking individual columns. + DisableSorting bool + // user action callbacks OnPlayTrackAt func(int) OnPlaySelection func(tracks []*mediaprovider.Track, shuffle bool) @@ -92,7 +95,7 @@ type Tracklist struct { } func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { - t := &Tracklist{tracks: tracks, visibleColumns: make([]bool, 12)} + t := &Tracklist{tracks: tracks, tracksOrigOrder: tracks, visibleColumns: make([]bool, 12)} t.ExtendBaseWidget(t) t.selectionMgr = util.NewListSelectionManager(t.lenTracks) @@ -348,6 +351,11 @@ func (t *Tracklist) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(t.container) } +func (t *Tracklist) Refresh() { + t.hdr.DisableSorting = t.DisableSorting + t.BaseWidget.Refresh() +} + func (t *Tracklist) onPlayTrackAt(idx int) { if t.OnPlayTrackAt != nil { t.OnPlayTrackAt(idx) From 84e1f01a68dc05b2dac8621f3ce45a567d1c4ae2 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 21 May 2023 21:16:22 -0700 Subject: [PATCH 05/11] add saving of tracklist sort in browsing history - but need to handle it Tracklist widget still --- ui/browsing/albumpage.go | 17 +++++++++++++- ui/browsing/artistpage.go | 11 ++++++--- ui/browsing/favoritespage.go | 7 ++++++ ui/browsing/playlistpage.go | 17 +++++++++++++- ui/widgets/tracklist.go | 43 ++++++++++++++++++++++-------------- 5 files changed, 74 insertions(+), 21 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 8135342..4faf619 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -34,6 +34,7 @@ type AlbumPage struct { type albumPageState struct { albumID string + sort widgets.TracklistSort cfg *backend.AlbumPageConfig mp mediaprovider.MediaProvider pm *backend.PlaybackManager @@ -48,6 +49,18 @@ func NewAlbumPage( mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, +) *AlbumPage { + return newAlbumPage(albumID, cfg, pm, mp, im, contr, widgets.TracklistSort{}) +} + +func newAlbumPage( + albumID string, + cfg *backend.AlbumPageConfig, + pm *backend.PlaybackManager, + mp mediaprovider.MediaProvider, + im *backend.ImageManager, + contr *controller.Controller, + sort widgets.TracklistSort, ) *AlbumPage { a := &AlbumPage{ albumPageState: albumPageState{ @@ -63,6 +76,7 @@ func NewAlbumPage( a.header = NewAlbumPageHeader(a) a.tracklist = widgets.NewTracklist(nil) a.tracklist.SetVisibleColumns(a.cfg.TracklistColumns) + a.tracklist.SetSorting(sort) a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.cfg.TracklistColumns = cols } @@ -82,6 +96,7 @@ func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer { func (a *AlbumPage) Save() SavedPage { s := a.albumPageState + s.sort = a.tracklist.Sorting() return &s } @@ -262,5 +277,5 @@ func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string { } func (s *albumPageState) Restore() Page { - return NewAlbumPage(s.albumID, s.cfg, s.pm, s.mp, s.im, s.contr) + return newAlbumPage(s.albumID, s.cfg, s.pm, s.mp, s.im, s.contr, s.sort) } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index e1a4c95..5cd5e20 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -27,6 +27,7 @@ var _ fyne.Widget = (*ArtistPage)(nil) type artistPageState struct { artistID string activeView int + trackSort widgets.TracklistSort cfg *backend.ArtistPageConfig pm *backend.PlaybackManager @@ -54,10 +55,10 @@ func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.P if cfg.InitialView == "Top Tracks" { activeView = 1 } - return newArtistPage(artistID, cfg, pm, mp, im, contr, activeView) + return newArtistPage(artistID, cfg, pm, mp, im, contr, activeView, widgets.TracklistSort{}) } -func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, activeView int) *ArtistPage { +func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, activeView int, sort widgets.TracklistSort) *ArtistPage { a := &ArtistPage{artistPageState: artistPageState{ artistID: artistID, cfg: cfg, @@ -112,6 +113,9 @@ func (a *ArtistPage) Reload() { func (a *ArtistPage) Save() SavedPage { s := a.artistPageState + if a.tracklistCtr != nil { + s.trackSort = a.tracklistCtr.Objects[0].(*widgets.Tracklist).Sorting() + } return &s } @@ -197,6 +201,7 @@ func (a *ArtistPage) showTopTracks() { tl := widgets.NewTracklist(ts) tl.AutoNumber = true tl.SetVisibleColumns(a.cfg.TracklistColumns) + tl.SetSorting(a.trackSort) tl.OnVisibleColumnsChanged = func(cols []string) { a.cfg.TracklistColumns = cols } @@ -232,7 +237,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } func (s *artistPageState) Restore() Page { - return newArtistPage(s.artistID, s.cfg, s.pm, s.mp, s.im, s.contr, s.activeView) + return newArtistPage(s.artistID, s.cfg, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort) } type ArtistPageHeader struct { diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 1201fe4..7acab39 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -27,6 +27,7 @@ type FavoritesPage struct { im *backend.ImageManager mp mediaprovider.MediaProvider + trackSort widgets.TracklistSort filter mediaprovider.AlbumFilter searchText string nowPlayingID string @@ -102,6 +103,7 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { im: saved.im, searchText: saved.searchText, filter: saved.filter, + trackSort: saved.trackSort, } a.ExtendBaseWidget(a) a.createHeader(saved.activeToggleBtn) @@ -188,6 +190,9 @@ func (a *FavoritesPage) Save() SavedPage { if a.searchGrid != nil { sf.searchGridState = a.searchGrid.SaveToState() } + if a.tracklistCtr != nil { + sf.trackSort = a.tracklistCtr.Objects[0].(*widgets.Tracklist).Sorting() + } return sf } @@ -324,6 +329,7 @@ func (a *FavoritesPage) onShowFavoriteSongs() { tracklist := widgets.NewTracklist(fav.Tracks) tracklist.AutoNumber = true tracklist.SetVisibleColumns(a.cfg.TracklistColumns) + tracklist.SetSorting(a.trackSort) tracklist.OnVisibleColumnsChanged = func(cols []string) { a.cfg.TracklistColumns = cols } @@ -358,6 +364,7 @@ type savedFavoritesPage struct { filter mediaprovider.AlbumFilter searchText string activeToggleBtn int + trackSort widgets.TracklistSort } func (s *savedFavoritesPage) Restore() Page { diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 42420d4..b18dca8 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -39,6 +39,7 @@ type playlistPageState struct { sm *backend.ServerManager pm *backend.PlaybackManager im *backend.ImageManager + trackSort widgets.TracklistSort } func NewPlaylistPage( @@ -48,12 +49,25 @@ func NewPlaylistPage( sm *backend.ServerManager, pm *backend.PlaybackManager, im *backend.ImageManager, +) *PlaylistPage { + return newPlaylistPage(playlistID, conf, contr, sm, pm, im, widgets.TracklistSort{}) +} + +func newPlaylistPage( + playlistID string, + conf *backend.PlaylistPageConfig, + contr *controller.Controller, + sm *backend.ServerManager, + pm *backend.PlaybackManager, + im *backend.ImageManager, + trackSort widgets.TracklistSort, ) *PlaylistPage { a := &PlaylistPage{playlistPageState: playlistPageState{playlistID: playlistID, conf: conf, contr: contr, sm: sm, pm: pm, im: im}} a.ExtendBaseWidget(a) a.header = NewPlaylistPageHeader(a) a.tracklist = widgets.NewTracklist(nil) a.tracklist.SetVisibleColumns(conf.TracklistColumns) + a.tracklist.SetSorting(trackSort) a.tracklist.OnVisibleColumnsChanged = func(cols []string) { conf.TracklistColumns = cols } @@ -83,6 +97,7 @@ func (a *PlaylistPage) CreateRenderer() fyne.WidgetRenderer { func (a *PlaylistPage) Save() SavedPage { p := a.playlistPageState + p.trackSort = a.tracklist.Sorting() return &p } @@ -284,5 +299,5 @@ func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *mediaprovider.Playlis } func (s *playlistPageState) Restore() Page { - return NewPlaylistPage(s.playlistID, s.conf, s.contr, s.sm, s.pm, s.im) + return newPlaylistPage(s.playlistID, s.conf, s.contr, s.sm, s.pm, s.im, s.trackSort) } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 603fdde..3f27130 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -42,6 +42,11 @@ var columns = []string{ ColumnFavorite, ColumnRating, ColumnPlays, ColumnBitrate, ColumnSize, ColumnPath, } +type TracklistSort struct { + SortOrder ColumnSort + ColumnName string +} + type Tracklist struct { widget.BaseWidget @@ -80,6 +85,7 @@ type Tracklist struct { OnTrackShown func(tracknum int) visibleColumns []bool + sorting TracklistSort tracksMutex sync.RWMutex tracks []*mediaprovider.Track @@ -103,7 +109,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { t.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 60, 60, 55, 100, 65, 75, 75, -1}) t.buildHeader() t.hdr.OnColumnSortChanged = func(idx int, sort ColumnSort) { - t.SortByColumn(colName(idx), sort) + t.SetSorting(TracklistSort{ColumnName: colName(idx), SortOrder: sort}) } t.hdr.OnColumnVisibilityChanged = t.setColumnVisible t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { @@ -229,48 +235,53 @@ func (t *Tracklist) intSort(fieldFn func(*mediaprovider.Track) int64, sortOrder t.tracks = new } -func (t *Tracklist) SortByColumn(columnName string, sortOrder ColumnSort) { +func (t *Tracklist) Sorting() TracklistSort { + return t.sorting +} + +func (t *Tracklist) SetSorting(sorting TracklistSort) { + t.sorting = sorting t.tracksMutex.Lock() - if sortOrder == SortNone { + if sorting.SortOrder == SortNone { t.tracks = t.tracksOrigOrder t.tracksMutex.Unlock() t.Refresh() return } - switch columnName { + switch sorting.ColumnName { case ColumnNum: - if sortOrder == SortDescending { + if sorting.SortOrder == SortDescending { t.tracks = sharedutil.Reversed(t.tracksOrigOrder) } else { t.tracks = t.tracksOrigOrder } case ColumnTitle: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.Name }, sortOrder) + t.stringSort(func(tr *mediaprovider.Track) string { return tr.Name }, sorting.SortOrder) case ColumnArtist: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.ArtistNames[0] }, sortOrder) + t.stringSort(func(tr *mediaprovider.Track) string { return tr.ArtistNames[0] }, sorting.SortOrder) case ColumnAlbum: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.Album }, sortOrder) + t.stringSort(func(tr *mediaprovider.Track) string { return tr.Album }, sorting.SortOrder) case ColumnPath: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.FilePath }, sortOrder) + t.stringSort(func(tr *mediaprovider.Track) string { return tr.FilePath }, sorting.SortOrder) case ColumnRating: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Rating) }, sortOrder) + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Rating) }, sorting.SortOrder) case ColumnTime: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Duration) }, sortOrder) + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Duration) }, sorting.SortOrder) case ColumnYear: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Year) }, sortOrder) + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Year) }, sorting.SortOrder) case ColumnSize: - t.intSort(func(tr *mediaprovider.Track) int64 { return tr.Size }, sortOrder) + t.intSort(func(tr *mediaprovider.Track) int64 { return tr.Size }, sorting.SortOrder) case ColumnPlays: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.PlayCount) }, sortOrder) + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.PlayCount) }, sorting.SortOrder) case ColumnBitrate: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.BitRate) }, sortOrder) + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.BitRate) }, sorting.SortOrder) case ColumnFavorite: t.intSort(func(tr *mediaprovider.Track) int64 { if tr.Favorite { return 1 } return 0 - }, sortOrder) + }, sorting.SortOrder) } t.tracksMutex.Unlock() t.Refresh() From 71ce5043b76409bc017af8406175c3af08b3aeda Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 22 May 2023 17:49:42 -0700 Subject: [PATCH 06/11] refactor a bit and have tracklist respond to set sorting calls --- ui/widgets/listheader.go | 41 ++++++---- ui/widgets/tracklist.go | 160 ++++++++++++++++++++------------------- 2 files changed, 112 insertions(+), 89 deletions(-) diff --git a/ui/widgets/listheader.go b/ui/widgets/listheader.go index f26f6da..30fd5f0 100644 --- a/ui/widgets/listheader.go +++ b/ui/widgets/listheader.go @@ -15,14 +15,19 @@ import ( "fyne.io/fyne/v2/widget" ) -type ColumnSort int +type SortType int const ( - SortNone ColumnSort = iota + SortNone SortType = iota SortAscending SortDescending ) +type ListHeaderSort struct { + ColNumber int + Type SortType +} + type ListColumn struct { Text string AlignTrailing bool @@ -34,10 +39,11 @@ type ListHeader struct { DisableSorting bool - OnColumnSortChanged func(int, ColumnSort) + OnColumnSortChanged func(ListHeaderSort) OnColumnVisibilityChanged func(int, bool) OnColumnVisibilityMenuShown func(*widget.PopUp) + sort ListHeaderSort columns []ListColumn columnVisible []bool columnsLayout *layouts.ColumnsLayout @@ -80,8 +86,8 @@ func (l *ListHeader) SetColumnVisible(colNum int, visible bool) { func (l *ListHeader) buildColumns() { for i, c := range l.columns { hdr := newColHeader(c, &l.DisableSorting) - hdr.OnSortChanged = func(i int) func(ColumnSort) { - return func(sort ColumnSort) { l.onSortChanged(i, sort) } + hdr.OnSortChanged = func(i int) func(SortType) { + return func(sort SortType) { l.SetSorting(ListHeaderSort{ColNumber: i, Type: sort}) } }(i) hdr.OnTappedSecondary = l.TappedSecondary l.columnsContainer.Add( @@ -95,17 +101,26 @@ func (l *ListHeader) buildColumns() { } } -func (l *ListHeader) onSortChanged(colNum int, sort ColumnSort) { +// Sets the sorting for the ListHeader. Will invoke +// OnColumnSortChanged if set. +func (l *ListHeader) SetSorting(sort ListHeaderSort) { + if l.sort == sort { + return + } + l.sort = sort for i, c := range l.columnsContainer.Objects { - if i != colNum { - c.(*fyne.Container).Objects[1].(*colHeader).Sort = SortNone - // TODO - //c.(*colHeader).Sort = sortNone + // TODO + //c.(*colHeader).Sort = sortNone + hdr := c.(*fyne.Container).Objects[1].(*colHeader) + if i == sort.ColNumber { + hdr.Sort = sort.Type + } else { + hdr.Sort = SortNone } } l.Refresh() if l.OnColumnSortChanged != nil { - l.OnColumnSortChanged(colNum, sort) + l.OnColumnSortChanged(sort) } } @@ -156,8 +171,8 @@ func (l *ListHeader) createOnChangedCallbk(colNum int) func(bool) { type colHeader struct { widget.BaseWidget - Sort ColumnSort - OnSortChanged func(ColumnSort) + Sort SortType + OnSortChanged func(SortType) OnTappedSecondary func(*fyne.PointEvent) sortDisabled *bool diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 3f27130..606cb27 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -43,7 +43,7 @@ var columns = []string{ } type TracklistSort struct { - SortOrder ColumnSort + SortOrder SortType ColumnName string } @@ -108,9 +108,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { // #, Title, Artist, Album, Time, Year, Favorite, Rating, Plays, Bitrate, Size, Path t.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 60, 60, 55, 100, 65, 75, 75, -1}) t.buildHeader() - t.hdr.OnColumnSortChanged = func(idx int, sort ColumnSort) { - t.SetSorting(TracklistSort{ColumnName: colName(idx), SortOrder: sort}) - } + t.hdr.OnColumnSortChanged = t.onSorted t.hdr.OnColumnVisibilityChanged = t.setColumnVisible t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { if t.OnColumnVisibilityMenuShown != nil { @@ -210,81 +208,16 @@ func (t *Tracklist) setColumnVisible(colNum int, vis bool) { } } -func (t *Tracklist) stringSort(fieldFn func(*mediaprovider.Track) string, sortOrder ColumnSort) { - new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) - copy(new, t.tracksOrigOrder) - sort.SliceStable(new, func(i, j int) bool { - cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) - if sortOrder == SortDescending { - return cmp > 0 - } - return cmp < 0 - }) - t.tracks = new -} - -func (t *Tracklist) intSort(fieldFn func(*mediaprovider.Track) int64, sortOrder ColumnSort) { - new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) - copy(new, t.tracksOrigOrder) - sort.SliceStable(new, func(i, j int) bool { - if sortOrder == SortDescending { - return fieldFn(new[i]) > fieldFn(new[j]) - } - return fieldFn(new[i]) < fieldFn(new[j]) - }) - t.tracks = new -} - func (t *Tracklist) Sorting() TracklistSort { return t.sorting } func (t *Tracklist) SetSorting(sorting TracklistSort) { - t.sorting = sorting - t.tracksMutex.Lock() - if sorting.SortOrder == SortNone { - t.tracks = t.tracksOrigOrder - t.tracksMutex.Unlock() - t.Refresh() + if sorting.ColumnName == "" { return } - switch sorting.ColumnName { - case ColumnNum: - if sorting.SortOrder == SortDescending { - t.tracks = sharedutil.Reversed(t.tracksOrigOrder) - } else { - t.tracks = t.tracksOrigOrder - } - case ColumnTitle: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.Name }, sorting.SortOrder) - case ColumnArtist: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.ArtistNames[0] }, sorting.SortOrder) - case ColumnAlbum: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.Album }, sorting.SortOrder) - case ColumnPath: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.FilePath }, sorting.SortOrder) - case ColumnRating: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Rating) }, sorting.SortOrder) - case ColumnTime: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Duration) }, sorting.SortOrder) - case ColumnYear: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Year) }, sorting.SortOrder) - case ColumnSize: - t.intSort(func(tr *mediaprovider.Track) int64 { return tr.Size }, sorting.SortOrder) - case ColumnPlays: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.PlayCount) }, sorting.SortOrder) - case ColumnBitrate: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.BitRate) }, sorting.SortOrder) - case ColumnFavorite: - t.intSort(func(tr *mediaprovider.Track) int64 { - if tr.Favorite { - return 1 - } - return 0 - }, sorting.SortOrder) - } - t.tracksMutex.Unlock() - t.Refresh() + // actual sorting will be handled in callback from header + t.hdr.SetSorting(ListHeaderSort{ColNumber: ColNumber(sorting.ColumnName), Type: sorting.SortOrder}) } func (t *Tracklist) SetNowPlaying(trackID string) { @@ -302,7 +235,7 @@ func (t *Tracklist) IncrementPlayCount(trackID string) { } } -// Remove all tracks from the tracklist. Thread-safe. +// Remove all tracks from the tracklist. Does not issue Refresh call. Thread-safe. func (t *Tracklist) Clear() { t.selectionMgr.UnselectAll() t.tracksMutex.Lock() @@ -310,12 +243,12 @@ func (t *Tracklist) Clear() { t.tracks = nil } -// Sets the tracks in the tracklist. Thread-safe. +// Sets the tracks in the tracklist. Does not issue Refresh call. Thread-safe. func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) { t.tracksMutex.Lock() defer t.tracksMutex.Unlock() - t.tracks = trs t.tracksOrigOrder = trs + t.doSortTracks() } // Returns the tracks in the tracklist in the current display order. @@ -325,7 +258,7 @@ func (t *Tracklist) GetTracks() []*mediaprovider.Track { return t.tracks } -// Append more tracks to the tracklist. Thread-safe. +// Append more tracks to the tracklist. Does not issue Refresh call. Thread-safe. func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) { t.tracksMutex.Lock() defer t.tracksMutex.Unlock() @@ -367,6 +300,81 @@ func (t *Tracklist) Refresh() { t.BaseWidget.Refresh() } +func (t *Tracklist) stringSort(fieldFn func(*mediaprovider.Track) string) { + new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) + copy(new, t.tracksOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) + if t.sorting.SortOrder == SortDescending { + return cmp > 0 + } + return cmp < 0 + }) + t.tracks = new +} + +func (t *Tracklist) intSort(fieldFn func(*mediaprovider.Track) int64) { + new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) + copy(new, t.tracksOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + if t.sorting.SortOrder == SortDescending { + return fieldFn(new[i]) > fieldFn(new[j]) + } + return fieldFn(new[i]) < fieldFn(new[j]) + }) + t.tracks = new +} + +func (t *Tracklist) doSortTracks() { + if t.sorting.SortOrder == SortNone { + t.tracks = t.tracksOrigOrder + return + } + switch t.sorting.ColumnName { + case ColumnNum: + if t.sorting.SortOrder == SortDescending { + t.tracks = sharedutil.Reversed(t.tracksOrigOrder) + } else { + t.tracks = t.tracksOrigOrder + } + case ColumnTitle: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.Name }) + case ColumnArtist: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.ArtistNames[0] }) + case ColumnAlbum: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.Album }) + case ColumnPath: + t.stringSort(func(tr *mediaprovider.Track) string { return tr.FilePath }) + case ColumnRating: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Rating) }) + case ColumnTime: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Duration) }) + case ColumnYear: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Year) }) + case ColumnSize: + t.intSort(func(tr *mediaprovider.Track) int64 { return tr.Size }) + case ColumnPlays: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.PlayCount) }) + case ColumnBitrate: + t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.BitRate) }) + case ColumnFavorite: + t.intSort(func(tr *mediaprovider.Track) int64 { + if tr.Favorite { + return 1 + } + return 0 + }) + } +} + +func (t *Tracklist) onSorted(sort ListHeaderSort) { + t.sorting = TracklistSort{ColumnName: colName(sort.ColNumber), SortOrder: sort.Type} + t.tracksMutex.Lock() + t.doSortTracks() + t.tracksMutex.Unlock() + t.Refresh() +} + func (t *Tracklist) onPlayTrackAt(idx int) { if t.OnPlayTrackAt != nil { t.OnPlayTrackAt(idx) From bd7388fbf2dc60a21454f2e6f85b6bb59c8bf450 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 23 May 2023 12:59:06 -0700 Subject: [PATCH 07/11] properly restore tracklist sort in artist page --- ui/browsing/artistpage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 5cd5e20..d12446a 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -67,6 +67,7 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.P im: im, contr: contr, activeView: activeView, + trackSort: sort, }} a.ExtendBaseWidget(a) a.header = NewArtistPageHeader(a) From 8bf9f20b8e870a3443efafbb7d9753f396b023e1 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 23 May 2023 14:24:02 -0700 Subject: [PATCH 08/11] add center alignment option to list columns --- ui/browsing/playlistspage.go | 8 ++++---- ui/widgets/artistgenrelist.go | 2 +- ui/widgets/listheader.go | 32 +++++++++++--------------------- ui/widgets/tracklist.go | 24 ++++++++++++------------ 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index e211c97..a31a185 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -286,10 +286,10 @@ func NewPlaylistList() *PlaylistList { func (p *PlaylistList) buildHeader() { p.header = widgets.NewListHeader([]widgets.ListColumn{ - {"Name", false, false}, - {"Description", false, false}, - {"Owner", false, false}, - {"Track Count", true, false}}, p.columnsLayout) + {"Name", fyne.TextAlignLeading, false}, + {"Description", fyne.TextAlignLeading, false}, + {"Owner", fyne.TextAlignLeading, false}, + {"Track Count", fyne.TextAlignTrailing, false}}, p.columnsLayout) p.header.DisableSorting = true } diff --git a/ui/widgets/artistgenrelist.go b/ui/widgets/artistgenrelist.go index a032ab9..e673bd6 100644 --- a/ui/widgets/artistgenrelist.go +++ b/ui/widgets/artistgenrelist.go @@ -65,7 +65,7 @@ func NewArtistGenreList(items []ArtistGenreListItemModel) *ArtistGenreList { } a.ExtendBaseWidget(a) a.hdr = NewListHeader([]ListColumn{ - {"Name", false, false}, {"Album Count", true, false}, {"Track Count", true, false}}, a.columnsLayout) + {"Name", fyne.TextAlignLeading, false}, {"Album Count", fyne.TextAlignTrailing, false}, {"Track Count", fyne.TextAlignTrailing, false}}, a.columnsLayout) a.hdr.DisableSorting = true a.list = widget.NewList( func() int { return len(a.Items) }, diff --git a/ui/widgets/listheader.go b/ui/widgets/listheader.go index 30fd5f0..7f3d418 100644 --- a/ui/widgets/listheader.go +++ b/ui/widgets/listheader.go @@ -8,7 +8,6 @@ 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/layout" "fyne.io/fyne/v2/theme" @@ -30,7 +29,7 @@ type ListHeaderSort struct { type ListColumn struct { Text string - AlignTrailing bool + Alignment fyne.TextAlign CanToggleVisible bool } @@ -90,14 +89,7 @@ func (l *ListHeader) buildColumns() { return func(sort SortType) { l.SetSorting(ListHeaderSort{ColNumber: i, Type: sort}) } }(i) hdr.OnTappedSecondary = l.TappedSecondary - l.columnsContainer.Add( - // hdr, - // TODO: remove debugging background - container.NewMax(container.New(&layouts.MaxPadLayout{PadLeft: 2, PadRight: 2}, - canvas.NewRectangle(theme.SelectionColor())), - hdr, - ), - ) + l.columnsContainer.Add(hdr) } } @@ -109,13 +101,10 @@ func (l *ListHeader) SetSorting(sort ListHeaderSort) { } l.sort = sort for i, c := range l.columnsContainer.Objects { - // TODO - //c.(*colHeader).Sort = sortNone - hdr := c.(*fyne.Container).Objects[1].(*colHeader) if i == sort.ColNumber { - hdr.Sort = sort.Type + c.(*colHeader).Sort = sort.Type } else { - hdr.Sort = SortNone + c.(*colHeader).Sort = SortNone } } l.Refresh() @@ -189,12 +178,10 @@ func newColHeader(columnCfg ListColumn, sortDisabled *bool) *colHeader { c.ExtendBaseWidget(c) c.label = widget.NewRichTextWithText(columnCfg.Text) - c.label.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true - al := fyne.TextAlignLeading - if columnCfg.AlignTrailing { - al = fyne.TextAlignTrailing + c.label.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ + TextStyle: fyne.TextStyle{Bold: true}, + Alignment: columnCfg.Alignment, } - c.label.Segments[0].(*widget.TextSegment).Style.Alignment = al c.sortIcon = widget.NewIcon(theme.MenuDropDownIcon()) // hack to remove extra icon space // should be hidden whenever sortIcon is hidden @@ -250,12 +237,15 @@ func (c *colHeader) Refresh() { func (c *colHeader) CreateRenderer() fyne.WidgetRenderer { if c.container == nil { c.container = container.New(&layouts.HboxCustomPadding{DisableThemePad: true, ExtraPad: -8}) - if c.columnCfg.AlignTrailing { + if c.columnCfg.Alignment != fyne.TextAlignLeading { c.container.Add(layout.NewSpacer()) } c.container.Add(c.label) c.container.Add(c.sortIcon) c.container.Add(c.sortIconNegSpacer) + if c.columnCfg.Alignment == fyne.TextAlignCenter { + c.container.Add(layout.NewSpacer()) + } } return widget.NewSimpleRenderer(c.container) } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 606cb27..cda38a8 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -144,18 +144,18 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { func (t *Tracklist) buildHeader() { t.hdr = NewListHeader([]ListColumn{ - {Text: "#", AlignTrailing: true, CanToggleVisible: false}, - {Text: "Title", AlignTrailing: false, CanToggleVisible: false}, - {Text: "Artist", AlignTrailing: false, CanToggleVisible: true}, - {Text: "Album", AlignTrailing: false, CanToggleVisible: true}, - {Text: "Time", AlignTrailing: true, CanToggleVisible: true}, - {Text: "Year", AlignTrailing: true, CanToggleVisible: true}, - {Text: "Fav.", AlignTrailing: false, CanToggleVisible: true}, - {Text: "Rating", AlignTrailing: false, CanToggleVisible: true}, - {Text: "Plays", AlignTrailing: true, CanToggleVisible: true}, - {Text: "Bitrate", AlignTrailing: true, CanToggleVisible: true}, - {Text: "Size", AlignTrailing: true, CanToggleVisible: true}, - {Text: "File Path", AlignTrailing: false, CanToggleVisible: true}}, + {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: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, + {Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}, + {Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, t.colLayout) } From 572a2e511dedbd9c898e90071b66af51ed1f07d5 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 24 May 2023 09:11:50 -0700 Subject: [PATCH 09/11] reorder playlist tracks based on original sort and switch to unsorted view --- ui/browsing/playlistpage.go | 19 +++++++++++++------ ui/widgets/tracklist.go | 8 ++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index b18dca8..c273173 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -158,16 +158,23 @@ func (a *PlaylistPage) onMoveSelectedToBottom() { } func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { - // TODO: revisit this for how to deal with sort order - idxs := a.tracklist.SelectedTrackIndexes() - newTracks := sharedutil.ReorderTracks(a.tracklist.GetTracks(), idxs, op) - ids := make([]string, len(newTracks)) - for i, tr := range newTracks { - ids[i] = tr.ID + // Since the tracklist view may be sorted in a different order than the + // actual running order, we need to get the IDs of the selected tracks + // from the tracklist and convert them to indices in the *original* run order + ids := a.tracklist.SelectedTrackIDs() + idxs := make([]int, 0, len(ids)) + for i, tr := range a.tracks { + if sharedutil.SliceContains(ids, tr.ID) { + idxs = append(idxs, i) + } } + newTracks := sharedutil.ReorderTracks(a.tracks, idxs, op) + ids = sharedutil.TracksToIDs(newTracks) if err := a.sm.Server.ReplacePlaylistTracks(a.playlistID, ids); err != nil { log.Printf("error updating playlist: %s", err.Error()) } else { + // force-switch back to unsorted view to show new track order + a.tracklist.SetSorting(widgets.TracklistSort{}) a.tracklist.SetTracks(newTracks) a.tracklist.UnselectAll() a.tracklist.Refresh() diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index cda38a8..866386c 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -214,6 +214,10 @@ func (t *Tracklist) Sorting() TracklistSort { func (t *Tracklist) SetSorting(sorting TracklistSort) { if sorting.ColumnName == "" { + // nil case - reset current sort + if sharedutil.SliceContains(columns, t.sorting.ColumnName) { + t.hdr.SetSorting(ListHeaderSort{ColNumber: ColNumber(t.sorting.ColumnName), Type: SortNone}) + } return } // actual sorting will be handled in callback from header @@ -425,7 +429,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItem("Add to playlist...", func() { if t.OnAddToPlaylist != nil { - t.OnAddToPlaylist(t.selectedTrackIDs()) + t.OnAddToPlaylist(t.SelectedTrackIDs()) } })) t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator()) @@ -513,7 +517,7 @@ func (t *Tracklist) selectedTracks() []*mediaprovider.Track { return tracks } -func (t *Tracklist) selectedTrackIDs() []string { +func (t *Tracklist) SelectedTrackIDs() []string { sel := t.selectionMgr.GetSelection() tracks := make([]string, 0, len(sel)) t.tracksMutex.RLock() From eafdae661b191cabbb41401690a213cd33d92235 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 24 May 2023 09:20:36 -0700 Subject: [PATCH 10/11] fix merge --- ui/browsing/genrespage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/browsing/genrespage.go b/ui/browsing/genrespage.go index f8ce5f5..f0bd765 100644 --- a/ui/browsing/genrespage.go +++ b/ui/browsing/genrespage.go @@ -178,7 +178,7 @@ func NewGenreList(items []*mediaprovider.Genre) *GenreList { } a.ExtendBaseWidget(a) a.hdr = widgets.NewListHeader([]widgets.ListColumn{ - {"Name", false, false}, {"Album Count", true, false}, {"Track Count", true, false}}, a.columnsLayout) + {"Name", fyne.TextAlignLeading, false}, {"Album Count", fyne.TextAlignTrailing, false}, {"Track Count", fyne.TextAlignTrailing, false}}, a.columnsLayout) a.list = widget.NewList( func() int { return len(a.Items) }, func() fyne.CanvasObject { From 289aa56f7faebcea31e67875d77a211035bf6e74 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 24 May 2023 09:27:01 -0700 Subject: [PATCH 11/11] fix merge: re-disable sorting on genres page --- ui/browsing/genrespage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/browsing/genrespage.go b/ui/browsing/genrespage.go index f0bd765..67f6ce1 100644 --- a/ui/browsing/genrespage.go +++ b/ui/browsing/genrespage.go @@ -179,6 +179,7 @@ func NewGenreList(items []*mediaprovider.Genre) *GenreList { a.ExtendBaseWidget(a) a.hdr = widgets.NewListHeader([]widgets.ListColumn{ {"Name", fyne.TextAlignLeading, false}, {"Album Count", fyne.TextAlignTrailing, false}, {"Track Count", fyne.TextAlignTrailing, false}}, a.columnsLayout) + a.hdr.DisableSorting = true a.list = widget.NewList( func() int { return len(a.Items) }, func() fyne.CanvasObject {