From 2f036b9bcca9f2e578eaec2c2094a0428e0dbc1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=9E=E3=83=AA=E3=82=A6=E3=82=B9?= Date: Mon, 2 Jan 2023 13:24:48 -0500 Subject: [PATCH] Implemented postdialog draft --- ui/windows/postdialog/postdialog.go | 331 ++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 ui/windows/postdialog/postdialog.go diff --git a/ui/windows/postdialog/postdialog.go b/ui/windows/postdialog/postdialog.go new file mode 100644 index 0000000..863cb12 --- /dev/null +++ b/ui/windows/postdialog/postdialog.go @@ -0,0 +1,331 @@ +package postdialog + +import ( + "fmt" + "strconv" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" + "github.com/mrusme/gobbs/aggregator" + "github.com/mrusme/gobbs/models/post" + "github.com/mrusme/gobbs/models/reply" + "github.com/mrusme/gobbs/ui/cmd" + "github.com/mrusme/gobbs/ui/ctx" + "github.com/mrusme/gobbs/ui/helpers" +) + +var ( + viewportStyle = lipgloss.NewStyle(). + Margin(0, 0, 0, 0). + Padding(0, 0). + BorderTop(false). + BorderLeft(false). + BorderRight(false). + BorderBottom(false) +) + +type KeyMap struct { + Refresh key.Binding + Select key.Binding + Esc key.Binding + Quit key.Binding + Reply key.Binding +} + +var DefaultKeyMap = KeyMap{ + Refresh: key.NewBinding( + key.WithKeys("ctrl+r"), + key.WithHelp("ctrl+r", "refresh"), + ), + Select: key.NewBinding( + key.WithKeys("r", "enter"), + key.WithHelp("r/enter", "read"), + ), + Esc: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "close"), + ), + Quit: key.NewBinding( + key.WithKeys("q"), + ), + Reply: key.NewBinding( + key.WithKeys("ctrl+s"), + key.WithHelp("ctrl+s", "reply"), + ), +} + +type Model struct { + ctx *ctx.Ctx + keymap KeyMap + focused bool + viewport viewport.Model + + a *aggregator.Aggregator + glam *glamour.TermRenderer + + buffer string + replyIDs []string + + activePost *post.Post + allReplies []*reply.Reply + activeReply *reply.Reply +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Focus() { + m.focused = true +} + +func (m Model) Blur() { + m.focused = false +} + +func NewModel(c *ctx.Ctx) Model { + m := Model{ + ctx: c, + keymap: DefaultKeyMap, + + buffer: "", + replyIDs: []string{}, + } + + m.a, _ = aggregator.New(m.ctx) + + return m +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + + case key.Matches(msg, m.keymap.Select): + if m.buffer != "" { + replyToID, err := strconv.Atoi(m.buffer) + if err != nil { + // TODO: Handle error + } + + if replyToID >= len(m.replyIDs) { + // TODO: Handle error + } + } + // m.WMOpen("reply") + + m.ctx.Logger.Debugln("caching view") + m.ctx.Logger.Debugf("buffer: %s", m.buffer) + // m.viewcache = m.buildView(false) + + return m, nil + + case key.Matches(msg, m.keymap.Esc), key.Matches(msg, m.keymap.Quit): + // m.WMClose("post") + return m, nil + + default: + switch msg.String() { + case "1", "2", "3", "4", "5", "6", "7", "8", "9", "0": + m.buffer += msg.String() + return m, nil + default: + m.buffer = "" + } + } + + case tea.WindowSizeMsg: + m.ctx.Logger.Debug("received WindowSizeMsg") + viewportWidth := m.ctx.Content[0] - 9 + viewportHeight := m.ctx.Content[1] - 10 + + viewportStyle.Width(viewportWidth) + viewportStyle.Height(viewportHeight) + m.viewport = viewport.New(viewportWidth-4, viewportHeight-4) + m.viewport.Width = viewportWidth - 4 + m.viewport.Height = viewportHeight + 1 + // cmds = append(cmds, viewport.Sync(m.viewport)) + + case *post.Post: + m.viewport.SetContent(m.renderViewport(msg)) + m.ctx.Loading = false + return m, nil + + case cmd.Command: + m.ctx.Logger.Debugf("got command: %v\n", msg) + switch msg.Call { + case cmd.WinFocus: + if msg.Target == "post" { + m.focused = true + } + return m, nil + case cmd.WinBlur: + if msg.Target == "post" { + m.focused = false + } + return m, nil + } + + default: + m.ctx.Logger.Debugf("received unhandled msg: %v\n", msg) + } + + var cmd tea.Cmd + + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m Model) View() string { + return m.buildView(true) +} + +func (m Model) buildView(cached bool) string { + var view strings.Builder = strings.Builder{} + + var l string = "" + view.WriteString(lipgloss.JoinHorizontal( + lipgloss.Top, + l, + )) + + var style lipgloss.Style + if m.focused { + style = m.ctx.Theme.DialogBox.Titlebar.Focused + } else { + style = m.ctx.Theme.DialogBox.Titlebar.Blurred + } + titlebar := style.Align(lipgloss.Center). + Width(m.viewport.Width + 4). + Render("Post") + + bottombar := m.ctx.Theme.DialogBox.Bottombar. + Width(m.viewport.Width + 4). + Render("[#]r reply ยท esc close") + + ui := lipgloss.JoinVertical( + lipgloss.Center, + titlebar, + viewportStyle.Render(m.viewport.View()), + bottombar, + ) + + var tmp string + if m.focused { + tmp = helpers.PlaceOverlay(3, 2, + m.ctx.Theme.DialogBox.Window.Focused.Render(ui), + view.String(), true) + } else { + tmp = helpers.PlaceOverlay(3, 2, + m.ctx.Theme.DialogBox.Window.Blurred.Render(ui), + view.String(), true) + } + + view = strings.Builder{} + view.WriteString(tmp) + + return view.String() +} + +func (m *Model) renderViewport(p *post.Post) string { + var out string = "" + + var err error + m.glam, err = glamour.NewTermRenderer( + glamour.WithAutoStyle(), + glamour.WithWordWrap(m.viewport.Width), + ) + if err != nil { + m.ctx.Logger.Error(err) + m.glam = nil + } + + adj := "writes" + if p.Subject[len(p.Subject)-1:] == "?" { + adj = "asks" + } + + body, err := m.glam.Render(p.Body) + if err != nil { + m.ctx.Logger.Error(err) + body = p.Body + } + out += fmt.Sprintf( + " %s\n\n %s\n%s", + m.ctx.Theme.Post.Author.Render( + fmt.Sprintf("%s %s:", p.Author.Name, adj), + ), + m.ctx.Theme.Post.Subject.Render(p.Subject), + body, + ) + + m.replyIDs = []string{p.ID} + m.activePost = p + out += m.renderReplies(0, p.Author.Name, &p.Replies) + + return out +} + +func (m *Model) renderReplies( + level int, + inReplyTo string, + replies *[]reply.Reply, +) string { + var out string = "" + + if replies == nil { + return "" + } + + for ri, re := range *replies { + var err error = nil + var body string = "" + var author string = "" + + if re.Deleted { + body = "\n DELETED\n\n" + author = "DELETED" + } else { + body, err = m.glam.Render(re.Body) + if err != nil { + m.ctx.Logger.Error(err) + body = re.Body + } + + author = re.Author.Name + } + + m.replyIDs = append(m.replyIDs, re.ID) + m.allReplies = append(m.allReplies, &(*replies)[ri]) + idx := len(m.replyIDs) - 1 + + out += fmt.Sprintf( + "\n\n %s %s%s%s\n%s", + m.ctx.Theme.Reply.Author.Render( + author, + ), + lipgloss.NewStyle(). + Foreground(m.ctx.Theme.Reply.Author.GetBackground()). + Render(fmt.Sprintf("writes in reply to %s:", inReplyTo)), + strings.Repeat(" ", (m.viewport.Width-len(author)-len(inReplyTo)-28)), + lipgloss.NewStyle(). + Foreground(lipgloss.Color("#777777")). + Render(fmt.Sprintf("#%d", idx)), + body, + ) + + idx++ + out += m.renderReplies(level+1, re.Author.Name, &re.Replies) + } + + return out +}