From b64dcdcc0b4cb6edae4abb5970be7012acc66dbf Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 2 Jun 2023 17:43:22 -0700 Subject: [PATCH 1/4] fix tracklist selection being maintained when sorting --- backend/playbackmanager.go | 9 +- sharedutil/sharedutil.go | 16 ++- ui/browsing/nowplayingpage.go | 2 +- ui/browsing/playlistpage.go | 9 +- ui/widgets/tracklist.go | 217 ++++++++++++++++++++++++---------- 5 files changed, 182 insertions(+), 71 deletions(-) diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 230f7a8..0b72e3d 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -241,16 +241,15 @@ func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) { } // trackIdxs must be sorted -func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) { - newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIdxs)) +func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) { + newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIDs)) rmCount := 0 - rmIdx := 0 + idSet := sharedutil.ToSet(trackIDs) for i, tr := range p.playQueue { - if rmIdx < len(trackIdxs) && trackIdxs[rmIdx] == i { + if _, ok := idSet[tr.ID]; ok { // removing this track // TODO: if we are removing the currently playing track, // we need to scrobble it if it played for more than the scrobble threshold - rmIdx++ if err := p.player.RemoveTrackAt(i - rmCount); err == nil { rmCount++ } else { diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index d7ad92b..35cd5cb 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -40,15 +40,19 @@ func MapSlice[T any, U any](ts []T, f func(T) U) []U { return result } -func IndexOf[T comparable](ts []T, t T) int { +func Find[T any](ts []T, f func(T) bool) int { for i, tt := range ts { - if t == tt { + if f(tt) { return i } } return -1 } +func IndexOf[T comparable](ts []T, t T) int { + return Find(ts, func(tt T) bool { return t == tt }) +} + func Reversed[T any](ts []T) []T { if ts == nil { return nil @@ -62,6 +66,14 @@ func Reversed[T any](ts []T) []T { return new } +func ToSet[T comparable](ts []T) map[T]interface{} { + set := make(map[T]interface{}, len(ts)) + for _, t := range ts { + set[t] = nil + } + return set +} + func FindTrackByID(id string, tracks []*mediaprovider.Track) *mediaprovider.Track { for _, tr := range tracks { if id == tr.ID { diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 46b121e..6ddb5e2 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -99,7 +99,7 @@ func (a *NowPlayingPage) onPlayTrackAt(tracknum int) { } func (a *NowPlayingPage) onRemoveSelectedFromQueue() { - a.pm.RemoveTracksFromQueue(a.tracklist.SelectedTrackIndexes()) + a.pm.RemoveTracksFromQueue(a.tracklist.SelectedTrackIDs()) a.tracklist.UnselectAll() a.Reload() } diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 2d3b74e..698b86b 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -186,7 +186,14 @@ func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { } func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { - a.sm.Server.EditPlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes()) + sel := sharedutil.ToSet(a.tracklist.SelectedTrackIDs()) + idxs := make([]int, 0, len(sel)) + for i, tr := range a.tracks { + if _, ok := sel[tr.ID]; ok { + idxs = append(idxs, i) + } + } + a.sm.Server.EditPlaylistTracks(a.playlistID, nil, idxs) a.tracklist.UnselectAll() go a.Reload() } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index bbad9ae..67aaf31 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -88,10 +88,9 @@ type Tracklist struct { sorting TracklistSort tracksMutex sync.RWMutex - tracks []*mediaprovider.Track - tracksOrigOrder []*mediaprovider.Track + tracks []*trackModel + tracksOrigOrder []*trackModel - selectionMgr util.ListSelectionManager nowPlayingID string colLayout *layouts.ColumnsLayout hdr *ListHeader @@ -100,11 +99,19 @@ type Tracklist struct { container *fyne.Container } -func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { - t := &Tracklist{tracks: tracks, tracksOrigOrder: tracks, visibleColumns: make([]bool, 12)} +type trackModel struct { + track *mediaprovider.Track + selected bool +} +func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { + t := &Tracklist{visibleColumns: make([]bool, 12)} t.ExtendBaseWidget(t) - t.selectionMgr = util.NewListSelectionManager(t.lenTracks) + + if len(tracks) > 0 { + t.SetTracks(tracks) + } + // #, 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() @@ -128,12 +135,11 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { func(itemID widget.ListItemID, item fyne.CanvasObject) { tr := item.(*TrackRow) tr.trackIdx = itemID - tr.Selected = t.selectionMgr.IsSelected(itemID) i := -1 // signal that we want to display the actual track num. if t.AutoNumber { i = itemID + 1 } - tr.Update(t.TrackAt(itemID), i) + tr.Update(t.trackModelAt(itemID), i) if t.OnTrackShown != nil { t.OnTrackShown(itemID) } @@ -161,9 +167,14 @@ func (t *Tracklist) buildHeader() { // Gets the track at the given index. Thread-safe. func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track { + return t.trackModelAt(idx).track +} + +func (t *Tracklist) trackModelAt(idx int) *trackModel { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() if idx >= len(t.tracks) { + log.Println("error: Tracklist.trackModelAt: index out of range") return nil } return t.tracks[idx] @@ -231,9 +242,8 @@ func (t *Tracklist) SetNowPlaying(trackID string) { func (t *Tracklist) IncrementPlayCount(trackID string) { t.tracksMutex.RLock() - tr := sharedutil.FindTrackByID(trackID, t.tracks) t.tracksMutex.RUnlock() - if tr != nil { + if tr := t.findTrackByID(trackID); tr != nil { tr.PlayCount += 1 t.list.Refresh() } @@ -241,17 +251,17 @@ func (t *Tracklist) IncrementPlayCount(trackID string) { // Remove all tracks from the tracklist. Does not issue Refresh call. Thread-safe. func (t *Tracklist) Clear() { - t.selectionMgr.UnselectAll() t.tracksMutex.Lock() defer t.tracksMutex.Unlock() t.tracks = nil + t.tracksOrigOrder = nil } // 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.tracksOrigOrder = trs + t.tracksOrigOrder = toTrackModels(trs) t.doSortTracks() } @@ -259,39 +269,55 @@ func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) { func (t *Tracklist) GetTracks() []*mediaprovider.Track { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - return t.tracks + return sharedutil.MapSlice(t.tracks, func(tm *trackModel) *mediaprovider.Track { + return tm.track + }) } // 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() - t.tracks = append(t.tracks, trs...) + t.tracksOrigOrder = append(t.tracks, toTrackModels(trs)...) + t.doSortTracks() } func (t *Tracklist) SelectAll() { - t.selectionMgr.SelectAll() + t.tracksMutex.RLock() + for _, tm := range t.tracks { + tm.selected = true + } + t.tracksMutex.RUnlock() t.list.Refresh() } func (t *Tracklist) UnselectAll() { - t.selectionMgr.UnselectAll() + t.unselectAll() t.list.Refresh() } +func (t *Tracklist) unselectAll() { + t.tracksMutex.RLock() + for _, tm := range t.tracks { + tm.selected = false + } + t.tracksMutex.RUnlock() +} + func (t *Tracklist) SelectAndScrollToTrack(trackID string) { t.tracksMutex.RLock() idx := -1 for i, tr := range t.tracks { - if tr.ID == trackID { + if tr.track.ID == trackID { idx = i - break + tr.selected = true + } else { + tr.selected = false } } t.tracksMutex.RUnlock() if idx >= 0 { t.list.ScrollTo(idx) - t.selectionMgr.Select(idx) } } @@ -304,12 +330,18 @@ func (t *Tracklist) Refresh() { t.BaseWidget.Refresh() } +func toTrackModels(trs []*mediaprovider.Track) []*trackModel { + return sharedutil.MapSlice(trs, func(tr *mediaprovider.Track) *trackModel { + return &trackModel{track: tr, selected: false} + }) +} + // do nothing Tapped handler so that tapping the separator between rows // doesn't fall through to the page (which calls UnselectAll on tracklist) func (t *Tracklist) Tapped(*fyne.PointEvent) {} -func (t *Tracklist) stringSort(fieldFn func(*mediaprovider.Track) string) { - new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) +func (t *Tracklist) stringSort(fieldFn func(*trackModel) string) { + new := make([]*trackModel, len(t.tracksOrigOrder)) copy(new, t.tracksOrigOrder) sort.SliceStable(new, func(i, j int) bool { cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) @@ -321,8 +353,8 @@ func (t *Tracklist) stringSort(fieldFn func(*mediaprovider.Track) string) { t.tracks = new } -func (t *Tracklist) intSort(fieldFn func(*mediaprovider.Track) int64) { - new := make([]*mediaprovider.Track, len(t.tracksOrigOrder)) +func (t *Tracklist) intSort(fieldFn func(*trackModel) int64) { + new := make([]*trackModel, len(t.tracksOrigOrder)) copy(new, t.tracksOrigOrder) sort.SliceStable(new, func(i, j int) bool { if t.sorting.SortOrder == SortDescending { @@ -346,28 +378,28 @@ func (t *Tracklist) doSortTracks() { t.tracks = t.tracksOrigOrder } case ColumnTitle: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.Name }) + t.stringSort(func(tr *trackModel) string { return tr.track.Name }) case ColumnArtist: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.ArtistNames[0] }) + t.stringSort(func(tr *trackModel) string { return tr.track.ArtistNames[0] }) case ColumnAlbum: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.Album }) + t.stringSort(func(tr *trackModel) string { return tr.track.Album }) case ColumnPath: - t.stringSort(func(tr *mediaprovider.Track) string { return tr.FilePath }) + t.stringSort(func(tr *trackModel) string { return tr.track.FilePath }) case ColumnRating: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Rating) }) + t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Rating) }) case ColumnTime: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Duration) }) + t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Duration) }) case ColumnYear: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.Year) }) + t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Year) }) case ColumnSize: - t.intSort(func(tr *mediaprovider.Track) int64 { return tr.Size }) + t.intSort(func(tr *trackModel) int64 { return tr.track.Size }) case ColumnPlays: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.PlayCount) }) + t.intSort(func(tr *trackModel) int64 { return int64(tr.track.PlayCount) }) case ColumnBitrate: - t.intSort(func(tr *mediaprovider.Track) int64 { return int64(tr.BitRate) }) + t.intSort(func(tr *trackModel) int64 { return int64(tr.track.BitRate) }) case ColumnFavorite: - t.intSort(func(tr *mediaprovider.Track) int64 { - if tr.Favorite { + t.intSort(func(tr *trackModel) int64 { + if tr.track.Favorite { return 1 } return 0 @@ -393,20 +425,60 @@ func (t *Tracklist) onSelectTrack(idx int) { if d, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok { mod := d.CurrentKeyModifiers() if mod&os.ControlModifier != 0 { - t.selectionMgr.SelectAddOrRemove(idx) + t.selectAddOrRemove(idx) } else if mod&fyne.KeyModifierShift != 0 { - t.selectionMgr.SelectRange(idx) + t.selectRange(idx) } else { - t.selectionMgr.Select(idx) + t.selectTrack(idx) } } else { - t.selectionMgr.Select(idx) + t.selectTrack(idx) } t.list.Refresh() } +func (t *Tracklist) selectAddOrRemove(idx int) { + t.tracksMutex.RLock() + defer t.tracksMutex.RUnlock() + t.tracks[idx].selected = !t.tracks[idx].selected +} + +func (t *Tracklist) selectTrack(idx int) { + t.tracksMutex.RLock() + defer t.tracksMutex.RUnlock() + if t.tracks[idx].selected { + return + } + t.unselectAll() + t.tracks[idx].selected = true +} + +func (t *Tracklist) selectRange(idx int) { + t.tracksMutex.RLock() + defer t.tracksMutex.RUnlock() + if t.tracks[idx].selected { + return + } + lastSelected := -1 + for i := len(t.tracks) - 1; i >= 0; i-- { + if t.tracks[i].selected { + lastSelected = i + break + } + } + if lastSelected < 0 { + t.tracks[idx].selected = true + return + } + from := minInt(idx, lastSelected) + to := maxInt(idx, lastSelected) + for i := from; i <= to; i++ { + t.tracks[i].selected = true + } +} + func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { - t.selectionMgr.Select(trackIdx) + t.selectTrack(trackIdx) t.list.Refresh() if t.ctxMenu == nil { t.ctxMenu = fyne.NewMenu("") @@ -459,7 +531,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 := t.findTrackByID(trackID) t.tracksMutex.RUnlock() t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false) } @@ -480,7 +552,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 := t.findTrackByID(trackID) t.tracksMutex.RUnlock() t.onSetRatings([]*mediaprovider.Track{tr}, rating, false) } @@ -510,30 +582,34 @@ func (t *Tracklist) onAlbumTapped(albumID string) { } } -func (t *Tracklist) selectedTracks() []*mediaprovider.Track { - sel := t.selectionMgr.GetSelection() - tracks := make([]*mediaprovider.Track, 0, len(sel)) +func (t *Tracklist) findTrackByID(id string) *mediaprovider.Track { + idx := sharedutil.Find(t.tracks, func(tr *trackModel) bool { + return tr.track.ID == id + }) + if idx >= 0 { + return t.tracks[idx].track + } + return nil +} + +func (t *Tracklist) selectedTrackModels() []*trackModel { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - for _, idx := range sel { - tracks = append(tracks, t.tracks[idx]) - } - return tracks + return sharedutil.FilterSlice(t.tracks, func(tm *trackModel) bool { + return tm.selected + }) +} + +func (t *Tracklist) selectedTracks() []*mediaprovider.Track { + return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) *mediaprovider.Track { + return tm.track + }) } func (t *Tracklist) SelectedTrackIDs() []string { - sel := t.selectionMgr.GetSelection() - tracks := make([]string, 0, len(sel)) - t.tracksMutex.RLock() - defer t.tracksMutex.RUnlock() - for _, idx := range sel { - tracks = append(tracks, t.tracks[idx].ID) - } - return tracks -} - -func (t *Tracklist) SelectedTrackIndexes() []int { - return t.selectionMgr.GetSelection() + return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) string { + return tm.track.ID + }) } func (t *Tracklist) lenTracks() int { @@ -629,9 +705,12 @@ func newTrailingAlignRichText() *widget.RichText { return rt } -func (t *TrackRow) Update(tr *mediaprovider.Track, rowNum int) { +func (t *TrackRow) Update(tm *trackModel, rowNum int) { + t.Selected = tm.selected + // Update info that can change if this row is bound to // a new track (*mediaprovider.Track) + tr := tm.track if tr.ID != t.trackID { if t.Focused { fyne.CurrentApp().Driver().CanvasForObject(t).Focus(nil) @@ -746,3 +825,17 @@ func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) { t.OnTappedSecondary(e, t.trackIdx) } } + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} From f17c48efa0502a4f6578c7b372821867f6840cd6 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 2 Jun 2023 17:43:56 -0700 Subject: [PATCH 2/4] rm unused listselectionmanager --- ui/util/listselectionmanager.go | 154 -------------------------------- 1 file changed, 154 deletions(-) delete mode 100644 ui/util/listselectionmanager.go diff --git a/ui/util/listselectionmanager.go b/ui/util/listselectionmanager.go deleted file mode 100644 index 64216bd..0000000 --- a/ui/util/listselectionmanager.go +++ /dev/null @@ -1,154 +0,0 @@ -package util - -type ListSelectionManager struct { - lastSelectedRow int - numSelected int - selected BitSet - len func() int -} - -func NewListSelectionManager(lenFn func() int) ListSelectionManager { - return ListSelectionManager{lastSelectedRow: -1, len: lenFn} -} - -// If the given row is not selected, reset the selection -// and select only the given row. -func (l *ListSelectionManager) Select(row int) { - if row < 0 || l.selected.IsSet(uint(row)) { - return - } - l.UnselectAll() - l.selectAdd(row) -} - -// If it is not selected, add the given row to the selection. -// If it is selected, unselet it. -func (l *ListSelectionManager) SelectAddOrRemove(row int) { - if row < 0 { - return - } - if l.selected.IsSet(uint(row)) { - // find new last selected row - if row == l.lastSelectedRow { - for i := row - 1; i >= -1; i++ { - if i == -1 { - l.lastSelectedRow = i - } else if l.selected.IsSet(uint(i)) { - l.lastSelectedRow = i - break - } - } - } - l.selected.Unset(uint(row)) - l.numSelected -= 1 - return - } - - l.selectAdd(row) -} - -func (l *ListSelectionManager) selectAdd(row int) { - l.numSelected += 1 - l.selected.Set(uint(row)) - if row > l.lastSelectedRow { - l.lastSelectedRow = row - } -} - -// Select a range between the given row and the furthest-down -// row that is currently selected (which may be above the given row) -// Note: this is modeled after what, as far as I can tell, is Gmail's selection behavior -func (l *ListSelectionManager) SelectRange(row int) { - if row < 0 || l.selected.IsSet(uint(row)) { - return - } - if l.numSelected == 0 { - l.selectAdd(row) - return - } - m := maxInt(row, l.lastSelectedRow) - for i := minInt(l.lastSelectedRow, row); i <= m; i++ { - l.selectAdd(i) - } -} - -func (l *ListSelectionManager) SelectAll() { - for i := 0; i < l.len(); i++ { - l.selectAdd(i) - } -} - -func (l *ListSelectionManager) UnselectAll() { - l.selected = nil - l.numSelected = 0 - l.lastSelectedRow = -1 -} - -func (l *ListSelectionManager) IsSelected(row int) bool { - return row >= 0 && l.selected.IsSet(uint(row)) -} - -func (l *ListSelectionManager) GetSelection() []int { - var sel []int - for i := 0; i < l.len(); i++ { - if l.selected.IsSet(uint(i)) { - sel = append(sel, i) - } - if len(sel) == l.numSelected { - break - } - } - return sel -} - -func (l *ListSelectionManager) AreAllSelected() bool { - return l.numSelected == l.len() -} - -func minInt(a, b int) int { - if a < b { - return a - } - return b -} - -func maxInt(a, b int) int { - if a > b { - return a - } - return b -} - -// BitSet implementation from -// https://stackoverflow.com/questions/2311373/how-to-implement-bitset-with-go - -const uint64size = 64 - -// BitSet is a set of bits that can be set, cleared and queried. -type BitSet []uint64 - -// Set ensures that the given bit is set in the BitSet. -func (s *BitSet) Set(i uint) { - if len(*s) < int(i/uint64size+1) { - r := make([]uint64, i/uint64size+1) - copy(r, *s) - *s = r - } - (*s)[i/uint64size] |= 1 << (i % uint64size) -} - -// Unset ensures that the given bit is cleared (not set) in the BitSet. -func (s *BitSet) Unset(i uint) { - if len(*s) >= int(i/uint64size+1) { - (*s)[i/uint64size] &^= 1 << (i % uint64size) - } -} - -// IsSet returns true if the given bit is set, false if it is cleared. -func (s *BitSet) IsSet(i uint) bool { - idx := i / uint64size - if idx >= uint(len(*s)) { - return false - } - return (*s)[i/uint64size]&(1<<(i%uint64size)) != 0 -} From 17f9370e0a05015dbc7a2933cd6f490eed8ceae6 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 3 Jun 2023 08:40:49 -0700 Subject: [PATCH 3/4] renumber correctly after removing tracks from playlist --- ui/browsing/playlistpage.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 698b86b..66c9bad 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -132,12 +132,7 @@ func (a *PlaylistPage) load() { log.Printf("Failed to get playlist: %s", err.Error()) return } - // Playlists, like albums, have a sequential running order. We want the number column to - // represent the track's original position in the playlist, if the user applies a sort. - // Re-purpose the TrackNumber field for the track's position in the playlist, rather than its parent album. - for i, tr := range playlist.Tracks { - tr.TrackNumber = i + 1 - } + renumberTracks(playlist.Tracks) a.tracks = playlist.Tracks a.tracklist.SetTracks(playlist.Tracks) a.tracklist.SetNowPlaying(a.nowPlayingID) @@ -145,6 +140,15 @@ func (a *PlaylistPage) load() { a.header.Update(playlist) } +func renumberTracks(tracks []*mediaprovider.Track) { + // Playlists, like albums, have a sequential running order. We want the number column to + // represent the track's original position in the playlist, if the user applies a sort. + // Re-purpose the TrackNumber field for the track's position in the playlist, rather than its parent album. + for i, tr := range tracks { + tr.TrackNumber = i + 1 + } +} + func (a *PlaylistPage) onMoveSelectedToTop() { a.doSetNewTrackOrder(sharedutil.MoveToTop) } @@ -177,6 +181,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 { + renumberTracks(newTracks) // force-switch back to unsorted view to show new track order a.tracklist.SetSorting(widgets.TracklistSort{}) a.tracklist.SetTracks(newTracks) From 6d555bc8b8cfda7fc2e8ae5b8e78c322bc616917 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 3 Jun 2023 08:43:32 -0700 Subject: [PATCH 4/4] remove obsolete comment --- backend/playbackmanager.go | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 0b72e3d..67a5d92 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -240,7 +240,6 @@ func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) { } } -// trackIdxs must be sorted func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) { newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIDs)) rmCount := 0