114 lines
4.0 KiB
Go
114 lines
4.0 KiB
Go
package chat
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// AudioMessageOptions describe a voice message the caller plays.
|
|
type AudioMessageOptions struct {
|
|
// Duration is the length; required.
|
|
Duration time.Duration
|
|
// Position is how far playback has gone, 0 to Duration.
|
|
Position time.Duration
|
|
Playing bool
|
|
// Waveform holds loudness samples from 0 to 1; empty draws a flat line.
|
|
Waveform []float32
|
|
}
|
|
|
|
// AudioMessageResult is an AudioMessage's element and the user's requests this frame.
|
|
type AudioMessageResult struct {
|
|
Element *ui.Element
|
|
toggled bool
|
|
seek time.Duration
|
|
seeked bool
|
|
}
|
|
|
|
// Toggled reports a press of play or pause.
|
|
func (r AudioMessageResult) Toggled() bool { return r.toggled }
|
|
|
|
// Seek returns the position the user moved to by click, drag or keys.
|
|
func (r AudioMessageResult) Seek() (time.Duration, bool) { return r.seek, r.seeked }
|
|
|
|
// audioBarWidth and audioBarGap space the waveform's bars.
|
|
const audioBarWidth, audioBarGap float32 = 2, 2
|
|
|
|
// AudioMessage shows a voice bar: play button, waveform with progress, and times.
|
|
func AudioMessage(c *ui.Context, opts AudioMessageOptions) AudioMessageResult {
|
|
if opts.Duration <= 0 || opts.Position < 0 || opts.Position > opts.Duration {
|
|
panic("mujica: AudioMessage needs 0 <= Position <= Duration and Duration > 0")
|
|
}
|
|
for _, v := range opts.Waveform {
|
|
if !(v >= 0 && v <= 1) {
|
|
panic("mujica: AudioMessage Waveform samples must be from 0 to 1")
|
|
}
|
|
}
|
|
k := core.Tokens(c)
|
|
u := core.Density(c).Unit()
|
|
var r AudioMessageResult
|
|
r.Element = ui.Row(c).Gap(u*2).AlignItems(ui.Center).Padding(u, u*3, u, u).Width(300).
|
|
Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border)
|
|
disabled := r.Element.IsDisabled()
|
|
r.Element.Children(func() {
|
|
icon, name := "play", core.Msg(c, "audio_message.play")
|
|
if opts.Playing {
|
|
icon, name = "pause", core.Msg(c, "audio_message.pause")
|
|
}
|
|
if core.IconAction(c, icons.Must(icon), name).Clicked() && !disabled {
|
|
r.toggled = true
|
|
slog.Debug("mujica: audio_message toggle", "playing", !opts.Playing)
|
|
}
|
|
pos := opts.Position.Seconds()
|
|
s := ui.SliderBase(c, &pos, 0, opts.Duration.Seconds()).Grow(1).Height(theme.MinHitSize).
|
|
Label(core.Msg(c, "audio_message.seek")).Description(audioClock(opts.Position) + " / " + audioClock(opts.Duration))
|
|
internal.Ring(s, k.Focus, theme.ControlRadius)
|
|
if s.Changed() && !disabled {
|
|
r.seek, r.seeked = time.Duration(pos*float64(time.Second)), true
|
|
slog.Debug("mujica: audio_message seek", "at", r.seek)
|
|
}
|
|
frac := float32(opts.Position.Seconds() / opts.Duration.Seconds())
|
|
s.Draw(func(p *ui.Painter, b ui.Rect) { audioWave(p, b, opts.Waveform, frac, k) })
|
|
ui.Text(c, audioClock(opts.Position)+" / "+audioClock(opts.Duration)).FontFeatures("tnum").
|
|
FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Shrink(0)
|
|
})
|
|
return r
|
|
}
|
|
|
|
// audioWave draws bars, played ones in AccentText, with a position line so progress is not color alone.
|
|
func audioWave(p *ui.Painter, b ui.Rect, wave []float32, frac float32, k theme.Tokens) {
|
|
n := int((b.W + audioBarGap) / (audioBarWidth + audioBarGap))
|
|
if n <= 0 {
|
|
return
|
|
}
|
|
cy := b.Y + b.H/2
|
|
for i := range n {
|
|
v := float32(0.12)
|
|
if len(wave) > 0 {
|
|
v = max(v, wave[i*len(wave)/n])
|
|
}
|
|
h := max(2, v*(b.H-8))
|
|
x := b.X + float32(i)*(audioBarWidth+audioBarGap)
|
|
col := k.TextMuted.Alpha(0.55)
|
|
if (x-b.X)/b.W < frac {
|
|
col = k.AccentText
|
|
}
|
|
p.Fill(ui.Rect{X: x, Y: cy - h/2, W: audioBarWidth, H: h}, col, 1)
|
|
}
|
|
at := internal.Snap(p, b.X+b.W*frac)
|
|
p.Fill(ui.Rect{X: at - 1, Y: b.Y + 2, W: 2, H: b.H - 4}, k.Text, 1)
|
|
}
|
|
|
|
// audioClock formats d as m:ss.
|
|
func audioClock(d time.Duration) string {
|
|
s := int(d.Round(time.Second).Seconds())
|
|
return fmt.Sprintf("%d:%02d", s/60, s%60)
|
|
}
|