package main import ( "context" "image/color" "log" "os" "os/exec" "sync" "gioui.org/app" "gioui.org/layout" "gioui.org/op" "gioui.org/text" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" ) func main() { ui := NewUi() go func() { err := ui.Run() if err != nil { log.Fatal(err) } os.Exit(0) }() app.Main() } type State struct { sync.Mutex Theme *material.Theme Title string UrlEditor widget.Editor PlayPauseButton widget.Clickable PlayPauseButtonText string PlayerContext context.Context PlayerCancel context.CancelFunc } type Ui struct { Window *app.Window State State } func NewUi() *Ui { ui := Ui{} ui.State.Theme = material.NewTheme() ui.Window = new(app.Window) ui.Window.Option(app.Title("soundbox app")) ui.State.Title = "soundbox" ui.State.PlayPauseButtonText = "Play" return &ui } func (ui *Ui) Cleanup() { ui.State.Lock() defer ui.State.Unlock() if ui.State.PlayerCancel != nil { ui.State.PlayerCancel() } } func (ui *Ui) Run() error { defer ui.Cleanup() var ops op.Ops for { switch e := ui.Window.Event().(type) { case app.DestroyEvent: return e.Err case app.FrameEvent: gtx := app.NewContext(&ops, e) ui.HandleInputs(gtx) ui.Layout(gtx) e.Frame(gtx.Ops) } } } func (ui *Ui) HandleInputs(gtx layout.Context) { ui.State.Lock() defer ui.State.Unlock() if ui.State.PlayPauseButton.Clicked(gtx) { if ui.State.UrlEditor.ReadOnly { ui.State.PlayerCancel() } else { ui.State.PlayerContext, ui.State.PlayerCancel = context.WithCancel(context.Background()) go mpv(ui.State.PlayerContext, ui.State.UrlEditor.Text(), ui) } } } func (ui *Ui) Layout(gtx layout.Context) layout.Dimensions { ui.State.Lock() defer ui.State.Unlock() inset := layout.UniformInset(unit.Dp(10)) flex := layout.Flex{ Axis: layout.Vertical, Spacing: layout.SpaceEnd, } h1 := material.H1(ui.State.Theme, ui.State.Title) h1.Color = color.NRGBA{R: 88, G: 88, B: 88, A: 255} h1.Alignment = text.Middle editor := material.Editor(ui.State.Theme, &ui.State.UrlEditor, "Audio stream URL") editor.Editor.Alignment = text.Middle button := material.Button(ui.State.Theme, &ui.State.PlayPauseButton, ui.State.PlayPauseButtonText) return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return flex.Layout(gtx, layout.Rigid(h1.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(25)}.Layout), layout.Rigid(editor.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(25)}.Layout), layout.Rigid(button.Layout), ) }) } func mpv(ctx context.Context, url string, ui *Ui) { setPlayingState := func(isPlaying bool) { ui.State.Lock() defer ui.Window.Invalidate() defer ui.State.Unlock() ui.State.UrlEditor.ReadOnly = isPlaying if isPlaying { ui.State.PlayPauseButtonText = "Stop" } else { ui.State.PlayPauseButtonText = "Play" } } setPlayingState(true) cmd := exec.CommandContext( ctx, "mpv", "--no-video", url, ) _ = cmd.Run() setPlayingState(false) }