272 lines
7.5 KiB
Go
272 lines
7.5 KiB
Go
package model
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/bjarneo/cliamp/history"
|
|
"github.com/bjarneo/cliamp/playlist"
|
|
"github.com/bjarneo/cliamp/provider"
|
|
"github.com/bjarneo/cliamp/ui"
|
|
)
|
|
|
|
func (m *Model) openPlaylistPicker(tracks []playlist.Track, title string) {
|
|
if m.localProvider == nil {
|
|
m.status.Show("Local playlists are unavailable", statusTTLDefault)
|
|
return
|
|
}
|
|
lists, err := m.localProvider.Playlists()
|
|
if err != nil {
|
|
m.status.Showf(statusTTLDefault, "Playlist list failed: %s", err)
|
|
return
|
|
}
|
|
playlists := make([]playlist.PlaylistInfo, 0, len(lists))
|
|
for _, pl := range lists {
|
|
if pl.Name != history.PlaylistName {
|
|
playlists = append(playlists, pl)
|
|
}
|
|
}
|
|
m.plPicker = playlistPickerState{
|
|
visible: true,
|
|
screen: plPickerChoose,
|
|
playlists: playlists,
|
|
tracks: append([]playlist.Track(nil), tracks...),
|
|
title: title,
|
|
}
|
|
m.refreshChrome()
|
|
m.applyHeightMode()
|
|
m.plPickerMaybeAdjustScroll(m.plPickerVisible())
|
|
}
|
|
|
|
func (m *Model) closePlaylistPicker() {
|
|
m.plPicker = playlistPickerState{}
|
|
m.refreshChrome()
|
|
m.applyHeightMode()
|
|
}
|
|
|
|
func (m *Model) plPickerCount() int {
|
|
return len(m.plPicker.playlists) + 1
|
|
}
|
|
|
|
func (m *Model) plPickerVisible() int {
|
|
return m.effectivePlaylistVisible()
|
|
}
|
|
|
|
func (m *Model) plPickerMaybeAdjustScroll(visible int) {
|
|
clampScroll(&m.plPicker.cursor, &m.plPicker.scroll, m.plPickerCount(), visible)
|
|
}
|
|
|
|
func (m Model) plPickerHeaderLine() string {
|
|
if m.plPicker.screen == plPickerNewName {
|
|
return promptHeader("New Playlist", m.plPicker.newName)
|
|
}
|
|
return sepHeaderN("Write to Playlist", m.plPicker.cursor+1, m.plPickerCount())
|
|
}
|
|
|
|
func (m Model) plPickerHelpLine() string {
|
|
if m.plPicker.screen == plPickerNewName {
|
|
if len(m.plPicker.tracks) == 0 {
|
|
return helpKey("Enter", "Create ") + helpKey("Esc", "Cancel")
|
|
}
|
|
return helpKey("Enter", "Create & add ") + helpKey("Esc", "Cancel")
|
|
}
|
|
return helpKey("↓↑", "Select ") + helpKey("Enter", "Write ") + helpKey("Esc", "Cancel")
|
|
}
|
|
|
|
func (m Model) renderPlaylistPickerBody() string {
|
|
budget := m.effectivePlaylistVisible()
|
|
if m.plPicker.screen == plPickerNewName {
|
|
msg := "Create an empty playlist."
|
|
if n := len(m.plPicker.tracks); n == 1 {
|
|
msg = "Create and add: " + truncate(m.plPicker.tracks[0].DisplayName(), max(1, ui.PanelWidth-18))
|
|
} else if n > 1 {
|
|
msg = fmt.Sprintf("Create and add %d tracks.", n)
|
|
}
|
|
return bodyMessage(msg, budget)
|
|
}
|
|
|
|
items := make([]string, len(m.plPicker.playlists)+1)
|
|
for i, pl := range m.plPicker.playlists {
|
|
items[i] = playlistLabel("", pl)
|
|
}
|
|
items[len(items)-1] = "+ New Playlist..."
|
|
|
|
var head string
|
|
switch n := len(m.plPicker.tracks); {
|
|
case m.plPicker.title != "":
|
|
head = m.plPicker.title
|
|
case n == 0:
|
|
head = "No tracks selected. Choose + New Playlist to create an empty one."
|
|
case n == 1:
|
|
head = "Track: " + m.plPicker.tracks[0].DisplayName()
|
|
default:
|
|
head = fmt.Sprintf("%d tracks selected", n)
|
|
}
|
|
head = dimStyle.Render(" " + truncate(head, max(1, ui.PanelWidth-2)))
|
|
list := windowList(items, m.plPicker.cursor, m.plPicker.scroll, max(0, budget-1))
|
|
return strings.Join([]string{head, list}, "\n")
|
|
}
|
|
|
|
func (m *Model) handlePlaylistPickerKey(msg tea.KeyPressMsg) tea.Cmd {
|
|
if m.plPicker.screen == plPickerNewName {
|
|
return m.handlePlaylistPickerNewNameKey(msg)
|
|
}
|
|
|
|
count := m.plPickerCount()
|
|
switch msg.String() {
|
|
case "ctrl+c":
|
|
m.closePlaylistPicker()
|
|
return m.quit()
|
|
case "esc", "backspace", "q":
|
|
m.closePlaylistPicker()
|
|
case "ctrl+x":
|
|
m.toggleExpandedView()
|
|
m.plPickerMaybeAdjustScroll(m.plPickerVisible())
|
|
case "up", "k":
|
|
if m.plPicker.cursor > 0 {
|
|
m.plPicker.cursor--
|
|
} else if count > 0 {
|
|
m.plPicker.cursor = count - 1
|
|
}
|
|
m.plPickerMaybeAdjustScroll(m.plPickerVisible())
|
|
case "down", "j":
|
|
if m.plPicker.cursor < count-1 {
|
|
m.plPicker.cursor++
|
|
} else if count > 0 {
|
|
m.plPicker.cursor = 0
|
|
}
|
|
m.plPickerMaybeAdjustScroll(m.plPickerVisible())
|
|
case "pgup", "ctrl+u":
|
|
if m.plPicker.cursor > 0 {
|
|
visible := m.plPickerVisible()
|
|
m.plPicker.cursor -= min(m.plPicker.cursor, visible)
|
|
m.plPickerMaybeAdjustScroll(visible)
|
|
}
|
|
case "pgdown", "ctrl+d":
|
|
if m.plPicker.cursor < count-1 {
|
|
visible := m.plPickerVisible()
|
|
m.plPicker.cursor = min(count-1, m.plPicker.cursor+visible)
|
|
m.plPickerMaybeAdjustScroll(visible)
|
|
}
|
|
case "home", "g":
|
|
m.plPicker.cursor = 0
|
|
m.plPickerMaybeAdjustScroll(m.plPickerVisible())
|
|
case "end", "G":
|
|
if count > 0 {
|
|
m.plPicker.cursor = count - 1
|
|
}
|
|
m.plPickerMaybeAdjustScroll(m.plPickerVisible())
|
|
case "enter":
|
|
if m.plPicker.cursor < len(m.plPicker.playlists) {
|
|
m.writePickerTracks(m.plPicker.playlists[m.plPicker.cursor].Name)
|
|
m.closePlaylistPicker()
|
|
return nil
|
|
}
|
|
m.plPicker.screen = plPickerNewName
|
|
m.plPicker.newName = ""
|
|
m.plPicker.cursor = 0
|
|
m.plPicker.scroll = 0
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) handlePlaylistPickerNewNameKey(msg tea.KeyPressMsg) tea.Cmd {
|
|
switch msg.Code {
|
|
case tea.KeyEscape:
|
|
m.plPicker.screen = plPickerChoose
|
|
m.plPicker.cursor = len(m.plPicker.playlists)
|
|
m.plPickerMaybeAdjustScroll(m.plPickerVisible())
|
|
case tea.KeyEnter:
|
|
name := strings.TrimSpace(m.plPicker.newName)
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
m.createPickerPlaylist(name)
|
|
m.closePlaylistPicker()
|
|
case tea.KeyBackspace:
|
|
m.plPicker.newName = removeLastRune(m.plPicker.newName)
|
|
case tea.KeySpace:
|
|
m.plPicker.newName += " "
|
|
default:
|
|
if len(msg.Text) > 0 {
|
|
m.plPicker.newName += msg.Text
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) createPickerPlaylist(name string) {
|
|
c, ok := m.localProvider.(provider.PlaylistCreator)
|
|
if !ok {
|
|
m.status.Show("Playlist creation is not supported", statusTTLDefault)
|
|
return
|
|
}
|
|
id, err := c.CreatePlaylist(context.Background(), name)
|
|
if err != nil {
|
|
m.status.Showf(statusTTLDefault, "Create failed: %s", err)
|
|
return
|
|
}
|
|
if len(m.plPicker.tracks) == 0 {
|
|
m.status.Showf(statusTTLDefault, "Created %q", name)
|
|
m.refreshPlaylistManagerAfterWrite(id)
|
|
return
|
|
}
|
|
m.writePickerTracks(id)
|
|
}
|
|
|
|
func (m *Model) writePickerTracks(name string) {
|
|
added, skipped, err := m.writeTracksToPlaylist(name, m.plPicker.tracks)
|
|
if err != nil {
|
|
m.status.Showf(statusTTLDefault, "Write failed: %s", err)
|
|
return
|
|
}
|
|
switch {
|
|
case added > 0 && skipped > 0:
|
|
m.status.Showf(statusTTLBatch, "Added %d to %q, skipped %d duplicates", added, name, skipped)
|
|
case added > 0:
|
|
m.status.Showf(statusTTLDefault, "Added %d to %q", added, name)
|
|
case skipped > 0:
|
|
m.status.Showf(statusTTLDefault, "Skipped %d duplicates in %q", skipped, name)
|
|
default:
|
|
m.status.Showf(statusTTLDefault, "Nothing added to %q", name)
|
|
}
|
|
m.refreshPlaylistManagerAfterWrite(name)
|
|
}
|
|
|
|
func (m *Model) writeTracksToPlaylist(name string, tracks []playlist.Track) (added, skipped int, err error) {
|
|
if len(tracks) == 0 {
|
|
return 0, 0, nil
|
|
}
|
|
if bw, ok := m.localProvider.(provider.PlaylistBatchWriter); ok {
|
|
return bw.AddTracksToPlaylist(context.Background(), name, tracks)
|
|
}
|
|
w, ok := m.localProvider.(provider.PlaylistWriter)
|
|
if !ok {
|
|
return 0, 0, fmt.Errorf("playlist writes are not supported")
|
|
}
|
|
for _, track := range tracks {
|
|
if err := w.AddTrackToPlaylist(context.Background(), name, track); err != nil {
|
|
return added, skipped, err
|
|
}
|
|
added++
|
|
}
|
|
return added, skipped, nil
|
|
}
|
|
|
|
func (m *Model) refreshPlaylistManagerAfterWrite(name string) {
|
|
if !m.plManager.visible {
|
|
return
|
|
}
|
|
m.plMgrRefreshList()
|
|
if m.plManager.screen == plMgrScreenTracks && m.plManager.selPlaylist == name {
|
|
if tracks, err := m.localProvider.Tracks(name); err == nil {
|
|
m.plManager.tracks = tracks
|
|
m.plMgrRecomputeFilter()
|
|
m.plMgrTracksMaybeAdjustScroll(m.plMgrTracksVisible())
|
|
}
|
|
}
|
|
}
|