- Switch servers from the browser (s) with auto-discovery; auto-fallback to discovery when a saved server is unreachable, so you're never locked to a rebuilt box. cliamp's Jellyfin provider is re-synced on every (re)login. - Ignore late LAN-discovery results once off the login screen (footer/input clobber fix). - install.sh: curl|bash one-liner that downloads the per-arch release binary, still builds from a clone when Go is present; adds a floating Walker entry (TUI.float app-id) + icon on Omarchy desktops. - Docs: USER_GUIDE.md plain-English walkthrough; README install one-liner, accurate keybindings, features section. - Bump version to 1.0.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
"tsplay/internal/discovery"
|
|
)
|
|
|
|
func (m Model) updateLogin(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if key, ok := msg.(tea.KeyMsg); ok {
|
|
switch key.String() {
|
|
case "ctrl+c":
|
|
return m, tea.Quit
|
|
case "esc":
|
|
// If we reached login from an active session (switch server) and the
|
|
// old browse view is still around, esc cancels and returns to it. On
|
|
// genuine first-run, or after a failed-server fallback (empty stack),
|
|
// there's nothing to go back to, so esc quits.
|
|
if m.client != nil && len(m.stack) > 0 {
|
|
m.screen = screenBrowse
|
|
m.status = ""
|
|
return m, nil
|
|
}
|
|
return m, tea.Quit
|
|
case "ctrl+n":
|
|
// Cycle through discovered servers, filling the URL field.
|
|
if len(m.discovered) > 1 {
|
|
m.discIdx = (m.discIdx + 1) % len(m.discovered)
|
|
m.inputs[0].SetValue(m.discovered[m.discIdx].Address)
|
|
m.status = discoveryStatus(m.discovered, m.discIdx)
|
|
}
|
|
return m, nil
|
|
case "ctrl+r":
|
|
m.status = "Scanning the network for Jellyfin servers…"
|
|
return m, discoverCmd()
|
|
case "tab", "down":
|
|
m.focusField(m.focus + 1)
|
|
return m, textinput.Blink
|
|
case "shift+tab", "up":
|
|
m.focusField(m.focus - 1)
|
|
return m, textinput.Blink
|
|
case "enter":
|
|
server := strings.TrimSpace(m.inputs[0].Value())
|
|
user := strings.TrimSpace(m.inputs[1].Value())
|
|
pass := m.inputs[2].Value()
|
|
if server == "" || user == "" {
|
|
m.status = "Server URL and username are required."
|
|
return m, nil
|
|
}
|
|
if m.logging {
|
|
return m, nil
|
|
}
|
|
m.logging = true
|
|
m.status = "Connecting…"
|
|
return m, m.doLogin(server, user, pass)
|
|
}
|
|
}
|
|
|
|
// Feed the event to every input so the cursor blinks and the focused one
|
|
// receives text.
|
|
var cmds []tea.Cmd
|
|
for i := range m.inputs {
|
|
var c tea.Cmd
|
|
m.inputs[i], c = m.inputs[i].Update(msg)
|
|
cmds = append(cmds, c)
|
|
}
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (m *Model) focusField(i int) {
|
|
n := len(m.inputs)
|
|
i = ((i % n) + n) % n // wrap
|
|
for j := range m.inputs {
|
|
if j == i {
|
|
m.inputs[j].Focus()
|
|
} else {
|
|
m.inputs[j].Blur()
|
|
}
|
|
}
|
|
m.focus = i
|
|
}
|
|
|
|
// discoveryStatus formats the "found N servers" line shown under the form.
|
|
func discoveryStatus(servers []discovery.Server, idx int) string {
|
|
s := servers[idx]
|
|
name := s.Name
|
|
if name == "" {
|
|
name = s.Address
|
|
}
|
|
if len(servers) == 1 {
|
|
return "Found: " + name
|
|
}
|
|
return fmt.Sprintf("Found %d servers — using %s [%d/%d]", len(servers), name, idx+1, len(servers))
|
|
}
|
|
|
|
func (m Model) viewLogin() string {
|
|
var b strings.Builder
|
|
b.WriteString(m.st.title.Render("tsplay") + m.st.muted.Render(" — Jellyfin terminal player"))
|
|
b.WriteString("\n\n")
|
|
for i := range m.inputs {
|
|
b.WriteString(m.inputs[i].View())
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("\n")
|
|
help := "tab move · enter sign in · esc quit"
|
|
if len(m.discovered) > 1 {
|
|
help = "tab move · ctrl+n next server · enter sign in · esc quit"
|
|
} else {
|
|
help += " · ctrl+r rescan"
|
|
}
|
|
b.WriteString(m.st.help.Render(help))
|
|
body := m.st.box.Render(b.String())
|
|
|
|
// Discovery messages are informational (accent); real errors stay red.
|
|
footer := ""
|
|
if m.status != "" {
|
|
style := m.st.accent
|
|
if strings.HasPrefix(m.status, "Login failed") || strings.HasPrefix(m.status, "No servers") {
|
|
style = m.st.bad
|
|
}
|
|
footer = "\n" + style.Render(m.status)
|
|
}
|
|
return lipgloss.JoinVertical(lipgloss.Left, body, footer)
|
|
}
|