Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

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())
}
}
}