Merge pull request #388 from dweymouth/improve-playlist-dialog
Improve Add to Playlist dialog
This commit is contained in:
+41
-37
@@ -322,49 +322,52 @@ func (m *Controller) PromptForFirstServer() {
|
|||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show dialog to prompt for playlist.
|
// Show dialog to select playlist.
|
||||||
// Depending on the results of that dialog, potentially create a new playlist
|
// Depending on the results of that dialog, potentially create a new playlist
|
||||||
// Add tracks to the user-specified playlist
|
// Add tracks to the user-specified playlist
|
||||||
func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
||||||
go func() {
|
sp := dialogs.NewSelectPlaylistDialog(m.App.ServerManager.Server, m.App.ImageManager, m.App.ServerManager.LoggedInUser)
|
||||||
pls, err := m.App.ServerManager.Server.GetPlaylists()
|
pop := widget.NewModalPopUp(sp.SearchDialog, m.MainWindow.Canvas())
|
||||||
pls = sharedutil.FilterSlice(pls, func(pl *mediaprovider.Playlist) bool {
|
sp.SetOnDismiss(func() {
|
||||||
return pl.Owner == m.App.ServerManager.LoggedInUser
|
pop.Hide()
|
||||||
})
|
m.doModalClosed()
|
||||||
if err != nil {
|
})
|
||||||
// TODO: surface this error to user
|
sp.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) {
|
||||||
log.Printf("error getting user-owned playlists: %s", err.Error())
|
pop.Hide()
|
||||||
return
|
if id == "" /* creating new playlist */ {
|
||||||
}
|
go m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs)
|
||||||
|
} else {
|
||||||
selectedIdx := -1
|
m.App.Config.Application.DefaultPlaylistID = id
|
||||||
plNames := make([]string, 0, len(pls))
|
if sp.SkipDuplicates {
|
||||||
for i, pl := range pls {
|
go func() {
|
||||||
plNames = append(plNames, pl.Name)
|
currentTrackIDs := make(map[string]struct{})
|
||||||
if defId := m.App.Config.Application.DefaultPlaylistID; defId != "" && pl.ID == defId {
|
if selectedPlaylist, err := m.App.ServerManager.Server.GetPlaylist(id); err != nil {
|
||||||
selectedIdx = i
|
log.Printf("error getting playlist: %s", err.Error())
|
||||||
}
|
} else {
|
||||||
}
|
for _, track := range selectedPlaylist.Tracks {
|
||||||
|
currentTrackIDs[track.ID] = struct{}{}
|
||||||
dlg := dialogs.NewAddToPlaylistDialog("Add to Playlist", plNames, selectedIdx)
|
}
|
||||||
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
filterTrackIDs := sharedutil.FilterSlice(trackIDs, func(trackID string) bool {
|
||||||
m.ClosePopUpOnEscape(pop)
|
_, ok := currentTrackIDs[trackID]
|
||||||
dlg.OnCanceled = pop.Hide
|
return !ok
|
||||||
dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) {
|
})
|
||||||
pop.Hide()
|
m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs)
|
||||||
m.doModalClosed()
|
}
|
||||||
if playlistChoice < 0 {
|
}()
|
||||||
go m.App.ServerManager.Server.CreatePlaylist(newPlaylistName, trackIDs)
|
|
||||||
} else {
|
} else {
|
||||||
playlist := pls[playlistChoice]
|
go m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs)
|
||||||
m.App.Config.Application.DefaultPlaylistID = playlist.ID
|
|
||||||
go m.App.ServerManager.Server.AddPlaylistTracks(
|
|
||||||
playlist.ID, trackIDs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.haveModal = true
|
|
||||||
pop.Show()
|
})
|
||||||
}()
|
m.ClosePopUpOnEscape(pop)
|
||||||
|
m.haveModal = true
|
||||||
|
min := sp.MinSize()
|
||||||
|
height := fyne.Max(min.Height, fyne.Min(min.Height*1.5, m.MainWindow.Canvas().Size().Height*0.7))
|
||||||
|
sp.SearchDialog.Show()
|
||||||
|
pop.Resize(fyne.NewSize(min.Width, height))
|
||||||
|
pop.Show()
|
||||||
|
m.MainWindow.Canvas().Focus(sp.GetSearchEntry())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
|
func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
|
||||||
@@ -642,6 +645,7 @@ func (c *Controller) ShowQuickSearch() {
|
|||||||
c.haveModal = true
|
c.haveModal = true
|
||||||
min := qs.MinSize()
|
min := qs.MinSize()
|
||||||
height := fyne.Max(min.Height, fyne.Min(min.Height*1.5, c.MainWindow.Canvas().Size().Height*0.7))
|
height := fyne.Max(min.Height, fyne.Min(min.Height*1.5, c.MainWindow.Canvas().Size().Height*0.7))
|
||||||
|
qs.SearchDialog.Show()
|
||||||
pop.Resize(fyne.NewSize(min.Width, height))
|
pop.Resize(fyne.NewSize(min.Width, height))
|
||||||
pop.Show()
|
pop.Show()
|
||||||
c.MainWindow.Canvas().Focus(qs.GetSearchEntry())
|
c.MainWindow.Canvas().Focus(qs.GetSearchEntry())
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package dialogs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/layout"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AddToPlaylistDialog struct {
|
|
||||||
widget.BaseWidget
|
|
||||||
|
|
||||||
OnCanceled func()
|
|
||||||
OnSubmit func(playlistChoice int, newPlaylistName string)
|
|
||||||
|
|
||||||
playlistSelect *widget.Select
|
|
||||||
newPlaylistLabel *widget.Label
|
|
||||||
newPlaylistName *widget.Entry
|
|
||||||
okBtn *widget.Button
|
|
||||||
|
|
||||||
container *fyne.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ fyne.Widget = (*AddToPlaylistDialog)(nil)
|
|
||||||
|
|
||||||
func NewAddToPlaylistDialog(title string, existingPlaylistNames []string, selectedIdx int) *AddToPlaylistDialog {
|
|
||||||
a := &AddToPlaylistDialog{}
|
|
||||||
a.ExtendBaseWidget(a)
|
|
||||||
|
|
||||||
titleLabel := widget.NewLabel(title)
|
|
||||||
titleLabel.TextStyle.Bold = true
|
|
||||||
options := []string{"New playlist..."}
|
|
||||||
options = append(options, existingPlaylistNames...)
|
|
||||||
a.playlistSelect = widget.NewSelect(options, func(_ string) {
|
|
||||||
a.onSelectionChanged()
|
|
||||||
})
|
|
||||||
a.playlistSelect.PlaceHolder = "(Choose playlist)"
|
|
||||||
if selectedIdx >= 0 {
|
|
||||||
// calling SetSelectedIndex before showing the Select crashes...
|
|
||||||
go func() {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
// add 1 to selectedIdx to account for "(Choose playlist)" entry
|
|
||||||
a.playlistSelect.SetSelectedIndex(selectedIdx + 1)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
a.newPlaylistName = widget.NewEntry()
|
|
||||||
a.newPlaylistName.Hidden = true
|
|
||||||
a.newPlaylistName.OnChanged = func(text string) {
|
|
||||||
if len(text) > 0 {
|
|
||||||
a.okBtn.Enable()
|
|
||||||
} else {
|
|
||||||
a.okBtn.Disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a.newPlaylistLabel = widget.NewLabel("Name")
|
|
||||||
a.newPlaylistLabel.Hidden = true
|
|
||||||
|
|
||||||
a.okBtn = widget.NewButton("OK", a.onOK)
|
|
||||||
a.okBtn.Importance = widget.HighImportance
|
|
||||||
a.okBtn.Disable()
|
|
||||||
cancelBtn := widget.NewButton("Cancel", a.onCancel)
|
|
||||||
|
|
||||||
a.container = container.NewVBox(
|
|
||||||
container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()),
|
|
||||||
container.New(layout.NewFormLayout(),
|
|
||||||
widget.NewLabel("Playlist"),
|
|
||||||
a.playlistSelect,
|
|
||||||
a.newPlaylistLabel,
|
|
||||||
a.newPlaylistName),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(layout.NewSpacer(), a.okBtn, cancelBtn))
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AddToPlaylistDialog) onOK() {
|
|
||||||
var newPlaylistName string
|
|
||||||
playlistChoice := -1
|
|
||||||
if sel := a.playlistSelect.SelectedIndex(); sel == 0 {
|
|
||||||
newPlaylistName = a.newPlaylistName.Text
|
|
||||||
} else {
|
|
||||||
playlistChoice = sel - 1
|
|
||||||
}
|
|
||||||
if a.OnSubmit != nil {
|
|
||||||
a.OnSubmit(playlistChoice, newPlaylistName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AddToPlaylistDialog) onSelectionChanged() {
|
|
||||||
if a.playlistSelect.SelectedIndex() == 0 {
|
|
||||||
a.newPlaylistName.Show()
|
|
||||||
a.newPlaylistLabel.Show()
|
|
||||||
if len(a.newPlaylistName.Text) == 0 {
|
|
||||||
a.okBtn.Disable()
|
|
||||||
} else {
|
|
||||||
a.okBtn.Enable()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
a.newPlaylistName.Hide()
|
|
||||||
a.newPlaylistLabel.Hide()
|
|
||||||
a.okBtn.Enable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AddToPlaylistDialog) onCancel() {
|
|
||||||
if a.OnCanceled != nil {
|
|
||||||
a.OnCanceled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AddToPlaylistDialog) MinSize() fyne.Size {
|
|
||||||
a.ExtendBaseWidget(a)
|
|
||||||
return fyne.NewSize(300, a.container.MinSize().Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AddToPlaylistDialog) CreateRenderer() fyne.WidgetRenderer {
|
|
||||||
return widget.NewSimpleRenderer(a.container)
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
package dialogs
|
package dialogs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
)
|
)
|
||||||
@@ -17,18 +14,8 @@ type QuickSearch struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
|
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
|
||||||
|
q := &QuickSearch{mp: mp}
|
||||||
q := &QuickSearch{
|
q.SearchDialog = NewSearchDialog(im, "Quick Search", "Close", q.onSearched)
|
||||||
mp: mp,
|
|
||||||
}
|
|
||||||
|
|
||||||
sd := NewSearchDialog(
|
|
||||||
im,
|
|
||||||
"Quick Search",
|
|
||||||
q.onSearched,
|
|
||||||
q.onUpdateSearchResult,
|
|
||||||
)
|
|
||||||
q.SearchDialog = sd
|
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,53 +31,6 @@ func (q *QuickSearch) onSearched(query string) []*mediaprovider.SearchResult {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QuickSearch) onUpdateSearchResult(sr *searchResult, result *mediaprovider.SearchResult) {
|
|
||||||
|
|
||||||
maybePluralize := func(s string, size int) string {
|
|
||||||
if size != 1 {
|
|
||||||
return s + "s"
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
var secondaryText string
|
|
||||||
switch result.Type {
|
|
||||||
case mediaprovider.ContentTypeAlbum:
|
|
||||||
secondaryText = result.ArtistName
|
|
||||||
case mediaprovider.ContentTypeArtist:
|
|
||||||
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size))
|
|
||||||
case mediaprovider.ContentTypeTrack:
|
|
||||||
secondaryText = result.ArtistName
|
|
||||||
case mediaprovider.ContentTypePlaylist:
|
|
||||||
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("track", result.Size))
|
|
||||||
case mediaprovider.ContentTypeGenre:
|
|
||||||
if result.Size > 0 {
|
|
||||||
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size))
|
|
||||||
} else {
|
|
||||||
secondaryText = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sr.secondary.Segments = []widget.RichTextSegment{
|
|
||||||
&widget.TextSegment{
|
|
||||||
Text: result.Type.String(),
|
|
||||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, TextStyle: fyne.TextStyle{Bold: true}, Inline: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if secondaryText != "" {
|
|
||||||
sr.secondary.Segments = append(sr.secondary.Segments,
|
|
||||||
&widget.TextSegment{
|
|
||||||
Text: " · ",
|
|
||||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
|
||||||
},
|
|
||||||
&widget.TextSegment{
|
|
||||||
Text: secondaryText,
|
|
||||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
sr.secondary.Refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *QuickSearch) SetOnDismiss(onDismiss func()) {
|
func (q *QuickSearch) SetOnDismiss(onDismiss func()) {
|
||||||
q.SearchDialog.OnDismiss = onDismiss
|
q.SearchDialog.OnDismiss = onDismiss
|
||||||
}
|
}
|
||||||
@@ -104,5 +44,5 @@ func (q *QuickSearch) MinSize() fyne.Size {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *QuickSearch) GetSearchEntry() fyne.Focusable {
|
func (q *QuickSearch) GetSearchEntry() fyne.Focusable {
|
||||||
return q.SearchDialog.SearchEntry
|
return q.SearchDialog.GetSearchEntry()
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-56
@@ -1,6 +1,7 @@
|
|||||||
package dialogs
|
package dialogs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -23,30 +24,36 @@ import (
|
|||||||
type SearchDialog struct {
|
type SearchDialog struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
SearchEntry fyne.Focusable // exported so it can be focused by the Controller
|
PlaceholderText string
|
||||||
|
|
||||||
imgSource util.ImageFetcher
|
// Additional item that can be placed to the left
|
||||||
|
// of the dismiss buttons
|
||||||
|
ActionItem fyne.CanvasObject
|
||||||
|
|
||||||
|
OnDismiss func()
|
||||||
|
OnNavigateTo func(mediaprovider.ContentType, string)
|
||||||
|
OnSearched func(string) []*mediaprovider.SearchResult
|
||||||
|
|
||||||
|
imgSource util.ImageFetcher
|
||||||
resultsMutex sync.RWMutex
|
resultsMutex sync.RWMutex
|
||||||
searchResults []*mediaprovider.SearchResult
|
searchResults []*mediaprovider.SearchResult
|
||||||
loadingDots *widgets.LoadingDots
|
|
||||||
list *widget.List
|
|
||||||
selectedIndex int
|
selectedIndex int
|
||||||
|
|
||||||
content *fyne.Container
|
searchEntry *searchEntry
|
||||||
|
loadingDots *widgets.LoadingDots
|
||||||
OnDismiss func()
|
list *widget.List
|
||||||
OnNavigateTo func(mediaprovider.ContentType, string)
|
dialogTitle string
|
||||||
OnSearched func(string) []*mediaprovider.SearchResult
|
dismissText string
|
||||||
OnUpdateSearchResults func(*searchResult, *mediaprovider.SearchResult)
|
content *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onUpdateSearchResult func(*searchResult, *mediaprovider.SearchResult)) *SearchDialog {
|
func NewSearchDialog(im util.ImageFetcher, title, dismissBtn string, onSearched func(string) []*mediaprovider.SearchResult) *SearchDialog {
|
||||||
sd := &SearchDialog{
|
sd := &SearchDialog{
|
||||||
imgSource: im,
|
imgSource: im,
|
||||||
loadingDots: widgets.NewLoadingDots(),
|
loadingDots: widgets.NewLoadingDots(),
|
||||||
OnSearched: onSearched,
|
OnSearched: onSearched,
|
||||||
OnUpdateSearchResults: onUpdateSearchResult,
|
dialogTitle: title,
|
||||||
|
dismissText: dismissBtn,
|
||||||
}
|
}
|
||||||
sd.ExtendBaseWidget(sd)
|
sd.ExtendBaseWidget(sd)
|
||||||
|
|
||||||
@@ -58,7 +65,7 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f
|
|||||||
se.OnTypedDown = sd.moveSelectionDown
|
se.OnTypedDown = sd.moveSelectionDown
|
||||||
se.OnTypedUp = sd.moveSelectionUp
|
se.OnTypedUp = sd.moveSelectionUp
|
||||||
se.OnTypedEscape = sd.onDismiss
|
se.OnTypedEscape = sd.onDismiss
|
||||||
sd.SearchEntry = se
|
sd.searchEntry = se
|
||||||
sd.list = widget.NewList(
|
sd.list = widget.NewList(
|
||||||
func() int {
|
func() int {
|
||||||
sd.resultsMutex.RLock()
|
sd.resultsMutex.RLock()
|
||||||
@@ -75,23 +82,34 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f
|
|||||||
sd.resultsMutex.RUnlock()
|
sd.resultsMutex.RUnlock()
|
||||||
sr := co.(*searchResult)
|
sr := co.(*searchResult)
|
||||||
sr.index = lii
|
sr.index = lii
|
||||||
sd.update(sr, result)
|
sr.Update(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
dismissBtn := widget.NewButton("Close", sd.onDismiss)
|
|
||||||
title := widget.NewRichText(&widget.TextSegment{Text: placeholderTitle, Style: util.BoldRichTextStyle})
|
|
||||||
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
|
||||||
sd.content = container.NewStack(
|
|
||||||
container.NewBorder(
|
|
||||||
container.NewVBox(title, se),
|
|
||||||
container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)),
|
|
||||||
nil, nil, sd.list),
|
|
||||||
container.NewCenter(sd.loadingDots),
|
|
||||||
)
|
|
||||||
return sd
|
return sd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSearchEntry returns the search Entry widget for focusing
|
||||||
|
func (sd *SearchDialog) GetSearchEntry() fyne.Focusable {
|
||||||
|
return sd.searchEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchQuery returns the current search query entered by the user
|
||||||
|
func (sd *SearchDialog) SearchQuery() string {
|
||||||
|
return sd.searchEntry.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) Show() {
|
||||||
|
sd.BaseWidget.Show()
|
||||||
|
go sd.onSearched("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) Refresh() {
|
||||||
|
if sd.PlaceholderText != "" {
|
||||||
|
sd.searchEntry.SetPlaceHolder(sd.PlaceholderText)
|
||||||
|
}
|
||||||
|
sd.BaseWidget.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
func (sd *SearchDialog) onDismiss() {
|
func (sd *SearchDialog) onDismiss() {
|
||||||
if sd.OnDismiss != nil {
|
if sd.OnDismiss != nil {
|
||||||
sd.OnDismiss()
|
sd.OnDismiss()
|
||||||
@@ -131,18 +149,7 @@ func (sd *SearchDialog) moveSelectionUp() {
|
|||||||
sd.list.Select(sd.selectedIndex)
|
sd.list.Select(sd.selectedIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *SearchDialog) onSearched(query string) {
|
func (sd *SearchDialog) setResults(results []*mediaprovider.SearchResult) {
|
||||||
sd.loadingDots.Start()
|
|
||||||
var results []*mediaprovider.SearchResult
|
|
||||||
if query != "" {
|
|
||||||
res := sd.OnSearched(query)
|
|
||||||
if len(res) == 0 {
|
|
||||||
log.Println("No results matched the query.")
|
|
||||||
} else {
|
|
||||||
results = res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sd.loadingDots.Stop()
|
|
||||||
sd.resultsMutex.Lock()
|
sd.resultsMutex.Lock()
|
||||||
sd.searchResults = results
|
sd.searchResults = results
|
||||||
sd.resultsMutex.Unlock()
|
sd.resultsMutex.Unlock()
|
||||||
@@ -152,7 +159,37 @@ func (sd *SearchDialog) onSearched(query string) {
|
|||||||
sd.list.Select(0)
|
sd.list.Select(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sd *SearchDialog) onSearched(query string) {
|
||||||
|
sd.loadingDots.Start()
|
||||||
|
var results []*mediaprovider.SearchResult
|
||||||
|
res := sd.OnSearched(query)
|
||||||
|
if len(res) == 0 {
|
||||||
|
log.Println("No results matched the query.")
|
||||||
|
} else {
|
||||||
|
results = res
|
||||||
|
}
|
||||||
|
sd.loadingDots.Stop()
|
||||||
|
sd.setResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
func (sd *SearchDialog) CreateRenderer() fyne.WidgetRenderer {
|
func (sd *SearchDialog) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
dismissBtn := widget.NewButton(sd.dismissText, sd.onDismiss)
|
||||||
|
title := widget.NewRichText(&widget.TextSegment{Text: sd.dialogTitle, Style: util.BoldRichTextStyle})
|
||||||
|
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
||||||
|
bottomRow := container.NewHBox()
|
||||||
|
if sd.ActionItem != nil {
|
||||||
|
bottomRow.Objects = []fyne.CanvasObject{sd.ActionItem, layout.NewSpacer(), dismissBtn}
|
||||||
|
} else {
|
||||||
|
bottomRow.Objects = []fyne.CanvasObject{layout.NewSpacer(), dismissBtn}
|
||||||
|
}
|
||||||
|
sd.content = container.NewStack(
|
||||||
|
container.NewBorder(
|
||||||
|
container.NewVBox(title, sd.searchEntry),
|
||||||
|
container.NewVBox(widget.NewSeparator(), bottomRow),
|
||||||
|
nil, nil, sd.list),
|
||||||
|
container.NewCenter(sd.loadingDots),
|
||||||
|
)
|
||||||
|
|
||||||
return widget.NewSimpleRenderer(sd.content)
|
return widget.NewSimpleRenderer(sd.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,22 +197,6 @@ func (sd *SearchDialog) MinSize() fyne.Size {
|
|||||||
return fyne.NewSize(400, 350)
|
return fyne.NewSize(400, 350)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *SearchDialog) update(sr *searchResult, result *mediaprovider.SearchResult) {
|
|
||||||
if result == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if sr.contentType == result.Type && sr.id == result.ID {
|
|
||||||
return // nothing to do
|
|
||||||
}
|
|
||||||
sr.id = result.ID
|
|
||||||
sr.contentType = result.Type
|
|
||||||
sr.image.CenterIcon = placeholderIconForContentType(result.Type)
|
|
||||||
sr.imageLoader.Load(result.CoverID)
|
|
||||||
sr.title.SetText(result.Name)
|
|
||||||
|
|
||||||
sd.OnUpdateSearchResults(sr, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func placeholderIconForContentType(c mediaprovider.ContentType) fyne.Resource {
|
func placeholderIconForContentType(c mediaprovider.ContentType) fyne.Resource {
|
||||||
switch c {
|
switch c {
|
||||||
case mediaprovider.ContentTypeAlbum:
|
case mediaprovider.ContentTypeAlbum:
|
||||||
@@ -231,6 +252,64 @@ func newSearchResult(parent *SearchDialog) *searchResult {
|
|||||||
return qs
|
return qs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *searchResult) Update(result *mediaprovider.SearchResult) {
|
||||||
|
if result == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.contentType == result.Type && s.id == result.ID && s.title.Text == result.Name {
|
||||||
|
return // nothing to do
|
||||||
|
}
|
||||||
|
s.id = result.ID
|
||||||
|
s.contentType = result.Type
|
||||||
|
s.image.CenterIcon = placeholderIconForContentType(result.Type)
|
||||||
|
s.imageLoader.Load(result.CoverID)
|
||||||
|
s.title.SetText(result.Name)
|
||||||
|
|
||||||
|
maybePluralize := func(s string, size int) string {
|
||||||
|
if size != 1 {
|
||||||
|
return s + "s"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
var secondaryText string
|
||||||
|
switch result.Type {
|
||||||
|
case mediaprovider.ContentTypeAlbum:
|
||||||
|
secondaryText = result.ArtistName
|
||||||
|
case mediaprovider.ContentTypeArtist:
|
||||||
|
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size))
|
||||||
|
case mediaprovider.ContentTypeTrack:
|
||||||
|
secondaryText = result.ArtistName
|
||||||
|
case mediaprovider.ContentTypePlaylist:
|
||||||
|
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("track", result.Size))
|
||||||
|
case mediaprovider.ContentTypeGenre:
|
||||||
|
if result.Size > 0 {
|
||||||
|
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size))
|
||||||
|
} else {
|
||||||
|
secondaryText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.secondary.Segments = []widget.RichTextSegment{
|
||||||
|
&widget.TextSegment{
|
||||||
|
Text: result.Type.String(),
|
||||||
|
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, TextStyle: fyne.TextStyle{Bold: true}, Inline: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if secondaryText != "" {
|
||||||
|
s.secondary.Segments = append(s.secondary.Segments,
|
||||||
|
&widget.TextSegment{
|
||||||
|
Text: " · ",
|
||||||
|
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
||||||
|
},
|
||||||
|
&widget.TextSegment{
|
||||||
|
Text: secondaryText,
|
||||||
|
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.secondary.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
func (q *searchResult) Tapped(_ *fyne.PointEvent) {
|
func (q *searchResult) Tapped(_ *fyne.PointEvent) {
|
||||||
q.parent.onSelected(q.index)
|
q.parent.onSelected(q.index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package dialogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "fmt"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/data/binding"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"github.com/deluan/sanitize"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectPlaylist struct {
|
||||||
|
SearchDialog *SearchDialog
|
||||||
|
mp mediaprovider.MediaProvider
|
||||||
|
loggedInUser string
|
||||||
|
allPlaylistResuts []*mediaprovider.SearchResult
|
||||||
|
SkipDuplicates bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetcher, loggedInUser string) *SelectPlaylist {
|
||||||
|
sp := &SelectPlaylist{
|
||||||
|
mp: mp,
|
||||||
|
loggedInUser: loggedInUser,
|
||||||
|
SkipDuplicates: false,
|
||||||
|
}
|
||||||
|
sd := NewSearchDialog(
|
||||||
|
im,
|
||||||
|
"Add to playlist",
|
||||||
|
"Cancel",
|
||||||
|
sp.onSearched,
|
||||||
|
)
|
||||||
|
sd.ActionItem = widget.NewCheckWithData("Skip duplicate tracks", binding.BindBool(&sp.SkipDuplicates))
|
||||||
|
sd.PlaceholderText = "Search playlists or new playlist name"
|
||||||
|
sp.SearchDialog = sd
|
||||||
|
return sp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *SelectPlaylist) fetchUserOwnedPlaylists() {
|
||||||
|
playlists, err := sp.mp.GetPlaylists()
|
||||||
|
if err != nil {
|
||||||
|
// TODO: surface this error to user
|
||||||
|
log.Printf("error getting playlists: %s", err.Error())
|
||||||
|
}
|
||||||
|
userPlaylists := sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool {
|
||||||
|
return playlist.Owner == sp.loggedInUser
|
||||||
|
})
|
||||||
|
sp.allPlaylistResuts = sharedutil.MapSlice(userPlaylists, sp.playlistToSearchResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *SelectPlaylist) playlistToSearchResult(playlist *mediaprovider.Playlist) *mediaprovider.SearchResult {
|
||||||
|
if playlist == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &mediaprovider.SearchResult{
|
||||||
|
Name: playlist.Name,
|
||||||
|
ID: playlist.ID,
|
||||||
|
CoverID: playlist.CoverArtID,
|
||||||
|
Type: mediaprovider.ContentTypePlaylist,
|
||||||
|
Size: playlist.TrackCount,
|
||||||
|
ArtistName: playlist.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult {
|
||||||
|
if sp.allPlaylistResuts == nil {
|
||||||
|
sp.fetchUserOwnedPlaylists()
|
||||||
|
}
|
||||||
|
var results []*mediaprovider.SearchResult
|
||||||
|
if query == "" {
|
||||||
|
results = sp.allPlaylistResuts
|
||||||
|
} else {
|
||||||
|
results = sharedutil.FilterSlice(sp.allPlaylistResuts, func(playlist *mediaprovider.SearchResult) bool {
|
||||||
|
return strings.Contains(
|
||||||
|
sanitize.Accents(strings.ToLower(playlist.Name)),
|
||||||
|
sanitize.Accents(strings.ToLower(query)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
results = append(results, &mediaprovider.SearchResult{
|
||||||
|
Name: fmt.Sprintf("Create new playlist: %s", query),
|
||||||
|
Type: mediaprovider.ContentTypePlaylist,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *SelectPlaylist) SetOnDismiss(onDismiss func()) {
|
||||||
|
sp.SearchDialog.OnDismiss = onDismiss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *SelectPlaylist) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string)) {
|
||||||
|
sp.SearchDialog.OnNavigateTo = onNavigateTo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *SelectPlaylist) MinSize() fyne.Size {
|
||||||
|
return sp.SearchDialog.MinSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *SelectPlaylist) GetSearchEntry() fyne.Focusable {
|
||||||
|
return sp.SearchDialog.GetSearchEntry()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user