diff --git a/cmd/main.go b/cmd/main.go index f0629fc..f32cc2a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,36 +34,36 @@ func main() { trainingService := services.NewTrainingService(dbService, settingsService, apiService) // 3. UI-Bildschirme erstellen - // Wir erstellen einen Container, der die Bildschirme verwaltet (ähnlich einem Stack) 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, contentContainer, mainWindow) + homeScreen := ui.MakeHomeScreen(trainingService, func() { navigateTo("training") }) trainingScreen := ui.MakeTrainingScreen(trainingService, settingsService, mainWindow) historyScreen := ui.MakeHistoryScreen(dbService, mainWindow) settingsScreen := ui.MakeSettingsScreen(settingsService, mainWindow) - contentContainer.Add(homeScreen) - contentContainer.Add(trainingScreen) - contentContainer.Add(historyScreen) - contentContainer.Add(settingsScreen) - - // Initial nur den Home-Bildschirm anzeigen - homeScreen.Show() - trainingScreen.Hide() - historyScreen.Hide() - settingsScreen.Hide() - - // 4. Benutzerdefinierte Navigationsleiste erstellen - navBar := ui.MakeNavBar(map[string]fyne.CanvasObject{ + screens := map[string]fyne.CanvasObject{ "home": homeScreen, "training": trainingScreen, "history": historyScreen, "settings": settingsScreen, - }, contentContainer) + } + + for _, s := range screens { + contentContainer.Add(s) + } + + // 4. Benutzerdefinierte Navigationsleiste erstellen + navBar, navigateFunc := ui.MakeNavBar(screens, contentContainer) + navigateTo = navigateFunc // Weisen der Navigationsfunktion die Implementierung aus der Navbar zu + + // Initial den Home-Bildschirm anzeigen + navigateTo("home") // 5. Hauptlayout mit Border-Layout erstellen - // Der Inhalt ist im Zentrum, die Navigationsleiste am unteren Rand. mainLayout := container.NewBorder(nil, navBar, nil, nil, contentContainer) mainWindow.SetContent(mainLayout) diff --git a/internal/ui/history.go b/internal/ui/history.go index 6554d22..98c40f5 100644 --- a/internal/ui/history.go +++ b/internal/ui/history.go @@ -5,6 +5,7 @@ import ( "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" @@ -13,55 +14,42 @@ import ( "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.NewLabel(""), widget.NewSeparator(), - widget.NewLabel(""), // Sätze - widget.NewLabel(""), // Gewicht - widget.NewLabel(""), // Reps - widget.NewLabel(""), // Dauer + 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})), + ), )) }, 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))) + content := card.Content.(*fyne.Container) + programLabel := content.Objects[0].(*widget.Label) + 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)) }, ) - // Funktion zum Neuladen der Daten refreshData := func() { var err error history, err = db.GetHistory() @@ -69,24 +57,31 @@ func MakeHistoryScreen(db *data.DatabaseService, parent fyne.Window) fyne.Canvas log.Printf("Fehler beim Laden der Historie: %v", err) dialog.ShowError(err, parent) } - if len(history) == 0 { - placeholder.Show() list.Hide() + placeholder.Show() } else { - placeholder.Hide() list.Show() + placeholder.Hide() } list.Refresh() } - // Initiales Laden - refreshData() + list.OnSelected = func(id widget.ListItemID) { + list.Unselect(id) + } - // Toolbar mit Refresh-Button toolbar := widget.NewToolbar( widget.NewToolbarAction(theme.ViewRefreshIcon(), refreshData), ) - return container.NewBorder(toolbar, nil, nil, nil, container.NewStack(list, placeholder)) + layout := container.NewBorder(toolbar, nil, nil, nil, container.NewStack(list, container.NewCenter(placeholder))) + + wrapper := container.NewMax(layout) + if wrapper.Visible() { + refreshData() + } + // wrapper.OnVisible = refreshData + + return wrapper } diff --git a/internal/ui/home.go b/internal/ui/home.go index a9bddc2..11e4fd3 100644 --- a/internal/ui/home.go +++ b/internal/ui/home.go @@ -4,54 +4,51 @@ import ( "fmt" "git.patanix.de/git/kettlebell-app/internal/services" + "git.patanix.de/git/kettlebell-app/internal/ui/theme" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" ) -func MakeHomeScreen(ts *services.TrainingService, content *fyne.Container, parent fyne.Window) fyne.CanvasObject { +func MakeHomeScreen(ts *services.TrainingService, onStart func()) fyne.CanvasObject { // Header + headerTitle := canvas.NewText("Patanix", theme.ColorSlate200) + headerTitle.TextSize = 28 + headerTitle.TextStyle.Bold = true + header := container.NewVBox( widget.NewLabel("Hallo,"), - widget.NewLabelWithStyle("Patanix", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), //, Size: 24}), + headerTitle, ) // 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), container.NewVBox( widget.NewLabel(fmt.Sprintf("Ziel: %d Wiederholungen pro Satz", state.CurrentReps)), - widget.NewButton("Training starten", func() { - // Wechsle zum Trainingsbildschirm - for _, child := range content.Objects { - if _, ok := child.(*fyne.Container).Objects[0].(*widget.Card); ok { - // Heuristik, um den Trainings-Screen zu finden - child.Show() - } else { - child.Hide() - } - } - content.Refresh() - }), + widget.NewButton("Training starten", onStart), ), ) // Letzte Leistung - // HINWEIS: Diese Daten müssten aus dem dbService geladen werden. - // Hier verwenden wir vorerst Platzhalter. 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})), )) - return container.NewVBox( + layout := container.NewVBox( header, widget.NewSeparator(), nextTrainingCard, statsCard, ) + + return container.NewPadded(layout) } diff --git a/internal/ui/navbar.go b/internal/ui/navbar.go index a32cc42..d310001 100644 --- a/internal/ui/navbar.go +++ b/internal/ui/navbar.go @@ -8,38 +8,38 @@ import ( "fyne.io/fyne/v2/theme" ) -// MakeNavBar erstellt die benutzerdefinierte Navigationsleiste am unteren Rand. -func MakeNavBar(screens map[string]fyne.CanvasObject, content *fyne.Container) fyne.CanvasObject { +// 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() if key == name { screen.Show() - } else { - screen.Hide() } } for key, button := range buttons { + button.SetActive(false) if key == name { button.SetActive(true) - } else { - button.SetActive(false) } } content.Refresh() } - buttons["home"] = components.NewNavButton("Home", theme.HomeIcon(), true, func() { navigateTo("home") }) + buttons["home"] = components.NewNavButton("Home", theme.HomeIcon(), false, func() { navigateTo("home") }) buttons["training"] = components.NewNavButton("Training", theme.MediaPlayIcon(), false, func() { navigateTo("training") }) buttons["history"] = components.NewNavButton("Historie", theme.ListIcon(), false, func() { navigateTo("history") }) buttons["settings"] = components.NewNavButton("Einstellungen", theme.SettingsIcon(), false, func() { navigateTo("settings") }) - return container.NewGridWithColumns(4, + navContainer := container.NewGridWithColumns(4, buttons["home"], buttons["training"], buttons["history"], buttons["settings"], ) + + return navContainer, navigateTo } diff --git a/internal/ui/settings.go b/internal/ui/settings.go index ed20d57..81a60b2 100644 --- a/internal/ui/settings.go +++ b/internal/ui/settings.go @@ -8,48 +8,57 @@ import ( "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)) + timeEntry := widget.NewEntry() + timeEntry.SetText(fmt.Sprintf("%d", currentSettings.TrainingTimeMinutes)) + timeEntry.Validator = func(s string) error { + if _, err := strconv.Atoi(s); err != nil { + return fmt.Errorf("muss eine Zahl sein") + } + return nil + } + + setsEntry := widget.NewEntry() + setsEntry.SetText(fmt.Sprintf("%d", currentSettings.GoalSets)) + setsEntry.Validator = timeEntry.Validator // Gleicher Validator weightLeftEntry := widget.NewEntry() weightLeftEntry.SetText(fmt.Sprintf("%.1f", currentSettings.WeightLeft)) + 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)) - - goalSetsEntry := widget.NewEntry() - goalSetsEntry.SetText(fmt.Sprintf("%d", currentSettings.GoalSets)) + weightRightEntry.Validator = weightLeftEntry.Validator // Gleicher Validator 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}, + {Text: "Trainingszeit (Minuten)", Widget: timeEntry}, + {Text: "Ziel-Sätze", Widget: setsEntry}, + {Text: "Links (kg)", Widget: weightLeftEntry}, + {Text: "Rechts (kg)", Widget: weightRightEntry}, }, 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 - } + timeMin, _ := strconv.Atoi(timeEntry.Text) + goal, _ := strconv.Atoi(setsEntry.Text) + weightL, _ := strconv.ParseFloat(weightLeftEntry.Text, 64) + weightR, _ := strconv.ParseFloat(weightRightEntry.Text, 64) newSettings := &services.Settings{ TrainingTimeMinutes: timeMin, + GoalSets: goal, WeightLeft: weightL, WeightRight: weightR, - GoalSets: goal, + InitialProgram: currentSettings.InitialProgram, // Beibehalten } settingsService.SaveSettings(newSettings) @@ -60,5 +69,14 @@ func MakeSettingsScreen(settingsService *services.SettingsService, parent fyne.W }, } - return container.NewPadded(form) + 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, + ) + + return container.NewPadded(layout) } diff --git a/internal/ui/theme/theme.go b/internal/ui/theme/theme.go index fa16797..e101b20 100644 --- a/internal/ui/theme/theme.go +++ b/internal/ui/theme/theme.go @@ -11,6 +11,7 @@ import ( 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 @@ -28,13 +29,13 @@ func (t *KettlebellTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVari case theme.ColorNameButton: return ColorSlate800 case theme.ColorNameDisabledButton: - return ColorSlate800 + return ColorSlate700 case theme.ColorNamePrimary: return ColorSky500 case theme.ColorNamePlaceHolder: return ColorSlate400 case theme.ColorNameHover: - return color.NRGBA{R: 0x33, G: 0x41, B: 0x55, A: 0xff} // Slate 700 + return ColorSlate700 case theme.ColorNameForeground: return ColorSlate200 case theme.ColorNameDisabled: @@ -44,20 +45,17 @@ func (t *KettlebellTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVari case theme.ColorNameInputBackground: return ColorSlate800 case theme.ColorNameSeparator: - return color.NRGBA{R: 0x33, G: 0x41, B: 0x55, A: 0xff} // Slate 700 + return ColorSlate700 default: return theme.DefaultTheme().Color(name, variant) } } func (t *KettlebellTheme) Icon(name fyne.ThemeIconName) fyne.Resource { - // Hier könnten wir benutzerdefinierte Icons laden, wenn nötig. - // Vorerst verwenden wir die Standard-Icons. return theme.DefaultTheme().Icon(name) } func (t *KettlebellTheme) Font(style fyne.TextStyle) fyne.Resource { - // Hier könnten wir eine benutzerdefinierte Schriftart wie 'Inter' laden. return theme.DefaultTheme().Font(style) } @@ -68,7 +66,9 @@ func (t *KettlebellTheme) Size(name fyne.ThemeSizeName) float32 { case theme.SizeNameText: return 16 case theme.SizeNameHeadingText: - return 28 + return 24 + case theme.SizeNameSubHeadingText: + return 20 default: return theme.DefaultTheme().Size(name) } diff --git a/internal/ui/training.go b/internal/ui/training.go index 1e40724..ce9b921 100644 --- a/internal/ui/training.go +++ b/internal/ui/training.go @@ -6,38 +6,56 @@ 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" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" ) func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsService, parent fyne.Window) fyne.CanvasObject { - // UI-Elemente - timerLabel := widget.NewLabelWithStyle("20:00", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) - // timerLabel.TextSize = 60 + // UI-Elemente mit canvas.Text für die Größensteuerung + timerLabel := canvas.NewText("20:00", theme.ColorSlate200) + timerLabel.TextSize = 60 + timerLabel.TextStyle.Bold = true + timerLabel.Alignment = fyne.TextAlignCenter - setsLabel := widget.NewLabelWithStyle("0 / 8", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) - // setsLabel.TextSize = 48 - // setsLabel.Color = theme.ColorSky400 + setsLabel := canvas.NewText("0 / 8", theme.ColorSky400) + setsLabel.TextSize = 48 + setsLabel.TextStyle.Bold = true + setsLabel.Alignment = fyne.TextAlignCenter - repsLabel := widget.NewLabelWithStyle("5 Wiederholungen", fyne.TextAlignCenter, fyne.TextStyle{}) - // repsLabel.TextSize = 20 + repsLabel := canvas.NewText("5 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 - timerLabel.SetText(formatDuration(int64(state.RemainingSeconds))) - setsLabel.SetText(fmt.Sprintf("%d / %d", state.SetsDone, state.GoalSets)) - repsLabel.SetText(fmt.Sprintf("%d Wiederholungen", state.RepsPerSet)) + timerLabel.Text = utils.FormatDuration(int64(state.RemainingSeconds)) + setsLabel.Text = fmt.Sprintf("%d / %d", state.SetsDone, state.GoalSets) + repsLabel.Text = fmt.Sprintf("%d Wiederholungen", state.RepsPerSet) + + timerLabel.Refresh() + setsLabel.Refresh() + repsLabel.Refresh() + + if state.IsTrainingRunning { + startButton.Disable() + } else { + startButton.Enable() + } } finishAction := func() { if mainTimer != nil { mainTimer.Stop() + mainTimer = nil } - // ... (Logik zum Speichern wie zuvor) ... session := &data.TrainingSession{ Date: time.Now(), Sets: int64(ts.State.SetsDone), @@ -58,7 +76,8 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi mainTimer = time.NewTicker(time.Second) go func() { - for range mainTimer.C { + for mainTimer != nil { + <-mainTimer.C if ts.State.RemainingSeconds <= 0 { finishAction() return @@ -70,10 +89,16 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi } setAction := func() { + if !ts.State.IsTrainingRunning { + // Starte das Training, wenn es noch nicht läuft + startAction() + } ts.CompleteSet() updateUI() } + startButton = widget.NewButton("Training beginnen", startAction) + // Layout im "Cockpit"-Stil topPart := container.NewVBox( widget.NewLabelWithStyle("Verbleibende Zeit", fyne.TextAlignCenter, fyne.TextStyle{}), @@ -87,15 +112,13 @@ func MakeTrainingScreen(ts *services.TrainingService, ss *services.SettingsServi ) finishButton := widget.NewButton("Training beenden", finishAction) - // Wir simulieren den roten Button durch die Error-Farbe des Themes finishButton.Importance = widget.HighImportance bottomPart := container.NewVBox( + startButton, // Start-Button hinzugefügt widget.NewButton("Satz abschließen", setAction), finishButton, ) - // HINWEIS: Start-Button fehlt hier, da der Flow vom Home-Screen ausgeht. - // Man könnte ihn bei Bedarf hinzufügen. return container.NewBorder(topPart, bottomPart, nil, nil, container.NewCenter(middlePart)) } diff --git a/internal/ui/utils/format.go b/internal/ui/utils/format.go new file mode 100644 index 0000000..63f54a9 --- /dev/null +++ b/internal/ui/utils/format.go @@ -0,0 +1,13 @@ +package utils + +import "fmt" + +// FormatDuration wandelt Sekunden in einen MM:SS String um. +func FormatDuration(totalSeconds int64) string { + if totalSeconds < 0 { + totalSeconds = 0 + } + mins := totalSeconds / 60 + secs := totalSeconds % 60 + return fmt.Sprintf("%02d:%02d", mins, secs) +} diff --git a/internal/utils/utils.go b/internal/ui/utils/utils.go similarity index 100% rename from internal/utils/utils.go rename to internal/ui/utils/utils.go