WIP components (as built)
This commit is contained in:
102 files changed
+6538
-1
No files matched your search
+131
@@ -0,0 +1,131 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"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) 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 := Tokens(c)
|
||||
d := Density(c)
|
||||
label := opts.Label
|
||||
if label == "" {
|
||||
label = 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", Secondary
|
||||
if running {
|
||||
icon, key, variant = "pause", "time_tracker.stop", Primary
|
||||
}
|
||||
if Button(c, msg(c, key), 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() {
|
||||
DurationPicker(c, draft, DurationPickerOptions{Seconds: true, Label: msg(c, "time_tracker.correct")})
|
||||
if Button(c, msg(c, "time_tracker.apply"), ButtonOptions{}).Clicked() {
|
||||
act("correct", func() { s.Adjust += *draft - total })
|
||||
*editing = false
|
||||
}
|
||||
if Button(c, msg(c, "calendar.cancel"), ButtonOptions{Variant: Ghost}).Clicked() {
|
||||
*editing = false
|
||||
}
|
||||
return
|
||||
}
|
||||
*editing = false
|
||||
fg := k.Text
|
||||
if running {
|
||||
fg = k.AccentText
|
||||
}
|
||||
ui.Text(c, StopwatchText(total)).Font(theme.MonoFont).FontSize(FontSize(c, theme.H3Size)).TextColor(fg).FontFeatures("tnum").
|
||||
Label(label + ", " + DurationText(c, total.Truncate(time.Second)))
|
||||
if running {
|
||||
ui.Text(c, msg(c, "time_tracker.running")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText)
|
||||
}
|
||||
ui.Box(c).Disabled(running).Children(func() {
|
||||
if b := iconAction(c, icons.Must("pencil"), 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 ChoiceResult{Element: row, changed: changed}
|
||||
}
|
||||
Reference in new issue
Block a user