add 'New Playlist' button to Playlists page
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package jellyfin
|
package jellyfin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
@@ -77,7 +78,7 @@ func (j *jellyfinMediaProvider) SetLibrary(id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jellyfinMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
func (j *jellyfinMediaProvider) CreatePlaylistWithTracks(name string, trackIDs []string) error {
|
||||||
return j.client.CreatePlaylist(name, trackIDs)
|
return j.client.CreatePlaylist(name, trackIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +94,10 @@ func (j *jellyfinMediaProvider) EditPlaylist(id, name, description string, publi
|
|||||||
return j.client.UpdatePlaylistMetadata(id, name, description)
|
return j.client.UpdatePlaylistMetadata(id, name, description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *jellyfinMediaProvider) CreatePlaylist(name, description string, public bool) error {
|
||||||
|
return errors.New("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
func (j *jellyfinMediaProvider) AddPlaylistTracks(id string, trackIDsToAdd []string) error {
|
func (j *jellyfinMediaProvider) AddPlaylistTracks(id string, trackIDsToAdd []string) error {
|
||||||
return j.client.AddSongsToPlaylist(id, trackIDsToAdd)
|
return j.client.AddSongsToPlaylist(id, trackIDsToAdd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,10 +256,12 @@ type MediaProvider interface {
|
|||||||
|
|
||||||
GetPlaylists() ([]*Playlist, error)
|
GetPlaylists() ([]*Playlist, error)
|
||||||
|
|
||||||
CreatePlaylist(name string, trackIDs []string) error
|
CreatePlaylistWithTracks(name string, trackIDs []string) error
|
||||||
|
|
||||||
CanMakePublicPlaylist() bool
|
CanMakePublicPlaylist() bool
|
||||||
|
|
||||||
|
CreatePlaylist(name, description string, public bool) error
|
||||||
|
|
||||||
EditPlaylist(id, name, description string, public bool) error
|
EditPlaylist(id, name, description string, public bool) error
|
||||||
|
|
||||||
AddPlaylistTracks(id string, trackIDsToAdd []string) error
|
AddPlaylistTracks(id string, trackIDsToAdd []string) error
|
||||||
|
|||||||
@@ -62,11 +62,32 @@ func (s *subsonicMediaProvider) SetLibrary(id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
func (s *subsonicMediaProvider) CreatePlaylistWithTracks(name string, trackIDs []string) error {
|
||||||
s.playlistsCached = nil
|
s.playlistsCached = nil
|
||||||
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name})
|
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) CreatePlaylist(name, description string, public bool) error {
|
||||||
|
pl, err := s.client.CreatePlaylist(map[string]string{"name": name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pl == nil || (description == "" && !public) {
|
||||||
|
// Subsonic <= 1.14.0 doesn't return a playlist
|
||||||
|
// Not having the ID, we can't set the description or public property
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make(map[string]string)
|
||||||
|
if description != "" {
|
||||||
|
params["description"] = description
|
||||||
|
}
|
||||||
|
if public {
|
||||||
|
params["public"] = "true"
|
||||||
|
}
|
||||||
|
return s.client.UpdatePlaylist(pl.ID, params)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) DeletePlaylist(id string) error {
|
func (s *subsonicMediaProvider) DeletePlaylist(id string) error {
|
||||||
s.playlistsCached = nil
|
s.playlistsCached = nil
|
||||||
return s.client.DeletePlaylist(id)
|
return s.client.DeletePlaylist(id)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ require (
|
|||||||
github.com/quarckster/go-mpris-server v1.0.3
|
github.com/quarckster/go-mpris-server v1.0.3
|
||||||
github.com/supersonic-app/fyne-lyrics v0.0.0-20250614151306-b1880a70a410
|
github.com/supersonic-app/fyne-lyrics v0.0.0-20250614151306-b1880a70a410
|
||||||
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449
|
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449
|
||||||
github.com/supersonic-app/go-subsonic v0.0.0-20250913173646-cf4fceb19b43
|
github.com/supersonic-app/go-subsonic v0.0.0-20250917154259-2bbe535cf8f3
|
||||||
github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5
|
github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5
|
||||||
github.com/zalando/go-keyring v0.2.6
|
github.com/zalando/go-keyring v0.2.6
|
||||||
golang.org/x/net v0.38.0
|
golang.org/x/net v0.38.0
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ github.com/supersonic-app/go-glfw/v3.3/glfw v0.0.0-20250906235349-c09e5a2f6b75 h
|
|||||||
github.com/supersonic-app/go-glfw/v3.3/glfw v0.0.0-20250906235349-c09e5a2f6b75/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
|
github.com/supersonic-app/go-glfw/v3.3/glfw v0.0.0-20250906235349-c09e5a2f6b75/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
|
||||||
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449 h1:UHIPI43VzyWuIF8z8WVMdjMsC/orZTZQJraD/Iv+ghw=
|
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449 h1:UHIPI43VzyWuIF8z8WVMdjMsC/orZTZQJraD/Iv+ghw=
|
||||||
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449/go.mod h1:1bQz6kBQumJopXEbkiqoLxIXLy7F7yWFBvknvpAtIC0=
|
github.com/supersonic-app/go-mpv v0.1.1-0.20250822102843-7a8cde5f5449/go.mod h1:1bQz6kBQumJopXEbkiqoLxIXLy7F7yWFBvknvpAtIC0=
|
||||||
github.com/supersonic-app/go-subsonic v0.0.0-20250913173646-cf4fceb19b43 h1:Ds3zznwB8v2EowUg+GPk2y+IDzUAujv522EN8H6XNdk=
|
github.com/supersonic-app/go-subsonic v0.0.0-20250917154259-2bbe535cf8f3 h1:RuCWkkDc0THP9Y5wbPG8q0Or5zT5cxfLdVzznX+hXbA=
|
||||||
github.com/supersonic-app/go-subsonic v0.0.0-20250913173646-cf4fceb19b43/go.mod h1:ClhAgC2qobwH19D6kdwULLYSHlOmldDGkdu2G2Z0Ijo=
|
github.com/supersonic-app/go-subsonic v0.0.0-20250917154259-2bbe535cf8f3/go.mod h1:ClhAgC2qobwH19D6kdwULLYSHlOmldDGkdu2G2Z0Ijo=
|
||||||
github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5 h1:aoUJKPFD/ZrNZjK6fl2Xhazwsttx42esKxhSSzEE3Bo=
|
github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5 h1:aoUJKPFD/ZrNZjK6fl2Xhazwsttx42esKxhSSzEE3Bo=
|
||||||
github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5/go.mod h1:ibt19zDV5/vvF14jHJpTv3AOorq1EbmrMAubxnuvR5Y=
|
github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5/go.mod h1:ibt19zDV5/vvF14jHJpTv3AOorq1EbmrMAubxnuvR5Y=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type PlaylistsPage struct {
|
|||||||
searchedPlaylists []*mediaprovider.Playlist
|
searchedPlaylists []*mediaprovider.Playlist
|
||||||
|
|
||||||
viewToggle *widgets.ToggleButtonGroup
|
viewToggle *widgets.ToggleButtonGroup
|
||||||
|
newBtn *widget.Button
|
||||||
searcher *widgets.SearchEntry
|
searcher *widgets.SearchEntry
|
||||||
titleDisp *widget.RichText
|
titleDisp *widget.RichText
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
@@ -86,6 +87,9 @@ func newPlaylistsPage(
|
|||||||
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResListSvg), a.showListView),
|
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResListSvg), a.showListView),
|
||||||
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResGridSvg), a.showGridView))
|
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResGridSvg), a.showGridView))
|
||||||
a.viewToggle.SetActivatedButton(activeView)
|
a.viewToggle.SetActivatedButton(activeView)
|
||||||
|
a.newBtn = widget.NewButtonWithIcon(lang.L("New Playlist"), theme.ContentAddIcon(), func() {
|
||||||
|
a.contr.DoCreatePlaylistWorkflow()
|
||||||
|
})
|
||||||
if activeView == 0 {
|
if activeView == 0 {
|
||||||
a.createListView()
|
a.createListView()
|
||||||
a.buildContainer(a.listView)
|
a.buildContainer(a.listView)
|
||||||
@@ -98,6 +102,21 @@ func newPlaylistsPage(
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) {
|
||||||
|
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
||||||
|
a.container = container.New(&layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, TopPadding: 5, BottomPadding: 15},
|
||||||
|
container.NewBorder(
|
||||||
|
container.NewHBox(
|
||||||
|
a.titleDisp,
|
||||||
|
container.NewCenter(a.viewToggle),
|
||||||
|
util.NewHSpace(2),
|
||||||
|
container.NewCenter(a.newBtn),
|
||||||
|
layout.NewSpacer(),
|
||||||
|
searchVbox,
|
||||||
|
),
|
||||||
|
nil, nil, nil, initialView))
|
||||||
|
}
|
||||||
|
|
||||||
var _ Scrollable = (*PlaylistsPage)(nil)
|
var _ Scrollable = (*PlaylistsPage)(nil)
|
||||||
|
|
||||||
func (p *PlaylistsPage) Scroll(scrollAmt float32) {
|
func (p *PlaylistsPage) Scroll(scrollAmt float32) {
|
||||||
@@ -317,14 +336,6 @@ func (s *savedPlaylistsPage) Restore() Page {
|
|||||||
return newPlaylistsPage(s.contr, s.pool, s.cfg, s.mp, s.searchText, s.activeView, s.listSort, s.listScrollPos, s.gridScrollPos)
|
return newPlaylistsPage(s.contr, s.pool, s.cfg, s.mp, s.searchText, s.activeView, s.listSort, s.listScrollPos, s.gridScrollPos)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) {
|
|
||||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
|
||||||
a.container = container.New(&layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, TopPadding: 5, BottomPadding: 15},
|
|
||||||
container.NewBorder(
|
|
||||||
container.NewHBox(a.titleDisp, container.NewCenter(a.viewToggle), layout.NewSpacer(), searchVbox),
|
|
||||||
nil, nil, nil, initialView))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {
|
func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {
|
||||||
return widget.NewSimpleRenderer(a.container)
|
return widget.NewSimpleRenderer(a.container)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
|||||||
m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates
|
m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates
|
||||||
if id == "" /* creating new playlist */ {
|
if id == "" /* creating new playlist */ {
|
||||||
go func() {
|
go func() {
|
||||||
err := m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs)
|
err := m.App.ServerManager.Server.CreatePlaylistWithTracks(sp.SearchDialog.SearchQuery(), trackIDs)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
notifySuccess(len(trackIDs))
|
notifySuccess(len(trackIDs))
|
||||||
} else {
|
} else {
|
||||||
@@ -132,8 +132,13 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
|
|||||||
pop.Hide()
|
pop.Hide()
|
||||||
m.doModalClosed()
|
m.doModalClosed()
|
||||||
go func() {
|
go func() {
|
||||||
err := m.App.ServerManager.Server.EditPlaylist(playlist.ID, dlg.Name, dlg.Description, dlg.IsPublic)
|
s := m.App.ServerManager.GetServer()
|
||||||
|
if s == nil {
|
||||||
|
return // logged out
|
||||||
|
}
|
||||||
|
err := s.EditPlaylist(playlist.ID, dlg.Name, dlg.Description, dlg.IsPublic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fyne.Do(func() { m.ToastProvider.ShowErrorToast(lang.L("Error updating playlist")) })
|
||||||
log.Printf("error updating playlist: %s", err.Error())
|
log.Printf("error updating playlist: %s", err.Error())
|
||||||
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
||||||
// if user is on playlist page, reload to get the updates
|
// if user is on playlist page, reload to get the updates
|
||||||
@@ -144,3 +149,33 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
|
|||||||
m.haveModal = true
|
m.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Controller) DoCreatePlaylistWorkflow() {
|
||||||
|
canMakePublic := m.App.ServerManager.Server.CanMakePublicPlaylist()
|
||||||
|
dlg := dialogs.NewCreatePlaylistDialog(canMakePublic)
|
||||||
|
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
||||||
|
m.ClosePopUpOnEscape(pop)
|
||||||
|
dlg.OnCanceled = func() {
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
}
|
||||||
|
dlg.OnUpdateMetadata = func() {
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
go func() {
|
||||||
|
s := m.App.ServerManager.GetServer()
|
||||||
|
if s == nil {
|
||||||
|
return // logged out
|
||||||
|
}
|
||||||
|
err := s.CreatePlaylist(dlg.Name, dlg.Description, dlg.IsPublic)
|
||||||
|
if err != nil {
|
||||||
|
fyne.Do(func() { m.ToastProvider.ShowErrorToast(lang.L("Error creating playlist")) })
|
||||||
|
log.Printf("error creating playlist: %s", err.Error())
|
||||||
|
} else {
|
||||||
|
fyne.Do(func() { m.ToastProvider.ShowSuccessToast(lang.L("Successfully created playlist")) })
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
m.haveModal = true
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,10 +26,19 @@ type EditPlaylistDialog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck bool) *EditPlaylistDialog {
|
func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck bool) *EditPlaylistDialog {
|
||||||
e := &EditPlaylistDialog{
|
return newEditPlaylistDialog(playlist, showPublicCheck)
|
||||||
IsPublic: playlist.Public,
|
}
|
||||||
Name: playlist.Name,
|
|
||||||
Description: playlist.Description,
|
func NewCreatePlaylistDialog(showPublicCheck bool) *EditPlaylistDialog {
|
||||||
|
return newEditPlaylistDialog(nil, showPublicCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck bool) *EditPlaylistDialog {
|
||||||
|
e := &EditPlaylistDialog{}
|
||||||
|
if playlist != nil {
|
||||||
|
e.IsPublic = playlist.Public
|
||||||
|
e.Name = playlist.Name
|
||||||
|
e.Description = playlist.Description
|
||||||
}
|
}
|
||||||
e.ExtendBaseWidget(e)
|
e.ExtendBaseWidget(e)
|
||||||
|
|
||||||
@@ -42,6 +51,7 @@ func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck boo
|
|||||||
e.OnDeletePlaylist()
|
e.OnDeletePlaylist()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
deleteBtn.Hidden = playlist == nil
|
||||||
submitBtn := widget.NewButtonWithIcon(lang.L("OK"), theme.ConfirmIcon(), func() {
|
submitBtn := widget.NewButtonWithIcon(lang.L("OK"), theme.ConfirmIcon(), func() {
|
||||||
if e.OnUpdateMetadata != nil {
|
if e.OnUpdateMetadata != nil {
|
||||||
e.OnUpdateMetadata()
|
e.OnUpdateMetadata()
|
||||||
|
|||||||
Reference in New Issue
Block a user