Merge pull request #183 from dweymouth/fix-track-sort
Fix tracklist selection not updating properly when sorting
This commit is contained in:
@@ -240,17 +240,15 @@ func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// trackIdxs must be sorted
|
func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) {
|
||||||
func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) {
|
newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIDs))
|
||||||
newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIdxs))
|
|
||||||
rmCount := 0
|
rmCount := 0
|
||||||
rmIdx := 0
|
idSet := sharedutil.ToSet(trackIDs)
|
||||||
for i, tr := range p.playQueue {
|
for i, tr := range p.playQueue {
|
||||||
if rmIdx < len(trackIdxs) && trackIdxs[rmIdx] == i {
|
if _, ok := idSet[tr.ID]; ok {
|
||||||
// removing this track
|
// removing this track
|
||||||
// TODO: if we are removing the currently playing track,
|
// TODO: if we are removing the currently playing track,
|
||||||
// we need to scrobble it if it played for more than the scrobble threshold
|
// we need to scrobble it if it played for more than the scrobble threshold
|
||||||
rmIdx++
|
|
||||||
if err := p.player.RemoveTrackAt(i - rmCount); err == nil {
|
if err := p.player.RemoveTrackAt(i - rmCount); err == nil {
|
||||||
rmCount++
|
rmCount++
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -40,15 +40,19 @@ func MapSlice[T any, U any](ts []T, f func(T) U) []U {
|
|||||||
return result
|
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 {
|
for i, tt := range ts {
|
||||||
if t == tt {
|
if f(tt) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1
|
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 {
|
func Reversed[T any](ts []T) []T {
|
||||||
if ts == nil {
|
if ts == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -62,6 +66,14 @@ func Reversed[T any](ts []T) []T {
|
|||||||
return new
|
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 {
|
func FindTrackByID(id string, tracks []*mediaprovider.Track) *mediaprovider.Track {
|
||||||
for _, tr := range tracks {
|
for _, tr := range tracks {
|
||||||
if id == tr.ID {
|
if id == tr.ID {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (a *NowPlayingPage) onPlayTrackAt(tracknum int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *NowPlayingPage) onRemoveSelectedFromQueue() {
|
func (a *NowPlayingPage) onRemoveSelectedFromQueue() {
|
||||||
a.pm.RemoveTracksFromQueue(a.tracklist.SelectedTrackIndexes())
|
a.pm.RemoveTracksFromQueue(a.tracklist.SelectedTrackIDs())
|
||||||
a.tracklist.UnselectAll()
|
a.tracklist.UnselectAll()
|
||||||
a.Reload()
|
a.Reload()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,12 +132,7 @@ func (a *PlaylistPage) load() {
|
|||||||
log.Printf("Failed to get playlist: %s", err.Error())
|
log.Printf("Failed to get playlist: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Playlists, like albums, have a sequential running order. We want the number column to
|
renumberTracks(playlist.Tracks)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
a.tracks = playlist.Tracks
|
a.tracks = playlist.Tracks
|
||||||
a.tracklist.SetTracks(playlist.Tracks)
|
a.tracklist.SetTracks(playlist.Tracks)
|
||||||
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
||||||
@@ -145,6 +140,15 @@ func (a *PlaylistPage) load() {
|
|||||||
a.header.Update(playlist)
|
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() {
|
func (a *PlaylistPage) onMoveSelectedToTop() {
|
||||||
a.doSetNewTrackOrder(sharedutil.MoveToTop)
|
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 {
|
if err := a.sm.Server.ReplacePlaylistTracks(a.playlistID, ids); err != nil {
|
||||||
log.Printf("error updating playlist: %s", err.Error())
|
log.Printf("error updating playlist: %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
renumberTracks(newTracks)
|
||||||
// force-switch back to unsorted view to show new track order
|
// force-switch back to unsorted view to show new track order
|
||||||
a.tracklist.SetSorting(widgets.TracklistSort{})
|
a.tracklist.SetSorting(widgets.TracklistSort{})
|
||||||
a.tracklist.SetTracks(newTracks)
|
a.tracklist.SetTracks(newTracks)
|
||||||
@@ -186,7 +191,14 @@ func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPage) onRemoveSelectedFromPlaylist() {
|
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()
|
a.tracklist.UnselectAll()
|
||||||
go a.Reload()
|
go a.Reload()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
+155
-62
@@ -88,10 +88,9 @@ type Tracklist struct {
|
|||||||
sorting TracklistSort
|
sorting TracklistSort
|
||||||
|
|
||||||
tracksMutex sync.RWMutex
|
tracksMutex sync.RWMutex
|
||||||
tracks []*mediaprovider.Track
|
tracks []*trackModel
|
||||||
tracksOrigOrder []*mediaprovider.Track
|
tracksOrigOrder []*trackModel
|
||||||
|
|
||||||
selectionMgr util.ListSelectionManager
|
|
||||||
nowPlayingID string
|
nowPlayingID string
|
||||||
colLayout *layouts.ColumnsLayout
|
colLayout *layouts.ColumnsLayout
|
||||||
hdr *ListHeader
|
hdr *ListHeader
|
||||||
@@ -100,11 +99,19 @@ type Tracklist struct {
|
|||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
|
type trackModel struct {
|
||||||
t := &Tracklist{tracks: tracks, tracksOrigOrder: tracks, visibleColumns: make([]bool, 12)}
|
track *mediaprovider.Track
|
||||||
|
selected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
|
||||||
|
t := &Tracklist{visibleColumns: make([]bool, 12)}
|
||||||
t.ExtendBaseWidget(t)
|
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
|
// #, 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.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 60, 60, 55, 100, 65, 75, 75, -1})
|
||||||
t.buildHeader()
|
t.buildHeader()
|
||||||
@@ -128,12 +135,11 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
|
|||||||
func(itemID widget.ListItemID, item fyne.CanvasObject) {
|
func(itemID widget.ListItemID, item fyne.CanvasObject) {
|
||||||
tr := item.(*TrackRow)
|
tr := item.(*TrackRow)
|
||||||
tr.trackIdx = itemID
|
tr.trackIdx = itemID
|
||||||
tr.Selected = t.selectionMgr.IsSelected(itemID)
|
|
||||||
i := -1 // signal that we want to display the actual track num.
|
i := -1 // signal that we want to display the actual track num.
|
||||||
if t.AutoNumber {
|
if t.AutoNumber {
|
||||||
i = itemID + 1
|
i = itemID + 1
|
||||||
}
|
}
|
||||||
tr.Update(t.TrackAt(itemID), i)
|
tr.Update(t.trackModelAt(itemID), i)
|
||||||
if t.OnTrackShown != nil {
|
if t.OnTrackShown != nil {
|
||||||
t.OnTrackShown(itemID)
|
t.OnTrackShown(itemID)
|
||||||
}
|
}
|
||||||
@@ -161,9 +167,14 @@ func (t *Tracklist) buildHeader() {
|
|||||||
|
|
||||||
// Gets the track at the given index. Thread-safe.
|
// Gets the track at the given index. Thread-safe.
|
||||||
func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track {
|
func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track {
|
||||||
|
return t.trackModelAt(idx).track
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracklist) trackModelAt(idx int) *trackModel {
|
||||||
t.tracksMutex.RLock()
|
t.tracksMutex.RLock()
|
||||||
defer t.tracksMutex.RUnlock()
|
defer t.tracksMutex.RUnlock()
|
||||||
if idx >= len(t.tracks) {
|
if idx >= len(t.tracks) {
|
||||||
|
log.Println("error: Tracklist.trackModelAt: index out of range")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return t.tracks[idx]
|
return t.tracks[idx]
|
||||||
@@ -231,9 +242,8 @@ func (t *Tracklist) SetNowPlaying(trackID string) {
|
|||||||
|
|
||||||
func (t *Tracklist) IncrementPlayCount(trackID string) {
|
func (t *Tracklist) IncrementPlayCount(trackID string) {
|
||||||
t.tracksMutex.RLock()
|
t.tracksMutex.RLock()
|
||||||
tr := sharedutil.FindTrackByID(trackID, t.tracks)
|
|
||||||
t.tracksMutex.RUnlock()
|
t.tracksMutex.RUnlock()
|
||||||
if tr != nil {
|
if tr := t.findTrackByID(trackID); tr != nil {
|
||||||
tr.PlayCount += 1
|
tr.PlayCount += 1
|
||||||
t.list.Refresh()
|
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.
|
// Remove all tracks from the tracklist. Does not issue Refresh call. Thread-safe.
|
||||||
func (t *Tracklist) Clear() {
|
func (t *Tracklist) Clear() {
|
||||||
t.selectionMgr.UnselectAll()
|
|
||||||
t.tracksMutex.Lock()
|
t.tracksMutex.Lock()
|
||||||
defer t.tracksMutex.Unlock()
|
defer t.tracksMutex.Unlock()
|
||||||
t.tracks = nil
|
t.tracks = nil
|
||||||
|
t.tracksOrigOrder = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the tracks in the tracklist. Does not issue Refresh call. Thread-safe.
|
// Sets the tracks in the tracklist. Does not issue Refresh call. Thread-safe.
|
||||||
func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) {
|
func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) {
|
||||||
t.tracksMutex.Lock()
|
t.tracksMutex.Lock()
|
||||||
defer t.tracksMutex.Unlock()
|
defer t.tracksMutex.Unlock()
|
||||||
t.tracksOrigOrder = trs
|
t.tracksOrigOrder = toTrackModels(trs)
|
||||||
t.doSortTracks()
|
t.doSortTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,39 +269,55 @@ func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) {
|
|||||||
func (t *Tracklist) GetTracks() []*mediaprovider.Track {
|
func (t *Tracklist) GetTracks() []*mediaprovider.Track {
|
||||||
t.tracksMutex.RLock()
|
t.tracksMutex.RLock()
|
||||||
defer t.tracksMutex.RUnlock()
|
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.
|
// Append more tracks to the tracklist. Does not issue Refresh call. Thread-safe.
|
||||||
func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) {
|
func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) {
|
||||||
t.tracksMutex.Lock()
|
t.tracksMutex.Lock()
|
||||||
defer t.tracksMutex.Unlock()
|
defer t.tracksMutex.Unlock()
|
||||||
t.tracks = append(t.tracks, trs...)
|
t.tracksOrigOrder = append(t.tracks, toTrackModels(trs)...)
|
||||||
|
t.doSortTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracklist) SelectAll() {
|
func (t *Tracklist) SelectAll() {
|
||||||
t.selectionMgr.SelectAll()
|
t.tracksMutex.RLock()
|
||||||
|
for _, tm := range t.tracks {
|
||||||
|
tm.selected = true
|
||||||
|
}
|
||||||
|
t.tracksMutex.RUnlock()
|
||||||
t.list.Refresh()
|
t.list.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracklist) UnselectAll() {
|
func (t *Tracklist) UnselectAll() {
|
||||||
t.selectionMgr.UnselectAll()
|
t.unselectAll()
|
||||||
t.list.Refresh()
|
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) {
|
func (t *Tracklist) SelectAndScrollToTrack(trackID string) {
|
||||||
t.tracksMutex.RLock()
|
t.tracksMutex.RLock()
|
||||||
idx := -1
|
idx := -1
|
||||||
for i, tr := range t.tracks {
|
for i, tr := range t.tracks {
|
||||||
if tr.ID == trackID {
|
if tr.track.ID == trackID {
|
||||||
idx = i
|
idx = i
|
||||||
break
|
tr.selected = true
|
||||||
|
} else {
|
||||||
|
tr.selected = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.tracksMutex.RUnlock()
|
t.tracksMutex.RUnlock()
|
||||||
if idx >= 0 {
|
if idx >= 0 {
|
||||||
t.list.ScrollTo(idx)
|
t.list.ScrollTo(idx)
|
||||||
t.selectionMgr.Select(idx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,12 +330,18 @@ func (t *Tracklist) Refresh() {
|
|||||||
t.BaseWidget.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
|
// do nothing Tapped handler so that tapping the separator between rows
|
||||||
// doesn't fall through to the page (which calls UnselectAll on tracklist)
|
// doesn't fall through to the page (which calls UnselectAll on tracklist)
|
||||||
func (t *Tracklist) Tapped(*fyne.PointEvent) {}
|
func (t *Tracklist) Tapped(*fyne.PointEvent) {}
|
||||||
|
|
||||||
func (t *Tracklist) stringSort(fieldFn func(*mediaprovider.Track) string) {
|
func (t *Tracklist) stringSort(fieldFn func(*trackModel) string) {
|
||||||
new := make([]*mediaprovider.Track, len(t.tracksOrigOrder))
|
new := make([]*trackModel, len(t.tracksOrigOrder))
|
||||||
copy(new, t.tracksOrigOrder)
|
copy(new, t.tracksOrigOrder)
|
||||||
sort.SliceStable(new, func(i, j int) bool {
|
sort.SliceStable(new, func(i, j int) bool {
|
||||||
cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j]))
|
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
|
t.tracks = new
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracklist) intSort(fieldFn func(*mediaprovider.Track) int64) {
|
func (t *Tracklist) intSort(fieldFn func(*trackModel) int64) {
|
||||||
new := make([]*mediaprovider.Track, len(t.tracksOrigOrder))
|
new := make([]*trackModel, len(t.tracksOrigOrder))
|
||||||
copy(new, t.tracksOrigOrder)
|
copy(new, t.tracksOrigOrder)
|
||||||
sort.SliceStable(new, func(i, j int) bool {
|
sort.SliceStable(new, func(i, j int) bool {
|
||||||
if t.sorting.SortOrder == SortDescending {
|
if t.sorting.SortOrder == SortDescending {
|
||||||
@@ -346,28 +378,28 @@ func (t *Tracklist) doSortTracks() {
|
|||||||
t.tracks = t.tracksOrigOrder
|
t.tracks = t.tracksOrigOrder
|
||||||
}
|
}
|
||||||
case ColumnTitle:
|
case ColumnTitle:
|
||||||
t.stringSort(func(tr *mediaprovider.Track) string { return tr.Name })
|
t.stringSort(func(tr *trackModel) string { return tr.track.Name })
|
||||||
case ColumnArtist:
|
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:
|
case ColumnAlbum:
|
||||||
t.stringSort(func(tr *mediaprovider.Track) string { return tr.Album })
|
t.stringSort(func(tr *trackModel) string { return tr.track.Album })
|
||||||
case ColumnPath:
|
case ColumnPath:
|
||||||
t.stringSort(func(tr *mediaprovider.Track) string { return tr.FilePath })
|
t.stringSort(func(tr *trackModel) string { return tr.track.FilePath })
|
||||||
case ColumnRating:
|
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:
|
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:
|
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:
|
case ColumnSize:
|
||||||
t.intSort(func(tr *mediaprovider.Track) int64 { return tr.Size })
|
t.intSort(func(tr *trackModel) int64 { return tr.track.Size })
|
||||||
case ColumnPlays:
|
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:
|
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:
|
case ColumnFavorite:
|
||||||
t.intSort(func(tr *mediaprovider.Track) int64 {
|
t.intSort(func(tr *trackModel) int64 {
|
||||||
if tr.Favorite {
|
if tr.track.Favorite {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@@ -393,20 +425,60 @@ func (t *Tracklist) onSelectTrack(idx int) {
|
|||||||
if d, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok {
|
if d, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok {
|
||||||
mod := d.CurrentKeyModifiers()
|
mod := d.CurrentKeyModifiers()
|
||||||
if mod&os.ControlModifier != 0 {
|
if mod&os.ControlModifier != 0 {
|
||||||
t.selectionMgr.SelectAddOrRemove(idx)
|
t.selectAddOrRemove(idx)
|
||||||
} else if mod&fyne.KeyModifierShift != 0 {
|
} else if mod&fyne.KeyModifierShift != 0 {
|
||||||
t.selectionMgr.SelectRange(idx)
|
t.selectRange(idx)
|
||||||
} else {
|
} else {
|
||||||
t.selectionMgr.Select(idx)
|
t.selectTrack(idx)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.selectionMgr.Select(idx)
|
t.selectTrack(idx)
|
||||||
}
|
}
|
||||||
t.list.Refresh()
|
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) {
|
func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
|
||||||
t.selectionMgr.Select(trackIdx)
|
t.selectTrack(trackIdx)
|
||||||
t.list.Refresh()
|
t.list.Refresh()
|
||||||
if t.ctxMenu == nil {
|
if t.ctxMenu == nil {
|
||||||
t.ctxMenu = fyne.NewMenu("")
|
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) {
|
func (t *Tracklist) onSetFavorite(trackID string, fav bool) {
|
||||||
t.tracksMutex.RLock()
|
t.tracksMutex.RLock()
|
||||||
tr := sharedutil.FindTrackByID(trackID, t.tracks)
|
tr := t.findTrackByID(trackID)
|
||||||
t.tracksMutex.RUnlock()
|
t.tracksMutex.RUnlock()
|
||||||
t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false)
|
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) {
|
func (t *Tracklist) onSetRating(trackID string, rating int) {
|
||||||
// update our own track model
|
// update our own track model
|
||||||
t.tracksMutex.RLock()
|
t.tracksMutex.RLock()
|
||||||
tr := sharedutil.FindTrackByID(trackID, t.tracks)
|
tr := t.findTrackByID(trackID)
|
||||||
t.tracksMutex.RUnlock()
|
t.tracksMutex.RUnlock()
|
||||||
t.onSetRatings([]*mediaprovider.Track{tr}, rating, false)
|
t.onSetRatings([]*mediaprovider.Track{tr}, rating, false)
|
||||||
}
|
}
|
||||||
@@ -510,30 +582,34 @@ func (t *Tracklist) onAlbumTapped(albumID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
|
func (t *Tracklist) findTrackByID(id string) *mediaprovider.Track {
|
||||||
sel := t.selectionMgr.GetSelection()
|
idx := sharedutil.Find(t.tracks, func(tr *trackModel) bool {
|
||||||
tracks := make([]*mediaprovider.Track, 0, len(sel))
|
return tr.track.ID == id
|
||||||
|
})
|
||||||
|
if idx >= 0 {
|
||||||
|
return t.tracks[idx].track
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracklist) selectedTrackModels() []*trackModel {
|
||||||
t.tracksMutex.RLock()
|
t.tracksMutex.RLock()
|
||||||
defer t.tracksMutex.RUnlock()
|
defer t.tracksMutex.RUnlock()
|
||||||
for _, idx := range sel {
|
return sharedutil.FilterSlice(t.tracks, func(tm *trackModel) bool {
|
||||||
tracks = append(tracks, t.tracks[idx])
|
return tm.selected
|
||||||
}
|
})
|
||||||
return tracks
|
}
|
||||||
|
|
||||||
|
func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
|
||||||
|
return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) *mediaprovider.Track {
|
||||||
|
return tm.track
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracklist) SelectedTrackIDs() []string {
|
func (t *Tracklist) SelectedTrackIDs() []string {
|
||||||
sel := t.selectionMgr.GetSelection()
|
return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) string {
|
||||||
tracks := make([]string, 0, len(sel))
|
return tm.track.ID
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracklist) lenTracks() int {
|
func (t *Tracklist) lenTracks() int {
|
||||||
@@ -629,9 +705,12 @@ func newTrailingAlignRichText() *widget.RichText {
|
|||||||
return rt
|
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
|
// Update info that can change if this row is bound to
|
||||||
// a new track (*mediaprovider.Track)
|
// a new track (*mediaprovider.Track)
|
||||||
|
tr := tm.track
|
||||||
if tr.ID != t.trackID {
|
if tr.ID != t.trackID {
|
||||||
if t.Focused {
|
if t.Focused {
|
||||||
fyne.CurrentApp().Driver().CanvasForObject(t).Focus(nil)
|
fyne.CurrentApp().Driver().CanvasForObject(t).Focus(nil)
|
||||||
@@ -746,3 +825,17 @@ func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) {
|
|||||||
t.OnTappedSecondary(e, t.trackIdx)
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user