Remove track list from AudioOptions
This commit is contained in:
+25
-350
@@ -1,16 +1,12 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
"gorm.io/gorm"
|
||||
"rhemawavetech.com/rhemawave/internal/colors"
|
||||
"rhemawavetech.com/rhemawave/internal/db"
|
||||
"rhemawavetech.com/rhemawave/internal/fontManager"
|
||||
@@ -19,6 +15,9 @@ import (
|
||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||
)
|
||||
|
||||
// AudioFileList is kept as the canonical seed data for the audio_files table.
|
||||
// Track selection UI no longer lives on this page, but other parts of the
|
||||
// app still read these rows from the database.
|
||||
var AudioFileList = []db.AudioFile{
|
||||
{ID: 14, Name: "Original Audio", Path: "OriginalAudio.wav", Selected: false, Order: 1},
|
||||
{ID: 1, Name: "174 Hz", Path: "174Hz.wav", Selected: false, Order: 2},
|
||||
@@ -36,13 +35,7 @@ var AudioFileList = []db.AudioFile{
|
||||
{ID: 13, Name: "Pineal Gland", Path: "PinealGland.wav", Selected: false, Order: 14},
|
||||
}
|
||||
|
||||
const (
|
||||
triangleSize = float32(16)
|
||||
visibleCount = 9
|
||||
listShiftDown = int32(50)
|
||||
scrollbarWidth = int32(4)
|
||||
scrollbarGap = int32(16) // gap between right edge of list and scrollbar
|
||||
)
|
||||
const triangleSize = float32(16)
|
||||
|
||||
type AudioOptions struct {
|
||||
Base
|
||||
@@ -58,7 +51,7 @@ type AudioOptions struct {
|
||||
// Audio toggle
|
||||
AudioStatusToggle StringItem
|
||||
|
||||
// Volume row (excluded from focus)
|
||||
// Volume row
|
||||
VolumeLabel StringItem
|
||||
volumePercent StringItem
|
||||
volumeBarX int32
|
||||
@@ -66,33 +59,6 @@ type AudioOptions struct {
|
||||
volumeBarW int32
|
||||
volumeBarH int32
|
||||
|
||||
// Separator + track section
|
||||
separatorY int32
|
||||
tracksHeading StringItem
|
||||
trackItems []StringItem
|
||||
|
||||
// Selected track (-1 = none)
|
||||
selectedTrack int
|
||||
|
||||
// Navigation
|
||||
// focusedIndex: 0 = AudioStatusToggle, 1..N = trackItems[focusedIndex-1]
|
||||
// Volume is intentionally excluded from focus.
|
||||
focusedIndex int
|
||||
|
||||
// Scrolling window
|
||||
windowStart int
|
||||
listTopY int32
|
||||
listLeftX int32
|
||||
itemHeight int32
|
||||
trackSpacing int32
|
||||
|
||||
// Scrollbar geometry (computed once in layout, thumb Y updated on scroll)
|
||||
scrollbarX int32
|
||||
scrollbarTrackY int32 // bottom of the scrollbar track
|
||||
scrollbarTrackH int32 // total height of the track
|
||||
scrollbarThumbH int32 // height of the thumb
|
||||
scrollbarThumbY int32 // current bottom Y of the thumb (updated on scroll)
|
||||
|
||||
// Triangle cursor
|
||||
triVAO uint32
|
||||
triVBO uint32
|
||||
@@ -106,10 +72,6 @@ type AudioOptions struct {
|
||||
func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
var err error
|
||||
p.verticalMargin = int32(20)
|
||||
p.focusedIndex = 0
|
||||
p.selectedTrack = -1
|
||||
p.windowStart = 0
|
||||
p.trackSpacing = p.verticalMargin / 2
|
||||
|
||||
// Font
|
||||
fm := fontManager.Get()
|
||||
@@ -146,7 +108,7 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
p.title.Scale(titleScale)
|
||||
|
||||
// Prompt
|
||||
p.prompt, err = NewStringItem("Up/Down to navigate -- Enter to select -- Left/Right to change volume -- Backspace to go back", p.font, colors.White)
|
||||
p.prompt, err = NewStringItem("Enter to toggle audio -- Left/Right to change volume -- Backspace to go back", p.font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating prompt: %w", err)
|
||||
}
|
||||
@@ -167,17 +129,11 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
||||
}
|
||||
|
||||
// "Available Tracks" heading
|
||||
headingScale := float32(40) / float32(baseFontSize)
|
||||
p.tracksHeading, err = NewStringItem("Available Tracks", p.font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating tracks heading: %w", err)
|
||||
}
|
||||
p.tracksHeading.Scale(headingScale)
|
||||
|
||||
// Track items
|
||||
if err = p.buildTrackItems(); err != nil {
|
||||
return fmt.Errorf("building track items: %w", err)
|
||||
// Make sure the audio_files table is populated. This page no longer
|
||||
// presents any track selection UI, but other parts of the app still
|
||||
// read from this table.
|
||||
if err = p.seedAudioFiles(); err != nil {
|
||||
logger.Error("failed to seed audio files: %v", err)
|
||||
}
|
||||
|
||||
// Triangle cursor VAO/VBO — stride is vec2 only, no tex coords
|
||||
@@ -200,84 +156,18 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Track items ───────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) buildTrackItems() error {
|
||||
var audioFileList = []db.AudioFile{}
|
||||
if err := p.Context.DbPtr.Find(&audioFileList).Error; err != nil {
|
||||
// seedAudioFiles ensures the audio_files table has its default rows.
|
||||
func (p *AudioOptions) seedAudioFiles() error {
|
||||
var existing []db.AudioFile
|
||||
if err := p.Context.DbPtr.Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(audioFileList) == 0 {
|
||||
audioFileList = AudioFileList
|
||||
p.Context.DbPtr.Save(&audioFileList)
|
||||
}
|
||||
|
||||
for i := range p.trackItems {
|
||||
if p.trackItems[i].ID != 0 {
|
||||
gl.DeleteTextures(1, &p.trackItems[i].ID)
|
||||
}
|
||||
}
|
||||
p.trackItems = make([]StringItem, len(AudioFileList))
|
||||
|
||||
for i, entry := range AudioFileList {
|
||||
color := colors.White
|
||||
if i == p.selectedTrack {
|
||||
color = colors.Green
|
||||
}
|
||||
item, err := NewStringItem(entry.Name, p.font, color)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating track item %d: %w", i, err)
|
||||
}
|
||||
item.Scale(p.ItemScale)
|
||||
p.trackItems[i] = item
|
||||
if len(existing) == 0 {
|
||||
return p.Context.DbPtr.Save(&AudioFileList).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// positionTrackItems places the visible window of items and parks the rest off-screen.
|
||||
func (p *AudioOptions) positionTrackItems() {
|
||||
peekCount := visibleCount + 1
|
||||
|
||||
for i := range p.trackItems {
|
||||
slot := i - p.windowStart
|
||||
|
||||
if slot < 0 || slot >= peekCount {
|
||||
p.trackItems[i].X = p.listLeftX
|
||||
p.trackItems[i].Y = -9999
|
||||
continue
|
||||
}
|
||||
|
||||
itemY := p.listTopY - int32(slot)*(p.itemHeight+p.trackSpacing)
|
||||
p.trackItems[i].X = p.listLeftX
|
||||
p.trackItems[i].Y = itemY
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scrollbar ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// updateScrollbarThumb recalculates the thumb Y position based on windowStart.
|
||||
// Called whenever windowStart changes.
|
||||
func (p *AudioOptions) updateScrollbarThumb() {
|
||||
total := len(AudioFileList)
|
||||
if total <= visibleCount {
|
||||
// Entire list fits — thumb fills the track, nothing to scroll
|
||||
p.scrollbarThumbY = p.scrollbarTrackY
|
||||
return
|
||||
}
|
||||
|
||||
// How far through the scrollable range are we?
|
||||
// maxWindowStart is the furthest windowStart can go
|
||||
maxWindowStart := total - visibleCount
|
||||
fraction := float32(p.windowStart) / float32(maxWindowStart)
|
||||
|
||||
// Thumb travels from the top of the track down to (trackTop - thumbH)
|
||||
// "top" in our Y-up projection means the larger Y value
|
||||
trackTop := p.scrollbarTrackY + p.scrollbarTrackH
|
||||
travelRange := p.scrollbarTrackH - p.scrollbarThumbH
|
||||
p.scrollbarThumbY = trackTop - p.scrollbarThumbH - int32(fraction*float32(travelRange))
|
||||
}
|
||||
|
||||
// ── Audio status toggle ───────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) CreateAudioStatusItem() error {
|
||||
@@ -345,7 +235,7 @@ func (p *AudioOptions) layout() {
|
||||
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
|
||||
p.prompt.Y = p.title.Y - p.verticalMargin - p.prompt.H
|
||||
|
||||
// Volume row (not focusable)
|
||||
// Volume row
|
||||
barH := int32(float32(p.Context.CfgPtr.StandardFontSize) * p.ItemScale)
|
||||
barW := int32(float32(p.ScreenWidth) * 0.30)
|
||||
barX := p.ScreenCenterX - barW/2
|
||||
@@ -363,100 +253,14 @@ func (p *AudioOptions) layout() {
|
||||
p.volumePercent.X = barX + barW + gap
|
||||
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
|
||||
|
||||
// "Available Tracks" heading — centred, left edge becomes the list anchor
|
||||
p.tracksHeading.X = p.ScreenCenterX - p.tracksHeading.W/2
|
||||
// Audio toggle — centred below the volume row
|
||||
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
||||
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
|
||||
|
||||
// Toggle — left-aligned to the heading
|
||||
p.AudioStatusToggle.X = p.tracksHeading.X
|
||||
p.AudioStatusToggle.Y = barY - p.verticalMargin*2 - p.AudioStatusToggle.H
|
||||
|
||||
// Separator
|
||||
p.separatorY = p.AudioStatusToggle.Y - p.verticalMargin*3
|
||||
|
||||
// Heading Y — below separator
|
||||
p.tracksHeading.Y = p.separatorY - p.verticalMargin - p.tracksHeading.H
|
||||
|
||||
// Track list geometry — shifted down by listShiftDown
|
||||
if len(p.trackItems) > 0 {
|
||||
p.itemHeight = p.trackItems[0].H
|
||||
}
|
||||
|
||||
p.listLeftX = p.tracksHeading.X
|
||||
p.listTopY = p.tracksHeading.Y - p.verticalMargin - listShiftDown
|
||||
|
||||
// ── Scrollbar geometry ────────────────────────────────────────────────────
|
||||
// The scrollbar track spans the full visible window height.
|
||||
// It sits to the right of the list with a small gap.
|
||||
windowH := int32(visibleCount)*(p.itemHeight+p.trackSpacing) - p.trackSpacing
|
||||
scrollbarBottom := p.listTopY - windowH
|
||||
p.scrollbarTrackH = windowH
|
||||
p.scrollbarTrackY = scrollbarBottom // bottom Y of the track (Y-up coords)
|
||||
|
||||
// Right edge of the list: use the widest track name as an approximation.
|
||||
// Since items are left-aligned and widths vary, we use a fixed width based
|
||||
// on the heading width which is a reasonable proxy for the list column width.
|
||||
listRightX := p.listLeftX + p.tracksHeading.W
|
||||
p.scrollbarX = listRightX + scrollbarGap
|
||||
|
||||
// Thumb height is proportional: visibleCount / total items
|
||||
total := len(AudioFileList)
|
||||
if total > 0 {
|
||||
thumbFraction := float32(visibleCount) / float32(total)
|
||||
p.scrollbarThumbH = int32(thumbFraction * float32(p.scrollbarTrackH))
|
||||
if p.scrollbarThumbH < 8 {
|
||||
p.scrollbarThumbH = 8 // minimum thumb size so it's always clickable
|
||||
}
|
||||
}
|
||||
|
||||
// Triangle cursor X — just outside the left edge of the list
|
||||
// Triangle cursor — just outside the left edge of the toggle, vertically centred on it
|
||||
cursorGap := int32(12)
|
||||
p.triX = float32(p.listLeftX) - float32(triangleSize) - float32(cursorGap)
|
||||
|
||||
p.positionTrackItems()
|
||||
p.updateScrollbarThumb()
|
||||
p.repositionCursor()
|
||||
}
|
||||
|
||||
// repositionCursor vertically centres the triangle on the focused item.
|
||||
func (p *AudioOptions) repositionCursor() {
|
||||
var targetY, targetH int32
|
||||
|
||||
if p.focusedIndex == 0 {
|
||||
targetY = p.AudioStatusToggle.Y
|
||||
targetH = p.AudioStatusToggle.H
|
||||
} else {
|
||||
trackIdx := p.focusedIndex - 1
|
||||
slot := trackIdx - p.windowStart
|
||||
targetY = p.listTopY - int32(slot)*(p.itemHeight+p.trackSpacing)
|
||||
targetH = p.itemHeight
|
||||
}
|
||||
|
||||
p.triCenterY = float32(targetY) + float32(targetH)/2
|
||||
}
|
||||
|
||||
// ── Scroll ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) scrollToFocus() {
|
||||
if p.focusedIndex == 0 {
|
||||
p.windowStart = 0
|
||||
p.positionTrackItems()
|
||||
p.updateScrollbarThumb()
|
||||
p.repositionCursor()
|
||||
return
|
||||
}
|
||||
|
||||
trackIdx := p.focusedIndex - 1
|
||||
|
||||
if trackIdx >= p.windowStart+visibleCount {
|
||||
p.windowStart = trackIdx - visibleCount + 1
|
||||
}
|
||||
if trackIdx < p.windowStart {
|
||||
p.windowStart = trackIdx
|
||||
}
|
||||
|
||||
p.positionTrackItems()
|
||||
p.updateScrollbarThumb()
|
||||
p.repositionCursor()
|
||||
p.triX = float32(p.AudioStatusToggle.X) - float32(triangleSize) - float32(cursorGap)
|
||||
p.triCenterY = float32(p.AudioStatusToggle.Y) + float32(p.AudioStatusToggle.H)/2
|
||||
}
|
||||
|
||||
// ── Toggle ────────────────────────────────────────────────────────────────────
|
||||
@@ -469,46 +273,6 @@ func (p *AudioOptions) ToggleAudio() {
|
||||
p.layout()
|
||||
}
|
||||
|
||||
// ── Track selection ───────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) onTrackSelected(index int, entry db.AudioFile) {
|
||||
p.selectedTrack = index
|
||||
if err := p.buildTrackItems(); err != nil {
|
||||
logger.Error("onTrackSelected: failed to rebuild track items: %v", err)
|
||||
return
|
||||
}
|
||||
p.layout()
|
||||
|
||||
// Create file names and paths
|
||||
// Set newly selected entry as Selected
|
||||
entry.Selected = true
|
||||
// Set path
|
||||
if !strings.Contains(entry.Path, ".local/") {
|
||||
entry.Path = filepath.Join(p.Context.CfgPtr.AssetsDir, "audio/", entry.Path)
|
||||
}
|
||||
// Clear previously selected.
|
||||
var previouslySelectedAudio db.AudioFile
|
||||
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&previouslySelectedAudio).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.Info("No audio previously selected")
|
||||
} else {
|
||||
logger.Error("Unable to fetch previously selected audio from database %s", err)
|
||||
}
|
||||
} else {
|
||||
previouslySelectedAudio.Selected = false
|
||||
if err = p.Context.DbPtr.Save(&previouslySelectedAudio).Error; err != nil {
|
||||
logger.Error("Unable to update selected audio: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Save it to the database
|
||||
if err := p.Context.DbPtr.Save(&entry).Error; err != nil {
|
||||
logger.Error("Error saving to database: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Updated audio selection: %s", entry.Name)
|
||||
}
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
||||
@@ -518,25 +282,8 @@ func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
||||
}
|
||||
|
||||
switch e.Keysym.Sym {
|
||||
case sdl.K_UP:
|
||||
if p.focusedIndex > 0 {
|
||||
p.focusedIndex--
|
||||
p.scrollToFocus()
|
||||
}
|
||||
|
||||
case sdl.K_DOWN:
|
||||
if p.focusedIndex < len(AudioFileList) {
|
||||
p.focusedIndex++
|
||||
p.scrollToFocus()
|
||||
}
|
||||
|
||||
case sdl.K_RETURN:
|
||||
if p.focusedIndex == 0 {
|
||||
p.ToggleAudio()
|
||||
} else {
|
||||
trackIdx := p.focusedIndex - 1
|
||||
p.onTrackSelected(trackIdx, AudioFileList[trackIdx])
|
||||
}
|
||||
p.ToggleAudio()
|
||||
|
||||
case sdl.K_RIGHT:
|
||||
p.UpdateVolume(10)
|
||||
@@ -563,17 +310,6 @@ func (p *AudioOptions) Update(dt float32) error {
|
||||
|
||||
// ── Render helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) renderSeparator() error {
|
||||
lineW := float32(p.ScreenWidth) * 0.80
|
||||
lineX := float32(p.ScreenCenterX) - lineW/2
|
||||
return p.RenderSolidColorQuad(
|
||||
p.solidShader,
|
||||
mgl32.Vec2{lineX, float32(p.separatorY)},
|
||||
mgl32.Vec2{lineW, 2},
|
||||
colors.White,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *AudioOptions) renderVolumeBar() error {
|
||||
vol := p.Context.CfgPtr.SavedVolume
|
||||
|
||||
@@ -609,37 +345,6 @@ func (p *AudioOptions) renderVolumeBar() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderScrollbar draws the scrollbar track (dim) and thumb (white).
|
||||
// Only rendered when the list is longer than the visible window.
|
||||
func (p *AudioOptions) renderScrollbar() error {
|
||||
if len(AudioFileList) <= visibleCount {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Track — a thin dim line the full height of the window
|
||||
trackColor := sdl.Color{R: 80, G: 80, B: 80, A: 255}
|
||||
if err := p.RenderSolidColorQuad(
|
||||
p.solidShader,
|
||||
mgl32.Vec2{float32(p.scrollbarX), float32(p.scrollbarTrackY)},
|
||||
mgl32.Vec2{float32(scrollbarWidth), float32(p.scrollbarTrackH)},
|
||||
trackColor,
|
||||
); err != nil {
|
||||
return fmt.Errorf("rendering scrollbar track: %w", err)
|
||||
}
|
||||
|
||||
// Thumb — white, positioned proportionally within the track
|
||||
if err := p.RenderSolidColorQuad(
|
||||
p.solidShader,
|
||||
mgl32.Vec2{float32(p.scrollbarX), float32(p.scrollbarThumbY)},
|
||||
mgl32.Vec2{float32(scrollbarWidth), float32(p.scrollbarThumbH)},
|
||||
colors.White,
|
||||
); err != nil {
|
||||
return fmt.Errorf("rendering scrollbar thumb: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderCursor draws the spinning triangle cursor.
|
||||
func (p *AudioOptions) renderCursor() error {
|
||||
scaleY := float32(math.Cos(2 * math.Pi * float64(p.spinElapsed)))
|
||||
@@ -693,32 +398,6 @@ func (p *AudioOptions) Render() error {
|
||||
return fmt.Errorf("render audio toggle: %w", err)
|
||||
}
|
||||
|
||||
// Separator
|
||||
if err := p.renderSeparator(); err != nil {
|
||||
return fmt.Errorf("render separator: %w", err)
|
||||
}
|
||||
|
||||
// "Available Tracks" heading — fixed, never moves
|
||||
if err := p.RenderTexture(p.textShader, p.tracksHeading.ID, p.tracksHeading.Position(), p.tracksHeading.RawDimensions(), p.tracksHeading.Color); err != nil {
|
||||
return fmt.Errorf("render tracks heading: %w", err)
|
||||
}
|
||||
|
||||
// Track list
|
||||
for i := range p.trackItems {
|
||||
item := &p.trackItems[i]
|
||||
if item.Y < 0 {
|
||||
continue
|
||||
}
|
||||
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color); err != nil {
|
||||
return fmt.Errorf("render track item %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollbar
|
||||
if err := p.renderScrollbar(); err != nil {
|
||||
return fmt.Errorf("render scrollbar: %w", err)
|
||||
}
|
||||
|
||||
// Spinning triangle cursor — always on top
|
||||
if err := p.renderCursor(); err != nil {
|
||||
return fmt.Errorf("render cursor: %w", err)
|
||||
@@ -735,10 +414,6 @@ func (p *AudioOptions) Destroy() error {
|
||||
gl.DeleteTextures(1, &p.VolumeLabel.ID)
|
||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
|
||||
gl.DeleteTextures(1, &p.tracksHeading.ID)
|
||||
for i := range p.trackItems {
|
||||
gl.DeleteTextures(1, &p.trackItems[i].ID)
|
||||
}
|
||||
gl.DeleteBuffers(1, &p.triVBO)
|
||||
gl.DeleteVertexArrays(1, &p.triVAO)
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user