Merge branch 'main' of github.com:dusnm/supersonic
This commit is contained in:
@@ -6,6 +6,8 @@
|
|||||||
- [#107](https://github.com/dweymouth/supersonic/issues/107) Add playback settings dropdown to choose output audio device
|
- [#107](https://github.com/dweymouth/supersonic/issues/107) Add playback settings dropdown to choose output audio device
|
||||||
- [#119](https://github.com/dweymouth/supersonic/issues/119) Add file path column to tracklist view
|
- [#119](https://github.com/dweymouth/supersonic/issues/119) Add file path column to tracklist view
|
||||||
- [#117](https://github.com/dweymouth/supersonic/issues/117) Add (optional) system tray menu and close to tray support
|
- [#117](https://github.com/dweymouth/supersonic/issues/117) Add (optional) system tray menu and close to tray support
|
||||||
|
- [#55](https://github.com/dweymouth/supersonic/issues/55) Show disc number and disc count for multi-disc albums
|
||||||
|
- [#115](https://github.com/dweymouth/supersonic/issues/115) Add search bar to artist, genres, and playlists pages
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- **todo-commithash** Don't show update available prompt if the found version is the same as the running app version
|
- **todo-commithash** Don't show update available prompt if the found version is the same as the running app version
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ A lightweight cross-platform desktop client for Subsonic music servers (Navidrom
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
Screenshots of Supersonic running against the Navidrome [demo server](https://www.navidrome.org/demo/)
|
||||||
|
|
||||||
<a href="https://raw.githubusercontent.com/dweymouth/supersonic/main/res/screenshots/AlbumsView.png"><img src="https://raw.github.com/dweymouth/supersonic/main/res/screenshots/AlbumsView.png" width="49.5%"/></a>
|
<a href="https://raw.githubusercontent.com/dweymouth/supersonic/main/res/screenshots/AlbumsView.png"><img src="https://raw.github.com/dweymouth/supersonic/main/res/screenshots/AlbumsView.png" width="49.5%"/></a>
|
||||||
<a href="https://raw.githubusercontent.com/dweymouth/supersonic/main/res/screenshots/AlbumView.png"><img src="https://raw.github.com/dweymouth/supersonic/main/res/screenshots/AlbumView.png" width="49.5%"/></a>
|
<a href="https://raw.githubusercontent.com/dweymouth/supersonic/main/res/screenshots/AlbumView.png"><img src="https://raw.github.com/dweymouth/supersonic/main/res/screenshots/AlbumView.png" width="49.5%"/></a>
|
||||||
<a href="https://raw.githubusercontent.com/dweymouth/supersonic/main/res/screenshots/ArtistView.png"><img src="https://raw.github.com/dweymouth/supersonic/main/res/screenshots/ArtistView.png" width="49.5%"/></a>
|
<a href="https://raw.githubusercontent.com/dweymouth/supersonic/main/res/screenshots/ArtistView.png"><img src="https://raw.github.com/dweymouth/supersonic/main/res/screenshots/ArtistView.png" width="49.5%"/></a>
|
||||||
@@ -19,6 +21,7 @@ A lightweight cross-platform desktop client for Subsonic music servers (Navidrom
|
|||||||
* [x] Browse by albums, artists, genres, playlists
|
* [x] Browse by albums, artists, genres, playlists
|
||||||
* [x] Album and playlist views with tracklist and cover image
|
* [x] Album and playlist views with tracklist and cover image
|
||||||
* [x] Artist view with biography, image, similar artists, and discography
|
* [x] Artist view with biography, image, similar artists, and discography
|
||||||
|
* [x] Play "artist radio" (mix of songs from given artist and similar artists, depends on your server's support)
|
||||||
* [x] Create, play, and update playlists
|
* [x] Create, play, and update playlists
|
||||||
* [x] Configure visible tracklist columns
|
* [x] Configure visible tracklist columns
|
||||||
* [x] Set/unset favorite and browse by favorite albums, artists, and songs
|
* [x] Set/unset favorite and browse by favorite albums, artists, and songs
|
||||||
@@ -31,12 +34,12 @@ A lightweight cross-platform desktop client for Subsonic music servers (Navidrom
|
|||||||
* [ ] Cast to uPnP/DLNA devices (likely planned)
|
* [ ] Cast to uPnP/DLNA devices (likely planned)
|
||||||
* [ ] Built-in multi-band equalizer (eventully planned)
|
* [ ] Built-in multi-band equalizer (eventully planned)
|
||||||
* [ ] Offline mode (eventually planned)
|
* [ ] Offline mode (eventually planned)
|
||||||
* [ ] iOS/Android support (eventually planned)
|
|
||||||
* [ ] Lyrics support (eventually planned)
|
* [ ] Lyrics support (eventually planned)
|
||||||
|
* [ ] iOS/Android support (maybe eventually planned)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Download the latest [release](https://github.com/dweymouth/supersonic/releases) for your operating system. If you prefer to build from source, **or there is no release build currently offered for your OS/architecture (Apple silicon Macs)**, then see the build instructions for your platform below.
|
If you are running Windows, Mac OS (Intel), or a Debian-based Linux distro, download the latest [release](https://github.com/dweymouth/supersonic/releases) for your operating system. If you prefer to build from source, **or there is no release build currently offered for your OS/architecture (Apple silicon Macs)**, then see the build instructions for your platform below.
|
||||||
|
|
||||||
**If you are on Linux** you must have libmpv installed on your system. On apt-based systems, run `sudo apt install libmpv-dev` if it is not already installed. The Windows and Mac release builds bundle the mpv dependencies.
|
**If you are on Linux** you must have libmpv installed on your system. On apt-based systems, run `sudo apt install libmpv-dev` if it is not already installed. The Windows and Mac release builds bundle the mpv dependencies.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/dweymouth/go-mpv"
|
"github.com/dweymouth/go-mpv"
|
||||||
@@ -156,6 +157,7 @@ func (p *Player) Init(maxCacheMB int) error {
|
|||||||
// Note that the Player API does not provide methods to read
|
// Note that the Player API does not provide methods to read
|
||||||
// the play queue. Clients are expected to maintain their own play queue model.
|
// the play queue. Clients are expected to maintain their own play queue model.
|
||||||
func (p *Player) AppendFile(url string) error {
|
func (p *Player) AppendFile(url string) error {
|
||||||
|
log.Printf("Adding playback URL: %s", url)
|
||||||
if !p.initialized {
|
if !p.initialized {
|
||||||
return ErrUnitialized
|
return ErrUnitialized
|
||||||
}
|
}
|
||||||
@@ -164,6 +166,7 @@ func (p *Player) AppendFile(url string) error {
|
|||||||
|
|
||||||
// Plays the specified file, clearing the previous play queue, if any.
|
// Plays the specified file, clearing the previous play queue, if any.
|
||||||
func (p *Player) PlayFile(url string) error {
|
func (p *Player) PlayFile(url string) error {
|
||||||
|
log.Printf("Adding playback URL: %s", url)
|
||||||
if !p.initialized {
|
if !p.initialized {
|
||||||
return ErrUnitialized
|
return ErrUnitialized
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ func IntSliceContains(slice []int, i int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FilterSlice[T any](ss []T, test func(T) bool) []T {
|
||||||
|
result := make([]T, 0)
|
||||||
|
for _, s := range ss {
|
||||||
|
if test(s) {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
|
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
|
||||||
for _, tr := range tracks {
|
for _, tr := range tracks {
|
||||||
if id == tr.ID {
|
if id == tr.ID {
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ func (a *AlbumPage) load() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.header.Update(album, a.im)
|
a.header.Update(album, a.im)
|
||||||
|
a.tracklist.ShowDiscNumber = album.Song[0].DiscNumber != album.Song[len(album.Song)-1].DiscNumber
|
||||||
a.tracklist.Tracks = album.Song
|
a.tracklist.Tracks = album.Song
|
||||||
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
||||||
}
|
}
|
||||||
@@ -236,7 +237,11 @@ func (a *AlbumPageHeader) showPopUpCover() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func formatMiscLabelStr(a *subsonic.AlbumID3) string {
|
func formatMiscLabelStr(a *subsonic.AlbumID3) string {
|
||||||
return fmt.Sprintf("%d · %d tracks · %s", a.Year, a.SongCount, util.SecondsToTimeString(float64(a.Duration)))
|
var discs string
|
||||||
|
if discCount := a.Song[len(a.Song)-1].DiscNumber; discCount > 1 {
|
||||||
|
discs = fmt.Sprintf("%d discs · ", discCount)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d · %d tracks · %s%s", a.Year, a.SongCount, discs, util.SecondsToTimeString(float64(a.Duration)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *albumPageState) Restore() Page {
|
func (s *albumPageState) Restore() Page {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package browsing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
|
"supersonic/sharedutil"
|
||||||
"supersonic/ui/controller"
|
"supersonic/ui/controller"
|
||||||
"supersonic/ui/layouts"
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/widgets"
|
"supersonic/ui/widgets"
|
||||||
@@ -10,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
@@ -23,12 +26,19 @@ type ArtistsGenresPage struct {
|
|||||||
isGenresPage bool
|
isGenresPage bool
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
sm *backend.ServerManager
|
sm *backend.ServerManager
|
||||||
titleDisp *widget.RichText
|
model []widgets.ArtistGenreListItemModel
|
||||||
container *fyne.Container
|
|
||||||
list *widgets.ArtistGenreList
|
list *widgets.ArtistGenreList
|
||||||
|
|
||||||
|
titleDisp *widget.RichText
|
||||||
|
container *fyne.Container
|
||||||
|
searcher *widgets.Searcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager) *ArtistsGenresPage {
|
func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager) *ArtistsGenresPage {
|
||||||
|
return newArtistsGenresPage(isGenresPage, contr, sm, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager, searchText string) *ArtistsGenresPage {
|
||||||
title := "Artists"
|
title := "Artists"
|
||||||
if isGenresPage {
|
if isGenresPage {
|
||||||
title = "Genres"
|
title = "Genres"
|
||||||
@@ -51,27 +61,55 @@ func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *b
|
|||||||
a.contr.NavigateTo(controller.ArtistRoute(id))
|
a.contr.NavigateTo(controller.ArtistRoute(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
a.searcher = widgets.NewSearcher()
|
||||||
|
a.searcher.OnSearched = a.onSearched
|
||||||
|
a.searcher.Entry.Text = searchText
|
||||||
a.buildContainer()
|
a.buildContainer()
|
||||||
go a.load()
|
go a.load(searchText != "")
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
// should be called asynchronously
|
// should be called asynchronously
|
||||||
func (a *ArtistsGenresPage) load() {
|
func (a *ArtistsGenresPage) load(searchOnLoad bool) {
|
||||||
if a.isGenresPage {
|
if a.isGenresPage {
|
||||||
genres, err := a.sm.Server.GetGenres()
|
genres, err := a.sm.Server.GetGenres()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error loading genres: %v", err.Error())
|
log.Printf("error loading genres: %v", err.Error())
|
||||||
}
|
}
|
||||||
a.list.Items = a.buildGenresListModel(genres)
|
a.model = a.buildGenresListModel(genres)
|
||||||
} else {
|
} else {
|
||||||
artists, err := a.sm.Server.GetArtists(nil)
|
artists, err := a.sm.Server.GetArtists(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error loading artists: %v", err.Error())
|
log.Printf("error loading artists: %v", err.Error())
|
||||||
}
|
}
|
||||||
a.list.Items = a.buildArtistListModel(artists)
|
a.model = a.buildArtistListModel(artists)
|
||||||
}
|
}
|
||||||
a.Refresh()
|
if searchOnLoad {
|
||||||
|
a.onSearched(a.searcher.Entry.Text)
|
||||||
|
} else {
|
||||||
|
a.list.Items = a.model
|
||||||
|
a.list.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtistsGenresPage) 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.model
|
||||||
|
} else {
|
||||||
|
result := sharedutil.FilterSlice(a.model, func(x widgets.ArtistGenreListItemModel) bool {
|
||||||
|
return strings.Contains(strings.ToLower(x.Name), strings.ToLower(query))
|
||||||
|
})
|
||||||
|
a.list.Items = result
|
||||||
|
}
|
||||||
|
a.list.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Searchable = (*ArtistsGenresPage)(nil)
|
||||||
|
|
||||||
|
func (a *ArtistsGenresPage) SearchWidget() fyne.Focusable {
|
||||||
|
return a.searcher.Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsGenresPage) Route() controller.Route {
|
func (a *ArtistsGenresPage) Route() controller.Route {
|
||||||
@@ -82,7 +120,7 @@ func (a *ArtistsGenresPage) Route() controller.Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsGenresPage) Reload() {
|
func (a *ArtistsGenresPage) Reload() {
|
||||||
go a.load()
|
go a.load(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsGenresPage) Save() SavedPage {
|
func (a *ArtistsGenresPage) Save() SavedPage {
|
||||||
@@ -90,6 +128,7 @@ func (a *ArtistsGenresPage) Save() SavedPage {
|
|||||||
isGenresPage: a.isGenresPage,
|
isGenresPage: a.isGenresPage,
|
||||||
contr: a.contr,
|
contr: a.contr,
|
||||||
sm: a.sm,
|
sm: a.sm,
|
||||||
|
searchText: a.searcher.Entry.Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +136,11 @@ type savedArtistsGenresPage struct {
|
|||||||
isGenresPage bool
|
isGenresPage bool
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
sm *backend.ServerManager
|
sm *backend.ServerManager
|
||||||
|
searchText string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *savedArtistsGenresPage) Restore() Page {
|
func (s *savedArtistsGenresPage) Restore() Page {
|
||||||
return NewArtistsGenresPage(s.isGenresPage, s.contr, s.sm)
|
return newArtistsGenresPage(s.isGenresPage, s.contr, s.sm, s.searchText)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsGenresPage) buildArtistListModel(artists *subsonic.ArtistsID3) []widgets.ArtistGenreListItemModel {
|
func (a *ArtistsGenresPage) buildArtistListModel(artists *subsonic.ArtistsID3) []widgets.ArtistGenreListItemModel {
|
||||||
@@ -133,9 +173,11 @@ func (a *ArtistsGenresPage) buildGenresListModel(genres []*subsonic.Genre) []wid
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsGenresPage) buildContainer() {
|
func (a *ArtistsGenresPage) buildContainer() {
|
||||||
|
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
|
||||||
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||||
container.NewBorder(
|
container.NewBorder(
|
||||||
container.New(&layouts.MaxPadLayout{PadLeft: -5}, a.titleDisp),
|
container.New(&layouts.MaxPadLayout{PadLeft: -5},
|
||||||
|
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox)),
|
||||||
nil, nil, nil, a.list))
|
nil, nil, nil, a.list))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package browsing
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
|
"supersonic/sharedutil"
|
||||||
"supersonic/ui/controller"
|
"supersonic/ui/controller"
|
||||||
"supersonic/ui/layouts"
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/widgets"
|
"supersonic/ui/widgets"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
@@ -20,12 +23,19 @@ type PlaylistsPage struct {
|
|||||||
|
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
sm *backend.ServerManager
|
sm *backend.ServerManager
|
||||||
|
playlists []*subsonic.Playlist
|
||||||
|
|
||||||
|
searcher *widgets.Searcher
|
||||||
titleDisp *widget.RichText
|
titleDisp *widget.RichText
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
list *PlaylistList
|
list *PlaylistList
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *PlaylistsPage {
|
func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *PlaylistsPage {
|
||||||
|
return newPlaylistsPage(contr, sm, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager, searchText string) *PlaylistsPage {
|
||||||
a := &PlaylistsPage{
|
a := &PlaylistsPage{
|
||||||
sm: sm,
|
sm: sm,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
@@ -37,47 +47,83 @@ func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *
|
|||||||
a.list.OnNavTo = func(id string) {
|
a.list.OnNavTo = func(id string) {
|
||||||
a.contr.NavigateTo(controller.PlaylistRoute(id))
|
a.contr.NavigateTo(controller.PlaylistRoute(id))
|
||||||
}
|
}
|
||||||
|
a.searcher = widgets.NewSearcher()
|
||||||
|
a.searcher.OnSearched = a.onSearched
|
||||||
|
a.searcher.Entry.Text = searchText
|
||||||
a.buildContainer()
|
a.buildContainer()
|
||||||
go a.loadAsync()
|
go a.load(searchText != "")
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) loadAsync() {
|
func (a *PlaylistsPage) load(searchOnLoad bool) {
|
||||||
playlists, err := a.sm.Server.GetPlaylists(nil)
|
playlists, err := a.sm.Server.GetPlaylists(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error loading playlists: %v", err.Error())
|
log.Printf("error loading playlists: %v", err.Error())
|
||||||
}
|
}
|
||||||
a.list.Playlists = playlists
|
a.playlists = playlists
|
||||||
|
if searchOnLoad {
|
||||||
|
a.onSearched(a.searcher.Entry.Text)
|
||||||
|
} else {
|
||||||
|
a.list.Playlists = playlists
|
||||||
|
a.list.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *PlaylistsPage) onSearched(query string) {
|
||||||
|
// since the playlist list is returned in full non-paginated, we will do our own
|
||||||
|
// simple search based on the name, description, and owner, rather than calling a server API
|
||||||
|
if query == "" {
|
||||||
|
a.list.Playlists = a.playlists
|
||||||
|
} else {
|
||||||
|
result := sharedutil.FilterSlice(a.playlists, func(p *subsonic.Playlist) bool {
|
||||||
|
qLower := strings.ToLower(query)
|
||||||
|
return strings.Contains(strings.ToLower(p.Name), qLower) ||
|
||||||
|
strings.Contains(strings.ToLower(p.Comment), qLower) ||
|
||||||
|
strings.Contains(strings.ToLower(p.Owner), qLower)
|
||||||
|
})
|
||||||
|
a.list.Playlists = result
|
||||||
|
}
|
||||||
a.list.Refresh()
|
a.list.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ Searchable = (*PlaylistsPage)(nil)
|
||||||
|
|
||||||
|
func (a *PlaylistsPage) SearchWidget() fyne.Focusable {
|
||||||
|
return a.searcher.Entry
|
||||||
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) Route() controller.Route {
|
func (a *PlaylistsPage) Route() controller.Route {
|
||||||
return controller.PlaylistsRoute()
|
return controller.PlaylistsRoute()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) Reload() {
|
func (a *PlaylistsPage) Reload() {
|
||||||
go a.loadAsync()
|
go a.load(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) Save() SavedPage {
|
func (a *PlaylistsPage) Save() SavedPage {
|
||||||
return &savedPlaylistsPage{
|
return &savedPlaylistsPage{
|
||||||
contr: a.contr,
|
contr: a.contr,
|
||||||
sm: a.sm,
|
sm: a.sm,
|
||||||
|
searchText: a.searcher.Entry.Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type savedPlaylistsPage struct {
|
type savedPlaylistsPage struct {
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
sm *backend.ServerManager
|
sm *backend.ServerManager
|
||||||
|
searchText string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *savedPlaylistsPage) Restore() Page {
|
func (s *savedPlaylistsPage) Restore() Page {
|
||||||
return NewPlaylistsPage(s.contr, s.sm)
|
return newPlaylistsPage(s.contr, s.sm, s.searchText)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) buildContainer() {
|
func (a *PlaylistsPage) buildContainer() {
|
||||||
|
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
|
||||||
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||||
container.NewBorder(a.titleDisp, nil, nil, nil, a.list))
|
container.NewBorder(
|
||||||
|
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox),
|
||||||
|
nil, nil, nil, a.list))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {
|
func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ 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.
|
||||||
|
// A fixed width column is any with a non-negative width and will
|
||||||
|
// be laid out with that width. A variable width column is created
|
||||||
|
// by using any negative number as its width. Variable width columns
|
||||||
|
// are all laid out with the same width, splitting the "leftover" space
|
||||||
|
// equally between themselves after accounting for the fixed-width columns.
|
||||||
|
// Hidden items are not shown and take up 0 space.
|
||||||
type ColumnsLayout struct {
|
type ColumnsLayout struct {
|
||||||
ColumnWidths []float32
|
ColumnWidths []float32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import (
|
|||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Lays out up to 3 objects such that the middle object, Objects[1],
|
||||||
|
// is centered in the available space and takes up a fixed width.
|
||||||
|
// The left, and right (if non-nil), split the leftover space equally.
|
||||||
type LeftMiddleRightLayout struct {
|
type LeftMiddleRightLayout struct {
|
||||||
middleWidth float32
|
middleWidth float32
|
||||||
hbox fyne.Layout
|
hbox fyne.Layout
|
||||||
|
|||||||
+17
-2
@@ -1,6 +1,7 @@
|
|||||||
package widgets
|
package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"supersonic/res"
|
"supersonic/res"
|
||||||
@@ -40,6 +41,10 @@ type Tracklist struct {
|
|||||||
// or to use the number from the track's metadata
|
// or to use the number from the track's metadata
|
||||||
AutoNumber bool
|
AutoNumber 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
|
// AuxiliaryMenuItems sets additional menu items appended to the context menu
|
||||||
// must be set before the context menu is shown for the first time
|
// must be set before the context menu is shown for the first time
|
||||||
AuxiliaryMenuItems []*fyne.MenuItem
|
AuxiliaryMenuItems []*fyne.MenuItem
|
||||||
@@ -77,7 +82,7 @@ func NewTracklist(tracks []*subsonic.Child) *Tracklist {
|
|||||||
t.ExtendBaseWidget(t)
|
t.ExtendBaseWidget(t)
|
||||||
t.selectionMgr = util.NewListSelectionManager(func() int { return len(t.Tracks) })
|
t.selectionMgr = util.NewListSelectionManager(func() int { return len(t.Tracks) })
|
||||||
// #, Title, Artist, Album, Time, Year, Favorite, Plays, Bitrate, Size, Path
|
// #, Title, Artist, Album, Time, Year, Favorite, Plays, Bitrate, Size, Path
|
||||||
t.colLayout = layouts.NewColumnsLayout([]float32{35, -1, -1, -1, 60, 60, 47, 65, 75, 70, -1})
|
t.colLayout = layouts.NewColumnsLayout([]float32{40, -1, -1, -1, 60, 60, 47, 65, 75, 70, -1})
|
||||||
t.buildHeader()
|
t.buildHeader()
|
||||||
t.hdr.OnColumnVisibilityChanged = t.setColumnVisible
|
t.hdr.OnColumnVisibilityChanged = t.setColumnVisible
|
||||||
t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) {
|
t.hdr.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) {
|
||||||
@@ -438,11 +443,21 @@ func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) {
|
|||||||
// Update track num if needed
|
// Update track num if needed
|
||||||
// (which can change based on bound *subsonic.Child or tracklist.AutoNumber)
|
// (which can change based on bound *subsonic.Child or tracklist.AutoNumber)
|
||||||
if t.trackNum != rowNum {
|
if t.trackNum != rowNum {
|
||||||
|
discNum := -1
|
||||||
|
var str string
|
||||||
if rowNum < 0 {
|
if rowNum < 0 {
|
||||||
rowNum = tr.Track
|
rowNum = tr.Track
|
||||||
|
if t.tracklist.ShowDiscNumber {
|
||||||
|
discNum = tr.DiscNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.trackNum = rowNum
|
t.trackNum = rowNum
|
||||||
t.num.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(rowNum)
|
if discNum >= 0 {
|
||||||
|
str = fmt.Sprintf("%d.%02d", discNum, rowNum)
|
||||||
|
} else {
|
||||||
|
str = strconv.Itoa(rowNum)
|
||||||
|
}
|
||||||
|
t.num.Segments[0].(*widget.TextSegment).Text = str
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update play count if needed
|
// Update play count if needed
|
||||||
|
|||||||
Reference in New Issue
Block a user