diff --git a/go.mod b/go.mod index 55dc4e3..55d34d7 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/andybalholm/brotli v1.2.6 github.com/ayn2op/arikawa/v3 v3.0.0-20261012034659-5b006907b2de github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e - github.com/ayn2op/tview v0.0.0-20261013011207-c8c627013874 + github.com/ayn2op/tview v0.0.0-20261013221052-78efeee1ac7c github.com/bogdanfinn/fhttp v0.6.9 github.com/bogdanfinn/tls-client v1.16.0 github.com/gdamore/tcell/v3 v3.6.0 diff --git a/go.sum b/go.sum index 85e2e57..8c6032b 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20261012034659-5b006907b2de h1:bO825TKutshlG github.com/ayn2op/arikawa/v3 v3.0.0-20261012034659-5b006907b2de/go.mod h1:YARcqZmk7hDjXB+lexlXX4wDFHULafbGnF0gsD0tuNw= github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e h1:QAei/IVEyZV7GcpkpcFUh37+QRqwrt57z7OT3ciHrhI= github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e/go.mod h1:RsvYiMzi4Si1F7vmml5NCL2DaKIZXkNUQC7QlGTMtCw= -github.com/ayn2op/tview v0.0.0-20261013011207-c8c627013874 h1:zXM8mKeDfXx+dzGEVTTZ0Xfo9vCXNBiX2qEWz/6KMV0= -github.com/ayn2op/tview v0.0.0-20261013011207-c8c627013874/go.mod h1:3aWH6S5uOqOkZv7JFePEVHukTj9hPb6Lbynm59Sks2E= +github.com/ayn2op/tview v0.0.0-20261013221052-78efeee1ac7c h1:WgEp8OeS15d15kjPVPOPpnSsaKYJZYjTIKxBbr8b5kI= +github.com/ayn2op/tview v0.0.0-20261013221052-78efeee1ac7c/go.mod h1:3aWH6S5uOqOkZv7JFePEVHukTj9hPb6Lbynm59Sks2E= github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= diff --git a/internal/ui/chat/messageslist/model.go b/internal/ui/chat/messageslist/model.go index 8ec0e06..bb3e3e5 100644 --- a/internal/ui/chat/messageslist/model.go +++ b/internal/ui/chat/messageslist/model.go @@ -26,6 +26,7 @@ import ( "github.com/ayn2op/discordo/internal/voice" "github.com/ayn2op/ningen/v3" "github.com/ayn2op/tview" + "github.com/ayn2op/tview/button" "github.com/ayn2op/tview/column" tviewimage "github.com/ayn2op/tview/image" "github.com/ayn2op/tview/keybind" @@ -69,6 +70,8 @@ type Model struct { // voicePlayer plays the voice message played last, and voiceStatus is what it last said of it. voicePlayer *voice.Player voiceStatus voice.PlayerStatus + // seeking reports whether the waveform of the voice message is dragged, which is sought once it is let go of. + seeking bool // kitty reports whether previews are drawn with kitty's graphics protocol. // kittyImages counts the images sent to the terminal. @@ -682,6 +685,9 @@ func (m Model) voiceMessageView(a discord.Attachment) tview.Widget { } waveform := slider.New(0, duration, status.Position, onSeek). + OnPress(func(position time.Duration) tview.Msg { return seekPressMsg(position) }). + OnRelease(seekReleaseMsg{}). + Dragging(loaded && m.seeking). Width(layout.Fixed(len(rail))). Rail(func(cell, handle int) (string, tview.Style) { if loaded && cell <= handle { @@ -691,7 +697,10 @@ func (m Model) voiceMessageView(a discord.Attachment) tview.Widget { return rail[cell], style.Dim(true) }) - return row.New(text.New(symbol), waveform, text.New(label)).Spacing(1).Height(layout.Fixed(1)) + width, _ := text.New(symbol).Size() + play := button.New().Label(symbol).Width(width).OnClick(playMsg(a)) + + return row.New(play, waveform, text.New(label)).Spacing(1).Height(layout.Fixed(1)) } func (m Model) drawEmbeds(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int, contentRoot ast.Node, contentSource []byte) { @@ -980,7 +989,9 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.OpenInBrowser.Keybind): return m, m.openInBrowser() case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.Play.Keybind): - return m, m.play() + if message, ok := m.selectedMessage(); ok && isVoiceMessage(*message) { + return m, m.play(message.Attachments[0]) + } case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.SeekBackward.Keybind): return m, m.seek(m.voiceStatus.Position - seekStep) case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.SeekForward.Keybind): @@ -1038,10 +1049,31 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { m.items[index].previews[msg.proxy] = preview return m, cmd + case playMsg: + return m, m.play(discord.Attachment(msg)) + case seekPressMsg: + m.seeking = true + m.voiceStatus.Position = time.Duration(msg) + return m, nil case seekMsg: - return m, m.seek(time.Duration(msg)) + m.voiceStatus.Position = time.Duration(msg) + if m.seeking { + return m, nil + } + + return m, m.seek(m.voiceStatus.Position) + case seekReleaseMsg: + m.seeking = false + return m, m.seek(m.voiceStatus.Position) case voiceMsg: - m.voiceStatus = m.voicePlayer.Status() + status := m.voicePlayer.Status() + // The waveform shows where it is dragged to rather than what is playing, while that voice message is the one loaded. + m.seeking = m.seeking && status.ID == m.voiceStatus.ID + if m.seeking { + status.Position = m.voiceStatus.Position + } + + m.voiceStatus = status return m, m.Init() case tview.ResizeMsg: cols, rows := msg.Size() diff --git a/internal/ui/chat/messageslist/msg.go b/internal/ui/chat/messageslist/msg.go index c5cdf08..c1b58c3 100644 --- a/internal/ui/chat/messageslist/msg.go +++ b/internal/ui/chat/messageslist/msg.go @@ -159,14 +159,13 @@ func isVoiceMessage(message discord.Message) bool { return message.Flags&discord.IsVoiceMessage != 0 && len(message.Attachments) == 1 } -// play plays the selected voice message, or pauses or resumes it if it is the one playing. -func (m Model) play() tview.Cmd { - message, ok := m.selectedMessage() - if !ok || !isVoiceMessage(*message) { - return nil - } +// playMsg asks to play the voice message whose play symbol was clicked. +type playMsg discord.Attachment - attachment := message.Attachments[0] +func (playMsg) messagesList() {} + +// play plays the voice message that is attachment, or pauses or resumes it if it is the one playing. +func (m Model) play(attachment discord.Attachment) tview.Cmd { return func() tview.Msg { if err := m.playVoiceMessage(attachment); err != nil { return ui.ErrorDialog("play voice message", err) @@ -196,10 +195,16 @@ func (m Model) playVoiceMessage(attachment discord.Attachment) error { return m.voicePlayer.Play(attachment.ID, audio) } -// seekMsg asks to seek to where the waveform of a voice message was clicked. -type seekMsg time.Duration +// seekPressMsg is where the waveform of a voice message was pressed, seekMsg where it was dragged to after, and seekReleaseMsg that it was let go of. +type ( + seekPressMsg time.Duration + seekMsg time.Duration + seekReleaseMsg struct{} +) -func (seekMsg) messagesList() {} +func (seekPressMsg) messagesList() {} +func (seekMsg) messagesList() {} +func (seekReleaseMsg) messagesList() {} // seek moves the voice message that is playing or paused to position. func (m Model) seek(position time.Duration) tview.Cmd { diff --git a/internal/ui/chat/model.go b/internal/ui/chat/model.go index 39df738..12d734a 100644 --- a/internal/ui/chat/model.go +++ b/internal/ui/chat/model.go @@ -367,7 +367,8 @@ func (p paneWidget) Draw(screen tview.Screen, area tview.Rectangle) { func (p paneWidget) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { out := p.child.Handle(msg, area) - if mouse, ok := msg.(tview.MouseMsg); ok && area.Contains(mouse.Position()) { + // Mouse input the pane leaves, but for a press, goes on to the other panes, for a slider dragged out of one to follow the pointer. + if mouse, ok := msg.(tview.MouseMsg); ok && area.Contains(mouse.Position()) && (out != msg || mouse.Action == tview.MouseLeftDown) { return paneMsg{pane: p.pane, msg: out, focus: mouse.Action == tview.MouseLeftDown} } // Keys and paste reach only the focused pane, so what it makes of them goes to that pane through route.