From 452040598b1960f72cfe9c6649d09884740dd8c0 Mon Sep 17 00:00:00 2001 From: Kyren223 Date: Sat, 30 Nov 2024 18:38:09 +0200 Subject: Created UI for create frequency popup --- internal/client/ui/core/core.go | 49 +++- .../ui/core/frequencycreation/frequencycreation.go | 286 +++++++++++++++++++++ internal/client/ui/core/network/network.go | 2 + .../ui/core/networkcreation/networkcreation.go | 1 - internal/packet/models.go | 13 + internal/server/api/models.go | 15 -- 6 files changed, 341 insertions(+), 25 deletions(-) create mode 100644 internal/client/ui/core/frequencycreation/frequencycreation.go create mode 100644 internal/packet/models.go delete mode 100644 internal/server/api/models.go (limited to 'internal') diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go index 6ba25c1..fdaab35 100644 --- a/internal/client/ui/core/core.go +++ b/internal/client/ui/core/core.go @@ -13,6 +13,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/kyren223/eko/internal/client/gateway" "github.com/kyren223/eko/internal/client/ui" + "github.com/kyren223/eko/internal/client/ui/core/frequencycreation" "github.com/kyren223/eko/internal/client/ui/core/network" "github.com/kyren223/eko/internal/client/ui/core/networkcreation" "github.com/kyren223/eko/internal/client/ui/core/networklist" @@ -47,10 +48,11 @@ type Model struct { timeout time.Duration connected bool - networkCreationPopup *networkcreation.Model - networkList networklist.Model - network network.Model - focus int + networkCreationPopup *networkcreation.Model + frequencyCreationPopup *frequencycreation.Model + networkList networklist.Model + network network.Model + focus int } func New(privKey ed25519.PrivateKey, name string) Model { @@ -88,8 +90,13 @@ func (m Model) View() string { result, ) + var popup string if m.networkCreationPopup != nil { - popup := m.networkCreationPopup.View() + popup = m.networkCreationPopup.View() + } else if m.frequencyCreationPopup != nil { + popup = m.frequencyCreationPopup.View() + } + if popup != "" { x := (ui.Width - lipgloss.Width(popup)) / 2 y := (ui.Height - lipgloss.Height(popup)) / 2 result = ui.PlaceOverlay(x, y, popup, result) @@ -178,14 +185,24 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { key := msg.Type switch key { case tea.KeyCtrlN: - if m.networkCreationPopup == nil { - popup := networkcreation.New() - m.networkCreationPopup = &popup + switch m.focus { + case FocusNetworkList: + if m.networkCreationPopup == nil { + popup := networkcreation.New() + m.networkCreationPopup = &popup + } + case FocusNetwork: + if m.frequencyCreationPopup == nil { + popup := frequencycreation.New() + m.frequencyCreationPopup = &popup + } } case tea.KeyEscape: if m.networkCreationPopup != nil { m.networkCreationPopup = nil + } else if m.frequencyCreationPopup != nil { + m.frequencyCreationPopup = nil } case tea.KeyEnter: @@ -195,6 +212,12 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { m.networkCreationPopup = nil } return cmd + } else if m.frequencyCreationPopup != nil { + cmd := m.frequencyCreationPopup.Select() + if cmd != nil { + m.frequencyCreationPopup = nil + } + return cmd } default: @@ -202,6 +225,10 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { popup, cmd := m.networkCreationPopup.Update(msg) m.networkCreationPopup = &popup return cmd + } else if m.frequencyCreationPopup != nil { + popup, cmd := m.frequencyCreationPopup.Update(msg) + m.frequencyCreationPopup = &popup + return cmd } if msg.String() == "L" && m.focus == FocusNetworkList { @@ -213,6 +240,11 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { } } + inPopup := m.networkCreationPopup != nil || m.frequencyCreationPopup != nil + if inPopup { + return nil + } + var cmd tea.Cmd switch m.focus { case FocusNetworkList: @@ -221,7 +253,6 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { case FocusNetwork: m.network, cmd = m.network.Update(msg) } - return cmd } diff --git a/internal/client/ui/core/frequencycreation/frequencycreation.go b/internal/client/ui/core/frequencycreation/frequencycreation.go new file mode 100644 index 0000000..d7a34a5 --- /dev/null +++ b/internal/client/ui/core/frequencycreation/frequencycreation.go @@ -0,0 +1,286 @@ +package frequencycreation + +import ( + "errors" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/kyren223/eko/internal/client/gateway" + "github.com/kyren223/eko/internal/client/ui/colors" + "github.com/kyren223/eko/internal/client/ui/field" + "github.com/kyren223/eko/internal/client/ui/layouts/flex" + "github.com/kyren223/eko/internal/packet" + "github.com/kyren223/eko/pkg/assert" +) + +var ( + width = 48 + + style = lipgloss.NewStyle(). + Border(lipgloss.ThickBorder()). + Padding(1, 4). + Align(lipgloss.Center, lipgloss.Center) + + headerStyle = lipgloss.NewStyle().Foreground(colors.Turquoise) + focusStyle = lipgloss.NewStyle().Foreground(colors.Focus) + + fieldBlurredStyle = lipgloss.NewStyle(). + PaddingLeft(1). + Border(lipgloss.RoundedBorder()). + BorderForeground(colors.DarkCyan) + fieldFocusedStyle = fieldBlurredStyle. + BorderForeground(colors.Focus). + Border(lipgloss.ThickBorder()) + + underlineStyle = func(s string, width int, color lipgloss.Color) string { + underline := lipgloss.NewStyle().Foreground(color). + Render(strings.Repeat(lipgloss.ThickBorder().Bottom, width)) + return lipgloss.JoinVertical(lipgloss.Left, s, underline) + } + + colorHeader = headerStyle.Bold(true).Render(" Color # ") + + blurredCreate = lipgloss.NewStyle(). + Background(colors.Gray).Padding(0, 1).Render("Create Frequency") + focusedCreate = lipgloss.NewStyle(). + Background(colors.Blue).Padding(0, 1).Render("Create Frequency") + + permsHeader = lipgloss.NewStyle(). + Foreground(colors.Turquoise). + Render("Permissions for non-admins:") + leftpad = 1 +) + +const ( + MaxHexDigits = 6 +) + +const ( + NameField = iota + ColorField + ReadWriteField + ReadOnlyField + NoAccessField + CreateField + FieldCount +) + +type Model struct { + name field.Model + precomputedStyle lipgloss.Style + lastColor lipgloss.Color + create string + color textinput.Model + perms int + nameWidth int + selected int +} + +func New() Model { + name := field.New(width) + name.Header = "Frequency Name" + name.HeaderStyle = headerStyle + name.FocusedStyle = fieldFocusedStyle + name.BlurredStyle = fieldBlurredStyle + name.ErrorStyle = lipgloss.NewStyle().Foreground(colors.Error) + name.Input.CharLimit = width + name.Focus() + name.Input.Validate = func(s string) error { + if strings.TrimSpace(s) == "" { + return errors.New("cannot be empty") + } + return nil + } + + nameWidth := lipgloss.Width(name.View()) + + color := textinput.New() + color.Prompt = "" + color.CharLimit = MaxHexDigits + color.Placeholder = "000000" + color.Validate = func(s string) error { + if len(s) != MaxHexDigits { + return errors.New("err") + } + return nil + } + color.SetValue(string(colors.White)[1:]) + + return Model{ + name: name, + color: color, + lastColor: lipgloss.Color("#" + color.Value()), + perms: packet.PermReadWrite, + create: blurredCreate, + + nameWidth: nameWidth, + precomputedStyle: lipgloss.NewStyle().Width(nameWidth / 3), + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) View() string { + name := m.name.View() + + color := colors.Gray + if m.color.Err != nil { + color = colors.Error + } else if m.selected == ColorField { + color = colors.Focus + } + colorInput := underlineStyle(m.color.View(), MaxHexDigits, color) + colorInput = lipgloss.NewStyle().Width(MaxHexDigits + 1).Render(colorInput) + colorIndicator := lipgloss.NewStyle().Foreground(m.lastColor).Render("■") + colorText := lipgloss.JoinHorizontal(lipgloss.Top, colorHeader, colorInput, colorIndicator) + + readWrite := "[ ] Read & Write" + if m.perms == packet.PermReadWrite { + readWrite = "[x] Read & Write" + } + if m.selected == ReadWriteField { + readWrite = focusStyle.Render(readWrite) + } + + readOnly := "[ ] Read Only" + if m.perms == packet.PermRead { + readOnly = "[x] Read Only" + } + if m.selected == ReadOnlyField { + readOnly = focusStyle.Render(readOnly) + } + + noAccess := "[ ] No Access" + if m.perms == packet.PermNoAccess { + noAccess = "[x] No Access" + } + if m.selected == NoAccessField { + noAccess = focusStyle.Render(noAccess) + } + + width := lipgloss.Width(noAccess) + lipgloss.Width(readOnly) + lipgloss.Width(readWrite) + padding := (m.nameWidth - (leftpad * 2) - width) / 2 + + readWrite = lipgloss.NewStyle().PaddingRight(padding).Render(readWrite) + readOnly = lipgloss.NewStyle().PaddingRight(padding).Render(readOnly) + + perms := lipgloss.JoinHorizontal(lipgloss.Top, readWrite, readOnly, noAccess) + perms = lipgloss.JoinVertical(lipgloss.Left, permsHeader, perms) + perms = lipgloss.NewStyle().PaddingLeft(leftpad).Render(perms) + + create := lipgloss.NewStyle().Width(m.nameWidth).Align(lipgloss.Center).Render(m.create) + + content := flex.NewVertical(name, perms, colorText, create).WithGap(1).View() + return style.Render(content) +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + key := msg.Type + switch key { + case tea.KeyTab: + return m, m.cycle(1) + case tea.KeyShiftTab: + return m, m.cycle(-1) + + default: + var cmd tea.Cmd + switch m.selected { + case NameField: + m.name, cmd = m.name.Update(msg) + case ColorField: + oldValue := m.color.Value() + position := m.color.Position() + m.color, cmd = m.color.Update(msg) + newValue := m.color.Value() + + hex := "0123456789abcdefABCDEF" + invalid := false + for _, c := range newValue { + if !strings.ContainsRune(hex, c) { + invalid = true + break + } + } + + if invalid { + m.color.SetValue(oldValue) + m.color.SetCursor(position) + } else if len(m.color.Value()) == 6 { + m.lastColor = lipgloss.Color("#" + m.color.Value()) + } + + } + return m, cmd + } + } + + return m, nil +} + +func (m *Model) cycle(step int) tea.Cmd { + m.selected += step + if m.selected < 0 { + m.selected = FieldCount - 1 + } else { + m.selected %= FieldCount + } + return m.updateFocus() +} + +func (m *Model) updateFocus() tea.Cmd { + m.name.Blur() + m.color.Blur() + m.create = blurredCreate + switch m.selected { + case NameField: + return m.name.Focus() + case ColorField: + return m.color.Focus() + case NoAccessField, ReadOnlyField, ReadWriteField: + return nil + case CreateField: + m.create = focusedCreate + return nil + default: + assert.Never("missing switch statement field in update focus", "selected", m.selected) + return nil + } +} + +func (m *Model) Select() tea.Cmd { + switch m.selected { + case NoAccessField: + m.perms = packet.PermNoAccess + return nil + case ReadOnlyField: + m.perms = packet.PermRead + return nil + case ReadWriteField: + m.perms = packet.PermReadWrite + return nil + } + + if m.selected != CreateField { + return nil + } + + m.name.Input.Err = m.name.Input.Validate(m.name.Input.Value()) + m.color.Err = m.color.Validate(m.color.Value()) + if m.name.Input.Err != nil || m.color.Err != nil { + return nil + } + + request := packet.CreateFrequency{ + Network: 0, // TODO: + Name: m.name.Input.Value(), + HexColor: "#" + m.color.Value(), + Perms: m.perms, + } + return gateway.Send(&request) +} diff --git a/internal/client/ui/core/network/network.go b/internal/client/ui/core/network/network.go index 5a5d0ec..8be44a3 100644 --- a/internal/client/ui/core/network/network.go +++ b/internal/client/ui/core/network/network.go @@ -149,6 +149,8 @@ func (m Model) Swap(dir int) (Model, tea.Cmd) { func (m *Model) Set(index int) { if index != -1 { m.network = &state.State.Networks[index] + } else { + m.network = nil } } diff --git a/internal/client/ui/core/networkcreation/networkcreation.go b/internal/client/ui/core/networkcreation/networkcreation.go index f1089ad..bf09306 100644 --- a/internal/client/ui/core/networkcreation/networkcreation.go +++ b/internal/client/ui/core/networkcreation/networkcreation.go @@ -205,7 +205,6 @@ func (m Model) View() string { private = privateStyle.Render(private) create := lipgloss.NewStyle().Width(m.nameWidth).Align(lipgloss.Center).Render(m.create) - // create := m.create content := flex.NewVertical(iconPreview, name, icon, private, create).WithGap(1).View() return style.Render(content) diff --git a/internal/packet/models.go b/internal/packet/models.go new file mode 100644 index 0000000..f1326e2 --- /dev/null +++ b/internal/packet/models.go @@ -0,0 +1,13 @@ +package packet + +const ( + MaxIconBytes = 16 + DefaultFrequencyName = "main" +) + +const ( + PermNoAccess = 0 + iota + PermRead + PermReadWrite + PermMax +) diff --git a/internal/server/api/models.go b/internal/server/api/models.go deleted file mode 100644 index 317ee6e..0000000 --- a/internal/server/api/models.go +++ /dev/null @@ -1,15 +0,0 @@ -package api - -const ( - MaxIconSize = 16 - DefaultFrequencyName = "main" -) - -const ( - PermNoAccess = 0 + iota - PermRead - PermReadWrite - PermMax -) - - -- cgit v1.3.1