diff --git a/ui/browsing/genrespage.go b/ui/browsing/genrespage.go index e23d730..0136601 100644 --- a/ui/browsing/genrespage.go +++ b/ui/browsing/genrespage.go @@ -2,6 +2,7 @@ package browsing import ( "log" + "sort" "strconv" "strings" @@ -34,10 +35,10 @@ type GenresPage struct { } func NewGenresPage(contr *controller.Controller, mp mediaprovider.MediaProvider) *GenresPage { - return newGenresPage(contr, mp, "") + return newGenresPage(contr, mp, "", widgets.ListHeaderSort{}) } -func newGenresPage(contr *controller.Controller, mp mediaprovider.MediaProvider, searchText string) *GenresPage { +func newGenresPage(contr *controller.Controller, mp mediaprovider.MediaProvider, searchText string, sorting widgets.ListHeaderSort) *GenresPage { a := &GenresPage{ contr: contr, mp: mp, @@ -45,7 +46,7 @@ func newGenresPage(contr *controller.Controller, mp mediaprovider.MediaProvider, } a.ExtendBaseWidget(a) a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText - a.list = NewGenreList(nil) + a.list = NewGenreList(sorting) a.list.OnNavTo = func(id string) { a.contr.NavigateTo(controller.GenreRoute(id)) } a.searcher = widgets.NewSearchEntry() a.searcher.OnSearched = a.onSearched @@ -65,7 +66,7 @@ func (a *GenresPage) load(searchOnLoad bool) { if searchOnLoad { a.onSearched(a.searcher.Entry.Text) } else { - a.list.Items = genres + a.list.SetGenres(a.genres) a.list.Refresh() } } @@ -74,13 +75,13 @@ func (a *GenresPage) onSearched(query string) { // since the artists and genres lists are returned in full non-paginated, we will do our own // simple search based on the artist/genre name, rather than calling a server API if query == "" { - a.list.Items = a.genres + a.list.SetGenres(a.genres) } else { query = strings.ToLower(query) result := sharedutil.FilterSlice(a.genres, func(x *mediaprovider.Genre) bool { return strings.Contains(strings.ToLower(x.Name), query) }) - a.list.Items = result + a.list.SetGenres(result) } a.list.Refresh() } @@ -100,22 +101,23 @@ func (a *GenresPage) Reload() { } func (a *GenresPage) Save() SavedPage { - return &savedArtistsGenresPage{ + return &savedGenresPage{ contr: a.contr, mp: a.mp, searchText: a.searcher.Entry.Text, + sorting: a.list.sorting, } } -type savedArtistsGenresPage struct { - isGenresPage bool - contr *controller.Controller - mp mediaprovider.MediaProvider - searchText string +type savedGenresPage struct { + contr *controller.Controller + mp mediaprovider.MediaProvider + searchText string + sorting widgets.ListHeaderSort } -func (s *savedArtistsGenresPage) Restore() Page { - return newGenresPage(s.contr, s.mp, s.searchText) +func (s *savedGenresPage) Restore() Page { + return newGenresPage(s.contr, s.mp, s.searchText, s.sorting) } func (a *GenresPage) buildContainer() { @@ -134,9 +136,12 @@ func (a *GenresPage) CreateRenderer() fyne.WidgetRenderer { type GenreList struct { widget.BaseWidget - Items []*mediaprovider.Genre OnNavTo func(string) + sorting widgets.ListHeaderSort + genres []*mediaprovider.Genre + genresOrigOrder []*mediaprovider.Genre + columnsLayout *layouts.ColumnsLayout hdr *widgets.ListHeader list *widget.List @@ -169,17 +174,18 @@ func NewGenreListRow(layout *layouts.ColumnsLayout) *GenreListRow { return a } -func NewGenreList(items []*mediaprovider.Genre) *GenreList { +func NewGenreList(sorting widgets.ListHeaderSort) *GenreList { a := &GenreList{ - Items: items, + sorting: sorting, columnsLayout: layouts.NewColumnsLayout([]float32{-1, 125, 125}), } a.ExtendBaseWidget(a) a.hdr = widgets.NewListHeader([]widgets.ListColumn{ {"Name", fyne.TextAlignLeading, false}, {"Album Count", fyne.TextAlignTrailing, false}, {"Track Count", fyne.TextAlignTrailing, false}}, a.columnsLayout) - a.hdr.DisableSorting = true + a.hdr.SetSorting(sorting) + a.hdr.OnColumnSortChanged = a.onSorted a.list = widget.NewList( - func() int { return len(a.Items) }, + func() int { return len(a.genres) }, func() fyne.CanvasObject { r := NewGenreListRow(a.columnsLayout) r.OnTapped = func() { a.onRowDoubleTapped(r.Item) } @@ -187,7 +193,7 @@ func NewGenreList(items []*mediaprovider.Genre) *GenreList { }, func(id widget.ListItemID, item fyne.CanvasObject) { row := item.(*GenreListRow) - row.Item = a.Items[id] + row.Item = a.genres[id] row.nameLabel.Text = row.Item.Name row.albumCountLabel.Text = strconv.Itoa(row.Item.AlbumCount) row.trackCountLabel.Text = strconv.Itoa(row.Item.TrackCount) @@ -198,6 +204,58 @@ func NewGenreList(items []*mediaprovider.Genre) *GenreList { return a } +func (g *GenreList) SetGenres(genres []*mediaprovider.Genre) { + g.genresOrigOrder = genres + g.doSortGenres() + g.Refresh() +} + +func (g *GenreList) onSorted(sort widgets.ListHeaderSort) { + g.sorting = sort + g.doSortGenres() + g.Refresh() +} + +func (g *GenreList) doSortGenres() { + if g.sorting.Type == widgets.SortNone { + g.genres = g.genresOrigOrder + return + } + switch g.sorting.ColNumber { + case 0: //Name + g.stringSort(func(g *mediaprovider.Genre) string { return g.Name }) + case 1: // Album Count + g.intSort(func(g *mediaprovider.Genre) int { return g.AlbumCount }) + case 2: // Track Count + g.intSort(func(g *mediaprovider.Genre) int { return g.TrackCount }) + } +} + +func (g *GenreList) stringSort(fieldFn func(*mediaprovider.Genre) string) { + new := make([]*mediaprovider.Genre, len(g.genresOrigOrder)) + copy(new, g.genresOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) + if g.sorting.Type == widgets.SortDescending { + return cmp > 0 + } + return cmp < 0 + }) + g.genres = new +} + +func (g *GenreList) intSort(fieldFn func(*mediaprovider.Genre) int) { + new := make([]*mediaprovider.Genre, len(g.genresOrigOrder)) + copy(new, g.genresOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + if g.sorting.Type == widgets.SortDescending { + return fieldFn(new[i]) > fieldFn(new[j]) + } + return fieldFn(new[i]) < fieldFn(new[j]) + }) + g.genres = new +} + func (a *GenreList) onRowDoubleTapped(item *mediaprovider.Genre) { if a.OnNavTo != nil { a.OnNavTo(item.Name) diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index e88444e..4d6214e 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -3,6 +3,7 @@ package browsing import ( "fmt" "log" + "sort" "strconv" "strings" @@ -38,6 +39,7 @@ type PlaylistsPage struct { titleDisp *widget.RichText container *fyne.Container listView *PlaylistList + listSort widgets.ListHeaderSort gridView *widgets.GridView } @@ -46,15 +48,16 @@ func NewPlaylistsPage(contr *controller.Controller, pool *util.WidgetPool, cfg * if cfg.InitialView == "Grid" { activeView = 1 } - return newPlaylistsPage(contr, pool, cfg, mp, "", activeView) + return newPlaylistsPage(contr, pool, cfg, mp, "", activeView, widgets.ListHeaderSort{}) } -func newPlaylistsPage(contr *controller.Controller, pool *util.WidgetPool, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider, searchText string, activeView int) *PlaylistsPage { +func newPlaylistsPage(contr *controller.Controller, pool *util.WidgetPool, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider, searchText string, activeView int, listSort widgets.ListHeaderSort) *PlaylistsPage { a := &PlaylistsPage{ pool: pool, cfg: cfg, mp: mp, contr: contr, + listSort: listSort, titleDisp: widget.NewRichTextWithText("Playlists"), } a.ExtendBaseWidget(a) @@ -92,7 +95,7 @@ func (a *PlaylistsPage) load(searchOnLoad bool) { } func (a *PlaylistsPage) createListView() { - a.listView = NewPlaylistList() + a.listView = NewPlaylistList(a.listSort) a.listView.OnNavTo = a.showPlaylistPage } @@ -140,9 +143,9 @@ func (a *PlaylistsPage) showListView() { if a.listView == nil { a.createListView() if a.searcher.Entry.Text != "" { - a.listView.Playlists = a.searchedPlaylists + a.listView.SetPlaylists(a.searchedPlaylists) } else { - a.listView.Playlists = a.playlists + a.listView.SetPlaylists(a.playlists) } } a.container.Objects[0].(*fyne.Container).Objects[0] = a.listView @@ -204,7 +207,7 @@ func (a *PlaylistsPage) onSearched(query string) { // refresh the active view func (a *PlaylistsPage) refreshView(playlists []*mediaprovider.Playlist) { if a.listView != nil { - a.listView.Playlists = playlists + a.listView.SetPlaylists(playlists) } if a.gridView != nil { a.gridView.ResetFixed(createPlaylistGridViewModel(playlists)) @@ -243,6 +246,9 @@ func (a *PlaylistsPage) Save() SavedPage { a.gridView.Clear() a.pool.Release(util.WidgetTypeGridView, a.gridView) } + if a.listView != nil { + s.listSort = a.listView.sorting + } return s } @@ -253,10 +259,11 @@ type savedPlaylistsPage struct { mp mediaprovider.MediaProvider searchText string activeView int + listSort widgets.ListHeaderSort } func (s *savedPlaylistsPage) Restore() Page { - return newPlaylistsPage(s.contr, s.pool, s.cfg, s.mp, s.searchText, s.activeView) + return newPlaylistsPage(s.contr, s.pool, s.cfg, s.mp, s.searchText, s.activeView, s.listSort) } func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) { @@ -274,8 +281,11 @@ func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer { type PlaylistList struct { widget.BaseWidget - Playlists []*mediaprovider.Playlist - OnNavTo func(string) + OnNavTo func(string) + + playlistsOrigOrder []*mediaprovider.Playlist + playlists []*mediaprovider.Playlist + sorting widgets.ListHeaderSort columnsLayout *layouts.ColumnsLayout header *widgets.ListHeader @@ -283,14 +293,15 @@ type PlaylistList struct { container *fyne.Container } -func NewPlaylistList() *PlaylistList { +func NewPlaylistList(initialSort widgets.ListHeaderSort) *PlaylistList { a := &PlaylistList{ + sorting: initialSort, columnsLayout: layouts.NewColumnsLayout([]float32{-1, -1, 200, 125}), } a.buildHeader() a.list = widget.NewList( func() int { - return len(a.Playlists) + return len(a.playlists) }, func() fyne.CanvasObject { r := NewPlaylistListRow(a.columnsLayout) @@ -299,11 +310,11 @@ func NewPlaylistList() *PlaylistList { }, func(id widget.ListItemID, item fyne.CanvasObject) { row := item.(*PlaylistListRow) - row.ID = a.Playlists[id].ID - row.nameLabel.Text = a.Playlists[id].Name - row.descrptionLabel.Text = a.Playlists[id].Description - row.ownerLabel.Text = a.Playlists[id].Owner - row.trackCountLabel.Text = strconv.Itoa(a.Playlists[id].TrackCount) + row.ID = a.playlists[id].ID + row.nameLabel.Text = a.playlists[id].Name + row.descrptionLabel.Text = a.playlists[id].Description + row.ownerLabel.Text = a.playlists[id].Owner + row.trackCountLabel.Text = strconv.Itoa(a.playlists[id].TrackCount) row.Refresh() }, ) @@ -314,12 +325,18 @@ func NewPlaylistList() *PlaylistList { func (p *PlaylistList) buildHeader() { p.header = widgets.NewListHeader([]widgets.ListColumn{ - {"Name", fyne.TextAlignLeading, false}, - {"Description", fyne.TextAlignLeading, false}, - {"Owner", fyne.TextAlignLeading, false}, - {"Track Count", fyne.TextAlignTrailing, false}}, p.columnsLayout) - p.header.DisableSorting = true + {Text: "Name", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, + {Text: "Description", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, + {Text: "Owner", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, + {Text: "Track Count", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}}, p.columnsLayout) + p.header.SetSorting(p.sorting) + p.header.OnColumnSortChanged = p.onSorted +} +// Sets the playlists in the list. Does not issue Refresh call. +func (p *PlaylistList) SetPlaylists(playlists []*mediaprovider.Playlist) { + p.playlistsOrigOrder = playlists + p.doSortPlaylists() } func (p *PlaylistList) CreateRenderer() fyne.WidgetRenderer { @@ -332,6 +349,54 @@ func (p *PlaylistList) onRowTapped(id string) { } } +func (p *PlaylistList) onSorted(sort widgets.ListHeaderSort) { + p.sorting = sort + p.doSortPlaylists() + p.Refresh() +} + +func (p *PlaylistList) doSortPlaylists() { + if p.sorting.Type == widgets.SortNone { + p.playlists = p.playlistsOrigOrder + return + } + switch p.sorting.ColNumber { + case 0: //Name + p.stringSort(func(p *mediaprovider.Playlist) string { return p.Name }) + case 1: // Description + p.stringSort(func(p *mediaprovider.Playlist) string { return p.Description }) + case 2: // Owner + p.stringSort(func(p *mediaprovider.Playlist) string { return p.Owner }) + case 3: // Track Count + p.intSort(func(p *mediaprovider.Playlist) int { return p.TrackCount }) + } +} + +func (p *PlaylistList) stringSort(fieldFn func(*mediaprovider.Playlist) string) { + new := make([]*mediaprovider.Playlist, len(p.playlistsOrigOrder)) + copy(new, p.playlistsOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) + if p.sorting.Type == widgets.SortDescending { + return cmp > 0 + } + return cmp < 0 + }) + p.playlists = new +} + +func (p *PlaylistList) intSort(fieldFn func(*mediaprovider.Playlist) int) { + new := make([]*mediaprovider.Playlist, len(p.playlistsOrigOrder)) + copy(new, p.playlistsOrigOrder) + sort.SliceStable(new, func(i, j int) bool { + if p.sorting.Type == widgets.SortDescending { + return fieldFn(new[i]) > fieldFn(new[j]) + } + return fieldFn(new[i]) < fieldFn(new[j]) + }) + p.playlists = new +} + type PlaylistListRow struct { widget.BaseWidget