Database is more usable now. All pages have been updated to use the database, and share an application configuration. Page Stack has been removed, no more pages hanging around in memory. Replaced with a 'Router' which uses an Enum to determine which page should be displayed, destroys the old page and initializes the new page all in one go. No more push, pop, rollback, etc. No more holding instances of pages. Logger has been updated with proper timestamps and sorting now works as expected."
247 lines
6.5 KiB
Go
247 lines
6.5 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/go-gl/gl/v3.3-core/gl"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"gorm.io/gorm"
|
|
"rhemawavetech.com/rhemawave/internal/colors"
|
|
"rhemawavetech.com/rhemawave/internal/db"
|
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/mixer"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
type AudioDevices struct {
|
|
Base
|
|
font *fontManager.FontEntry
|
|
textShader *shaderManager.Shader
|
|
solidShader *shaderManager.Shader
|
|
title StringItem
|
|
prompt StringItem
|
|
devices []StringItem
|
|
highlightIndex int
|
|
selectedIndex int
|
|
verticalMargin int32
|
|
CfgPtr *db.AppConfig
|
|
dbPtr *gorm.DB
|
|
}
|
|
|
|
func (p *AudioDevices) Init(ctx *PageCtx) error {
|
|
var err error
|
|
p.verticalMargin = int32(20)
|
|
|
|
// Get font
|
|
fm := fontManager.Get()
|
|
font, ok := fm.GetFont("Roboto-Regular")
|
|
if !ok {
|
|
return fmt.Errorf("failed to get font: %w", sdl.GetError())
|
|
}
|
|
p.font = font
|
|
|
|
// Get shaders
|
|
sm := shaderManager.Get()
|
|
p.textShader, ok = sm.Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("failed to get text shader: %w", sdl.GetError())
|
|
}
|
|
p.solidShader, ok = sm.Get("solid_color")
|
|
if !ok {
|
|
return fmt.Errorf("failed to get solid_color shader: %w", sdl.GetError())
|
|
}
|
|
|
|
// Initialize base
|
|
if err := p.Base.Init(ctx); err != nil {
|
|
return fmt.Errorf("base init failed: %w", err)
|
|
}
|
|
|
|
// Get font size from database
|
|
baseFontSize := p.Base.Context.CfgPtr.StandardFontSize
|
|
|
|
// Calculate scales
|
|
titleScale := float32(64) / float32(baseFontSize)
|
|
itemScale := float32(32) / float32(baseFontSize)
|
|
|
|
// Create title
|
|
p.title, err = NewStringItem("Select an Audio Output Device", p.font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("creating title: %w", err)
|
|
}
|
|
p.title.Scale(titleScale)
|
|
|
|
// Create prompt
|
|
p.prompt, err = NewStringItem("Use Arrow Keys to select, Enter to confirm", p.font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("creating prompt: %w", err)
|
|
}
|
|
p.prompt.Scale(itemScale)
|
|
|
|
// Get list of audio output devices
|
|
devices := mixer.ListDevices()
|
|
if len(devices) == 0 {
|
|
// Create a default entry so the page doesn't break
|
|
devices = []string{"System Default"}
|
|
}
|
|
|
|
// Get currently selected device from database
|
|
currentDevice := p.Base.Context.CfgPtr.SelectedAudioDevice
|
|
|
|
// Create UI items for each device
|
|
for i, name := range devices { // Iterate over the devices
|
|
item, err := NewStringItem(name, p.font, colors.White) // Create a new string string texture
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create device item name: %w", err)
|
|
}
|
|
item.Scale(itemScale) // Scale the new string item
|
|
p.devices = append(p.devices, item) // Add the new scaled string item to the list of items
|
|
|
|
// Mark currently selected device
|
|
if name == currentDevice {
|
|
p.selectedIndex = i
|
|
p.highlightIndex = i // Start with cursor on current device
|
|
}
|
|
}
|
|
|
|
on := p.Base.Context.CfgPtr.AudioEnabled
|
|
|
|
audioStatus := "Turn Audio On"
|
|
if on {
|
|
audioStatus = "Turn Audio Off"
|
|
}
|
|
|
|
audioStatusStringItem, err := NewStringItem(audioStatus, p.font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create Turn Audio Off string item: %w", err)
|
|
}
|
|
|
|
// Scale the new option
|
|
audioStatusStringItem.Scale(itemScale)
|
|
|
|
p.devices = append(p.devices, audioStatusStringItem)
|
|
|
|
p.layout()
|
|
return nil
|
|
}
|
|
|
|
func (p *AudioDevices) layout() {
|
|
// Position title at bottom
|
|
p.title.X = p.ScreenCenterX - p.title.W/2
|
|
p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2
|
|
|
|
// Position prompt above title
|
|
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
|
|
p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H
|
|
|
|
// Position device items stacked upward from prompt
|
|
for i := range p.devices {
|
|
p.devices[i].X = p.ScreenCenterX - p.devices[i].W/2
|
|
if i == 0 {
|
|
p.devices[i].Y = p.prompt.Y - p.verticalMargin*2 - p.devices[i].H
|
|
} else {
|
|
p.devices[i].Y = p.devices[i-1].Y - p.devices[i].H - 10
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
|
|
e, ok := (*event).(*sdl.KeyboardEvent)
|
|
if !ok || e.Type != sdl.KEYDOWN {
|
|
return nil
|
|
}
|
|
|
|
switch e.Keysym.Sym {
|
|
case sdl.K_UP:
|
|
if p.highlightIndex > 0 {
|
|
p.highlightIndex--
|
|
}
|
|
|
|
case sdl.K_DOWN:
|
|
if p.highlightIndex < len(p.devices)-1 {
|
|
p.highlightIndex++
|
|
}
|
|
|
|
case sdl.K_RETURN:
|
|
selection := p.devices[p.highlightIndex].content
|
|
|
|
// Handle audio disable
|
|
if selection == "Turn Audio Off" || selection == "Turn Audio On" {
|
|
newStatus := !p.Base.Context.CfgPtr.AudioEnabled
|
|
updatedConfig := p.Base.Context.CfgPtr
|
|
updatedConfig.AudioEnabled = newStatus
|
|
p.Base.Context.DbPtr.Save(&updatedConfig)
|
|
p.Base.Context.RefreshConfig()
|
|
return nil
|
|
}
|
|
|
|
// Map "System Default" to empty string for SDL
|
|
deviceName := selection
|
|
if selection == "System Default" {
|
|
deviceName = ""
|
|
}
|
|
|
|
// Validate by attempting the switch
|
|
if err := mixer.SwitchDevice(deviceName); err != nil {
|
|
return fmt.Errorf("failed to switch to device %q: %w", selection, err)
|
|
}
|
|
|
|
// Save settings
|
|
p.Base.Context.CfgPtr.SelectedAudioDevice = deviceName
|
|
p.Base.Context.CfgPtr.AudioEnabled = true
|
|
p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr)
|
|
p.Base.Context.RefreshConfig()
|
|
case sdl.K_BACKSPACE:
|
|
p.Base.Context.NavigateTo(SETTINGS)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *AudioDevices) Render() error {
|
|
// Always render title and prompt
|
|
p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color)
|
|
p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color)
|
|
|
|
// No devices to display
|
|
if len(p.devices) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Render highlight box behind highlighted item
|
|
p.RenderSolidColorQuad(
|
|
p.solidShader,
|
|
p.devices[p.highlightIndex].Position(),
|
|
p.devices[p.highlightIndex].RawDimensions(),
|
|
colors.White,
|
|
)
|
|
|
|
// Render device items with appropriate colors
|
|
for i, item := range p.devices {
|
|
if i == p.highlightIndex && i == p.selectedIndex {
|
|
// Currently highlighted AND selected device - dark green
|
|
item.Color = colors.DarkGreen
|
|
} else if i == p.highlightIndex {
|
|
// Just highlighted - black (shows on white background)
|
|
item.Color = colors.Black
|
|
} else if i == p.selectedIndex {
|
|
// Just selected - green
|
|
item.Color = colors.Green
|
|
} else {
|
|
// Normal item - white
|
|
item.Color = colors.White
|
|
}
|
|
p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *AudioDevices) Destroy() error {
|
|
gl.DeleteTextures(1, &p.title.ID)
|
|
gl.DeleteTextures(1, &p.prompt.ID)
|
|
for _, item := range p.devices {
|
|
gl.DeleteTextures(1, &item.ID)
|
|
}
|
|
return nil
|
|
}
|