From c3fcf5955ef0433c0eab5208f3898bc5e015d437 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 12 Feb 2023 10:29:52 -0800 Subject: [PATCH] add add'l tracklist columns, begin work toward config. visibility --- ui/browsing/albumpage.go | 2 + ui/browsing/nowplayingpage.go | 2 + ui/browsing/playlistpage.go | 2 + ui/layouts/columnslayout.go | 3 + ui/widgets/tracklist.go | 270 +++++++++++++++++++++------------- 5 files changed, 180 insertions(+), 99 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 51ca39d..5abc2e7 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -61,6 +61,8 @@ func NewAlbumPage( a.ExtendBaseWidget(a) a.header = NewAlbumPageHeader(a) a.tracklist = widgets.NewTracklist(nil) + a.tracklist.SetVisibleColumns([]widgets.TracklistColumn{ + widgets.ColumnArtist, widgets.ColumnTime, widgets.ColumnPlays}) // connect tracklist actions a.tracklist.OnPlayTrackAt = a.onPlayTrackAt a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true) } diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 55f9df1..5b4c431 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -39,6 +39,8 @@ func NewNowPlayingPage( a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{contr: contr, sm: sm, pm: pm, nav: nav}} a.ExtendBaseWidget(a) a.tracklist = widgets.NewTracklist(nil) + a.tracklist.SetVisibleColumns([]widgets.TracklistColumn{ + widgets.ColumnArtist, widgets.ColumnAlbum, widgets.ColumnTime}) a.tracklist.AutoNumber = true a.tracklist.DisablePlaybackMenu = true a.tracklist.OnPlayTrackAt = a.onPlayTrackAt diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index c41daf6..da02e03 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -49,6 +49,8 @@ func NewPlaylistPage( a.ExtendBaseWidget(a) a.header = NewPlaylistPageHeader(a) a.tracklist = widgets.NewTracklist(nil) + a.tracklist.SetVisibleColumns([]widgets.TracklistColumn{ + widgets.ColumnArtist, widgets.ColumnAlbum, widgets.ColumnTime, widgets.ColumnPlays}) a.tracklist.AutoNumber = true a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{ fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist), diff --git a/ui/layouts/columnslayout.go b/ui/layouts/columnslayout.go index bc40147..614915e 100644 --- a/ui/layouts/columnslayout.go +++ b/ui/layouts/columnslayout.go @@ -46,6 +46,9 @@ func (c *ColumnsLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { var x float32 for i := 0; i < len(objects); i++ { + if !objects[i].Visible() { + continue + } w := objects[i].MinSize().Width if i < len(c.ColumnWidths) && c.ColumnWidths[i] > w { w = c.ColumnWidths[i] diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index d7b14aa..9f7c29d 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -16,102 +16,15 @@ import ( "github.com/dweymouth/go-subsonic/subsonic" ) -type TrackRow struct { - widget.BaseWidget +type TracklistColumn string - // internal state - trackIdx int - trackID string - isPlaying bool - tappedAt int64 // unixMillis - - num *widget.RichText - name *widget.RichText - artist *widget.RichText - dur *widget.RichText - - OnTapped func() - OnDoubleTapped func() - OnTappedSecondary func(e *fyne.PointEvent, trackIdx int) - - playingIcon fyne.CanvasObject - selectionRect *canvas.Rectangle - container *fyne.Container -} - -func NewTrackRow(layout *layouts.ColumnsLayout, playingIcon fyne.CanvasObject) *TrackRow { - t := &TrackRow{playingIcon: playingIcon} - t.ExtendBaseWidget(t) - t.num = widget.NewRichTextWithText("") - t.num.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing - t.name = widget.NewRichTextWithText("") - t.name.Wrapping = fyne.TextTruncate - t.artist = widget.NewRichTextWithText("") - t.artist.Wrapping = fyne.TextTruncate - t.dur = widget.NewRichTextWithText("") - t.dur.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing - - t.selectionRect = canvas.NewRectangle(theme.SelectionColor()) - t.selectionRect.Hidden = true - t.container = container.NewMax(t.selectionRect, - container.New(layout, - t.num, t.name, t.artist, t.dur)) - return t -} - -func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) { - if tr.ID == t.trackID && isPlaying == t.isPlaying { - return - } - t.isPlaying = isPlaying - t.trackID = tr.ID - - if rowNum < 0 { - rowNum = tr.Track - } - t.num.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(rowNum) - t.name.Segments[0].(*widget.TextSegment).Text = tr.Title - t.artist.Segments[0].(*widget.TextSegment).Text = tr.Artist - t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration)) - - t.name.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying} - t.artist.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying} - t.dur.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying} - - if isPlaying { - t.container.Objects[1].(*fyne.Container).Objects[0] = container.NewCenter(t.playingIcon) - } else { - t.container.Objects[1].(*fyne.Container).Objects[0] = t.num - } - - t.Refresh() -} - -func (t *TrackRow) CreateRenderer() fyne.WidgetRenderer { - return widget.NewSimpleRenderer(t.container) -} - -// We implement our own double tapping so that the Tapped behavior -// can be triggered instantly. -func (t *TrackRow) Tapped(*fyne.PointEvent) { - prevTap := t.tappedAt - t.tappedAt = time.Now().UnixMilli() - if t.tappedAt-prevTap < 300 { - if t.OnDoubleTapped != nil { - t.OnDoubleTapped() - } - } else { - if t.OnTapped != nil { - t.OnTapped() - } - } -} - -func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) { - if t.OnTappedSecondary != nil { - t.OnTappedSecondary(e, t.trackIdx) - } -} +const ( + ColumnArtist TracklistColumn = "Artist" + ColumnAlbum TracklistColumn = "Album" + ColumnTime TracklistColumn = "Time" + ColumnPlays TracklistColumn = "Plays" + ColumnBitrate TracklistColumn = "Bitrate" +) type Tracklist struct { widget.BaseWidget @@ -128,6 +41,8 @@ type Tracklist struct { OnAddToQueue func(trackIDs []*subsonic.Child) OnAddToPlaylist func(trackIDs []string) + visibleColumns []bool + selectionMgr util.ListSelectionManager nowPlayingIdx int colLayout *layouts.ColumnsLayout @@ -138,16 +53,19 @@ type Tracklist struct { } func NewTracklist(tracks []*subsonic.Child) *Tracklist { - t := &Tracklist{Tracks: tracks, nowPlayingIdx: -1} + t := &Tracklist{Tracks: tracks, nowPlayingIdx: -1, visibleColumns: make([]bool, 5)} + t.ExtendBaseWidget(t) t.selectionMgr = util.NewListSelectionManager(func() int { return len(t.Tracks) }) - t.colLayout = layouts.NewColumnsLayout([]float32{35, -1, -1, 60}) - t.hdr = NewListHeader([]ListColumn{{"#", true}, {"Title", false}, {"Artist", false}, {"Time", true}}, t.colLayout) + t.colLayout = layouts.NewColumnsLayout([]float32{35, -1, -1, -1, 60, 65, 75}) + t.hdr = NewListHeader([]ListColumn{ + {"#", true}, {"Title", false}, {"Artist", false}, {"Album", false}, {"Time", true}, {"Plays", true}, {"Bitrate", true}}, + t.colLayout) playingIcon := container.NewCenter(container.NewHBox(NewHSpace(2), widget.NewIcon(theme.MediaPlayIcon()))) t.list = widget.NewList( func() int { return len(t.Tracks) }, func() fyne.CanvasObject { - tr := NewTrackRow(t.colLayout, playingIcon) + tr := NewTrackRow(t, playingIcon) tr.OnTapped = func() { t.onSelectTrack(tr.trackIdx) } tr.OnTappedSecondary = t.onShowContextMenu tr.OnDoubleTapped = func() { t.onPlayTrackAt(tr.trackIdx) } @@ -167,6 +85,15 @@ func NewTracklist(tracks []*subsonic.Child) *Tracklist { return t } +func (t *Tracklist) SetVisibleColumns(cols []TracklistColumn) { + for i := range t.visibleColumns { + t.visibleColumns[i] = false + } + for _, col := range cols { + t.visibleColumns[col.ColNumber()] = true + } +} + func (t *Tracklist) SetNowPlaying(trackID string) { t.nowPlayingIdx = -1 for i, tr := range t.Tracks { @@ -188,6 +115,14 @@ func (t *Tracklist) UnselectAll() { t.Refresh() } +func (t *Tracklist) Refresh() { + for i, tf := range t.visibleColumns { + // first 2 columns are built-in and always visible + t.hdr.SetColumnVisible(i+2, tf) + } + t.BaseWidget.Refresh() +} + func (t *Tracklist) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(t.container) } @@ -267,3 +202,140 @@ func (t *Tracklist) selectedTrackIDs() []string { func (t *Tracklist) SelectedTrackIndexes() []int { return t.selectionMgr.GetSelection() } + +func (c TracklistColumn) ColNumber() int { + // built-in columns # and Title are always visible + switch c { + case ColumnArtist: + return 0 + case ColumnAlbum: + return 1 + case ColumnTime: + return 2 + case ColumnPlays: + return 3 + case ColumnBitrate: + return 4 + default: + return -100 + } +} + +type TrackRow struct { + widget.BaseWidget + + // internal state + tracklist *Tracklist + trackIdx int + trackID string + isPlaying bool + tappedAt int64 // unixMillis + + num *widget.RichText + name *widget.RichText + artist *widget.RichText + album *widget.RichText + dur *widget.RichText + bitrate *widget.RichText + plays *widget.RichText + + OnTapped func() + OnDoubleTapped func() + OnTappedSecondary func(e *fyne.PointEvent, trackIdx int) + + playingIcon fyne.CanvasObject + selectionRect *canvas.Rectangle + container *fyne.Container +} + +func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow { + t := &TrackRow{tracklist: tracklist, playingIcon: playingIcon} + t.ExtendBaseWidget(t) + t.num = widget.NewRichTextWithText("") + t.num.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing + t.name = widget.NewRichTextWithText("") + t.name.Wrapping = fyne.TextTruncate + t.artist = widget.NewRichTextWithText("") + t.artist.Wrapping = fyne.TextTruncate + t.album = widget.NewRichTextWithText("") + t.album.Wrapping = fyne.TextTruncate + t.dur = widget.NewRichTextWithText("") + t.dur.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing + t.plays = widget.NewRichTextWithText("") + t.plays.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing + t.bitrate = widget.NewRichTextWithText("") + t.bitrate.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing + + t.selectionRect = canvas.NewRectangle(theme.SelectionColor()) + t.selectionRect.Hidden = true + t.container = container.NewMax(t.selectionRect, + container.New(tracklist.colLayout, + t.num, t.name, t.artist, t.album, t.dur, t.plays, t.bitrate)) + return t +} + +func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) { + if tr.ID == t.trackID && isPlaying == t.isPlaying { + return + } + t.isPlaying = isPlaying + t.trackID = tr.ID + + if rowNum < 0 { + rowNum = tr.Track + } + t.num.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(rowNum) + t.name.Segments[0].(*widget.TextSegment).Text = tr.Title + t.artist.Segments[0].(*widget.TextSegment).Text = tr.Artist + t.album.Segments[0].(*widget.TextSegment).Text = tr.Album + t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration)) + t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount)) + t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate) + + t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + t.artist.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + t.album.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + t.dur.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + t.plays.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + t.bitrate.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + + t.artist.Hidden = !t.tracklist.visibleColumns[ColumnArtist.ColNumber()] + t.album.Hidden = !t.tracklist.visibleColumns[ColumnAlbum.ColNumber()] + t.dur.Hidden = !t.tracklist.visibleColumns[ColumnTime.ColNumber()] + t.plays.Hidden = !t.tracklist.visibleColumns[ColumnPlays.ColNumber()] + t.bitrate.Hidden = !t.tracklist.visibleColumns[ColumnBitrate.ColNumber()] + + if isPlaying { + t.container.Objects[1].(*fyne.Container).Objects[0] = container.NewCenter(t.playingIcon) + } else { + t.container.Objects[1].(*fyne.Container).Objects[0] = t.num + } + + t.Refresh() +} + +func (t *TrackRow) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(t.container) +} + +// We implement our own double tapping so that the Tapped behavior +// can be triggered instantly. +func (t *TrackRow) Tapped(*fyne.PointEvent) { + prevTap := t.tappedAt + t.tappedAt = time.Now().UnixMilli() + if t.tappedAt-prevTap < 300 { + if t.OnDoubleTapped != nil { + t.OnDoubleTapped() + } + } else { + if t.OnTapped != nil { + t.OnTapped() + } + } +} + +func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) { + if t.OnTappedSecondary != nil { + t.OnTappedSecondary(e, t.trackIdx) + } +}