refactor: new ui step3

This commit is contained in:
Patryk Hegenberg 2025-06-27 20:18:32 +02:00
parent 24430d0fae
commit c15cdea57d
6 changed files with 172 additions and 153 deletions

View file

@ -4,25 +4,23 @@ import (
"log"
"path/filepath"
"git.patanix.de/git/kettlebell-app/internal/data"
"git.patanix.de/git/kettlebell-app/internal/services"
"git.patanix.de/git/kettlebell-app/internal/ui"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"git.patanix.de/git/kettlebell-app/internal/ui/theme" // <-- Neuer Theme-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"
"git.patanix.de/git/kettlebell-app/internal/ui/theme"
)
func main() {
// 1. App initialisieren und unser neues Theme setzen
myApp := app.NewWithID("com.patani.kettlebell-tracker")
myApp.Settings().SetTheme(&theme.KettlebellTheme{}) // <-- Setzt unser benutzerdefiniertes Theme
myApp.Settings().SetTheme(&theme.KettlebellTheme{})
mainWindow := myApp.NewWindow("Kettlebell Tracker")
// 2. Services initialisieren (wie zuvor)
// 2. Services initialisieren
dbDir := myApp.Storage().RootURI().Path()
dbPath := filepath.Join(dbDir, "giant_training.db")
dbService, err := data.NewDatabaseService(dbPath)
@ -33,15 +31,19 @@ func main() {
apiService := services.NewApiService(myApp.UniqueID())
trainingService := services.NewTrainingService(dbService, settingsService, apiService)
// 3. UI-Bildschirme erstellen
// 3. UI-Bildschirme und Aktionen erstellen
contentContainer := container.NewMax()
// Navigationsfunktion definieren, die wir an die Screens weitergeben können
var navigateTo func(string)
// Die Bildschirme erstellen und dem Container hinzufügen
homeScreen := ui.MakeHomeScreen(trainingService, func() { navigateTo("training") })
trainingScreen := ui.MakeTrainingScreen(trainingService, settingsService, mainWindow)
// 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")
})
historyScreen := ui.MakeHistoryScreen(dbService, mainWindow)
settingsScreen := ui.MakeSettingsScreen(settingsService, mainWindow)
@ -58,7 +60,7 @@ func main() {
// 4. Benutzerdefinierte Navigationsleiste erstellen
navBar, navigateFunc := ui.MakeNavBar(screens, contentContainer)
navigateTo = navigateFunc // Weisen der Navigationsfunktion die Implementierung aus der Navbar zu
navigateTo = navigateFunc
// Initial den Home-Bildschirm anzeigen
navigateTo("home")
@ -67,7 +69,7 @@ func main() {
mainLayout := container.NewBorder(nil, navBar, nil, nil, contentContainer)
mainWindow.SetContent(mainLayout)
mainWindow.Resize(fyne.NewSize(360, 740)) // Mobile-freundliche Größe
mainWindow.Resize(fyne.NewSize(360, 740))
mainWindow.SetMaster()
mainWindow.ShowAndRun()
}

View file

@ -5,9 +5,10 @@ import (
"log"
"time"
_ "modernc.org/sqlite" // Importiert den SQLite-Treiber
_ "modernc.org/sqlite"
)
// ... (DatabaseService struct und NewDatabaseService bleiben gleich) ...
type DatabaseService struct {
DB *sql.DB
}
@ -17,11 +18,9 @@ func NewDatabaseService(dbPath string) (*DatabaseService, error) {
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
createTableSQL := `
CREATE TABLE IF NOT EXISTS training (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -34,23 +33,18 @@ func NewDatabaseService(dbPath string) (*DatabaseService, error) {
program TEXT,
blockDay INTEGER
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Printf("Fehler beim Erstellen der Tabelle: %v", err)
return nil, err
}
// Hier könnten wir auch komplexere Migrationen wie dein _onUpgrade handle,
// aber für den Anfang reicht das Erstellen der Tabelle.
log.Println("Datenbank erfolgreich initialisiert.")
return &DatabaseService{DB: db}, nil
}
// ... (SaveTraining und GetHistory bleiben gleich) ...
func (s *DatabaseService) SaveTraining(session *TrainingSession) error {
dateStr := session.Date.Format(time.RFC3339)
query := `
INSERT INTO training (id, date, sets, weightLeft, weightRight, repsPerSet, duration, program, blockDay)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
@ -68,11 +62,58 @@ func (s *DatabaseService) SaveTraining(session *TrainingSession) error {
if session.ID != 0 {
id = session.ID
}
_, err := s.DB.Exec(query, id, dateStr, session.Sets, session.WeightLeft, session.WeightRight, session.RepsPerSet, session.Duration, session.Program, session.BlockDay)
return err
}
func (s *DatabaseService) GetHistory() ([]TrainingSession, error) {
query := `SELECT id, date, sets, weightLeft, weightRight, repsPerSet, duration, program, blockDay FROM training ORDER BY date DESC LIMIT 20;`
rows, err := s.DB.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var sessions []TrainingSession
for rows.Next() {
var sess TrainingSession
var dateStr string
err := rows.Scan(&sess.ID, &dateStr, &sess.Sets, &sess.WeightLeft, &sess.WeightRight, &sess.RepsPerSet, &sess.Duration, &sess.Program, &sess.BlockDay)
if err != nil {
return nil, err
}
sess.Date, err = time.Parse(time.RFC3339, dateStr)
if err != nil {
return nil, err
}
sessions = append(sessions, sess)
}
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)
var session TrainingSession
var dateStr string
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, err
}
session.Date, err = time.Parse(time.RFC3339, dateStr)
if err != nil {
return nil, err
}
return &session, nil
}
// GetTrainingCount zählt alle Trainingseinheiten.
func (s *DatabaseService) GetTrainingCount() (int, error) {
var count int
query := "SELECT COUNT(*) FROM training;"
@ -85,33 +126,3 @@ func (s *DatabaseService) GetTrainingCount() (int, error) {
}
return count, nil
}
func (s *DatabaseService) GetHistory() ([]TrainingSession, error) {
query := `SELECT id, date, sets, weightLeft, weightRight, repsPerSet, duration, program, blockDay FROM training ORDER BY date DESC LIMIT 20;`
rows, err := s.DB.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var sessions []TrainingSession
for rows.Next() {
var s TrainingSession
var dateStr string
err := rows.Scan(&s.ID, &dateStr, &s.Sets, &s.WeightLeft, &s.WeightRight, &s.RepsPerSet, &s.Duration, &s.Program, &s.BlockDay)
if err != nil {
return nil, err
}
s.Date, err = time.Parse(time.RFC3339, dateStr)
if err != nil {
return nil, err
}
sessions = append(sessions, s)
}
return sessions, nil
}

View file

@ -4,14 +4,13 @@ import (
"fmt"
"log"
"git.patanix.de/git/kettlebell-app/internal/data"
"git.patanix.de/git/kettlebell-app/internal/ui/utils"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"git.patanix.de/git/kettlebell-app/internal/data"
"git.patanix.de/git/kettlebell-app/internal/ui/utils"
)
func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.CanvasObject {
@ -19,19 +18,14 @@ func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.Canvas
placeholder := widget.NewLabel("Noch keine Trainingsdaten vorhanden.")
list := widget.NewList(
func() int {
return len(history)
},
func() int { return len(history) },
func() fyne.CanvasObject {
return widget.NewCard("", "", container.NewVBox(
widget.NewLabel(""),
widget.NewSeparator(),
container.NewGridWithColumns(3,
stats := container.NewGridWithColumns(3,
container.NewVBox(widget.NewLabel("Sätze"), widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})),
container.NewVBox(widget.NewLabel("Dauer"), widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})),
container.NewVBox(widget.NewLabel("Reps/Satz"), widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})),
),
))
)
return widget.NewCard("", "", container.NewVBox(widget.NewLabel(""), widget.NewSeparator(), stats))
},
func(i widget.ListItemID, o fyne.CanvasObject) {
session := history[i]
@ -43,7 +37,6 @@ func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.Canvas
statsGrid := content.Objects[2].(*fyne.Container)
programLabel.SetText(fmt.Sprintf("%s - Tag %d", session.Program, session.BlockDay))
statsGrid.Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", session.Sets))
statsGrid.Objects[1].(*fyne.Container).Objects[1].(*widget.Label).SetText(utils.FormatDuration(session.Duration))
statsGrid.Objects[2].(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", session.RepsPerSet))
@ -56,32 +49,25 @@ func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.Canvas
if err != nil {
log.Printf("Fehler beim Laden der Historie: %v", err)
dialog.ShowError(err, parent)
return
}
if len(history) == 0 {
list.Hide()
placeholder.Show()
list.Hide()
} else {
list.Show()
placeholder.Hide()
list.Show()
}
list.Refresh()
}
list.OnSelected = func(id widget.ListItemID) {
list.Unselect(id)
}
toolbar := widget.NewToolbar(widget.NewToolbarAction(theme.ViewRefreshIcon(), refreshData))
content := container.NewStack(list, container.NewCenter(placeholder))
layout := container.NewBorder(toolbar, nil, nil, nil, content)
toolbar := widget.NewToolbar(
widget.NewToolbarAction(theme.ViewRefreshIcon(), refreshData),
)
layout := container.NewBorder(toolbar, nil, nil, nil, container.NewStack(list, container.NewCenter(placeholder)))
wrapper := container.NewMax(layout)
if wrapper.Visible() {
// Daten laden, wenn der Container sichtbar wird
if layout.Visible() {
refreshData()
}
// wrapper.OnVisible = refreshData
return wrapper
return layout
}

View file

@ -2,9 +2,12 @@ package ui
import (
"fmt"
"log"
"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"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
@ -12,7 +15,7 @@ import (
"fyne.io/fyne/v2/widget"
)
func MakeHomeScreen(ts *services.TrainingService, onStart func()) fyne.CanvasObject {
func MakeHomeScreen(ts *services.TrainingService, db *data.DatabaseService, onStart func()) fyne.CanvasObject {
// Header
headerTitle := canvas.NewText("Patanix", theme.ColorSlate200)
headerTitle.TextSize = 28
@ -25,8 +28,6 @@ func MakeHomeScreen(ts *services.TrainingService, onStart func()) fyne.CanvasObj
// Nächstes Training CTA
state := ts.State
// Verwende ein Card-Widget für das Styling
nextTrainingCard := widget.NewCard(
"Nächstes Training",
fmt.Sprintf("%s - Tag %d", state.CurrentProgram, state.CurrentBlockDay),
@ -37,12 +38,30 @@ func MakeHomeScreen(ts *services.TrainingService, onStart func()) fyne.CanvasObj
)
// 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})
statsCard := widget.NewCard("Letzte Leistung", "", container.NewGridWithColumns(3,
container.NewVBox(widget.NewLabel("Sätze"), widget.NewLabelWithStyle("8", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})),
container.NewVBox(widget.NewLabel("Dauer"), widget.NewLabelWithStyle("18:45", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})),
container.NewVBox(widget.NewLabel("Gewicht"), widget.NewLabelWithStyle("16kg", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})),
container.NewVBox(widget.NewLabel("Sätze"), setsValue),
container.NewVBox(widget.NewLabel("Dauer"), durationValue),
container.NewVBox(widget.NewLabel("Gewicht"), weightValue),
))
// Funktion zum Laden der letzten Leistung
loadLastPerformance := func() {
lastSession, err := db.GetLastTraining()
if err != nil {
log.Printf("Fehler beim Laden der letzten Session: %v", err)
return
}
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
}
}
layout := container.NewVBox(
header,
widget.NewSeparator(),
@ -50,5 +69,12 @@ func MakeHomeScreen(ts *services.TrainingService, onStart func()) fyne.CanvasObj
statsCard,
)
return container.NewPadded(layout)
paddedLayout := container.NewPadded(layout)
// Daten laden, wenn der Bildschirm sichtbar wird
if paddedLayout.Visible() {
loadLastPerformance()
}
// paddedLayout.OnVisible = loadLastPerformance
return paddedLayout
}

