176 lines
4.5 KiB
Go
176 lines
4.5 KiB
Go
package ui
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/charmbracelet/lipgloss"
|
||
|
||
"skyrama-tui/internal/model"
|
||
"skyrama-tui/internal/service"
|
||
"skyrama-tui/internal/style"
|
||
)
|
||
|
||
// ─── Messages ─────────────────────────────────────────────────────────────────
|
||
|
||
type ContractsLoadedMsg struct{ Contracts []model.Contract }
|
||
type ContractsErrMsg struct{ Err error }
|
||
|
||
// ─── ContractsModel ───────────────────────────────────────────────────────────
|
||
|
||
type ContractsModel struct {
|
||
client *service.GameService
|
||
airportID uint32
|
||
contracts []model.Contract
|
||
cursor int
|
||
statusMsg string
|
||
focused bool
|
||
width int
|
||
height int
|
||
}
|
||
|
||
func NewContractsModel(client *service.GameService, airportID uint32) ContractsModel {
|
||
return ContractsModel{client: client, airportID: airportID}
|
||
}
|
||
|
||
func (m ContractsModel) Init() tea.Cmd { return m.load() }
|
||
|
||
func (m ContractsModel) Update(msg tea.Msg) (ContractsModel, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
case tea.WindowSizeMsg:
|
||
m.width = msg.Width
|
||
m.height = msg.Height
|
||
|
||
case ContractsLoadedMsg:
|
||
m.contracts = msg.Contracts
|
||
if m.cursor >= len(m.contracts) {
|
||
m.cursor = clampMin(len(m.contracts)-1, 0)
|
||
}
|
||
|
||
case ContractsErrMsg:
|
||
m.statusMsg = style.Error.String() + msg.Err.Error()
|
||
|
||
case tea.KeyMsg:
|
||
if !m.focused {
|
||
break
|
||
}
|
||
switch msg.String() {
|
||
case "up", "k":
|
||
if m.cursor > 0 {
|
||
m.cursor--
|
||
}
|
||
case "down", "j":
|
||
if m.cursor < len(m.contracts)-1 {
|
||
m.cursor++
|
||
}
|
||
case "r":
|
||
m.statusMsg = ""
|
||
return m, m.load()
|
||
}
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m ContractsModel) View() string {
|
||
header := style.PanelHeader.Render("📋 Active Contracts")
|
||
|
||
statusLine := ""
|
||
if m.statusMsg != "" {
|
||
statusLine = "\n" + m.statusMsg + "\n"
|
||
}
|
||
|
||
if len(m.contracts) == 0 {
|
||
return lipgloss.JoinVertical(lipgloss.Left,
|
||
header,
|
||
statusLine,
|
||
"",
|
||
style.Muted.Render("No active contracts right now."),
|
||
)
|
||
}
|
||
|
||
rows := make([]string, len(m.contracts))
|
||
for i, c := range m.contracts {
|
||
rows[i] = m.renderContract(c, i == m.cursor)
|
||
}
|
||
|
||
help := style.HelpKey.Render("↑↓ / jk") + style.HelpDesc.Render(" navigate ") +
|
||
style.HelpKey.Render("r") + style.HelpDesc.Render(" refresh")
|
||
|
||
return lipgloss.JoinVertical(lipgloss.Left,
|
||
header,
|
||
statusLine,
|
||
strings.Join(rows, "\n"),
|
||
"",
|
||
style.Muted.Render(help),
|
||
)
|
||
}
|
||
|
||
func (m ContractsModel) renderContract(c model.Contract, selected bool) string {
|
||
pct := c.ProgressPct()
|
||
barW := 24
|
||
bar := style.ResourceBar(int(c.CurrentAmountDelivered), int(c.RequiredAmount), barW, style.ColorGreen)
|
||
|
||
expiresIn := time.Until(c.ExpiresAt).Round(time.Minute)
|
||
urgency := style.Muted
|
||
if expiresIn < 30*time.Minute {
|
||
urgency = style.Error
|
||
} else if expiresIn < 2*time.Hour {
|
||
urgency = style.Warning
|
||
}
|
||
|
||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||
style.Bold.Render(c.Title),
|
||
style.Muted.Render(fmt.Sprintf("Deliver %d × %s", c.RequiredAmount, c.ProductName)),
|
||
lipgloss.JoinHorizontal(lipgloss.Top,
|
||
bar,
|
||
style.Muted.Render(fmt.Sprintf(" %d/%d (%d%%)",
|
||
c.CurrentAmountDelivered, c.RequiredAmount, pct)),
|
||
),
|
||
urgency.Render(fmt.Sprintf("⏰ Expires in %s", fmtContractDuration(expiresIn))),
|
||
)
|
||
|
||
if selected {
|
||
return style.PanelFocused.Render(inner)
|
||
}
|
||
return style.Panel.Render(inner)
|
||
}
|
||
|
||
// ─── Exported for parent AppModel ─────────────────────────────────────────────
|
||
|
||
// Load is the exported version for use by parent AppModel.
|
||
func (m ContractsModel) Load() tea.Cmd { return m.load() }
|
||
|
||
func (m ContractsModel) load() tea.Cmd {
|
||
client := m.client
|
||
airportID := m.airportID
|
||
return func() tea.Msg {
|
||
contracts, err := client.GetActiveContracts(airportID)
|
||
if err != nil {
|
||
return ContractsErrMsg{Err: err}
|
||
}
|
||
return ContractsLoadedMsg{Contracts: contracts}
|
||
}
|
||
}
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
func clampMin(v, min int) int {
|
||
if v < min {
|
||
return min
|
||
}
|
||
return v
|
||
}
|
||
|
||
func fmtContractDuration(d time.Duration) string {
|
||
if d <= 0 {
|
||
return "expired"
|
||
}
|
||
h := int(d.Hours())
|
||
m := int(d.Minutes()) % 60
|
||
if h > 0 {
|
||
return fmt.Sprintf("%dh %dm", h, m)
|
||
}
|
||
return fmt.Sprintf("%dm", m)
|
||
}
|