added installation progress tab
All checks were successful
/ test-on-windows (push) Successful in 14s
/ test-on-alpine (push) Successful in 4s

This commit is contained in:
partisan 2024-12-25 19:06:39 +01:00
parent 9e5457c2ec
commit ad826b3b43
3 changed files with 254 additions and 76 deletions

73
circle_animator.go Normal file
View file

@ -0,0 +1,73 @@
package main
import (
rl "github.com/gen2brain/raylib-go/raylib"
)
// CircleAnimator manages both the position/radius alpha (0..1)
// and the displayed progress which eases toward the actual SPM progress.
type CircleAnimator struct {
PosAlpha float32 // 0 => top-right, 1 => center
DisplayProgress float32 // The "smooth" progress shown on screen
targetStep int // The step controlling circle's final position (0..2 => top-right, 3 => center)
// For smoothing the circle position alpha with "easeInOutCubic"
accumSec float32
duration float32
startAlpha float32
endAlpha float32
// For smoothing the displayed progress
// We'll do a simple exponential approach each frame.
// Alternatively, you could do a time-based tween.
smoothingFactor float32 // e.g. 0.1 => about 10% approach per frame
}
// NewCircleAnimator sets initial defaults
func NewCircleAnimator() *CircleAnimator {
return &CircleAnimator{
PosAlpha: 0.0, // start at top-right
DisplayProgress: 0.0,
duration: 1.0, // 1 second total for position tween
smoothingFactor: 0.07, // how fast progress approaches real
}
}
// SetStep sets whether we want circle in top-right (step<3 => alpha=0) or center (step=3 => alpha=1).
// This starts a new positional tween from current alpha to the new alpha.
func (c *CircleAnimator) SetStep(step int) {
c.targetStep = step
var newEnd float32 = 0.0
if step == 3 {
newEnd = 1.0
}
// Start new tween from current alpha to newEnd
c.startAlpha = c.PosAlpha
c.endAlpha = newEnd
c.accumSec = 0
}
// Update updates both the position alpha (with easeInOutCubic) and the displayed progress.
func (c *CircleAnimator) Update(realProgress int) {
dt := rl.GetFrameTime() // seconds
// 1) Update position alpha
if c.accumSec < c.duration {
c.accumSec += dt
if c.accumSec > c.duration {
c.accumSec = c.duration
}
p := c.accumSec / c.duration
eased := easeInOutCubic(p)
// Lerp from startAlpha to endAlpha with eased fraction
c.PosAlpha = c.startAlpha + (c.endAlpha-c.startAlpha)*eased
} else {
// no active tween, just keep the final alpha
c.PosAlpha = c.endAlpha
}
// 2) Smoothly approach the real progress
// (exponential approach: new = old + factor*(target - old))
target := float32(realProgress)
c.DisplayProgress += c.smoothingFactor * (target - c.DisplayProgress)
}

View file