View file

@ -12,10 +12,17 @@ import (
)
func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.Window) fyne.CanvasObject {
currentSettings := settingsService.LoadSettings()
var timeEntry, setsEntry, weightLeftEntry, weightRightEntry *widget.Entry
timeEntry := widget.NewEntry()
loadData := func() {
currentSettings := settingsService.LoadSettings()
timeEntry.SetText(fmt.Sprintf("%d", currentSettings.TrainingTimeMinutes))
setsEntry.SetText(fmt.Sprintf("%d", currentSettings.GoalSets))
weightLeftEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightLeft))
weightRightEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightRight))
}
timeEntry = widget.NewEntry()
timeEntry.Validator = func(s string) error {
if _, err := strconv.Atoi(s); err != nil {
return fmt.Errorf("muss eine Zahl sein")
@ -23,22 +30,18 @@ func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.W
return nil
}
setsEntry := widget.NewEntry()
setsEntry.SetText(fmt.Sprintf("%d", currentSettings.GoalSets))
setsEntry.Validator = timeEntry.Validator // Gleicher Validator
setsEntry = widget.NewEntry()
setsEntry.Validator = timeEntry.Validator
weightLeftEntry := widget.NewEntry()
weightLeftEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightLeft))
weightLeftEntry = widget.NewEntry()
weightLeftEntry.Validator = func(s string) error {
if _, err := strconv.ParseFloat(s, 64); err != nil {
return fmt.Errorf("muss eine Zahl sein")
}
return nil
}
weightRightEntry := widget.NewEntry()
weightRightEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightRight))
weightRightEntry.Validator = weightLeftEntry.Validator // Gleicher Validator
weightRightEntry = widget.NewEntry()
weightRightEntry.Validator = weightLeftEntry.Validator
form := &widget.Form{
Items: []*widget.FormItem{
@ -58,10 +61,9 @@ func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.W
GoalSets: goal,
WeightLeft: weightL,
WeightRight: weightR,
InitialProgram: currentSettings.InitialProgram, // Beibehalten
InitialProgram: settingsService.LoadSettings().InitialProgram,
}
settingsService.SaveSettings(newSettings)
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Gespeichert",
Content: "Die Einstellungen wurden erfolgreich aktualisiert.",
@ -70,13 +72,11 @@ func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.W
}
title := widget.NewLabelWithStyle("Einstellungen", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
title.TextStyle.Monospace = true // Workaround to force refresh on size change
layout := container.NewVBox(title, widget.NewSeparator(), form)
paddedLayout := container.NewPadded(layout)
if paddedLayout.Visible() {
loadData()
} // Daten laden, wenn sichtbar
layout := container.NewVBox(
title,
widget.NewSeparator(),
form,
)
return container.NewPadded(layout)
return paddedLayout
}

