add context menu actions to search dialog, refactor controller
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/sharedutil"
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([]*mediaprovider.SearchResult, error) {
|
func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([]*mediaprovider.SearchResult, error) {
|
||||||
limit := maxResults / 3
|
limit := maxResults / 3
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var albums []*jellyfin.Album
|
var albums []*jellyfin.Album
|
||||||
@@ -22,19 +22,19 @@ func (s *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
albumResult, _ := s.client.Search(searchQuery, jellyfin.TypeAlbum, jellyfin.Paging{Limit: limit})
|
albumResult, _ := j.client.Search(searchQuery, jellyfin.TypeAlbum, jellyfin.Paging{Limit: limit})
|
||||||
albums = albumResult.Albums
|
albums = albumResult.Albums
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
artistResult, _ := s.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{Limit: limit})
|
artistResult, _ := j.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{Limit: limit})
|
||||||
artists = artistResult.Artists
|
artists = artistResult.Artists
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
songResult, _ := s.client.Search(searchQuery, jellyfin.TypeSong, jellyfin.Paging{Limit: limit})
|
songResult, _ := j.client.Search(searchQuery, jellyfin.TypeSong, jellyfin.Paging{Limit: limit})
|
||||||
songs = songResult.Songs
|
songs = songResult.Songs
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
@@ -44,7 +44,7 @@ func (s *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
p, e := s.client.GetPlaylists()
|
p, e := j.client.GetPlaylists()
|
||||||
if e == nil {
|
if e == nil {
|
||||||
playlists = sharedutil.FilterSlice(p, func(p *jellyfin.Playlist) bool {
|
playlists = sharedutil.FilterSlice(p, func(p *jellyfin.Playlist) bool {
|
||||||
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(p.Name)), queryLowerWords)
|
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(p.Name)), queryLowerWords)
|
||||||
@@ -55,7 +55,7 @@ func (s *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
g, e := s.client.GetGenres(jellyfin.Paging{})
|
g, e := j.client.GetGenres(jellyfin.Paging{})
|
||||||
if e == nil {
|
if e == nil {
|
||||||
genres = sharedutil.FilterSlice(g, func(g jellyfin.NameID) bool {
|
genres = sharedutil.FilterSlice(g, func(g jellyfin.NameID) bool {
|
||||||
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(g.Name)), queryLowerWords)
|
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(g.Name)), queryLowerWords)
|
||||||
@@ -66,13 +66,13 @@ func (s *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
results := mergeResults(albums, artists, songs, playlists, genres)
|
results := j.mergeResults(albums, artists, songs, playlists, genres)
|
||||||
helpers.RankSearchResults(results, searchQuery, queryLowerWords)
|
helpers.RankSearchResults(results, searchQuery, queryLowerWords)
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeResults(
|
func (j *jellyfinMediaProvider) mergeResults(
|
||||||
albums []*jellyfin.Album,
|
albums []*jellyfin.Album,
|
||||||
artists []*jellyfin.Artist,
|
artists []*jellyfin.Artist,
|
||||||
songs []*jellyfin.Song,
|
songs []*jellyfin.Song,
|
||||||
@@ -93,6 +93,7 @@ func mergeResults(
|
|||||||
Name: al.Name,
|
Name: al.Name,
|
||||||
ArtistName: strings.Join(sharedutil.MapSlice(al.Artists, getArtistNames), ","),
|
ArtistName: strings.Join(sharedutil.MapSlice(al.Artists, getArtistNames), ","),
|
||||||
Size: al.ChildCount,
|
Size: al.ChildCount,
|
||||||
|
Item: toAlbum(al),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ func mergeResults(
|
|||||||
CoverID: ar.ID,
|
CoverID: ar.ID,
|
||||||
Name: ar.Name,
|
Name: ar.Name,
|
||||||
Size: ar.AlbumCount,
|
Size: ar.AlbumCount,
|
||||||
|
Item: toArtist(ar),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ func mergeResults(
|
|||||||
Name: tr.Name,
|
Name: tr.Name,
|
||||||
ArtistName: strings.Join(sharedutil.MapSlice(tr.Artists, getArtistNames), ","),
|
ArtistName: strings.Join(sharedutil.MapSlice(tr.Artists, getArtistNames), ","),
|
||||||
Size: int(tr.RunTimeTicks / 10_000_000),
|
Size: int(tr.RunTimeTicks / 10_000_000),
|
||||||
|
Item: toTrack(tr),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +127,7 @@ func mergeResults(
|
|||||||
CoverID: pl.ID,
|
CoverID: pl.ID,
|
||||||
Name: pl.Name,
|
Name: pl.Name,
|
||||||
Size: pl.SongCount,
|
Size: pl.SongCount,
|
||||||
|
Item: j.toPlaylist(pl),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,4 +279,8 @@ type SearchResult struct {
|
|||||||
|
|
||||||
// Unset for ContentTypes Artist, Playlist, Genre, and RadioStation
|
// Unset for ContentTypes Artist, Playlist, Genre, and RadioStation
|
||||||
ArtistName string
|
ArtistName string
|
||||||
|
|
||||||
|
// The actual item corresponding to this search result
|
||||||
|
// *mediaprovider.Artist for ContentTypeArtist, etc
|
||||||
|
Item any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ func mergeResults(
|
|||||||
Name: al.Name,
|
Name: al.Name,
|
||||||
ArtistName: getNameString(al.Artist, al.Artists),
|
ArtistName: getNameString(al.Artist, al.Artists),
|
||||||
Size: al.SongCount,
|
Size: al.SongCount,
|
||||||
|
Item: toAlbum(al),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +112,7 @@ func mergeResults(
|
|||||||
CoverID: ar.CoverArt,
|
CoverID: ar.CoverArt,
|
||||||
Name: ar.Name,
|
Name: ar.Name,
|
||||||
Size: ar.AlbumCount,
|
Size: ar.AlbumCount,
|
||||||
|
Item: toArtistFromID3(ar),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +124,7 @@ func mergeResults(
|
|||||||
Name: tr.Title,
|
Name: tr.Title,
|
||||||
ArtistName: getNameString(tr.Artist, tr.Artists),
|
ArtistName: getNameString(tr.Artist, tr.Artists),
|
||||||
Size: tr.Duration,
|
Size: tr.Duration,
|
||||||
|
Item: toTrack(tr),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +135,7 @@ func mergeResults(
|
|||||||
CoverID: pl.CoverArt,
|
CoverID: pl.CoverArt,
|
||||||
Name: pl.Name,
|
Name: pl.Name,
|
||||||
Size: pl.SongCount,
|
Size: pl.SongCount,
|
||||||
|
Item: toPlaylist(pl),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +153,7 @@ func mergeResults(
|
|||||||
Type: mediaprovider.ContentTypeRadioStation,
|
Type: mediaprovider.ContentTypeRadioStation,
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
|
Item: r,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
@@ -11,7 +10,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,7 +28,6 @@ import (
|
|||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/lang"
|
"fyne.io/fyne/v2/lang"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -260,327 +257,6 @@ func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Controller) PromptForFirstServer() {
|
|
||||||
d := dialogs.NewAddEditServerDialog(lang.L("Connect to Server"), false, nil, m.MainWindow.Canvas().Focus)
|
|
||||||
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
|
||||||
d.OnSubmit = func() {
|
|
||||||
d.DisableSubmit()
|
|
||||||
go func() {
|
|
||||||
if m.testConnectionAndUpdateDialogText(d) {
|
|
||||||
// connection is good
|
|
||||||
pop.Hide()
|
|
||||||
m.doModalClosed()
|
|
||||||
conn := backend.ServerConnection{
|
|
||||||
ServerType: d.ServerType,
|
|
||||||
Hostname: d.Host,
|
|
||||||
AltHostname: d.AltHost,
|
|
||||||
Username: d.Username,
|
|
||||||
LegacyAuth: d.LegacyAuth,
|
|
||||||
}
|
|
||||||
server := m.App.ServerManager.AddServer(d.Nickname, conn)
|
|
||||||
if err := m.trySetPasswordAndConnectToServer(server, d.Password); err != nil {
|
|
||||||
log.Printf("error connecting to server: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.EnableSubmit()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
m.haveModal = true
|
|
||||||
pop.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show dialog to select playlist.
|
|
||||||
// Depending on the results of that dialog, potentially create a new playlist
|
|
||||||
// Add tracks to the user-specified playlist
|
|
||||||
func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
|
||||||
sp := dialogs.NewSelectPlaylistDialog(m.App.ServerManager.Server, m.App.ImageManager,
|
|
||||||
m.App.ServerManager.LoggedInUser, m.App.Config.Application.AddToPlaylistSkipDuplicates)
|
|
||||||
pop := widget.NewModalPopUp(sp.SearchDialog, m.MainWindow.Canvas())
|
|
||||||
sp.SetOnDismiss(func() {
|
|
||||||
pop.Hide()
|
|
||||||
m.doModalClosed()
|
|
||||||
})
|
|
||||||
sp.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) {
|
|
||||||
notifySuccess := func(n int) {
|
|
||||||
fyne.Do(func() {
|
|
||||||
msg := lang.LocalizePluralKey("playlist.addedtracks",
|
|
||||||
"Added tracks to playlist", n, map[string]string{"trackCount": strconv.Itoa(n)})
|
|
||||||
m.ToastProvider.ShowSuccessToast(msg)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
notifyError := func() {
|
|
||||||
fyne.Do(func() {
|
|
||||||
m.ToastProvider.ShowErrorToast(
|
|
||||||
lang.L("An error occurred adding tracks to the playlist"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pop.Hide()
|
|
||||||
m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates
|
|
||||||
if id == "" /* creating new playlist */ {
|
|
||||||
go func() {
|
|
||||||
err := m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs)
|
|
||||||
if err == nil {
|
|
||||||
notifySuccess(len(trackIDs))
|
|
||||||
} else {
|
|
||||||
log.Printf("error adding tracks to playlist: %s", err.Error())
|
|
||||||
notifyError()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
m.App.Config.Application.DefaultPlaylistID = id
|
|
||||||
if sp.SkipDuplicates {
|
|
||||||
go func() {
|
|
||||||
currentTrackIDs := make(map[string]struct{})
|
|
||||||
if selectedPlaylist, err := m.App.ServerManager.Server.GetPlaylist(id); err != nil {
|
|
||||||
log.Printf("error getting playlist: %s", err.Error())
|
|
||||||
notifyError()
|
|
||||||
} else {
|
|
||||||
for _, track := range selectedPlaylist.Tracks {
|
|
||||||
currentTrackIDs[track.ID] = struct{}{}
|
|
||||||
}
|
|
||||||
filterTrackIDs := sharedutil.FilterSlice(trackIDs, func(trackID string) bool {
|
|
||||||
_, ok := currentTrackIDs[trackID]
|
|
||||||
return !ok
|
|
||||||
})
|
|
||||||
err := m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs)
|
|
||||||
if err == nil {
|
|
||||||
notifySuccess(len(filterTrackIDs))
|
|
||||||
} else {
|
|
||||||
log.Printf("error adding tracks to playlist: %s", err.Error())
|
|
||||||
notifyError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
go func() {
|
|
||||||
err := m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs)
|
|
||||||
if err == nil {
|
|
||||||
notifySuccess(len(trackIDs))
|
|
||||||
} else {
|
|
||||||
log.Printf("error adding tracks to playlist: %s", err.Error())
|
|
||||||
notifyError()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
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) {
|
|
||||||
canMakePublic := m.App.ServerManager.Server.CanMakePublicPlaylist()
|
|
||||||
dlg := dialogs.NewEditPlaylistDialog(playlist, canMakePublic)
|
|
||||||
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
|
||||||
m.ClosePopUpOnEscape(pop)
|
|
||||||
dlg.OnCanceled = func() {
|
|
||||||
pop.Hide()
|
|
||||||
m.doModalClosed()
|
|
||||||
}
|
|
||||||
dlg.OnDeletePlaylist = func() {
|
|
||||||
pop.Hide()
|
|
||||||
dialog.ShowCustomConfirm(lang.L("Confirm Delete Playlist"), lang.L("OK"), lang.L("Cancel"), layout.NewSpacer(), /*custom content*/
|
|
||||||
func(ok bool) {
|
|
||||||
if !ok {
|
|
||||||
pop.Show()
|
|
||||||
} else {
|
|
||||||
m.doModalClosed()
|
|
||||||
go func() {
|
|
||||||
if err := m.App.ServerManager.Server.DeletePlaylist(playlist.ID); err != nil {
|
|
||||||
log.Printf("error deleting playlist: %s", err.Error())
|
|
||||||
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
|
||||||
// navigate to playlists page if user is still on the page of the deleted playlist
|
|
||||||
fyne.Do(func() { m.NavigateTo(PlaylistsRoute()) })
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}, m.MainWindow)
|
|
||||||
}
|
|
||||||
dlg.OnUpdateMetadata = func() {
|
|
||||||
pop.Hide()
|
|
||||||
m.doModalClosed()
|
|
||||||
go func() {
|
|
||||||
err := m.App.ServerManager.Server.EditPlaylist(playlist.ID, dlg.Name, dlg.Description, dlg.IsPublic)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error updating playlist: %s", err.Error())
|
|
||||||
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
|
||||||
// if user is on playlist page, reload to get the updates
|
|
||||||
fyne.Do(m.ReloadFunc)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
m.haveModal = true
|
|
||||||
pop.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoConnectToServerWorkflow does the workflow for connecting to the last active server on startup
|
|
||||||
func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) {
|
|
||||||
pass, err := c.App.ServerManager.GetServerPassword(server.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error getting password from keyring: %v", err)
|
|
||||||
c.PromptForLoginAndConnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// try connecting to last used server - set up cancelable modal dialog
|
|
||||||
canceled := false
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
dlg := dialog.NewCustom(lang.L("Connecting"), lang.L("Cancel"),
|
|
||||||
widget.NewLabel(fmt.Sprintf(lang.L("Connecting to")+" %s", server.Nickname)), c.MainWindow)
|
|
||||||
dlg.SetOnClosed(func() {
|
|
||||||
canceled = true
|
|
||||||
cancel()
|
|
||||||
})
|
|
||||||
c.haveModal = true
|
|
||||||
dlg.Show()
|
|
||||||
|
|
||||||
// try to connect
|
|
||||||
go func() {
|
|
||||||
defer cancel() // make sure to free up ctx resources if user does not cancel
|
|
||||||
|
|
||||||
if err := c.tryConnectToServer(ctx, server, pass); err != nil {
|
|
||||||
fyne.Do(func() {
|
|
||||||
dlg.Hide()
|
|
||||||
c.haveModal = false
|
|
||||||
if canceled {
|
|
||||||
c.PromptForLoginAndConnect()
|
|
||||||
} else {
|
|
||||||
// connection failure
|
|
||||||
dlg := dialog.NewError(err, c.MainWindow)
|
|
||||||
dlg.SetOnClosed(func() {
|
|
||||||
c.PromptForLoginAndConnect()
|
|
||||||
})
|
|
||||||
c.haveModal = true
|
|
||||||
dlg.Show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
fyne.Do(func() {
|
|
||||||
dlg.Hide()
|
|
||||||
c.haveModal = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Controller) PromptForLoginAndConnect() {
|
|
||||||
d := dialogs.NewLoginDialog(m.App.Config.Servers, m.App.ServerManager.GetServerPassword)
|
|
||||||
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
|
||||||
d.OnSubmit = func(server *backend.ServerConfig, password string) {
|
|
||||||
d.DisableSubmit()
|
|
||||||
d.SetInfoText(lang.L("Testing connection") + "...")
|
|
||||||
go func() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := m.App.ServerManager.TestConnectionAndAuth(ctx, server.ServerConnection, password)
|
|
||||||
fyne.Do(func() {
|
|
||||||
if err == backend.ErrUnreachable {
|
|
||||||
d.SetErrorText(lang.L("Server unreachable"))
|
|
||||||
} else if err != nil {
|
|
||||||
d.SetErrorText(lang.L("Authentication failed"))
|
|
||||||
} else {
|
|
||||||
pop.Hide()
|
|
||||||
m.trySetPasswordAndConnectToServer(server, password)
|
|
||||||
m.doModalClosed()
|
|
||||||
}
|
|
||||||
d.EnableSubmit()
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
d.OnEditServer = func(server *backend.ServerConfig) {
|
|
||||||
pop.Hide()
|
|
||||||
editD := dialogs.NewAddEditServerDialog(lang.L("Edit server"), true, server, m.MainWindow.Canvas().Focus)
|
|
||||||
editPop := widget.NewModalPopUp(editD, m.MainWindow.Canvas())
|
|
||||||
editD.OnSubmit = func() {
|
|
||||||
d.DisableSubmit()
|
|
||||||
go func() {
|
|
||||||
success := m.testConnectionAndUpdateDialogText(editD)
|
|
||||||
fyne.Do(func() {
|
|
||||||
if success {
|
|
||||||
// connection is good
|
|
||||||
editPop.Hide()
|
|
||||||
server.Hostname = editD.Host
|
|
||||||
server.AltHostname = editD.AltHost
|
|
||||||
server.Nickname = editD.Nickname
|
|
||||||
server.Username = editD.Username
|
|
||||||
server.LegacyAuth = editD.LegacyAuth
|
|
||||||
m.trySetPasswordAndConnectToServer(server, editD.Password)
|
|
||||||
m.doModalClosed()
|
|
||||||
}
|
|
||||||
d.EnableSubmit()
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
editD.OnCancel = func() {
|
|
||||||
editPop.Hide()
|
|
||||||
pop.Show()
|
|
||||||
}
|
|
||||||
editPop.Show()
|
|
||||||
}
|
|
||||||
d.OnNewServer = func() {
|
|
||||||
pop.Hide()
|
|
||||||
newD := dialogs.NewAddEditServerDialog(lang.L("Add Server"), true, nil, m.MainWindow.Canvas().Focus)
|
|
||||||
newPop := widget.NewModalPopUp(newD, m.MainWindow.Canvas())
|
|
||||||
newD.OnSubmit = func() {
|
|
||||||
d.DisableSubmit()
|
|
||||||
go func() {
|
|
||||||
success := m.testConnectionAndUpdateDialogText(newD)
|
|
||||||
fyne.Do(func() {
|
|
||||||
if success {
|
|
||||||
// connection is good
|
|
||||||
newPop.Hide()
|
|
||||||
conn := backend.ServerConnection{
|
|
||||||
ServerType: newD.ServerType,
|
|
||||||
Hostname: newD.Host,
|
|
||||||
AltHostname: newD.AltHost,
|
|
||||||
Username: newD.Username,
|
|
||||||
LegacyAuth: newD.LegacyAuth,
|
|
||||||
}
|
|
||||||
server := m.App.ServerManager.AddServer(newD.Nickname, conn)
|
|
||||||
m.trySetPasswordAndConnectToServer(server, newD.Password)
|
|
||||||
m.doModalClosed()
|
|
||||||
}
|
|
||||||
d.EnableSubmit()
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
newD.OnCancel = func() {
|
|
||||||
newPop.Hide()
|
|
||||||
pop.Show()
|
|
||||||
}
|
|
||||||
newPop.Show()
|
|
||||||
}
|
|
||||||
d.OnDeleteServer = func(server *backend.ServerConfig) {
|
|
||||||
pop.Hide()
|
|
||||||
dialog.ShowConfirm(lang.L("Confirm Delete Server"),
|
|
||||||
fmt.Sprintf(lang.L("Are you sure you want to delete the server")+" %q?", server.Nickname),
|
|
||||||
func(ok bool) {
|
|
||||||
if ok {
|
|
||||||
m.App.ServerManager.DeleteServer(server.ID)
|
|
||||||
m.App.DeleteServerCacheDir(server.ID)
|
|
||||||
d.SetServers(m.App.Config.Servers)
|
|
||||||
}
|
|
||||||
if len(m.App.Config.Servers) == 0 {
|
|
||||||
m.PromptForFirstServer()
|
|
||||||
} else {
|
|
||||||
pop.Show()
|
|
||||||
}
|
|
||||||
}, m.MainWindow)
|
|
||||||
|
|
||||||
}
|
|
||||||
m.haveModal = true
|
|
||||||
pop.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) ShowAboutDialog() {
|
func (c *Controller) ShowAboutDialog() {
|
||||||
dlg := dialogs.NewAboutDialog(c.AppVersion)
|
dlg := dialogs.NewAboutDialog(c.AppVersion)
|
||||||
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
||||||
@@ -643,91 +319,6 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
|||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ShowQuickSearch() {
|
|
||||||
qs := dialogs.NewQuickSearch(c.App.ServerManager.Server, c.App.ImageManager)
|
|
||||||
pop := widget.NewModalPopUp(qs.SearchDialog, c.MainWindow.Canvas())
|
|
||||||
qs.SetOnDismiss(func() {
|
|
||||||
pop.Hide()
|
|
||||||
c.doModalClosed()
|
|
||||||
})
|
|
||||||
qs.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) {
|
|
||||||
pop.Hide()
|
|
||||||
c.doModalClosed()
|
|
||||||
switch contentType {
|
|
||||||
case mediaprovider.ContentTypeAlbum:
|
|
||||||
c.NavigateTo(AlbumRoute(id))
|
|
||||||
case mediaprovider.ContentTypeArtist:
|
|
||||||
c.NavigateTo(ArtistRoute(id))
|
|
||||||
case mediaprovider.ContentTypeTrack:
|
|
||||||
go c.App.PlaybackManager.PlayTrack(id)
|
|
||||||
case mediaprovider.ContentTypePlaylist:
|
|
||||||
c.NavigateTo(PlaylistRoute(id))
|
|
||||||
case mediaprovider.ContentTypeGenre:
|
|
||||||
c.NavigateTo(GenreRoute(id))
|
|
||||||
case mediaprovider.ContentTypeRadioStation:
|
|
||||||
if rp, ok := c.App.ServerManager.Server.(mediaprovider.RadioProvider); ok {
|
|
||||||
if radio, err := rp.GetRadioStation(id); err == nil {
|
|
||||||
go c.App.PlaybackManager.PlayRadioStation(radio)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
c.ClosePopUpOnEscape(pop)
|
|
||||||
c.haveModal = true
|
|
||||||
min := qs.MinSize()
|
|
||||||
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.Show()
|
|
||||||
c.MainWindow.Canvas().Focus(qs.GetSearchEntry())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error {
|
|
||||||
if err := c.App.ServerManager.SetServerPassword(server, password); err != nil {
|
|
||||||
log.Printf("error setting keyring credentials: %v", err)
|
|
||||||
// Don't return an error; fall back to just using the password in-memory
|
|
||||||
// User will need to log in with the password on subsequent runs.
|
|
||||||
}
|
|
||||||
return c.tryConnectToServer(context.Background(), server, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to connect to the given server, with the configured timeout added to the context
|
|
||||||
func (c *Controller) tryConnectToServer(ctx context.Context, server *backend.ServerConfig, password string) error {
|
|
||||||
timeout := time.Duration(c.App.Config.Application.RequestTimeoutSeconds) * time.Second
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
if err := c.App.ServerManager.TestConnectionAndAuth(ctx, server.ServerConnection, password); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.App.ServerManager.ConnectToServer(server, password); err != nil {
|
|
||||||
log.Printf("error connecting to server: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServerDialog) bool {
|
|
||||||
dlg.SetInfoText(lang.L("Testing connection") + "...")
|
|
||||||
conn := backend.ServerConnection{
|
|
||||||
ServerType: dlg.ServerType,
|
|
||||||
Hostname: dlg.Host,
|
|
||||||
AltHostname: dlg.AltHost,
|
|
||||||
Username: dlg.Username,
|
|
||||||
LegacyAuth: dlg.LegacyAuth,
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
err := c.App.ServerManager.TestConnectionAndAuth(ctx, conn, dlg.Password)
|
|
||||||
if err == backend.ErrUnreachable {
|
|
||||||
dlg.SetErrorText(lang.L("Could not reach server") + fmt.Sprintf(" (%s?)", lang.L("wrong URL")))
|
|
||||||
return false
|
|
||||||
} else if err != nil {
|
|
||||||
dlg.SetErrorText(lang.L("Authentication failed") + fmt.Sprintf(" (%s)", lang.L("wrong username/password")))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) doModalClosed() {
|
func (c *Controller) doModalClosed() {
|
||||||
c.haveModal = false
|
c.haveModal = false
|
||||||
if c.runOnModalClosed != nil {
|
if c.runOnModalClosed != nil {
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/lang"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
"github.com/dweymouth/supersonic/ui/dialogs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Show dialog to select playlist.
|
||||||
|
// Depending on the results of that dialog, potentially create a new playlist
|
||||||
|
// Add tracks to the user-specified playlist
|
||||||
|
func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
||||||
|
sp := dialogs.NewSelectPlaylistDialog(m.App.ServerManager.Server, m.App.ImageManager,
|
||||||
|
m.App.ServerManager.LoggedInUser, m.App.Config.Application.AddToPlaylistSkipDuplicates)
|
||||||
|
pop := widget.NewModalPopUp(sp.SearchDialog, m.MainWindow.Canvas())
|
||||||
|
sp.SetOnDismiss(func() {
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
})
|
||||||
|
sp.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) {
|
||||||
|
notifySuccess := func(n int) {
|
||||||
|
fyne.Do(func() {
|
||||||
|
msg := lang.LocalizePluralKey("playlist.addedtracks",
|
||||||
|
"Added tracks to playlist", n, map[string]string{"trackCount": strconv.Itoa(n)})
|
||||||
|
m.ToastProvider.ShowSuccessToast(msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
notifyError := func() {
|
||||||
|
fyne.Do(func() {
|
||||||
|
m.ToastProvider.ShowErrorToast(
|
||||||
|
lang.L("An error occurred adding tracks to the playlist"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pop.Hide()
|
||||||
|
m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates
|
||||||
|
if id == "" /* creating new playlist */ {
|
||||||
|
go func() {
|
||||||
|
err := m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs)
|
||||||
|
if err == nil {
|
||||||
|
notifySuccess(len(trackIDs))
|
||||||
|
} else {
|
||||||
|
log.Printf("error adding tracks to playlist: %s", err.Error())
|
||||||
|
notifyError()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
m.App.Config.Application.DefaultPlaylistID = id
|
||||||
|
if sp.SkipDuplicates {
|
||||||
|
go func() {
|
||||||
|
currentTrackIDs := make(map[string]struct{})
|
||||||
|
if selectedPlaylist, err := m.App.ServerManager.Server.GetPlaylist(id); err != nil {
|
||||||
|
log.Printf("error getting playlist: %s", err.Error())
|
||||||
|
notifyError()
|
||||||
|
} else {
|
||||||
|
for _, track := range selectedPlaylist.Tracks {
|
||||||
|
currentTrackIDs[track.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
filterTrackIDs := sharedutil.FilterSlice(trackIDs, func(trackID string) bool {
|
||||||
|
_, ok := currentTrackIDs[trackID]
|
||||||
|
return !ok
|
||||||
|
})
|
||||||
|
err := m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs)
|
||||||
|
if err == nil {
|
||||||
|
notifySuccess(len(filterTrackIDs))
|
||||||
|
} else {
|
||||||
|
log.Printf("error adding tracks to playlist: %s", err.Error())
|
||||||
|
notifyError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
go func() {
|
||||||
|
err := m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs)
|
||||||
|
if err == nil {
|
||||||
|
notifySuccess(len(trackIDs))
|
||||||
|
} else {
|
||||||
|
log.Printf("error adding tracks to playlist: %s", err.Error())
|
||||||
|
notifyError()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
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) {
|
||||||
|
canMakePublic := m.App.ServerManager.Server.CanMakePublicPlaylist()
|
||||||
|
dlg := dialogs.NewEditPlaylistDialog(playlist, canMakePublic)
|
||||||
|
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
||||||
|
m.ClosePopUpOnEscape(pop)
|
||||||
|
dlg.OnCanceled = func() {
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
}
|
||||||
|
dlg.OnDeletePlaylist = func() {
|
||||||
|
pop.Hide()
|
||||||
|
dialog.ShowCustomConfirm(lang.L("Confirm Delete Playlist"), lang.L("OK"), lang.L("Cancel"), layout.NewSpacer(), /*custom content*/
|
||||||
|
func(ok bool) {
|
||||||
|
if !ok {
|
||||||
|
pop.Show()
|
||||||
|
} else {
|
||||||
|
m.doModalClosed()
|
||||||
|
go func() {
|
||||||
|
if err := m.App.ServerManager.Server.DeletePlaylist(playlist.ID); err != nil {
|
||||||
|
log.Printf("error deleting playlist: %s", err.Error())
|
||||||
|
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
||||||
|
// navigate to playlists page if user is still on the page of the deleted playlist
|
||||||
|
fyne.Do(func() { m.NavigateTo(PlaylistsRoute()) })
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}, m.MainWindow)
|
||||||
|
}
|
||||||
|
dlg.OnUpdateMetadata = func() {
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
go func() {
|
||||||
|
err := m.App.ServerManager.Server.EditPlaylist(playlist.ID, dlg.Name, dlg.Description, dlg.IsPublic)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error updating playlist: %s", err.Error())
|
||||||
|
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
||||||
|
// if user is on playlist page, reload to get the updates
|
||||||
|
fyne.Do(m.ReloadFunc)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
m.haveModal = true
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"github.com/dweymouth/supersonic/backend"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
"github.com/dweymouth/supersonic/ui/dialogs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Controller) ShowQuickSearch() {
|
||||||
|
qs := dialogs.NewQuickSearch(c.App.ServerManager.Server, c.App.ImageManager)
|
||||||
|
pop := widget.NewModalPopUp(qs.SearchDialog, c.MainWindow.Canvas())
|
||||||
|
qs.SetOnDismiss(func() {
|
||||||
|
pop.Hide()
|
||||||
|
c.doModalClosed()
|
||||||
|
})
|
||||||
|
|
||||||
|
qs.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) {
|
||||||
|
pop.Hide()
|
||||||
|
c.doModalClosed()
|
||||||
|
switch contentType {
|
||||||
|
case mediaprovider.ContentTypeAlbum:
|
||||||
|
c.NavigateTo(AlbumRoute(id))
|
||||||
|
case mediaprovider.ContentTypeArtist:
|
||||||
|
c.NavigateTo(ArtistRoute(id))
|
||||||
|
case mediaprovider.ContentTypeTrack:
|
||||||
|
go c.App.PlaybackManager.PlayTrack(id)
|
||||||
|
case mediaprovider.ContentTypePlaylist:
|
||||||
|
c.NavigateTo(PlaylistRoute(id))
|
||||||
|
case mediaprovider.ContentTypeGenre:
|
||||||
|
c.NavigateTo(GenreRoute(id))
|
||||||
|
case mediaprovider.ContentTypeRadioStation:
|
||||||
|
if rp, ok := c.App.ServerManager.Server.(mediaprovider.RadioProvider); ok {
|
||||||
|
if radio, err := rp.GetRadioStation(id); err == nil {
|
||||||
|
go c.App.PlaybackManager.PlayRadioStation(radio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
qs.OnPlay = func(t mediaprovider.ContentType, id string, item any, shuffle bool) {
|
||||||
|
switch t {
|
||||||
|
case mediaprovider.ContentTypeTrack:
|
||||||
|
c.App.PlaybackManager.LoadTracks(
|
||||||
|
[]*mediaprovider.Track{item.(*mediaprovider.Track)},
|
||||||
|
backend.Replace, false /*shuffle*/)
|
||||||
|
c.App.PlaybackManager.PlayFromBeginning()
|
||||||
|
case mediaprovider.ContentTypeAlbum:
|
||||||
|
go c.App.PlaybackManager.PlayAlbum(id, 0, shuffle)
|
||||||
|
case mediaprovider.ContentTypeArtist:
|
||||||
|
go c.App.PlaybackManager.PlayArtistDiscography(id, shuffle)
|
||||||
|
case mediaprovider.ContentTypePlaylist:
|
||||||
|
go c.App.PlaybackManager.PlayPlaylist(id, 0, shuffle)
|
||||||
|
case mediaprovider.ContentTypeGenre:
|
||||||
|
go c.App.PlaybackManager.PlayRandomSongs(id /*genre name*/)
|
||||||
|
case mediaprovider.ContentTypeRadioStation:
|
||||||
|
go c.App.PlaybackManager.PlayRadioStation(item.(*mediaprovider.RadioStation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qs.OnAddToQueue = c.handleSearchDialogOnAddToQueue
|
||||||
|
qs.OnAddToPlaylist = c.handleSearchDialogOnAddToPlaylist
|
||||||
|
qs.OnDownload = func(track *mediaprovider.Track) {
|
||||||
|
c.ShowDownloadDialog([]*mediaprovider.Track{track}, track.Metadata().Name)
|
||||||
|
}
|
||||||
|
qs.OnPlaySongRadio = func(track *mediaprovider.Track) {
|
||||||
|
go func() {
|
||||||
|
tracks, err := c.GetSongRadioTracks(track)
|
||||||
|
if err != nil {
|
||||||
|
c.App.PlaybackManager.LoadTracks(tracks, backend.Replace, false)
|
||||||
|
c.App.PlaybackManager.PlayFromBeginning()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
qs.OnSetFavorite = func(trackID string, fav bool) {
|
||||||
|
go c.SetTrackFavorites([]string{trackID}, fav)
|
||||||
|
}
|
||||||
|
qs.OnSetRating = func(trackID string, rating int) {
|
||||||
|
go c.SetTrackRatings([]string{trackID}, rating)
|
||||||
|
}
|
||||||
|
qs.OnShare = func(trackID string) {
|
||||||
|
c.ShowShareDialog(trackID)
|
||||||
|
}
|
||||||
|
qs.OnShowTrackInfo = func(track *mediaprovider.Track) {
|
||||||
|
c.ShowTrackInfoDialog(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ClosePopUpOnEscape(pop)
|
||||||
|
c.haveModal = true
|
||||||
|
min := qs.MinSize()
|
||||||
|
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.Show()
|
||||||
|
c.MainWindow.Canvas().Focus(qs.GetSearchEntry())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) handleSearchDialogOnAddToQueue(t mediaprovider.ContentType, id string, item any, next bool) {
|
||||||
|
insertMode := backend.Append
|
||||||
|
if next {
|
||||||
|
insertMode = backend.InsertNext
|
||||||
|
}
|
||||||
|
switch t {
|
||||||
|
case mediaprovider.ContentTypeTrack:
|
||||||
|
c.App.PlaybackManager.LoadTracks(
|
||||||
|
[]*mediaprovider.Track{item.(*mediaprovider.Track)},
|
||||||
|
insertMode, false /*shuffle*/)
|
||||||
|
case mediaprovider.ContentTypeAlbum:
|
||||||
|
go c.App.PlaybackManager.LoadAlbum(id, insertMode, false)
|
||||||
|
case mediaprovider.ContentTypeArtist:
|
||||||
|
go func() {
|
||||||
|
tracks := c.GetArtistTracks(id)
|
||||||
|
c.App.PlaybackManager.LoadTracks(tracks, insertMode, false)
|
||||||
|
}()
|
||||||
|
case mediaprovider.ContentTypePlaylist:
|
||||||
|
go c.App.PlaybackManager.LoadPlaylist(id, insertMode, false)
|
||||||
|
case mediaprovider.ContentTypeGenre:
|
||||||
|
go func() {
|
||||||
|
tr, err := c.App.ServerManager.Server.GetRandomTracks(id /*genre name*/, c.App.Config.Application.EnqueueBatchSize)
|
||||||
|
if err != nil {
|
||||||
|
c.App.PlaybackManager.LoadTracks(tr, insertMode, false /*shuffle*/)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case mediaprovider.ContentTypeRadioStation:
|
||||||
|
c.App.PlaybackManager.LoadRadioStation(item.(*mediaprovider.RadioStation), insertMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) handleSearchDialogOnAddToPlaylist(t mediaprovider.ContentType, id string, item any) {
|
||||||
|
switch t {
|
||||||
|
case mediaprovider.ContentTypeTrack:
|
||||||
|
c.DoAddTracksToPlaylistWorkflow([]string{id})
|
||||||
|
case mediaprovider.ContentTypeAlbum:
|
||||||
|
go func() {
|
||||||
|
album, err := c.App.ServerManager.Server.GetAlbum(id)
|
||||||
|
if err == nil && len(album.Tracks) > 0 {
|
||||||
|
trackIDs := sharedutil.MapSlice(album.Tracks, func(t *mediaprovider.Track) string {
|
||||||
|
return t.ID
|
||||||
|
})
|
||||||
|
fyne.Do(func() { c.DoAddTracksToPlaylistWorkflow(trackIDs) })
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case mediaprovider.ContentTypeArtist:
|
||||||
|
go func() {
|
||||||
|
tracks := c.GetArtistTracks(id)
|
||||||
|
if len(tracks) > 0 {
|
||||||
|
trackIDs := sharedutil.MapSlice(tracks, func(t *mediaprovider.Track) string {
|
||||||
|
return t.ID
|
||||||
|
})
|
||||||
|
fyne.Do(func() { c.DoAddTracksToPlaylistWorkflow(trackIDs) })
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case mediaprovider.ContentTypePlaylist:
|
||||||
|
go func() {
|
||||||
|
playlist, err := c.App.ServerManager.Server.GetPlaylist(id)
|
||||||
|
if err == nil && len(playlist.Tracks) > 0 {
|
||||||
|
trackIDs := sharedutil.MapSlice(playlist.Tracks, func(t *mediaprovider.Track) string {
|
||||||
|
return t.ID
|
||||||
|
})
|
||||||
|
fyne.Do(func() { c.DoAddTracksToPlaylistWorkflow(trackIDs) })
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case mediaprovider.ContentTypeGenre:
|
||||||
|
go func() {
|
||||||
|
tracks, err := c.App.ServerManager.Server.GetRandomTracks(id /*genre name*/, c.App.Config.Application.EnqueueBatchSize)
|
||||||
|
if err == nil && len(tracks) > 0 {
|
||||||
|
trackIDs := sharedutil.MapSlice(tracks, func(t *mediaprovider.Track) string {
|
||||||
|
return t.ID
|
||||||
|
})
|
||||||
|
fyne.Do(func() { c.DoAddTracksToPlaylistWorkflow(trackIDs) })
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case mediaprovider.ContentTypeRadioStation:
|
||||||
|
log.Println("Cannot add radio station to playlist")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/lang"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"github.com/dweymouth/supersonic/backend"
|
||||||
|
"github.com/dweymouth/supersonic/ui/dialogs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Controller) PromptForFirstServer() {
|
||||||
|
d := dialogs.NewAddEditServerDialog(lang.L("Connect to Server"), false, nil, m.MainWindow.Canvas().Focus)
|
||||||
|
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
||||||
|
d.OnSubmit = func() {
|
||||||
|
d.DisableSubmit()
|
||||||
|
go func() {
|
||||||
|
if m.testConnectionAndUpdateDialogText(d) {
|
||||||
|
// connection is good
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
conn := backend.ServerConnection{
|
||||||
|
ServerType: d.ServerType,
|
||||||
|
Hostname: d.Host,
|
||||||
|
AltHostname: d.AltHost,
|
||||||
|
Username: d.Username,
|
||||||
|
LegacyAuth: d.LegacyAuth,
|
||||||
|
}
|
||||||
|
server := m.App.ServerManager.AddServer(d.Nickname, conn)
|
||||||
|
if err := m.trySetPasswordAndConnectToServer(server, d.Password); err != nil {
|
||||||
|
log.Printf("error connecting to server: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.EnableSubmit()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
m.haveModal = true
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoConnectToServerWorkflow does the workflow for connecting to the last active server on startup
|
||||||
|
func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) {
|
||||||
|
pass, err := c.App.ServerManager.GetServerPassword(server.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting password from keyring: %v", err)
|
||||||
|
c.PromptForLoginAndConnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try connecting to last used server - set up cancelable modal dialog
|
||||||
|
canceled := false
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
dlg := dialog.NewCustom(lang.L("Connecting"), lang.L("Cancel"),
|
||||||
|
widget.NewLabel(fmt.Sprintf(lang.L("Connecting to")+" %s", server.Nickname)), c.MainWindow)
|
||||||
|
dlg.SetOnClosed(func() {
|
||||||
|
canceled = true
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
|
c.haveModal = true
|
||||||
|
dlg.Show()
|
||||||
|
|
||||||
|
// try to connect
|
||||||
|
go func() {
|
||||||
|
defer cancel() // make sure to free up ctx resources if user does not cancel
|
||||||
|
|
||||||
|
if err := c.tryConnectToServer(ctx, server, pass); err != nil {
|
||||||
|
fyne.Do(func() {
|
||||||
|
dlg.Hide()
|
||||||
|
c.haveModal = false
|
||||||
|
if canceled {
|
||||||
|
c.PromptForLoginAndConnect()
|
||||||
|
} else {
|
||||||
|
// connection failure
|
||||||
|
dlg := dialog.NewError(err, c.MainWindow)
|
||||||
|
dlg.SetOnClosed(func() {
|
||||||
|
c.PromptForLoginAndConnect()
|
||||||
|
})
|
||||||
|
c.haveModal = true
|
||||||
|
dlg.Show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fyne.Do(func() {
|
||||||
|
dlg.Hide()
|
||||||
|
c.haveModal = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Controller) PromptForLoginAndConnect() {
|
||||||
|
d := dialogs.NewLoginDialog(m.App.Config.Servers, m.App.ServerManager.GetServerPassword)
|
||||||
|
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
|
||||||
|
d.OnSubmit = func(server *backend.ServerConfig, password string) {
|
||||||
|
d.DisableSubmit()
|
||||||
|
d.SetInfoText(lang.L("Testing connection") + "...")
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := m.App.ServerManager.TestConnectionAndAuth(ctx, server.ServerConnection, password)
|
||||||
|
fyne.Do(func() {
|
||||||
|
if err == backend.ErrUnreachable {
|
||||||
|
d.SetErrorText(lang.L("Server unreachable"))
|
||||||
|
} else if err != nil {
|
||||||
|
d.SetErrorText(lang.L("Authentication failed"))
|
||||||
|
} else {
|
||||||
|
pop.Hide()
|
||||||
|
m.trySetPasswordAndConnectToServer(server, password)
|
||||||
|
m.doModalClosed()
|
||||||
|
}
|
||||||
|
d.EnableSubmit()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
d.OnEditServer = func(server *backend.ServerConfig) {
|
||||||
|
pop.Hide()
|
||||||
|
editD := dialogs.NewAddEditServerDialog(lang.L("Edit server"), true, server, m.MainWindow.Canvas().Focus)
|
||||||
|
editPop := widget.NewModalPopUp(editD, m.MainWindow.Canvas())
|
||||||
|
editD.OnSubmit = func() {
|
||||||
|
d.DisableSubmit()
|
||||||
|
go func() {
|
||||||
|
success := m.testConnectionAndUpdateDialogText(editD)
|
||||||
|
fyne.Do(func() {
|
||||||
|
if success {
|
||||||
|
// connection is good
|
||||||
|
editPop.Hide()
|
||||||
|
server.Hostname = editD.Host
|
||||||
|
server.AltHostname = editD.AltHost
|
||||||
|
server.Nickname = editD.Nickname
|
||||||
|
server.Username = editD.Username
|
||||||
|
server.LegacyAuth = editD.LegacyAuth
|
||||||
|
m.trySetPasswordAndConnectToServer(server, editD.Password)
|
||||||
|
m.doModalClosed()
|
||||||
|
}
|
||||||
|
d.EnableSubmit()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
editD.OnCancel = func() {
|
||||||
|
editPop.Hide()
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
|
editPop.Show()
|
||||||
|
}
|
||||||
|
d.OnNewServer = func() {
|
||||||
|
pop.Hide()
|
||||||
|
newD := dialogs.NewAddEditServerDialog(lang.L("Add Server"), true, nil, m.MainWindow.Canvas().Focus)
|
||||||
|
newPop := widget.NewModalPopUp(newD, m.MainWindow.Canvas())
|
||||||
|
newD.OnSubmit = func() {
|
||||||
|
d.DisableSubmit()
|
||||||
|
go func() {
|
||||||
|
success := m.testConnectionAndUpdateDialogText(newD)
|
||||||
|
fyne.Do(func() {
|
||||||
|
if success {
|
||||||
|
// connection is good
|
||||||
|
newPop.Hide()
|
||||||
|
conn := backend.ServerConnection{
|
||||||
|
ServerType: newD.ServerType,
|
||||||
|
Hostname: newD.Host,
|
||||||
|
AltHostname: newD.AltHost,
|
||||||
|
Username: newD.Username,
|
||||||
|
LegacyAuth: newD.LegacyAuth,
|
||||||
|
}
|
||||||
|
server := m.App.ServerManager.AddServer(newD.Nickname, conn)
|
||||||
|
m.trySetPasswordAndConnectToServer(server, newD.Password)
|
||||||
|
m.doModalClosed()
|
||||||
|
}
|
||||||
|
d.EnableSubmit()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
newD.OnCancel = func() {
|
||||||
|
newPop.Hide()
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
|
newPop.Show()
|
||||||
|
}
|
||||||
|
d.OnDeleteServer = func(server *backend.ServerConfig) {
|
||||||
|
pop.Hide()
|
||||||
|
dialog.ShowConfirm(lang.L("Confirm Delete Server"),
|
||||||
|
fmt.Sprintf(lang.L("Are you sure you want to delete the server")+" %q?", server.Nickname),
|
||||||
|
func(ok bool) {
|
||||||
|
if ok {
|
||||||
|
m.App.ServerManager.DeleteServer(server.ID)
|
||||||
|
m.App.DeleteServerCacheDir(server.ID)
|
||||||
|
d.SetServers(m.App.Config.Servers)
|
||||||
|
}
|
||||||
|
if len(m.App.Config.Servers) == 0 {
|
||||||
|
m.PromptForFirstServer()
|
||||||
|
} else {
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
|
}, m.MainWindow)
|
||||||
|
|
||||||
|
}
|
||||||
|
m.haveModal = true
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error {
|
||||||
|
if err := c.App.ServerManager.SetServerPassword(server, password); err != nil {
|
||||||
|
log.Printf("error setting keyring credentials: %v", err)
|
||||||
|
// Don't return an error; fall back to just using the password in-memory
|
||||||
|
// User will need to log in with the password on subsequent runs.
|
||||||
|
}
|
||||||
|
return c.tryConnectToServer(context.Background(), server, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to connect to the given server, with the configured timeout added to the context
|
||||||
|
func (c *Controller) tryConnectToServer(ctx context.Context, server *backend.ServerConfig, password string) error {
|
||||||
|
timeout := time.Duration(c.App.Config.Application.RequestTimeoutSeconds) * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := c.App.ServerManager.TestConnectionAndAuth(ctx, server.ServerConnection, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.App.ServerManager.ConnectToServer(server, password); err != nil {
|
||||||
|
log.Printf("error connecting to server: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServerDialog) bool {
|
||||||
|
dlg.SetInfoText(lang.L("Testing connection") + "...")
|
||||||
|
conn := backend.ServerConnection{
|
||||||
|
ServerType: dlg.ServerType,
|
||||||
|
Hostname: dlg.Host,
|
||||||
|
AltHostname: dlg.AltHost,
|
||||||
|
Username: dlg.Username,
|
||||||
|
LegacyAuth: dlg.LegacyAuth,
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
err := c.App.ServerManager.TestConnectionAndAuth(ctx, conn, dlg.Password)
|
||||||
|
if err == backend.ErrUnreachable {
|
||||||
|
dlg.SetErrorText(lang.L("Could not reach server") + fmt.Sprintf(" (%s?)", lang.L("wrong URL")))
|
||||||
|
return false
|
||||||
|
} else if err != nil {
|
||||||
|
dlg.SetErrorText(lang.L("Authentication failed") + fmt.Sprintf(" (%s)", lang.L("wrong username/password")))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -5,31 +5,115 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/lang"
|
"fyne.io/fyne/v2/lang"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QuickSearch struct {
|
type QuickSearch struct {
|
||||||
SearchDialog *SearchDialog
|
SearchDialog *SearchDialog
|
||||||
|
results []*mediaprovider.SearchResult
|
||||||
mp mediaprovider.MediaProvider
|
mp mediaprovider.MediaProvider
|
||||||
|
|
||||||
|
OnPlay func(t mediaprovider.ContentType, id string, item any, shuffle bool)
|
||||||
|
OnAddToQueue func(t mediaprovider.ContentType, id string, item any, next bool)
|
||||||
|
OnAddToPlaylist func(t mediaprovider.ContentType, id string, item any)
|
||||||
|
OnSetFavorite func(trackID string, fav bool)
|
||||||
|
OnSetRating func(trackID string, rating int)
|
||||||
|
OnDownload func(track *mediaprovider.Track)
|
||||||
|
OnShare func(trackID string)
|
||||||
|
OnPlaySongRadio func(track *mediaprovider.Track)
|
||||||
|
OnShowTrackInfo func(track *mediaprovider.Track)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
|
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
|
||||||
q := &QuickSearch{mp: mp}
|
q := &QuickSearch{mp: mp}
|
||||||
q.SearchDialog = NewSearchDialog(im, lang.L("Search Everywhere"), lang.L("Close"), q.onSearched)
|
q.SearchDialog = NewSearchDialog(im, lang.L("Search Everywhere"), lang.L("Close"), q.onSearched)
|
||||||
|
q.SearchDialog.OnShowContextMenu = q.showMenu
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QuickSearch) onSearched(query string) []*mediaprovider.SearchResult {
|
func (q *QuickSearch) onSearched(query string) []*mediaprovider.SearchResult {
|
||||||
var results []*mediaprovider.SearchResult
|
|
||||||
if query != "" {
|
if query != "" {
|
||||||
if res, err := q.mp.SearchAll(query, 20); err != nil {
|
if res, err := q.mp.SearchAll(query, 20); err != nil {
|
||||||
|
q.results = nil
|
||||||
log.Printf("Error searching: %s", err.Error())
|
log.Printf("Error searching: %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
results = res
|
q.results = res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results
|
return q.results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QuickSearch) showMenu(idx int, pos fyne.Position) {
|
||||||
|
cType := q.results[idx].Type
|
||||||
|
id := q.results[idx].ID
|
||||||
|
item := q.results[idx].Item
|
||||||
|
|
||||||
|
canvas := fyne.CurrentApp().Driver().CanvasForObject(q.SearchDialog)
|
||||||
|
|
||||||
|
switch cType {
|
||||||
|
case mediaprovider.ContentTypeTrack:
|
||||||
|
menu := util.NewTrackContextMenu(false, nil)
|
||||||
|
menu.OnPlay = func(shuffle bool) {
|
||||||
|
q.OnPlay(cType, id, item, shuffle)
|
||||||
|
}
|
||||||
|
menu.OnAddToQueue = func(next bool) {
|
||||||
|
q.OnAddToQueue(cType, id, item, next)
|
||||||
|
}
|
||||||
|
menu.OnAddToPlaylist = func() {
|
||||||
|
q.OnAddToPlaylist(cType, id, item)
|
||||||
|
}
|
||||||
|
menu.OnDownload = func() {
|
||||||
|
q.OnDownload(item.(*mediaprovider.Track))
|
||||||
|
}
|
||||||
|
menu.OnFavorite = func(fav bool) {
|
||||||
|
q.OnSetFavorite(id, fav)
|
||||||
|
}
|
||||||
|
menu.OnSetRating = func(rating int) {
|
||||||
|
q.OnSetRating(id, rating)
|
||||||
|
}
|
||||||
|
menu.OnPlaySongRadio = func() {
|
||||||
|
q.OnPlaySongRadio(item.(*mediaprovider.Track))
|
||||||
|
}
|
||||||
|
menu.OnShowInfo = func() {
|
||||||
|
q.OnShowTrackInfo(item.(*mediaprovider.Track))
|
||||||
|
}
|
||||||
|
menu.OnShare = func() {
|
||||||
|
q.OnShare(id)
|
||||||
|
}
|
||||||
|
menu.ShowAtPosition(pos, canvas)
|
||||||
|
default:
|
||||||
|
play := fyne.NewMenuItem(lang.L("Play"), func() {
|
||||||
|
q.OnPlay(cType, id, item, false)
|
||||||
|
})
|
||||||
|
play.Icon = theme.MediaPlayIcon()
|
||||||
|
shuffle := fyne.NewMenuItem(lang.L("Shuffle"), func() {
|
||||||
|
q.OnPlay(cType, id, item, true)
|
||||||
|
})
|
||||||
|
shuffle.Icon = myTheme.ShuffleIcon
|
||||||
|
playNext := fyne.NewMenuItem(lang.L("Play next"), func() {
|
||||||
|
q.OnAddToQueue(cType, id, item, true)
|
||||||
|
})
|
||||||
|
playNext.Icon = myTheme.PlayNextIcon
|
||||||
|
add := fyne.NewMenuItem(lang.L("Add to queue"), func() {
|
||||||
|
q.OnAddToQueue(cType, id, item, false)
|
||||||
|
})
|
||||||
|
add.Icon = theme.ContentAddIcon()
|
||||||
|
menu := fyne.NewMenu("", play, shuffle, playNext, add)
|
||||||
|
|
||||||
|
if cType != mediaprovider.ContentTypeRadioStation {
|
||||||
|
playlist := fyne.NewMenuItem(lang.L("Add to playlist")+"...", func() {
|
||||||
|
q.OnAddToPlaylist(cType, id, item)
|
||||||
|
})
|
||||||
|
playlist.Icon = myTheme.PlaylistIcon
|
||||||
|
menu.Items = append(menu.Items, playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.ShowPopUpMenuAtPosition(menu, canvas, pos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QuickSearch) SetOnDismiss(onDismiss func()) {
|
func (q *QuickSearch) SetOnDismiss(onDismiss func()) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type SearchDialog struct {
|
|||||||
|
|
||||||
OnDismiss func()
|
OnDismiss func()
|
||||||
OnNavigateTo func(mediaprovider.ContentType, string)
|
OnNavigateTo func(mediaprovider.ContentType, string)
|
||||||
|
OnShowContextMenu func(itemIdx int, pos fyne.Position)
|
||||||
OnSearched func(string) []*mediaprovider.SearchResult
|
OnSearched func(string) []*mediaprovider.SearchResult
|
||||||
|
|
||||||
imgSource util.ImageFetcher
|
imgSource util.ImageFetcher
|
||||||
@@ -326,6 +327,12 @@ func (q *searchResult) Tapped(_ *fyne.PointEvent) {
|
|||||||
q.parent.onSelected(q.index)
|
q.parent.onSelected(q.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *searchResult) TappedSecondary(e *fyne.PointEvent) {
|
||||||
|
if q.parent.OnShowContextMenu != nil {
|
||||||
|
q.parent.OnShowContextMenu(q.index, e.AbsolutePosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (q *searchResult) CreateRenderer() fyne.WidgetRenderer {
|
func (q *searchResult) CreateRenderer() fyne.WidgetRenderer {
|
||||||
if q.content == nil {
|
if q.content == nil {
|
||||||
q.content = container.NewBorder(nil, nil, container.NewCenter(q.image), nil,
|
q.content = container.NewBorder(nil, nil, container.NewCenter(q.image), nil,
|
||||||
|
|||||||
Reference in New Issue
Block a user