phase 0.3
This commit is contained in:
commit
4a94bfc4e7
52 changed files with 11653 additions and 0 deletions
310
internal/adminui/buildings.go
Normal file
310
internal/adminui/buildings.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package adminui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"skyrama-tui/internal/db"
|
||||
)
|
||||
|
||||
// ── Messages ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type bldgLoadedMsg []db.AdminBuilding
|
||||
type bldgDoneMsg struct{ err error }
|
||||
|
||||
// ── Field indices ─────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
bfName = 0
|
||||
bfMinLevel = 1
|
||||
bfCost = 2
|
||||
bfType = 3
|
||||
// storage
|
||||
bfCapacity = 4
|
||||
bfStorType = 5
|
||||
bfStorSubtype = 6
|
||||
// operation
|
||||
bfOpType = 7
|
||||
bfOpSize = 8
|
||||
// passive
|
||||
bfPassiveType = 9
|
||||
bfPassiveRate = 10
|
||||
)
|
||||
|
||||
// ── Model ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
type buildingsTab struct {
|
||||
client *db.AdminClient
|
||||
items []db.AdminBuilding
|
||||
cursor int
|
||||
inForm bool
|
||||
isEdit bool
|
||||
editID uint32
|
||||
fields []field
|
||||
focusIdx int
|
||||
status string
|
||||
isErr bool
|
||||
w, h int
|
||||
}
|
||||
|
||||
func newBuildingsTab(client *db.AdminClient) *buildingsTab {
|
||||
return &buildingsTab{client: client}
|
||||
}
|
||||
|
||||
func (t *buildingsTab) InForm() bool { return t.inForm }
|
||||
func (t *buildingsTab) Init() tea.Cmd { return t.load() }
|
||||
|
||||
func (t *buildingsTab) load() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
items, err := t.client.ListBuildings()
|
||||
if err != nil {
|
||||
return bldgDoneMsg{err}
|
||||
}
|
||||
return bldgLoadedMsg(items)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *buildingsTab) initFields() {
|
||||
t.fields = []field{
|
||||
newTextField("Name", "e.g. Small Hangar"),
|
||||
newTextField("Min Level", "1"),
|
||||
newTextField("Cost (cash)", "100"),
|
||||
newEnumField("Type", []string{"none", "storage", "operation", "passive"}),
|
||||
// storage
|
||||
newTextField("Capacity", "10"),
|
||||
newEnumField("Storage Type", []string{"fuel", "plane", "product"}),
|
||||
newEnumField("Storage Subtype", []string{"none", "airplane", "helicopter", "seaplane", "spaceship"}),
|
||||
// operation
|
||||
newEnumField("Op Type", []string{"runway", "service", "production"}),
|
||||
newEnumField("Op Size", []string{"small", "medium", "large", "huge"}),
|
||||
// passive
|
||||
newEnumField("Passive Type", []string{"passengers", "fuel"}),
|
||||
newTextField("Passive Rate", "10"),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *buildingsTab) openForm() {
|
||||
t.initFields()
|
||||
t.fields[bfName].input.Focus()
|
||||
t.focusIdx = bfName
|
||||
t.isEdit = false
|
||||
t.updateHidden()
|
||||
t.inForm = true
|
||||
t.status = ""
|
||||
}
|
||||
|
||||
func (t *buildingsTab) openEditForm(b db.AdminBuilding) {
|
||||
t.initFields()
|
||||
t.fields[bfName].input.SetValue(b.Name)
|
||||
t.fields[bfMinLevel].input.SetValue(strconv.Itoa(int(b.MinLevel)))
|
||||
t.fields[bfCost].input.SetValue(strconv.Itoa(int(b.Cost)))
|
||||
setEnumSel(&t.fields[bfType], b.BuildingType)
|
||||
t.fields[bfCapacity].input.SetValue(strconv.Itoa(b.Capacity))
|
||||
setEnumSel(&t.fields[bfStorType], b.StorageType)
|
||||
setEnumSel(&t.fields[bfStorSubtype], b.StorageSubtype)
|
||||
setEnumSel(&t.fields[bfOpType], b.OperationType)
|
||||
setEnumSel(&t.fields[bfOpSize], b.OperationSize)
|
||||
setEnumSel(&t.fields[bfPassiveType], b.PassiveType)
|
||||
t.fields[bfPassiveRate].input.SetValue(strconv.Itoa(b.PassiveRate))
|
||||
t.fields[bfName].input.Focus()
|
||||
t.focusIdx = bfName
|
||||
t.isEdit = true
|
||||
t.editID = b.ID
|
||||
t.updateHidden()
|
||||
t.inForm = true
|
||||
t.status = ""
|
||||
}
|
||||
|
||||
// updateHidden shows/hides type-specific fields based on the type enum selection.
|
||||
func (t *buildingsTab) updateHidden() {
|
||||
btype := t.fields[bfType].options[t.fields[bfType].sel]
|
||||
for i := range t.fields {
|
||||
switch i {
|
||||
case bfCapacity, bfStorType, bfStorSubtype:
|
||||
t.fields[i].hidden = btype != "storage"
|
||||
case bfOpType, bfOpSize:
|
||||
t.fields[i].hidden = btype != "operation"
|
||||
case bfPassiveType, bfPassiveRate:
|
||||
t.fields[i].hidden = btype != "passive"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *buildingsTab) submit() tea.Cmd {
|
||||
name := strings.TrimSpace(t.fields[bfName].value())
|
||||
if name == "" {
|
||||
t.status = "name is required"
|
||||
t.isErr = true
|
||||
return nil
|
||||
}
|
||||
lvl, err := strconv.ParseUint(strings.TrimSpace(t.fields[bfMinLevel].value()), 10, 32)
|
||||
if err != nil {
|
||||
t.status = "min level must be a number"
|
||||
t.isErr = true
|
||||
return nil
|
||||
}
|
||||
cost, err := strconv.ParseUint(strings.TrimSpace(t.fields[bfCost].value()), 10, 32)
|
||||
if err != nil {
|
||||
t.status = "cost must be a number"
|
||||
t.isErr = true
|
||||
return nil
|
||||
}
|
||||
btype := t.fields[bfType].value()
|
||||
|
||||
p := db.AddBuildingParams{
|
||||
Name: name,
|
||||
MinLevel: uint32(lvl),
|
||||
Cost: uint32(cost),
|
||||
BuildingType: btype,
|
||||
}
|
||||
|
||||
switch btype {
|
||||
case "storage":
|
||||
cap, err := strconv.Atoi(strings.TrimSpace(t.fields[bfCapacity].value()))
|
||||
if err != nil || cap <= 0 {
|
||||
t.status = "capacity must be a positive number"
|
||||
t.isErr = true
|
||||
return nil
|
||||
}
|
||||
p.Capacity = cap
|
||||
p.StorageType = t.fields[bfStorType].value()
|
||||
p.StorageSubtype = t.fields[bfStorSubtype].value()
|
||||
case "operation":
|
||||
p.OperationType = t.fields[bfOpType].value()
|
||||
p.OperationSize = t.fields[bfOpSize].value()
|
||||
case "passive":
|
||||
rate, err := strconv.Atoi(strings.TrimSpace(t.fields[bfPassiveRate].value()))
|
||||
if err != nil {
|
||||
t.status = "passive rate must be a number"
|
||||
t.isErr = true
|
||||
return nil
|
||||
}
|
||||
p.PassiveType = t.fields[bfPassiveType].value()
|
||||
p.PassiveRate = rate
|
||||
}
|
||||
|
||||
if t.isEdit {
|
||||
id := t.editID
|
||||
return func() tea.Msg { return bldgDoneMsg{t.client.UpdateBuilding(id, p)} }
|
||||
}
|
||||
return func() tea.Msg { return bldgDoneMsg{t.client.AddBuilding(p)} }
|
||||
}
|
||||
|
||||
func (t *buildingsTab) 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 bldgLoadedMsg:
|
||||
t.items = []db.AdminBuilding(m)
|
||||
if t.cursor >= len(t.items) && t.cursor > 0 {
|
||||
t.cursor = len(t.items) - 1
|
||||
}
|
||||
return t, nil
|
||||
|
||||
case bldgDoneMsg:
|
||||
if m.err != nil {
|
||||
t.status = m.err.Error()
|
||||
t.isErr = true
|
||||
return t, nil
|
||||
}
|
||||
t.inForm = false
|
||||
t.status = "saved"
|
||||
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.submit()
|
||||
default:
|
||||
prevType := t.fields[bfType].sel
|
||||
var cmd tea.Cmd
|
||||
t.focusIdx, cmd = handleFormKey(t.fields, t.focusIdx, m)
|
||||
// If type enum changed, update field visibility
|
||||
if t.fields[bfType].sel != prevType {
|
||||
t.updateHidden()
|
||||
// If current focus is now hidden, jump to next visible
|
||||
if t.focusIdx < len(t.fields) && t.fields[t.focusIdx].hidden {
|
||||
t.focusIdx = nextVisible(t.fields, t.focusIdx)
|
||||
applyFocus(t.fields, t.focusIdx)
|
||||
}
|
||||
}
|
||||
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 "a":
|
||||
t.openForm()
|
||||
case "e":
|
||||
if len(t.items) > 0 {
|
||||
t.openEditForm(t.items[t.cursor])
|
||||
}
|
||||
case "d":
|
||||
if len(t.items) > 0 {
|
||||
id := t.items[t.cursor].ID
|
||||
return t, func() tea.Msg {
|
||||
return bldgDoneMsg{t.client.DeleteBuilding(id)}
|
||||
}
|
||||
}
|
||||
case "r":
|
||||
return t, t.load()
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *buildingsTab) View() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(hdrSt.Render(" Buildings") + "\n\n")
|
||||
|
||||
if t.inForm {
|
||||
formTitle := "Add Building"
|
||||
if t.isEdit {
|
||||
formTitle = fmt.Sprintf("Edit Building #%d", t.editID)
|
||||
}
|
||||
sb.WriteString(titleSt.Render(" "+formTitle) + "\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()
|
||||
}
|
||||
|
||||
header := fmt.Sprintf(" %-5s %-24s %-12s %-8s %s", "ID", "Name", "Type", "Cost", "Details")
|
||||
sb.WriteString(dimSt.Render(header) + "\n")
|
||||
sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 65)) + "\n")
|
||||
|
||||
if len(t.items) == 0 {
|
||||
sb.WriteString(dimSt.Render(" (no buildings — press a to add)") + "\n")
|
||||
}
|
||||
for i, b := range t.items {
|
||||
line := fmt.Sprintf(" %-5d %-24s %-12s %-8d %s",
|
||||
b.ID, b.Name, b.BuildingType, b.Cost, b.Details)
|
||||
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(helpList))
|
||||
return sb.String()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue