refactor: perform clean up

This commit is contained in:
Patryk Hegenberg 2025-06-28 14:13:14 +02:00
parent c15cdea57d
commit a7e427ca14
13 changed files with 133 additions and 85 deletions

View file

@ -14,13 +14,11 @@ import (
)
func main() {
// 1. App initialisieren und unser neues Theme setzen
myApp := app.NewWithID("com.patani.kettlebell-tracker")
myApp.Settings().SetTheme(&theme.KettlebellTheme{})
myApp.Settings().SetTheme(&theme.KettlebellThemeOneDark{})
mainWindow := myApp.NewWindow("Kettlebell Tracker")
// 2. Services initialisieren
dbDir := myApp.Storage().RootURI().Path()
dbPath := filepath.Join(dbDir, "giant_training.db")
dbService, err := data.NewDatabaseService(dbPath)
@ -31,14 +29,11 @@ func main() {
apiService := services.NewApiService(myApp.UniqueID())
trainingService := services.NewTrainingService(dbService, settingsService, apiService)
// 3. UI-Bildschirme und Aktionen erstellen
contentContainer := container.NewMax()
var navigateTo func(string)
// Erstelle den Trainings-Screen und hole seine Start-Aktion
trainingScreen, startTrainingAction := ui.MakeTrainingScreen(trainingService, settingsService, mainWindow)
// Erstelle den Home-Screen und übergebe ihm die Start-Aktion und die Navigationsfunktion
homeScreen := ui.MakeHomeScreen(trainingService, dbService, func() {
startTrainingAction()
navigateTo("training")
@ -58,14 +53,11 @@ func main() {
contentContainer.Add(s)
}
// 4. Benutzerdefinierte Navigationsleiste erstellen
navBar, navigateFunc := ui.MakeNavBar(screens, contentContainer)
navigateTo = navigateFunc
// Initial den Home-Bildschirm anzeigen
navigateTo("home")
// 5. Hauptlayout mit Border-Layout erstellen
mainLayout := container.NewBorder(nil, navBar, nil, nil, contentContainer)
mainWindow.SetContent(mainLayout)

View file

@ -8,7 +8,6 @@ import (
_ "modernc.org/sqlite"
)
// ... (DatabaseService struct und NewDatabaseService bleiben gleich) ...
type DatabaseService struct {
DB *sql.DB
}
@ -42,7 +41,6 @@ func NewDatabaseService(dbPath string) (*DatabaseService, error) {
return &DatabaseService{DB: db}, nil
}
// ... (SaveTraining und GetHistory bleiben gleich) ...
func (s *DatabaseService) SaveTraining(session *TrainingSession) error {
dateStr := session.Date.Format(time.RFC3339)
query := `
@ -90,7 +88,6 @@ func (s *DatabaseService) GetHistory() ([]TrainingSession, error) {
return sessions, nil
}
// GetLastTraining ruft die letzte einzelne Trainingseinheit ab.
func (s *DatabaseService) GetLastTraining() (*TrainingSession, error) {
query := `SELECT id, date, sets, weightLeft, weightRight, repsPerSet, duration, program, blockDay FROM training ORDER BY date DESC LIMIT 1;`
row := s.DB.QueryRow(query)
@ -100,7 +97,7 @@ func (s *DatabaseService) GetLastTraining() (*TrainingSession, error) {
err := row.Scan(&session.ID, &dateStr, &session.Sets, &session.WeightLeft, &session.WeightRight, &session.RepsPerSet, &session.Duration, &session.Program, &session.BlockDay)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // Kein Fehler, nur keine Einträge
return nil, nil
}
return nil, err
}
@ -113,7 +110,6 @@ func (s *DatabaseService) GetLastTraining() (*TrainingSession, error) {
return &session, nil
}
// GetTrainingCount zählt alle Trainingseinheiten.
func (s *DatabaseService) GetTrainingCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM training;"

View file

@ -2,8 +2,6 @@ package data
import "time"
// TrainingSession repräsentiert eine einzelne Trainingseinheit.
// Die `db`-Tags werden verwendet, um die Struct-Felder den Datenbankspalten zuzuordnen.
type TrainingSession struct {
ID int64 `db:"id"`
Date time.Time `db:"date"`
@ -11,7 +9,7 @@ type TrainingSession struct {
WeightLeft float64 `db:"weightLeft"`
WeightRight float64 `db:"weightRight"`
RepsPerSet int64 `db:"repsPerSet"`
Duration int64 `db:"duration"` // in Sekunden
Duration int64 `db:"duration"`
Program string `db:"program"`
BlockDay int64 `db:"blockDay"`
}

View file

@ -10,8 +10,6 @@ import (
"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"`
@ -19,17 +17,14 @@ type TrainingPayload struct {
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,
},
@ -38,15 +33,12 @@ func NewApiService(appUUID string) *ApiService {
}
}
// 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,
@ -54,14 +46,12 @@ func (s *ApiService) SendTrainingData(session *data.TrainingSession) {
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)
@ -69,7 +59,6 @@ func (s *ApiService) SendTrainingData(session *data.TrainingSession) {
}
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 {
@ -78,7 +67,6 @@ func (s *ApiService) SendTrainingData(session *data.TrainingSession) {
}
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 {

View file

@ -11,7 +11,6 @@ import (
"fyne.io/fyne/v2/widget"
)
// NavButton ist unser benutzerdefinierter Navigationsbutton
type NavButton struct {
widget.BaseWidget
icon *canvas.Image
@ -52,11 +51,9 @@ func (b *NavButton) Tapped(*fyne.PointEvent) {
func (b *NavButton) SetActive(active bool) {
b.isActive = active
if b.isActive {
// b.icon.Resource.Color = theme.ColorSky400
b.label.Color = theme.ColorSky400
b.label.Color = theme.OneDarkGreen
} else {
// b.icon.Resource.Color = theme.ColorSlate400
b.label.Color = theme.ColorSlate400
b.label.Color = theme.OneDarkText
}
b.Refresh()
}

View file

@ -65,7 +65,6 @@ func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.Canvas
content := container.NewStack(list, container.NewCenter(placeholder))
layout := container.NewBorder(toolbar, nil, nil, nil, content)
// Daten laden, wenn der Container sichtbar wird
if layout.Visible() {
refreshData()
}

View file

@ -6,6 +6,7 @@ import (
"git.patanix.de/git/kettlebell-app/internal/data"
"git.patanix.de/git/kettlebell-app/internal/services"
"git.patanix.de/git/kettlebell-app/internal/ui/theme"
"git.patanix.de/git/kettlebell-app/internal/ui/utils"
@ -16,17 +17,14 @@ import (
)
func MakeHomeScreen(ts *services.TrainingService, db *data.DatabaseService, onStart func()) fyne.CanvasObject {
// Header
headerTitle := canvas.NewText("Patanix", theme.ColorSlate200)
headerTitle := canvas.NewText("Kettlebell Workout Tracker", theme.OneDarkText)
headerTitle.TextSize = 28
headerTitle.TextStyle.Bold = true
header := container.NewVBox(
widget.NewLabel("Hallo,"),
header := container.NewCenter(
headerTitle,
)
// Nächstes Training CTA
state := ts.State
nextTrainingCard := widget.NewCard(
"Nächstes Training",
@ -37,7 +35,6 @@ func MakeHomeScreen(ts *services.TrainingService, db *data.DatabaseService, onSt
),
)
// Letzte Leistung
setsValue := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
durationValue := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
weightValue := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
@ -48,7 +45,6 @@ func MakeHomeScreen(ts *services.TrainingService, db *data.DatabaseService, onSt
container.NewVBox(widget.NewLabel("Gewicht"), weightValue),
))
// Funktion zum Laden der letzten Leistung
loadLastPerformance := func() {
lastSession, err := db.GetLastTraining()
if err != nil {
@ -58,7 +54,7 @@ func MakeHomeScreen(ts *services.TrainingService, db *data.DatabaseService, onSt
if lastSession != nil {
setsValue.SetText(fmt.Sprintf("%d", lastSession.Sets))
durationValue.SetText(utils.FormatDuration(lastSession.Duration))
weightValue.SetText(fmt.Sprintf("%.1fkg", lastSession.WeightLeft)) // Annahme: linkes Gewicht ist repräsentativ
weightValue.SetText(fmt.Sprintf("%.1fkg", lastSession.WeightLeft))
}
}
@ -70,11 +66,9 @@ func MakeHomeScreen(ts *services.TrainingService, db *data.DatabaseService, onSt
)
paddedLayout := container.NewPadded(layout)
// Daten laden, wenn der Bildschirm sichtbar wird
if paddedLayout.Visible() {
loadLastPerformance()
}
// paddedLayout.OnVisible = loadLastPerformance
return paddedLayout
}

View file

@ -8,11 +8,9 @@ import (
"fyne.io/fyne/v2/theme"
)
// MakeNavBar erstellt die benutzerdefinierte Navigationsleiste und gibt die Navigationsfunktion zurück.
func MakeNavBar(screens map[string]fyne.CanvasObject, content *fyne.Container) (fyne.CanvasObject, func(string)) {
buttons := make(map[string]*components.NavButton)
// Funktion zum Umschalten der Ansichten
navigateTo := func(name string) {
for key, screen := range screens {
screen.Hide()

View file

@ -76,7 +76,7 @@ func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.W
paddedLayout := container.NewPadded(layout)
if paddedLayout.Visible() {
loadData()
} // Daten laden, wenn sichtbar
}
return paddedLayout
}

View file

@ -1,3 +1,82 @@
// package theme
//
// import (
//
// "image/color"
//
// "fyne.io/fyne/v2"
// "fyne.io/fyne/v2/theme"
//
// )
//
// // Definiere unsere benutzerdefinierten Farben basierend auf dem Mock-up
// var (
//
// ColorSlate900 = color.NRGBA{R: 0x0f, G: 0x17, B: 0x2a, A: 0xff} // bg-slate-900
// ColorSlate800 = color.NRGBA{R: 0x1e, G: 0x29, B: 0x3b, A: 0xff} // bg-slate-800
// ColorSlate700 = color.NRGBA{R: 0x33, G: 0x41, B: 0x55, A: 0xff} // bg-slate-700
// ColorSlate400 = color.NRGBA{R: 0x94, G: 0xa3, B: 0xb8, A: 0xff} // text-slate-400
// ColorSlate200 = color.NRGBA{R: 0xe2, G: 0xe8, B: 0xf0, A: 0xff} // text-slate-200
// ColorSky500 = color.NRGBA{R: 0x0e, G: 0xa5, B: 0xe9, A: 0xff} // bg-sky-500
// ColorSky400 = color.NRGBA{R: 0x38, G: 0xbd, B: 0xf8, A: 0xff} // text-sky-400
// ColorRed500 = color.NRGBA{R: 0xef, G: 0x44, B: 0x44, A: 0xff} // red-500
//
// )
//
// // KettlebellTheme ist unsere benutzerdefinierte Theme-Implementierung
// type KettlebellTheme struct{}
//
// func (t *KettlebellTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
// switch name {
// case theme.ColorNameBackground:
// return ColorSlate900
// case theme.ColorNameButton:
// return ColorSlate800
// case theme.ColorNameDisabledButton:
// return ColorSlate700
// case theme.ColorNamePrimary:
// return ColorSky500
// case theme.ColorNamePlaceHolder:
// return ColorSlate400
// case theme.ColorNameHover:
// return ColorSlate700
// case theme.ColorNameForeground:
// return ColorSlate200
// case theme.ColorNameDisabled:
// return ColorSlate400
// case theme.ColorNameError:
// return ColorRed500
// case theme.ColorNameInputBackground:
// return ColorSlate800
// case theme.ColorNameSeparator:
// return ColorSlate700
// default:
// return theme.DefaultTheme().Color(name, variant)
// }
// }
//
// func (t *KettlebellTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
// return theme.DefaultTheme().Icon(name)
// }
//
// func (t *KettlebellTheme) Font(style fyne.TextStyle) fyne.Resource {
// return theme.DefaultTheme().Font(style)
// }
//
// func (t *KettlebellTheme) Size(name fyne.ThemeSizeName) float32 {
// switch name {
// case theme.SizeNamePadding:
// return 8
// case theme.SizeNameText:
// return 16
// case theme.SizeNameHeadingText:
// return 24
// case theme.SizeNameSubHeadingText:
// return 20
// default:
// return theme.DefaultTheme().Size(name)
// }
// }
package theme
import (
@ -7,59 +86,69 @@ import (
"fyne.io/fyne/v2/theme"
)
// Definiere unsere benutzerdefinierten Farben basierend auf dem Mock-up
// One Dark Farben (angepasst für Fyne)
var (
ColorSlate900 = color.NRGBA{R: 0x0f, G: 0x17, B: 0x2a, A: 0xff} // bg-slate-900
ColorSlate800 = color.NRGBA{R: 0x1e, G: 0x29, B: 0x3b, A: 0xff} // bg-slate-800
ColorSlate700 = color.NRGBA{R: 0x33, G: 0x41, B: 0x55, A: 0xff} // bg-slate-700
ColorSlate400 = color.NRGBA{R: 0x94, G: 0xa3, B: 0xb8, A: 0xff} // text-slate-400
ColorSlate200 = color.NRGBA{R: 0xe2, G: 0xe8, B: 0xf0, A: 0xff} // text-slate-200
ColorSky500 = color.NRGBA{R: 0x0e, G: 0xa5, B: 0xe9, A: 0xff} // bg-sky-500
ColorSky400 = color.NRGBA{R: 0x38, G: 0xbd, B: 0xf8, A: 0xff} // text-sky-400
ColorRed500 = color.NRGBA{R: 0xef, G: 0x44, B: 0x44, A: 0xff} // red-500
// Basisfarben
OneDarkBackground = color.NRGBA{R: 40, G: 44, B: 52, A: 0xff} // #282c34 (helleres Grau)
OneDarkCardBackground = color.NRGBA{R: 30, G: 32, B: 40, A: 0xff} // #1e2028 (dunkler für Cards)
OneDarkText = color.NRGBA{R: 171, G: 178, B: 191, A: 0xff} // #abb2bf (Standard-Text)
OneDarkSubtleText = color.NRGBA{R: 110, G: 115, B: 141, A: 0xff} // #6e738d (deaktiviert, Placeholder)
// Akzentfarben (One Dark)
OneDarkGreen = color.NRGBA{R: 152, G: 195, B: 121, A: 0xff} // #98c379
OneDarkRed = color.NRGBA{R: 224, G: 108, B: 117, A: 0xff} // #e06c75
OneDarkYellow = color.NRGBA{R: 229, G: 192, B: 123, A: 0xff} // #e5c07b
)
// KettlebellTheme ist unsere benutzerdefinierte Theme-Implementierung
type KettlebellTheme struct{}
// KettlebellThemeOneDark ist das angepasste Theme
type KettlebellThemeOneDark struct{}
func (t *KettlebellTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
func (t *KettlebellThemeOneDark) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
switch name {
case theme.ColorNameBackground:
return ColorSlate900
return OneDarkBackground
case theme.ColorNameButton:
return ColorSlate800
return OneDarkGreen
case theme.ColorNameDisabledButton:
return ColorSlate700
return OneDarkSubtleText
case theme.ColorNamePrimary:
return ColorSky500
return OneDarkGreen
case theme.ColorNamePlaceHolder:
return ColorSlate400
return OneDarkSubtleText
case theme.ColorNameHover:
return ColorSlate700
return OneDarkYellow
case theme.ColorNameForeground:
return ColorSlate200
return OneDarkText
case theme.ColorNameDisabled:
return ColorSlate400
return OneDarkSubtleText
case theme.ColorNameError:
return ColorRed500
return OneDarkRed
case theme.ColorNameInputBackground:
return ColorSlate800
return OneDarkCardBackground
case theme.ColorNameSeparator:
return ColorSlate700
return OneDarkSubtleText
case theme.ColorNameSelection:
return OneDarkYellow
case theme.ColorNameShadow:
return OneDarkCardBackground
case theme.ColorNameFocus:
return OneDarkYellow
// case theme.ColorNameCardBackground:
// return OneDarkCardBackground
default:
return theme.DefaultTheme().Color(name, variant)
}
}
func (t *KettlebellTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
func (t *KettlebellThemeOneDark) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
func (t *KettlebellTheme) Font(style fyne.TextStyle) fyne.Resource {
func (t *KettlebellThemeOneDark) Font(style fyne.TextStyle) fyne.Resource {
return theme.DefaultTheme().Font(style)
}
func (t *KettlebellTheme) Size(name fyne.ThemeSizeName) float32 {
func (t *KettlebellThemeOneDark) Size(name fyne.ThemeSizeName) float32 {
switch name {
case theme.SizeNamePadding:
return 8

View file

@ -15,19 +15,18 @@ import (
"fyne.io/fyne/v2/widget"
)
// MakeTrainingScreen gibt jetzt das CanvasObject und die Start-Funktion zurück
func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsService, parent fyne.Window) (fyne.CanvasObject, func()) {
timerLabel := canvas.NewText("00:00", theme.ColorSlate200)
timerLabel := canvas.NewText("00:00", theme.OneDarkText)
timerLabel.TextSize = 60
timerLabel.TextStyle.Bold = true
timerLabel.Alignment = fyne.TextAlignCenter
setsLabel := canvas.NewText("0 / 0", theme.ColorSky400)
setsLabel := canvas.NewText("0 / 0", theme.OneDarkGreen)
setsLabel.TextSize = 48
setsLabel.TextStyle.Bold = true
setsLabel.Alignment = fyne.TextAlignCenter
repsLabel := canvas.NewText("0 Wiederholungen", theme.ColorSlate200)
repsLabel := canvas.NewText("0 Wiederholungen", theme.OneDarkSubtleText)
repsLabel.TextSize = 20
repsLabel.Alignment = fyne.TextAlignCenter
@ -91,7 +90,7 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi
setAction := func() {
if !ts.State.IsTrainingRunning {
return // Kein Satz ohne laufendes Training
return
}
ts.CompleteSet()
updateUI()
@ -100,19 +99,19 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi
topPart := container.NewVBox(widget.NewLabelWithStyle("Verbleibende Zeit", fyne.TextAlignCenter, fyne.TextStyle{}), timerLabel)
middlePart := container.NewVBox(widget.NewLabelWithStyle("Sätze", fyne.TextAlignCenter, fyne.TextStyle{}), setsLabel, repsLabel)
finishButton := widget.NewButton("Training beenden", finishAction)
finishButton.Importance = widget.HighImportance
setButton := widget.NewButton("Satz abschließen", setAction)
setButton.Importance = widget.HighImportance
setButton.Resize(fyne.NewSize(120, 60))
bottomPart := container.NewVBox(
widget.NewButton("Satz abschließen", setAction),
setButton,
finishButton,
)
layout := container.NewBorder(topPart, bottomPart, nil, nil, container.NewCenter(middlePart))
// UI aktualisieren, wenn der Bildschirm sichtbar wird
if layout.Visible() {
updateUI()
}
// layout.OnVisible = updateUI
return layout, startAction
}

View file

@ -2,7 +2,6 @@ package utils
import "fmt"
// FormatDuration wandelt Sekunden in einen MM:SS String um.
func FormatDuration(totalSeconds int64) string {
if totalSeconds < 0 {
totalSeconds = 0

View file

@ -2,7 +2,6 @@ package utils
import "fmt"
// Hilfsfunktion (kann in einer utils.go Datei platziert werden)
func formatDuration(totalSeconds int64) string {
mins := totalSeconds / 60
secs := totalSeconds % 60