Downloads are working and the goroutine is reporting updates appropriately.

This commit is contained in:
NeilClack
2026-06-18 21:28:16 -05:00
parent 397d4c9f19
commit 7ecb90b63b
6 changed files with 126 additions and 337 deletions
+38 -23
View File
@@ -6,15 +6,16 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"rhemawavetech.com/rhemawave/internal/logger"
) )
// DownloadStatus holds information related to downloads and is // DownloadStatus holds information related to downloads and is
// the container for information returned via channels within this // the container for information returned via channels within this
// package. // package.
type DownloadStatus struct { type DownloadStatus struct {
Min int64 // Current file's bytes downloaded Max int64 // Total bytes to download
Max int64 // Current file's total size Cur int64 // Total bytes downloaded
Cur int64 // Overall bytes downloaded
Msg string // Status message Msg string // Status message
Err error // Error, if any Err error // Error, if any
} }
@@ -25,11 +26,14 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
defer close(progressChan) defer close(progressChan)
urlBase := "https://files.rhemawavetech.com/audio/" urlBase := "https://files.rhemawavetech.com/audio/"
totalDownloaded := int64(0)
totalSize := int64(0)
// First, get the total size of all files // First, calculate the total size of files that need to be downloaded
totalSize := int64(0)
filesToDownload := []string{}
for _, filename := range filenames { for _, filename := range filenames {
destPath := filepath.Join(destDir, filename)
// Check if the file already exists
url := urlBase + filename url := urlBase + filename
resp, err := http.Head(url) resp, err := http.Head(url)
if err != nil { if err != nil {
@@ -39,7 +43,6 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
} }
return return
} }
resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
progressChan <- DownloadStatus{ progressChan <- DownloadStatus{
Msg: fmt.Sprintf("Failed to get size for %s: status %s", filename, resp.Status), Msg: fmt.Sprintf("Failed to get size for %s: status %s", filename, resp.Status),
@@ -47,19 +50,39 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
} }
return return
} }
totalSize += resp.ContentLength fileStat, err := os.Stat(destPath)
// If the file is incomplete, or does not exist
if os.IsNotExist(err) || fileStat.Size() != resp.ContentLength {
// Oh, and log a warning if the file is not complete
if !os.IsNotExist(err) {
logger.Warning("%s incomplete/corrupt. Adding to download queue.", filename)
} else {
logger.Info("%s not found. Adding to download queue.", filename)
}
// Append it to the list to download
filesToDownload = append(filesToDownload, filename)
// And increase the total bytes to download
totalSize += resp.ContentLength
} else if err != nil {
logger.Error("unknown error: %v", err)
} else {
logger.Error("File %s exists, skipping...", filename)
}
resp.Body.Close()
} }
// Send initial status // Send initial status
progressChan <- DownloadStatus{ progressChan <- DownloadStatus{
Min: 0,
Max: totalSize, Max: totalSize,
Cur: 0, Cur: 0,
Msg: "Starting download...", Msg: "Starting download...",
} }
// Download each file totalDownloaded := int64(0)
for _, filename := range filenames {
// Download each file that doesn't exist
for _, filename := range filesToDownload {
url := urlBase + filename url := urlBase + filename
destPath := filepath.Join(destDir, filename) destPath := filepath.Join(destDir, filename)
@@ -96,10 +119,6 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
return return
} }
// Track progress for this file
fileSize := resp.ContentLength
downloaded := int64(0)
// Use a buffered reader for efficiency // Use a buffered reader for efficiency
buf := make([]byte, 8192) buf := make([]byte, 8192)
for { for {
@@ -129,13 +148,11 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
} }
// Update progress // Update progress
downloaded += int64(n)
totalDownloaded += int64(n) totalDownloaded += int64(n)
progressChan <- DownloadStatus{ progressChan <- DownloadStatus{
Min: downloaded, Max: totalSize,
Max: fileSize,
Cur: totalDownloaded, Cur: totalDownloaded,
Msg: fmt.Sprintf("Downloading %s...", filename), Msg: fmt.Sprintf("%s", filename),
} }
} }
@@ -143,8 +160,7 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
// Send completion for this file // Send completion for this file
progressChan <- DownloadStatus{ progressChan <- DownloadStatus{
Min: fileSize, Max: totalSize,
Max: fileSize,
Cur: totalDownloaded, Cur: totalDownloaded,
Msg: fmt.Sprintf("Finished downloading %s", filename), Msg: fmt.Sprintf("Finished downloading %s", filename),
} }
@@ -152,9 +168,8 @@ func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string,
// Send final status // Send final status
progressChan <- DownloadStatus{ progressChan <- DownloadStatus{
Min: totalSize,
Max: totalSize, Max: totalSize,
Cur: totalDownloaded, Cur: totalDownloaded,
Msg: "All downloads completed!", Msg: "All audio downloads completed!",
} }
} }
+29
View File
@@ -0,0 +1,29 @@
package page
import (
"github.com/veandco/go-sdl2/sdl"
)
type MessagePage struct {
Msg string
}
func (p *MessagePage) Init(ctx *PageCtx) error {
return nil
}
func (p *MessagePage) HandleEvent(event *sdl.Event) error {
return nil
}
func (p *MessagePage) Update(dt float32) error {
return nil
}
func (p *MessagePage) Render() error {
return nil
}
func (p *MessagePage) Destroy() error {
return nil
}
+56 -12
View File
@@ -2,12 +2,14 @@ package page
import ( import (
"fmt" "fmt"
"path/filepath"
"time" "time"
"github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32" "github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/download"
"rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager" "rhemawavetech.com/rhemawave/internal/shaderManager"
) )
@@ -16,16 +18,20 @@ var GitVersion = "1.0"
type Home struct { type Home struct {
Base Base
title StringItem fm *fontManager.FontManager
version StringItem font *fontManager.FontEntry
prompt StringItem title StringItem
note StringItem version StringItem
licenseInfoHeading StringItem prompt StringItem
emailDisplay StringItem note StringItem
licenseKeyDisplay StringItem message StringItem
hasLicenseInfo bool downloadProgressChan chan download.DownloadStatus
lastDisplayUpdate time.Time licenseInfoHeading StringItem
baseFontSize int // cached from db at init emailDisplay StringItem
licenseKeyDisplay StringItem
hasLicenseInfo bool
lastDisplayUpdate time.Time
baseFontSize int // cached from db at init
} }
// Init performs basic page initialization and creation of static assets. // Init performs basic page initialization and creation of static assets.
@@ -40,11 +46,12 @@ func (p *Home) Init(ctx *PageCtx) error {
} }
// Get font // Get font
fm := fontManager.Get() p.fm = fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular") font, ok := p.fm.GetFont("Roboto-Regular")
if !ok { if !ok {
return fmt.Errorf("unable to fetch font") return fmt.Errorf("unable to fetch font")
} }
p.font = font
// Create title texture // Create title texture
var err error var err error
@@ -72,6 +79,9 @@ func (p *Home) Init(ctx *PageCtx) error {
return fmt.Errorf("unable to create note string texture: %w", err) return fmt.Errorf("unable to create note string texture: %w", err)
} }
// Create download channel and message
p.downloadProgressChan = make(chan download.DownloadStatus)
// Create license info textures if data exists // Create license info textures if data exists
if p.Context.CfgPtr.EmailAddress != "" || p.Context.CfgPtr.LicenseKey != "" { if p.Context.CfgPtr.EmailAddress != "" || p.Context.CfgPtr.LicenseKey != "" {
p.hasLicenseInfo = true p.hasLicenseInfo = true
@@ -123,6 +133,25 @@ func (p *Home) Init(ctx *PageCtx) error {
} }
} }
audioFileNames := []string{
"OriginalAudio.wav",
"174Hz.wav",
"396Hz.wav",
"417Hz.wav",
"432Hz.wav",
"639Hz.wav",
"741Hz.wav",
"852Hz.wav",
"963Hz.wav",
"1111Hz.wav",
"14Hz_Binaural.wav",
"40Hz_Binaural.wav",
"AlphaWaves.wav",
"PinealGland.wav",
}
go download.DownloadAudioFiles(p.downloadProgressChan, audioFileNames, filepath.Join(ctx.CfgPtr.AssetsDir, "audio"))
return nil return nil
} }
@@ -133,6 +162,21 @@ func (p *Home) HandleEvent(event *sdl.Event) error {
// Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32. // Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32.
func (p *Home) Update(dt float32) error { func (p *Home) Update(dt float32) error {
var err error
select {
case progressUpdate := <-p.downloadProgressChan:
if progressUpdate.Err != nil {
p.message, err = NewStringItem(progressUpdate.Msg, p.font, colors.White)
if err != nil {
return err
}
} else {
p.message, err = NewStringItem(progressUpdate.Err.Error(), p.font, colors.Red)
}
default:
return nil
}
return nil return nil
} }
-111
View File
@@ -1,111 +0,0 @@
package page
import (
"fmt"
"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/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type MessagePage struct {
Base
message StringItem
instructions StringItem
msgDim mgl32.Vec2
instDim mgl32.Vec2
textShader *shaderManager.Shader
msgPos mgl32.Vec2
instPos mgl32.Vec2
}
func (p *MessagePage) Init(ctx *PageCtx) error {
var err error
var ok bool
if ctx.Msg == "" {
ctx.Msg = "This is an example message because no message was passed"
}
if ctx.Inst == "" {
ctx.Inst = "Press F1 to exit"
}
if err = p.Base.Init(ctx); err != nil {
return fmt.Errorf("failed to init Base page: %w", err)
}
// Fetch and cache shader
sm := shaderManager.Get()
p.textShader, ok = sm.Get("text")
if !ok {
return fmt.Errorf("failed to fetch text shader")
}
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("unable to fetch font")
}
p.message, err = NewStringItem(ctx.Msg, font, colors.Orange)
if err != nil {
return fmt.Errorf("unable to create message string texture: %w", err)
}
p.instructions, err = NewStringItem(ctx.Inst, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create instructions string texture: %w", err)
}
baseFontSize := float32(p.Context.CfgPtr.StandardFontSize)
p.message.Scale(float32(48) / baseFontSize)
p.instructions.Scale(float32(28) / baseFontSize)
// Cache scaled dimensions — never recomputed unless textures are replaced
p.msgDim = p.message.RawDimensions()
p.instDim = p.instructions.RawDimensions()
p.msgPos = mgl32.Vec2{
float32(p.Base.ScreenCenterX) - p.msgDim[0]/2,
float32(p.Base.ScreenCenterY) - p.msgDim[1]/2,
}
p.instPos = mgl32.Vec2{
float32(p.Base.ScreenCenterX) - p.instDim[0]/2,
float32(20),
}
return nil
}
func (p *MessagePage) HandleEvent(event *sdl.Event) error {
return nil
}
func (p *MessagePage) Update(dt float32) error {
return nil
}
func (p *MessagePage) Render() error {
p.Base.RenderTexture(p.textShader, p.message.ID, p.msgPos, p.msgDim, colors.Orange)
p.Base.RenderTexture(p.textShader, p.instructions.ID, p.instPos, p.instDim, colors.White)
return nil
}
func (p *MessagePage) Destroy() error {
gl.DeleteTextures(1, &p.message.ID)
gl.DeleteTextures(1, &p.instructions.ID)
return p.Base.Destroy()
}
func (p *MessagePage) SetMessage(msg string) error {
return nil
}
func (p *MessagePage) SetInstructions(inst string) error {
return nil
}
+3 -3
View File
@@ -17,8 +17,8 @@ const (
LICENSE_KEY LICENSE_KEY
EMAIL_ADDRESS EMAIL_ADDRESS
LOGS LOGS
CHECK_UPDATES
MESSAGE MESSAGE
DOWNLOADING_ASSETS
) )
type PageCtx struct { type PageCtx struct {
@@ -69,10 +69,10 @@ func (r *Router) NavigateTo(p pageType) {
r.ActivePage = &EmailAddressPage{} r.ActivePage = &EmailAddressPage{}
case LOGS: case LOGS:
r.ActivePage = &LogViewerPage{} r.ActivePage = &LogViewerPage{}
case CHECK_UPDATES:
r.ActivePage = &UpdateCheckPage{}
case MESSAGE: case MESSAGE:
r.ActivePage = &MessagePage{} r.ActivePage = &MessagePage{}
case DOWNLOADING_ASSETS:
r.ActivePage = &DownloadingAssets{}
} }
r.ActivePage.Init(&r.Context) r.ActivePage.Init(&r.Context)
-188
View File
@@ -1,188 +0,0 @@
package page
import (
"fmt"
"os/exec"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type updateCheckState int
const (
updateCheckStateChecking updateCheckState = iota
updateCheckStateDone
)
type UpdateCheckPage struct {
Base
textShader *shaderManager.Shader
state updateCheckState
// "Checking for Updates." / ".." / "..."
checkingTextures [3]StringItem
// "No updates available" and "Press F3 to return"
doneMessage StringItem
doneHint StringItem
// Error message if service failed to start
errorMessage StringItem
hasError bool
pollTimer float32
dotIndex int
dotTimer float32
}
func (p *UpdateCheckPage) Init(ctx *PageCtx) error {
var err error
if err := p.Base.Init(ctx); 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")
}
// Get scale
baseFontSize := ctx.CfgPtr.StandardFontSize
scale := float32(48) / float32(baseFontSize)
// Create checking textures with animated dots
dots := []string{"Checking for Updates.", "Checking for Updates..", "Checking for Updates..."}
for i, text := range dots {
item, err := NewStringItem(text, font, colors.White)
if err != nil {
return fmt.Errorf("creating checking texture %d: %w", i, err)
}
item.Scale(scale)
p.checkingTextures[i] = item
}
// Create done textures
hintScale := float32(32) / float32(baseFontSize)
p.doneMessage, err = NewStringItem("No updates available", font, colors.White)
if err != nil {
return fmt.Errorf("creating done message: %w", err)
}
p.doneMessage.Scale(scale)
p.doneHint, err = NewStringItem("Press F3 to return", font, colors.White)
if err != nil {
return fmt.Errorf("creating done hint: %w", err)
}
p.doneHint.Scale(hintScale)
// Start the update service
p.state = updateCheckStateChecking
if err := exec.Command("systemctl", "start", "nixos-upgrade.service").Run(); err != nil {
p.errorMessage, _ = NewStringItem("Update service unavailable", font, colors.White)
p.errorMessage.Scale(scale)
p.hasError = true
p.state = updateCheckStateDone
}
return nil
}
func (p *UpdateCheckPage) Update(dt float32) error {
if p.state != updateCheckStateChecking {
return nil
}
// Animate dots
p.dotTimer += dt
if p.dotTimer >= 0.5 {
p.dotTimer -= 0.5
p.dotIndex = (p.dotIndex + 1) % 3
}
// Poll service status every 2 seconds
p.pollTimer += dt
if p.pollTimer >= 2.0 {
p.pollTimer -= 2.0
output, _ := exec.Command("systemctl", "is-active", "rhemawave-update.service").Output()
status := strings.TrimSpace(string(output))
if status != "active" && status != "activating" {
p.state = updateCheckStateDone
}
}
return nil
}
func (p *UpdateCheckPage) HandleEvent(event *sdl.Event) error {
if p.state != updateCheckStateDone {
return nil
}
switch e := (*event).(type) {
case *sdl.KeyboardEvent:
if e.Type != sdl.KEYDOWN {
return nil
}
switch e.Keysym.Sym {
case sdl.K_F3, sdl.K_BACKSPACE:
p.Context.NavigateTo(SETTINGS)
}
}
return nil
}
func (p *UpdateCheckPage) Render() error {
switch p.state {
case updateCheckStateChecking:
item := p.checkingTextures[p.dotIndex]
item.X = p.ScreenCenterX - item.W/2
item.Y = p.ScreenCenterY - item.H/2
p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color)
case updateCheckStateDone:
var msg *StringItem
if p.hasError {
msg = &p.errorMessage
} else {
msg = &p.doneMessage
}
msg.X = p.ScreenCenterX - msg.W/2
msg.Y = p.ScreenCenterY + msg.H/2
p.RenderTexture(p.textShader, msg.ID, msg.Position(), msg.RawDimensions(), msg.Color)
p.doneHint.X = p.ScreenCenterX - p.doneHint.W/2
p.doneHint.Y = msg.Y - p.doneHint.H - 20
p.RenderTexture(p.textShader, p.doneHint.ID, p.doneHint.Position(), p.doneHint.RawDimensions(), p.doneHint.Color)
}
return nil
}
func (p *UpdateCheckPage) Destroy() error {
for i := range p.checkingTextures {
gl.DeleteTextures(1, &p.checkingTextures[i].ID)
}
gl.DeleteTextures(1, &p.doneMessage.ID)
gl.DeleteTextures(1, &p.doneHint.ID)
if p.hasError {
gl.DeleteTextures(1, &p.errorMessage.ID)
}
return nil
}