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