616 lines
16 KiB
Go
616 lines
16 KiB
Go
package controller
|
|
|
|
import (
|
|
"archive/zip"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"io"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
fynetooltip "github.com/dweymouth/fyne-tooltip"
|
|
"github.com/dweymouth/supersonic/backend"
|
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
"github.com/dweymouth/supersonic/backend/player"
|
|
"github.com/dweymouth/supersonic/backend/player/mpv"
|
|
"github.com/dweymouth/supersonic/sharedutil"
|
|
"github.com/dweymouth/supersonic/ui/dialogs"
|
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
|
"github.com/dweymouth/supersonic/ui/util"
|
|
"github.com/dweymouth/supersonic/ui/widgets"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/lang"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
type NavigationHandler func(Route)
|
|
|
|
type CurPageFunc func() Route
|
|
|
|
type ToastProvider interface {
|
|
ShowSuccessToast(string)
|
|
ShowErrorToast(string)
|
|
}
|
|
|
|
type Controller struct {
|
|
visualizationData
|
|
AppVersion string
|
|
App *backend.App
|
|
MainWindow fyne.Window
|
|
|
|
// dependencies injected from MainWindow
|
|
NavHandler NavigationHandler
|
|
CurPageFunc CurPageFunc
|
|
ReloadFunc func()
|
|
RefreshPageFunc func()
|
|
SelectAllPageFunc func()
|
|
UnselectAllPageFunc func()
|
|
ToastProvider ToastProvider
|
|
|
|
popUpQueueMutex sync.Mutex
|
|
popUpQueue *widget.PopUp
|
|
popUpQueueList *widgets.PlayQueueList
|
|
popUpQueueLastUsed int64
|
|
escapablePopUp fyne.CanvasObject
|
|
haveModal bool
|
|
runOnModalClosed func()
|
|
}
|
|
|
|
func New(app *backend.App, appVersion string, mainWindow fyne.Window) *Controller {
|
|
c := &Controller{
|
|
AppVersion: appVersion,
|
|
MainWindow: mainWindow,
|
|
App: app,
|
|
}
|
|
c.initVisualizations()
|
|
c.App.PlaybackManager.OnQueueChange(func() {
|
|
c.popUpQueueMutex.Lock()
|
|
defer c.popUpQueueMutex.Unlock()
|
|
if c.popUpQueue != nil {
|
|
c.popUpQueueList.SetItems(c.App.PlaybackManager.GetPlayQueue())
|
|
}
|
|
})
|
|
c.App.PlaybackManager.OnSongChange(func(track mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
|
c.popUpQueueMutex.Lock()
|
|
defer c.popUpQueueMutex.Unlock()
|
|
if c.popUpQueue == nil {
|
|
return
|
|
}
|
|
if track == nil {
|
|
c.popUpQueueList.SetNowPlaying("")
|
|
} else {
|
|
c.popUpQueueList.SetNowPlaying(track.Metadata().ID)
|
|
}
|
|
})
|
|
return c
|
|
}
|
|
|
|
func (m *Controller) SelectAll() {
|
|
m.popUpQueueMutex.Lock()
|
|
if m.popUpQueue != nil && m.popUpQueue.Visible() {
|
|
m.popUpQueueList.SelectAll()
|
|
m.popUpQueueMutex.Unlock()
|
|
return
|
|
}
|
|
m.popUpQueueMutex.Unlock()
|
|
if m.SelectAllPageFunc != nil {
|
|
m.SelectAllPageFunc()
|
|
}
|
|
}
|
|
|
|
func (m *Controller) UnselectAll() {
|
|
m.popUpQueueMutex.Lock()
|
|
if m.popUpQueue != nil && m.popUpQueue.Visible() {
|
|
m.popUpQueueList.UnselectAll()
|
|
m.popUpQueueMutex.Unlock()
|
|
return
|
|
}
|
|
m.popUpQueueMutex.Unlock()
|
|
if m.SelectAllPageFunc != nil {
|
|
m.UnselectAllPageFunc()
|
|
}
|
|
}
|
|
|
|
func (m *Controller) NavigateTo(route Route) {
|
|
m.NavHandler(route)
|
|
}
|
|
|
|
func (m *Controller) ClosePopUpOnEscape(pop fyne.CanvasObject) {
|
|
m.escapablePopUp = pop
|
|
}
|
|
|
|
func (m *Controller) CloseEscapablePopUp() {
|
|
if m.escapablePopUp != nil {
|
|
m.escapablePopUp.Hide()
|
|
m.escapablePopUp = nil
|
|
m.doModalClosed()
|
|
}
|
|
}
|
|
|
|
// If there is currently no modal popup managed by the Controller visible,
|
|
// then run f (which should create and show a modal dialog) immediately.
|
|
// else run f when the current modal dialog workflow has ended.
|
|
func (m *Controller) QueueShowModalFunc(f func()) {
|
|
if m.haveModal {
|
|
m.runOnModalClosed = f
|
|
} else {
|
|
f()
|
|
}
|
|
}
|
|
|
|
func (m *Controller) HaveModal() bool {
|
|
return m.haveModal
|
|
}
|
|
|
|
func (m *Controller) ShowCastMenu(onPendingPlayerChange func()) {
|
|
rp := m.App.PlaybackManager.CurrentRemotePlayer()
|
|
devices := m.App.PlaybackManager.RemotePlayers()
|
|
local := fyne.NewMenuItem(lang.L("Local player"), func() {
|
|
onPendingPlayerChange()
|
|
go func() {
|
|
if err := m.App.PlaybackManager.SetRemotePlayer(nil); err != nil {
|
|
fyne.Do(func() { m.ToastProvider.ShowErrorToast("Failed to disconnect from remote player") })
|
|
}
|
|
}()
|
|
})
|
|
local.Icon = theme.ComputerIcon()
|
|
local.Checked = rp == nil
|
|
|
|
menu := fyne.NewMenu("", local)
|
|
for _, d := range devices {
|
|
_d := d
|
|
item := fyne.NewMenuItem(d.Name, func() {
|
|
onPendingPlayerChange()
|
|
go func() {
|
|
if err := m.App.PlaybackManager.SetRemotePlayer(&_d); err != nil {
|
|
fyne.Do(func() { m.ToastProvider.ShowErrorToast("Failed to connect to " + _d.Name) })
|
|
}
|
|
}()
|
|
})
|
|
item.Icon = myTheme.CastIcon
|
|
item.Checked = rp != nil && _d.URL == rp.URL
|
|
menu.Items = append(menu.Items, item)
|
|
}
|
|
pop := widget.NewPopUpMenu(menu, m.MainWindow.Canvas())
|
|
canvasSize := m.MainWindow.Canvas().Size()
|
|
pop.ShowAtPosition(fyne.NewPos(
|
|
canvasSize.Width-pop.MinSize().Width-10,
|
|
canvasSize.Height-pop.MinSize().Height-100,
|
|
))
|
|
}
|
|
|
|
func (m *Controller) ShowPopUpPlayQueue() {
|
|
m.popUpQueueMutex.Lock()
|
|
if m.popUpQueue == nil {
|
|
m.popUpQueueList = widgets.NewPlayQueueList(m.App.ImageManager, false)
|
|
m.popUpQueueList.Reorderable = true
|
|
m.popUpQueueList.SetItems(m.App.PlaybackManager.GetPlayQueue())
|
|
m.ConnectPlayQueuelistActions(m.popUpQueueList)
|
|
|
|
title := widget.NewRichTextWithText(lang.L("Play Queue"))
|
|
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
|
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
|
|
ctr := container.NewBorder(title, nil, nil, nil,
|
|
container.NewPadded(m.popUpQueueList),
|
|
)
|
|
m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas())
|
|
fynetooltip.AddPopUpToolTipLayer(m.popUpQueue)
|
|
|
|
container.NewThemeOverride(m.popUpQueue, myTheme.WithColorTransformOverride(
|
|
theme.ColorNameOverlayBackground,
|
|
func(c color.Color) color.Color {
|
|
if nrgba, ok := c.(color.NRGBA); ok {
|
|
nrgba.A = 245
|
|
return nrgba
|
|
}
|
|
c_ := c.(color.RGBA)
|
|
c_.A = 245
|
|
return c_
|
|
},
|
|
))
|
|
|
|
// free popUpQueue if it hasn't been used in awhile
|
|
go func() {
|
|
t := time.NewTicker(1 * time.Minute)
|
|
for range t.C {
|
|
m.popUpQueueMutex.Lock()
|
|
now := time.Now().UnixMilli()
|
|
if m.popUpQueueLastUsed < now-120_000 /*2 min*/ {
|
|
fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue)
|
|
m.popUpQueue = nil
|
|
m.popUpQueueList = nil
|
|
m.popUpQueueLastUsed = 0
|
|
m.popUpQueueMutex.Unlock()
|
|
t.Stop()
|
|
return
|
|
}
|
|
m.popUpQueueMutex.Unlock()
|
|
}
|
|
}()
|
|
}
|
|
m.popUpQueueLastUsed = time.Now().UnixMilli()
|
|
popUpQueueList := m.popUpQueueList
|
|
pop := m.popUpQueue
|
|
m.popUpQueueMutex.Unlock()
|
|
|
|
npID := ""
|
|
if np := m.App.PlaybackManager.NowPlaying(); np != nil {
|
|
npID = np.Metadata().ID
|
|
}
|
|
popUpQueueList.SetNowPlaying(npID)
|
|
popUpQueueList.UnselectAll()
|
|
|
|
m.ClosePopUpOnEscape(pop)
|
|
minSize := fyne.NewSize(300, 400)
|
|
maxSize := fyne.NewSize(800, 1000)
|
|
canvasSize := m.MainWindow.Canvas().Size()
|
|
size := minSize.Max(maxSize.Min(
|
|
fyne.NewSize(canvasSize.Width*0.4, canvasSize.Height*0.5),
|
|
))
|
|
pop.Resize(size)
|
|
popUpQueueList.ScrollToNowPlaying() // must come after resize
|
|
pop.ShowAtPosition(fyne.NewPos(
|
|
canvasSize.Width-size.Width-10,
|
|
canvasSize.Height-size.Height-100,
|
|
))
|
|
}
|
|
|
|
func (m *Controller) ShowPopUpImage(img image.Image) {
|
|
s := m.MainWindow.Canvas().Size()
|
|
var popS fyne.Size
|
|
if asp := util.ImageAspect(img); s.Width/s.Height > asp {
|
|
// window height is limiting factor
|
|
h := s.Height * 0.8
|
|
popS = fyne.NewSize(h*asp, h)
|
|
} else {
|
|
w := s.Width * 0.8
|
|
popS = fyne.NewSize(w, w*(1/asp))
|
|
}
|
|
|
|
pop := widgets.NewImagePopUp(img, m.MainWindow.Canvas(), popS)
|
|
|
|
m.ClosePopUpOnEscape(pop)
|
|
pop.Show()
|
|
}
|
|
|
|
func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track {
|
|
if mp := m.App.ServerManager.Server; mp != nil {
|
|
if tr, err := mp.GetArtistTracks(artistID); err != nil {
|
|
log.Println(err.Error())
|
|
return nil
|
|
} else {
|
|
return tr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) ShowAboutDialog() {
|
|
dlg := dialogs.NewAboutDialog(c.AppVersion)
|
|
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
|
dlg.OnDismiss = func() {
|
|
pop.Hide()
|
|
c.doModalClosed()
|
|
}
|
|
c.ClosePopUpOnEscape(pop)
|
|
c.haveModal = true
|
|
pop.Show()
|
|
}
|
|
|
|
func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map[string]string) {
|
|
|
|
devs, err := c.App.LocalPlayer.ListAudioDevices()
|
|
if err != nil {
|
|
log.Printf("error listing audio devices: %v", err)
|
|
devs = []mpv.AudioDevice{{Name: "auto", Description: lang.L("Autoselect device")}}
|
|
}
|
|
|
|
curPlayer := c.App.PlaybackManager.CurrentPlayer()
|
|
_, isReplayGainPlayer := curPlayer.(player.ReplayGainPlayer)
|
|
_, isEqualizerPlayer := curPlayer.(*mpv.Player)
|
|
_, canSavePlayQueue := c.App.ServerManager.Server.(mediaprovider.CanSavePlayQueue)
|
|
isLocalPlayer := isEqualizerPlayer
|
|
bands := c.App.LocalPlayer.Equalizer().BandFrequencies()
|
|
|
|
dlg := dialogs.NewSettingsDialog(c.App.Config,
|
|
devs, themeFiles, bands,
|
|
c.App.ServerManager.Server.ClientDecidesScrobble(),
|
|
isLocalPlayer, isReplayGainPlayer, isEqualizerPlayer, canSavePlayQueue,
|
|
c.MainWindow)
|
|
dlg.OnReplayGainSettingsChanged = func() {
|
|
c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain)
|
|
}
|
|
dlg.OnAudioExclusiveSettingChanged = func() {
|
|
c.App.LocalPlayer.SetAudioExclusive(c.App.Config.LocalPlayback.AudioExclusive)
|
|
}
|
|
dlg.OnAudioDeviceSettingChanged = func() {
|
|
c.App.LocalPlayer.SetAudioDevice(c.App.Config.LocalPlayback.AudioDeviceName)
|
|
}
|
|
dlg.OnThemeSettingChanged = themeUpdateCallbk
|
|
dlg.OnEqualizerSettingsChanged = func() {
|
|
// currently we only have one equalizer type
|
|
eq := c.App.LocalPlayer.Equalizer().(*mpv.ISO15BandEqualizer)
|
|
eq.Disabled = !c.App.Config.LocalPlayback.EqualizerEnabled
|
|
eq.EQPreamp = c.App.Config.LocalPlayback.EqualizerPreamp
|
|
copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands)
|
|
c.App.LocalPlayer.SetEqualizer(eq)
|
|
}
|
|
dlg.OnPageNeedsRefresh = c.RefreshPageFunc
|
|
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
|
fynetooltip.AddPopUpToolTipLayer(pop)
|
|
dlg.OnDismiss = func() {
|
|
pop.Hide()
|
|
fynetooltip.DestroyPopUpToolTipLayer(pop)
|
|
c.doModalClosed()
|
|
c.App.SaveConfigFile()
|
|
}
|
|
c.ClosePopUpOnEscape(pop)
|
|
c.haveModal = true
|
|
pop.Show()
|
|
|
|
}
|
|
|
|
func (c *Controller) doModalClosed() {
|
|
c.haveModal = false
|
|
if c.runOnModalClosed != nil {
|
|
c.runOnModalClosed()
|
|
c.runOnModalClosed = nil
|
|
}
|
|
}
|
|
|
|
func (c *Controller) SetTrackFavorites(trackIDs []string, favorite bool) {
|
|
go c.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{
|
|
TrackIDs: trackIDs,
|
|
}, favorite)
|
|
|
|
for _, id := range trackIDs {
|
|
c.App.PlaybackManager.OnTrackFavoriteStatusChanged(id, favorite)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) SetTrackRatings(trackIDs []string, rating int) {
|
|
r, ok := c.App.ServerManager.Server.(mediaprovider.SupportsRating)
|
|
if !ok {
|
|
return
|
|
}
|
|
go r.SetRating(mediaprovider.RatingFavoriteParameters{
|
|
TrackIDs: trackIDs,
|
|
}, rating)
|
|
|
|
// Notify PlaybackManager of rating change to update
|
|
// the in-memory track models
|
|
for _, id := range trackIDs {
|
|
c.App.PlaybackManager.OnTrackRatingChanged(id, rating)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) ShowShareDialog(id string) {
|
|
go func() {
|
|
shareUrl, err := c.createShareURL(id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
fyne.Do(func() {
|
|
hyperlink := widget.NewHyperlink(shareUrl.String(), shareUrl)
|
|
dlg := dialog.NewCustom(lang.L("Share content"), lang.L("OK"),
|
|
container.NewHBox(
|
|
hyperlink,
|
|
widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
|
|
c.MainWindow.Clipboard().SetContent(hyperlink.Text)
|
|
}),
|
|
widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), func() {
|
|
if shareUrl, err := c.createShareURL(id); err == nil {
|
|
hyperlink.Text = shareUrl.String()
|
|
hyperlink.URL = shareUrl
|
|
hyperlink.Refresh()
|
|
}
|
|
}),
|
|
),
|
|
c.MainWindow,
|
|
)
|
|
dlg.Show()
|
|
})
|
|
}()
|
|
}
|
|
|
|
func (c *Controller) createShareURL(id string) (*url.URL, error) {
|
|
r, ok := c.App.ServerManager.Server.(mediaprovider.SupportsSharing)
|
|
if !ok {
|
|
return nil, fmt.Errorf("server does not support sharing")
|
|
}
|
|
|
|
shareUrl, err := r.CreateShareURL(id)
|
|
if err != nil {
|
|
log.Printf("error creating share URL: %v", err)
|
|
c.showError(
|
|
"Failed to share content. This commonly occurs when the server does not support sharing, " +
|
|
"or has the feature disabled.\nPlease check the server's settings and try again.",
|
|
)
|
|
return nil, err
|
|
}
|
|
return shareUrl, nil
|
|
}
|
|
|
|
func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track, downloadName string) {
|
|
numTracks := len(tracks)
|
|
var fileName string
|
|
if numTracks == 1 {
|
|
fileName = filepath.Base(tracks[0].FilePath)
|
|
} else {
|
|
fileName = "downloaded_tracks.zip"
|
|
}
|
|
|
|
dg := dialog.NewFileSave(
|
|
func(file fyne.URIWriteCloser, err error) {
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
|
|
if file == nil {
|
|
return
|
|
}
|
|
if numTracks == 1 {
|
|
go c.downloadTrack(tracks[0], file.URI().Path())
|
|
} else {
|
|
go c.downloadTracks(tracks, file.URI().Path(), downloadName)
|
|
}
|
|
|
|
},
|
|
c.MainWindow)
|
|
dg.SetFileName(fileName)
|
|
dg.Show()
|
|
}
|
|
|
|
func (c *Controller) downloadTrack(track *mediaprovider.Track, filePath string) {
|
|
reader, err := c.App.ServerManager.Server.DownloadTrack(track.ID)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
|
|
file, err := os.Create(filePath)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
_, err = io.Copy(file, reader)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
|
|
log.Printf("Saved song %s to: %s\n", track.Title, filePath)
|
|
c.sendNotification(fmt.Sprintf(lang.L("Download completed")+": %s", track.Title), fmt.Sprintf(lang.L("Saved at")+": %s", filePath))
|
|
}
|
|
|
|
func (c *Controller) downloadTracks(tracks []*mediaprovider.Track, filePath, downloadName string) {
|
|
zipFile, err := os.Create(filePath)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
defer zipFile.Close()
|
|
|
|
zipWriter := zip.NewWriter(zipFile)
|
|
defer zipWriter.Close()
|
|
|
|
for _, track := range tracks {
|
|
reader, err := c.App.ServerManager.Server.DownloadTrack(track.ID)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
fileName := filepath.Base(track.FilePath)
|
|
|
|
fileWriter, err := zipWriter.Create(fileName)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
_, err = io.Copy(fileWriter, reader)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
log.Printf("Saved song %s to: %s\n", track.Title, filePath)
|
|
}
|
|
|
|
c.sendNotification(fmt.Sprintf(lang.L("Download completed")+": %s", downloadName), fmt.Sprintf("Saved at: %s", filePath))
|
|
}
|
|
|
|
func (c *Controller) sendNotification(title, content string) {
|
|
fyne.CurrentApp().SendNotification(&fyne.Notification{
|
|
Title: title,
|
|
Content: content,
|
|
})
|
|
}
|
|
|
|
func (c *Controller) showError(content string) {
|
|
// TODO: display an in-app toast message instead of a dialog.
|
|
dialog.ShowError(fmt.Errorf(content), c.MainWindow)
|
|
}
|
|
|
|
func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover image.Image) {
|
|
go func() {
|
|
albumInfo, err := c.App.ServerManager.Server.GetAlbumInfo(albumID)
|
|
if err != nil {
|
|
log.Print("Error getting album info: ", err)
|
|
return
|
|
}
|
|
fyne.Do(func() {
|
|
dlg := dialogs.NewAlbumInfoDialog(albumInfo, albumName, albumCover)
|
|
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
|
dlg.OnDismiss = func() {
|
|
pop.Hide()
|
|
c.doModalClosed()
|
|
}
|
|
c.ClosePopUpOnEscape(pop)
|
|
c.haveModal = true
|
|
pop.Show()
|
|
})
|
|
}()
|
|
}
|
|
|
|
func (c *Controller) ShowTrackInfoDialog(track *mediaprovider.Track) {
|
|
info := dialogs.NewTrackInfoDialog(track)
|
|
pop := widget.NewModalPopUp(info, c.MainWindow.Canvas())
|
|
info.OnDismiss = func() {
|
|
pop.Hide()
|
|
c.doModalClosed()
|
|
}
|
|
info.OnNavigateToAlbum = func(albumID string) {
|
|
info.OnDismiss()
|
|
c.NavigateTo(AlbumRoute(albumID))
|
|
}
|
|
info.OnNavigateToArtist = func(artistID string) {
|
|
info.OnDismiss()
|
|
c.NavigateTo(ArtistRoute(artistID))
|
|
}
|
|
info.OnNavigateToGenre = func(genre string) {
|
|
info.OnDismiss()
|
|
c.NavigateTo(GenreRoute(genre))
|
|
}
|
|
info.OnCopyFilePath = func() {
|
|
c.MainWindow.Clipboard().SetContent(track.FilePath)
|
|
}
|
|
c.ClosePopUpOnEscape(pop)
|
|
winSize := c.MainWindow.Canvas().Size()
|
|
popMin := pop.MinSize()
|
|
width := fyne.Min(750, fyne.Max(popMin.Width, winSize.Width*0.8))
|
|
height := fyne.Min(650, fyne.Max(popMin.Height, winSize.Height*0.8))
|
|
pop.Resize(fyne.NewSize(width, height))
|
|
c.haveModal = true
|
|
pop.Show()
|
|
}
|
|
|
|
func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*mediaprovider.Track, error) {
|
|
radioTracks, err := c.App.ServerManager.Server.GetSongRadio(sourceTrack.ID, 100)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting song radio: %s", err.Error())
|
|
}
|
|
|
|
// The goal of this implementation is to place the source track first in the queue.
|
|
filteredTracks := sharedutil.FilterSlice(radioTracks, func(track *mediaprovider.Track) bool {
|
|
return track.ID != sourceTrack.ID
|
|
})
|
|
tracks := []*mediaprovider.Track{sourceTrack}
|
|
tracks = append(tracks, filteredTracks...)
|
|
return tracks, nil
|
|
}
|