322 lines
10 KiB
Go
322 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"gioui.org/app"
|
|
"gioui.org/font/gofont"
|
|
"gioui.org/io/event"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"gioui.org/widget/material"
|
|
)
|
|
|
|
type Page int
|
|
|
|
const (
|
|
PageLanding Page = iota
|
|
PageWorkoutReview
|
|
PagePerformance
|
|
PageMeasurements
|
|
)
|
|
|
|
type AppState struct {
|
|
Theme *material.Theme
|
|
CurrentPage Page
|
|
|
|
BtnLanding widget.Clickable
|
|
BtnWorkoutReview widget.Clickable
|
|
BtnPerformance widget.Clickable
|
|
BtnMeasurements widget.Clickable
|
|
}
|
|
|
|
func main() {
|
|
go func() {
|
|
w := new(app.Window)
|
|
w.Option(app.Title("Logbook Review Mockup"))
|
|
w.Option(app.Size(unit.Dp(400), unit.Dp(700)))
|
|
if err := loop(w); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
os.Exit(0)
|
|
}()
|
|
app.Main()
|
|
}
|
|
|
|
func loop(w *app.Window) error {
|
|
th := material.NewTheme()
|
|
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
|
var ops op.Ops
|
|
state := &AppState{
|
|
Theme: th,
|
|
}
|
|
events := make(chan event.Event)
|
|
acks := make(chan struct{})
|
|
|
|
go func() {
|
|
for {
|
|
ev := w.Event()
|
|
events <- ev
|
|
<-acks
|
|
if _, ok := ev.(app.DestroyEvent); ok {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
for {
|
|
select {
|
|
case e := <-events:
|
|
switch e:= e.(type) {
|
|
case app.DestroyEvent:
|
|
acks <- struct{}{}
|
|
return e.Err
|
|
case app.FrameEvent:
|
|
gtx := app.NewContext(&ops, e)
|
|
state.Layout(gtx)
|
|
e.Frame(gtx.Ops)
|
|
}
|
|
acks <- struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (state *AppState) Layout(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{
|
|
Axis: layout.Vertical,
|
|
}.Layout(gtx,
|
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
|
switch state.CurrentPage {
|
|
case PageLanding:
|
|
return LandingPage(state, gtx)
|
|
case PageWorkoutReview:
|
|
return WorkoutReviewPage(state, gtx)
|
|
case PagePerformance:
|
|
return PerformancePage(state, gtx)
|
|
case PageMeasurements:
|
|
return MeasurementsPage(state, gtx)
|
|
default:
|
|
return layout.Dimensions{}
|
|
}
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return state.BottomNav(gtx)
|
|
}),
|
|
)
|
|
}
|
|
|
|
func (state *AppState) BottomNav(gtx layout.Context) layout.Dimensions {
|
|
th := state.Theme
|
|
|
|
labels := []string{"Home", "Workouts", "Performance", "Measurements"}
|
|
clicks := []*widget.Clickable{
|
|
&state.BtnLanding, &state.BtnWorkoutReview, &state.BtnPerformance, &state.BtnMeasurements,
|
|
}
|
|
|
|
// Measure button widths using a dry run
|
|
btnWidths := make([]int, len(labels))
|
|
btnHeights := make([]int, len(labels))
|
|
for i, label := range labels {
|
|
btn := material.Button(th, clicks[i], label)
|
|
// Use a scratch context with unconstrained width
|
|
mctx := gtx
|
|
mctx.Constraints = layout.Constraints{
|
|
Min: image.Pt(0, 0),
|
|
Max: image.Pt(gtx.Dp(1000), gtx.Constraints.Max.Y),
|
|
}
|
|
dims := btn.Layout(mctx)
|
|
btnWidths[i] = dims.Size.X
|
|
btnHeights[i] = dims.Size.Y
|
|
}
|
|
|
|
gap := gtx.Dp(8)
|
|
totalWidth := btnWidths[0] + btnWidths[1] + btnWidths[2] + btnWidths[3] + gap*3
|
|
|
|
if totalWidth <= gtx.Constraints.Max.X {
|
|
// All buttons fit in one row
|
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnLanding.Clicked(gtx) {
|
|
state.CurrentPage = PageLanding
|
|
}
|
|
return material.Button(th, &state.BtnLanding, "Home").Layout(gtx)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnWorkoutReview.Clicked(gtx) {
|
|
state.CurrentPage = PageWorkoutReview
|
|
}
|
|
return material.Button(th, &state.BtnWorkoutReview, "Workouts").Layout(gtx)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnPerformance.Clicked(gtx) {
|
|
state.CurrentPage = PagePerformance
|
|
}
|
|
return material.Button(th, &state.BtnPerformance, "Performance").Layout(gtx)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnMeasurements.Clicked(gtx) {
|
|
state.CurrentPage = PageMeasurements
|
|
}
|
|
return material.Button(th, &state.BtnMeasurements, "Measurements").Layout(gtx)
|
|
}),
|
|
)
|
|
} else {
|
|
// Two rows, two buttons per row
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnLanding.Clicked(gtx) {
|
|
state.CurrentPage = PageLanding
|
|
}
|
|
return material.Button(th, &state.BtnLanding, "Home").Layout(gtx)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnWorkoutReview.Clicked(gtx) {
|
|
state.CurrentPage = PageWorkoutReview
|
|
}
|
|
return material.Button(th, &state.BtnWorkoutReview, "Workouts").Layout(gtx)
|
|
}),
|
|
)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnPerformance.Clicked(gtx) {
|
|
state.CurrentPage = PagePerformance
|
|
}
|
|
return material.Button(th, &state.BtnPerformance, "Performance").Layout(gtx)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if state.BtnMeasurements.Clicked(gtx) {
|
|
state.CurrentPage = PageMeasurements
|
|
}
|
|
return material.Button(th, &state.BtnMeasurements, "Measurements").Layout(gtx)
|
|
}),
|
|
)
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
func LandingPage(state *AppState, gtx layout.Context) layout.Dimensions {
|
|
th := state.Theme
|
|
return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(material.H4(th, "Next Planned Workout").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
layout.Rigid(material.Body1(th, "Date: "+time.Now().Add(24*time.Hour).Format("2006-01-02")).Layout),
|
|
layout.Rigid(material.Body1(th, "Type: Upper Body").Layout),
|
|
layout.Rigid(material.Body1(th, "Exercises: Bench Press, Row, Curl").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
|
|
layout.Rigid(material.Button(th, new(widget.Clickable), "Modify Plan").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
layout.Rigid(material.Button(th, new(widget.Clickable), "Start Session").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(32)}.Layout),
|
|
layout.Rigid(material.H6(th, "Quick Stats").Layout),
|
|
layout.Rigid(material.Body2(th, "Bodyweight: 175 lb").Layout),
|
|
layout.Rigid(material.Body2(th, "Current 1RM (Bench): 225 lb").Layout),
|
|
)
|
|
})
|
|
}
|
|
|
|
func WorkoutReviewPage(state *AppState, gtx layout.Context) layout.Dimensions {
|
|
th := state.Theme
|
|
workouts := []struct {
|
|
Date string
|
|
Type string
|
|
Exercises string
|
|
Note string
|
|
}{
|
|
{"2025-05-13", "Upper Body", "Bench Press, Row, Curl", "Felt strong, PR on bench."},
|
|
{"2025-05-10", "Lower Body", "Squat, Deadlift", "Tired, but completed all sets."},
|
|
}
|
|
var list layout.List
|
|
return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(material.H4(th, "Past Workouts").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
|
return list.Layout(gtx, len(workouts), func(gtx layout.Context, i int) layout.Dimensions {
|
|
w := workouts[i]
|
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
// Use a colored rectangle instead of material.Card
|
|
dr := image.Rect(0, 0, gtx.Constraints.Max.X, 70)
|
|
paint.FillShape(gtx.Ops, color.NRGBA{R: 0xee, G: 0xee, B: 0xff, A: 0xff}, clip.Rect(dr).Op())
|
|
return layout.Inset{Left: unit.Dp(8), Top: unit.Dp(8)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(material.Body1(th, w.Date+" - "+w.Type).Layout),
|
|
layout.Rigid(material.Body2(th, w.Exercises).Layout),
|
|
layout.Rigid(material.Caption(th, w.Note).Layout),
|
|
)
|
|
})
|
|
})
|
|
})
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
|
|
func PerformancePage(state *AppState, gtx layout.Context) layout.Dimensions {
|
|
th := state.Theme
|
|
return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(material.H4(th, "Bench Press 1RM Trend").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
// Just a colored rectangle as a placeholder for a graph
|
|
cs := gtx.Constraints
|
|
sz := cs.Constrain(image.Pt(300, 100))
|
|
dr := image.Rectangle{Max: sz}
|
|
paintColor := color.NRGBA{R: 0x80, G: 0x80, B: 0xff, A: 0xff}
|
|
paint.FillShape(gtx.Ops, paintColor, clip.Rect(dr).Op())
|
|
return layout.Dimensions{Size: sz}
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
layout.Rigid(material.Body2(th, "Actual (blue), Predicted (red)").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
|
|
layout.Rigid(material.Body2(th, "Best: 225 lb (Aug)").Layout),
|
|
)
|
|
})
|
|
}
|
|
|
|
func MeasurementsPage(state *AppState, gtx layout.Context) layout.Dimensions {
|
|
th := state.Theme
|
|
dates := []string{"2025-05-01", "2025-05-08", "2025-05-15"}
|
|
weights := []string{"174.8", "175.2", "175.0"}
|
|
waist := []string{"34.1", "34.0", "33.9"}
|
|
var list layout.List
|
|
return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(material.H4(th, "Measurements").Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return list.Layout(gtx, len(dates), func(gtx layout.Context, i int) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
layout.Rigid(material.Body2(th, dates[i]).Layout),
|
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
layout.Rigid(material.Body2(th, weights[i]+" lb").Layout),
|
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
layout.Rigid(material.Body2(th, waist[i]+" in").Layout),
|
|
)
|
|
})
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
|