added SPM
This commit is contained in:
parent
17bb547c74
commit
9e5457c2ec
8 changed files with 779 additions and 18 deletions
109
installer.go
Normal file
109
installer.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"spitfire-installer/spm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Installer manages the download, decompression, and installation processes.
|
||||||
|
type Installer struct {
|
||||||
|
// Progress info from SPM
|
||||||
|
Progress int
|
||||||
|
Task string
|
||||||
|
|
||||||
|
// Internal states
|
||||||
|
IsDownloading bool
|
||||||
|
IsInstalling bool
|
||||||
|
DoneDownload bool
|
||||||
|
DoneInstall bool
|
||||||
|
LastError error
|
||||||
|
|
||||||
|
// Paths
|
||||||
|
DownloadDir string
|
||||||
|
TempDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstaller creates a new Installer with initial state.
|
||||||
|
func NewInstaller() *Installer {
|
||||||
|
return &Installer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartDownloadDecompress starts the download and decompression in a background goroutine.
|
||||||
|
func (inst *Installer) StartDownloadDecompress() {
|
||||||
|
inst.IsDownloading = true
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
inst.IsDownloading = false
|
||||||
|
inst.DoneDownload = (inst.LastError == nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Prepare download directory
|
||||||
|
spm.UpdateProgress(0, "Preparing to download...")
|
||||||
|
inst.DownloadDir = spm.GetTempDownloadDir()
|
||||||
|
|
||||||
|
// Download APPINDEX
|
||||||
|
appIndexPath := filepath.Join(inst.DownloadDir, "APPINDEX")
|
||||||
|
spm.UpdateProgress(0, "Downloading APPINDEX")
|
||||||
|
if err := spm.DownloadAppIndex(appIndexPath); err != nil {
|
||||||
|
inst.LastError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download package
|
||||||
|
packageName := "spitfire-browser"
|
||||||
|
release := "nightly"
|
||||||
|
spm.UpdateProgress(0, "Downloading package...")
|
||||||
|
if err := spm.DownloadPackageFromAppIndex(appIndexPath, packageName, release, inst.DownloadDir); err != nil {
|
||||||
|
inst.LastError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompress
|
||||||
|
spm.UpdateProgress(0, "Decompressing...")
|
||||||
|
packagePath := filepath.Join(inst.DownloadDir, "browser-amd64-nightly-linux.tar.gz")
|
||||||
|
tempDir, err := spm.DecompressToTemp(packagePath)
|
||||||
|
if err != nil {
|
||||||
|
inst.LastError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.TempDir = tempDir
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FinalInstall moves files to the final install directory in a background goroutine.
|
||||||
|
func (inst *Installer) FinalInstall() {
|
||||||
|
if !inst.DoneDownload {
|
||||||
|
inst.LastError = fmt.Errorf("Cannot install: download and decompression are not complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.IsInstalling = true
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
inst.IsInstalling = false
|
||||||
|
inst.DoneInstall = (inst.LastError == nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Generate default install directory
|
||||||
|
installDir, err := spm.GetDefaultInstallDir()
|
||||||
|
if err != nil {
|
||||||
|
inst.LastError = fmt.Errorf("failed to determine default install directory: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := spm.MoveFilesToInstallDir(inst.TempDir, installDir); err != nil {
|
||||||
|
inst.LastError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
spm.UpdateProgress(100, "Installation complete!")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollProgress fetches the latest progress and task from SPM.
|
||||||
|
func (inst *Installer) PollProgress() {
|
||||||
|
p, t := spm.GetProgress()
|
||||||
|
inst.Progress, inst.Task = p, t
|
||||||
|
}
|
230
main.go
230
main.go
|
@ -2,18 +2,23 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
rl "github.com/gen2brain/raylib-go/raylib"
|
rl "github.com/gen2brain/raylib-go/raylib"
|
||||||
)
|
)
|
||||||
|
|
||||||
var transition = NewTransitionManager()
|
// Keep your global variables for steps and transitions
|
||||||
var currentStep = 0
|
var (
|
||||||
var targetStep = 0
|
transition = NewTransitionManager()
|
||||||
var useDefault = false
|
currentStep = 0
|
||||||
|
targetStep = 0
|
||||||
|
useDefault = false
|
||||||
|
|
||||||
var step1DefaultRect rl.Rectangle
|
step1DefaultRect rl.Rectangle
|
||||||
var step1CustomRect rl.Rectangle
|
step1CustomRect rl.Rectangle
|
||||||
|
|
||||||
|
// Our global Installer from installer.go
|
||||||
|
installer = NewInstaller()
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
monitor := rl.GetCurrentMonitor()
|
monitor := rl.GetCurrentMonitor()
|
||||||
|
@ -32,6 +37,9 @@ func main() {
|
||||||
|
|
||||||
InitBackground(rl.GetScreenWidth(), rl.GetScreenHeight())
|
InitBackground(rl.GetScreenWidth(), rl.GetScreenHeight())
|
||||||
|
|
||||||
|
// Start the download+decompress in background immediately:
|
||||||
|
installer.StartDownloadDecompress()
|
||||||
|
|
||||||
targetStep = 0
|
targetStep = 0
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
|
@ -46,12 +54,24 @@ func main() {
|
||||||
nextX := int32(screenW - 150)
|
nextX := int32(screenW - 150)
|
||||||
nextY := prevY
|
nextY := prevY
|
||||||
|
|
||||||
|
// Update transition
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll SPM progress for the GUI
|
||||||
|
installer.PollProgress()
|
||||||
|
|
||||||
|
// If an unrecoverable error occurred, you could handle it here:
|
||||||
|
// (For now, we just print it in the console.)
|
||||||
|
if installer.LastError != nil {
|
||||||
|
fmt.Println("SPM Error:", installer.LastError)
|
||||||
|
// You might choose to show a popup or do something else
|
||||||
|
}
|
||||||
|
|
||||||
|
// GUI 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)
|
||||||
}
|
}
|
||||||
|
@ -93,20 +113,27 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw the semi-transparent loading circle if user is downloading or installing
|
||||||
|
if installer.IsDownloading || installer.IsInstalling {
|
||||||
|
drawInstallProgress(screenW, installer.Progress, installer.Task)
|
||||||
|
}
|
||||||
|
|
||||||
rl.EndDrawing()
|
rl.EndDrawing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTransition(from, to int) {
|
// handleInput acts on mouse clicks in each step
|
||||||
targetStep = to
|
func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int,
|
||||||
transition.Start(from, to)
|
prevX, prevY, nextX, nextY int32) {
|
||||||
}
|
|
||||||
|
|
||||||
func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, prevX, prevY, nextX, nextY int32) {
|
|
||||||
if currentStep == 0 {
|
if currentStep == 0 {
|
||||||
if overRect(mousePos, step1DefaultRect) {
|
if overRect(mousePos, step1DefaultRect) {
|
||||||
fmt.Println("Installation complete with default settings.")
|
// user clicked "Default" => do final install if not already installing
|
||||||
os.Exit(0)
|
useDefault = true
|
||||||
|
if !installer.IsInstalling && !installer.DoneInstall {
|
||||||
|
installer.FinalInstall()
|
||||||
|
}
|
||||||
|
fmt.Println("Installation started with default settings.")
|
||||||
}
|
}
|
||||||
if overRect(mousePos, step1CustomRect) {
|
if overRect(mousePos, step1CustomRect) {
|
||||||
useDefault = false
|
useDefault = false
|
||||||
|
@ -124,9 +151,12 @@ func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, pr
|
||||||
selectTheme(mousePos)
|
selectTheme(mousePos)
|
||||||
} else if currentStep == 2 {
|
} else if currentStep == 2 {
|
||||||
if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) {
|
if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) {
|
||||||
fmt.Printf("Installation complete:\nDefault: %v\nColor: %s\nTheme: %s\nLayout: %s\n",
|
// user clicked "Finish" => final install if not already installing
|
||||||
|
if !installer.IsInstalling && !installer.DoneInstall {
|
||||||
|
installer.FinalInstall()
|
||||||
|
}
|
||||||
|
fmt.Printf("Installation started:\nDefault: %v\nColor: %s\nTheme: %s\nLayout: %s\n",
|
||||||
useDefault, selectedColor, selectedTheme, selectedLayout)
|
useDefault, selectedColor, selectedTheme, selectedLayout)
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
@ -135,6 +165,7 @@ func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
@ -142,3 +173,170 @@ func drawHeader(screenW int) {
|
||||||
rl.DrawText(title, (int32(screenW)-titleWidth)/2, 20, titleFontSize, rl.RayWhite)
|
rl.DrawText(title, (int32(screenW)-titleWidth)/2, 20, titleFontSize, rl.RayWhite)
|
||||||
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,
|
||||||
|
// plus the current task text below it, in the top-right area.
|
||||||
|
func drawInstallProgress(screenW int, progress int, task string) {
|
||||||
|
circleX := float32(screenW - 80)
|
||||||
|
circleY := float32(100)
|
||||||
|
radius := float32(30)
|
||||||
|
|
||||||
|
// Colors for the circle
|
||||||
|
bgColor := rl.Color{R: 255, G: 255, B: 255, A: 80}
|
||||||
|
fillColor := rl.Color{R: 255, G: 255, B: 255, A: 200}
|
||||||
|
|
||||||
|
// Draw background circle
|
||||||
|
rl.DrawCircle(int32(circleX), int32(circleY), radius, bgColor)
|
||||||
|
|
||||||
|
// Draw progress arc
|
||||||
|
angle := float32(progress) / 100.0 * 360.0
|
||||||
|
rl.DrawCircleSector(
|
||||||
|
rl.Vector2{X: circleX, Y: circleY},
|
||||||
|
radius,
|
||||||
|
0,
|
||||||
|
angle,
|
||||||
|
40,
|
||||||
|
fillColor,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Print numeric progress
|
||||||
|
txt := fmt.Sprintf("%3d%%", progress)
|
||||||
|
rl.DrawText(txt, int32(circleX)-rl.MeasureText(txt, 18)/2, int32(circleY)-10, 18, rl.White)
|
||||||
|
|
||||||
|
// Draw wrapped task text below the circle
|
||||||
|
drawTextWrapped(task, int32(circleX)-100, int32(circleY)+40, 200, 18, rl.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom function to draw text with wrapping
|
||||||
|
func drawTextWrapped(text string, x, y int32, maxWidth, fontSize int32, color rl.Color) {
|
||||||
|
words := splitIntoWords(text)
|
||||||
|
line := ""
|
||||||
|
offsetY := int32(0)
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
testLine := line + word + " "
|
||||||
|
if rl.MeasureText(testLine, fontSize) > int32(maxWidth) {
|
||||||
|
rl.DrawText(line, x, y+offsetY, fontSize, color)
|
||||||
|
line = word + " "
|
||||||
|
offsetY += fontSize + 2
|
||||||
|
} else {
|
||||||
|
line = testLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the last line
|
||||||
|
if line != "" {
|
||||||
|
rl.DrawText(line, x, y+offsetY, fontSize, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to split text into words
|
||||||
|
func splitIntoWords(text string) []string {
|
||||||
|
words := []string{}
|
||||||
|
word := ""
|
||||||
|
for _, char := range text {
|
||||||
|
if char == ' ' || char == '\n' {
|
||||||
|
if word != "" {
|
||||||
|
words = append(words, word)
|
||||||
|
word = ""
|
||||||
|
}
|
||||||
|
if char == '\n' {
|
||||||
|
words = append(words, "\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
word += string(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if word != "" {
|
||||||
|
words = append(words, word)
|
||||||
|
}
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
// startTransition is your existing function
|
||||||
|
func startTransition(from, to int) {
|
||||||
|
targetStep = to
|
||||||
|
transition.Start(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPM example
|
||||||
|
|
||||||
|
// package main
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "fmt"
|
||||||
|
// "os"
|
||||||
|
// "path/filepath"
|
||||||
|
// "spitfire-installer/spm"
|
||||||
|
// "time"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// func main() {
|
||||||
|
// // Start a goroutine to display progress updates
|
||||||
|
// done := make(chan bool)
|
||||||
|
// go func() {
|
||||||
|
// for {
|
||||||
|
// select {
|
||||||
|
// case <-done:
|
||||||
|
// return
|
||||||
|
// default:
|
||||||
|
// percentage, task := spm.GetProgress()
|
||||||
|
// fmt.Printf("\r[%3d%%] %s", percentage, task)
|
||||||
|
// time.Sleep(500 * time.Millisecond)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
|
||||||
|
// // Set up the download directory
|
||||||
|
// downloadDir := spm.GetTempDownloadDir()
|
||||||
|
// fmt.Println("\nTemporary download directory:", downloadDir)
|
||||||
|
|
||||||
|
// // Download the APPINDEX
|
||||||
|
// appIndexPath := filepath.Join(downloadDir, "APPINDEX")
|
||||||
|
// spm.UpdateProgress(0, "Starting APPINDEX download")
|
||||||
|
// if err := spm.DownloadAppIndex(appIndexPath); err != nil {
|
||||||
|
// fmt.Println("\nError downloading APPINDEX:", err)
|
||||||
|
// done <- true
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Download the desired package version (e.g., nightly)
|
||||||
|
// packageName := "spitfire-browser"
|
||||||
|
// release := "nightly"
|
||||||
|
|
||||||
|
// spm.UpdateProgress(0, "Starting package download")
|
||||||
|
// if err := spm.DownloadPackageFromAppIndex(appIndexPath, packageName, release, downloadDir); err != nil {
|
||||||
|
// fmt.Println("\nError downloading package:", err)
|
||||||
|
// done <- true
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Decompress and install
|
||||||
|
// packagePath := filepath.Join(downloadDir, "browser-amd64-nightly-linux.tar.gz")
|
||||||
|
// spm.UpdateProgress(0, "Starting decompression")
|
||||||
|
// tempDir, err := spm.DecompressToTemp(packagePath)
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println("\nError decompressing package:", err)
|
||||||
|
// done <- true
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
// fmt.Println("\nDecompressed package to:", tempDir)
|
||||||
|
|
||||||
|
// // Generate default install directory
|
||||||
|
// installDir, err := spm.GetDefaultInstallDir()
|
||||||
|
// if err != nil {
|
||||||
|
// inst.LastError = fmt.Errorf("failed to determine default install directory: %w", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// spm.UpdateProgress(0, "Starting installation")
|
||||||
|
// if err := spm.MoveFilesToInstallDir(tempDir, installDir); err != nil {
|
||||||
|
// fmt.Println("\nError installing package:", err)
|
||||||
|
// done <- true
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Notify progress display to stop and finalize
|
||||||
|
// done <- true
|
||||||
|
// fmt.Printf("\nSuccessfully installed %s (%s) to %s\n", packageName, release, installDir)
|
||||||
|
// }
|
||||||
|
|
108
spm/appindex.go
Normal file
108
spm/appindex.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package spm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const appIndexURL = "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX"
|
||||||
|
|
||||||
|
func DownloadAppIndex(dest string) error {
|
||||||
|
UpdateProgress(0, "Downloading APPINDEX")
|
||||||
|
resp, err := http.Get(appIndexURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
totalSize := resp.ContentLength
|
||||||
|
var downloaded int64
|
||||||
|
|
||||||
|
// Track progress as bytes are downloaded
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
n, err := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
downloaded += int64(n)
|
||||||
|
percentage := int(float64(downloaded) / float64(totalSize) * 100)
|
||||||
|
UpdateProgress(percentage, "Downloading APPINDEX")
|
||||||
|
if _, err := out.Write(buf[:n]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateProgress(100, "APPINDEX downloaded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppIndexEntry struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Release string
|
||||||
|
Arch string
|
||||||
|
OS string
|
||||||
|
DownloadURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var entries []AppIndexEntry
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
entry := AppIndexEntry{}
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.HasPrefix(line, "C:") {
|
||||||
|
if entry.Name != "" {
|
||||||
|
entries = append(entries, entry)
|
||||||
|
entry = AppIndexEntry{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[0] {
|
||||||
|
case "P":
|
||||||
|
entry.Name = parts[1]
|
||||||
|
case "R":
|
||||||
|
entry.Release = parts[1]
|
||||||
|
case "V":
|
||||||
|
entry.Version = parts[1]
|
||||||
|
case "A":
|
||||||
|
entry.Arch = parts[1]
|
||||||
|
case "p":
|
||||||
|
entry.OS = parts[1]
|
||||||
|
case "d":
|
||||||
|
entry.DownloadURL = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Name != "" {
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, scanner.Err()
|
||||||
|
}
|
91
spm/download.go
Normal file
91
spm/download.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package spm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Package struct {
|
||||||
|
Name string
|
||||||
|
Arch string
|
||||||
|
OS string
|
||||||
|
DownloadURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadPackage(pkg Package, destDir string) error {
|
||||||
|
UpdateProgress(0, fmt.Sprintf("Downloading %s", pkg.Name))
|
||||||
|
resp, err := http.Get(pkg.DownloadURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
filePath := filepath.Join(destDir, filepath.Base(pkg.DownloadURL))
|
||||||
|
out, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
totalSize := resp.ContentLength
|
||||||
|
var downloaded int64
|
||||||
|
|
||||||
|
// Track progress as bytes are downloaded
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
n, err := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
downloaded += int64(n)
|
||||||
|
percentage := int(float64(downloaded) / float64(totalSize) * 100)
|
||||||
|
UpdateProgress(percentage, fmt.Sprintf("Downloading %s", pkg.Name))
|
||||||
|
if _, err := out.Write(buf[:n]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateProgress(100, fmt.Sprintf("%s downloaded", pkg.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindPackage(entries []AppIndexEntry, name, release, arch, os string) (*AppIndexEntry, error) {
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Name == name && entry.Release == release && entry.Arch == arch && entry.OS == os {
|
||||||
|
return &entry, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("package not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadPackageFromAppIndex(appIndexPath, packageName, release, destDir string) error {
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
osName := runtime.GOOS
|
||||||
|
|
||||||
|
entries, err := ParseAppIndex(appIndexPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := FindPackage(entries, packageName, release, arch, osName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DownloadPackage(Package{
|
||||||
|
Name: entry.Name,
|
||||||
|
Arch: entry.Arch,
|
||||||
|
OS: entry.OS,
|
||||||
|
DownloadURL: entry.DownloadURL,
|
||||||
|
}, destDir)
|
||||||
|
}
|
161
spm/install.go
Normal file
161
spm/install.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package spm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DecompressToTemp(filePath string) (string, error) {
|
||||||
|
UpdateProgress(0, "Decompressing package")
|
||||||
|
tempDir, err := os.MkdirTemp("", "spm_decompress")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
gzr, err := gzip.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(gzr)
|
||||||
|
var totalFiles, processedFiles int
|
||||||
|
// Count total files
|
||||||
|
for {
|
||||||
|
_, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
totalFiles++
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Seek(0, io.SeekStart)
|
||||||
|
gzr.Reset(file)
|
||||||
|
tarReader = tar.NewReader(gzr)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(tempDir, header.Name)
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
case tar.TypeReg:
|
||||||
|
outFile, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
outFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
processedFiles++
|
||||||
|
UpdateProgress(int(float64(processedFiles)/float64(totalFiles)*100), "Decompressing package")
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateProgress(100, "Package decompressed")
|
||||||
|
return tempDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveFilesToInstallDir(tempDir, installDir string) error {
|
||||||
|
// Count total files to copy
|
||||||
|
var totalFiles, copiedFiles int
|
||||||
|
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
totalFiles++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy files and track progress
|
||||||
|
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(tempDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(installDir, relPath)
|
||||||
|
if info.IsDir() {
|
||||||
|
// Create directories in the install directory
|
||||||
|
if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Copy files to the install directory
|
||||||
|
if err := copyFile(path, targetPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copiedFiles++
|
||||||
|
UpdateProgress(int(float64(copiedFiles)/float64(totalFiles)*100), "Copying files to install directory")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temporary directory
|
||||||
|
UpdateProgress(100, "Cleaning up temporary files")
|
||||||
|
return os.RemoveAll(tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile copies the contents of the source file to the destination file.
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
sourceFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
// Create the destination file
|
||||||
|
destinationFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer destinationFile.Close()
|
||||||
|
|
||||||
|
// Copy the file content
|
||||||
|
if _, err := io.Copy(destinationFile, sourceFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve file permissions
|
||||||
|
info, err := sourceFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chmod(dst, info.Mode())
|
||||||
|
}
|
30
spm/progress.go
Normal file
30
spm/progress.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package spm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Progress struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
percentage int
|
||||||
|
task string
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = Progress{}
|
||||||
|
|
||||||
|
// UpdateProgress sets the current percentage and task.
|
||||||
|
func UpdateProgress(percentage int, task string) {
|
||||||
|
progress.mu.Lock()
|
||||||
|
defer progress.mu.Unlock()
|
||||||
|
progress.percentage = percentage
|
||||||
|
progress.task = task
|
||||||
|
fmt.Printf("\r[%3d%%] %s", percentage, task) // Print progress to the terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProgress returns the current progress state.
|
||||||
|
func GetProgress() (int, string) {
|
||||||
|
progress.mu.Lock()
|
||||||
|
defer progress.mu.Unlock()
|
||||||
|
return progress.percentage, progress.task
|
||||||
|
}
|
64
spm/utils.go
Normal file
64
spm/utils.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package spm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTempDownloadDir() string {
|
||||||
|
dir, err := os.MkdirTemp("", "spm_downloads")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDownloadFolder(customDir string) (string, error) {
|
||||||
|
if err := os.MkdirAll(customDir, os.ModePerm); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return customDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultInstallDir generates the default installation directory based on the OS and environment.
|
||||||
|
func GetDefaultInstallDir() (string, error) {
|
||||||
|
var installDir string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
// Use %APPDATA% or Program Files on Windows
|
||||||
|
appData := os.Getenv("APPDATA")
|
||||||
|
if appData != "" {
|
||||||
|
installDir = filepath.Join(appData, "Spitfire")
|
||||||
|
} else {
|
||||||
|
programFiles := os.Getenv("ProgramFiles")
|
||||||
|
if programFiles == "" {
|
||||||
|
return "", fmt.Errorf("unable to determine default install directory on Windows")
|
||||||
|
}
|
||||||
|
installDir = filepath.Join(programFiles, "Spitfire")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "darwin":
|
||||||
|
// Use ~/Library/Application Support on macOS
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to determine home directory on macOS: %w", err)
|
||||||
|
}
|
||||||
|
installDir = filepath.Join(homeDir, "Library", "Application Support", "Spitfire")
|
||||||
|
|
||||||
|
case "linux":
|
||||||
|
// Use ~/.local/share or /opt on Linux
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to determine home directory on Linux: %w", err)
|
||||||
|
}
|
||||||
|
installDir = filepath.Join(homeDir, ".local", "share", "Spitfire")
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return installDir, nil
|
||||||
|
}
|
|
@ -122,9 +122,9 @@ func (t *TransitionManager) Update() (
|
||||||
oldAlpha, oldScale, oldOffsetX = 1, 1, 0
|
oldAlpha, oldScale, oldOffsetX = 1, 1, 0
|
||||||
newAlpha, newScale, newOffsetX = 1, 1, 0
|
newAlpha, newScale, newOffsetX = 1, 1, 0
|
||||||
|
|
||||||
slideDir := float32(-1)
|
slideDir := float32(1)
|
||||||
if t.direction == DirectionBackward {
|
if t.direction == DirectionBackward {
|
||||||
slideDir = 1
|
slideDir = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
p := t.accumSec / t.totalSec
|
p := t.accumSec / t.totalSec
|
||||||
|
|
Loading…
Add table
Reference in a new issue