mirror of
https://github.com/Kyren223/eko.git
synced 2025-09-05 13:08:20 +00:00
Implemented updating frequency properties both client-side and server-side
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
295
internal/client/ui/core/frequencyupdate/frequencyupdate.go
Normal file
295
internal/client/ui/core/frequencyupdate/frequencyupdate.go
Normal file
@@ -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)
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -98,7 +98,7 @@ type UpdateFrequency struct {
|
||||
Name string
|
||||
HexColor string
|
||||
Frequency snowflake.ID
|
||||
Perms byte
|
||||
Perms int
|
||||
}
|
||||
|
||||
func (m *UpdateFrequency) Type() PacketType {
|
||||
|
@@ -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,
|
||||
})
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -18,6 +18,12 @@ INSERT INTO frequencies (
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateFrequency :one
|
||||
UPDATE frequencies SET
|
||||
name = ?, hex_color = ?, perms = ?
|
||||
WHERE id = ?
|
||||
RETURNING *;
|
||||
|
||||
-- name: SwapFrequencies :exec
|
||||
UPDATE frequencies SET
|
||||
position = CASE
|
||||
|
Reference in New Issue
Block a user