275 lines
7.3 KiB
Go
275 lines
7.3 KiB
Go
package adminui
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"skyrama-tui/internal/db"
|
|
)
|
|
|
|
// ── Messages ──────────────────────────────────────────────────────────────────
|
|
|
|
type airportLoadedMsg []db.AdminAirport
|
|
type airportDoneMsg struct{ err error }
|
|
|
|
// ── Action mode ───────────────────────────────────────────────────────────────
|
|
|
|
type airportAction int
|
|
|
|
const (
|
|
airportActionNone airportAction = iota
|
|
airportActionGiveCash
|
|
airportActionGiveBuilding
|
|
)
|
|
|
|
// ── Model ─────────────────────────────────────────────────────────────────────
|
|
|
|
// field indices for airport edit form
|
|
const (
|
|
aeqCash = 0
|
|
aeqLevel = 1
|
|
aeqMaxFuel = 2
|
|
aeqMaxPax = 3
|
|
)
|
|
|
|
type airportsTab struct {
|
|
client *db.AdminClient
|
|
items []db.AdminAirport
|
|
cursor int
|
|
// simple single-input actions (give cash / give building)
|
|
action airportAction
|
|
input textinput.Model
|
|
// full edit form
|
|
inForm bool
|
|
editID uint32
|
|
fields []field
|
|
focusIdx int
|
|
status string
|
|
isErr bool
|
|
w, h int
|
|
}
|
|
|
|
func newAirportsTab(client *db.AdminClient) *airportsTab {
|
|
ti := textinput.New()
|
|
ti.CharLimit = 20
|
|
return &airportsTab{client: client, input: ti}
|
|
}
|
|
|
|
func (t *airportsTab) InForm() bool { return t.action != airportActionNone || t.inForm }
|
|
func (t *airportsTab) Init() tea.Cmd { return t.load() }
|
|
|
|
func (t *airportsTab) load() tea.Cmd {
|
|
return func() tea.Msg {
|
|
items, err := t.client.ListAirports()
|
|
if err != nil {
|
|
return airportDoneMsg{err}
|
|
}
|
|
return airportLoadedMsg(items)
|
|
}
|
|
}
|
|
|
|
func (t *airportsTab) openEditForm(a db.AdminAirport) {
|
|
t.fields = []field{
|
|
prefill("Cash (set)", strconv.FormatUint(a.Cash, 10)),
|
|
prefill("Player Level", strconv.Itoa(int(a.Level))),
|
|
prefill("Max Fuel Capacity", strconv.Itoa(int(a.MaxFuel))),
|
|
prefill("Max Passenger Cap", strconv.Itoa(int(a.MaxPax))),
|
|
}
|
|
t.fields[aeqCash].input.Focus()
|
|
t.focusIdx = aeqCash
|
|
t.editID = a.ID
|
|
t.inForm = true
|
|
t.status = ""
|
|
}
|
|
|
|
func (t *airportsTab) submitEdit() tea.Cmd {
|
|
cash, e1 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqCash].value()), 10, 64)
|
|
level, e2 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqLevel].value()), 10, 32)
|
|
maxFuel, e3 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqMaxFuel].value()), 10, 32)
|
|
maxPax, e4 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqMaxPax].value()), 10, 32)
|
|
if e1 != nil || e2 != nil || e3 != nil || e4 != nil {
|
|
t.status = "all fields must be valid numbers"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
id := t.editID
|
|
return func() tea.Msg {
|
|
return airportDoneMsg{t.client.UpdateAirport(id, cash, uint32(level), uint32(maxFuel), uint32(maxPax))}
|
|
}
|
|
}
|
|
|
|
func (t *airportsTab) openAction(a airportAction, placeholder string) {
|
|
t.action = a
|
|
t.input.Reset()
|
|
t.input.Placeholder = placeholder
|
|
t.input.Focus()
|
|
t.status = ""
|
|
}
|
|
|
|
func (t *airportsTab) submitAction() tea.Cmd {
|
|
if len(t.items) == 0 {
|
|
return nil
|
|
}
|
|
airport := t.items[t.cursor]
|
|
raw := strings.TrimSpace(t.input.Value())
|
|
|
|
switch t.action {
|
|
case airportActionGiveCash:
|
|
amount, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil || amount == 0 {
|
|
t.status = "enter a positive cash amount"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
id := airport.ID
|
|
return func() tea.Msg {
|
|
return airportDoneMsg{t.client.GiveAirportCash(id, amount)}
|
|
}
|
|
|
|
case airportActionGiveBuilding:
|
|
bID, err := strconv.ParseUint(raw, 10, 32)
|
|
if err != nil || bID == 0 {
|
|
t.status = "enter a valid building ID"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
id := airport.ID
|
|
return func() tea.Msg {
|
|
return airportDoneMsg{t.client.GiveAirportBuilding(id, uint32(bID))}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *airportsTab) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch m := msg.(type) {
|
|
case tea.WindowSizeMsg:
|
|
t.w, t.h = m.Width, m.Height
|
|
return t, nil
|
|
|
|
case airportLoadedMsg:
|
|
t.items = []db.AdminAirport(m)
|
|
if t.cursor >= len(t.items) && t.cursor > 0 {
|
|
t.cursor = len(t.items) - 1
|
|
}
|
|
return t, nil
|
|
|
|
case airportDoneMsg:
|
|
if m.err != nil {
|
|
t.status = m.err.Error()
|
|
t.isErr = true
|
|
return t, nil
|
|
}
|
|
t.action = airportActionNone
|
|
t.inForm = false
|
|
t.status = "done"
|
|
t.isErr = false
|
|
return t, t.load()
|
|
|
|
case tea.KeyMsg:
|
|
if t.inForm {
|
|
switch m.String() {
|
|
case "esc":
|
|
t.inForm = false
|
|
t.status = ""
|
|
case "ctrl+s":
|
|
return t, t.submitEdit()
|
|
default:
|
|
var cmd tea.Cmd
|
|
t.focusIdx, cmd = handleFormKey(t.fields, t.focusIdx, m)
|
|
return t, cmd
|
|
}
|
|
return t, nil
|
|
}
|
|
if t.action != airportActionNone {
|
|
switch m.String() {
|
|
case "esc":
|
|
t.action = airportActionNone
|
|
t.status = ""
|
|
case "enter", "ctrl+s":
|
|
return t, t.submitAction()
|
|
default:
|
|
var cmd tea.Cmd
|
|
t.input, cmd = t.input.Update(m)
|
|
return t, cmd
|
|
}
|
|
return t, nil
|
|
}
|
|
switch m.String() {
|
|
case "q":
|
|
return t, tea.Quit
|
|
case "j", "down":
|
|
if t.cursor < len(t.items)-1 {
|
|
t.cursor++
|
|
}
|
|
case "k", "up":
|
|
if t.cursor > 0 {
|
|
t.cursor--
|
|
}
|
|
case "e":
|
|
if len(t.items) > 0 {
|
|
t.openEditForm(t.items[t.cursor])
|
|
}
|
|
case "g":
|
|
t.openAction(airportActionGiveCash, "amount, e.g. 5000")
|
|
case "b":
|
|
t.openAction(airportActionGiveBuilding, "building ID, e.g. 3")
|
|
case "r":
|
|
return t, t.load()
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (t *airportsTab) View() string {
|
|
var sb strings.Builder
|
|
sb.WriteString(hdrSt.Render(" Airports") + "\n\n")
|
|
|
|
if t.inForm {
|
|
sb.WriteString(titleSt.Render(fmt.Sprintf(" Edit Airport #%d", t.editID)) + "\n\n")
|
|
sb.WriteString(renderForm(t.fields, t.focusIdx))
|
|
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
|
|
sb.WriteString("\n\n " + dimSt.Render(helpForm))
|
|
return sb.String()
|
|
}
|
|
|
|
if t.action != airportActionNone {
|
|
if len(t.items) > 0 {
|
|
ap := t.items[t.cursor]
|
|
sb.WriteString(fmt.Sprintf(" Selected: %s (owner: %s, cash: %d)\n\n",
|
|
titleSt.Render(ap.Name), ap.Username, ap.Cash))
|
|
}
|
|
label := "Give Cash Amount:"
|
|
if t.action == airportActionGiveBuilding {
|
|
label = "Building ID to add:"
|
|
}
|
|
sb.WriteString(" " + labelSt.Render(label) + " " + t.input.View() + "\n")
|
|
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
|
|
sb.WriteString("\n\n " + dimSt.Render("enter:confirm esc:cancel"))
|
|
return sb.String()
|
|
}
|
|
|
|
header := fmt.Sprintf(" %-5s %-20s %-16s %-12s %-8s %s",
|
|
"ID", "Airport Name", "Owner", "Cash", "Fuel", "Lvl")
|
|
sb.WriteString(dimSt.Render(header) + "\n")
|
|
sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 70)) + "\n")
|
|
|
|
if len(t.items) == 0 {
|
|
sb.WriteString(dimSt.Render(" (no airports)") + "\n")
|
|
}
|
|
for i, a := range t.items {
|
|
line := fmt.Sprintf(" %-5d %-20s %-16s %-12d %-8d %d",
|
|
a.ID, a.Name, a.Username, a.Cash, a.Fuel, a.Level)
|
|
if i == t.cursor {
|
|
sb.WriteString(cursorSt.Render("▸") + activeTS.Render(line[1:]) + "\n")
|
|
} else {
|
|
sb.WriteString(line + "\n")
|
|
}
|
|
}
|
|
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
|
|
sb.WriteString("\n\n " + dimSt.Render("e:edit g:give cash b:give building j/k:nav r:reload q:quit"))
|
|
return sb.String()
|
|
}
|