230 lines
6.5 KiB
Go
230 lines
6.5 KiB
Go
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/bjarneo/cliamp/ui"
|
|
)
|
|
|
|
// — provider browser (nav) —
|
|
|
|
type navViewKind int
|
|
|
|
const (
|
|
navViewMenu navViewKind = iota
|
|
navViewArtists
|
|
navViewAlbums
|
|
navViewTracks
|
|
)
|
|
|
|
// navView collapses the nav browser's mode + screen into the list actually
|
|
// shown, so the header, help, and body all agree.
|
|
func (m Model) navView() navViewKind {
|
|
switch m.navBrowser.mode {
|
|
case navBrowseModeByAlbum:
|
|
if m.navBrowser.screen == navBrowseScreenTracks {
|
|
return navViewTracks
|
|
}
|
|
return navViewAlbums
|
|
case navBrowseModeByArtist:
|
|
if m.navBrowser.screen == navBrowseScreenTracks {
|
|
return navViewTracks
|
|
}
|
|
return navViewArtists
|
|
case navBrowseModeByArtistAlbum:
|
|
switch m.navBrowser.screen {
|
|
case navBrowseScreenAlbums:
|
|
return navViewAlbums
|
|
case navBrowseScreenTracks:
|
|
return navViewTracks
|
|
default:
|
|
return navViewArtists
|
|
}
|
|
default:
|
|
return navViewMenu
|
|
}
|
|
}
|
|
|
|
func (m Model) navTrackBreadcrumb() string {
|
|
switch m.navBrowser.mode {
|
|
case navBrowseModeByArtist:
|
|
return "Artist: " + m.navBrowser.selArtist.Name
|
|
case navBrowseModeByAlbum:
|
|
return "Album: " + m.navBrowser.selAlbum.Name
|
|
case navBrowseModeByArtistAlbum:
|
|
return m.navBrowser.selArtist.Name + " / " + m.navBrowser.selAlbum.Name
|
|
}
|
|
return "Tracks"
|
|
}
|
|
|
|
func (m Model) navHeaderLine() string {
|
|
if m.navBrowser.searching {
|
|
return filterPromptHeader(m.navBrowser.search)
|
|
}
|
|
switch m.navView() {
|
|
case navViewArtists:
|
|
return sepHeaderN("Artists", m.navBrowser.cursor+1, len(m.navBrowser.artists))
|
|
case navViewAlbums:
|
|
label := "Albums"
|
|
if m.navBrowser.mode == navBrowseModeByArtistAlbum {
|
|
label = "Albums: " + m.navBrowser.selArtist.Name
|
|
} else if s := m.navSortLabel(m.navBrowser.sortType); s != "" {
|
|
label += " Sort: " + s
|
|
}
|
|
return sepHeaderN(label, m.navBrowser.cursor+1, len(m.navBrowser.albums))
|
|
case navViewTracks:
|
|
return sepHeaderN(m.navTrackBreadcrumb(), m.navBrowser.cursor+1, len(m.navBrowser.tracks))
|
|
default:
|
|
name := "Browse"
|
|
if m.navBrowser.prov != nil {
|
|
name = m.navBrowser.prov.Name()
|
|
}
|
|
return sepHeader(name)
|
|
}
|
|
}
|
|
|
|
func (m Model) navHelpLine() string {
|
|
if m.navBrowser.searching {
|
|
return helpKey("Enter", "Confirm ") + helpKey("Esc", "Cancel ") + helpKey("Type", "Filter")
|
|
}
|
|
switch m.navView() {
|
|
case navViewArtists:
|
|
return helpKey("←↓↑→", "Navigate ") + helpKey("Enter", "Open ") + helpKey("/", "Search")
|
|
case navViewAlbums:
|
|
h := helpKey("←↓↑→", "Navigate ") + helpKey("Enter", "Open ")
|
|
if m.navBrowser.mode == navBrowseModeByAlbum {
|
|
h += helpKey("s", "Sort ")
|
|
}
|
|
return h + helpKey("/", "Search")
|
|
case navViewTracks:
|
|
return helpKey("←↓↑→", "Navigate ") + helpKey("Enter", "Play from here ") +
|
|
helpKey("q", "Queue ") + helpKey("R", "Replace ") + helpKey("a", "Append ") + helpKey("/", "Search")
|
|
default:
|
|
return helpKey("↓↑", "Scroll ") + helpKey("Enter", "Select ") + helpKey("Esc", "Close")
|
|
}
|
|
}
|
|
|
|
func (m Model) renderNavBody() string {
|
|
budget := m.effectivePlaylistVisible()
|
|
switch m.navView() {
|
|
case navViewArtists:
|
|
if m.navBrowser.loading && len(m.navBrowser.artists) == 0 {
|
|
return bodyLines([]string{loadingLine("Loading artists…")}, budget)
|
|
}
|
|
if len(m.navBrowser.artists) == 0 {
|
|
return bodyMessage("No artists found.", budget)
|
|
}
|
|
items := m.navScrollItems(len(m.navBrowser.artists), func(i int) string {
|
|
a := m.navBrowser.artists[i]
|
|
return truncate(fmt.Sprintf("%s (%d albums)", a.Name, a.AlbumCount), ui.PanelWidth-6)
|
|
})
|
|
return strings.Join(items, "\n")
|
|
case navViewAlbums:
|
|
if m.navBrowser.loading && len(m.navBrowser.albums) == 0 {
|
|
return bodyLines([]string{loadingLine("Loading albums…")}, budget)
|
|
}
|
|
if len(m.navBrowser.albums) == 0 {
|
|
return bodyMessage("No albums found.", budget)
|
|
}
|
|
items := m.navScrollItems(len(m.navBrowser.albums), func(i int) string {
|
|
a := m.navBrowser.albums[i]
|
|
if a.Year > 0 {
|
|
return truncate(fmt.Sprintf("%s — %s (%d)", a.Name, a.Artist, a.Year), ui.PanelWidth-6)
|
|
}
|
|
return truncate(fmt.Sprintf("%s — %s", a.Name, a.Artist), ui.PanelWidth-6)
|
|
})
|
|
return strings.Join(items, "\n")
|
|
case navViewTracks:
|
|
return m.renderNavTrackBody(budget)
|
|
default:
|
|
items := []string{"By Album", "By Artist", "By Artist / Album"}
|
|
return windowList(items, m.navBrowser.cursor, 0, budget)
|
|
}
|
|
}
|
|
|
|
func (m Model) renderNavTrackBody(budget int) string {
|
|
if m.navBrowser.loading && len(m.navBrowser.tracks) == 0 {
|
|
return bodyLines([]string{loadingLine("Loading tracks…")}, budget)
|
|
}
|
|
if len(m.navBrowser.tracks) == 0 {
|
|
return bodyMessage("No tracks found.", budget)
|
|
}
|
|
|
|
if len(m.navBrowser.searchIdx) > 0 || m.navBrowser.search != "" {
|
|
items := m.navScrollItems(len(m.navBrowser.tracks), func(i int) string {
|
|
t := m.navBrowser.tracks[i]
|
|
return formatTrackRow(i+1, t.DisplayName()+trackAlbumSuffix(t, m.showAlbumHeaders), t.DurationSecs)
|
|
})
|
|
return strings.Join(items, "\n")
|
|
}
|
|
|
|
return m.renderTrackRowsBody(m.navBrowser.tracks, m.navBrowser.cursor, m.navBrowser.scroll, budget)
|
|
}
|
|
|
|
// — file browser —
|
|
|
|
func (m Model) fbHeaderLine() string {
|
|
if m.fileBrowser.searching {
|
|
return filterPromptHeader(m.fileBrowser.search)
|
|
}
|
|
label := "Files: " + m.fileBrowser.dir
|
|
if n := len(m.fileBrowser.selected); n > 0 {
|
|
label += fmt.Sprintf(" [%d selected]", n)
|
|
}
|
|
return sepHeader(label)
|
|
}
|
|
|
|
func (m Model) renderFileBrowserBody() string {
|
|
budget := m.effectivePlaylistVisible()
|
|
if budget <= 0 {
|
|
return ""
|
|
}
|
|
|
|
var lines []string
|
|
if m.fileBrowser.err != "" {
|
|
lines = append(lines, errorStyle.Render(" "+m.fileBrowser.err))
|
|
}
|
|
|
|
count := m.fbCount()
|
|
if count == 0 {
|
|
if m.fileBrowser.search != "" {
|
|
lines = append(lines, dimStyle.Render(" No matches"))
|
|
} else {
|
|
lines = append(lines, dimStyle.Render(" (empty)"))
|
|
}
|
|
return bodyLines(lines, budget)
|
|
}
|
|
|
|
scroll := max(m.fileBrowser.scroll, 0)
|
|
if scroll > count-1 {
|
|
scroll = max(0, count-1)
|
|
}
|
|
for i := scroll; i < count && len(lines) < budget; i++ {
|
|
e := m.fbEntry(i)
|
|
check := " "
|
|
if m.fileBrowser.selected[e.path] {
|
|
check = "✓ "
|
|
}
|
|
suffix := ""
|
|
if e.isAudio {
|
|
suffix = " ♫"
|
|
}
|
|
label := truncate(check+e.name+suffix, max(1, ui.PanelWidth-2))
|
|
|
|
switch {
|
|
case m.fileBrowser.searching:
|
|
lines = append(lines, dimStyle.Render(" "+label))
|
|
case i == m.fileBrowser.cursor:
|
|
lines = append(lines, playlistSelectedStyle.Render("> "+label))
|
|
case e.isDir:
|
|
lines = append(lines, trackStyle.Render(" "+label))
|
|
case e.isAudio:
|
|
lines = append(lines, playlistItemStyle.Render(" "+label))
|
|
default:
|
|
lines = append(lines, dimStyle.Render(" "+label))
|
|
}
|
|
}
|
|
return bodyLines(lines, budget)
|
|
}
|