skyrama_tui/internal/ui/login.go
2026-06-04 20:51:20 +03:00

178 lines
4.7 KiB
Go

package ui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"skyrama-tui/internal/service"
"skyrama-tui/internal/style"
)
// ─── Messages ─────────────────────────────────────────────────────────────────
type LoginSuccessMsg struct {
UserID int32
Username string
}
type LoginErrMsg struct{ Err error }
// ─── Model ────────────────────────────────────────────────────────────────────
type LoginModel struct {
client *service.GameService
inputs []textinput.Model // 0=username, 1=password
focus int
mode string // "login" | "register"
err string
width int
}
func NewLoginModel(client *service.GameService) LoginModel {
user := textinput.New()
user.Placeholder = "username"
user.Focus()
user.CharLimit = 64
user.Width = 30
pass := textinput.New()
pass.Placeholder = "password"
pass.EchoMode = textinput.EchoPassword
pass.EchoCharacter = '•'
pass.CharLimit = 128
pass.Width = 30
return LoginModel{
client: client,
inputs: []textinput.Model{user, pass},
mode: "login",
}
}
func (m LoginModel) Init() tea.Cmd {
return textinput.Blink
}
func (m LoginModel) Update(msg tea.Msg) (LoginModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
case tea.KeyMsg:
switch msg.String() {
case "tab", "down":
m.focus = (m.focus + 1) % len(m.inputs)
for i := range m.inputs {
if i == m.focus {
m.inputs[i].Focus()
} else {
m.inputs[i].Blur()
}
}
case "shift+tab", "up":
m.focus = (m.focus - 1 + len(m.inputs)) % len(m.inputs)
for i := range m.inputs {
if i == m.focus {
m.inputs[i].Focus()
} else {
m.inputs[i].Blur()
}
}
case "ctrl+r":
m.mode = "register"
m.err = ""
case "ctrl+l":
m.mode = "login"
m.err = ""
case "enter":
username := strings.TrimSpace(m.inputs[0].Value())
password := m.inputs[1].Value()
if username == "" || password == "" {
m.err = "username and password required"
return m, nil
}
if m.mode == "login" {
return m, m.doLogin(username, password)
}
return m, m.doRegister(username, password)
}
}
// Forward input events to focused field
var cmds []tea.Cmd
for i := range m.inputs {
var cmd tea.Cmd
m.inputs[i], cmd = m.inputs[i].Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
func (m LoginModel) View() string {
banner := style.Title.Render("✈ SKYRAMA ✈")
modeLabel := style.PanelHeader.Render("Login")
if m.mode == "register" {
modeLabel = style.PanelHeader.Render("Register")
}
form := lipgloss.JoinVertical(lipgloss.Left,
style.Muted.Render("Username"),
m.inputs[0].View(),
"",
style.Muted.Render("Password"),
m.inputs[1].View(),
)
errLine := ""
if m.err != "" {
errLine = "\n" + style.Error.String() + style.Muted.Render(m.err)
}
hints := "\n" +
style.HelpKey.Render("enter") + style.HelpDesc.Render(" confirm ") +
style.HelpKey.Render("ctrl+r") + style.HelpDesc.Render(" register ") +
style.HelpKey.Render("ctrl+l") + style.HelpDesc.Render(" login ") +
style.HelpKey.Render("tab") + style.HelpDesc.Render(" next field")
box := style.Panel.Padding(1, 3).Render(
lipgloss.JoinVertical(lipgloss.Left, modeLabel, "", form, errLine, hints),
)
content := lipgloss.JoinVertical(lipgloss.Center, banner, "", box)
return lipgloss.Place(m.width, 24, lipgloss.Center, lipgloss.Center, content)
}
// SetErr injects an error string from outside (e.g. from main after a LoginErrMsg).
func (m *LoginModel) SetErr(e string) { m.err = e }
// ─── Commands ─────────────────────────────────────────────────────────────────
func (m LoginModel) doLogin(username, password string) tea.Cmd {
return func() tea.Msg {
id, err := m.client.Login(username, password)
if err != nil {
return LoginErrMsg{Err: err}
}
return LoginSuccessMsg{UserID: id, Username: username}
}
}
func (m LoginModel) doRegister(username, password string) tea.Cmd {
return func() tea.Msg {
_, err := m.client.Register(username, password)
if err != nil {
return LoginErrMsg{Err: fmt.Errorf("register: %w", err)}
}
// Auto-login after register
id, err := m.client.Login(username, password)
if err != nil {
return LoginErrMsg{Err: err}
}
return LoginSuccessMsg{UserID: id, Username: username}
}
}