package page import ( "fmt" "strconv" "github.com/go-gl/gl/v3.3-core/gl" "github.com/veandco/go-sdl2/sdl" "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 } func (p *AudioDevices) Init(app ApplicationInterface) 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(app); err != nil { return fmt.Errorf("base init failed: %w", err) } // Get font size from database baseFontSizeStr, err := db.GetConfigValue("init_font_size") if err != nil { return fmt.Errorf("retrieving init_font_size from db: %w", err) } baseFontSize, err := strconv.Atoi(baseFontSizeStr) if err != nil { return fmt.Errorf("parsing init_font_size %q: %w", baseFontSizeStr, err) } // 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, err := db.GetConfigValue("audio_device_name") if err != nil { return fmt.Errorf("retrieving audio_device_name from db: %w", err) } // 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, err := db.GetConfigValue("audio_enabled") if err != nil { return fmt.Errorf("unable to query database for audio_enabled: %w", err) } audioStatus := "Turn Audio On" if on == "true" { 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" { on, err := db.GetConfigValue("audio_enabled") if err != nil { return fmt.Errorf("failed to fetch audio_enabled from database: %w", err) } currentStatus, err := strconv.ParseBool(on) if err != nil { return fmt.Errorf("failed to parse audio_enabled value: %w", err) } newStatus := strconv.FormatBool(!currentStatus) if err := db.SetConfigValue("audio_enabled", newStatus); err != nil { return fmt.Errorf("failed to update audio_enabled: %w", err) } p.App.UnwindToPage(&Settings{}) 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 if err := db.SetConfigValue("audio_device_name", deviceName); err != nil { return fmt.Errorf("failed to save audio device: %w", err) } if err := db.SetConfigValue("audio_enabled", "true"); err != nil { return fmt.Errorf("failed to enable audio: %w", err) } p.App.UnwindToPage(&Settings{}) case sdl.K_BACKSPACE: p.App.UnwindToPage(&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 }