@ -13,11 +13,12 @@ type Installer struct {
Task string Task string
// Internal states // Internal states
IsDownloading bool IsDownloading bool
IsInstalling bool IsInstalling bool
DoneDownload bool DoneDownload bool
DoneInstall bool DoneInstall bool
LastError error LastError error
PendingInstall bool
// Paths // Paths
DownloadDir string DownloadDir string
@ -36,13 +37,18 @@ func (inst *Installer) StartDownloadDecompress() {
defer func() { defer func() {
inst.IsDownloading = false inst.IsDownloading = false
inst.DoneDownload = (inst.LastError == nil) inst.DoneDownload = (inst.LastError == nil)
// If user requested install while we were downloading (PendingInstall),
// automatically do the install now that we're done decompressing.
if inst.PendingInstall && inst.DoneDownload && !inst.IsInstalling && !inst.DoneInstall {
inst.doFinalInstall()
}
}() }()
// Prepare download directory
spm.UpdateProgress(0, "Preparing to download...") spm.UpdateProgress(0, "Preparing to download...")
inst.DownloadDir = spm.GetTempDownloadDir() inst.DownloadDir = spm.GetTempDownloadDir()
// Download APPINDEX // 1) Download APPINDEX
appIndexPath := filepath.Join(inst.DownloadDir, "APPINDEX") appIndexPath := filepath.Join(inst.DownloadDir, "APPINDEX")
spm.UpdateProgress(0, "Downloading APPINDEX") spm.UpdateProgress(0, "Downloading APPINDEX")
if err := spm.DownloadAppIndex(appIndexPath); err != nil { if err := spm.DownloadAppIndex(appIndexPath); err != nil {
@ -50,7 +56,7 @@ func (inst *Installer) StartDownloadDecompress() {
return return
} }
// Download package // 2) Download package
packageName := "spitfire-browser" packageName := "spitfire-browser"
release := "nightly" release := "nightly"
spm.UpdateProgress(0, "Downloading package...") spm.UpdateProgress(0, "Downloading package...")
@ -59,7 +65,7 @@ func (inst *Installer) StartDownloadDecompress() {
return return
} }
// Decompress // 3) Decompress
spm.UpdateProgress(0, "Decompressing...") spm.UpdateProgress(0, "Decompressing...")
packagePath := filepath.Join(inst.DownloadDir, "browser-amd64-nightly-linux.tar.gz") packagePath := filepath.Join(inst.DownloadDir, "browser-amd64-nightly-linux.tar.gz")
tempDir, err := spm.DecompressToTemp(packagePath) tempDir, err := spm.DecompressToTemp(packagePath)
@ -67,19 +73,34 @@ func (inst *Installer) StartDownloadDecompress() {
inst.LastError = err inst.LastError = err
return return
} }
inst.TempDir = tempDir inst.TempDir = tempDir
}() }()
} }
// FinalInstall moves files to the final install directory in a background goroutine. // FinalInstall is called by the UI to request installation.
// If download is done, it runs immediately, otherwise sets PendingInstall=true.
func (inst *Installer) FinalInstall() { func (inst *Installer) FinalInstall() {
if !inst.DoneDownload { // Already installed or installing => ignore repeated calls
inst.LastError = fmt.Errorf("Cannot install: download and decompression are not complete") if inst.IsInstalling || inst.DoneInstall {
return return
} }
// If not done downloading, just mark that we want to install once finished
if !inst.DoneDownload {
fmt.Println("Cannot install now: download and decompression not complete -> pending install.")
inst.PendingInstall = true
return
}
// Otherwise, go ahead and install now
inst.doFinalInstall()
}
// doFinalInstall does the actual file move and sets states
func (inst *Installer) doFinalInstall() {
inst.IsInstalling = true inst.IsInstalling = true
inst.PendingInstall = false // we are fulfilling the install now
go func() { go func() {
defer func() { defer func() {
inst.IsInstalling = false inst.IsInstalling = false
@ -93,6 +114,8 @@ func (inst *Installer) FinalInstall() {
return return
} }
// Move files
spm.UpdateProgress(0, "Installing...")
if err := spm.MoveFilesToInstallDir(inst.TempDir, installDir); err != nil { if err := spm.MoveFilesToInstallDir(inst.TempDir, installDir); err != nil {
inst.LastError = err inst.LastError = err
return return

208
main.go
View file

@ -2,24 +2,32 @@ package main
import ( import (
"fmt" "fmt"
"math"
"os"
rl "github.com/gen2brain/raylib-go/raylib" rl "github.com/gen2brain/raylib-go/raylib"
) )
// Keep your global variables for steps and transitions
var ( var (
transition = NewTransitionManager() transition = NewTransitionManager()
currentStep = 0 currentStep = 0
targetStep = 0 targetStep = 0
useDefault = false useDefault = false
step1DefaultRect rl.Rectangle step1DefaultRect rl.Rectangle
step1CustomRect rl.Rectangle step1CustomRect rl.Rectangle
// Our global Installer from installer.go
installer = NewInstaller() installer = NewInstaller()
// We no longer store circleToCenter or "display progress" here.
// Instead, we use the new CircleAnimator:
circleAnimator = NewCircleAnimator()
// textHoverFade => for the small circles info text in steps 0..2
textHoverFade float32
) )
const finalStep = 3
func main() { func main() {
monitor := rl.GetCurrentMonitor() monitor := rl.GetCurrentMonitor()
screenW := rl.GetMonitorWidth(monitor) screenW := rl.GetMonitorWidth(monitor)
@ -31,16 +39,17 @@ func main() {
refreshRate := rl.GetMonitorRefreshRate(monitor) refreshRate := rl.GetMonitorRefreshRate(monitor)
if refreshRate <= 0 { if refreshRate <= 0 {
refreshRate = 60 // Fallback to 60 if detection fails refreshRate = 60
} }
rl.SetTargetFPS(int32(refreshRate)) rl.SetTargetFPS(int32(refreshRate))
InitBackground(rl.GetScreenWidth(), rl.GetScreenHeight()) InitBackground(rl.GetScreenWidth(), rl.GetScreenHeight())
// Start the download+decompress in background immediately: // Start the download+decompress in background immediately
installer.StartDownloadDecompress() installer.StartDownloadDecompress()
targetStep = 0 // Initially step 0 => circle is top-right => circleAnimator.PosAlpha=0
circleAnimator.SetStep(0)
for !rl.WindowShouldClose() { for !rl.WindowShouldClose() {
screenW = rl.GetScreenWidth() screenW = rl.GetScreenWidth()
@ -54,24 +63,50 @@ func main() {
nextX := int32(screenW - 150) nextX := int32(screenW - 150)
nextY := prevY nextY := prevY
// Update transition // Transition update
oldAlpha, oldScale, oldOffsetX, newAlpha, newScale, newOffsetX := transition.Update() oldAlpha, oldScale, oldOffsetX, newAlpha, newScale, newOffsetX := transition.Update()
if !transition.IsActive() && currentStep != targetStep { if !transition.IsActive() && currentStep != targetStep {
currentStep = targetStep currentStep = targetStep
// Update circleAnimator step so it moves (0 => top-right, 3 => center)
circleAnimator.SetStep(currentStep)
} }
// Poll SPM progress for the GUI // Poll SPM progress
installer.PollProgress() installer.PollProgress()
// If an unrecoverable error occurred, you could handle it here: // If error
// (For now, we just print it in the console.)
if installer.LastError != nil { if installer.LastError != nil {
fmt.Println("SPM Error:", installer.LastError) fmt.Println("SPM Error:", installer.LastError)
// You might choose to show a popup or do something else
} }
// GUI input // Update circle animator every frame
// This will ease the actual SPM progress => "DisplayProgress"
circleAnimator.Update(installer.Progress)
// textHoverFade for step<3
if currentStep < finalStep {
topRightX := float32(screenW - 80)
topRightY := float32(100)
radius := float32(30)
dx := mousePos.X - topRightX
dy := mousePos.Y - topRightY
dist := float32(math.Sqrt(float64(dx*dx + dy*dy)))
if dist < radius+30 {
textHoverFade += 0.1
if textHoverFade > 1 {
textHoverFade = 1
}
} else {
textHoverFade -= 0.1
if textHoverFade < 0 {
textHoverFade = 0
}
}
} else {
textHoverFade = 0
}
// Mouse input
if !transition.IsActive() && rl.IsMouseButtonPressed(rl.MouseLeftButton) { if !transition.IsActive() && rl.IsMouseButtonPressed(rl.MouseLeftButton) {
handleInput(mousePos, screenW, screenH, buttonW, buttonH, prevX, prevY, nextX, nextY) handleInput(mousePos, screenW, screenH, buttonW, buttonH, prevX, prevY, nextX, nextY)
} }
@ -89,7 +124,6 @@ func main() {
oldStep = transition.oldStep oldStep = transition.oldStep
newStep = transition.newStep newStep = transition.newStep
} }
phase := transition.GetPhase() phase := transition.GetPhase()
if transition.IsActive() { if transition.IsActive() {
if phase == TransitionOutFadeScale { if phase == TransitionOutFadeScale {
@ -99,10 +133,11 @@ func main() {
drawStep(newStep, screenW, screenH, mousePos, newAlpha, newScale, newOffsetX) drawStep(newStep, screenW, screenH, mousePos, newAlpha, newScale, newOffsetX)
} }
} else { } else {
drawStep(currentStep, screenW, screenH, mousePos, 1.0, 1.0, 0.0) drawStep(currentStep, screenW, screenH, mousePos, 1, 1, 0)
} }
if !transition.IsActive() { // Nav buttons if not final step
if !transition.IsActive() && currentStep < finalStep {
if currentStep > 0 && currentStep < 3 { if currentStep > 0 && currentStep < 3 {
drawButton("Previous", prevX, prevY, int32(buttonW), int32(buttonH), mousePos) drawButton("Previous", prevX, prevY, int32(buttonW), int32(buttonH), mousePos)
} }
@ -113,33 +148,29 @@ func main() {
} }
} }
// Draw the semi-transparent loading circle if user is downloading or installing // Draw the circle
if installer.IsDownloading || installer.IsInstalling { drawInstallCircle(screenW, screenH, circleAnimator.PosAlpha, circleAnimator.DisplayProgress, installer.DoneInstall, textHoverFade)
drawInstallProgress(screenW, installer.Progress, installer.Task)
}
rl.EndDrawing() rl.EndDrawing()
} }
} }
// handleInput acts on mouse clicks in each step func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, prevX, prevY, nextX, nextY int32) {
func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, switch currentStep {
prevX, prevY, nextX, nextY int32) { case 0:
if currentStep == 0 {
if overRect(mousePos, step1DefaultRect) { if overRect(mousePos, step1DefaultRect) {
// user clicked "Default" => do final install if not already installing
useDefault = true useDefault = true
if !installer.IsInstalling && !installer.DoneInstall { if !installer.IsInstalling && !installer.DoneInstall {
installer.FinalInstall() installer.FinalInstall()
} }
fmt.Println("Installation started with default settings.") startTransition(currentStep, finalStep)
} }
if overRect(mousePos, step1CustomRect) { if overRect(mousePos, step1CustomRect) {
useDefault = false useDefault = false
startTransition(currentStep, 1) startTransition(currentStep, 1)
} }
} else if currentStep == 1 {
case 1:
if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) { if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) {
startTransition(currentStep, 2) startTransition(currentStep, 2)
} }
@ -149,14 +180,14 @@ func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int,
selectColor(mousePos) selectColor(mousePos)
selectContrastIcon(mousePos) selectContrastIcon(mousePos)
selectTheme(mousePos) selectTheme(mousePos)
} else if currentStep == 2 {
case 2:
if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) { if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) {
// user clicked "Finish" => final install if not already installing
if !installer.IsInstalling && !installer.DoneInstall { if !installer.IsInstalling && !installer.DoneInstall {
installer.FinalInstall() installer.FinalInstall()
} }
fmt.Printf("Installation started:\nDefault: %v\nColor: %s\nTheme: %s\nLayout: %s\n", fmt.Printf("Installation started:\nDefault: %v\nColor: %s\nTheme: %s\nLayout: %s\n", useDefault, selectedColor, selectedTheme, selectedLayout)
useDefault, selectedColor, selectedTheme, selectedLayout) startTransition(currentStep, finalStep)
} }
if overButton(mousePos, int32(prevX), int32(prevY), int32(buttonW), int32(buttonH)) { if overButton(mousePos, int32(prevX), int32(prevY), int32(buttonW), int32(buttonH)) {
startTransition(currentStep, 1) startTransition(currentStep, 1)
@ -165,7 +196,6 @@ func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int,
} }
} }
// drawHeader is your original function
func drawHeader(screenW int) { func drawHeader(screenW int) {
title := "Spitfire Browser Installer" title := "Spitfire Browser Installer"
titleFontSize := int32(30) titleFontSize := int32(30)
@ -174,37 +204,89 @@ func drawHeader(screenW int) {
rl.DrawLine(50, 60, int32(screenW)-50, 60, rl.Fade(rl.White, 0.5)) rl.DrawLine(50, 60, int32(screenW)-50, 60, rl.Fade(rl.White, 0.5))
} }
// drawInstallProgress displays a simple white circle with partial alpha, // Now we pass "displayProgress" (0..100) as a float, "posAlpha" in [0..1],
// plus the current task text below it, in the top-right area. // plus "doneInstall" to show the "Run App" button
func drawInstallProgress(screenW int, progress int, task string) { func drawInstallCircle(screenW, screenH int, posAlpha, displayProgress float32, doneInstall bool, hoverAlpha float32) {
circleX := float32(screenW - 80) // Lerp position/radius
circleY := float32(100) topRight := rl.Vector2{X: float32(screenW - 80), Y: 100}
radius := float32(30) center := rl.Vector2{X: float32(screenW) / 2, Y: float32(screenH)/2 - 50}
x := lerp(topRight.X, center.X, posAlpha)
// Colors for the circle y := lerp(topRight.Y, center.Y, posAlpha)
bgColor := rl.Color{R: 255, G: 255, B: 255, A: 80} r := lerp(30, 60, posAlpha)
fillColor := rl.Color{R: 255, G: 255, B: 255, A: 200}
// Draw background circle // Draw background circle
rl.DrawCircle(int32(circleX), int32(circleY), radius, bgColor) bgColor := rl.Color{255, 255, 255, 80}
fillColor := rl.Color{255, 255, 255, 200}
// Draw progress arc rl.DrawCircle(int32(x), int32(y), r, bgColor)
angle := float32(progress) / 100.0 * 360.0 angle := displayProgress / 100.0 * 360.0
rl.DrawCircleSector( rl.DrawCircleSector(rl.Vector2{X: x, Y: y}, r, 0, angle, 60, fillColor)
rl.Vector2{X: circleX, Y: circleY},
radius,
0,
angle,
40,
fillColor,
)
// Print numeric progress // Draw numeric progress
txt := fmt.Sprintf("%3d%%", progress) txt := fmt.Sprintf("%3.0f%%", displayProgress)
rl.DrawText(txt, int32(circleX)-rl.MeasureText(txt, 18)/2, int32(circleY)-10, 18, rl.White) var fontSize int32 = 18
if posAlpha > 0.5 {
fontSize = 24 // bigger in final steps
}
txtW := rl.MeasureText(txt, fontSize)
rl.DrawText(txt, int32(x)-txtW/2, int32(y)-(fontSize/2), fontSize, rl.White)
// Draw wrapped task text below the circle if currentStep < finalStep {
drawTextWrapped(task, int32(circleX)-100, int32(circleY)+40, 200, 18, rl.White) // show the task text with "hoverAlpha"
if hoverAlpha > 0 {
drawTextWrappedAlpha(installer.Task, int32(x)-100, int32(y)+35, 200, 18, rl.White, hoverAlpha)
}
} else {
// final step => always show text, lower so bigger circle won't overlap
drawTextWrapped(installer.Task, int32(x)-100, int32(y+r+10), 200, 18, rl.White)
if doneInstall {
drawRunAppButton(x, y+r+80)
}
}
}
// "Run App" button logic is the same
func drawRunAppButton(cx, cy float32) {
w := float32(180)
h := float32(50)
rect := rl.Rectangle{X: cx - w/2, Y: cy, Width: w, Height: h}
hovered := overRect(rl.GetMousePosition(), rect)
drawRoundedRectButton(rect, "Start Spitfire", 1.0, hovered)
if hovered && rl.IsMouseButtonPressed(rl.MouseLeftButton) {
fmt.Println("Launching the app (placeholder)...")
os.Exit(0)
}
}
// linear interpolation
func lerp(a, b, t float32) float32 {
return a + (b-a)*t
}
// Helper for text wrapping with alpha
func drawTextWrappedAlpha(text string, x, y, maxWidth, fontSize int32, color rl.Color, alpha float32) {
words := splitIntoWords(text)
line := ""
offsetY := int32(0)
for _, word := range words {
testLine := line + word + " "
if rl.MeasureText(testLine, fontSize) > int32(maxWidth) {
drawColoredText(line, x, y+offsetY, fontSize, color)
line = word + " "
offsetY += fontSize + 2
} else {
line = testLine
}
}
if line != "" {
drawColoredText(line, x, y+offsetY, fontSize, color)
}
}
// Same as drawTextWrapped, just using a color param
func drawColoredText(text string, x, y, fontSize int32, color rl.Color) {
rl.DrawText(text, x, y, fontSize, color)
} }
// Custom function to draw text with wrapping // Custom function to draw text with wrapping