diff options
author | xengineering <me@xengineering.eu> | 2024-09-29 12:38:30 +0200 |
---|---|---|
committer | xengineering <me@xengineering.eu> | 2024-09-29 12:38:30 +0200 |
commit | 9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309 (patch) | |
tree | 246c044d7cc9ecab9941c0419d82447a6749ce1a | |
parent | 58e6db7eca97e4514df82464cc2a1fe8d49c1adc (diff) | |
download | soundbox-app-9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309.tar soundbox-app-9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309.tar.zst soundbox-app-9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309.zip |
Add web radio player app as MVP
While streaming to soundbox devices is not supported this MVP is a
working mpv-based GUI to play sound from web radio URLs on a Linux
computer.
-rw-r--r-- | go.mod | 16 | ||||
-rw-r--r-- | go.sum | 23 | ||||
-rw-r--r-- | main.go | 157 |
3 files changed, 196 insertions, 0 deletions
@@ -0,0 +1,16 @@ +module xengineering.eu/soundbox + +go 1.22.5 + +require gioui.org v0.7.1 + +require ( + gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect + gioui.org/shader v1.0.8 // indirect + github.com/go-text/typesetting v0.1.1 // indirect + golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect + golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect + golang.org/x/image v0.18.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect +) @@ -0,0 +1,23 @@ +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= +gioui.org v0.7.1 h1:l7OVj47n1z8acaszQ6Wlu+Rxme+HqF3q8b+Fs68+x3w= +gioui.org v0.7.1/go.mod h1:5Kw/q7R1BWc5MKStuTNvhCgSrRqbfHc9Dzfjs4IGgZo= +gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= +gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= +gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= +github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU= +golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= @@ -0,0 +1,157 @@ +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) +} |