phase 0.3
This commit is contained in:
commit
4a94bfc4e7
52 changed files with 11653 additions and 0 deletions
275
internal/adminui/airports.go
Normal file
275
internal/adminui/airports.go
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue