package main import ( "context" "image/color" "log" "net" "os" "sync" "gioui.org/app" "gioui.org/layout" "gioui.org/op" "gioui.org/text" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "xengineering.eu/soundbox-go/soundbox" ) func main() { config, err := loadConfig() if err != nil { log.Fatal(err) } ui := NewUi(config) go func() { err := ui.Run() if err != nil { log.Fatal(err) } os.Exit(0) }() app.Main() } type State struct { sync.Mutex Config Config Theme *material.Theme IsPlaying bool Title string UrlSelector widget.Enum PlayPauseButton widget.Clickable PlayerContext context.Context PlayerCancel context.CancelFunc } type Ui struct { Window *app.Window State State } func NewUi(config Config) *Ui { ui := Ui{} ui.State.Config = config ui.State.IsPlaying = false ui.State.Theme = material.NewTheme() ui.Window = new(app.Window) ui.Window.Option(app.Title("soundbox")) ui.State.Title = "soundbox" return &ui } func (ui *Ui) Cleanup() { ui.State.Lock() defer ui.State.Unlock() if ui.State.PlayerCancel != nil { done := ui.State.PlayerContext.Done() ui.State.PlayerCancel() <-done } } 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.IsPlaying { ui.State.PlayerCancel() } else { if ui.State.UrlSelector.Value == "" { log.Println("A URL has to be selected.") } else { ui.State.PlayerContext, ui.State.PlayerCancel = context.WithCancel(context.Background()) var targets []net.HardwareAddr for _, entry := range ui.State.Config.Soundboxes { if entry.Enabled { targets = append(targets, net.HardwareAddr(entry.Mac)) } } go play(ui.State.PlayerContext, ui.State.UrlSelector.Value, targets, 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 var playPauseButtonText string if ui.State.IsPlaying { playPauseButtonText = "Stop" } else { playPauseButtonText = "Play" } button := material.Button(ui.State.Theme, &ui.State.PlayPauseButton, playPauseButtonText) entries := []layout.FlexChild{ layout.Rigid(h1.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(25)}.Layout), } for _, url := range ui.State.Config.URLs { entries = append(entries, layout.Rigid(material.RadioButton( ui.State.Theme, &ui.State.UrlSelector, url.Url, url.Name).Layout)) } entries = append(entries, layout.Rigid(layout.Spacer{Height: unit.Dp(25)}.Layout)) entries = append(entries, layout.Rigid(button.Layout)) return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return flex.Layout(gtx, entries...) }) } func play(ctx context.Context, url string, targets []net.HardwareAddr, ui *Ui) { setPlayingState := func(isPlaying bool) { ui.State.Lock() defer ui.Window.Invalidate() defer ui.State.Unlock() ui.State.IsPlaying = isPlaying } setPlayingState(true) defer setPlayingState(false) err := soundbox.StreamURLContext(ctx, url, targets) if err != nil { log.Println(err) } }