Merge pull request #174 from dweymouth/feature/sort-tracklist-cols
Add sorting by columns to tracklist view
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -26,6 +26,7 @@ type AlbumPage struct {
|
||||
albumPageState
|
||||
|
||||
header *AlbumPageHeader
|
||||
tracks []*mediaprovider.Track
|
||||
tracklist *widgets.Tracklist
|
||||
nowPlayingID string
|
||||
container *fyne.Container
|
||||
@@ -33,6 +34,7 @@ type AlbumPage struct {
|
||||
|
||||
type albumPageState struct {
|
||||
albumID string
|
||||
sort widgets.TracklistSort
|
||||
cfg *backend.AlbumPageConfig
|
||||
mp mediaprovider.MediaProvider
|
||||
pm *backend.PlaybackManager
|
||||
@@ -47,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{
|
||||
@@ -62,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
|
||||
}
|
||||
@@ -81,6 +96,7 @@ func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer {
|
||||
|
||||
func (a *AlbumPage) Save() SavedPage {
|
||||
s := a.albumPageState
|
||||
s.sort = a.tracklist.Sorting()
|
||||
return &s
|
||||
}
|
||||
|
||||
@@ -119,7 +135,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 +186,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 +195,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))
|
||||
}
|
||||
@@ -260,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)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ var _ fyne.Widget = (*ArtistPage)(nil)
|
||||
type artistPageState struct {
|
||||
artistID string
|
||||
activeView int
|
||||
trackSort widgets.TracklistSort
|
||||
|
||||
cfg *backend.ArtistPageConfig
|
||||
pm *backend.PlaybackManager
|
||||
@@ -53,10 +54,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,
|
||||
@@ -65,6 +66,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)
|
||||
@@ -111,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
|
||||
}
|
||||
|
||||
@@ -187,6 +192,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
|
||||
}
|
||||
@@ -222,7 +228,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 {
|
||||
|
||||
@@ -28,6 +28,7 @@ type FavoritesPage struct {
|
||||
im *backend.ImageManager
|
||||
mp mediaprovider.MediaProvider
|
||||
|
||||
trackSort widgets.TracklistSort
|
||||
filter mediaprovider.AlbumFilter
|
||||
searchText string
|
||||
nowPlayingID string
|
||||
@@ -103,6 +104,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)
|
||||
@@ -155,7 +157,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()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -323,6 +328,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
|
||||
}
|
||||
@@ -357,6 +363,7 @@ type savedFavoritesPage struct {
|
||||
filter mediaprovider.AlbumFilter
|
||||
searchText string
|
||||
activeToggleBtn int
|
||||
trackSort widgets.TracklistSort
|
||||
}
|
||||
|
||||
func (s *savedFavoritesPage) Restore() Page {
|
||||
|
||||
@@ -178,7 +178,8 @@ 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.hdr.DisableSorting = true
|
||||
a.list = widget.NewList(
|
||||
func() int { return len(a.Items) },
|
||||
func() fyne.CanvasObject {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+36
-11
@@ -26,6 +26,7 @@ type PlaylistPage struct {
|
||||
|
||||
header *PlaylistPageHeader
|
||||
tracklist *widgets.Tracklist
|
||||
tracks []*mediaprovider.Track
|
||||
nowPlayingID string
|
||||
container *fyne.Container
|
||||
}
|
||||
@@ -37,6 +38,7 @@ type playlistPageState struct {
|
||||
sm *backend.ServerManager
|
||||
pm *backend.PlaybackManager
|
||||
im *backend.ImageManager
|
||||
trackSort widgets.TracklistSort
|
||||
}
|
||||
|
||||
func NewPlaylistPage(
|
||||
@@ -46,12 +48,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
|
||||
}
|
||||
@@ -81,6 +96,7 @@ func (a *PlaylistPage) CreateRenderer() fyne.WidgetRenderer {
|
||||
|
||||
func (a *PlaylistPage) Save() SavedPage {
|
||||
p := a.playlistPageState
|
||||
p.trackSort = a.tracklist.Sorting()
|
||||
return &p
|
||||
}
|
||||
|
||||
@@ -117,7 +133,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)
|
||||
@@ -140,16 +157,24 @@ func (a *PlaylistPage) onMoveSelectedToBottom() {
|
||||
}
|
||||
|
||||
func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) {
|
||||
idxs := a.tracklist.SelectedTrackIndexes()
|
||||
newTracks := sharedutil.ReorderTracks(a.tracklist.Tracks, 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 {
|
||||
a.tracklist.Tracks = newTracks
|
||||
// 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()
|
||||
}
|
||||
@@ -199,12 +224,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
|
||||
@@ -217,7 +242,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))
|
||||
}
|
||||
@@ -280,5 +305,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)
|
||||
}
|
||||
|
||||
@@ -287,10 +287,11 @@ 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
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
+140
-10
@@ -5,25 +5,44 @@ 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/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type SortType int
|
||||
|
||||
const (
|
||||
SortNone SortType = iota
|
||||
SortAscending
|
||||
SortDescending
|
||||
)
|
||||
|
||||
type ListHeaderSort struct {
|
||||
ColNumber int
|
||||
Type SortType
|
||||
}
|
||||
|
||||
type ListColumn struct {
|
||||
Text string
|
||||
AlignTrailing bool
|
||||
Alignment fyne.TextAlign
|
||||
CanToggleVisible bool
|
||||
}
|
||||
|
||||
type ListHeader struct {
|
||||
widget.BaseWidget
|
||||
|
||||
DisableSorting bool
|
||||
|
||||
OnColumnSortChanged func(ListHeaderSort)
|
||||
OnColumnVisibilityChanged func(int, bool)
|
||||
OnColumnVisibilityMenuShown func(*widget.PopUp)
|
||||
|
||||
sort ListHeaderSort
|
||||
columns []ListColumn
|
||||
columnVisible []bool
|
||||
columnsLayout *layouts.ColumnsLayout
|
||||
@@ -40,7 +59,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)
|
||||
@@ -64,15 +83,33 @@ 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
|
||||
for i, c := range l.columns {
|
||||
hdr := newColHeader(c, &l.DisableSorting)
|
||||
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(hdr)
|
||||
}
|
||||
t.Segments[0].(*widget.TextSegment).Style.Alignment = al
|
||||
l.columnsContainer.Add(t)
|
||||
}
|
||||
|
||||
// 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 == sort.ColNumber {
|
||||
c.(*colHeader).Sort = sort.Type
|
||||
} else {
|
||||
c.(*colHeader).Sort = SortNone
|
||||
}
|
||||
}
|
||||
l.Refresh()
|
||||
if l.OnColumnSortChanged != nil {
|
||||
l.OnColumnSortChanged(sort)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,3 +156,96 @@ func (l *ListHeader) createOnChangedCallbk(colNum int) func(bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type colHeader struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Sort SortType
|
||||
OnSortChanged func(SortType)
|
||||
OnTappedSecondary func(*fyne.PointEvent)
|
||||
|
||||
sortDisabled *bool
|
||||
columnCfg ListColumn
|
||||
|
||||
label *widget.RichText
|
||||
sortIcon *widget.Icon
|
||||
sortIconNegSpacer fyne.CanvasObject
|
||||
container *fyne.Container
|
||||
}
|
||||
|
||||
func newColHeader(columnCfg ListColumn, sortDisabled *bool) *colHeader {
|
||||
c := &colHeader{columnCfg: columnCfg, sortDisabled: sortDisabled}
|
||||
c.ExtendBaseWidget(c)
|
||||
|
||||
c.label = widget.NewRichTextWithText(columnCfg.Text)
|
||||
c.label.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
||||
TextStyle: fyne.TextStyle{Bold: true},
|
||||
Alignment: columnCfg.Alignment,
|
||||
}
|
||||
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) Tapped(*fyne.PointEvent) {
|
||||
if *c.sortDisabled {
|
||||
return
|
||||
}
|
||||
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.Sort == SortDescending {
|
||||
c.sortIcon.Resource = theme.MenuDropDownIcon()
|
||||
} else {
|
||||
c.sortIcon.Resource = theme.MenuDropUpIcon()
|
||||
}
|
||||
|
||||
if c.Sort > 0 && c.sortIcon.Hidden {
|
||||
c.sortIcon.Show()
|
||||
c.container.Add(c.sortIconNegSpacer)
|
||||
} else if (c.Sort == SortNone || *c.sortDisabled) && !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.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)
|
||||
}
|
||||
|
||||
+169
-82
@@ -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,19 @@ const (
|
||||
ColumnPath = "Path"
|
||||
)
|
||||
|
||||
var columns = []string{
|
||||
ColumnNum, ColumnTitle, ColumnArtist, ColumnAlbum, ColumnTime, ColumnYear,
|
||||
ColumnFavorite, ColumnRating, ColumnPlays, ColumnBitrate, ColumnSize, ColumnPath,
|
||||
}
|
||||
|
||||
type TracklistSort struct {
|
||||
SortOrder SortType
|
||||
ColumnName string
|
||||
}
|
||||
|
||||
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
|
||||
@@ -57,6 +66,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)
|
||||
@@ -73,8 +85,12 @@ type Tracklist struct {
|
||||
OnTrackShown func(tracknum int)
|
||||
|
||||
visibleColumns []bool
|
||||
sorting TracklistSort
|
||||
|
||||
tracksMutex sync.RWMutex
|
||||
tracks []*mediaprovider.Track
|
||||
tracksOrigOrder []*mediaprovider.Track
|
||||
|
||||
selectionMgr util.ListSelectionManager
|
||||
nowPlayingID string
|
||||
colLayout *layouts.ColumnsLayout
|
||||
@@ -85,13 +101,14 @@ 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)
|
||||
// #, 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.OnColumnSortChanged = t.onSorted
|
||||
t.hdr.OnColumnVisibilityChanged = t.setColumnVisible
|
||||
t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) {
|
||||
if t.OnColumnVisibilityMenuShown != nil {
|
||||
@@ -127,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)
|
||||
}
|
||||
|
||||
@@ -146,10 +163,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 +208,22 @@ func (t *Tracklist) setColumnVisible(colNum int, vis bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracklist) Sorting() TracklistSort {
|
||||
return t.sorting
|
||||
}
|
||||
|
||||
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
|
||||
t.hdr.SetSorting(ListHeaderSort{ColNumber: ColNumber(sorting.ColumnName), Type: sorting.SortOrder})
|
||||
}
|
||||
|
||||
func (t *Tracklist) SetNowPlaying(trackID string) {
|
||||
t.nowPlayingID = trackID
|
||||
t.list.Refresh()
|
||||
@@ -198,7 +231,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
|
||||
@@ -206,19 +239,34 @@ 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()
|
||||
defer t.tracksMutex.Unlock()
|
||||
t.Tracks = nil
|
||||
t.tracks = nil
|
||||
}
|
||||
|
||||
// Append more tracks to 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.tracksOrigOrder = trs
|
||||
t.doSortTracks()
|
||||
}
|
||||
|
||||
// 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. 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.tracks = append(t.tracks, trs...)
|
||||
}
|
||||
|
||||
func (t *Tracklist) SelectAll() {
|
||||
@@ -234,7 +282,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
|
||||
@@ -251,6 +299,86 @@ 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) 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)
|
||||
@@ -301,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())
|
||||
@@ -327,7 +455,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 +476,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,18 +512,18 @@ 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
|
||||
}
|
||||
|
||||
func (t *Tracklist) selectedTrackIDs() []string {
|
||||
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)
|
||||
tracks = append(tracks, t.tracks[idx].ID)
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
@@ -407,64 +535,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 {
|
||||
|
||||
Reference in New Issue
Block a user