package tui import ( "fmt" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "tuistream-play/internal/api" ) func (m Model) updateBrowse(msg tea.Msg) (tea.Model, tea.Cmd) { // Nothing loaded yet (initial Views fetch in flight). if len(m.stack) == 0 { if key, ok := msg.(tea.KeyMsg); ok && (key.String() == "ctrl+c" || key.String() == "q") { return m, tea.Quit } return m, nil } cur := &m.stack[len(m.stack)-1] filtering := cur.FilterState() == list.Filtering if key, ok := msg.(tea.KeyMsg); ok && !filtering { switch key.String() { case "ctrl+c", "q": return m, tea.Quit case "esc", "backspace", "h", "left": if len(m.stack) > 1 { m.stack = m.stack[:len(m.stack)-1] } return m, nil case "c": return m, m.loadResume() case "enter", "l", "right": sel, ok := cur.SelectedItem().(listItem) if !ok { return m, nil } it := sel.item if it.Playable() { m.status = "Starting mpv…" // Capture the sibling list as the play queue so audio can // auto-advance. Built from the current level's items in their // displayed order. m.queue, m.queueIdx = currentItems(*cur), cur.Index() return m, m.startPlayback(it) } return m, m.loadChildren(displayTitle(it), it.ID) } } var cmd tea.Cmd m.stack[len(m.stack)-1], cmd = cur.Update(msg) return m, cmd } func (m Model) viewBrowse() string { if len(m.stack) == 0 { return lipgloss.JoinVertical(lipgloss.Left, m.headerBar(), "", m.st.muted.Render(" Loading libraries…")) } cur := m.stack[len(m.stack)-1] help := "enter play/open · c continue · / filter · esc back · q quit" return lipgloss.JoinVertical(lipgloss.Left, m.headerBar(), "", cur.View(), m.footerBar(help, m.status), ) } // currentItems extracts the api.Items from a list, in display order, so the // playback queue mirrors exactly what the user sees. func currentItems(l list.Model) []api.Item { rows := l.Items() out := make([]api.Item, 0, len(rows)) for _, r := range rows { if li, ok := r.(listItem); ok { out = append(out, li.item) } } return out } // ---- display helpers ---- func displayTitle(it api.Item) string { switch it.Type { case "Episode": if it.ParentIndexNumber > 0 && it.IndexNumber > 0 { return fmt.Sprintf("S%02dE%02d %s", it.ParentIndexNumber, it.IndexNumber, it.Name) } if it.IndexNumber > 0 { return fmt.Sprintf("%d. %s", it.IndexNumber, it.Name) } case "Audio": if it.IndexNumber > 0 { return fmt.Sprintf("%d. %s", it.IndexNumber, it.Name) } case "Movie", "Series": if it.ProductionYear > 0 { return fmt.Sprintf("%s (%d)", it.Name, it.ProductionYear) } } return it.Name } func displayDesc(it api.Item) string { var parts []string switch it.Type { case "CollectionFolder", "UserView": if it.CollectionType != "" { parts = append(parts, it.CollectionType) } else { parts = append(parts, "library") } case "Episode": if it.SeriesName != "" { parts = append(parts, it.SeriesName) } case "Series", "Season": parts = append(parts, "folder") default: if it.IsFolder { parts = append(parts, "folder") } } if d := formatDuration(it.RunTimeTicks); d != "" { parts = append(parts, d) } if it.UserData.Played { parts = append(parts, "✓ watched") } else if it.UserData.PlayedPercentage > 1 { parts = append(parts, fmt.Sprintf("%.0f%%", it.UserData.PlayedPercentage)) } if len(parts) == 0 { return " " } out := parts[0] for _, p := range parts[1:] { out += " · " + p } return out } // formatDuration turns RunTimeTicks (100ns units) into "1h 23m" / "12m". func formatDuration(ticks int64) string { if ticks <= 0 { return "" } secs := ticks / 1e7 h := secs / 3600 mn := (secs % 3600) / 60 if h > 0 { return fmt.Sprintf("%dh %02dm", h, mn) } return fmt.Sprintf("%dm", mn) }