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

92
internal/ui/history.go Normal file
View file

@ -0,0 +1,92 @@
package ui
import (
"fmt"
"log"
"git.patanix.de/git/kettlebell-app/internal/data"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func formatDuration(totalSeconds int64) string {
mins := totalSeconds / 60
secs := totalSeconds % 60
return fmt.Sprintf("%02d:%02d", mins, secs)
}
// MakeHistoryScreen erstellt den Bildschirm für die Trainingshistorie.
func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.CanvasObject {
var history []data.TrainingSession
// Platzhalter, wenn die Liste leer ist
placeholder := widget.NewLabel("Noch keine Trainingsdaten vorhanden.")
placeholder.Alignment = fyne.TextAlignCenter
list := widget.NewList(
func() int {
return len(history)
},
func() fyne.CanvasObject {
// Template für einen Listeneintrag
return widget.NewCard("", "", container.NewVBox(
widget.NewLabel(""), // Datum
widget.NewSeparator(),
widget.NewLabel(""), // Sätze
widget.NewLabel(""), // Gewicht
widget.NewLabel(""), // Reps
widget.NewLabel(""), // Dauer
))
},
func(i widget.ListItemID, o fyne.CanvasObject) {
// Template mit Daten füllen
session := history[i]
card := o.(*widget.Card)
// Datum als Titel der Karte
card.SetTitle(session.Date.Format("02.01.2006 15:04"))
// Details im Inhalt der Karte
box := card.Content.(*fyne.Container)
labels := box.Objects
labels[0].(*widget.Label).SetText(fmt.Sprintf("Programm: %s - Tag %d", session.Program, session.BlockDay))
labels[2].(*widget.Label).SetText(fmt.Sprintf("Sätze: %d", session.Sets))
labels[3].(*widget.Label).SetText(fmt.Sprintf("Kettlebells: %.1fkg / %.1fkg", session.WeightLeft, session.WeightRight))
labels[4].(*widget.Label).SetText(fmt.Sprintf("Reps pro Satz: %d", session.RepsPerSet))
labels[5].(*widget.Label).SetText(fmt.Sprintf("Dauer: %s", formatDuration(session.Duration)))
},
)
// Funktion zum Neuladen der Daten
refreshData := func() {
var err error
history, err = db.GetHistory()
if err != nil {
log.Printf("Fehler beim Laden der Historie: %v", err)
dialog.ShowError(err, parent)
}
if len(history) == 0 {
placeholder.Show()
list.Hide()
} else {
placeholder.Hide()
list.Show()
}
list.Refresh()
}
// Initiales Laden
refreshData()
// Toolbar mit Refresh-Button
toolbar := widget.NewToolbar(
widget.NewToolbarAction(theme.ViewRefreshIcon(), refreshData),
)
return container.NewBorder(toolbar, nil, nil, nil, container.NewStack(list, placeholder))
}

36
internal/ui/home.go Normal file
View file

@ -0,0 +1,36 @@
package ui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// MakeHomeScreen erstellt den statischen Willkommensbildschirm.
func MakeHomeScreen() fyne.CanvasObject {
primaryColor := theme.PrimaryColor()
title := canvas.NewText("Willkommen beim Giant Programm Tracker!", primaryColor)
title.TextStyle.Bold = true
title.Alignment = fyne.TextAlignCenter
title.TextSize = 24
subtitle := widget.NewLabel("Verwalte deine Kettlebell-Trainings effizient.")
subtitle.Alignment = fyne.TextAlignCenter
icon := widget.NewIcon(theme.MediaPlayIcon())
icon.Resize(fyne.NewSize(150, 150))
// Layout erstellen, das dem Flutter-Layout entspricht
content := container.NewVBox(
title,
widget.NewSeparator(),
subtitle,
container.NewPadded(icon), // Icon mit etwas Abstand
)
// Zentriert den Inhalt
return container.NewCenter(content)
}

64
internal/ui/settings.go Normal file
View file

@ -0,0 +1,64 @@
package ui
import (
"fmt"
"strconv"
"git.patanix.de/git/kettlebell-app/internal/services"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.Window) fyne.CanvasObject {
currentSettings := settingsService.LoadSettings()
trainingTimeEntry := widget.NewEntry()
trainingTimeEntry.SetText(fmt.Sprintf("%d", currentSettings.TrainingTimeMinutes))
weightLeftEntry := widget.NewEntry()
weightLeftEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightLeft))
weightRightEntry := widget.NewEntry()
weightRightEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightRight))
goalSetsEntry := widget.NewEntry()
goalSetsEntry.SetText(fmt.Sprintf("%d", currentSettings.GoalSets))
form := &widget.Form{
Items: []*widget.FormItem{
{Text: "Trainingszeit (Minuten)", Widget: trainingTimeEntry},
{Text: "Linke Kettlebell (kg)", Widget: weightLeftEntry},
{Text: "Rechte Kettlebell (kg)", Widget: weightRightEntry},
{Text: "Ziel-Sätze", Widget: goalSetsEntry},
},
OnSubmit: func() {
timeMin, err1 := strconv.Atoi(trainingTimeEntry.Text)
weightL, err2 := strconv.ParseFloat(weightLeftEntry.Text, 64)
weightR, err3 := strconv.ParseFloat(weightRightEntry.Text, 64)
goal, err4 := strconv.Atoi(goalSetsEntry.Text)
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
dialog.ShowError(fmt.Errorf("Bitte gib gültige Zahlen ein"), parent)
return
}
newSettings := &services.Settings{
TrainingTimeMinutes: timeMin,
WeightLeft: weightL,
WeightRight: weightR,
GoalSets: goal,
}
settingsService.SaveSettings(newSettings)
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Gespeichert",
Content: "Die Einstellungen wurden erfolgreich aktualisiert.",
})
},
}
return container.NewPadded(form)
}

161
internal/ui/training.go Normal file
View file

@ -0,0 +1,161 @@
package ui
import (
"fmt"
"log"
"time"
"git.patanix.de/git/kettlebell-app/internal/data"
"git.patanix.de/git/kettlebell-app/internal/services"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsService, parent fyne.Window) fyne.CanvasObject {
programLabel := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
blockDayLabel := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{})
repsLabel := widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
timerLabel := widget.NewLabelWithStyle("00:00", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
progressBar := widget.NewProgressBar()
progressLabel := widget.NewLabelWithStyle("Fortschritt: 0%", fyne.TextAlignCenter, fyne.TextStyle{})
var startButton, setButton, finishButton *widget.Button
setHistoryList := widget.NewList(
func() int {
return len(ts.State.SetTimes)
},
func() fyne.CanvasObject {
return widget.NewLabel("")
},
func(id widget.ListItemID, obj fyne.CanvasObject) {
t := ts.State.SetTimes[id]
obj.(*widget.Label).SetText(fmt.Sprintf("#%d um %s (%d Reps)", id+1, t.Format("15:04:05"), ts.State.CurrentReps))
},
)
var mainTimer, lastSetTimer *time.Ticker
updateUI := func() {
state := ts.State
programLabel.SetText(state.CurrentProgram)
blockDayLabel.SetText(fmt.Sprintf("Block Tag: %d", state.CurrentBlockDay))
repsLabel.SetText(fmt.Sprintf("Reps pro Satz: %d", state.CurrentReps))
timerLabel.SetText(formatDuration(int64(state.RemainingSeconds)))
progressBar.SetValue(state.Progress)
progressLabel.SetText(fmt.Sprintf("Fortschritt: %.0f%%", state.Progress*100))
if state.IsTrainingRunning {
startButton.Disable()
setButton.Enable()
finishButton.Enable()
} else {
startButton.Enable()
setButton.Disable()
finishButton.Disable()
}
setHistoryList.Refresh()
}
stopTimers := func() {
if mainTimer != nil {
mainTimer.Stop()
mainTimer = nil
}
if lastSetTimer != nil {
lastSetTimer.Stop()
lastSetTimer = nil
}
}
startAction := func() {
settings := ss.LoadSettings()
ts.StartTraining(settings.TrainingTimeMinutes, settings.GoalSets)
updateUI()
mainTimer = time.NewTicker(time.Second)
go func() {
for range mainTimer.C {
if ts.State.RemainingSeconds <= 0 {
stopTimers()
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Zeit abgelaufen!",
Content: "Training wird automatisch gespeichert.",
})
finishButton.OnTapped()
return
}
ts.Tick()
updateUI()
}
}()
lastSetTimer = time.NewTicker(time.Second)
go func() {
for range lastSetTimer.C {
ts.TickLastSetTimer()
}
}()
}
setAction := func() {
ts.CompleteSet()
fyne.CurrentApp().SendNotification(&fyne.Notification{Title: "Satz gespeichert!", Content: ""})
updateUI()
}
finishAction := func() {
stopTimers()
settings := ss.LoadSettings()
state := ts.State
session := &data.TrainingSession{
Date: time.Now(),
Sets: int64(state.SetsDone),
WeightLeft: settings.WeightLeft,
WeightRight: settings.WeightRight,
RepsPerSet: int64(state.RepsPerSet),
Duration: int64(state.InitialDurationSeconds - state.RemainingSeconds),
}
if err := ts.FinishTraining(session); err != nil {
dialog.ShowError(err, parent)
log.Printf("Fehler beim Speichern des Trainings: %v", err)
} else {
fyne.CurrentApp().SendNotification(&fyne.Notification{Title: "Training gespeichert!", Content: "Gut gemacht!"})
}
updateUI()
}
startButton = widget.NewButtonWithIcon("Start", theme.MediaPlayIcon(), startAction)
setButton = widget.NewButtonWithIcon("Satz", theme.ConfirmIcon(), setAction)
finishButton = widget.NewButtonWithIcon("Beenden", theme.MediaStopIcon(), finishAction)
updateUI()
headerCard := widget.NewCard("", "", container.NewVBox(programLabel, blockDayLabel, repsLabel))
timerCard := widget.NewCard("", "", container.NewVBox(
widget.NewLabelWithStyle("Verbleibende Zeit", fyne.TextAlignCenter, fyne.TextStyle{}),
timerLabel,
progressBar,
progressLabel,
))
actionButtons := container.NewGridWithColumns(3, startButton, setButton, finishButton)
historyCard := widget.NewCard("Satz-Historie", "", setHistoryList)
return container.NewVBox(
headerCard,
timerCard,
actionButtons,
historyCard,
)
}