Merge pull request #723 from dweymouth/feature/new-playlist-btn

Add a Create Playlist button to the Playlists page
This commit is contained in:
Drew Weymouth
2025-09-28 15:44:34 -07:00
committed by GitHub
10 changed files with 128 additions and 27 deletions
@@ -77,8 +77,8 @@ func (j *jellyfinMediaProvider) SetLibrary(id string) error {
return nil
}
func (j *jellyfinMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
return j.client.CreatePlaylist(name, trackIDs)
func (j *jellyfinMediaProvider) CreatePlaylistWithTracks(name string, trackIDs []string) error {
return j.client.CreatePlaylist(name, "", false, trackIDs)
}
func (j *jellyfinMediaProvider) DeletePlaylist(id string) error {
@@ -90,7 +90,11 @@ func (j *jellyfinMediaProvider) CanMakePublicPlaylist() bool {
}
func (j *jellyfinMediaProvider) EditPlaylist(id, name, description string, public bool) error {
return j.client.UpdatePlaylistMetadata(id, name, description)
return j.client.UpdatePlaylistMetadata(id, name, description, false)
}
func (j *jellyfinMediaProvider) CreatePlaylist(name, description string, public bool) error {
return j.client.CreatePlaylist(name, description, public, nil)
}
func (j *jellyfinMediaProvider) AddPlaylistTracks(id string, trackIDsToAdd []string) error {
+3 -1
View File
@@ -258,10 +258,12 @@ type MediaProvider interface {
GetPlaylists() ([]*Playlist, error)
CreatePlaylist(name string, trackIDs []string) error
CreatePlaylistWithTracks(name string, trackIDs []string) error
CanMakePublicPlaylist() bool
CreatePlaylist(name, description string, public bool) error
EditPlaylist(id, name, description string, public bool) error
AddPlaylistTracks(id string, trackIDsToAdd []string) error
@@ -62,11 +62,32 @@ func (s *subsonicMediaProvider) SetLibrary(id string) error {
return nil
}
func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
func (s *subsonicMediaProvider) CreatePlaylistWithTracks(name string, trackIDs []string) error {
s.playlistsCached = nil
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 {
s.playlistsCached = nil
return s.client.DeletePlaylist(id)
+3 -3
View File
@@ -7,10 +7,11 @@ require (
github.com/20after4/configdir v0.1.1
github.com/Microsoft/go-winio v0.6.2
github.com/cenkalti/dominantcolor v1.0.3
github.com/charlievieth/strcase v0.0.5
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c
github.com/dweymouth/fyne-tooltip v0.3.0
github.com/dweymouth/go-jellyfin v0.0.0-20250914000657-c172d6f678bb
github.com/dweymouth/go-jellyfin v0.0.0-20250928223159-bd2fb9681ef5
github.com/go-audio/audio v1.0.0
github.com/go-audio/wav v1.1.0
github.com/godbus/dbus/v5 v5.1.0
@@ -20,7 +21,7 @@ require (
github.com/quarckster/go-mpris-server v1.0.3
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-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/zalando/go-keyring v0.2.6
golang.org/x/net v0.38.0
@@ -33,7 +34,6 @@ require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/charlievieth/strcase v0.0.5 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
+4 -4
View File
@@ -25,8 +25,8 @@ github.com/dweymouth/fyne-tooltip v0.3.0 h1:NKCyTkh9NtvnTsiHtTOtaJzRDOFYP8AckQ2t
github.com/dweymouth/fyne-tooltip v0.3.0/go.mod h1:m04ShLW/Tp6LXrNieTumApvNgo7YSB+wi+jZTN+kDBU=
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4 h1:Q3r94AcVL8yaF4Nrd3EQFKyLL3UN/zHPkj+My9B2vCQ=
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
github.com/dweymouth/go-jellyfin v0.0.0-20250914000657-c172d6f678bb h1:KNCB0eDnrT81L5Up6y5jp91fnih3Yh6l1+vYy7JicEE=
github.com/dweymouth/go-jellyfin v0.0.0-20250914000657-c172d6f678bb/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
github.com/dweymouth/go-jellyfin v0.0.0-20250928223159-bd2fb9681ef5 h1:Or5VJodg7cGmdnBIcS+FrEH0twBi7mZsFCz6V0vCOQI=
github.com/dweymouth/go-jellyfin v0.0.0-20250928223159-bd2fb9681ef5/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0 h1:mYcctuWgVArHhSLJxndlUM43C3hoE18BLDBkXKM2tl0=
github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0/go.mod h1:bp2870jtp/ixAJLIOdShBfl1WpyLGDZ57jnVWMgkgIc=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
@@ -134,8 +134,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-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-subsonic v0.0.0-20250913173646-cf4fceb19b43 h1:Ds3zznwB8v2EowUg+GPk2y+IDzUAujv522EN8H6XNdk=
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 h1:RuCWkkDc0THP9Y5wbPG8q0Or5zT5cxfLdVzznX+hXbA=
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/go.mod h1:ibt19zDV5/vvF14jHJpTv3AOorq1EbmrMAubxnuvR5Y=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+3
View File
@@ -88,6 +88,7 @@
"EPs": "EPs",
"Equalizer": "Equalizer",
"Error": "Error",
"Error creating playlist": "Error creating playlist",
"Exclusive mode": "Exclusive mode",
"Favorites": "Favorites",
"Field Recording": "Field Recording",
@@ -132,6 +133,7 @@
"Name": "Name",
"Name (A-Z)": "Name (A-Z)",
"never": "never",
"New Playlist": "New Playlist",
"Next": "Next",
"Nickname": "Nickname",
"Normal": "Normal",
@@ -224,6 +226,7 @@
"Stop after current track": "Stop after current track",
"Stopped": "Stopped",
"Success": "Success",
"Successfully created playlist": "Successfully created playlist",
"Support the project": "Support the project",
"Switch Servers": "Switch Servers",
"Testing connection": "Testing connection",
+3
View File
@@ -73,6 +73,7 @@
"EPs": "EPs",
"Equalizer": "Ecualizador",
"Error": "Error",
"Error creating playlist": "Ha ocurrido un error al crear la lista de reproducción",
"Exclusive mode": "Modo exclusivo",
"Favorites": "Favoritos",
"Field Recording": "Grabación en campo",
@@ -114,6 +115,7 @@
"Name": "Nombre",
"Name (A-Z)": "Nombre (A-Z)",
"never": "nunca",
"New Playlist": "Crear Nueva",
"Next": "Siguiente",
"Nickname": "Apodo",
"Now Playing": "Reproduciendo",
@@ -194,6 +196,7 @@
"Startup page": "Página de inicio",
"Stopped": "Detenida",
"Success": "Éxito",
"Successfully created playlist": "Éxito al crear la lista de reproducción",
"Support the project": "Apoya el proyecto",
"Switch Servers": "Cambiar servidores",
"Testing connection": "Probando conexión",
+19 -8
View File
@@ -36,6 +36,7 @@ type PlaylistsPage struct {
searchedPlaylists []*mediaprovider.Playlist
viewToggle *widgets.ToggleButtonGroup
newBtn *widget.Button
searcher *widgets.SearchEntry
titleDisp *widget.RichText
container *fyne.Container
@@ -86,6 +87,9 @@ func newPlaylistsPage(
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResListSvg), a.showListView),
widget.NewButtonWithIcon("", theme.NewThemedResource(res.ResGridSvg), a.showGridView))
a.viewToggle.SetActivatedButton(activeView)
a.newBtn = widget.NewButtonWithIcon(lang.L("New Playlist"), theme.ContentAddIcon(), func() {
a.contr.DoCreatePlaylistWorkflow()
})
if activeView == 0 {
a.createListView()
a.buildContainer(a.listView)
@@ -98,6 +102,21 @@ func newPlaylistsPage(
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)
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)
}
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 {
return widget.NewSimpleRenderer(a.container)
}
+42 -2
View File
@@ -43,7 +43,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
m.App.Config.Application.AddToPlaylistSkipDuplicates = sp.SkipDuplicates
if id == "" /* creating new playlist */ {
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 {
notifySuccess(len(trackIDs))
} else {
@@ -131,8 +131,13 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
pop.Hide()
m.doModalClosed()
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 {
fyne.Do(func() { m.ToastProvider.ShowErrorToast(lang.L("Error updating playlist")) })
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
@@ -143,3 +148,38 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
m.haveModal = true
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() {
// Right now, this workflow is only initiated by the "New Playlist" button
// on the playlists page. Reload it so the new playlist shows up.
m.ReloadFunc()
m.ToastProvider.ShowSuccessToast(lang.L("Successfully created playlist"))
})
}
}()
}
m.haveModal = true
pop.Show()
}
+22 -5
View File
@@ -26,10 +26,19 @@ type EditPlaylistDialog struct {
}
func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck bool) *EditPlaylistDialog {
e := &EditPlaylistDialog{
IsPublic: playlist.Public,
Name: playlist.Name,
Description: playlist.Description,
return newEditPlaylistDialog(playlist, showPublicCheck)
}
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)
@@ -42,6 +51,7 @@ func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck boo
e.OnDeletePlaylist()
}
})
deleteBtn.Hidden = playlist == nil
submitBtn := widget.NewButtonWithIcon(lang.L("OK"), theme.ConfirmIcon(), func() {
if e.OnUpdateMetadata != nil {
e.OnUpdateMetadata()
@@ -54,7 +64,14 @@ func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck boo
}
})
title := widget.NewLabel(lang.L("Edit Playlist"))
var titleStr string
if playlist == nil {
titleStr = lang.L("New Playlist")
} else {
titleStr = lang.L("Edit Playlist")
}
title := widget.NewLabel(titleStr)
title.Alignment = fyne.TextAlignCenter
title.TextStyle.Bold = true
e.container = container.NewVBox(