refactor: rebuild the app in golang with the fyne framework

This commit is contained in:
Patryk Hegenberg 2025-06-27 08:16:24 +02:00
parent 479e4dffa8
commit a8ed9c9ed1
161 changed files with 1212 additions and 6641 deletions

90
internal/services/api.go Normal file
View file

@ -0,0 +1,90 @@
package services
import (
"bytes"
"encoding/json"
"log"
"net/http"
"time"
"git.patanix.de/git/kettlebell-app/internal/data"
)
// TrainingPayload ist die JSON-Struktur, die an das Backend gesendet wird.
// Die `json:"..."`-Tags stellen sicher, dass die Feldnamen im JSON korrekt sind.
type TrainingPayload struct {
Reps int `json:"reps"`
Rest float64 `json:"rest"`
Sets int `json:"sets"`
UUID string `json:"uuid"`
}
// ApiService kümmert sich um die Kommunikation mit dem Backend.
type ApiService struct {
client *http.Client
endpoint string
uuid string
}
// NewApiService erstellt einen neuen Service für die API-Kommunikation.
func NewApiService(appUUID string) *ApiService {
return &ApiService{
// Erstellt einen HTTP-Client mit einem 5-Sekunden-Timeout, genau wie in deiner Flutter-App.
client: &http.Client{
Timeout: 5 * time.Second,
},
endpoint: "http://192.168.178.43:8080/trainings/",
uuid: appUUID,
}
}
// SendTrainingData sendet eine abgeschlossene Trainingseinheit an das Backend.
func (s *ApiService) SendTrainingData(session *data.TrainingSession) {
// Berechnung für 'rest' durchführen.
var rest float64
if session.Sets > 0 {
rest = float64(session.Duration) / float64(session.Sets)
}
// Die zu sendenden Daten vorbereiten.
payload := TrainingPayload{
Reps: int(session.RepsPerSet),
Rest: rest,
Sets: int(session.Sets),
UUID: s.uuid,
}
// Daten in JSON umwandeln.
jsonData, err := json.Marshal(payload)
if err != nil {
log.Printf("API Fehler: Konnte Payload nicht in JSON umwandeln: %v", err)
return
}
// Den HTTP-Request erstellen.
req, err := http.NewRequest("POST", s.endpoint, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("API Fehler: Konnte Request nicht erstellen: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
// Request senden.
log.Printf("Sende Training an Backend: %s", string(jsonData))
resp, err := s.client.Do(req)
if err != nil {
log.Printf("API Fehler: Fehler beim Senden des Trainings: %v", err)
return
}
defer resp.Body.Close()
// Antwort des Servers prüfen.
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
log.Println("Training erfolgreich an Backend gesendet.")
} else {
log.Printf("API Fehler: Unerwarteter Statuscode: %s", resp.Status)
// Optional: Den Body der Antwort lesen, um mehr Details zu erhalten.
// body, _ := io.ReadAll(resp.Body)
// log.Printf("Antwort-Body: %s", string(body))
}
}

View file

@ -0,0 +1,41 @@
package services
import (
"fyne.io/fyne/v2"
)
type Settings struct {
TrainingTimeMinutes int
WeightLeft float64
WeightRight float64
GoalSets int
InitialProgram string
}
type SettingsService struct {
prefs fyne.Preferences
}
func NewSettingsService(app fyne.App) *SettingsService {
return &SettingsService{
prefs: app.Preferences(),
}
}
func (s *SettingsService) LoadSettings() *Settings {
return &Settings{
TrainingTimeMinutes: s.prefs.IntWithFallback("trainingTimeMinutes", 20),
WeightLeft: s.prefs.FloatWithFallback("weightLeft", 16.0),
WeightRight: s.prefs.FloatWithFallback("weightRight", 16.0),
GoalSets: s.prefs.IntWithFallback("goalSets", 5),
InitialProgram: s.prefs.StringWithFallback("initialProgram", "giant_1.0"),
}
}
func (s *SettingsService) SaveSettings(settings *Settings) {
s.prefs.SetInt("trainingTimeMinutes", settings.TrainingTimeMinutes)
s.prefs.SetFloat("weightLeft", settings.WeightLeft)
s.prefs.SetFloat("weightRight", settings.WeightRight)
s.prefs.SetInt("goalSets", settings.GoalSets)
s.prefs.SetString("initialProgram", settings.InitialProgram)
}

View file

@ -0,0 +1,356 @@
// package services
//
// import (
//
// "log"
// "time"
//
// "git.patanix.de/git/kettlebell-app/internal/data"
//
// )
//
// // TrainingState hält den aktuellen Zustand einer laufenden Trainingseinheit.
//
// type TrainingState struct {
// IsTrainingRunning bool
// RemainingSeconds int
// InitialDurationSeconds int
// SetsDone int
// GoalSets int
// RepsPerSet int
// SetTimes []time.Time
// Progress float64
// SecondsSinceLastSet int
// LastSetTimestamp *time.Time
// CurrentProgram string
// CurrentBlockDay int
// CurrentReps int
// TotalTrainingDays int
// }
//
// func NewTrainingState() *TrainingState {
// return &TrainingState{
// IsTrainingRunning: false,
// RemainingSeconds: 0,
// InitialDurationSeconds: 0,
// SetsDone: 0,
// GoalSets: 5,
// RepsPerSet: 5,
// Progress: 0.0,
// SecondsSinceLastSet: 0,
// LastSetTimestamp: nil,
// CurrentProgram: "giant_1.0",
// CurrentBlockDay: 1,
// CurrentReps: 5,
// TotalTrainingDays: 0,
// SetTimes: []time.Time{},
// }
// }
//
// type TrainingService struct {
// State *TrainingState
// dbService *data.DatabaseService
// settingsService *SettingsService
// }
//
// func NewTrainingService(db *data.DatabaseService, settings *SettingsService) *TrainingService {
// initialState := NewTrainingState()
// trainingCount, err := db.GetTrainingCount()
// if err != nil {
// log.Printf("Fehler beim Abrufen der Trainingsanzahl, setze auf 0: %v", err)
// initialState.TotalTrainingDays = 0
// } else {
// initialState.TotalTrainingDays = trainingCount
// }
// return &TrainingService{
// State: initialState,
// dbService: db,
// settingsService: settings,
// }
// }
//
// func (s *TrainingService) updateProgram() {
// st := s.State
// newTotalDays := st.TotalTrainingDays + 1
// newProgram := st.CurrentProgram
// newDay := (st.CurrentBlockDay % 3) + 1
// newReps := st.CurrentReps
//
// if newTotalDays > 0 && newTotalDays%12 == 0 {
// switch st.CurrentProgram {
// case "giant_1.0":
// newProgram = "ksk_1.0"
// case "giant_1.1":
// newProgram = "ksk_1.1"
// case "giant_1.2":
// newProgram = "ksk_1.2"
// case "ksk_1.0":
// newProgram = "giant_1.1"
// case "ksk_1.1":
// newProgram = "giant_1.2"
// case "ksk_1.2":
// newProgram = "giant_1.0"
// default:
// newProgram = "giant_1.0"
// }
// newDay = 1
// }
//
// repsMap := map[string][]int{
// "giant_1.0": {5, 6, 4},
// "giant_1.1": {6, 8, 7},
// "giant_1.2": {7, 9, 8},
// "ksk_1.0": {5, 6, 4},
// "ksk_1.1": {6, 8, 7},
// "ksk_1.2": {7, 9, 8},
// }
//
// if reps, ok := repsMap[newProgram]; ok && len(reps) >= newDay {
// newReps = reps[newDay-1]
// } else {
// newReps = 5
// }
//
// st.CurrentProgram = newProgram
// st.CurrentBlockDay = newDay
// st.CurrentReps = newReps
// st.TotalTrainingDays = newTotalDays
// }
//
// func (s *TrainingService) StartTraining(minutes, goal int) {
// s.updateProgram()
// duration := minutes * 60
// s.State = &TrainingState{
// IsTrainingRunning: true,
// InitialDurationSeconds: duration,
// RemainingSeconds: duration,
// GoalSets: goal,
// RepsPerSet: s.State.CurrentReps,
// CurrentProgram: s.State.CurrentProgram,
// CurrentBlockDay: s.State.CurrentBlockDay,
// TotalTrainingDays: s.State.TotalTrainingDays,
// SetTimes: []time.Time{},
// }
// }
//
// func (s *TrainingService) Tick() {
// if s.State.RemainingSeconds > 0 {
// s.State.RemainingSeconds--
// }
// }
//
// func (s *TrainingService) TickLastSetTimer() {
// if s.State.IsTrainingRunning && s.State.LastSetTimestamp != nil {
// s.State.SecondsSinceLastSet = int(time.Since(*s.State.LastSetTimestamp).Seconds())
// }
// }
//
// func (s *TrainingService) CompleteSet() {
// st := s.State
// st.SetsDone++
// now := time.Now()
// st.SetTimes = append(st.SetTimes, now)
// if st.GoalSets > 0 {
// st.Progress = min(float64(st.SetsDone)/float64(st.GoalSets), 1.0)
// }
// st.LastSetTimestamp = &now
// st.SecondsSinceLastSet = 0
// }
//
// func (s *TrainingService) FinishTraining(session *data.TrainingSession) error {
// session.Program = s.State.CurrentProgram
// session.BlockDay = int64(s.State.CurrentBlockDay)
//
// err := s.dbService.SaveTraining(session)
// if err != nil {
// return err
// }
//
// // Platzhalter für den API-Aufruf (aus api_service.dart)
// s.sendToBackend(session)
//
// s.ResetTraining()
// return nil
// }
//
// func (s *TrainingService) ResetTraining() {
// // Diesen Teil nochmals pruefen
// s.State = NewTrainingState()
// trainingCount, err := s.dbService.GetTrainingCount()
// if err != nil {
// log.Print("Unable to get training count")
// }
// s.State.CurrentBlockDay = trainingCount
// // Hier müsste man TotalTrainingDays wieder korrekt laden.
// }
//
// // sendToBackend ist ein Platzhalter für deinen API-Aufruf.
//
// func (s *TrainingService) sendToBackend(session *data.TrainingSession) {
// // Hier würde die Logik aus deinem `api_service.dart` hinkommen.
// // z.B. ein HTTP POST Request mit den Trainingsdaten.
// // Da der Service nicht existiert, loggen wir es nur.
// log.Println("Sende Trainingsdaten an das Backend (Platzhalter)...")
// // rest := float64(session.Duration) / float64(session.Sets)
// // log.Printf("Reps: %d, Rest: %.2f, Sets: %d", session.RepsPerSet, rest, session.Sets)
// }
package services
import (
"log"
"time"
"git.patanix.de/git/kettlebell-app/internal/data"
)
type TrainingState struct {
IsTrainingRunning bool
RemainingSeconds int
InitialDurationSeconds int
SetsDone int
GoalSets int
RepsPerSet int
SetTimes []time.Time
Progress float64
SecondsSinceLastSet int
LastSetTimestamp *time.Time
CurrentProgram string
CurrentBlockDay int
CurrentReps int
TotalTrainingDays int
}
func calculateStateByDayCount(totalDays int) (program string, blockDay, reps int) {
program = "giant_1.0"
blockDay = 1
reps = 5
if totalDays > 0 {
cycleIndex := (totalDays / 12) % 6
programs := []string{"giant_1.0", "ksk_1.0", "giant_1.1", "ksk_1.1", "giant_1.2", "ksk_1.2"}
program = programs[cycleIndex]
blockDay = (totalDays % 3) + 1
}
repsMap := map[string][]int{
"giant_1.0": {5, 6, 4},
"giant_1.1": {6, 8, 7},
"giant_1.2": {7, 9, 8},
"ksk_1.0": {5, 6, 4},
"ksk_1.1": {6, 8, 7},
"ksk_1.2": {7, 9, 8},
}
if r, ok := repsMap[program]; ok && len(r) >= blockDay {
reps = r[blockDay-1]
}
return
}
func NewTrainingState(db *data.DatabaseService) *TrainingState {
trainingCount, err := db.GetTrainingCount()
if err != nil {
log.Printf("Fehler beim Abrufen der Trainingsanzahl, setze auf 0: %v", err)
trainingCount = 0
}
program, blockDay, reps := calculateStateByDayCount(trainingCount)
return &TrainingState{
IsTrainingRunning: false,
TotalTrainingDays: trainingCount,
CurrentProgram: program,
CurrentBlockDay: blockDay,
CurrentReps: reps,
SetTimes: []time.Time{},
GoalSets: 5,
RepsPerSet: reps,
}
}
type TrainingService struct {
State *TrainingState
dbService *data.DatabaseService
settingsService *SettingsService
apiService *ApiService
}
func NewTrainingService(db *data.DatabaseService, settings *SettingsService, api *ApiService) *TrainingService {
return &TrainingService{
State: NewTrainingState(db),
dbService: db,
settingsService: settings,
apiService: api,
}
}
func (s *TrainingService) StartTraining(minutes, goal int) {
program, blockDay, reps := calculateStateByDayCount(s.State.TotalTrainingDays)
st := s.State
st.IsTrainingRunning = true
st.InitialDurationSeconds = minutes * 60
st.RemainingSeconds = st.InitialDurationSeconds
st.GoalSets = goal
st.CurrentProgram = program
st.CurrentBlockDay = blockDay
st.CurrentReps = reps
st.RepsPerSet = reps
st.SetsDone = 0
st.Progress = 0.0
st.SetTimes = []time.Time{}
st.LastSetTimestamp = nil
}
func (s *TrainingService) Tick() {
if s.State.RemainingSeconds > 0 {
s.State.RemainingSeconds--
}
}
func (s *TrainingService) TickLastSetTimer() {
if s.State.IsTrainingRunning && s.State.LastSetTimestamp != nil {
s.State.SecondsSinceLastSet = int(time.Since(*s.State.LastSetTimestamp).Seconds())
}
}
func (s *TrainingService) CompleteSet() {
st := s.State
st.SetsDone++
now := time.Now()
st.SetTimes = append(st.SetTimes, now)
if st.GoalSets > 0 {
st.Progress = min(float64(st.SetsDone)/float64(st.GoalSets), 1.0)
}
st.LastSetTimestamp = &now
st.SecondsSinceLastSet = 0
}
func (s *TrainingService) FinishTraining(session *data.TrainingSession) error {
session.Program = s.State.CurrentProgram
session.BlockDay = int64(s.State.CurrentBlockDay)
err := s.dbService.SaveTraining(session)
if err != nil {
return err
}
// s.sendToBackend(session)
go s.apiService.SendTrainingData(session)
s.ResetTraining()
return nil
}
func (s *TrainingService) ResetTraining() {
s.State = NewTrainingState(s.dbService)
}
// sendToBackend ist ein Platzhalter für deinen API-Aufruf.
func (s *TrainingService) sendToBackend(session *data.TrainingSession) {
log.Println("Sende Trainingsdaten an das Backend (Platzhalter)...")
}