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 }