diff options
| author | Kyren223 <Kyren223@proton.me> | 2025-01-14 17:33:15 +0200 |
|---|---|---|
| committer | Kyren223 <Kyren223@proton.me> | 2025-01-14 17:33:15 +0200 |
| commit | 43bd630ecb9470b997c9f8fa47aa6960ac551faa (patch) | |
| tree | cdd7c7a6b7dd564e8bee9f1acb0696c9cedaeb35 /internal | |
| parent | 016e8cfc3c67c1d9bd1d116851c82763ab63e95b (diff) | |
Implemented updating frequency properties both client-side and server-side
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/client/ui/core/core.go | 25 | ||||
| -rw-r--r-- | internal/client/ui/core/frequencyupdate/frequencyupdate.go | 295 | ||||
| -rw-r--r-- | internal/data/frequencies.sql.go | 33 | ||||
| -rw-r--r-- | internal/packet/types.go | 2 | ||||
| -rw-r--r-- | internal/server/api/api.go | 59 | ||||
| -rw-r--r-- | internal/server/server.go | 2 |
6 files changed, 415 insertions, 1 deletions
diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go index d8e4c1f..7b2d711 100644 --- a/internal/client/ui/core/core.go +++ b/internal/client/ui/core/core.go @@ -16,6 +16,7 @@ import ( "github.com/kyren223/eko/internal/client/ui/core/chat" "github.com/kyren223/eko/internal/client/ui/core/frequencycreation" "github.com/kyren223/eko/internal/client/ui/core/frequencylist" + "github.com/kyren223/eko/internal/client/ui/core/frequencyupdate" "github.com/kyren223/eko/internal/client/ui/core/networkcreation" "github.com/kyren223/eko/internal/client/ui/core/networkjoin" "github.com/kyren223/eko/internal/client/ui/core/networklist" @@ -55,6 +56,7 @@ type Model struct { networkUpdatePopup *networkupdate.Model networkJoinPopup *networkjoin.Model frequencyCreationPopup *frequencycreation.Model + frequencyUpdatePopup *frequencyupdate.Model networkList networklist.Model frequencyList frequencylist.Model chat chat.Model @@ -73,6 +75,7 @@ func New(privKey ed25519.PrivateKey, name string) Model { networkUpdatePopup: nil, networkJoinPopup: nil, frequencyCreationPopup: nil, + frequencyUpdatePopup: nil, networkList: networklist.New(), frequencyList: frequencylist.New(), chat: chat.New(70), @@ -110,6 +113,8 @@ func (m Model) View() string { popup = m.networkUpdatePopup.View() } else if m.frequencyCreationPopup != nil { popup = m.frequencyCreationPopup.View() + } else if m.frequencyUpdatePopup != nil { + popup = m.frequencyUpdatePopup.View() } else if m.networkJoinPopup != nil { popup = m.networkJoinPopup.View() } @@ -283,12 +288,20 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { case "u": index := m.networkList.Index() networkFocus := m.focus == FocusNetworkList + frequencyFocus := m.focus == FocusFrequencyList if !m.HasPopup() && networkFocus && index != networklist.PeersIndex { networkId := state.NetworkId(index) if networkId != nil { popup := networkupdate.New(*networkId) m.networkUpdatePopup = &popup } + } else if !m.HasPopup() && frequencyFocus { + networkId := state.NetworkId(index) + if networkId != nil { + index := m.frequencyList.Index() + popup := frequencyupdate.New(*networkId, index) + m.frequencyUpdatePopup = &popup + } } else { cmd := m.updatePopups(msg) if cmd != nil { @@ -301,6 +314,7 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { m.networkCreationPopup = nil m.networkUpdatePopup = nil m.frequencyCreationPopup = nil + m.frequencyUpdatePopup = nil m.networkJoinPopup = nil } @@ -323,6 +337,12 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { m.frequencyCreationPopup = nil } return cmd + } else if m.frequencyUpdatePopup != nil { + cmd := m.frequencyUpdatePopup.Select() + if cmd != nil { + m.frequencyUpdatePopup = nil + } + return cmd } else if m.networkJoinPopup != nil { cmd := m.networkJoinPopup.Select() if cmd != nil { @@ -417,6 +437,10 @@ func (m *Model) updatePopups(msg tea.Msg) tea.Cmd { popup, cmd := m.frequencyCreationPopup.Update(msg) m.frequencyCreationPopup = &popup return cmd + } else if m.frequencyUpdatePopup != nil { + popup, cmd := m.frequencyUpdatePopup.Update(msg) + m.frequencyUpdatePopup = &popup + return cmd } else if m.networkJoinPopup != nil { popup, cmd := m.networkJoinPopup.Update(msg) m.networkJoinPopup = &popup @@ -429,5 +453,6 @@ func (m *Model) HasPopup() bool { return m.networkCreationPopup != nil || m.networkUpdatePopup != nil || m.frequencyCreationPopup != nil || + m.frequencyUpdatePopup != nil || m.networkJoinPopup != nil } diff --git a/internal/client/ui/core/frequencyupdate/frequencyupdate.go b/internal/client/ui/core/frequencyupdate/frequencyupdate.go new file mode 100644 index 0000000..cfef86e --- /dev/null +++ b/internal/client/ui/core/frequencyupdate/frequencyupdate.go @@ -0,0 +1,295 @@ +package frequencyupdate + +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/core/state" + "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" + "github.com/kyren223/eko/pkg/snowflake" +) + +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 # ") + + blurredUpdate = lipgloss.NewStyle(). + Background(colors.Gray).Padding(0, 1).Render("Update Frequency") + focusedUpdate = lipgloss.NewStyle(). + Background(colors.Blue).Padding(0, 1).Render("Update Frequency") + + permsHeader = lipgloss.NewStyle(). + Foreground(colors.Turquoise). + Render("Permissions for non-admins:") + leftpad = 1 +) + +const ( + MaxHexDigits = 6 +) + +const ( + NameField = iota + ReadWriteField + ReadOnlyField + NoAccessField + ColorField + UpdateField + FieldCount +) + +type Model struct { + name field.Model + precomputedStyle lipgloss.Style + lastColor lipgloss.Color + update string + color textinput.Model + perms int + nameWidth int + selected int + network snowflake.ID + frequency snowflake.ID +} + +func New(network snowflake.ID, frequencyIndex int) Model { + frequency := state.State.Frequencies[network][frequencyIndex] + + 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 = packet.MaxFrequencyName + name.Focus() + name.Input.Validate = func(s string) error { + if strings.TrimSpace(s) == "" { + return errors.New("cannot be empty") + } + return nil + } + name.Input.SetValue(frequency.Name) + + 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(frequency.HexColor[1:]) + + return Model{ + name: name, + color: color, + lastColor: lipgloss.Color("#" + color.Value()), + perms: int(frequency.Perms), + update: blurredUpdate, + network: network, + frequency: frequency.ID, + + 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) + + update := lipgloss.NewStyle().Width(m.nameWidth).Align(lipgloss.Center).Render(m.update) + + content := flex.NewVertical(name, perms, colorText, update).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.update = blurredUpdate + switch m.selected { + case NameField: + return m.name.Focus() + case ColorField: + return m.color.Focus() + case NoAccessField, ReadOnlyField, ReadWriteField: + return nil + case UpdateField: + m.update = focusedUpdate + 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 != UpdateField { + 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.UpdateFrequency{ + Frequency: m.frequency, + Name: m.name.Input.Value(), + HexColor: "#" + m.color.Value(), + Perms: m.perms, + } + return gateway.Send(&request) +} diff --git a/internal/data/frequencies.sql.go b/internal/data/frequencies.sql.go index 9ee8994..171943f 100644 --- a/internal/data/frequencies.sql.go +++ b/internal/data/frequencies.sql.go @@ -134,3 +134,36 @@ func (q *Queries) SwapFrequencies(ctx context.Context, arg SwapFrequenciesParams _, err := q.db.ExecContext(ctx, swapFrequencies, arg.Pos1, arg.Pos2, arg.NetworkID) return err } + +const updateFrequency = `-- name: UpdateFrequency :one +UPDATE frequencies SET + name = ?, hex_color = ?, perms = ? +WHERE id = ? +RETURNING id, network_id, name, hex_color, perms, position +` + +type UpdateFrequencyParams struct { + Name string + HexColor string + Perms int64 + ID snowflake.ID +} + +func (q *Queries) UpdateFrequency(ctx context.Context, arg UpdateFrequencyParams) (Frequency, error) { + row := q.db.QueryRowContext(ctx, updateFrequency, + arg.Name, + arg.HexColor, + arg.Perms, + arg.ID, + ) + var i Frequency + err := row.Scan( + &i.ID, + &i.NetworkID, + &i.Name, + &i.HexColor, + &i.Perms, + &i.Position, + ) + return i, err +} diff --git a/internal/packet/types.go b/internal/packet/types.go index a790cda..e03a419 100644 --- a/internal/packet/types.go +++ b/internal/packet/types.go @@ -98,7 +98,7 @@ type UpdateFrequency struct { Name string HexColor string Frequency snowflake.ID - Perms byte + Perms int } func (m *UpdateFrequency) Type() PacketType { diff --git a/internal/server/api/api.go b/internal/server/api/api.go index b11bf36..a19e4dc 100644 --- a/internal/server/api/api.go +++ b/internal/server/api/api.go @@ -761,3 +761,62 @@ func UpdateNetwork(ctx context.Context, sess *session.Session, request *packet.U Partial: true, }) } + +func UpdateFrequency(ctx context.Context, sess *session.Session, request *packet.UpdateFrequency) packet.Payload { + queries := data.New(db) + + frequency, err := queries.GetFrequencyById(ctx, request.Frequency) + if err == sql.ErrNoRows { + return &packet.Error{Error: "frequency doesn't exist"} + } + if err != nil { + log.Println("database error 0:", err) + return &ErrInternalError + } + + isAdmin, err := IsNetworkAdmin(ctx, queries, sess.ID(), frequency.NetworkID) + if err == sql.ErrNoRows { + return &packet.Error{Error: "either network doesn't exist or user is not apart of this network"} + } + if err != nil { + log.Println("database error 1:", err) + return &ErrInternalError + } + if !isAdmin { + return &ErrPermissionDenied + } + + if len(request.Name) > packet.MaxFrequencyName { + return &packet.Error{Error: fmt.Sprintf( + "exceeded allowed frequency name length, max %v bytes", + packet.MaxFrequencyName, + )} + } + + if ok, err := isValidHexColor(request.HexColor); !ok { + return &packet.Error{Error: err} + } + + if request.Perms < 0 || request.Perms >= packet.PermMax { + return &packet.Error{Error: fmt.Sprintf( + "exceeded allowed perms value: 0 <= perms < %v", packet.PermMax, + )} + } + + frequency, err = queries.UpdateFrequency(ctx, data.UpdateFrequencyParams{ + Name: request.Name, + HexColor: request.HexColor, + Perms: int64(request.Perms), + ID: frequency.ID, + }) + if err != nil { + log.Println("database error 2:", err) + return &ErrInternalError + } + + return NetworkPropagate(ctx, sess, frequency.NetworkID, &packet.FrequenciesInfo{ + RemovedFrequencies: nil, + Frequencies: []data.Frequency{frequency}, + Network: frequency.NetworkID, + }) +} diff --git a/internal/server/server.go b/internal/server/server.go index 3bce2de..26ab7b7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -352,6 +352,8 @@ func processRequest(ctx context.Context, sess *session.Session, request packet.P case *packet.CreateFrequency: response = timeout(5*time.Millisecond, api.CreateFrequency, ctx, sess, request) + case *packet.UpdateFrequency: + response = timeout(5*time.Millisecond, api.UpdateFrequency, ctx, sess, request) case *packet.DeleteFrequency: response = timeout(200*time.Millisecond, api.DeleteFrequency, ctx, sess, request) case *packet.SwapFrequencies: |