View file

@ -15,24 +15,23 @@ import (
"fyne.io/fyne/v2/widget"
)
func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsService, parent fyne.Window) fyne.CanvasObject {
// UI-Elemente mit canvas.Text für die Größensteuerung
timerLabel := canvas.NewText("20:00", theme.ColorSlate200)
// 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.TextSize = 60
timerLabel.TextStyle.Bold = true
timerLabel.Alignment = fyne.TextAlignCenter
setsLabel := canvas.NewText("0 / 8", theme.ColorSky400)
setsLabel := canvas.NewText("0 / 0", theme.ColorSky400)
setsLabel.TextSize = 48
setsLabel.TextStyle.Bold = true
setsLabel.Alignment = fyne.TextAlignCenter
repsLabel := canvas.NewText("5 Wiederholungen", theme.ColorSlate200)
repsLabel := canvas.NewText("0 Wiederholungen", theme.ColorSlate200)
repsLabel.TextSize = 20
repsLabel.Alignment = fyne.TextAlignCenter
var mainTimer *time.Ticker
var startButton *widget.Button // Start-Button deklarieren
updateUI := func() {
state := ts.State
@ -43,12 +42,6 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi
timerLabel.Refresh()
setsLabel.Refresh()
repsLabel.Refresh()
if state.IsTrainingRunning {
startButton.Disable()
} else {
startButton.Enable()
}
}
finishAction := func() {
@ -56,6 +49,9 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi
mainTimer.Stop()
mainTimer = nil
}
if !ts.State.IsTrainingRunning {
return // Nichts tun, wenn kein Training läuft
}
session := &data.TrainingSession{
Date: time.Now(),
Sets: int64(ts.State.SetsDone),
@ -70,6 +66,9 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi
}
startAction := func() {
if ts.State.IsTrainingRunning {
return // Verhindere Neustart
}
settings := ss.LoadSettings()
ts.StartTraining(settings.TrainingTimeMinutes, settings.GoalSets)
updateUI()
@ -77,7 +76,8 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi
mainTimer = time.NewTicker(time.Second)
go func() {
for mainTimer != nil {
<-mainTimer.C
select {
case <-mainTimer.C:
if ts.State.RemainingSeconds <= 0 {
finishAction()
return
@ -85,40 +85,34 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi
ts.Tick()
updateUI()
}
}
}()
}
setAction := func() {
if !ts.State.IsTrainingRunning {
// Starte das Training, wenn es noch nicht läuft
startAction()
return // Kein Satz ohne laufendes Training
}
ts.CompleteSet()
updateUI()
}
startButton = widget.NewButton("Training beginnen", startAction)
// Layout im "Cockpit"-Stil
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,
)
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
bottomPart := container.NewVBox(
startButton, // Start-Button hinzugefügt
widget.NewButton("Satz abschließen", setAction),
finishButton,
)
return container.NewBorder(topPart, bottomPart, nil, nil, container.NewCenter(middlePart))
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
}