673 lines
19 KiB
Go
673 lines
19 KiB
Go
package widgets
|
|
|
|
import (
|
|
"log"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/dweymouth/supersonic/backend"
|
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
"github.com/dweymouth/supersonic/sharedutil"
|
|
"github.com/dweymouth/supersonic/ui/layouts"
|
|
"github.com/dweymouth/supersonic/ui/util"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/driver/desktop"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
type TracklistSort struct {
|
|
SortOrder SortType
|
|
ColumnName string
|
|
}
|
|
|
|
type TracklistOptions struct {
|
|
// 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
|
|
|
|
// Reorderable sets whether the tracklist supports drag-and-drop reordering.
|
|
Reorderable bool
|
|
|
|
// ShowDiscNumber sets whether to display the disc number as part of the '#' column,
|
|
// (with format %d.%02d). Only applies if AutoNumber==false.
|
|
ShowDiscNumber bool
|
|
|
|
// AuxiliaryMenuItems sets additional menu items appended to the context menu
|
|
// must be set before the context menu is shown for the first time
|
|
AuxiliaryMenuItems []*fyne.MenuItem
|
|
|
|
// DisablePlaybackMenu sets whether to disable playback options in
|
|
// the tracklist context menu.
|
|
DisablePlaybackMenu bool
|
|
|
|
// Disables sorting the tracklist by clicking individual columns.
|
|
DisableSorting bool
|
|
|
|
// Disables the five star rating widget.
|
|
DisableRating bool
|
|
|
|
// Disables the sharing option.
|
|
DisableSharing bool
|
|
}
|
|
|
|
type Tracklist struct {
|
|
widget.BaseWidget
|
|
|
|
compactRows bool
|
|
columns []TracklistColumn
|
|
|
|
Options TracklistOptions
|
|
|
|
// user action callbacks
|
|
OnPlayTrackAt func(int)
|
|
OnPlaySelection func(tracks []*mediaprovider.Track, shuffle bool)
|
|
OnPlaySelectionNext func(trackIDs []*mediaprovider.Track)
|
|
OnAddToQueue func(trackIDs []*mediaprovider.Track)
|
|
OnAddToPlaylist func(trackIDs []string)
|
|
OnSetFavorite func(trackIDs []string, fav bool)
|
|
OnSetRating func(trackIDs []string, rating int)
|
|
OnDownload func(tracks []*mediaprovider.Track, downloadName string)
|
|
OnShare func(trackID string)
|
|
OnPlaySongRadio func(track *mediaprovider.Track)
|
|
OnReorderTracks func(trackIDs []string, insertPos int)
|
|
OnShowTrackInfo func(track *mediaprovider.Track)
|
|
|
|
OnShowArtistPage func(artistID string)
|
|
OnShowAlbumPage func(albumID string)
|
|
OnShowGenrePage func(genre string)
|
|
|
|
OnColumnVisibilityMenuShown func(*widget.PopUp)
|
|
OnVisibleColumnsChanged func([]string)
|
|
OnTrackShown func(tracknum int)
|
|
|
|
visibleColumns []bool
|
|
sorting TracklistSort
|
|
|
|
tracks []*util.TrackListModel
|
|
tracksOrigOrder []*util.TrackListModel
|
|
|
|
nowPlayingID string
|
|
colLayout *layouts.ColumnsLayout
|
|
hdr *ListHeader
|
|
list *FocusList
|
|
ctxMenu *util.TrackContextMenu
|
|
loadingDots *LoadingDots
|
|
container *fyne.Container
|
|
}
|
|
|
|
func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCompactRows bool) *Tracklist {
|
|
initTracklistColumns()
|
|
playIcon := theme.NewThemedResource(theme.MediaPlayIcon())
|
|
playIcon.ColorName = theme.ColorNamePrimary
|
|
|
|
t := &Tracklist{compactRows: useCompactRows}
|
|
t.ExtendBaseWidget(t)
|
|
t.columns = ExpandedTracklistRowColumns
|
|
colWidths := ExpandedTracklistRowColumnWidths
|
|
var playingIcon fyne.CanvasObject
|
|
if useCompactRows {
|
|
t.columns = CompactTracklistRowColumns
|
|
colWidths = CompactTracklistRowColumnWidths
|
|
playIconImg := canvas.NewImageFromResource(playIcon)
|
|
playIconImg.FillMode = canvas.ImageFillContain
|
|
playIconImg.SetMinSize(fyne.NewSquareSize(theme.IconInlineSize() * 1.2))
|
|
playingIcon = container.NewCenter(container.NewHBox(util.NewHSpace(4), playIconImg))
|
|
} else {
|
|
playIconImg := canvas.NewImageFromResource(playIcon)
|
|
playIconImg.FillMode = canvas.ImageFillContain
|
|
playIconImg.SetMinSize(fyne.NewSquareSize(theme.IconInlineSize() * 1.5))
|
|
playingIcon = container.NewCenter(playIconImg)
|
|
}
|
|
t.visibleColumns = make([]bool, len(t.columns))
|
|
|
|
if len(tracks) > 0 {
|
|
t._setTracks(tracks)
|
|
}
|
|
|
|
t.colLayout = layouts.NewColumnsLayout(colWidths)
|
|
t.hdr = NewListHeader(sharedutil.MapSlice(t.columns,
|
|
func(t TracklistColumn) ListColumn { return t.Col }), t.colLayout)
|
|
t.hdr.OnColumnSortChanged = t.onSorted
|
|
t.hdr.OnColumnVisibilityChanged = t.setColumnVisible
|
|
t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) {
|
|
if t.OnColumnVisibilityMenuShown != nil {
|
|
t.OnColumnVisibilityMenuShown(pop)
|
|
}
|
|
}
|
|
|
|
t.list = NewFocusList(
|
|
func() int { return len(t.tracks) },
|
|
func() fyne.CanvasObject {
|
|
var tr TracklistRow
|
|
if t.compactRows {
|
|
tr = NewCompactTracklistRow(t, playingIcon)
|
|
} else {
|
|
tr = NewExpandedTracklistRow(t, im, playingIcon)
|
|
}
|
|
tr.SetOnTapped(func() {
|
|
t.onSelectTrack(tr.ItemID())
|
|
})
|
|
tr.SetOnTappedSecondary(t.onShowContextMenu)
|
|
tr.SetOnDoubleTapped(func() {
|
|
t.onPlayTrackAt(tr.ItemID())
|
|
})
|
|
tr.SetOnFocusNeighbor(func(up bool) {
|
|
t.list.FocusNeighbor(tr.ItemID(), up)
|
|
})
|
|
return tr
|
|
},
|
|
func(itemID widget.ListItemID, item fyne.CanvasObject) {
|
|
// we could have removed tracks from the list in between
|
|
// Fyne calling the length callback and this update callback
|
|
// so the itemID may be out of bounds. if so, do nothing.
|
|
if itemID >= len(t.tracks) {
|
|
return
|
|
}
|
|
model := t.tracks[itemID]
|
|
|
|
tr := item.(TracklistRow)
|
|
if tr.TrackID() != model.Item.Metadata().ID || tr.ItemID() != itemID {
|
|
tr.SetItemID(itemID)
|
|
}
|
|
i := -1 // signal that we want to display the actual track num.
|
|
if t.Options.AutoNumber {
|
|
i = itemID + 1
|
|
}
|
|
tr.Update(model, i, func() {})
|
|
if t.OnTrackShown != nil {
|
|
t.OnTrackShown(itemID)
|
|
}
|
|
})
|
|
t.list.OnDragBegin = func(id int) {
|
|
if !t.tracks[id].Selected {
|
|
t.selectTrack(id)
|
|
t.list.Refresh()
|
|
}
|
|
}
|
|
t.list.OnDragEnd = func(dragged, insertPos int) {
|
|
if t.OnReorderTracks != nil {
|
|
t.OnReorderTracks(t.SelectedTrackIDs(), insertPos)
|
|
}
|
|
}
|
|
t.loadingDots = NewLoadingDots()
|
|
t.container = container.NewBorder(t.hdr, nil, nil, nil,
|
|
container.NewStack(t.list, container.NewCenter(t.loadingDots)))
|
|
return t
|
|
}
|
|
|
|
func (t *Tracklist) Reset() {
|
|
t.SetLoading(false)
|
|
t.Clear()
|
|
t.Options = TracklistOptions{}
|
|
t.ctxMenu = nil
|
|
t.SetSorting(TracklistSort{})
|
|
}
|
|
|
|
func (t *Tracklist) SetLoading(loading bool) {
|
|
if loading {
|
|
t.loadingDots.Start()
|
|
} else {
|
|
t.loadingDots.Stop()
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) ScrollBy(amount float32) {
|
|
t.list.ScrollToOffset(t.list.GetScrollOffset() + amount)
|
|
}
|
|
|
|
func (t *Tracklist) GetScrollOffset() float32 {
|
|
return t.list.GetScrollOffset()
|
|
}
|
|
|
|
func (t *Tracklist) ScrollToOffset(offset float32) {
|
|
t.list.ScrollToOffset(offset)
|
|
}
|
|
|
|
// Gets the track at the given index.
|
|
func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track {
|
|
if idx >= len(t.tracks) {
|
|
log.Println("error: Tracklist.TrackAt: index out of range")
|
|
return nil
|
|
}
|
|
return t.tracks[idx].Track()
|
|
}
|
|
|
|
func (t *Tracklist) SetVisibleColumns(cols []string) {
|
|
t.visibleColumns[0] = true
|
|
t.visibleColumns[1] = true
|
|
|
|
for i := 2; i < len(t.columns); i++ {
|
|
t.visibleColumns[i] = false
|
|
t.hdr.SetColumnVisible(i, false)
|
|
}
|
|
for _, col := range cols {
|
|
if num := t.ColNumber(col); num < 0 {
|
|
log.Printf("Unknown tracklist column %q", col)
|
|
} else {
|
|
t.visibleColumns[num] = true
|
|
t.hdr.SetColumnVisible(num, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) VisibleColumns() []string {
|
|
var cols []string
|
|
for i := 2; i < len(t.visibleColumns); i++ {
|
|
if t.visibleColumns[i] {
|
|
cols = append(cols, string(t.colName(i)))
|
|
}
|
|
}
|
|
return cols
|
|
}
|
|
|
|
func (t *Tracklist) setColumnVisible(colNum int, vis bool) {
|
|
if colNum >= len(t.visibleColumns) {
|
|
log.Printf("error: Tracklist.SetColumnVisible: column index %d out of range", colNum)
|
|
return
|
|
}
|
|
t.visibleColumns[colNum] = vis
|
|
t.list.Refresh()
|
|
if t.OnVisibleColumnsChanged != nil {
|
|
t.OnVisibleColumnsChanged(t.VisibleColumns())
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) Sorting() TracklistSort {
|
|
return t.sorting
|
|
}
|
|
|
|
func (t *Tracklist) SetSorting(sorting TracklistSort) {
|
|
if sorting.ColumnName == "" {
|
|
// nil case - reset current sort
|
|
if slices.ContainsFunc(t.columns, func(c TracklistColumn) bool {
|
|
return c.Name == t.sorting.ColumnName
|
|
}) {
|
|
t.hdr.SetSorting(ListHeaderSort{ColNumber: t.ColNumber(t.sorting.ColumnName), Type: SortNone})
|
|
}
|
|
return
|
|
}
|
|
// actual sorting will be handled in callback from header
|
|
t.hdr.SetSorting(ListHeaderSort{ColNumber: t.ColNumber(sorting.ColumnName), Type: sorting.SortOrder})
|
|
}
|
|
|
|
// Sets the currently playing track ID and updates the list rendering
|
|
func (t *Tracklist) SetNowPlaying(trackID string) {
|
|
prevNowPlaying := t.nowPlayingID
|
|
trPrev, idxPrev := util.FindItemByID(t.tracks, prevNowPlaying)
|
|
tr, idx := util.FindItemByID(t.tracks, trackID)
|
|
t.nowPlayingID = trackID
|
|
if trPrev != nil {
|
|
t.list.RefreshItem(idxPrev)
|
|
}
|
|
if tr != nil {
|
|
t.list.RefreshItem(idx)
|
|
}
|
|
}
|
|
|
|
// Increments the play count of the given track and updates the list rendering
|
|
func (t *Tracklist) IncrementPlayCount(trackID string) {
|
|
tr, idx := util.FindItemByID(t.tracks, trackID)
|
|
if tr != nil {
|
|
tr.(*mediaprovider.Track).PlayCount += 1
|
|
t.list.RefreshItem(idx)
|
|
}
|
|
}
|
|
|
|
// Remove all tracks from the tracklist. Does not issue Refresh call.
|
|
func (t *Tracklist) Clear() {
|
|
t.tracks = nil
|
|
t.tracksOrigOrder = nil
|
|
}
|
|
|
|
// Sets the tracks in the tracklist.
|
|
func (t *Tracklist) SetTracks(trs []*mediaprovider.Track) {
|
|
t._setTracks(trs)
|
|
t.Refresh()
|
|
}
|
|
|
|
func (t *Tracklist) _setTracks(trs []*mediaprovider.Track) {
|
|
t.tracksOrigOrder = util.ToTrackListModels(trs)
|
|
t.doSortTracks()
|
|
}
|
|
|
|
// Returns the tracks in the tracklist in the current display order.
|
|
func (t *Tracklist) GetTracks() []*mediaprovider.Track {
|
|
return sharedutil.MapSlice(t.tracks, func(tm *util.TrackListModel) *mediaprovider.Track {
|
|
return tm.Track()
|
|
})
|
|
}
|
|
|
|
// Append more tracks to the tracklist.
|
|
func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) {
|
|
t.tracksOrigOrder = append(t.tracksOrigOrder, util.ToTrackListModels(trs)...)
|
|
t.doSortTracks()
|
|
t.list.Refresh()
|
|
}
|
|
|
|
func (t *Tracklist) SelectAll() {
|
|
util.SelectAllItems(t.tracks)
|
|
t.list.Refresh()
|
|
}
|
|
|
|
func (t *Tracklist) UnselectAll() {
|
|
t.unselectAll()
|
|
t.list.Refresh()
|
|
}
|
|
|
|
func (t *Tracklist) unselectAll() {
|
|
util.UnselectAllItems(t.tracks)
|
|
}
|
|
|
|
func (t *Tracklist) SelectAndScrollToTrack(trackID string) {
|
|
idx := -1
|
|
for i, tr := range t.tracks {
|
|
if tr.Item.Metadata().ID == trackID {
|
|
idx = i
|
|
tr.Selected = true
|
|
} else {
|
|
tr.Selected = false
|
|
}
|
|
}
|
|
if idx >= 0 {
|
|
t.list.ScrollTo(idx)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) CreateRenderer() fyne.WidgetRenderer {
|
|
return widget.NewSimpleRenderer(t.container)
|
|
}
|
|
|
|
func (t *Tracklist) Refresh() {
|
|
t.list.EnableDragging = t.Options.Reorderable
|
|
t.hdr.DisableSorting = t.Options.DisableSorting
|
|
t.BaseWidget.Refresh()
|
|
}
|
|
|
|
// 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(*util.TrackListModel) string) {
|
|
new := make([]*util.TrackListModel, 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(*util.TrackListModel) int64) {
|
|
new := make([]*util.TrackListModel, 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, ColumnTitleArtist:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().Title })
|
|
case ColumnArtist:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return strings.Join(tr.Track().ArtistNames, ", ") })
|
|
case ColumnAlbumArtist:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return strings.Join(tr.Track().AlbumArtistNames, ", ") })
|
|
case ColumnAlbum:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().Album })
|
|
case ColumnGenre:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return strings.Join(tr.Track().Genres, ", ") })
|
|
case ColumnPath:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().FilePath })
|
|
case ColumnRating:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track().Rating) })
|
|
case ColumnTime:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().Duration.Milliseconds() })
|
|
case ColumnYear:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track().Year) })
|
|
case ColumnSize:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().Size })
|
|
case ColumnPlays:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track().PlayCount) })
|
|
case ColumnLastPlayed:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().LastPlayed.Unix() })
|
|
case ColumnComment:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().Comment })
|
|
case ColumnFileType:
|
|
t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().Extension })
|
|
case ColumnBPM:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track().BPM) })
|
|
case ColumnBitrate:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track().BitRate) })
|
|
case ColumnFavorite:
|
|
t.intSort(func(tr *util.TrackListModel) int64 {
|
|
if tr.Track().Favorite {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
case ColumnDateAdded:
|
|
t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().DateAdded.Unix() })
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onSorted(sort ListHeaderSort) {
|
|
t.sorting = TracklistSort{ColumnName: t.colName(sort.ColNumber), SortOrder: sort.Type}
|
|
t.doSortTracks()
|
|
t.Refresh()
|
|
}
|
|
|
|
func (t *Tracklist) onPlayTrackAt(idx int) {
|
|
if t.OnPlayTrackAt != nil {
|
|
t.OnPlayTrackAt(idx)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onSelectTrack(idx int) {
|
|
if d, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok {
|
|
mod := d.CurrentKeyModifiers()
|
|
if mod&fyne.KeyModifierShortcutDefault != 0 {
|
|
t.selectAddOrRemove(idx)
|
|
} else if mod&fyne.KeyModifierShift != 0 {
|
|
t.selectRange(idx)
|
|
} else {
|
|
t.selectTrack(idx)
|
|
}
|
|
} else {
|
|
t.selectTrack(idx)
|
|
}
|
|
t.list.Refresh()
|
|
}
|
|
|
|
func (t *Tracklist) selectAddOrRemove(idx int) {
|
|
t.tracks[idx].Selected = !t.tracks[idx].Selected
|
|
}
|
|
|
|
func (t *Tracklist) selectTrack(idx int) bool {
|
|
return util.SelectItem(t.tracks, idx)
|
|
}
|
|
|
|
func (t *Tracklist) selectRange(idx int) {
|
|
util.SelectItemRange(t.tracks, idx)
|
|
}
|
|
|
|
func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
|
|
if t.selectTrack(trackIdx) {
|
|
t.list.Refresh()
|
|
}
|
|
if t.ctxMenu == nil {
|
|
t.ctxMenu = util.NewTrackContextMenu(t.Options.DisablePlaybackMenu, t.Options.AuxiliaryMenuItems)
|
|
t.ctxMenu.OnPlay = func(shuffle bool) {
|
|
t.OnPlaySelection(t.SelectedTracks(), shuffle)
|
|
}
|
|
t.ctxMenu.OnAddToQueue = func(next bool) {
|
|
if next {
|
|
t.OnPlaySelectionNext(t.SelectedTracks())
|
|
} else {
|
|
t.OnAddToQueue(t.SelectedTracks())
|
|
}
|
|
}
|
|
t.ctxMenu.OnPlaySongRadio = func() {
|
|
t.onPlaySongRadio(t.SelectedTracks())
|
|
}
|
|
t.ctxMenu.OnAddToPlaylist = func() {
|
|
t.OnAddToPlaylist(t.SelectedTrackIDs())
|
|
}
|
|
t.ctxMenu.OnDownload = func() {
|
|
t.onDownload(t.SelectedTracks(), "Selected tracks")
|
|
}
|
|
t.ctxMenu.OnShowInfo = func() {
|
|
t.OnShowTrackInfo(t.SelectedTracks()[0])
|
|
}
|
|
t.ctxMenu.OnFavorite = func(fav bool) {
|
|
t.onSetFavorites(t.SelectedTracks(), fav, true /*needRefresh*/)
|
|
}
|
|
t.ctxMenu.OnShare = func() {
|
|
t.onShare(t.SelectedTracks())
|
|
}
|
|
t.ctxMenu.OnSetRating = func(rating int) {
|
|
t.onSetRatings(t.SelectedTracks(), rating, true /*needRefresh*/)
|
|
}
|
|
}
|
|
t.ctxMenu.SetRatingDisabled(t.Options.DisableRating)
|
|
t.ctxMenu.SetShareDisabled(t.Options.DisableSharing || len(t.SelectedTracks()) != 1)
|
|
t.ctxMenu.SetInfoDisabled(len(t.SelectedTracks()) != 1)
|
|
t.ctxMenu.ShowAtPosition(e.AbsolutePosition, fyne.CurrentApp().Driver().CanvasForObject(t))
|
|
}
|
|
|
|
func (t *Tracklist) onSetFavorite(trackID string, fav bool) {
|
|
item, _ := util.FindItemByID(t.tracks, trackID)
|
|
t.onSetFavorites([]*mediaprovider.Track{item.(*mediaprovider.Track)}, fav, false)
|
|
}
|
|
|
|
func (t *Tracklist) onSetFavorites(tracks []*mediaprovider.Track, fav bool, needRefresh bool) {
|
|
for _, tr := range tracks {
|
|
tr.Favorite = fav
|
|
}
|
|
if needRefresh {
|
|
t.Refresh()
|
|
}
|
|
// notify listener
|
|
if t.OnSetFavorite != nil {
|
|
t.OnSetFavorite(sharedutil.TracksToIDs(tracks), fav)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onSetRating(trackID string, rating int) {
|
|
// update our own track model
|
|
item, _ := util.FindItemByID(t.tracks, trackID)
|
|
t.onSetRatings([]*mediaprovider.Track{item.(*mediaprovider.Track)}, rating, false)
|
|
}
|
|
|
|
func (t *Tracklist) onSetRatings(tracks []*mediaprovider.Track, rating int, needRefresh bool) {
|
|
for _, tr := range tracks {
|
|
tr.Rating = rating
|
|
}
|
|
if needRefresh {
|
|
t.Refresh()
|
|
}
|
|
// notify listener
|
|
if t.OnSetRating != nil {
|
|
t.OnSetRating(sharedutil.TracksToIDs(tracks), rating)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onArtistTapped(artistID string) {
|
|
if t.OnShowArtistPage != nil {
|
|
t.OnShowArtistPage(artistID)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onAlbumTapped(albumID string) {
|
|
if t.OnShowAlbumPage != nil {
|
|
t.OnShowAlbumPage(albumID)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onGenreTapped(genre string) {
|
|
if t.OnShowGenrePage != nil {
|
|
t.OnShowGenrePage(genre)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onDownload(tracks []*mediaprovider.Track, downloadName string) {
|
|
if t.OnDownload != nil {
|
|
t.OnDownload(tracks, downloadName)
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onShare(tracks []*mediaprovider.Track) {
|
|
if t.OnShare != nil {
|
|
if len(tracks) > 0 {
|
|
t.OnShare(tracks[0].ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) onPlaySongRadio(tracks []*mediaprovider.Track) {
|
|
if t.OnPlaySongRadio != nil {
|
|
if len(tracks) > 0 {
|
|
t.OnPlaySongRadio(tracks[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Tracklist) SelectedTracks() []*mediaprovider.Track {
|
|
return util.SelectedTracks(t.tracks)
|
|
}
|
|
|
|
func (t *Tracklist) SelectedTrackIDs() []string {
|
|
return util.SelectedItemIDs(t.tracks)
|
|
}
|
|
|
|
// SelectedTrackIndexes returns the indexes of the selected tracks in the
|
|
// original sort order (ie if tracklist is sorted by some column), the indexes
|
|
// returned will correspond to the order of tracks when the list was initialized.
|
|
func (t *Tracklist) SelectedTrackIndexes() []int {
|
|
idx := -1
|
|
return sharedutil.FilterMapSlice(t.tracksOrigOrder, func(t *util.TrackListModel) (int, bool) {
|
|
idx++
|
|
return idx, t.Selected
|
|
})
|
|
}
|
|
|
|
func (t *Tracklist) ColNumber(colName string) int {
|
|
i := slices.IndexFunc(t.columns, func(c TracklistColumn) bool {
|
|
return c.Name == colName
|
|
})
|
|
if i < 0 {
|
|
log.Printf("error: Tracklist: invalid column name %s", colName)
|
|
}
|
|
return i
|
|
}
|
|
|
|
func (t *Tracklist) colName(i int) string {
|
|
if i < len(t.columns) {
|
|
return t.columns[i].Name
|
|
}
|
|
log.Println("notReached: Tracklist.colName")
|
|
return ""
|
|
}
|