package page import ( "fmt" "strconv" "sync/atomic" "github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/mathgl/mgl32" "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/keybinds" "rhemawavetech.com/rhemawave/internal/shaderManager" ) type downloadState int const ( downloadStateDownloading downloadState = iota downloadStateFinished ) // DownloadProgressPage displays a progress bar while an audio file is downloading. // The page is inescapable: no input is accepted while downloading, and only // Enter is accepted once the download finishes. type DownloadProgressPage struct { Base textShader *shaderManager.Shader solidShader *shaderManager.Shader // Set by the application before pushing this page. TotalBytes *atomic.Int64 BytesWritten *atomic.Int64 Done *atomic.Bool HasError *atomic.Bool NextPage Page // where to navigate on success state downloadState title StringItem prompt StringItem percentText StringItem lastPercent int barX, barY, barW, barH int32 } func (p *DownloadProgressPage) Init(app ApplicationInterface) error { if err := p.Base.Init(app); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } fm := fontManager.Get() sm := shaderManager.Get() font, ok := fm.GetFont("Roboto-Regular") if !ok { return fmt.Errorf("failed to get font") } p.textShader, ok = sm.Get("text") if !ok { return fmt.Errorf("failed to get text shader") } p.solidShader, ok = sm.Get("solid_color") if !ok { return fmt.Errorf("failed to get solid_color shader") } 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: %w", err) } titleScale := float32(48) / float32(baseFontSize) p.title, err = NewStringItem("Downloading Audio...", font, colors.White) if err != nil { return fmt.Errorf("creating title: %w", err) } p.title.Scale(titleScale) promptScale := float32(32) / float32(baseFontSize) p.prompt, err = NewStringItem("Press Enter to continue", font, colors.White) if err != nil { return fmt.Errorf("creating prompt: %w", err) } p.prompt.Scale(promptScale) p.percentText, err = NewStringItem("0%", font, colors.White) if err != nil { return fmt.Errorf("creating percent text: %w", err) } p.percentText.Scale(promptScale) p.lastPercent = 0 // Progress bar dimensions: 60% of screen width, 30px tall, centered p.barW = int32(float32(p.ScreenWidth) * 0.6) p.barH = 30 p.barX = p.ScreenCenterX - p.barW/2 p.barY = p.ScreenCenterY - p.barH/2 // Position title above bar p.title.X = p.ScreenCenterX - p.title.W/2 p.title.Y = p.barY + p.barH + 40 p.state = downloadStateDownloading keybinds.Enabled = false return nil } func (p *DownloadProgressPage) Update(dt float32) error { if p.state == downloadStateFinished { return nil } if p.Done.Load() { p.state = downloadStateFinished // Force progress to 100% on success if !p.HasError.Load() && p.TotalBytes.Load() > 0 { p.BytesWritten.Store(p.TotalBytes.Load()) p.lastPercent = 100 fm := fontManager.Get() font, ok := fm.GetFont("Roboto-Regular") if ok { gl.DeleteTextures(1, &p.percentText.ID) baseFontSizeStr, _ := db.GetConfigValue("init_font_size") baseFontSize, _ := strconv.Atoi(baseFontSizeStr) percentScale := float32(32) / float32(baseFontSize) newText, err := NewStringItem("100%", font, colors.White) if err == nil { newText.Scale(percentScale) p.percentText = newText } } } // Update title to reflect result fm := fontManager.Get() font, ok := fm.GetFont("Roboto-Regular") if !ok { return nil } baseFontSizeStr, _ := db.GetConfigValue("init_font_size") baseFontSize, _ := strconv.Atoi(baseFontSizeStr) titleScale := float32(48) / float32(baseFontSize) gl.DeleteTextures(1, &p.title.ID) var titleText string if p.HasError.Load() { titleText = "Download Failed" } else { titleText = "Download Complete" } newTitle, err := NewStringItem(titleText, font, colors.White) if err == nil { newTitle.Scale(titleScale) p.title = newTitle p.title.X = p.ScreenCenterX - p.title.W/2 p.title.Y = p.barY + p.barH + 40 } return nil } // Update percentage texture when it changes percent := 0 if p.TotalBytes.Load() > 0 { percent = int(p.BytesWritten.Load() * 100 / p.TotalBytes.Load()) if percent > 100 { percent = 100 } } if percent != p.lastPercent { p.lastPercent = percent fm := fontManager.Get() font, ok := fm.GetFont("Roboto-Regular") if !ok { return nil } gl.DeleteTextures(1, &p.percentText.ID) baseFontSizeStr, _ := db.GetConfigValue("init_font_size") baseFontSize, _ := strconv.Atoi(baseFontSizeStr) percentScale := float32(32) / float32(baseFontSize) newText, err := NewStringItem(fmt.Sprintf("%d%%", percent), font, colors.White) if err == nil { newText.Scale(percentScale) p.percentText = newText } } return nil } func (p *DownloadProgressPage) HandleEvent(event *sdl.Event) error { // Inescapable while downloading if p.state != downloadStateFinished { return nil } switch e := (*event).(type) { case *sdl.KeyboardEvent: if e.Type != sdl.KEYDOWN { return nil } if e.Keysym.Sym == sdl.K_RETURN { if p.HasError.Load() || p.NextPage == nil { p.App.SchedulePop() } else { p.App.SwitchPage(p.NextPage) } } } return nil } func (p *DownloadProgressPage) Render() error { // Render title p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color) // Progress bar background (dark gray) bgColor := sdl.Color{R: 60, G: 60, B: 60, A: 255} barPos := mgl32.Vec2{float32(p.barX), float32(p.barY)} barDim := mgl32.Vec2{float32(p.barW), float32(p.barH)} p.RenderSolidColorQuad(p.solidShader, barPos, barDim, bgColor) // Progress bar foreground progress := float32(0) if p.TotalBytes.Load() > 0 { progress = float32(p.BytesWritten.Load()) / float32(p.TotalBytes.Load()) if progress > 1.0 { progress = 1.0 } } if progress > 0 { fgColor := sdl.Color{R: 255, G: 255, B: 255, A: 255} fgDim := mgl32.Vec2{float32(p.barW) * progress, float32(p.barH)} p.RenderSolidColorQuad(p.solidShader, barPos, fgDim, fgColor) } // Render percentage text below bar p.percentText.X = p.ScreenCenterX - p.percentText.W/2 p.percentText.Y = p.barY - p.percentText.H - 20 p.RenderTexture(p.textShader, p.percentText.ID, p.percentText.Position(), p.percentText.RawDimensions(), p.percentText.Color) // Show prompt when finished if p.state == downloadStateFinished { p.prompt.X = p.ScreenCenterX - p.prompt.W/2 p.prompt.Y = p.barY - p.percentText.H - 20 - p.prompt.H - 20 p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color) } return nil } func (p *DownloadProgressPage) Destroy() error { keybinds.Enabled = true gl.DeleteTextures(1, &p.title.ID) gl.DeleteTextures(1, &p.prompt.ID) gl.DeleteTextures(1, &p.percentText.ID) return nil }