135 lines
4.3 KiB
Go
135 lines
4.3 KiB
Go
package project
|
|
|
|
import (
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/datetime"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/input"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// TimeEntry records a span; zero End means running.
|
|
type TimeEntry struct {
|
|
Start, End time.Time
|
|
}
|
|
|
|
// TimeTrackerState holds the caller's tracked spans and manual correction.
|
|
type TimeTrackerState struct {
|
|
Entries []TimeEntry
|
|
// Adjust is added to the tracked spans by manual correction.
|
|
Adjust time.Duration
|
|
}
|
|
|
|
// Running reports whether the last entry is still open.
|
|
func (s *TimeTrackerState) Running() bool {
|
|
return len(s.Entries) > 0 && s.Entries[len(s.Entries)-1].End.IsZero()
|
|
}
|
|
|
|
// Total returns the tracked time at now, including the correction.
|
|
func (s *TimeTrackerState) Total(now time.Time) time.Duration {
|
|
t := s.Adjust
|
|
for _, e := range s.Entries {
|
|
end := e.End
|
|
if end.IsZero() {
|
|
end = now
|
|
}
|
|
t += end.Sub(e.Start)
|
|
}
|
|
return t
|
|
}
|
|
|
|
// TimeTrackerOptions configure a TimeTracker.
|
|
type TimeTrackerOptions struct {
|
|
// Now fixes the clock; zero uses the system clock.
|
|
Now time.Time
|
|
Disabled bool
|
|
Label string
|
|
}
|
|
|
|
// TimeTracker records spans and supports manual total corrections.
|
|
func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) core.ChoiceResult {
|
|
now := opts.Now
|
|
live := now.IsZero()
|
|
if live {
|
|
now = c.Now()
|
|
}
|
|
for i, e := range s.Entries {
|
|
if e.Start.IsZero() || e.Start.After(now) || !e.End.IsZero() && (e.End.Before(e.Start) || e.End.After(now)) || e.End.IsZero() && i != len(s.Entries)-1 || i > 0 && e.Start.Before(s.Entries[i-1].End) {
|
|
panic("mujica: TimeTracker entries must be ordered spans with only the last open")
|
|
}
|
|
}
|
|
if s.Total(now) < 0 {
|
|
panic("mujica: TimeTracker total must be non-negative")
|
|
}
|
|
k := core.Tokens(c)
|
|
d := core.Density(c)
|
|
label := opts.Label
|
|
if label == "" {
|
|
label = core.Msg(c, "time_tracker.label")
|
|
}
|
|
changed := false
|
|
row := ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled)
|
|
editing := ui.Local(row, "time-tracker-edit", func() bool { return false })
|
|
draft := ui.Local(row, "time-tracker-draft", func() time.Duration { return 0 })
|
|
act := func(what string, fn func()) {
|
|
if row.IsDisabled() {
|
|
return
|
|
}
|
|
fn()
|
|
changed = true
|
|
slog.Debug("mujica: time_tracker "+what, "total", s.Total(now))
|
|
}
|
|
row.Children(func() {
|
|
total := s.Total(now)
|
|
running := s.Running()
|
|
icon, key, variant := "play", "time_tracker.start", input.Secondary
|
|
if running {
|
|
icon, key, variant = "pause", "time_tracker.stop", input.Primary
|
|
}
|
|
if input.Button(c, core.Msg(c, key), input.ButtonOptions{Variant: variant, Icon: icons.Must(icon)}).Clicked() {
|
|
act(key[len("time_tracker."):], func() {
|
|
if running {
|
|
s.Entries[len(s.Entries)-1].End = now
|
|
} else {
|
|
s.Entries = append(s.Entries, TimeEntry{Start: now})
|
|
}
|
|
})
|
|
}
|
|
if *editing && !row.IsDisabled() {
|
|
datetime.DurationPicker(c, draft, datetime.DurationPickerOptions{Seconds: true, Label: core.Msg(c, "time_tracker.correct")})
|
|
if input.Button(c, core.Msg(c, "time_tracker.apply"), input.ButtonOptions{}).Clicked() {
|
|
act("correct", func() { s.Adjust += *draft - total })
|
|
*editing = false
|
|
}
|
|
if input.Button(c, core.Msg(c, "calendar.cancel"), input.ButtonOptions{Variant: input.Ghost}).Clicked() {
|
|
*editing = false
|
|
}
|
|
return
|
|
}
|
|
*editing = false
|
|
fg := k.Text
|
|
if running {
|
|
fg = k.AccentText
|
|
}
|
|
ui.Text(c, datetime.StopwatchText(total)).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(fg).FontFeatures("tnum").
|
|
Label(label + ", " + datetime.DurationText(c, total.Truncate(time.Second)))
|
|
if running {
|
|
ui.Text(c, core.Msg(c, "time_tracker.running")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.AccentText)
|
|
}
|
|
ui.Box(c).Disabled(running).Children(func() {
|
|
if b := core.IconAction(c, icons.Must("pencil"), core.Msg(c, "time_tracker.correct")); b.Clicked() && !b.IsDisabled() {
|
|
*editing, *draft = true, total.Truncate(time.Second)
|
|
}
|
|
})
|
|
})
|
|
if s.Running() && live {
|
|
c.After(100*time.Millisecond - s.Total(now)%(100*time.Millisecond))
|
|
}
|
|
return core.NewChoiceResult(row, changed)
|
|
}
|