368 lines
9.2 KiB
Go
368 lines
9.2 KiB
Go
// state.go defines sub-structs that group related fields in the Model,
|
|
// making the overall model scannable and maintainable.
|
|
|
|
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bjarneo/cliamp/applog"
|
|
"github.com/bjarneo/cliamp/lyrics"
|
|
"github.com/bjarneo/cliamp/player"
|
|
"github.com/bjarneo/cliamp/playlist"
|
|
"github.com/bjarneo/cliamp/provider"
|
|
)
|
|
|
|
// searchState holds state for the playlist search overlay.
|
|
type searchState struct {
|
|
active bool
|
|
query string
|
|
results []int // indices into playlist tracks
|
|
cursor int
|
|
scroll int
|
|
}
|
|
|
|
// netSearchScreenType identifies which screen of the net search overlay is active.
|
|
type netSearchScreenType int
|
|
|
|
const (
|
|
netSearchInput netSearchScreenType = iota // typing search query
|
|
netSearchResults // browsing search results
|
|
)
|
|
|
|
// netSearchState holds state for the internet search overlay.
|
|
type netSearchState struct {
|
|
active bool
|
|
screen netSearchScreenType
|
|
query string
|
|
soundcloud bool // true = SoundCloud (scsearch), false = YouTube (ytsearch)
|
|
loading bool
|
|
results []playlist.Track
|
|
cursor int
|
|
scroll int
|
|
err string
|
|
}
|
|
|
|
// provSearchState holds state for filtering the provider playlist list.
|
|
type provSearchState struct {
|
|
active bool
|
|
query string
|
|
results []int // indices into providerLists
|
|
cursor int
|
|
scroll int
|
|
}
|
|
|
|
// seekState holds debounce state for yt-dlp seek-by-restart.
|
|
type seekState struct {
|
|
active bool // true from first keypress until seek completes
|
|
targetPos time.Duration // absolute target position
|
|
timer int // tick countdown for debounce (0 = idle)
|
|
grace int // ticks to suppress reconnect after seek completes
|
|
timerFor time.Duration
|
|
graceFor time.Duration
|
|
}
|
|
|
|
// themePickerState holds state for the theme picker overlay.
|
|
type themePickerState struct {
|
|
visible bool
|
|
cursor int
|
|
scroll int
|
|
savedIdx int // themeIdx before opening picker, for cancel/restore
|
|
}
|
|
|
|
// visPickerState holds state for the visualizer picker overlay.
|
|
type visPickerState struct {
|
|
visible bool
|
|
cursor int
|
|
scroll int
|
|
savedMode int // vis.Mode before opening, for cancel/restore
|
|
modes []string // mode names captured at open (stable while open)
|
|
}
|
|
|
|
// lyricsState holds state for the lyrics display overlay.
|
|
type lyricsState struct {
|
|
visible bool
|
|
lines []lyrics.Line
|
|
loading bool
|
|
err error
|
|
query string // "artist\ntitle" of the last fetch
|
|
scroll int
|
|
}
|
|
|
|
// keymapOverlay holds state for the keybindings overlay.
|
|
type keymapOverlay struct {
|
|
visible bool
|
|
cursor int
|
|
scroll int
|
|
savedCursor int
|
|
savedScroll int
|
|
searching bool
|
|
search string
|
|
filtered []int // indices into entries
|
|
entries []keymapEntry // core keys + plugin keys, rebuilt on openKeymap
|
|
}
|
|
|
|
// queueOverlay holds state for the queue manager overlay.
|
|
type queueOverlay struct {
|
|
visible bool
|
|
cursor int
|
|
scroll int
|
|
}
|
|
|
|
// plManagerState holds state for the playlist manager overlay.
|
|
type plManagerState struct {
|
|
visible bool
|
|
screen plMgrScreenType
|
|
cursor int // view-index: offset into filtered when filter != "", else direct index
|
|
scroll int
|
|
playlists []playlist.PlaylistInfo
|
|
selPlaylist string // playlist name open in screen 1
|
|
tracks []playlist.Track // tracks in the selected playlist
|
|
newName string
|
|
confirmDel bool
|
|
renameOldName string
|
|
renameName string
|
|
marked map[int]bool // real track indices marked on the tracks screen
|
|
sortMode int
|
|
undo plManagerUndo
|
|
|
|
// Filter (`/`) state. Reset on screen change. `filtered` indexes into
|
|
// `playlists` (list screen) or `tracks` (tracks screen).
|
|
filtering bool
|
|
filter string
|
|
filtered []int
|
|
savedCursor int // cursor before `/` was pressed, restored on Esc
|
|
savedScroll int
|
|
}
|
|
|
|
type plManagerUndoKind int
|
|
|
|
const (
|
|
plUndoNone plManagerUndoKind = iota
|
|
plUndoTracks
|
|
plUndoPlaylist
|
|
)
|
|
|
|
type plManagerUndo struct {
|
|
kind plManagerUndoKind
|
|
name string
|
|
tracks []playlist.Track
|
|
}
|
|
|
|
type playlistPickerScreen int
|
|
|
|
const (
|
|
plPickerChoose playlistPickerScreen = iota
|
|
plPickerNewName
|
|
)
|
|
|
|
// playlistPickerState holds the reusable local "write to playlist" picker.
|
|
type playlistPickerState struct {
|
|
visible bool
|
|
screen playlistPickerScreen
|
|
cursor int
|
|
scroll int
|
|
playlists []playlist.PlaylistInfo
|
|
tracks []playlist.Track
|
|
title string
|
|
newName string
|
|
}
|
|
|
|
// fileBrowserState holds state for the file browser overlay.
|
|
type fileBrowserState struct {
|
|
visible bool
|
|
dir string
|
|
entries []fbEntry
|
|
cursor int
|
|
scroll int
|
|
savedCursor int
|
|
savedScroll int
|
|
selected map[string]bool
|
|
err string
|
|
searching bool
|
|
search string
|
|
filtered []int // indices into entries
|
|
targetPlaylist string
|
|
}
|
|
|
|
// navBrowserState holds state for the provider browser overlay.
|
|
type navBrowserState struct {
|
|
prov playlist.Provider
|
|
visible bool
|
|
mode navBrowseModeType
|
|
screen navBrowseScreenType
|
|
cursor int
|
|
scroll int
|
|
artists []provider.ArtistInfo
|
|
albums []provider.AlbumInfo
|
|
tracks []playlist.Track
|
|
selArtist provider.ArtistInfo
|
|
selAlbum provider.AlbumInfo
|
|
sortType string
|
|
albumLoading bool
|
|
albumDone bool
|
|
loading bool
|
|
searching bool
|
|
search string
|
|
searchIdx []int
|
|
}
|
|
|
|
// spotSearchScreenType identifies which screen of the Spotify search overlay is active.
|
|
type spotSearchScreenType int
|
|
|
|
const (
|
|
spotSearchInput spotSearchScreenType = iota // typing search query
|
|
spotSearchResults // browsing search results
|
|
spotSearchPlaylist // picking a playlist to add to
|
|
spotSearchNewName // typing new playlist name
|
|
)
|
|
|
|
// spotSearchState holds state for the provider search + add-to-playlist overlay.
|
|
type spotSearchState struct {
|
|
prov playlist.Provider // the provider being searched (may differ from active provider)
|
|
visible bool
|
|
screen spotSearchScreenType
|
|
query string
|
|
results []playlist.Track
|
|
cursor int
|
|
scroll int
|
|
loading bool
|
|
playlists []playlist.PlaylistInfo // user's Spotify playlists for picker
|
|
selTrack playlist.Track // track selected to add
|
|
newName string // new playlist name input
|
|
err string
|
|
}
|
|
|
|
// catalogBatchState holds state for lazy-loading catalog entries from a provider.CatalogLoader.
|
|
type catalogBatchState struct {
|
|
offset int // next offset to fetch
|
|
loading bool // true while a fetch is in flight
|
|
done bool // true when all stations have been loaded
|
|
}
|
|
|
|
// ytdlBatchState holds state for incremental yt-dlp playlist loading.
|
|
type ytdlBatchState struct {
|
|
url string
|
|
gen uint64
|
|
offset int
|
|
done bool
|
|
loading bool
|
|
}
|
|
|
|
// reconnectState holds state for stream auto-reconnect with exponential backoff.
|
|
type reconnectState struct {
|
|
attempts int
|
|
at time.Time
|
|
}
|
|
|
|
// devicePickerState holds state for the audio device picker overlay.
|
|
type devicePickerState struct {
|
|
visible bool
|
|
devices []player.AudioDevice
|
|
cursor int
|
|
scroll int
|
|
loading bool
|
|
}
|
|
|
|
type saveState struct {
|
|
pendingDownloads int
|
|
}
|
|
|
|
func (s saveState) activityText() string {
|
|
switch s.pendingDownloads {
|
|
case 0:
|
|
return ""
|
|
case 1:
|
|
return "Downloading..."
|
|
default:
|
|
return fmt.Sprintf("Downloading... (%d)", s.pendingDownloads)
|
|
}
|
|
}
|
|
|
|
func (s *saveState) startDownload() {
|
|
s.pendingDownloads++
|
|
}
|
|
|
|
func (s *saveState) finishDownload() {
|
|
if s.pendingDownloads > 0 {
|
|
s.pendingDownloads--
|
|
}
|
|
}
|
|
|
|
// statusTTL is how long a status line stays visible.
|
|
type statusTTL time.Duration
|
|
|
|
func (t statusTTL) expiresAt(now time.Time) time.Time {
|
|
return now.Add(time.Duration(t))
|
|
}
|
|
|
|
// statusMsg holds a temporary status message shown at the bottom of the UI.
|
|
type statusMsg struct {
|
|
text string
|
|
expiresAt time.Time // zero = no active message
|
|
}
|
|
|
|
func (s statusMsg) Expired(now time.Time) bool {
|
|
return !s.expiresAt.IsZero() && !now.Before(s.expiresAt)
|
|
}
|
|
|
|
func (s *statusMsg) Show(text string, ttl statusTTL) {
|
|
s.ShowAt(time.Now(), text, ttl)
|
|
}
|
|
|
|
func (s *statusMsg) Showf(ttl statusTTL, format string, args ...any) {
|
|
s.Show(fmt.Sprintf(format, args...), ttl)
|
|
}
|
|
|
|
func (s *statusMsg) ShowAt(now time.Time, text string, ttl statusTTL) {
|
|
s.text = text
|
|
s.expiresAt = ttl.expiresAt(now)
|
|
}
|
|
|
|
func (s *statusMsg) Clear() {
|
|
*s = statusMsg{}
|
|
}
|
|
|
|
// logLine is a timestamped log message shown in the footer.
|
|
type logLine struct {
|
|
text string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
const logLineTTL = 6 * time.Second
|
|
|
|
// tickLogLines drains the applog buffer and expires old entries.
|
|
func (m *Model) tickLogLines(now time.Time) {
|
|
for _, e := range applog.Drain() {
|
|
text := strings.TrimRight(e.Text, "\n")
|
|
m.logLines = append(m.logLines, logLine{
|
|
text: text,
|
|
expiresAt: e.At.Add(logLineTTL),
|
|
})
|
|
}
|
|
// Expire old entries.
|
|
n := 0
|
|
for _, l := range m.logLines {
|
|
if now.Before(l.expiresAt) {
|
|
m.logLines[n] = l
|
|
n++
|
|
}
|
|
}
|
|
m.logLines = m.logLines[:n]
|
|
}
|
|
|
|
// networkStats tracks network throughput for the stream status bar.
|
|
type networkStats struct {
|
|
speed float64 // bytes per second (smoothed)
|
|
lastBytes int64
|
|
sampleFor time.Duration
|
|
}
|
|
|
|
type terminalTitleState struct {
|
|
introActive bool
|
|
introOffset int
|
|
introTick int
|
|
}
|